commit 21281d05fe84043846c485aaa0354ff34d17ec0c Author: zhouxin5253@163.com <9a@2gvp!hs3> Date: Sat Jan 16 10:20:33 2021 +0800 commit message diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7493c87 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,4 @@ +FROM java:8 +VOLUME /tmp +ADD webdav-teambition.jar /webdav-teambition.jar +ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/webdav-teambition.jar"] \ No newline at end of file diff --git a/mvnw b/mvnw new file mode 100644 index 0000000..a16b543 --- /dev/null +++ b/mvnw @@ -0,0 +1,310 @@ +#!/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 +# +# https://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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven 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 Mingw, 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)`" +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 + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +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 + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +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 0000000..c8d4337 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,182 @@ +@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 https://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 Maven 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 keystroke 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 set title of command window +title %0 +@REM enable echoing by 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 + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + +FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%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/pom.xml b/pom.xml new file mode 100644 index 0000000..c09e70e --- /dev/null +++ b/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.4.1 + + + com.github.zxbu + webdav-teambition + 0.0.1-SNAPSHOT + webdav-teambition + Demo project for Spring Boot + + + 1.8 + + + + + org.springframework.boot + spring-boot-starter-web + + + + com.squareup.okhttp3 + okhttp + 3.14.9 + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/src/main/java/com/github/zxbu/webdavteambition/WebdavTeambitionApplication.java b/src/main/java/com/github/zxbu/webdavteambition/WebdavTeambitionApplication.java new file mode 100644 index 0000000..666e112 --- /dev/null +++ b/src/main/java/com/github/zxbu/webdavteambition/WebdavTeambitionApplication.java @@ -0,0 +1,48 @@ +package com.github.zxbu.webdavteambition; + +import com.github.zxbu.webdavteambition.store.TeambitionFileSystemStore; +import net.sf.webdav.LocalFileSystemStore; +import net.sf.webdav.WebdavServlet; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.boot.web.servlet.ServletRegistrationBean; +import org.springframework.boot.web.servlet.support.ErrorPageFilter; +import org.springframework.context.annotation.Bean; + +import java.util.LinkedHashMap; +import java.util.Map; + +@SpringBootApplication +public class WebdavTeambitionApplication { + + public static void main(String[] args) { + SpringApplication.run(WebdavTeambitionApplication.class, args); + } + + @Bean + public ServletRegistrationBean myServlet(){ + ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean<>(new WebdavServlet(), "/*"); + Map inits = new LinkedHashMap<>(); + inits.put("ResourceHandlerImplementation", TeambitionFileSystemStore.class.getName()); +// inits.put("ResourceHandlerImplementation", LocalFileSystemStore.class.getName()); + inits.put("rootpath", "./"); + inits.put("storeDebug", "1"); + servletRegistrationBean.setInitParameters(inits); + return servletRegistrationBean; + } + + @Bean + public ErrorPageFilter errorPageFilter() { + return new ErrorPageFilter(); + } + + @Bean + public FilterRegistrationBean disableSpringBootErrorFilter(ErrorPageFilter filter) { + FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(); + filterRegistrationBean.setFilter(filter); + filterRegistrationBean.setEnabled(false); + return filterRegistrationBean; + } + +} diff --git a/src/main/java/com/github/zxbu/webdavteambition/client/TeambitionClient.java b/src/main/java/com/github/zxbu/webdavteambition/client/TeambitionClient.java new file mode 100644 index 0000000..dd8639a --- /dev/null +++ b/src/main/java/com/github/zxbu/webdavteambition/client/TeambitionClient.java @@ -0,0 +1,156 @@ +package com.github.zxbu.webdavteambition.client; + +import com.github.zxbu.webdavteambition.config.TeambitionProperties; +import com.github.zxbu.webdavteambition.util.JsonUtil; +import okhttp3.*; +import okio.BufferedSink; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +public class TeambitionClient { + private static final Logger LOGGER = LoggerFactory.getLogger(TeambitionClient.class); + private OkHttpClient okHttpClient; + private TeambitionProperties teambitionProperties; + + public TeambitionClient(OkHttpClient okHttpClient, TeambitionProperties teambitionProperties) { + this.okHttpClient = okHttpClient; + this.teambitionProperties = teambitionProperties; + } + + public void init() { + if (getOrgId() == null || getRootId() == null || getDriveId() == null || getSpaceId() == null) { + String personalJson = get("https://www.teambition.com/api/organizations/personal", Collections.emptyMap()); + String orgId = (String) JsonUtil.getJsonNodeValue(personalJson, "_id"); + teambitionProperties.setOrgId(orgId); + String memberId = (String) JsonUtil.getJsonNodeValue(personalJson, "_creatorId"); + + String orgJson = get("/pan/api/orgs/" + orgId, Collections.singletonMap("orgId", orgId)); + String driveId = (String) JsonUtil.getJsonNodeValue(orgJson, "data.driveId"); + teambitionProperties.setDriveId(driveId); + + Map params = new LinkedHashMap<>(); + params.put("orgId", orgId); + params.put("memberId", memberId); + String spacesJson = get("/pan/api/spaces", params); + String rootId = (String) JsonUtil.getJsonNodeValue(spacesJson, "[0].rootId"); + String spaceId = (String) JsonUtil.getJsonNodeValue(spacesJson, "[0].spaceId"); + teambitionProperties.setRootId(rootId); + teambitionProperties.setSpaceId(spaceId); + } + } + + + public String getOrgId() { + return teambitionProperties.getOrgId(); + } + + public String getDriveId() { + return teambitionProperties.getDriveId(); + } + + public String getSpaceId() { + return teambitionProperties.getSpaceId(); + } + + public String getRootId() { + return teambitionProperties.getRootId(); + } + + public InputStream download(String url) { + Request request = new Request.Builder().url(url).build(); + Response response = null; + try { + response = okHttpClient.newCall(request).execute(); + return response.body().byteStream(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public void upload(String url, byte[] bytes, final int offset, final int byteCount) { + Request request = new Request.Builder() + .put(RequestBody.create(MediaType.parse(""), bytes, offset, byteCount)) + .url(url).build(); + try (Response response = okHttpClient.newCall(request).execute()){ + LOGGER.info("post {}, code {}", url, response.code()); + if (!response.isSuccessful()) { + LOGGER.error("请求失败,url={}, code={}, body={}", url, response.code(), response.body().string()); + throw new RuntimeException("请求失败:" + url); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public String post(String url, Object body) { + Request request = new Request.Builder() + .post(RequestBody.create(MediaType.parse("application/json; charset=utf-8"), JsonUtil.toJson(body))) + .url(getTotalUrl(url)).build(); + try (Response response = okHttpClient.newCall(request).execute()){ + LOGGER.info("post {}, code {}", url, response.code()); + if (!response.isSuccessful()) { + LOGGER.error("请求失败,url={}, code={}, body={}", url, response.code(), response.body().string()); + throw new RuntimeException("请求失败:" + url); + } + return toString(response.body()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public String put(String url, Object body) { + Request request = new Request.Builder() + .put(RequestBody.create(MediaType.parse("application/json; charset=utf-8"), JsonUtil.toJson(body))) + .url(getTotalUrl(url)).build(); + try (Response response = okHttpClient.newCall(request).execute()){ + LOGGER.info("put {}, code {}", url, response.code()); + if (!response.isSuccessful()) { + LOGGER.error("请求失败,url={}, code={}, body={}", url, response.code(), response.body().string()); + throw new RuntimeException("请求失败:" + url); + } + return toString(response.body()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public String get(String url, Map params) { + try { + HttpUrl.Builder urlBuilder = HttpUrl.parse(getTotalUrl(url)).newBuilder(); + params.forEach(urlBuilder::addQueryParameter); + + Request request = new Request.Builder().get().url(urlBuilder.build()).build(); + try (Response response = okHttpClient.newCall(request).execute()){ + LOGGER.info("get {}, code {}", urlBuilder.build(), response.code()); + if (!response.isSuccessful()) { + throw new RuntimeException("请求失败:" + urlBuilder.build().toString()); + } + return toString(response.body()); + } + + } catch (Exception e) { + throw new RuntimeException(e); + } + + } + + private String toString(ResponseBody responseBody) throws IOException { + if (responseBody == null) { + return null; + } + return responseBody.string(); + } + + private String getTotalUrl(String url) { + if (url.startsWith("http")) { + return url; + } + return teambitionProperties.getUrl() + url; + } +} diff --git a/src/main/java/com/github/zxbu/webdavteambition/config/TeambitionAutoConfig.java b/src/main/java/com/github/zxbu/webdavteambition/config/TeambitionAutoConfig.java new file mode 100644 index 0000000..5911d85 --- /dev/null +++ b/src/main/java/com/github/zxbu/webdavteambition/config/TeambitionAutoConfig.java @@ -0,0 +1,77 @@ +package com.github.zxbu.webdavteambition.config; + +import com.github.zxbu.webdavteambition.client.TeambitionClient; +import com.github.zxbu.webdavteambition.store.TeambitionFileSystemStore; +import okhttp3.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.StringUtils; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Configuration +@EnableConfigurationProperties(TeambitionProperties.class) +public class TeambitionAutoConfig implements ApplicationContextAware { + private static final Logger LOGGER = LoggerFactory.getLogger(TeambitionAutoConfig.class); + + @Autowired + private TeambitionProperties teambitionProperties; + + @Bean + public TeambitionClient teambitionClient(ApplicationContext applicationContext) throws Exception { + + OkHttpClient okHttpClient = new OkHttpClient.Builder().addInterceptor(new Interceptor() { + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request(); + request = request.newBuilder() + .removeHeader("User-Agent") + .addHeader("User-Agent", teambitionProperties.getAgent()) + .build(); + return chain.proceed(request); + } + }).cookieJar(new CookieJar() { + @Override + public void saveFromResponse(HttpUrl url, List cookies) { + if (StringUtils.hasLength(teambitionProperties.getCookies())) { + // do nothing + } + } + + @Override + public List loadForRequest(HttpUrl url) { + String cookies = teambitionProperties.getCookies(); + String[] cookieSplit = cookies.split("; "); + List cookieList = new ArrayList<>(cookieSplit.length); + for (String cookie : cookieSplit) { + Cookie parse = Cookie.parse(url, cookie); + cookieList.add(parse); + } + return cookieList; + } + }).build(); + TeambitionClient teambitionClient = new TeambitionClient(okHttpClient, teambitionProperties); + try (Response response = okHttpClient.newCall(new Request.Builder().get().url(teambitionProperties.getUrl() + "/pan/api").build()).execute()) { + if (response.isSuccessful()) { + LOGGER.info("TeambitionClient 启动成功"); + } + } + teambitionClient.init(); + return teambitionClient; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + TeambitionFileSystemStore.setApplicationContext(applicationContext); + } +} diff --git a/src/main/java/com/github/zxbu/webdavteambition/config/TeambitionProperties.java b/src/main/java/com/github/zxbu/webdavteambition/config/TeambitionProperties.java new file mode 100644 index 0000000..b62a59b --- /dev/null +++ b/src/main/java/com/github/zxbu/webdavteambition/config/TeambitionProperties.java @@ -0,0 +1,70 @@ +package com.github.zxbu.webdavteambition.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "teambition", ignoreUnknownFields = true) +public class TeambitionProperties { + private String url = "https://pan.teambition.com"; + private String cookies; + private String agent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 11_0_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36"; + private String orgId; + private String driveId; + private String spaceId; + private String rootId; + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getCookies() { + return cookies; + } + + public void setCookies(String cookies) { + this.cookies = cookies; + } + + public String getOrgId() { + return orgId; + } + + public void setOrgId(String orgId) { + this.orgId = orgId; + } + + public String getDriveId() { + return driveId; + } + + public void setDriveId(String driveId) { + this.driveId = driveId; + } + + public String getSpaceId() { + return spaceId; + } + + public String getAgent() { + return agent; + } + + public void setAgent(String agent) { + this.agent = agent; + } + + public void setSpaceId(String spaceId) { + this.spaceId = spaceId; + } + + public String getRootId() { + return rootId; + } + + public void setRootId(String rootId) { + this.rootId = rootId; + } +} diff --git a/src/main/java/com/github/zxbu/webdavteambition/model/BaseQuery.java b/src/main/java/com/github/zxbu/webdavteambition/model/BaseQuery.java new file mode 100644 index 0000000..da711d2 --- /dev/null +++ b/src/main/java/com/github/zxbu/webdavteambition/model/BaseQuery.java @@ -0,0 +1,40 @@ +package com.github.zxbu.webdavteambition.model; + +public class BaseQuery extends Page{ + private String orgId; + private String driveId; + private String spaceId; + private String parentId; + + public String getOrgId() { + return orgId; + } + + public void setOrgId(String orgId) { + this.orgId = orgId; + } + + public String getDriveId() { + return driveId; + } + + public void setDriveId(String driveId) { + this.driveId = driveId; + } + + public String getSpaceId() { + return spaceId; + } + + public void setSpaceId(String spaceId) { + this.spaceId = spaceId; + } + + public String getParentId() { + return parentId; + } + + public void setParentId(String parentId) { + this.parentId = parentId; + } +} diff --git a/src/main/java/com/github/zxbu/webdavteambition/model/CreateFileRequest.java b/src/main/java/com/github/zxbu/webdavteambition/model/CreateFileRequest.java new file mode 100644 index 0000000..e5393a7 --- /dev/null +++ b/src/main/java/com/github/zxbu/webdavteambition/model/CreateFileRequest.java @@ -0,0 +1,76 @@ +package com.github.zxbu.webdavteambition.model; + +public class CreateFileRequest { + private String ccpParentId; + private String checkNameMode = "refuse"; + private String driveId; + private String name; + private String orgId; + private String parentId; + private String spaceId; + private String type; + + public String getCheckNameMode() { + return checkNameMode; + } + + public void setCheckNameMode(String checkNameMode) { + this.checkNameMode = checkNameMode; + } + + public String getDriveId() { + return driveId; + } + + public void setDriveId(String driveId) { + this.driveId = driveId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getOrgId() { + return orgId; + } + + public void setOrgId(String orgId) { + this.orgId = orgId; + } + + public String getParentId() { + return parentId; + } + + public void setParentId(String parentId) { + this.parentId = parentId; + } + + public String getSpaceId() { + return spaceId; + } + + public void setSpaceId(String spaceId) { + this.spaceId = spaceId; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getCcpParentId() { + return ccpParentId; + } + + public void setCcpParentId(String ccpParentId) { + this.ccpParentId = ccpParentId; + } +} diff --git a/src/main/java/com/github/zxbu/webdavteambition/model/FileType.java b/src/main/java/com/github/zxbu/webdavteambition/model/FileType.java new file mode 100644 index 0000000..cf9e591 --- /dev/null +++ b/src/main/java/com/github/zxbu/webdavteambition/model/FileType.java @@ -0,0 +1,5 @@ +package com.github.zxbu.webdavteambition.model; + +public enum FileType { + folder, file; +} diff --git a/src/main/java/com/github/zxbu/webdavteambition/model/MoveRequest.java b/src/main/java/com/github/zxbu/webdavteambition/model/MoveRequest.java new file mode 100644 index 0000000..94a03d5 --- /dev/null +++ b/src/main/java/com/github/zxbu/webdavteambition/model/MoveRequest.java @@ -0,0 +1,51 @@ +package com.github.zxbu.webdavteambition.model; + +import java.util.List; + +public class MoveRequest { + private String driveId; + private String parentId; + private String orgId; + private boolean sameLevel = false; + private List ids; + + public String getDriveId() { + return driveId; + } + + public void setDriveId(String driveId) { + this.driveId = driveId; + } + + public String getParentId() { + return parentId; + } + + public void setParentId(String parentId) { + this.parentId = parentId; + } + + public String getOrgId() { + return orgId; + } + + public void setOrgId(String orgId) { + this.orgId = orgId; + } + + public boolean isSameLevel() { + return sameLevel; + } + + public void setSameLevel(boolean sameLevel) { + this.sameLevel = sameLevel; + } + + public List getIds() { + return ids; + } + + public void setIds(List ids) { + this.ids = ids; + } +} diff --git a/src/main/java/com/github/zxbu/webdavteambition/model/MoveRequestId.java b/src/main/java/com/github/zxbu/webdavteambition/model/MoveRequestId.java new file mode 100644 index 0000000..7713c13 --- /dev/null +++ b/src/main/java/com/github/zxbu/webdavteambition/model/MoveRequestId.java @@ -0,0 +1,22 @@ +package com.github.zxbu.webdavteambition.model; + +public class MoveRequestId { + private String ccpFileId; + private String id; + + public String getCcpFileId() { + return ccpFileId; + } + + public void setCcpFileId(String ccpFileId) { + this.ccpFileId = ccpFileId; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } +} diff --git a/src/main/java/com/github/zxbu/webdavteambition/model/NodeQuery.java b/src/main/java/com/github/zxbu/webdavteambition/model/NodeQuery.java new file mode 100644 index 0000000..d376aa9 --- /dev/null +++ b/src/main/java/com/github/zxbu/webdavteambition/model/NodeQuery.java @@ -0,0 +1,4 @@ +package com.github.zxbu.webdavteambition.model; + +public class NodeQuery extends BaseQuery { +} diff --git a/src/main/java/com/github/zxbu/webdavteambition/model/Page.java b/src/main/java/com/github/zxbu/webdavteambition/model/Page.java new file mode 100644 index 0000000..5240b44 --- /dev/null +++ b/src/main/java/com/github/zxbu/webdavteambition/model/Page.java @@ -0,0 +1,40 @@ +package com.github.zxbu.webdavteambition.model; + +public class Page { + private int offset; + private int limit; + private String orderBy; + private String orderDirection; + + public int getOffset() { + return offset; + } + + public void setOffset(int offset) { + this.offset = offset; + } + + public int getLimit() { + return limit; + } + + public void setLimit(int limit) { + this.limit = limit; + } + + public String getOrderBy() { + return orderBy; + } + + public void setOrderBy(String orderBy) { + this.orderBy = orderBy; + } + + public String getOrderDirection() { + return orderDirection; + } + + public void setOrderDirection(String orderDirection) { + this.orderDirection = orderDirection; + } +} diff --git a/src/main/java/com/github/zxbu/webdavteambition/model/PathInfo.java b/src/main/java/com/github/zxbu/webdavteambition/model/PathInfo.java new file mode 100644 index 0000000..9674229 --- /dev/null +++ b/src/main/java/com/github/zxbu/webdavteambition/model/PathInfo.java @@ -0,0 +1,31 @@ +package com.github.zxbu.webdavteambition.model; + +public class PathInfo { + private String path; + private String parentPath; + private String name; + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public String getParentPath() { + return parentPath; + } + + public void setParentPath(String parentPath) { + this.parentPath = parentPath; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/src/main/java/com/github/zxbu/webdavteambition/model/RemoveRequest.java b/src/main/java/com/github/zxbu/webdavteambition/model/RemoveRequest.java new file mode 100644 index 0000000..b1b2e9c --- /dev/null +++ b/src/main/java/com/github/zxbu/webdavteambition/model/RemoveRequest.java @@ -0,0 +1,24 @@ +package com.github.zxbu.webdavteambition.model; + +import java.util.List; + +public class RemoveRequest { + private String orgId; + private List nodeIds; + + public List getNodeIds() { + return nodeIds; + } + + public void setNodeIds(List nodeIds) { + this.nodeIds = nodeIds; + } + + public String getOrgId() { + return orgId; + } + + public void setOrgId(String orgId) { + this.orgId = orgId; + } +} diff --git a/src/main/java/com/github/zxbu/webdavteambition/model/RenameRequest.java b/src/main/java/com/github/zxbu/webdavteambition/model/RenameRequest.java new file mode 100644 index 0000000..af4316d --- /dev/null +++ b/src/main/java/com/github/zxbu/webdavteambition/model/RenameRequest.java @@ -0,0 +1,40 @@ +package com.github.zxbu.webdavteambition.model; + +public class RenameRequest { + private String ccpFileId; + private String driveId; + private String name; + private String orgId; + + public String getCcpFileId() { + return ccpFileId; + } + + public void setCcpFileId(String ccpFileId) { + this.ccpFileId = ccpFileId; + } + + public String getDriveId() { + return driveId; + } + + public void setDriveId(String driveId) { + this.driveId = driveId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getOrgId() { + return orgId; + } + + public void setOrgId(String orgId) { + this.orgId = orgId; + } +} diff --git a/src/main/java/com/github/zxbu/webdavteambition/model/UploadFinalRequest.java b/src/main/java/com/github/zxbu/webdavteambition/model/UploadFinalRequest.java new file mode 100644 index 0000000..9eda77d --- /dev/null +++ b/src/main/java/com/github/zxbu/webdavteambition/model/UploadFinalRequest.java @@ -0,0 +1,49 @@ +package com.github.zxbu.webdavteambition.model; + +public class UploadFinalRequest { + private String ccpFileId; + private String driveId; + private String nodeId; + private String orgId; + private String uploadId; + + public String getDriveId() { + return driveId; + } + + public void setDriveId(String driveId) { + this.driveId = driveId; + } + + public String getNodeId() { + return nodeId; + } + + public void setNodeId(String nodeId) { + this.nodeId = nodeId; + } + + public String getOrgId() { + return orgId; + } + + public void setOrgId(String orgId) { + this.orgId = orgId; + } + + public String getUploadId() { + return uploadId; + } + + public void setUploadId(String uploadId) { + this.uploadId = uploadId; + } + + public String getCcpFileId() { + return ccpFileId; + } + + public void setCcpFileId(String ccpFileId) { + this.ccpFileId = ccpFileId; + } +} diff --git a/src/main/java/com/github/zxbu/webdavteambition/model/UploadPreInfo.java b/src/main/java/com/github/zxbu/webdavteambition/model/UploadPreInfo.java new file mode 100644 index 0000000..6765e4e --- /dev/null +++ b/src/main/java/com/github/zxbu/webdavteambition/model/UploadPreInfo.java @@ -0,0 +1,67 @@ +package com.github.zxbu.webdavteambition.model; + +public class UploadPreInfo { + private String ccpParentId; + private int chunkCount; + private String contentType = ""; + private String driveId; + private String name; + private int size; + private String type; + + public int getChunkCount() { + return chunkCount; + } + + public void setChunkCount(int chunkCount) { + this.chunkCount = chunkCount; + } + + public String getContentType() { + return contentType; + } + + public void setContentType(String contentType) { + this.contentType = contentType; + } + + public String getDriveId() { + return driveId; + } + + public void setDriveId(String driveId) { + this.driveId = driveId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getSize() { + return size; + } + + public void setSize(int size) { + this.size = size; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getCcpParentId() { + return ccpParentId; + } + + public void setCcpParentId(String ccpParentId) { + this.ccpParentId = ccpParentId; + } +} diff --git a/src/main/java/com/github/zxbu/webdavteambition/model/UploadPreRequest.java b/src/main/java/com/github/zxbu/webdavteambition/model/UploadPreRequest.java new file mode 100644 index 0000000..d927c87 --- /dev/null +++ b/src/main/java/com/github/zxbu/webdavteambition/model/UploadPreRequest.java @@ -0,0 +1,51 @@ +package com.github.zxbu.webdavteambition.model; + +import java.util.List; + +public class UploadPreRequest { + private String checkNameMode = "autoRename"; + private String orgId; + private String parentId; + private String spaceId; + private List infos; + + public String getCheckNameMode() { + return checkNameMode; + } + + public void setCheckNameMode(String checkNameMode) { + this.checkNameMode = checkNameMode; + } + + public String getOrgId() { + return orgId; + } + + public void setOrgId(String orgId) { + this.orgId = orgId; + } + + public String getParentId() { + return parentId; + } + + public void setParentId(String parentId) { + this.parentId = parentId; + } + + public String getSpaceId() { + return spaceId; + } + + public void setSpaceId(String spaceId) { + this.spaceId = spaceId; + } + + public List getInfos() { + return infos; + } + + public void setInfos(List infos) { + this.infos = infos; + } +} diff --git a/src/main/java/com/github/zxbu/webdavteambition/model/result/ListResult.java b/src/main/java/com/github/zxbu/webdavteambition/model/result/ListResult.java new file mode 100644 index 0000000..b54e9e9 --- /dev/null +++ b/src/main/java/com/github/zxbu/webdavteambition/model/result/ListResult.java @@ -0,0 +1,15 @@ +package com.github.zxbu.webdavteambition.model.result; + +import java.util.List; + +public class ListResult { + private List data; + + public List getData() { + return data; + } + + public void setData(List data) { + this.data = data; + } +} diff --git a/src/main/java/com/github/zxbu/webdavteambition/model/result/TFile.java b/src/main/java/com/github/zxbu/webdavteambition/model/result/TFile.java new file mode 100644 index 0000000..a8414e5 --- /dev/null +++ b/src/main/java/com/github/zxbu/webdavteambition/model/result/TFile.java @@ -0,0 +1,105 @@ +package com.github.zxbu.webdavteambition.model.result; + +import java.util.Date; + +public class TFile { + private String kind; + private String nodeId; + private String name; + private Date created; + private Date updated; + private String parentId; + private String status; + private String downloadUrl; + private Long size; + private String ccpFileId; + private String ccpParentFileId; + + public String getKind() { + return kind; + } + + public void setKind(String kind) { + this.kind = kind; + } + + public String getNodeId() { + return nodeId; + } + + public void setNodeId(String nodeId) { + this.nodeId = nodeId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public Date getUpdated() { + return updated; + } + + public void setUpdated(Date updated) { + this.updated = updated; + } + + public String getParentId() { + return parentId; + } + + public void setParentId(String parentId) { + this.parentId = parentId; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public Long getSize() { + return size; + } + + public String getDownloadUrl() { + return downloadUrl; + } + + public void setDownloadUrl(String downloadUrl) { + this.downloadUrl = downloadUrl; + } + + public String getCcpFileId() { + return ccpFileId; + } + + public void setCcpFileId(String ccpFileId) { + this.ccpFileId = ccpFileId; + } + + public String getCcpParentFileId() { + return ccpParentFileId; + } + + public void setCcpParentFileId(String ccpParentFileId) { + this.ccpParentFileId = ccpParentFileId; + } + + public void setSize(Long size) { + this.size = size; + } +} diff --git a/src/main/java/com/github/zxbu/webdavteambition/model/result/UploadPreResult.java b/src/main/java/com/github/zxbu/webdavteambition/model/result/UploadPreResult.java new file mode 100644 index 0000000..d5d1979 --- /dev/null +++ b/src/main/java/com/github/zxbu/webdavteambition/model/result/UploadPreResult.java @@ -0,0 +1,60 @@ +package com.github.zxbu.webdavteambition.model.result; + +import java.util.List; + +public class UploadPreResult { + private String ccpFileId; + private String nodeId; + private String name; + private String kind; + private String uploadId; + private List uploadUrl; + + public String getNodeId() { + return nodeId; + } + + public void setNodeId(String nodeId) { + this.nodeId = nodeId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getCcpFileId() { + return ccpFileId; + } + + public void setCcpFileId(String ccpFileId) { + this.ccpFileId = ccpFileId; + } + + public String getKind() { + return kind; + } + + public void setKind(String kind) { + this.kind = kind; + } + + public String getUploadId() { + return uploadId; + } + + public void setUploadId(String uploadId) { + this.uploadId = uploadId; + } + + public List getUploadUrl() { + return uploadUrl; + } + + public void setUploadUrl(List uploadUrl) { + this.uploadUrl = uploadUrl; + } +} diff --git a/src/main/java/com/github/zxbu/webdavteambition/store/TeambitionClientService.java b/src/main/java/com/github/zxbu/webdavteambition/store/TeambitionClientService.java new file mode 100644 index 0000000..312f1ea --- /dev/null +++ b/src/main/java/com/github/zxbu/webdavteambition/store/TeambitionClientService.java @@ -0,0 +1,282 @@ +package com.github.zxbu.webdavteambition.store; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.zxbu.webdavteambition.client.TeambitionClient; +import com.github.zxbu.webdavteambition.model.*; +import com.github.zxbu.webdavteambition.model.result.ListResult; +import com.github.zxbu.webdavteambition.model.result.TFile; +import com.github.zxbu.webdavteambition.model.result.UploadPreResult; +import com.github.zxbu.webdavteambition.util.JsonUtil; +import org.apache.tomcat.util.http.fileupload.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Service +public class TeambitionClientService { + private static final Logger LOGGER = LoggerFactory.getLogger(TeambitionClientService.class); + private static ObjectMapper objectMapper = new ObjectMapper(); + private static String rootPath = "/"; + private static int chunkSize = 10485760; // 10MB + private TFile rootTFile = null; + private Map nodeIdMap = new ConcurrentHashMap<>(); + + private final TeambitionClient client; + + public TeambitionClientService(TeambitionClient teambitionClient) { + this.client = teambitionClient; + } + + public List getTFiles(String nodeId) { + NodeQuery nodeQuery = new NodeQuery(); + nodeQuery.setOrgId(client.getOrgId()); + nodeQuery.setOffset(0); + nodeQuery.setLimit(10000); + nodeQuery.setOrderBy("updateTime"); + nodeQuery.setOrderDirection("desc"); + nodeQuery.setDriveId(client.getDriveId()); + nodeQuery.setSpaceId(client.getSpaceId()); + nodeQuery.setParentId(nodeId); + String json = client.get("/pan/api/nodes", toMap(nodeQuery)); + ListResult tFileListResult = JsonUtil.readValue(json, new TypeReference>() { + }); + return tFileListResult.getData(); + } + + + private Map toMap(Object o) { + try { + String json = objectMapper.writeValueAsString(o); + Map rawMap = objectMapper.readValue(json, new TypeReference>() { + }); + Map stringMap = new LinkedHashMap<>(); + rawMap.forEach((s, o1) -> { + if (o1 != null) { + stringMap.put(s, o1.toString()); + } + }); + return stringMap; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public void uploadPre(String path, int size, InputStream inputStream) { + path = normalizingPath(path); + PathInfo pathInfo = getPathInfo(path); + TFile parent = getTFileByPath(pathInfo.getParentPath()); + if (parent == null) { + return; + } + int chunkCount = (int) Math.ceil(((double) size) / chunkSize); // 进1法 + + UploadPreRequest uploadPreRequest = new UploadPreRequest(); + uploadPreRequest.setOrgId(client.getOrgId()); + uploadPreRequest.setParentId(parent.getNodeId()); + uploadPreRequest.setSpaceId(client.getSpaceId()); + UploadPreInfo uploadPreInfo = new UploadPreInfo(); + uploadPreInfo.setCcpParentId(parent.getCcpFileId()); + uploadPreInfo.setDriveId(client.getDriveId()); + uploadPreInfo.setName(pathInfo.getName()); + uploadPreInfo.setSize(size); + uploadPreInfo.setChunkCount(chunkCount); + uploadPreInfo.setType(FileType.file.name()); + uploadPreRequest.setInfos(Collections.singletonList(uploadPreInfo)); + LOGGER.info("开始上传文件,文件名:{},总大小:{}, 文件块数量:{}", path, size, chunkCount); + + String json = client.post("/pan/api/nodes/file", uploadPreRequest); + List uploadPreResultList = JsonUtil.readValue(json, new TypeReference>() { + }); + UploadPreResult uploadPreResult = uploadPreResultList.get(0); + List uploadUrl = uploadPreResult.getUploadUrl(); + LOGGER.info("文件预处理成功,开始上传。文件名:{},上传URL数量:{}", path, uploadUrl.size()); + + byte[] buffer = new byte[chunkSize]; + for (String oneUploadUrl : uploadUrl) { + try { + int read = IOUtils.read(inputStream, buffer, 0, buffer.length); + if (read == -1) { + return; + } + client.upload(oneUploadUrl, buffer, 0, read); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + UploadFinalRequest uploadFinalRequest = new UploadFinalRequest(); + uploadFinalRequest.setCcpFileId(uploadPreResult.getCcpFileId()); + uploadFinalRequest.setDriveId(client.getDriveId()); + uploadFinalRequest.setNodeId(uploadPreResult.getNodeId()); + uploadFinalRequest.setOrgId(client.getOrgId()); + uploadFinalRequest.setUploadId(uploadPreResult.getUploadId()); + client.post("/pan/api/nodes/complete", uploadFinalRequest); + LOGGER.info("文件上传成功。文件名:{}", path); + if (!uploadPreResult.getName().equals(pathInfo.getName())) { + LOGGER.info("上传文件名{}与原文件名{}不同,对文件进行重命名", uploadPreResult.getName(), pathInfo.getName()); + RenameRequest renameRequest = new RenameRequest(); + renameRequest.setCcpFileId(uploadPreResult.getCcpFileId()); + renameRequest.setDriveId(client.getDriveId()); + renameRequest.setOrgId(client.getOrgId()); + renameRequest.setName(pathInfo.getName()); + client.put("/pan/api/nodes/" + parent.getNodeId(), renameRequest); + } + clearCache(path); + } + + public void rename(String sourcePath, String newName) { + sourcePath = normalizingPath(sourcePath); + TFile tFile = getTFileByPath(sourcePath); + RenameRequest renameRequest = new RenameRequest(); + renameRequest.setCcpFileId(tFile.getCcpFileId()); + renameRequest.setDriveId(client.getDriveId()); + renameRequest.setOrgId(client.getOrgId()); + renameRequest.setName(newName); + client.put("/pan/api/nodes/" + tFile.getParentId(), renameRequest); + clearCache(sourcePath); + } + + public void move(String sourcePath, String targetPath) { + sourcePath = normalizingPath(sourcePath); + targetPath = normalizingPath(targetPath); + + TFile sourceTFile = getTFileByPath(sourcePath); + TFile targetTFile = getTFileByPath(targetPath); + MoveRequest moveRequest = new MoveRequest(); + moveRequest.setOrgId(client.getOrgId()); + moveRequest.setDriveId(client.getDriveId()); + moveRequest.setParentId(targetTFile.getNodeId()); + MoveRequestId moveRequestId = new MoveRequestId(); + moveRequestId.setCcpFileId(sourceTFile.getCcpFileId()); + moveRequestId.setId(sourceTFile.getNodeId()); + moveRequest.setIds(Collections.singletonList(moveRequestId)); + client.post("/pan/api/nodes/move", moveRequest); + clearCache(sourcePath); + clearCache(targetPath); + } + + public void remove(String path) { + path = normalizingPath(path); + TFile tFile = getTFileByPath(path); + if (tFile == null) { + return; + } + RemoveRequest removeRequest = new RemoveRequest(); + removeRequest.setOrgId(client.getOrgId()); + removeRequest.setNodeIds(Collections.singletonList(tFile.getNodeId())); + client.post("/pan/api/nodes/archive", removeRequest); + clearCache(path); + } + + + public void createFolder(String path) { + path = normalizingPath(path); + PathInfo pathInfo = getPathInfo(path); + TFile parent = getTFileByPath(pathInfo.getParentPath()); + if (parent == null) { + return; + } + + CreateFileRequest createFileRequest = new CreateFileRequest(); + createFileRequest.setCcpParentId(parent.getCcpFileId()); + createFileRequest.setDriveId(client.getDriveId()); + createFileRequest.setName(pathInfo.getName()); + createFileRequest.setOrgId(client.getOrgId()); + createFileRequest.setParentId(parent.getNodeId()); + createFileRequest.setSpaceId(client.getSpaceId()); + createFileRequest.setType(FileType.folder.name()); + client.post("/pan/api/nodes/folder", createFileRequest); + clearCache(path); + } + + + public TFile getTFileByPath(String path) { + path = normalizingPath(path); + + return nodeIdMap.computeIfAbsent(path, this::getNodeIdByPath2); + } + + public InputStream download(String path) { + String downloadUrl = getTFileByPath(path).getDownloadUrl(); + return client.download(downloadUrl); + } + + private TFile getNodeIdByPath2(String path) { + if (!StringUtils.hasLength(path)) { + path = rootPath; + } + if (path.equals(rootPath)) { + return getRootTFile(); + } + PathInfo pathInfo = getPathInfo(path); + TFile tFile = getTFileByPath(pathInfo.getParentPath()); + if (tFile == null ) { + return null; + } + return getNodeIdByParentId(tFile.getNodeId(), pathInfo.getName()); + } + + + public PathInfo getPathInfo(String path) { + path = normalizingPath(path); + if (path.equals(rootPath)) { + PathInfo pathInfo = new PathInfo(); + pathInfo.setPath(path); + pathInfo.setName(path); + return pathInfo; + } + int index = path.lastIndexOf("/"); + String parentPath = path.substring(0, index + 1); + String name = path.substring(index+1); + PathInfo pathInfo = new PathInfo(); + pathInfo.setPath(path); + pathInfo.setParentPath(parentPath); + pathInfo.setName(name); + return pathInfo; + } + + private TFile getRootTFile() { + if (rootTFile == null) { + NodeQuery nodeQuery = new NodeQuery(); + nodeQuery.setOrgId(client.getOrgId()); + nodeQuery.setDriveId(client.getDriveId()); + nodeQuery.setSpaceId(client.getSpaceId()); + String json = client.get("/pan/api/nodes/" + client.getRootId(), toMap(nodeQuery)); + rootTFile = JsonUtil.readValue(json, TFile.class); + } + return rootTFile; + } + + private TFile getNodeIdByParentId(String parentId, String name) { + List tFiles = getTFiles(parentId); + for (TFile tFile : tFiles) { + if (tFile.getName().equals(name)) { + return tFile; + } + } + + return null; + } + + private String normalizingPath(String path) { + path = path.replaceAll("//", "/"); + if (path.endsWith("/")) { + path = path.substring(0, path.length() - 1); + } + return path; + } + + private void clearCache(String path) { + nodeIdMap.remove(path); + } +} diff --git a/src/main/java/com/github/zxbu/webdavteambition/store/TeambitionFileSystemStore.java b/src/main/java/com/github/zxbu/webdavteambition/store/TeambitionFileSystemStore.java new file mode 100644 index 0000000..aafb73f --- /dev/null +++ b/src/main/java/com/github/zxbu/webdavteambition/store/TeambitionFileSystemStore.java @@ -0,0 +1,164 @@ +package com.github.zxbu.webdavteambition.store; + +import com.github.zxbu.webdavteambition.model.PathInfo; +import com.github.zxbu.webdavteambition.model.result.TFile; +import net.sf.webdav.ITransaction; +import net.sf.webdav.IWebdavStore; +import net.sf.webdav.StoredObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.http.HttpServletRequest; +import java.io.File; +import java.io.InputStream; +import java.security.Principal; +import java.util.Date; +import java.util.List; + +public class TeambitionFileSystemStore implements IWebdavStore { + private static final Logger LOGGER = LoggerFactory.getLogger(TeambitionFileSystemStore.class); + + private static ApplicationContext applicationContext; + private static TeambitionClientService teambitionClientService; + + + public TeambitionFileSystemStore(File file) { + } + + public static void setApplicationContext(ApplicationContext applicationContext) { + TeambitionFileSystemStore.applicationContext = applicationContext; + TeambitionFileSystemStore.teambitionClientService = applicationContext.getBean(TeambitionClientService.class); + } + + + + + @Override + public void destroy() { + LOGGER.debug("destroy"); + + } + + @Override + public ITransaction begin(Principal principal) { + LOGGER.debug("begin"); + return null; + } + + @Override + public void checkAuthentication(ITransaction transaction) { + LOGGER.debug("checkAuthentication"); + + } + + @Override + public void commit(ITransaction transaction) { + LOGGER.debug("commit"); + } + + @Override + public void rollback(ITransaction transaction) { + LOGGER.debug("rollback"); + + } + + @Override + public void createFolder(ITransaction transaction, String folderUri) { + LOGGER.info("createFolder {}", folderUri); + + teambitionClientService.createFolder(folderUri); + } + + @Override + public void createResource(ITransaction transaction, String resourceUri) { + LOGGER.info("createResource {}", resourceUri); + + } + + @Override + public InputStream getResourceContent(ITransaction transaction, String resourceUri) { + LOGGER.debug("getResourceContent: {}", resourceUri); + return teambitionClientService.download(resourceUri); + } + + @Override + public long setResourceContent(ITransaction transaction, String resourceUri, InputStream content, String contentType, String characterEncoding) { + LOGGER.info("setResourceContent {}", resourceUri); + ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + HttpServletRequest request = requestAttributes.getRequest(); + int contentLength = request.getContentLength(); + if (contentLength <= 0) { + return 0; + } + teambitionClientService.uploadPre(resourceUri, contentLength, content); + return contentLength; + } + + @Override + public String[] getChildrenNames(ITransaction transaction, String folderUri) { + LOGGER.debug("getChildrenNames: {}", folderUri); + TFile tFile = teambitionClientService.getTFileByPath(folderUri); + List tFileList = teambitionClientService.getTFiles(tFile.getNodeId()); + return tFileList.stream().map(TFile::getName).toArray(String[]::new); + } + + + + @Override + public long getResourceLength(ITransaction transaction, String path) { + LOGGER.debug("getResourceLength: {}", path); + TFile tFile = teambitionClientService.getTFileByPath(path); + if (tFile == null || tFile.getSize() == null) { + return 384; + } + + return tFile.getSize(); + } + + @Override + public void removeObject(ITransaction transaction, String uri) { + LOGGER.info("removeObject: {}", uri); + teambitionClientService.remove(uri); + } + + @Override + public boolean moveObject(ITransaction transaction, String destinationPath, String sourcePath) { + LOGGER.info("moveObject, destinationPath={}, sourcePath={}", destinationPath, sourcePath); + + PathInfo destinationPathInfo = teambitionClientService.getPathInfo(destinationPath); + PathInfo sourcePathInfo = teambitionClientService.getPathInfo(sourcePath); + // 名字相同,说明是移动目录 + if (sourcePathInfo.getName().equals(destinationPathInfo.getName())) { + teambitionClientService.move(sourcePath, destinationPathInfo.getParentPath()); + } else { + if (!destinationPathInfo.getParentPath().equals(sourcePathInfo.getParentPath())) { + throw new RuntimeException("不支持目录和名字同时修改"); + } + // 名字不同,说明是修改名字。不考虑目录和名字同时修改的情况 + teambitionClientService.rename(sourcePath, destinationPathInfo.getName()); + } + return true; + } + + @Override + public StoredObject getStoredObject(ITransaction transaction, String uri) { + LOGGER.debug("getStoredObject: {}", uri); + TFile tFile = teambitionClientService.getTFileByPath(uri); + if (tFile != null) { + StoredObject so = new StoredObject(); + so.setFolder(tFile.getKind().equalsIgnoreCase("folder")); + so.setResourceLength(getResourceLength(transaction, uri)); + so.setCreationDate(tFile.getCreated()); + so.setLastModified(tFile.getUpdated()); + return so; + } + + return null; + } + + +} diff --git a/src/main/java/com/github/zxbu/webdavteambition/util/JsonUtil.java b/src/main/java/com/github/zxbu/webdavteambition/util/JsonUtil.java new file mode 100644 index 0000000..da16b0d --- /dev/null +++ b/src/main/java/com/github/zxbu/webdavteambition/util/JsonUtil.java @@ -0,0 +1,143 @@ +package com.github.zxbu.webdavteambition.util; + +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class JsonUtil { + private static ObjectMapper objectMapper = new ObjectMapper(); + + public static String toJson(Object o) { + try { + return objectMapper.writeValueAsString(o); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + public static T readValue(String json, TypeReference valueTypeRef) { + try { + return objectMapper.readValue(json, valueTypeRef); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + + public static T readValue(String json, Class valueType) { + try { + return objectMapper.readValue(json, valueType); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + static { + // 忽略未知的字段 + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + // 读取不认识的枚举时,当null值处理 + objectMapper.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true); + + objectMapper.configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, true); + + } + + public static Object getJsonNodeValue(String json, String path) { + try { + JsonNode jsonNode = getJsonNode(json); + List pathTokens = getPathTokens(path); + for (PathToken pathToken : pathTokens) { + if (pathToken.getType() == PathType.KEY) { + jsonNode = jsonNode.get(pathToken.getValue()); + } + if (pathToken.getType() == PathType.NUMBER) { + jsonNode = jsonNode.get(Integer.parseInt(pathToken.getValue())); + } + } + return objectMapper.treeToValue(jsonNode, Object.class); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + + + private static List getPathTokens(String path) { + List pathTokenList = new ArrayList<>(); + StringBuilder sb = new StringBuilder(); + boolean escape = false; + for (char c : path.toCharArray()) { + if (c == '[') { + if (sb.length() > 0) { + pathTokenList.add(new PathToken(PathType.KEY, sb.toString())); + } + sb.setLength(0); + } else if (c == ']') { + if (sb.length() > 0) { + pathTokenList.add(new PathToken(PathType.NUMBER, sb.toString())); + } + sb.setLength(0); + } else if (c == '.') { + if (escape) { + sb.append(c); + } else { + if (sb.length() > 0) { + pathTokenList.add(new PathToken(PathType.KEY, sb.toString())); + } + sb.setLength(0); + } + + } else if (c == '\\') { + escape = true; + } else { + sb.append(c); + } + } + if (sb.length() > 0) { + pathTokenList.add(new PathToken(PathType.KEY, sb.toString())); + } + return pathTokenList; + } + + + + + private static JsonNode getJsonNode(String responseBody) { + try { + return objectMapper.readTree(responseBody); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + private enum PathType { + KEY, NUMBER; + } + + private static class PathToken { + private PathType type; // 0 是key, 1 是索引数字 + private String value; + + public PathToken(PathType type, String value) { + this.type = type; + this.value = value; + } + + public PathType getType() { + return type; + } + + public String getValue() { + return value; + } + } +} diff --git a/src/main/java/net/sf/webdav/IMethodExecutor.java b/src/main/java/net/sf/webdav/IMethodExecutor.java new file mode 100644 index 0000000..a913120 --- /dev/null +++ b/src/main/java/net/sf/webdav/IMethodExecutor.java @@ -0,0 +1,30 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed 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. + */ +package net.sf.webdav; + +import java.io.IOException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import net.sf.webdav.exceptions.LockFailedException; + +public interface IMethodExecutor { + + void execute(ITransaction transaction, HttpServletRequest req, + HttpServletResponse resp) throws IOException, LockFailedException; + +} diff --git a/src/main/java/net/sf/webdav/IMimeTyper.java b/src/main/java/net/sf/webdav/IMimeTyper.java new file mode 100644 index 0000000..2403b48 --- /dev/null +++ b/src/main/java/net/sf/webdav/IMimeTyper.java @@ -0,0 +1,13 @@ +package net.sf.webdav; + +public interface IMimeTyper { + + /** + * Detect the mime type of this object + * + * @param transaction + * @param path + * @return + */ + String getMimeType(ITransaction transaction, String path); +} diff --git a/src/main/java/net/sf/webdav/ITransaction.java b/src/main/java/net/sf/webdav/ITransaction.java new file mode 100644 index 0000000..6b505c3 --- /dev/null +++ b/src/main/java/net/sf/webdav/ITransaction.java @@ -0,0 +1,9 @@ +package net.sf.webdav; + +import java.security.Principal; + +public interface ITransaction { + + Principal getPrincipal(); + +} diff --git a/src/main/java/net/sf/webdav/IWebdavStore.java b/src/main/java/net/sf/webdav/IWebdavStore.java new file mode 100644 index 0000000..334d195 --- /dev/null +++ b/src/main/java/net/sf/webdav/IWebdavStore.java @@ -0,0 +1,223 @@ +/* + * $Header: /Users/ak/temp/cvs2svn/webdav-servlet/src/main/java/net/sf/webdav/IWebdavStore.java,v 1.1 2008-08-05 07:38:42 bauhardt Exp $ + * $Revision: 1.1 $ + * $Date: 2008-08-05 07:38:42 $ + * + * ==================================================================== + * + * Copyright 2004 The Apache Software Foundation + * + * Licensed 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. + * + */ + +package net.sf.webdav; + +import java.io.InputStream; +import java.security.Principal; + +import net.sf.webdav.exceptions.WebdavException; + +/** + * Interface for simple implementation of any store for the WebdavServlet + *

+ * based on the BasicWebdavStore from Oliver Zeigermann, that was part of the + * Webdav Construcktion Kit from slide + * + */ +public interface IWebdavStore { + + /** + * Life cycle method, called by WebdavServlet's destroy() method. Should be used to clean up resources. + */ + void destroy(); + + /** + * Indicates that a new request or transaction with this store involved has + * been started. The request will be terminated by either {@link #commit()} + * or {@link #rollback()}. If only non-read methods have been called, the + * request will be terminated by a {@link #commit()}. This method will be + * called by (@link WebdavStoreAdapter} at the beginning of each request. + * + * + * @param principal + * the principal that started this request or null if + * there is non available + * + * @throws WebdavException + */ + ITransaction begin(Principal principal); + + /** + * Checks if authentication information passed in is valid. If not throws an + * exception. + * + * @param transaction + * indicates that the method is within the scope of a WebDAV + * transaction + */ + void checkAuthentication(ITransaction transaction); + + /** + * Indicates that all changes done inside this request shall be made + * permanent and any transactions, connections and other temporary resources + * shall be terminated. + * + * @param transaction + * indicates that the method is within the scope of a WebDAV + * transaction + * + * @throws WebdavException + * if something goes wrong on the store level + */ + void commit(ITransaction transaction); + + /** + * Indicates that all changes done inside this request shall be undone and + * any transactions, connections and other temporary resources shall be + * terminated. + * + * @param transaction + * indicates that the method is within the scope of a WebDAV + * transaction + * + * @throws WebdavException + * if something goes wrong on the store level + */ + void rollback(ITransaction transaction); + + /** + * Creates a folder at the position specified by folderUri. + * + * @param transaction + * indicates that the method is within the scope of a WebDAV + * transaction + * @param folderUri + * URI of the folder + * @throws WebdavException + * if something goes wrong on the store level + */ + void createFolder(ITransaction transaction, String folderUri); + + /** + * Creates a content resource at the position specified by + * resourceUri. + * + * @param transaction + * indicates that the method is within the scope of a WebDAV + * transaction + * @param resourceUri + * URI of the content resource + * @throws WebdavException + * if something goes wrong on the store level + */ + void createResource(ITransaction transaction, String resourceUri); + + /** + * Gets the content of the resource specified by resourceUri. + * + * @param transaction + * indicates that the method is within the scope of a WebDAV + * transaction + * @param resourceUri + * URI of the content resource + * @return input stream you can read the content of the resource from + * @throws WebdavException + * if something goes wrong on the store level + */ + InputStream getResourceContent(ITransaction transaction, String resourceUri); + + /** + * Sets / stores the content of the resource specified by + * resourceUri. + * + * @param transaction + * indicates that the method is within the scope of a WebDAV + * transaction + * @param resourceUri + * URI of the resource where the content will be stored + * @param content + * input stream from which the content will be read from + * @param contentType + * content type of the resource or null if unknown + * @param characterEncoding + * character encoding of the resource or null if unknown + * or not applicable + * @return lenght of resource + * @throws WebdavException + * if something goes wrong on the store level + */ + long setResourceContent(ITransaction transaction, String resourceUri, + InputStream content, String contentType, String characterEncoding); + + /** + * Gets the names of the children of the folder specified by + * folderUri. + * + * @param transaction + * indicates that the method is within the scope of a WebDAV + * transaction + * @param folderUri + * URI of the folder + * @return a (possibly empty) list of children, or null if the + * uri points to a file + * @throws WebdavException + * if something goes wrong on the store level + */ + String[] getChildrenNames(ITransaction transaction, String folderUri); + + /** + * Gets the length of the content resource specified by + * resourceUri. + * + * @param transaction + * indicates that the method is within the scope of a WebDAV + * transaction + * @param resourceUri + * URI of the content resource + * @return length of the resource in bytes, -1 declares this + * value as invalid and asks the adapter to try to set it from the + * properties if possible + * @throws WebdavException + * if something goes wrong on the store level + */ + long getResourceLength(ITransaction transaction, String path); + + /** + * Removes the object specified by uri. + * + * @param transaction + * indicates that the method is within the scope of a WebDAV + * transaction + * @param uri + * URI of the object, i.e. content resource or folder + * @throws WebdavException + * if something goes wrong on the store level + */ + void removeObject(ITransaction transaction, String uri); + + boolean moveObject(ITransaction transaction, String destinationPath, String sourcePath); + + /** + * Gets the storedObject specified by uri + * + * @param transaction + * indicates that the method is within the scope of a WebDAV + * transaction + * @param uri + * URI + * @return StoredObject + */ + StoredObject getStoredObject(ITransaction transaction, String uri); + +} diff --git a/src/main/java/net/sf/webdav/LocalFileSystemStore.java b/src/main/java/net/sf/webdav/LocalFileSystemStore.java new file mode 100644 index 0000000..2422ff9 --- /dev/null +++ b/src/main/java/net/sf/webdav/LocalFileSystemStore.java @@ -0,0 +1,228 @@ +/* + * + * Licensed 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. + * + */ +package net.sf.webdav; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.Principal; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import net.sf.webdav.exceptions.UnauthenticatedException; +import net.sf.webdav.exceptions.WebdavException; + +/** + * Reference Implementation of WebdavStore + * + * @author joa + * @author re + */ +public class LocalFileSystemStore implements IWebdavStore { + + private static org.slf4j.Logger LOG = org.slf4j.LoggerFactory + .getLogger(LocalFileSystemStore.class); + + private static int BUF_SIZE = 65536; + + private File _root = null; + + public LocalFileSystemStore(File root) { + _root = root; + } + + public void destroy() { + ; + } + + public ITransaction begin(Principal principal) throws WebdavException { + LOG.info("LocalFileSystemStore.begin()"); + if (!_root.exists()) { + if (!_root.mkdirs()) { + throw new WebdavException("root path: " + + _root.getAbsolutePath() + + " does not exist and could not be created"); + } + } +// if (principal == null) { +// throw new UnauthenticatedException(WebdavStatus.SC_UNAUTHORIZED); +// } + return null; + } + + public void checkAuthentication(ITransaction transaction) + throws SecurityException { + LOG.info("LocalFileSystemStore.checkAuthentication()"); + // do nothing +// throw new UnauthenticatedException(WebdavStatus.SC_FORBIDDEN); + } + + public void commit(ITransaction transaction) throws WebdavException { + // do nothing + LOG.info("LocalFileSystemStore.commit()"); + } + + public void rollback(ITransaction transaction) throws WebdavException { + // do nothing + LOG.info("LocalFileSystemStore.rollback()"); + + } + + public void createFolder(ITransaction transaction, String uri) + throws WebdavException { + LOG.info("LocalFileSystemStore.createFolder(" + uri + ")"); + File file = new File(_root, uri); + if (!file.mkdir()) + throw new WebdavException("cannot create folder: " + uri); + } + + public void createResource(ITransaction transaction, String uri) + throws WebdavException { + LOG.info("LocalFileSystemStore.createResource(" + uri + ")"); + File file = new File(_root, uri); + try { + if (!file.createNewFile()) + throw new WebdavException("cannot create file: " + uri); + } catch (IOException e) { + LOG + .error("LocalFileSystemStore.createResource(" + uri + + ") failed"); + throw new WebdavException(e); + } + } + + public long setResourceContent(ITransaction transaction, String uri, + InputStream is, String contentType, String characterEncoding) + throws WebdavException { + + LOG.info("LocalFileSystemStore.setResourceContent(" + uri + ")"); + File file = new File(_root, uri); + try { + OutputStream os = new BufferedOutputStream(new FileOutputStream( + file), BUF_SIZE); + try { + int read; + byte[] copyBuffer = new byte[BUF_SIZE]; + + while ((read = is.read(copyBuffer, 0, copyBuffer.length)) != -1) { + os.write(copyBuffer, 0, read); + } + } finally { + try { + is.close(); + } finally { + os.close(); + } + } + } catch (IOException e) { + LOG.error("LocalFileSystemStore.setResourceContent(" + uri + + ") failed"); + throw new WebdavException(e); + } + long length = -1; + + try { + length = file.length(); + } catch (SecurityException e) { + LOG.error("LocalFileSystemStore.setResourceContent(" + uri + + ") failed" + "\nCan't get file.length"); + } + + return length; + } + + public String[] getChildrenNames(ITransaction transaction, String uri) + throws WebdavException { + LOG.info("LocalFileSystemStore.getChildrenNames(" + uri + ")"); + File file = new File(_root, uri); + String[] childrenNames = null; + if (file.isDirectory()) { + File[] children = file.listFiles(); + List childList = new ArrayList(); + String name = null; + for (int i = 0; i < children.length; i++) { + name = children[i].getName(); + childList.add(name); + LOG.info("Child " + i + ": " + name); + } + childrenNames = new String[childList.size()]; + childrenNames = (String[]) childList.toArray(childrenNames); + } + return childrenNames; + } + + public void removeObject(ITransaction transaction, String uri) + throws WebdavException { + File file = new File(_root, uri); + boolean success = file.delete(); + LOG.info("LocalFileSystemStore.removeObject(" + uri + ")=" + success); + if (!success) { + throw new WebdavException("cannot delete object: " + uri); + } + + } + + @Override + public boolean moveObject(ITransaction transaction, String destinationPath, String path) { + return false; + } + + public InputStream getResourceContent(ITransaction transaction, String uri) + throws WebdavException { + LOG.info("LocalFileSystemStore.getResourceContent(" + uri + ")"); + File file = new File(_root, uri); + + InputStream in; + try { + in = new BufferedInputStream(new FileInputStream(file)); + } catch (IOException e) { + LOG.error("LocalFileSystemStore.getResourceContent(" + uri + + ") failed"); + throw new WebdavException(e); + } + return in; + } + + public long getResourceLength(ITransaction transaction, String uri) + throws WebdavException { + LOG.info("LocalFileSystemStore.getResourceLength(" + uri + ")"); + File file = new File(_root, uri); + return file.length(); + } + + public StoredObject getStoredObject(ITransaction transaction, String uri) { + + StoredObject so = null; + + File file = new File(_root, uri); + if (file.exists()) { + so = new StoredObject(); + so.setFolder(file.isDirectory()); + so.setLastModified(new Date(file.lastModified())); + so.setCreationDate(new Date(file.lastModified())); + so.setResourceLength(getResourceLength(transaction, uri)); + } + + return so; + } + +} diff --git a/src/main/java/net/sf/webdav/StoredObject.java b/src/main/java/net/sf/webdav/StoredObject.java new file mode 100644 index 0000000..443e149 --- /dev/null +++ b/src/main/java/net/sf/webdav/StoredObject.java @@ -0,0 +1,165 @@ +/* + * ==================================================================== + * + * Copyright 2004 The Apache Software Foundation + * + * Licensed 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. + * + */ + +package net.sf.webdav; + +import java.util.Date; + +public class StoredObject { + + private boolean isFolder; + private Date lastModified; + private Date creationDate; + private long contentLength; + private String mimeType; + + private boolean isNullRessource; + + /** + * Determines whether the StoredObject is a folder or a resource + * + * @return true if the StoredObject is a collection + */ + public boolean isFolder() { + return (isFolder); + } + + /** + * Determines whether the StoredObject is a folder or a resource + * + * @return true if the StoredObject is a resource + */ + public boolean isResource() { + return (!isFolder); + } + + /** + * Sets a new StoredObject as a collection or resource + * + * @param f + * true - collection ; false - resource + */ + public void setFolder(boolean f) { + this.isFolder = f; + } + + /** + * Gets the date of the last modification + * + * @return last modification Date + */ + public Date getLastModified() { + return (lastModified); + } + + /** + * Sets the date of the last modification + * + * @param d + * date of the last modification + */ + public void setLastModified(Date d) { + this.lastModified = d; + } + + /** + * Gets the date of the creation + * + * @return creation Date + */ + public Date getCreationDate() { + return (creationDate); + } + + /** + * Sets the date of the creation + * + * @param d + * date of the creation + */ + public void setCreationDate(Date c) { + this.creationDate = c; + } + + /** + * Gets the length of the resource content + * + * @return length of the resource content + */ + public long getResourceLength() { + return (contentLength); + } + + /** + * Sets the length of the resource content + * + * @param l + * the length of the resource content + */ + public void setResourceLength(long l) { + this.contentLength = l; + } + + /** + * Gets the state of the resource + * + * @return true if the resource is in lock-null state + */ + public boolean isNullResource() { + return isNullRessource; + } + + /** + * Sets a StoredObject as a lock-null resource + * + * @param f + * true to set the resource as lock-null resource + */ + public void setNullResource(boolean f) { + this.isNullRessource = f; + this.isFolder = false; + this.creationDate = null; + this.lastModified = null; + // this.content = null; + this.contentLength = 0; + this.mimeType= null; + } + + /** + * Retrieve the myme type from the store object. + * Can also return NULL if the store does not handle + * mime type stuff. + * In that case the mime type is determined by the servletcontext + * + * @return the mimeType + */ + public String getMimeType() { + return mimeType; + } + + /** + * Set the mime type of this object + * + * @param mimeType the mimeType to set + */ + public void setMimeType(String mimeType) { + this.mimeType = mimeType; + } + +} diff --git a/src/main/java/net/sf/webdav/WebDavServletBean.java b/src/main/java/net/sf/webdav/WebDavServletBean.java new file mode 100644 index 0000000..146710f --- /dev/null +++ b/src/main/java/net/sf/webdav/WebDavServletBean.java @@ -0,0 +1,219 @@ +package net.sf.webdav; + +import java.io.IOException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.Principal; +import java.util.Enumeration; +import java.util.HashMap; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import net.sf.webdav.exceptions.UnauthenticatedException; +import net.sf.webdav.exceptions.WebdavException; +import net.sf.webdav.fromcatalina.MD5Encoder; +import net.sf.webdav.locking.ResourceLocks; +import net.sf.webdav.methods.DoCopy; +import net.sf.webdav.methods.DoDelete; +import net.sf.webdav.methods.DoGet; +import net.sf.webdav.methods.DoHead; +import net.sf.webdav.methods.DoLock; +import net.sf.webdav.methods.DoMkcol; +import net.sf.webdav.methods.DoMove; +import net.sf.webdav.methods.DoNotImplemented; +import net.sf.webdav.methods.DoOptions; +import net.sf.webdav.methods.DoPropfind; +import net.sf.webdav.methods.DoProppatch; +import net.sf.webdav.methods.DoPut; +import net.sf.webdav.methods.DoUnlock; + +public class WebDavServletBean extends HttpServlet { + + private static org.slf4j.Logger LOG = org.slf4j.LoggerFactory + .getLogger(WebDavServletBean.class); + + /** + * MD5 message digest provider. + */ + protected static MessageDigest MD5_HELPER; + + /** + * The MD5 helper object for this class. + */ + protected static final MD5Encoder MD5_ENCODER = new MD5Encoder(); + + private static final boolean READ_ONLY = false; + protected ResourceLocks _resLocks; + protected IWebdavStore _store; + private HashMap _methodMap = new HashMap(); + + public WebDavServletBean() { + _resLocks = new ResourceLocks(); + + try { + MD5_HELPER = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(); + } + } + + public void init(IWebdavStore store, String dftIndexFile, + String insteadOf404, int nocontentLenghHeaders, + boolean lazyFolderCreationOnPut) throws ServletException { + + _store = store; + + IMimeTyper mimeTyper = new IMimeTyper() { + public String getMimeType(ITransaction transaction, String path) { + String retVal= _store.getStoredObject(transaction, path).getMimeType(); + if ( retVal== null) { + retVal= getServletContext().getMimeType( path); + } + return retVal; + } + }; + + register("GET", new DoGet(store, dftIndexFile, insteadOf404, _resLocks, + mimeTyper, nocontentLenghHeaders)); + register("HEAD", new DoHead(store, dftIndexFile, insteadOf404, + _resLocks, mimeTyper, nocontentLenghHeaders)); + DoDelete doDelete = (DoDelete) register("DELETE", new DoDelete(store, + _resLocks, READ_ONLY)); + DoCopy doCopy = (DoCopy) register("COPY", new DoCopy(store, _resLocks, + doDelete, READ_ONLY)); + register("LOCK", new DoLock(store, _resLocks, READ_ONLY)); + register("UNLOCK", new DoUnlock(store, _resLocks, READ_ONLY)); + register("MOVE", new DoMove(store, _resLocks, doDelete, doCopy, READ_ONLY)); + register("MKCOL", new DoMkcol(store, _resLocks, READ_ONLY)); + register("OPTIONS", new DoOptions(store, _resLocks)); + register("PUT", new DoPut(store, _resLocks, READ_ONLY, + lazyFolderCreationOnPut)); + register("PROPFIND", new DoPropfind(store, _resLocks, mimeTyper)); + register("PROPPATCH", new DoProppatch(store, _resLocks, READ_ONLY)); + register("*NO*IMPL*", new DoNotImplemented(READ_ONLY)); + } + + @Override + public void destroy() { + if(_store != null) + _store.destroy(); + super.destroy(); + } + + protected IMethodExecutor register(String methodName, IMethodExecutor method) { + _methodMap.put(methodName, method); + return method; + } + + /** + * Handles the special WebDAV methods. + */ + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + String methodName = req.getMethod(); + ITransaction transaction = null; + boolean needRollback = false; + + if (LOG.isTraceEnabled()) + debugRequest(methodName, req); + + try { + Principal userPrincipal = getUserPrincipal(req); + transaction = _store.begin(userPrincipal); + needRollback = true; + _store.checkAuthentication(transaction); + resp.setStatus(WebdavStatus.SC_OK); + + try { + IMethodExecutor methodExecutor = (IMethodExecutor) _methodMap + .get(methodName); + if (methodExecutor == null) { + methodExecutor = (IMethodExecutor) _methodMap + .get("*NO*IMPL*"); + } + + methodExecutor.execute(transaction, req, resp); + + _store.commit(transaction); + /** Clear not consumed data + * + * Clear input stream if available otherwise later access + * include current input. These cases occure if the client + * sends a request with body to an not existing resource. + */ + if (req.getContentLength() != 0 && req.getInputStream().available() > 0) { + if (LOG.isTraceEnabled()) { LOG.trace("Clear not consumed data!"); } + while (req.getInputStream().available() > 0) { + req.getInputStream().read(); + } + } + needRollback = false; + } catch (IOException e) { + java.io.StringWriter sw = new java.io.StringWriter(); + java.io.PrintWriter pw = new java.io.PrintWriter(sw); + e.printStackTrace(pw); + LOG.error("IOException: " + sw.toString()); + resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR); + _store.rollback(transaction); + throw new ServletException(e); + } + + } catch (UnauthenticatedException e) { + resp.sendError(e.getCode()); + } catch (WebdavException e) { + java.io.StringWriter sw = new java.io.StringWriter(); + java.io.PrintWriter pw = new java.io.PrintWriter(sw); + e.printStackTrace(pw); + LOG.error("WebdavException: " + sw.toString()); + throw new ServletException(e); + } catch (Exception e) { + java.io.StringWriter sw = new java.io.StringWriter(); + java.io.PrintWriter pw = new java.io.PrintWriter(sw); + e.printStackTrace(pw); + LOG.error("Exception: " + sw.toString()); + } finally { + if (needRollback) + _store.rollback(transaction); + } + + } + + /** + * Method that permit to customize the way + * user information are extracted from the request, default use JAAS + * @param req + * @return + */ + protected Principal getUserPrincipal(HttpServletRequest req) { + return req.getUserPrincipal(); + } + + private void debugRequest(String methodName, HttpServletRequest req) { + LOG.trace("-----------"); + LOG.trace("WebdavServlet\n request: methodName = " + methodName); + LOG.trace("time: " + System.currentTimeMillis()); + LOG.trace("path: " + req.getRequestURI()); + LOG.trace("-----------"); + Enumeration e = req.getHeaderNames(); + while (e.hasMoreElements()) { + String s = (String) e.nextElement(); + LOG.trace("header: " + s + " " + req.getHeader(s)); + } + e = req.getAttributeNames(); + while (e.hasMoreElements()) { + String s = (String) e.nextElement(); + LOG.trace("attribute: " + s + " " + req.getAttribute(s)); + } + e = req.getParameterNames(); + while (e.hasMoreElements()) { + String s = (String) e.nextElement(); + LOG.trace("parameter: " + s + " " + req.getParameter(s)); + } + } + +} diff --git a/src/main/java/net/sf/webdav/WebdavServlet.java b/src/main/java/net/sf/webdav/WebdavServlet.java new file mode 100644 index 0000000..c9540be --- /dev/null +++ b/src/main/java/net/sf/webdav/WebdavServlet.java @@ -0,0 +1,114 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed 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. + */ + +package net.sf.webdav; + +import java.io.File; +import java.lang.reflect.Constructor; + +import javax.servlet.ServletException; + +import net.sf.webdav.exceptions.WebdavException; + +/** + * Servlet which provides support for WebDAV level 2. + * + * the original class is org.apache.catalina.servlets.WebdavServlet by Remy + * Maucherat, which was heavily changed + * + * @author Remy Maucherat + */ + +public class WebdavServlet extends WebDavServletBean { + + private static final String ROOTPATH_PARAMETER = "rootpath"; + + public void init() throws ServletException { + + // Parameters from web.xml + String clazzName = getServletConfig().getInitParameter( + "ResourceHandlerImplementation"); + if (clazzName == null || clazzName.equals("")) { + clazzName = LocalFileSystemStore.class.getName(); + } + + File root = getFileRoot(); + + IWebdavStore webdavStore = constructStore(clazzName, root); + + boolean lazyFolderCreationOnPut = getInitParameter("lazyFolderCreationOnPut") != null + && getInitParameter("lazyFolderCreationOnPut").equals("1"); + + String dftIndexFile = getInitParameter("default-index-file"); + String insteadOf404 = getInitParameter("instead-of-404"); + + int noContentLengthHeader = getIntInitParameter("no-content-length-headers"); + + super.init(webdavStore, dftIndexFile, insteadOf404, + noContentLengthHeader, lazyFolderCreationOnPut); + } + + private int getIntInitParameter(String key) { + return getInitParameter(key) == null ? -1 : Integer + .parseInt(getInitParameter(key)); + } + + protected IWebdavStore constructStore(String clazzName, File root) { + IWebdavStore webdavStore; + try { + Class clazz = WebdavServlet.class.getClassLoader().loadClass( + clazzName); + + Constructor ctor = clazz + .getConstructor(new Class[] { File.class }); + + webdavStore = (IWebdavStore) ctor + .newInstance(new Object[] { root }); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("some problem making store component", e); + } + return webdavStore; + } + + private File getFileRoot() { + String rootPath = getInitParameter(ROOTPATH_PARAMETER); + if (rootPath == null) { + throw new WebdavException("missing parameter: " + + ROOTPATH_PARAMETER); + } + if (rootPath.equals("*WAR-FILE-ROOT*")) { + String file = LocalFileSystemStore.class.getProtectionDomain() + .getCodeSource().getLocation().getFile().replace('\\', '/'); + if (file.charAt(0) == '/' + && System.getProperty("os.name").indexOf("Windows") != -1) { + file = file.substring(1, file.length()); + } + + int ix = file.indexOf("/WEB-INF/"); + if (ix != -1) { + rootPath = file.substring(0, ix).replace('/', + File.separatorChar); + } else { + throw new WebdavException( + "Could not determine root of war file. Can't extract from path '" + + file + "' for this web container"); + } + } + return new File(rootPath); + } + +} diff --git a/src/main/java/net/sf/webdav/WebdavStatus.java b/src/main/java/net/sf/webdav/WebdavStatus.java new file mode 100644 index 0000000..ae206cc --- /dev/null +++ b/src/main/java/net/sf/webdav/WebdavStatus.java @@ -0,0 +1,273 @@ +package net.sf.webdav; + +import java.util.Hashtable; + +import javax.servlet.http.HttpServletResponse; + +/** + * Wraps the HttpServletResponse class to abstract the specific protocol used. + * To support other protocols we would only need to modify this class and the + * WebDavRetCode classes. + * + * @author Marc Eaddy + * @version 1.0, 16 Nov 1997 + */ +public class WebdavStatus { + + // ----------------------------------------------------- Instance Variables + + /** + * This Hashtable contains the mapping of HTTP and WebDAV status codes to + * descriptive text. This is a static variable. + */ + private static Hashtable _mapStatusCodes = new Hashtable(); + + // ------------------------------------------------------ HTTP Status Codes + + /** + * Status code (200) indicating the request succeeded normally. + */ + public static final int SC_OK = HttpServletResponse.SC_OK; + + /** + * Status code (201) indicating the request succeeded and created a new + * resource on the server. + */ + public static final int SC_CREATED = HttpServletResponse.SC_CREATED; + + /** + * Status code (202) indicating that a request was accepted for processing, + * but was not completed. + */ + public static final int SC_ACCEPTED = HttpServletResponse.SC_ACCEPTED; + + /** + * Status code (204) indicating that the request succeeded but that there + * was no new information to return. + */ + public static final int SC_NO_CONTENT = HttpServletResponse.SC_NO_CONTENT; + + /** + * Status code (301) indicating that the resource has permanently moved to a + * new location, and that future references should use a new URI with their + * requests. + */ + public static final int SC_MOVED_PERMANENTLY = HttpServletResponse.SC_MOVED_PERMANENTLY; + + /** + * Status code (302) indicating that the resource has temporarily moved to + * another location, but that future references should still use the + * original URI to access the resource. + */ + public static final int SC_MOVED_TEMPORARILY = HttpServletResponse.SC_MOVED_TEMPORARILY; + + /** + * Status code (304) indicating that a conditional GET operation found that + * the resource was available and not modified. + */ + public static final int SC_NOT_MODIFIED = HttpServletResponse.SC_NOT_MODIFIED; + + /** + * Status code (400) indicating the request sent by the client was + * syntactically incorrect. + */ + public static final int SC_BAD_REQUEST = HttpServletResponse.SC_BAD_REQUEST; + + /** + * Status code (401) indicating that the request requires HTTP + * authentication. + */ + public static final int SC_UNAUTHORIZED = HttpServletResponse.SC_UNAUTHORIZED; + + /** + * Status code (403) indicating the server understood the request but + * refused to fulfill it. + */ + public static final int SC_FORBIDDEN = HttpServletResponse.SC_FORBIDDEN; + + /** + * Status code (404) indicating that the requested resource is not + * available. + */ + public static final int SC_NOT_FOUND = HttpServletResponse.SC_NOT_FOUND; + + /** + * Status code (500) indicating an error inside the HTTP service which + * prevented it from fulfilling the request. + */ + public static final int SC_INTERNAL_SERVER_ERROR = HttpServletResponse.SC_INTERNAL_SERVER_ERROR; + + /** + * Status code (501) indicating the HTTP service does not support the + * functionality needed to fulfill the request. + */ + public static final int SC_NOT_IMPLEMENTED = HttpServletResponse.SC_NOT_IMPLEMENTED; + + /** + * Status code (502) indicating that the HTTP server received an invalid + * response from a server it consulted when acting as a proxy or gateway. + */ + public static final int SC_BAD_GATEWAY = HttpServletResponse.SC_BAD_GATEWAY; + + /** + * Status code (503) indicating that the HTTP service is temporarily + * overloaded, and unable to handle the request. + */ + public static final int SC_SERVICE_UNAVAILABLE = HttpServletResponse.SC_SERVICE_UNAVAILABLE; + + /** + * Status code (100) indicating the client may continue with its request. + * This interim response is used to inform the client that the initial part + * of the request has been received and has not yet been rejected by the + * server. + */ + public static final int SC_CONTINUE = 100; + + /** + * Status code (405) indicating the method specified is not allowed for the + * resource. + */ + public static final int SC_METHOD_NOT_ALLOWED = 405; + + /** + * Status code (409) indicating that the request could not be completed due + * to a conflict with the current state of the resource. + */ + public static final int SC_CONFLICT = 409; + + /** + * Status code (412) indicating the precondition given in one or more of the + * request-header fields evaluated to false when it was tested on the + * server. + */ + public static final int SC_PRECONDITION_FAILED = 412; + + /** + * Status code (413) indicating the server is refusing to process a request + * because the request entity is larger than the server is willing or able + * to process. + */ + public static final int SC_REQUEST_TOO_LONG = 413; + + /** + * Status code (415) indicating the server is refusing to service the + * request because the entity of the request is in a format not supported by + * the requested resource for the requested method. + */ + public static final int SC_UNSUPPORTED_MEDIA_TYPE = 415; + + // -------------------------------------------- Extended WebDav status code + + /** + * Status code (207) indicating that the response requires providing status + * for multiple independent operations. + */ + public static final int SC_MULTI_STATUS = 207; + + // This one colides with HTTP 1.1 + // "207 Parital Update OK" + + /** + * Status code (418) indicating the entity body submitted with the PATCH + * method was not understood by the resource. + */ + public static final int SC_UNPROCESSABLE_ENTITY = 418; + + // This one colides with HTTP 1.1 + // "418 Reauthentication Required" + + /** + * Status code (419) indicating that the resource does not have sufficient + * space to record the state of the resource after the execution of this + * method. + */ + public static final int SC_INSUFFICIENT_SPACE_ON_RESOURCE = 419; + + // This one colides with HTTP 1.1 + // "419 Proxy Reauthentication Required" + + /** + * Status code (420) indicating the method was not executed on a particular + * resource within its scope because some part of the method's execution + * failed causing the entire method to be aborted. + */ + public static final int SC_METHOD_FAILURE = 420; + + /** + * Status code (423) indicating the destination resource of a method is + * locked, and either the request did not contain a valid Lock-Info header, + * or the Lock-Info header identifies a lock held by another principal. + */ + public static final int SC_LOCKED = 423; + + // ------------------------------------------------------------ Initializer + + static { + // HTTP 1.0 Status Code + addStatusCodeMap(SC_OK, "OK"); + addStatusCodeMap(SC_CREATED, "Created"); + addStatusCodeMap(SC_ACCEPTED, "Accepted"); + addStatusCodeMap(SC_NO_CONTENT, "No Content"); + addStatusCodeMap(SC_MOVED_PERMANENTLY, "Moved Permanently"); + addStatusCodeMap(SC_MOVED_TEMPORARILY, "Moved Temporarily"); + addStatusCodeMap(SC_NOT_MODIFIED, "Not Modified"); + addStatusCodeMap(SC_BAD_REQUEST, "Bad Request"); + addStatusCodeMap(SC_UNAUTHORIZED, "Unauthorized"); + addStatusCodeMap(SC_FORBIDDEN, "Forbidden"); + addStatusCodeMap(SC_NOT_FOUND, "Not Found"); + addStatusCodeMap(SC_INTERNAL_SERVER_ERROR, "Internal Server Error"); + addStatusCodeMap(SC_NOT_IMPLEMENTED, "Not Implemented"); + addStatusCodeMap(SC_BAD_GATEWAY, "Bad Gateway"); + addStatusCodeMap(SC_SERVICE_UNAVAILABLE, "Service Unavailable"); + addStatusCodeMap(SC_CONTINUE, "Continue"); + addStatusCodeMap(SC_METHOD_NOT_ALLOWED, "Method Not Allowed"); + addStatusCodeMap(SC_CONFLICT, "Conflict"); + addStatusCodeMap(SC_PRECONDITION_FAILED, "Precondition Failed"); + addStatusCodeMap(SC_REQUEST_TOO_LONG, "Request Too Long"); + addStatusCodeMap(SC_UNSUPPORTED_MEDIA_TYPE, "Unsupported Media Type"); + // WebDav Status Codes + addStatusCodeMap(SC_MULTI_STATUS, "Multi-Status"); + addStatusCodeMap(SC_UNPROCESSABLE_ENTITY, "Unprocessable Entity"); + addStatusCodeMap(SC_INSUFFICIENT_SPACE_ON_RESOURCE, + "Insufficient Space On Resource"); + addStatusCodeMap(SC_METHOD_FAILURE, "Method Failure"); + addStatusCodeMap(SC_LOCKED, "Locked"); + } + + // --------------------------------------------------------- Public Methods + + /** + * Returns the HTTP status text for the HTTP or WebDav status code specified + * by looking it up in the static mapping. This is a static function. + * + * @param nHttpStatusCode + * [IN] HTTP or WebDAV status code + * @return A string with a short descriptive phrase for the HTTP status code + * (e.g., "OK"). + */ + public static String getStatusText(int nHttpStatusCode) { + Integer intKey = new Integer(nHttpStatusCode); + + if (!_mapStatusCodes.containsKey(intKey)) { + return ""; + } else { + return (String) _mapStatusCodes.get(intKey); + } + } + + // -------------------------------------------------------- Private Methods + + /** + * Adds a new status code -> status text mapping. This is a static method + * because the mapping is a static variable. + * + * @param nKey + * [IN] HTTP or WebDAV status code + * @param strVal + * [IN] HTTP status text + */ + private static void addStatusCodeMap(int nKey, String strVal) { + _mapStatusCodes.put(new Integer(nKey), strVal); + } + +}; diff --git a/src/main/java/net/sf/webdav/exceptions/AccessDeniedException.java b/src/main/java/net/sf/webdav/exceptions/AccessDeniedException.java new file mode 100644 index 0000000..667a528 --- /dev/null +++ b/src/main/java/net/sf/webdav/exceptions/AccessDeniedException.java @@ -0,0 +1,36 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed 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. + */ + +package net.sf.webdav.exceptions; + +public class AccessDeniedException extends WebdavException { + + public AccessDeniedException() { + super(); + } + + public AccessDeniedException(String message) { + super(message); + } + + public AccessDeniedException(String message, Throwable cause) { + super(message, cause); + } + + public AccessDeniedException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/net/sf/webdav/exceptions/LockFailedException.java b/src/main/java/net/sf/webdav/exceptions/LockFailedException.java new file mode 100644 index 0000000..b6e4c0f --- /dev/null +++ b/src/main/java/net/sf/webdav/exceptions/LockFailedException.java @@ -0,0 +1,20 @@ +package net.sf.webdav.exceptions; + +public class LockFailedException extends WebdavException { + + public LockFailedException() { + super(); + } + + public LockFailedException(String message) { + super(message); + } + + public LockFailedException(String message, Throwable cause) { + super(message, cause); + } + + public LockFailedException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/net/sf/webdav/exceptions/ObjectAlreadyExistsException.java b/src/main/java/net/sf/webdav/exceptions/ObjectAlreadyExistsException.java new file mode 100644 index 0000000..d093800 --- /dev/null +++ b/src/main/java/net/sf/webdav/exceptions/ObjectAlreadyExistsException.java @@ -0,0 +1,36 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed 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. + */ + +package net.sf.webdav.exceptions; + +public class ObjectAlreadyExistsException extends WebdavException { + + public ObjectAlreadyExistsException() { + super(); + } + + public ObjectAlreadyExistsException(String message) { + super(message); + } + + public ObjectAlreadyExistsException(String message, Throwable cause) { + super(message, cause); + } + + public ObjectAlreadyExistsException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/net/sf/webdav/exceptions/ObjectNotFoundException.java b/src/main/java/net/sf/webdav/exceptions/ObjectNotFoundException.java new file mode 100644 index 0000000..ae0a736 --- /dev/null +++ b/src/main/java/net/sf/webdav/exceptions/ObjectNotFoundException.java @@ -0,0 +1,36 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed 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. + */ + +package net.sf.webdav.exceptions; + +public class ObjectNotFoundException extends WebdavException { + + public ObjectNotFoundException() { + super(); + } + + public ObjectNotFoundException(String message) { + super(message); + } + + public ObjectNotFoundException(String message, Throwable cause) { + super(message, cause); + } + + public ObjectNotFoundException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/net/sf/webdav/exceptions/UnauthenticatedException.java b/src/main/java/net/sf/webdav/exceptions/UnauthenticatedException.java new file mode 100644 index 0000000..462df69 --- /dev/null +++ b/src/main/java/net/sf/webdav/exceptions/UnauthenticatedException.java @@ -0,0 +1,30 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed 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. + */ + +package net.sf.webdav.exceptions; + +public class UnauthenticatedException extends WebdavException { + private final int code; + + public UnauthenticatedException(int code) { + super(); + this.code = code; + } + + public int getCode() { + return code; + } +} diff --git a/src/main/java/net/sf/webdav/exceptions/WebdavException.java b/src/main/java/net/sf/webdav/exceptions/WebdavException.java new file mode 100644 index 0000000..6d8a620 --- /dev/null +++ b/src/main/java/net/sf/webdav/exceptions/WebdavException.java @@ -0,0 +1,36 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed 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. + */ + +package net.sf.webdav.exceptions; + +public class WebdavException extends RuntimeException { + + public WebdavException() { + super(); + } + + public WebdavException(String message) { + super(message); + } + + public WebdavException(String message, Throwable cause) { + super(message, cause); + } + + public WebdavException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/net/sf/webdav/fromcatalina/MD5Encoder.java b/src/main/java/net/sf/webdav/fromcatalina/MD5Encoder.java new file mode 100644 index 0000000..1e955c6 --- /dev/null +++ b/src/main/java/net/sf/webdav/fromcatalina/MD5Encoder.java @@ -0,0 +1,64 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed 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. + */ + +package net.sf.webdav.fromcatalina; + +/** + * Encode an MD5 digest into a String. + *

+ * The 128 bit MD5 hash is converted into a 32 character long String. Each + * character of the String is the hexadecimal representation of 4 bits of the + * digest. + * + * @author Remy Maucherat + * @version $Revision: 1.2 $ $Date: 2008-08-05 07:38:45 $ + */ + +public final class MD5Encoder { + + // ----------------------------------------------------- Instance Variables + + private static final char[] HEXADECIMAL = { '0', '1', '2', '3', '4', '5', + '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; + + // --------------------------------------------------------- Public Methods + + /** + * Encodes the 128 bit (16 bytes) MD5 into a 32 character String. + * + * @param binaryData + * Array containing the digest + * @return Encoded MD5, or null if encoding failed + */ + public String encode(byte[] binaryData) { + + if (binaryData.length != 16) + return null; + + char[] buffer = new char[32]; + + for (int i = 0; i < 16; i++) { + int low = (int) (binaryData[i] & 0x0f); + int high = (int) ((binaryData[i] & 0xf0) >> 4); + buffer[i * 2] = HEXADECIMAL[high]; + buffer[i * 2 + 1] = HEXADECIMAL[low]; + } + + return new String(buffer); + + } + +} diff --git a/src/main/java/net/sf/webdav/fromcatalina/RequestUtil.java b/src/main/java/net/sf/webdav/fromcatalina/RequestUtil.java new file mode 100644 index 0000000..d247e4d --- /dev/null +++ b/src/main/java/net/sf/webdav/fromcatalina/RequestUtil.java @@ -0,0 +1,510 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed 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. + */ + +package net.sf.webdav.fromcatalina; + +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Map; +import java.util.TimeZone; + +import javax.servlet.http.Cookie; + +/** + * General purpose request parsing and encoding utility methods. + * + * @author Craig R. McClanahan + * @author Tim Tye + * @version $Revision: 1.2 $ $Date: 2008-08-05 07:38:45 $ + */ + +public final class RequestUtil { + + /** + * Encode a cookie as per RFC 2109. The resulting string can be used as the + * value for a Set-Cookie header. + * + * @param cookie + * The cookie to encode. + * @return A string following RFC 2109. + */ + public static String encodeCookie(Cookie cookie) { + + StringBuffer buf = new StringBuffer(cookie.getName()); + buf.append("="); + buf.append(cookie.getValue()); + + String comment = cookie.getComment(); + if (comment != null) { + buf.append("; Comment=\""); + buf.append(comment); + buf.append("\""); + } + + String domain = cookie.getDomain(); + if (domain != null) { + buf.append("; Domain=\""); + buf.append(domain); + buf.append("\""); + } + + int age = cookie.getMaxAge(); + if (age >= 0) { + buf.append("; Max-Age=\""); + buf.append(age); + buf.append("\""); + } + + String path = cookie.getPath(); + if (path != null) { + buf.append("; Path=\""); + buf.append(path); + buf.append("\""); + } + + if (cookie.getSecure()) { + buf.append("; Secure"); + } + + int version = cookie.getVersion(); + if (version > 0) { + buf.append("; Version=\""); + buf.append(version); + buf.append("\""); + } + + return (buf.toString()); + } + + /** + * Filter the specified message string for characters that are sensitive in + * HTML. This avoids potential attacks caused by including JavaScript codes + * in the request URL that is often reported in error messages. + * + * @param message + * The message string to be filtered + */ + public static String filter(String message) { + + if (message == null) + return (null); + + char content[] = new char[message.length()]; + message.getChars(0, message.length(), content, 0); + StringBuffer result = new StringBuffer(content.length + 50); + for (int i = 0; i < content.length; i++) { + switch (content[i]) { + case '<': + result.append("<"); + break; + case '>': + result.append(">"); + break; + case '&': + result.append("&"); + break; + case '"': + result.append("""); + break; + default: + result.append(content[i]); + } + } + return (result.toString()); + + } + + /** + * Normalize a relative URI path that may have relative values ("/./", + * "/../", and so on ) it it. WARNING - This method is + * useful only for normalizing application-generated paths. It does not try + * to perform security checks for malicious input. + * + * @param path + * Relative path to be normalized + */ + public static String normalize(String path) { + + if (path == null) + return null; + + // Create a place for the normalized path + String normalized = path; + + if (normalized.equals("/.")) + return "/"; + + // Add a leading "/" if necessary + if (!normalized.startsWith("/")) + normalized = "/" + normalized; + + // Resolve occurrences of "//" in the normalized path + while (true) { + int index = normalized.indexOf("//"); + if (index < 0) + break; + normalized = normalized.substring(0, index) + + normalized.substring(index + 1); + } + + // Resolve occurrences of "/./" in the normalized path + while (true) { + int index = normalized.indexOf("/./"); + if (index < 0) + break; + normalized = normalized.substring(0, index) + + normalized.substring(index + 2); + } + + // Resolve occurrences of "/../" in the normalized path + while (true) { + int index = normalized.indexOf("/../"); + if (index < 0) + break; + if (index == 0) + return (null); // Trying to go outside our context + int index2 = normalized.lastIndexOf('/', index - 1); + normalized = normalized.substring(0, index2) + + normalized.substring(index + 3); + } + + // Return the normalized path that we have completed + return (normalized); + + } + + /** + * Parse the character encoding from the specified content type header. If + * the content type is null, or there is no explicit character encoding, + * null is returned. + * + * @param contentType + * a content type header + */ + public static String parseCharacterEncoding(String contentType) { + + if (contentType == null) + return (null); + int start = contentType.indexOf("charset="); + if (start < 0) + return (null); + String encoding = contentType.substring(start + 8); + int end = encoding.indexOf(';'); + if (end >= 0) + encoding = encoding.substring(0, end); + encoding = encoding.trim(); + if ((encoding.length() > 2) && (encoding.startsWith("\"")) + && (encoding.endsWith("\""))) + encoding = encoding.substring(1, encoding.length() - 1); + return (encoding.trim()); + + } + + /** + * Parse a cookie header into an array of cookies according to RFC 2109. + * + * @param header + * Value of an HTTP "Cookie" header + */ + public static Cookie[] parseCookieHeader(String header) { + + if ((header == null) || (header.length() < 1)) + return (new Cookie[0]); + + ArrayList cookies = new ArrayList(); + while (header.length() > 0) { + int semicolon = header.indexOf(';'); + if (semicolon < 0) + semicolon = header.length(); + if (semicolon == 0) + break; + String token = header.substring(0, semicolon); + if (semicolon < header.length()) + header = header.substring(semicolon + 1); + else + header = ""; + try { + int equals = token.indexOf('='); + if (equals > 0) { + String name = token.substring(0, equals).trim(); + String value = token.substring(equals + 1).trim(); + cookies.add(new Cookie(name, value)); + } + } catch (Throwable e) { + ; + } + } + + return ((Cookie[]) cookies.toArray(new Cookie[cookies.size()])); + + } + + /** + * Append request parameters from the specified String to the specified Map. + * It is presumed that the specified Map is not accessed from any other + * thread, so no synchronization is performed. + *

+ * IMPLEMENTATION NOTE: URL decoding is performed + * individually on the parsed name and value elements, rather than on the + * entire query string ahead of time, to properly deal with the case where + * the name or value includes an encoded "=" or "&" character that would + * otherwise be interpreted as a delimiter. + * + * @param map + * Map that accumulates the resulting parameters + * @param data + * Input string containing request parameters + * + * @exception IllegalArgumentException + * if the data is malformed + */ + public static void parseParameters(Map map, String data, + String encoding) throws UnsupportedEncodingException { + + if ((data != null) && (data.length() > 0)) { + + // use the specified encoding to extract bytes out of the + // given string so that the encoding is not lost. If an + // encoding is not specified, let it use platform default + byte[] bytes = null; + try { + if (encoding == null) { + bytes = data.getBytes(); + } else { + bytes = data.getBytes(encoding); + } + } catch (UnsupportedEncodingException uee) { + } + + parseParameters(map, bytes, encoding); + } + + } + + /** + * Decode and return the specified URL-encoded String. When the byte array + * is converted to a string, the system default character encoding is + * used... This may be different than some other servers. + * + * @param str + * The url-encoded string + * + * @exception IllegalArgumentException + * if a '%' character is not followed by a valid 2-digit hexadecimal + * number + */ + public static String URLDecode(String str) { + + return URLDecode(str, null); + + } + + /** + * Decode and return the specified URL-encoded String. + * + * @param str + * The url-encoded string + * @param enc + * The encoding to use; if null, the default encoding is used + * @exception IllegalArgumentException + * if a '%' character is not followed by a valid 2-digit hexadecimal + * number + */ + public static String URLDecode(String str, String enc) { + + if (str == null) + return (null); + + // use the specified encoding to extract bytes out of the + // given string so that the encoding is not lost. If an + // encoding is not specified, let it use platform default + byte[] bytes = null; + try { + if (enc == null) { + bytes = str.getBytes(); + } else { + bytes = str.getBytes(enc); + } + } catch (UnsupportedEncodingException uee) { + } + + return URLDecode(bytes, enc); + + } + + /** + * Decode and return the specified URL-encoded byte array. + * + * @param bytes + * The url-encoded byte array + * @exception IllegalArgumentException + * if a '%' character is not followed by a valid 2-digit hexadecimal + * number + */ + public static String URLDecode(byte[] bytes) { + return URLDecode(bytes, null); + } + + /** + * Decode and return the specified URL-encoded byte array. + * + * @param bytes + * The url-encoded byte array + * @param enc + * The encoding to use; if null, the default encoding is used + * @exception IllegalArgumentException + * if a '%' character is not followed by a valid 2-digit hexadecimal + * number + */ + public static String URLDecode(byte[] bytes, String enc) { + + if (bytes == null) + return (null); + + int len = bytes.length; + int ix = 0; + int ox = 0; + while (ix < len) { + byte b = bytes[ix++]; // Get byte to test + if (b == '+') { + b = (byte) ' '; + } else if (b == '%') { + b = (byte) ((convertHexDigit(bytes[ix++]) << 4) + convertHexDigit(bytes[ix++])); + } + bytes[ox++] = b; + } + if (enc != null) { + try { + return new String(bytes, 0, ox, enc); + } catch (Exception e) { + e.printStackTrace(); + } + } + return new String(bytes, 0, ox); + + } + + /** + * Convert a byte character value to hexidecimal digit value. + * + * @param b + * the character value byte + */ + private static byte convertHexDigit(byte b) { + if ((b >= '0') && (b <= '9')) + return (byte) (b - '0'); + if ((b >= 'a') && (b <= 'f')) + return (byte) (b - 'a' + 10); + if ((b >= 'A') && (b <= 'F')) + return (byte) (b - 'A' + 10); + return 0; + } + + /** + * Put name and value pair in map. When name already exist, add value to + * array of values. + * + * @param map + * The map to populate + * @param name + * The parameter name + * @param value + * The parameter value + */ + private static void putMapEntry(Map map, String name, + String value) { + String[] newValues = null; + String[] oldValues = (String[]) map.get(name); + if (oldValues == null) { + newValues = new String[1]; + newValues[0] = value; + } else { + newValues = new String[oldValues.length + 1]; + System.arraycopy(oldValues, 0, newValues, 0, oldValues.length); + newValues[oldValues.length] = value; + } + map.put(name, newValues); + } + + /** + * Append request parameters from the specified String to the specified Map. + * It is presumed that the specified Map is not accessed from any other + * thread, so no synchronization is performed. + *

+ * IMPLEMENTATION NOTE: URL decoding is performed + * individually on the parsed name and value elements, rather than on the + * entire query string ahead of time, to properly deal with the case where + * the name or value includes an encoded "=" or "&" character that would + * otherwise be interpreted as a delimiter. NOTE: byte array data is + * modified by this method. Caller beware. + * + * @param map + * Map that accumulates the resulting parameters + * @param data + * Input string containing request parameters + * @param encoding + * Encoding to use for converting hex + * + * @exception UnsupportedEncodingException + * if the data is malformed + */ + public static void parseParameters(Map map, byte[] data, + String encoding) throws UnsupportedEncodingException { + + if (data != null && data.length > 0) { + int ix = 0; + int ox = 0; + String key = null; + String value = null; + while (ix < data.length) { + byte c = data[ix++]; + switch ((char) c) { + case '&': + value = new String(data, 0, ox, encoding); + if (key != null) { + putMapEntry(map, key, value); + key = null; + } + ox = 0; + break; + case '=': + if (key == null) { + key = new String(data, 0, ox, encoding); + ox = 0; + } else { + data[ox++] = c; + } + break; + case '+': + data[ox++] = (byte) ' '; + break; + case '%': + data[ox++] = (byte) ((convertHexDigit(data[ix++]) << 4) + convertHexDigit(data[ix++])); + break; + default: + data[ox++] = c; + } + } + // The last value does not end in '&'. So save it now. + if (key != null) { + value = new String(data, 0, ox, encoding); + putMapEntry(map, key, value); + } + } + + } + +} diff --git a/src/main/java/net/sf/webdav/fromcatalina/URLEncoder.java b/src/main/java/net/sf/webdav/fromcatalina/URLEncoder.java new file mode 100644 index 0000000..2047b01 --- /dev/null +++ b/src/main/java/net/sf/webdav/fromcatalina/URLEncoder.java @@ -0,0 +1,107 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed 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. + */ +package net.sf.webdav.fromcatalina; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.util.BitSet; + +/** + * + * This class is very similar to the java.net.URLEncoder class. + * + * Unfortunately, with java.net.URLEncoder there is no way to specify to the + * java.net.URLEncoder which characters should NOT be encoded. + * + * This code was moved from DefaultServlet.java + * + * @author Craig R. McClanahan + * @author Remy Maucherat + */ +public class URLEncoder +{ + private static org.slf4j.Logger LOG = org.slf4j.LoggerFactory + .getLogger(URLEncoder.class); + + + protected static final char[] HEXADECIMAL = { '0', '1', '2', '3', '4', '5', + '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; + + // Array containing the safe characters set. + protected BitSet _safeCharacters = new BitSet(256); + + public URLEncoder() { + for (char i = 'a'; i <= 'z'; i++) { + addSafeCharacter(i); + } + for (char i = 'A'; i <= 'Z'; i++) { + addSafeCharacter(i); + } + for (char i = '0'; i <= '9'; i++) { + addSafeCharacter(i); + } + for(char c : "$-_.+!*'(),".toCharArray()){ + addSafeCharacter(c); + } + } + + public void addSafeCharacter(char c) { + _safeCharacters.set(c); + } + + public String encode(String path) { + int maxBytesPerChar = 10; + // int caseDiff = ('a' - 'A'); + StringBuffer rewrittenPath = new StringBuffer(path.length()); + ByteArrayOutputStream buf = new ByteArrayOutputStream(maxBytesPerChar); + OutputStreamWriter writer = null; + try { + writer = new OutputStreamWriter(buf, "UTF8"); + } catch (Exception e) { + LOG.error("Error in encode <"+path+">", e); + writer = new OutputStreamWriter(buf); + } + + for (int i = 0; i < path.length(); i++) { + int c = (int) path.charAt(i); + if (_safeCharacters.get(c)) { + rewrittenPath.append((char) c); + } else { + // convert to external encoding before hex conversion + try { + writer.write((char) c); + writer.flush(); + } catch (IOException e) { + buf.reset(); + continue; + } + byte[] ba = buf.toByteArray(); + for (int j = 0; j < ba.length; j++) { + // Converting each byte in the buffer + byte toEncode = ba[j]; + rewrittenPath.append('%'); + int low = (int) (toEncode & 0x0f); + int high = (int) ((toEncode & 0xf0) >> 4); + rewrittenPath.append(HEXADECIMAL[high]); + rewrittenPath.append(HEXADECIMAL[low]); + } + buf.reset(); + } + } + return rewrittenPath.toString(); + } +} diff --git a/src/main/java/net/sf/webdav/fromcatalina/XMLHelper.java b/src/main/java/net/sf/webdav/fromcatalina/XMLHelper.java new file mode 100644 index 0000000..0ff6159 --- /dev/null +++ b/src/main/java/net/sf/webdav/fromcatalina/XMLHelper.java @@ -0,0 +1,42 @@ +package net.sf.webdav.fromcatalina; + +import java.util.Vector; + +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +public class XMLHelper { + + public static Node findSubElement(Node parent, String localName) { + if (parent == null) { + return null; + } + Node child = parent.getFirstChild(); + while (child != null) { + if ((child.getNodeType() == Node.ELEMENT_NODE) + && (child.getLocalName().equals(localName))) { + return child; + } + child = child.getNextSibling(); + } + return null; + } + + public static Vector getPropertiesFromXML(Node propNode) { + Vector properties; + properties = new Vector(); + NodeList childList = propNode.getChildNodes(); + + for (int i = 0; i < childList.getLength(); i++) { + Node currentNode = childList.item(i); + if (currentNode.getNodeType() == Node.ELEMENT_NODE) { + String nodeName = currentNode.getLocalName(); + String namespace = currentNode.getNamespaceURI(); + // href is a live property which is handled differently + properties.addElement(namespace + ":" + nodeName); + } + } + return properties; + } + +} diff --git a/src/main/java/net/sf/webdav/fromcatalina/XMLWriter.java b/src/main/java/net/sf/webdav/fromcatalina/XMLWriter.java new file mode 100644 index 0000000..1558fcc --- /dev/null +++ b/src/main/java/net/sf/webdav/fromcatalina/XMLWriter.java @@ -0,0 +1,214 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed 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. + */ + +package net.sf.webdav.fromcatalina; + +import java.io.IOException; +import java.io.Writer; +import java.util.Iterator; +import java.util.Map; + +/** + * XMLWriter helper class. + * + * @author Remy Maucherat + */ +public class XMLWriter { + + // -------------------------------------------------------------- Constants + + /** + * Opening tag. + */ + public static final int OPENING = 0; + + /** + * Closing tag. + */ + public static final int CLOSING = 1; + + /** + * Element with no content. + */ + public static final int NO_CONTENT = 2; + + // ----------------------------------------------------- Instance Variables + + /** + * Buffer. + */ + protected StringBuffer _buffer = new StringBuffer(); + + /** + * Writer. + */ + protected Writer _writer = null; + + /** + * Namespaces to be declared in the root element + */ + protected Map _namespaces; + + /** + * Is true until the root element is written + */ + protected boolean _isRootElement = true; + + // ----------------------------------------------------------- Constructors + + /** + * Constructor. + */ + public XMLWriter(Map namespaces) { + _namespaces = namespaces; + } + + /** + * Constructor. + */ + public XMLWriter(Writer writer, Map namespaces) { + _writer = writer; + _namespaces = namespaces; + } + + // --------------------------------------------------------- Public Methods + + /** + * Retrieve generated XML. + * + * @return String containing the generated XML + */ + public String toString() { + return _buffer.toString(); + } + + /** + * Write property to the XML. + * + * @param name + * Property name + * @param value + * Property value + */ + public void writeProperty(String name, String value) { + writeElement(name, OPENING); + _buffer.append(value); + writeElement(name, CLOSING); + } + + /** + * Write property to the XML. + * + * @param name + * Property name + */ + public void writeProperty(String name) { + writeElement(name, NO_CONTENT); + } + + /** + * Write an element. + * + * @param name + * Element name + * @param type + * Element type + */ + public void writeElement(String name, int type) { + StringBuffer nsdecl = new StringBuffer(); + + if (_isRootElement) { + for (Iterator iter = _namespaces.keySet().iterator(); iter + .hasNext();) { + String fullName = (String) iter.next(); + String abbrev = (String) _namespaces.get(fullName); + nsdecl.append(" xmlns:").append(abbrev).append("=\"").append( + fullName).append("\""); + } + _isRootElement = false; + } + + int pos = name.lastIndexOf(':'); + if (pos >= 0) { + // lookup prefix for namespace + String fullns = name.substring(0, pos); + String prefix = (String) _namespaces.get(fullns); + if (prefix == null) { + // there is no prefix for this namespace + name = name.substring(pos + 1); + nsdecl.append(" xmlns=\"").append(fullns).append("\""); + } else { + // there is a prefix + name = prefix + ":" + name.substring(pos + 1); + } + } else { + throw new IllegalArgumentException( + "All XML elements must have a namespace"); + } + + switch (type) { + case OPENING: + _buffer.append("<" + name + nsdecl + ">"); + break; + case CLOSING: + _buffer.append("\n"); + break; + case NO_CONTENT: + default: + _buffer.append("<" + name + nsdecl + "/>"); + break; + } + } + + /** + * Write text. + * + * @param text + * Text to append + */ + public void writeText(String text) { + _buffer.append(text); + } + + /** + * Write data. + * + * @param data + * Data to append + */ + public void writeData(String data) { + _buffer.append(""); + } + + /** + * Write XML Header. + */ + public void writeXMLHeader() { + _buffer.append("\n"); + } + + /** + * Send data and reinitializes buffer. + */ + public void sendData() throws IOException { + if (_writer != null) { + _writer.write(_buffer.toString()); + _writer.flush(); + _buffer = new StringBuffer(); + } + } + +} diff --git a/src/main/java/net/sf/webdav/locking/IResourceLocks.java b/src/main/java/net/sf/webdav/locking/IResourceLocks.java new file mode 100644 index 0000000..24743b0 --- /dev/null +++ b/src/main/java/net/sf/webdav/locking/IResourceLocks.java @@ -0,0 +1,144 @@ +package net.sf.webdav.locking; + +import net.sf.webdav.ITransaction; +import net.sf.webdav.exceptions.LockFailedException; + +public interface IResourceLocks { + + /** + * Tries to lock the resource at "path". + * + * @param transaction + * @param path + * what resource to lock + * @param owner + * the owner of the lock + * @param exclusive + * if the lock should be exclusive (or shared) + * @param depth + * depth + * @param timeout + * Lock Duration in seconds. + * @return true if the resource at path was successfully locked, false if an + * existing lock prevented this + * @throws LockFailedException + */ + boolean lock(ITransaction transaction, String path, String owner, + boolean exclusive, int depth, int timeout, boolean temporary) + throws LockFailedException; + + /** + * Unlocks all resources at "path" (and all subfolders if existing)

that + * have the same owner. + * + * @param transaction + * @param id + * id to the resource to unlock + * @param owner + * who wants to unlock + */ + boolean unlock(ITransaction transaction, String id, String owner); + + /** + * Unlocks all resources at "path" (and all subfolders if existing)

that + * have the same owner. + * + * @param transaction + * @param path + * what resource to unlock + * @param owner + * who wants to unlock + */ + void unlockTemporaryLockedObjects(ITransaction transaction, String path, + String owner); + + /** + * Deletes LockedObjects, where timeout has reached. + * + * @param transaction + * @param temporary + * Check timeout on temporary or real locks + */ + void checkTimeouts(ITransaction transaction, boolean temporary); + + /** + * Tries to lock the resource at "path" exclusively. + * + * @param transaction + * Transaction + * @param path + * what resource to lock + * @param owner + * the owner of the lock + * @param depth + * depth + * @param timeout + * Lock Duration in seconds. + * @return true if the resource at path was successfully locked, false if an + * existing lock prevented this + * @throws LockFailedException + */ + boolean exclusiveLock(ITransaction transaction, String path, String owner, + int depth, int timeout) throws LockFailedException; + + /** + * Tries to lock the resource at "path" shared. + * + * @param transaction + * Transaction + * @param path + * what resource to lock + * @param owner + * the owner of the lock + * @param depth + * depth + * @param timeout + * Lock Duration in seconds. + * @return true if the resource at path was successfully locked, false if an + * existing lock prevented this + * @throws LockFailedException + */ + boolean sharedLock(ITransaction transaction, String path, String owner, + int depth, int timeout) throws LockFailedException; + + /** + * Gets the LockedObject corresponding to specified id. + * + * @param transaction + * @param id + * LockToken to requested resource + * @return LockedObject or null if no LockedObject on specified path exists + */ + LockedObject getLockedObjectByID(ITransaction transaction, String id); + + /** + * Gets the LockedObject on specified path. + * + * @param transaction + * @param path + * Path to requested resource + * @return LockedObject or null if no LockedObject on specified path exists + */ + LockedObject getLockedObjectByPath(ITransaction transaction, String path); + + /** + * Gets the LockedObject corresponding to specified id (locktoken). + * + * @param transaction + * @param id + * LockToken to requested resource + * @return LockedObject or null if no LockedObject on specified path exists + */ + LockedObject getTempLockedObjectByID(ITransaction transaction, String id); + + /** + * Gets the LockedObject on specified path. + * + * @param transaction + * @param path + * Path to requested resource + * @return LockedObject or null if no LockedObject on specified path exists + */ + LockedObject getTempLockedObjectByPath(ITransaction transaction, String path); + +} diff --git a/src/main/java/net/sf/webdav/locking/LockedObject.java b/src/main/java/net/sf/webdav/locking/LockedObject.java new file mode 100644 index 0000000..2dda386 --- /dev/null +++ b/src/main/java/net/sf/webdav/locking/LockedObject.java @@ -0,0 +1,422 @@ +package net.sf.webdav.locking; + +import java.util.UUID; + +/** + * a helper class for ResourceLocks, represents the Locks + * + * @author re + * + */ +public class LockedObject { + + private ResourceLocks _resourceLocks; + + private String _path; + + private String _id; + + /** + * Describing the depth of a locked collection. If the locked resource is + * not a collection, depth is 0 / doesn't matter. + */ + protected int _lockDepth; + + /** + * Describing the timeout of a locked object (ms) + */ + protected long _expiresAt; + + /** + * owner of the lock. shared locks can have multiple owners. is null if no + * owner is present + */ + // protected String[] _owner = null; + protected String[] _owner = null; + + /** + * children of that lock + */ + protected LockedObject[] _children = null; + + protected LockedObject _parent = null; + + /** + * weather the lock is exclusive or not. if owner=null the exclusive value + * doesn't matter + */ + protected boolean _exclusive = false; + + /** + * weather the lock is a write or read lock + */ + protected String _type = null; + + /** + * @param _resourceLocks + * the resourceLocks where locks are stored + * @param path + * the path to the locked object + * @param temporary + * indicates if the LockedObject should be temporary or not + */ + public LockedObject(ResourceLocks resLocks, String path, boolean temporary) { + _path = path; + _id = UUID.randomUUID().toString(); + _resourceLocks = resLocks; + + if (!temporary) { + _resourceLocks._locks.put(path, this); + _resourceLocks._locksByID.put(_id, this); + } else { + _resourceLocks._tempLocks.put(path, this); + _resourceLocks._tempLocksByID.put(_id, this); + } + _resourceLocks._cleanupCounter++; + } + + /** + * adds a new owner to a lock + * + * @param owner + * string that represents the owner + * @return true if the owner was added, false otherwise + */ + public boolean addLockedObjectOwner(String owner) { + + if (_owner == null) { + _owner = new String[1]; + } else { + + int size = _owner.length; + String[] newLockObjectOwner = new String[size + 1]; + + // check if the owner is already here (that should actually not + // happen) + for (int i = 0; i < size; i++) { + if (_owner[i].equals(owner)) { + return false; + } + } + + System.arraycopy(_owner, 0, newLockObjectOwner, 0, size); + _owner = newLockObjectOwner; + } + + _owner[_owner.length - 1] = owner; + return true; + } + + /** + * tries to remove the owner from the lock + * + * @param owner + * string that represents the owner + */ + public void removeLockedObjectOwner(String owner) { + + try { + if (_owner != null) { + int size = _owner.length; + for (int i = 0; i < size; i++) { + // check every owner if it is the requested one + if (_owner[i].equals(owner)) { + // remove the owner + size -= 1; + String[] newLockedObjectOwner = new String[size]; + for (int j = 0; j < size; j++) { + if (j < i) { + newLockedObjectOwner[j] = _owner[j]; + } else { + newLockedObjectOwner[j] = _owner[j + 1]; + } + } + _owner = newLockedObjectOwner; + + } + } + if (_owner.length == 0) { + _owner = null; + } + } + } catch (ArrayIndexOutOfBoundsException e) { + System.out.println("LockedObject.removeLockedObjectOwner()"); + System.out.println(e.toString()); + } + } + + /** + * adds a new child lock to this lock + * + * @param newChild + * new child + */ + public void addChild(LockedObject newChild) { + if (_children == null) { + _children = new LockedObject[0]; + } + int size = _children.length; + LockedObject[] newChildren = new LockedObject[size + 1]; + System.arraycopy(_children, 0, newChildren, 0, size); + newChildren[size] = newChild; + _children = newChildren; + } + + /** + * deletes this Lock object. assumes that it has no children and no owners + * (does not check this itself) + * + */ + public void removeLockedObject() { + if (this != _resourceLocks._root && !this.getPath().equals("/")) { + + int size = _parent._children.length; + for (int i = 0; i < size; i++) { + if (_parent._children[i].equals(this)) { + LockedObject[] newChildren = new LockedObject[size - 1]; + for (int i2 = 0; i2 < (size - 1); i2++) { + if (i2 < i) { + newChildren[i2] = _parent._children[i2]; + } else { + newChildren[i2] = _parent._children[i2 + 1]; + } + } + if (newChildren.length != 0) { + _parent._children = newChildren; + } else { + _parent._children = null; + } + break; + } + } + + // removing from hashtable + _resourceLocks._locksByID.remove(getID()); + _resourceLocks._locks.remove(getPath()); + + // now the garbage collector has some work to do + } + } + + /** + * deletes this Lock object. assumes that it has no children and no owners + * (does not check this itself) + * + */ + public void removeTempLockedObject() { + if (this != _resourceLocks._tempRoot) { + // removing from tree + if (_parent != null && _parent._children != null) { + int size = _parent._children.length; + for (int i = 0; i < size; i++) { + if (_parent._children[i].equals(this)) { + LockedObject[] newChildren = new LockedObject[size - 1]; + for (int i2 = 0; i2 < (size - 1); i2++) { + if (i2 < i) { + newChildren[i2] = _parent._children[i2]; + } else { + newChildren[i2] = _parent._children[i2 + 1]; + } + } + if (newChildren.length != 0) { + _parent._children = newChildren; + } else { + _parent._children = null; + } + break; + } + } + + // removing from hashtable + _resourceLocks._tempLocksByID.remove(getID()); + _resourceLocks._tempLocks.remove(getPath()); + + // now the garbage collector has some work to do + } + } + } + + /** + * checks if a lock of the given exclusivity can be placed, only considering + * children up to "depth" + * + * @param exclusive + * wheather the new lock should be exclusive + * @param depth + * the depth to which should be checked + * @return true if the lock can be placed + */ + public boolean checkLocks(boolean exclusive, int depth) { + if (checkParents(exclusive) && checkChildren(exclusive, depth)) { + return true; + } + return false; + } + + /** + * helper of checkLocks(). looks if the parents are locked + * + * @param exclusive + * wheather the new lock should be exclusive + * @return true if no locks at the parent path are forbidding a new lock + */ + private boolean checkParents(boolean exclusive) { + if (_path.equals("/")) { + return true; + } else { + if (_owner == null) { + // no owner, checking parents + return _parent != null && _parent.checkParents(exclusive); + } else { + // there already is a owner + return !(_exclusive || exclusive) + && _parent.checkParents(exclusive); + } + } + } + + /** + * helper of checkLocks(). looks if the children are locked + * + * @param exclusive + * wheather the new lock should be exclusive + * @return true if no locks at the children paths are forbidding a new lock + * @param depth + * depth + */ + private boolean checkChildren(boolean exclusive, int depth) { + if (_children == null) { + // a file + + return _owner == null || !(_exclusive || exclusive); + } else { + // a folder + + if (_owner == null) { + // no owner, checking children + + if (depth != 0) { + boolean canLock = true; + int limit = _children.length; + for (int i = 0; i < limit; i++) { + if (!_children[i].checkChildren(exclusive, depth - 1)) { + canLock = false; + } + } + return canLock; + } else { + // depth == 0 -> we don't care for children + return true; + } + } else { + // there already is a owner + return !(_exclusive || exclusive); + } + } + + } + + /** + * Sets a new timeout for the LockedObject + * + * @param timeout + */ + public void refreshTimeout(int timeout) { + _expiresAt = System.currentTimeMillis() + (timeout * 1000); + } + + /** + * Gets the timeout for the LockedObject + * + * @return timeout + */ + public long getTimeoutMillis() { + return (_expiresAt - System.currentTimeMillis()); + } + + /** + * Return true if the lock has expired. + * + * @return true if timeout has passed + */ + public boolean hasExpired() { + if (_expiresAt != 0) { + return (System.currentTimeMillis() > _expiresAt); + } else { + return true; + } + } + + /** + * Gets the LockID (locktoken) for the LockedObject + * + * @return locktoken + */ + public String getID() { + return _id; + } + + /** + * Gets the owners for the LockedObject + * + * @return owners + */ + public String[] getOwner() { + return _owner; + } + + /** + * Gets the path for the LockedObject + * + * @return path + */ + public String getPath() { + return _path; + } + + /** + * Sets the exclusivity for the LockedObject + * + * @param exclusive + */ + public void setExclusive(boolean exclusive) { + _exclusive = exclusive; + } + + /** + * Gets the exclusivity for the LockedObject + * + * @return exclusivity + */ + public boolean isExclusive() { + return _exclusive; + } + + /** + * Gets the exclusivity for the LockedObject + * + * @return exclusivity + */ + public boolean isShared() { + return !_exclusive; + } + + /** + * Gets the type of the lock + * + * @return type + */ + public String getType() { + return _type; + } + + /** + * Gets the depth of the lock + * + * @return depth + */ + public int getLockDepth() { + return _lockDepth; + } + +} diff --git a/src/main/java/net/sf/webdav/locking/ResourceLocks.java b/src/main/java/net/sf/webdav/locking/ResourceLocks.java new file mode 100644 index 0000000..1463403 --- /dev/null +++ b/src/main/java/net/sf/webdav/locking/ResourceLocks.java @@ -0,0 +1,384 @@ +/* + * Copyright 2005-2006 webdav-servlet group. + * + * Licensed 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. + */ + +package net.sf.webdav.locking; + +import java.util.Enumeration; +import java.util.Hashtable; + +import net.sf.webdav.ITransaction; +import net.sf.webdav.exceptions.LockFailedException; + +/** + * simple locking management for concurrent data access, NOT the webdav locking. + * ( could that be used instead? ) + * + * IT IS ACTUALLY USED FOR DOLOCK + * + * @author re + */ +public class ResourceLocks implements IResourceLocks { + + private static org.slf4j.Logger LOG = org.slf4j.LoggerFactory + .getLogger(ResourceLocks.class); + + /** + * after creating this much LockedObjects, a cleanup deletes unused + * LockedObjects + */ + private final int _cleanupLimit = 100000; + + protected int _cleanupCounter = 0; + + /** + * keys: path value: LockedObject from that path + */ + protected Hashtable _locks = new Hashtable(); + + /** + * keys: id value: LockedObject from that id + */ + protected Hashtable _locksByID = new Hashtable(); + + /** + * keys: path value: Temporary LockedObject from that path + */ + protected Hashtable _tempLocks = new Hashtable(); + + /** + * keys: id value: Temporary LockedObject from that id + */ + protected Hashtable _tempLocksByID = new Hashtable(); + + // REMEMBER TO REMOVE UNUSED LOCKS FROM THE HASHTABLE AS WELL + + protected LockedObject _root = null; + + protected LockedObject _tempRoot = null; + + private boolean _temporary = true; + + public ResourceLocks() { + _root = new LockedObject(this, "/", true); + _tempRoot = new LockedObject(this, "/", false); + } + + public synchronized boolean lock(ITransaction transaction, String path, + String owner, boolean exclusive, int depth, int timeout, + boolean temporary) throws LockFailedException { + + LockedObject lo = null; + + if (temporary) { + lo = generateTempLockedObjects(transaction, path); + lo._type = "read"; + } else { + lo = generateLockedObjects(transaction, path); + lo._type = "write"; + } + + if (lo.checkLocks(exclusive, depth)) { + + lo._exclusive = exclusive; + lo._lockDepth = depth; + lo._expiresAt = System.currentTimeMillis() + (timeout * 1000); + if (lo._parent != null) { + lo._parent._expiresAt = lo._expiresAt; + if (lo._parent.equals(_root)) { + LockedObject rootLo = getLockedObjectByPath(transaction, + _root.getPath()); + rootLo._expiresAt = lo._expiresAt; + } else if (lo._parent.equals(_tempRoot)) { + LockedObject tempRootLo = getTempLockedObjectByPath( + transaction, _tempRoot.getPath()); + tempRootLo._expiresAt = lo._expiresAt; + } + } + if (lo.addLockedObjectOwner(owner)) { + return true; + } else { + LOG.trace("Couldn't set owner \"" + owner + + "\" to resource at '" + path + "'"); + return false; + } + } else { + // can not lock + LOG.trace("Lock resource at " + path + " failed because" + + "\na parent or child resource is currently locked"); + return false; + } + } + + public synchronized boolean unlock(ITransaction transaction, String id, + String owner) { + + if (_locksByID.containsKey(id)) { + String path = _locksByID.get(id).getPath(); + if (_locks.containsKey(path)) { + LockedObject lo = _locks.get(path); + lo.removeLockedObjectOwner(owner); + + if (lo._children == null && lo._owner == null) + lo.removeLockedObject(); + + } else { + // there is no lock at that path. someone tried to unlock it + // anyway. could point to a problem + LOG + .trace("net.sf.webdav.locking.ResourceLocks.unlock(): no lock for path " + + path); + return false; + } + + if (_cleanupCounter > _cleanupLimit) { + _cleanupCounter = 0; + cleanLockedObjects(transaction, _root, !_temporary); + } + } + checkTimeouts(transaction, !_temporary); + + return true; + + } + + public synchronized void unlockTemporaryLockedObjects( + ITransaction transaction, String path, String owner) { + if (_tempLocks.containsKey(path)) { + LockedObject lo = _tempLocks.get(path); + lo.removeLockedObjectOwner(owner); + + } else { + // there is no lock at that path. someone tried to unlock it + // anyway. could point to a problem + LOG + .trace("net.sf.webdav.locking.ResourceLocks.unlock(): no lock for path " + + path); + } + + if (_cleanupCounter > _cleanupLimit) { + _cleanupCounter = 0; + cleanLockedObjects(transaction, _tempRoot, _temporary); + } + + checkTimeouts(transaction, _temporary); + + } + + public void checkTimeouts(ITransaction transaction, boolean temporary) { + if (!temporary) { + Enumeration lockedObjects = _locks.elements(); + while (lockedObjects.hasMoreElements()) { + LockedObject currentLockedObject = lockedObjects.nextElement(); + + if (currentLockedObject._expiresAt < System.currentTimeMillis()) { + currentLockedObject.removeLockedObject(); + } + } + } else { + Enumeration lockedObjects = _tempLocks.elements(); + while (lockedObjects.hasMoreElements()) { + LockedObject currentLockedObject = lockedObjects.nextElement(); + + if (currentLockedObject._expiresAt < System.currentTimeMillis()) { + currentLockedObject.removeTempLockedObject(); + } + } + } + + } + + public boolean exclusiveLock(ITransaction transaction, String path, + String owner, int depth, int timeout) throws LockFailedException { + return lock(transaction, path, owner, true, depth, timeout, false); + } + + public boolean sharedLock(ITransaction transaction, String path, + String owner, int depth, int timeout) throws LockFailedException { + return lock(transaction, path, owner, false, depth, timeout, false); + } + + public LockedObject getLockedObjectByID(ITransaction transaction, String id) { + if (_locksByID.containsKey(id)) { + return _locksByID.get(id); + } else { + return null; + } + } + + public LockedObject getLockedObjectByPath(ITransaction transaction, + String path) { + if (_locks.containsKey(path)) { + return (LockedObject) this._locks.get(path); + } else { + return null; + } + } + + public LockedObject getTempLockedObjectByID(ITransaction transaction, + String id) { + if (_tempLocksByID.containsKey(id)) { + return _tempLocksByID.get(id); + } else { + return null; + } + } + + public LockedObject getTempLockedObjectByPath(ITransaction transaction, + String path) { + if (_tempLocks.containsKey(path)) { + return (LockedObject) this._tempLocks.get(path); + } else { + return null; + } + } + + /** + * generates real LockedObjects for the resource at path and its parent + * folders. does not create new LockedObjects if they already exist + * + * @param transaction + * @param path + * path to the (new) LockedObject + * @return the LockedObject for path. + */ + private LockedObject generateLockedObjects(ITransaction transaction, + String path) { + if (!_locks.containsKey(path)) { + LockedObject returnObject = new LockedObject(this, path, + !_temporary); + String parentPath = getParentPath(path); + if (parentPath != null) { + LockedObject parentLockedObject = generateLockedObjects( + transaction, parentPath); + parentLockedObject.addChild(returnObject); + returnObject._parent = parentLockedObject; + } + return returnObject; + } else { + // there is already a LockedObject on the specified path + return (LockedObject) this._locks.get(path); + } + + } + + /** + * generates temporary LockedObjects for the resource at path and its parent + * folders. does not create new LockedObjects if they already exist + * + * @param transaction + * @param path + * path to the (new) LockedObject + * @return the LockedObject for path. + */ + private LockedObject generateTempLockedObjects(ITransaction transaction, + String path) { + if (!_tempLocks.containsKey(path)) { + LockedObject returnObject = new LockedObject(this, path, _temporary); + String parentPath = getParentPath(path); + if (parentPath != null) { + LockedObject parentLockedObject = generateTempLockedObjects( + transaction, parentPath); + parentLockedObject.addChild(returnObject); + returnObject._parent = parentLockedObject; + } + return returnObject; + } else { + // there is already a LockedObject on the specified path + return (LockedObject) this._tempLocks.get(path); + } + + } + + /** + * deletes unused LockedObjects and resets the counter. works recursively + * starting at the given LockedObject + * + * @param transaction + * @param lo + * LockedObject + * @param temporary + * Clean temporary or real locks + * + * @return if cleaned + */ + private boolean cleanLockedObjects(ITransaction transaction, + LockedObject lo, boolean temporary) { + + if (lo._children == null) { + if (lo._owner == null) { + if (temporary) { + lo.removeTempLockedObject(); + } else { + lo.removeLockedObject(); + } + + return true; + } else { + return false; + } + } else { + boolean canDelete = true; + int limit = lo._children.length; + for (int i = 0; i < limit; i++) { + if (!cleanLockedObjects(transaction, lo._children[i], temporary)) { + canDelete = false; + } else { + + // because the deleting shifts the array + i--; + limit--; + } + } + if (canDelete) { + if (lo._owner == null) { + if (temporary) { + lo.removeTempLockedObject(); + } else { + lo.removeLockedObject(); + } + return true; + } else { + return false; + } + } else { + return false; + } + } + } + + /** + * creates the parent path from the given path by removing the last '/' and + * everything after that + * + * @param path + * the path + * @return parent path + */ + private String getParentPath(String path) { + int slash = path.lastIndexOf('/'); + if (slash == -1) { + return null; + } else { + if (slash == 0) { + // return "root" if parent path is empty string + return "/"; + } else { + return path.substring(0, slash); + } + } + } + +} diff --git a/src/main/java/net/sf/webdav/methods/AbstractMethod.java b/src/main/java/net/sf/webdav/methods/AbstractMethod.java new file mode 100644 index 0000000..7a44bb0 --- /dev/null +++ b/src/main/java/net/sf/webdav/methods/AbstractMethod.java @@ -0,0 +1,596 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed 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. + */ + +package net.sf.webdav.methods; + +import java.io.IOException; +import java.io.Writer; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Locale; +import java.util.TimeZone; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import net.sf.webdav.IMethodExecutor; +import net.sf.webdav.ITransaction; +import net.sf.webdav.StoredObject; +import net.sf.webdav.WebdavStatus; +import net.sf.webdav.exceptions.LockFailedException; +import net.sf.webdav.fromcatalina.RequestUtil; +import net.sf.webdav.fromcatalina.URLEncoder; +import net.sf.webdav.fromcatalina.XMLWriter; +import net.sf.webdav.locking.IResourceLocks; +import net.sf.webdav.locking.LockedObject; + +public abstract class AbstractMethod implements IMethodExecutor { + + private static final ThreadLocal thLastmodifiedDateFormat = new ThreadLocal(); + private static final ThreadLocal thCreationDateFormat = new ThreadLocal(); + private static final ThreadLocal thLocalDateFormat = new ThreadLocal(); + + /** + * Array containing the safe characters set. + */ + protected static URLEncoder URL_ENCODER; + + /** + * Default depth is infite. + */ + protected static final int INFINITY = 3; + + /** + * Simple date format for the creation date ISO 8601 representation + * (partial). + */ + protected static final String CREATION_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'"; + + /** + * Simple date format for the last modified date. (RFC 822 updated by RFC + * 1123) + */ + protected static final String LAST_MODIFIED_DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss z"; + + protected static final String LOCAL_DATE_FORMAT = "dd/MM/yy' 'HH:mm:ss"; + + static { + /** + * GMT timezone - all HTTP dates are on GMT + */ + URL_ENCODER = new URLEncoder(); + URL_ENCODER.addSafeCharacter('-'); + URL_ENCODER.addSafeCharacter('_'); + URL_ENCODER.addSafeCharacter('.'); + URL_ENCODER.addSafeCharacter('*'); + URL_ENCODER.addSafeCharacter('/'); + } + + /** + * size of the io-buffer + */ + protected static int BUF_SIZE = 65536; + + /** + * Default lock timeout value. + */ + protected static final int DEFAULT_TIMEOUT = 3600; + + /** + * Maximum lock timeout. + */ + protected static final int MAX_TIMEOUT = 604800; + + /** + * Boolean value to temporary lock resources (for method locks) + */ + protected static final boolean TEMPORARY = true; + + /** + * Timeout for temporary locks + */ + protected static final int TEMP_TIMEOUT = 10; + + + public static String lastModifiedDateFormat(final Date date) { + DateFormat df = thLastmodifiedDateFormat.get(); + if( df == null ) { + df = new SimpleDateFormat(LAST_MODIFIED_DATE_FORMAT, Locale.US); + df.setTimeZone(TimeZone.getTimeZone("GMT")); + thLastmodifiedDateFormat.set( df ); + } + return df.format(date); + } + + public static String creationDateFormat(final Date date) { + DateFormat df = thCreationDateFormat.get(); + if( df == null ) { + df = new SimpleDateFormat(CREATION_DATE_FORMAT); + df.setTimeZone(TimeZone.getTimeZone("GMT")); + thCreationDateFormat.set( df ); + } + return df.format(date); + } + + public static String getLocalDateFormat(final Date date, final Locale loc) { + DateFormat df = thLocalDateFormat.get(); + if( df == null ) { + df = new SimpleDateFormat(LOCAL_DATE_FORMAT, loc); + } + return df.format(date); + } + + + /** + * Parses and normalizes the destination header. + * + * @param req + * Servlet request + * @param resp + * Servlet response + * @return destinationPath + * @throws IOException + * if an error occurs while sending response + */ + protected String parseDestinationHeader(HttpServletRequest req, + HttpServletResponse resp) throws IOException { + String destinationPath = req.getHeader("Destination"); + + if (destinationPath == null) { + resp.sendError(WebdavStatus.SC_BAD_REQUEST); + return null; + } + + // Remove url encoding from destination + destinationPath = RequestUtil.URLDecode(destinationPath, "UTF8"); + + int protocolIndex = destinationPath.indexOf("://"); + if (protocolIndex >= 0) { + // if the Destination URL contains the protocol, we can safely + // trim everything upto the first "/" character after "://" + int firstSeparator = destinationPath + .indexOf("/", protocolIndex + 4); + if (firstSeparator < 0) { + destinationPath = "/"; + } else { + destinationPath = destinationPath.substring(firstSeparator); + } + } else { + String hostName = req.getServerName(); + if ((hostName != null) && (destinationPath.startsWith(hostName))) { + destinationPath = destinationPath.substring(hostName.length()); + } + + int portIndex = destinationPath.indexOf(":"); + if (portIndex >= 0) { + destinationPath = destinationPath.substring(portIndex); + } + + if (destinationPath.startsWith(":")) { + int firstSeparator = destinationPath.indexOf("/"); + if (firstSeparator < 0) { + destinationPath = "/"; + } else { + destinationPath = destinationPath.substring(firstSeparator); + } + } + } + + // Normalize destination path (remove '.' and' ..') + destinationPath = normalize(destinationPath); + + String contextPath = req.getContextPath(); + if ((contextPath != null) && (destinationPath.startsWith(contextPath))) { + destinationPath = destinationPath.substring(contextPath.length()); + } + + String pathInfo = req.getPathInfo(); + if (pathInfo != null) { + String servletPath = req.getServletPath(); + if ((servletPath != null) + && (destinationPath.startsWith(servletPath))) { + destinationPath = destinationPath.substring(servletPath + .length()); + } + } + + return destinationPath; + } + + /** + * Return a context-relative path, beginning with a "/", that represents the + * canonical version of the specified path after ".." and "." elements are + * resolved out. If the specified path attempts to go outside the boundaries + * of the current context (i.e. too many ".." path elements are present), + * return null instead. + * + * @param path + * Path to be normalized + * @return normalized path + */ + protected String normalize(String path) { + + if (path == null) + return null; + + // Create a place for the normalized path + String normalized = path; + + if (normalized.equals("/.")) + return "/"; + + // Normalize the slashes and add leading slash if necessary + if (normalized.indexOf('\\') >= 0) + normalized = normalized.replace('\\', '/'); + if (!normalized.startsWith("/")) + normalized = "/" + normalized; + + // Resolve occurrences of "//" in the normalized path + while (true) { + int index = normalized.indexOf("//"); + if (index < 0) + break; + normalized = normalized.substring(0, index) + + normalized.substring(index + 1); + } + + // Resolve occurrences of "/./" in the normalized path + while (true) { + int index = normalized.indexOf("/./"); + if (index < 0) + break; + normalized = normalized.substring(0, index) + + normalized.substring(index + 2); + } + + // Resolve occurrences of "/../" in the normalized path + while (true) { + int index = normalized.indexOf("/../"); + if (index < 0) + break; + if (index == 0) + return (null); // Trying to go outside our context + int index2 = normalized.lastIndexOf('/', index - 1); + normalized = normalized.substring(0, index2) + + normalized.substring(index + 3); + } + + // Return the normalized path that we have completed + return (normalized); + + } + + /** + * Return the relative path associated with this servlet. + * + * @param request + * The servlet request we are processing + */ + protected String getRelativePath(HttpServletRequest request) { + + // Are we being processed by a RequestDispatcher.include()? + if (request.getAttribute("javax.servlet.include.request_uri") != null) { + String result = (String) request + .getAttribute("javax.servlet.include.path_info"); + // if (result == null) + // result = (String) request + // .getAttribute("javax.servlet.include.servlet_path"); + if ((result == null) || (result.equals(""))) + result = "/"; + return (result); + } + + // No, extract the desired path directly from the request + String result = request.getPathInfo(); + // if (result == null) { + // result = request.getServletPath(); + // } + if ((result == null) || (result.equals(""))) { + result = "/"; + } + return (result); + + } + + /** + * creates the parent path from the given path by removing the last '/' and + * everything after that + * + * @param path + * the path + * @return parent path + */ + protected String getParentPath(String path) { + int slash = path.lastIndexOf('/'); + if (slash != -1) { + return path.substring(0, slash); + } + return null; + } + + /** + * removes a / at the end of the path string, if present + * + * @param path + * the path + * @return the path without trailing / + */ + protected String getCleanPath(String path) { + + if (path.endsWith("/") && path.length() > 1) + path = path.substring(0, path.length() - 1); + return path; + } + + /** + * Return JAXP document builder instance. + */ + protected DocumentBuilder getDocumentBuilder() throws ServletException { + DocumentBuilder documentBuilder = null; + DocumentBuilderFactory documentBuilderFactory = null; + try { + documentBuilderFactory = DocumentBuilderFactory.newInstance(); + documentBuilderFactory.setNamespaceAware(true); + documentBuilder = documentBuilderFactory.newDocumentBuilder(); + } catch (ParserConfigurationException e) { + throw new ServletException("jaxp failed"); + } + return documentBuilder; + } + + /** + * reads the depth header from the request and returns it as a int + * + * @param req + * @return the depth from the depth header + */ + protected int getDepth(HttpServletRequest req) { + int depth = INFINITY; + String depthStr = req.getHeader("Depth"); + if (depthStr != null) { + if (depthStr.equals("0")) { + depth = 0; + } else if (depthStr.equals("1")) { + depth = 1; + } + } + return depth; + } + + /** + * URL rewriter. + * + * @param path + * Path which has to be rewiten + * @return the rewritten path + */ + protected String rewriteUrl(String path) { + return URL_ENCODER.encode(path); + } + + /** + * Get the ETag associated with a file. + * + * @param StoredObject + * StoredObject to get resourceLength, lastModified and a hashCode of + * StoredObject + * @return the ETag + */ + protected String getETag(StoredObject so) { + + String resourceLength = ""; + String lastModified = ""; + + if (so != null && so.isResource()) { + resourceLength = new Long(so.getResourceLength()).toString(); + lastModified = new Long(so.getLastModified().getTime()).toString(); + } + + return "W/\"" + resourceLength + "-" + lastModified + "\""; + + } + + protected String[] getLockIdFromIfHeader(HttpServletRequest req) { + String[] ids = new String[2]; + String id = req.getHeader("If"); + + if (id != null && !id.equals("")) { + if (id.indexOf(">)") == id.lastIndexOf(">)")) { + id = id.substring(id.indexOf("(<"), id.indexOf(">)")); + + if (id.indexOf("locktoken:") != -1) { + id = id.substring(id.indexOf(':') + 1); + } + ids[0] = id; + } else { + String firstId = id.substring(id.indexOf("(<"), id + .indexOf(">)")); + if (firstId.indexOf("locktoken:") != -1) { + firstId = firstId.substring(firstId.indexOf(':') + 1); + } + ids[0] = firstId; + + String secondId = id.substring(id.lastIndexOf("(<"), id + .lastIndexOf(">)")); + if (secondId.indexOf("locktoken:") != -1) { + secondId = secondId.substring(secondId.indexOf(':') + 1); + } + ids[1] = secondId; + } + + } else { + ids = null; + } + return ids; + } + + protected String getLockIdFromLockTokenHeader(HttpServletRequest req) { + String id = req.getHeader("Lock-Token"); + + if (id != null) { + id = id.substring(id.indexOf(":") + 1, id.indexOf(">")); + + } + + return id; + } + + /** + * Checks if locks on resources at the given path exists and if so checks + * the If-Header to make sure the If-Header corresponds to the locked + * resource. Returning true if no lock exists or the If-Header is + * corresponding to the locked resource + * + * @param req + * Servlet request + * @param resp + * Servlet response + * @param resourceLocks + * @param path + * path to the resource + * @param errorList + * List of error to be displayed + * @return true if no lock on a resource with the given path exists or if + * the If-Header corresponds to the locked resource + * @throws IOException + * @throws LockFailedException + */ + protected boolean checkLocks(ITransaction transaction, + HttpServletRequest req, HttpServletResponse resp, + IResourceLocks resourceLocks, String path) throws IOException, + LockFailedException { + + LockedObject loByPath = resourceLocks.getLockedObjectByPath( + transaction, path); + if (loByPath != null) { + + if (loByPath.isShared()) + return true; + + // the resource is locked + String[] lockTokens = getLockIdFromIfHeader(req); + String lockToken = null; + if (lockTokens != null) + lockToken = lockTokens[0]; + else { + return false; + } + if (lockToken != null) { + LockedObject loByIf = resourceLocks.getLockedObjectByID( + transaction, lockToken); + if (loByIf == null) { + // no locked resource to the given lockToken + return false; + } + if (!loByIf.equals(loByPath)) { + loByIf = null; + return false; + } + loByIf = null; + } + + } + loByPath = null; + return true; + } + + /** + * Send a multistatus element containing a complete error report to the + * client. If the errorList contains only one error, send the error + * directly without wrapping it in a multistatus message. + * + * @param req + * Servlet request + * @param resp + * Servlet response + * @param errorList + * List of error to be displayed + */ + protected void sendReport(HttpServletRequest req, HttpServletResponse resp, + Hashtable errorList) throws IOException { + + if (errorList.size() == 1) { + int code = errorList.elements().nextElement(); + if (WebdavStatus.getStatusText(code) != "") { + resp.sendError(code, WebdavStatus.getStatusText(code)); + } else { + resp.sendError(code); + } + } + else + { + resp.setStatus(WebdavStatus.SC_MULTI_STATUS); + + String absoluteUri = req.getRequestURI(); + // String relativePath = getRelativePath(req); + + HashMap namespaces = new HashMap(); + namespaces.put("DAV:", "D"); + + XMLWriter generatedXML = new XMLWriter(namespaces); + generatedXML.writeXMLHeader(); + + generatedXML.writeElement("DAV::multistatus", XMLWriter.OPENING); + + Enumeration pathList = errorList.keys(); + while (pathList.hasMoreElements()) { + + String errorPath = (String) pathList.nextElement(); + int errorCode = ((Integer) errorList.get(errorPath)).intValue(); + + generatedXML.writeElement("DAV::response", XMLWriter.OPENING); + + generatedXML.writeElement("DAV::href", XMLWriter.OPENING); + String toAppend = null; + if (absoluteUri.endsWith(errorPath)) { + toAppend = absoluteUri; + + } else if (absoluteUri.contains(errorPath)) { + + int endIndex = absoluteUri.indexOf(errorPath) + + errorPath.length(); + toAppend = absoluteUri.substring(0, endIndex); + } + if (!toAppend.startsWith("/") && !toAppend.startsWith("http:")) + toAppend = "/" + toAppend; + generatedXML.writeText(errorPath); + generatedXML.writeElement("DAV::href", XMLWriter.CLOSING); + generatedXML.writeElement("DAV::status", XMLWriter.OPENING); + generatedXML.writeText("HTTP/1.1 " + errorCode + " " + + WebdavStatus.getStatusText(errorCode)); + generatedXML.writeElement("DAV::status", XMLWriter.CLOSING); + + generatedXML.writeElement("DAV::response", XMLWriter.CLOSING); + + } + + generatedXML.writeElement("DAV::multistatus", XMLWriter.CLOSING); + + Writer writer = resp.getWriter(); + writer.write(generatedXML.toString()); + writer.close(); + } + } + +} diff --git a/src/main/java/net/sf/webdav/methods/DeterminableMethod.java b/src/main/java/net/sf/webdav/methods/DeterminableMethod.java new file mode 100644 index 0000000..8b7fffa --- /dev/null +++ b/src/main/java/net/sf/webdav/methods/DeterminableMethod.java @@ -0,0 +1,58 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed 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. + */ +package net.sf.webdav.methods; + +import net.sf.webdav.StoredObject; + +public abstract class DeterminableMethod extends AbstractMethod { + + private static final String NULL_RESOURCE_METHODS_ALLOWED = "OPTIONS, MKCOL, PUT, PROPFIND, LOCK, UNLOCK"; + + private static final String RESOURCE_METHODS_ALLOWED = "OPTIONS, GET, HEAD, POST, DELETE, TRACE" + + ", PROPPATCH, COPY, MOVE, LOCK, UNLOCK, PROPFIND"; + + private static final String FOLDER_METHOD_ALLOWED = ", PUT"; + + private static final String LESS_ALLOWED_METHODS = "OPTIONS, MKCOL, PUT"; + + /** + * Determines the methods normally allowed for the resource. + * + * @param so + * StoredObject representing the resource + * @return all allowed methods, separated by commas + */ + protected static String determineMethodsAllowed(StoredObject so) { + + try { + if (so != null) { + if (so.isNullResource()) { + + return NULL_RESOURCE_METHODS_ALLOWED; + + } else if (so.isFolder()) { + return RESOURCE_METHODS_ALLOWED + FOLDER_METHOD_ALLOWED; + } + // else resource + return RESOURCE_METHODS_ALLOWED; + } + } catch (Exception e) { + // we do nothing, just return less allowed methods + } + + return LESS_ALLOWED_METHODS; + } +} diff --git a/src/main/java/net/sf/webdav/methods/DoCopy.java b/src/main/java/net/sf/webdav/methods/DoCopy.java new file mode 100644 index 0000000..9b84118 --- /dev/null +++ b/src/main/java/net/sf/webdav/methods/DoCopy.java @@ -0,0 +1,348 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed 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. + */ +package net.sf.webdav.methods; + +import java.io.IOException; +import java.util.Hashtable; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import net.sf.webdav.ITransaction; +import net.sf.webdav.IWebdavStore; +import net.sf.webdav.StoredObject; +import net.sf.webdav.WebdavStatus; +import net.sf.webdav.exceptions.AccessDeniedException; +import net.sf.webdav.exceptions.LockFailedException; +import net.sf.webdav.exceptions.ObjectAlreadyExistsException; +import net.sf.webdav.exceptions.ObjectNotFoundException; +import net.sf.webdav.exceptions.WebdavException; +import net.sf.webdav.fromcatalina.RequestUtil; +import net.sf.webdav.locking.ResourceLocks; + +public class DoCopy extends AbstractMethod { + + private static org.slf4j.Logger LOG = org.slf4j.LoggerFactory + .getLogger(DoCopy.class); + + private IWebdavStore _store; + private ResourceLocks _resourceLocks; + private DoDelete _doDelete; + private boolean _readOnly; + + public DoCopy(IWebdavStore store, ResourceLocks resourceLocks, + DoDelete doDelete, boolean readOnly) { + _store = store; + _resourceLocks = resourceLocks; + _doDelete = doDelete; + _readOnly = readOnly; + } + + public void execute(ITransaction transaction, HttpServletRequest req, + HttpServletResponse resp) throws IOException, LockFailedException { + LOG.trace("-- " + this.getClass().getName()); + + String path = getRelativePath(req); + if (!_readOnly) { + + String tempLockOwner = "doCopy" + System.currentTimeMillis() + + req.toString(); + if (_resourceLocks.lock(transaction, path, tempLockOwner, false, 0, + TEMP_TIMEOUT, TEMPORARY)) { + try { + if (!copyResource(transaction, req, resp)) + return; + } catch (AccessDeniedException e) { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + } catch (ObjectAlreadyExistsException e) { + resp.sendError(WebdavStatus.SC_CONFLICT, req + .getRequestURI()); + } catch (ObjectNotFoundException e) { + resp.sendError(WebdavStatus.SC_NOT_FOUND, req + .getRequestURI()); + } catch (WebdavException e) { + resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR); + } finally { + _resourceLocks.unlockTemporaryLockedObjects(transaction, + path, tempLockOwner); + } + } else { + resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR); + } + + } else { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + } + + } + + /** + * Copy a resource. + * + * @param transaction + * indicates that the method is within the scope of a WebDAV + * transaction + * @param req + * Servlet request + * @param resp + * Servlet response + * @return true if the copy is successful + * @throws WebdavException + * if an error in the underlying store occurs + * @throws IOException + * when an error occurs while sending the response + * @throws LockFailedException + */ + public boolean copyResource(ITransaction transaction, + HttpServletRequest req, HttpServletResponse resp) + throws WebdavException, IOException, LockFailedException { + + // Parsing destination header + String destinationPath = parseDestinationHeader(req, resp); + + if (destinationPath == null) + return false; + + String path = getRelativePath(req); + + if (path.equals(destinationPath)) { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + return false; + } + + Hashtable errorList = new Hashtable(); + String parentDestinationPath = getParentPath(getCleanPath(destinationPath)); + + if (!checkLocks(transaction, req, resp, _resourceLocks, + parentDestinationPath)) { + resp.setStatus(WebdavStatus.SC_LOCKED); + return false; // parentDestination is locked + } + + if (!checkLocks(transaction, req, resp, _resourceLocks, destinationPath)) { + resp.setStatus(WebdavStatus.SC_LOCKED); + return false; // destination is locked + } + + // Parsing overwrite header + + boolean overwrite = true; + String overwriteHeader = req.getHeader("Overwrite"); + + if (overwriteHeader != null) { + overwrite = overwriteHeader.equalsIgnoreCase("T"); + } + + // Overwriting the destination + String lockOwner = "copyResource" + System.currentTimeMillis() + + req.toString(); + + if (_resourceLocks.lock(transaction, destinationPath, lockOwner, false, + 0, TEMP_TIMEOUT, TEMPORARY)) { + StoredObject copySo, destinationSo = null; + try { + copySo = _store.getStoredObject(transaction, path); + // Retrieve the resources + if (copySo == null) { + resp.sendError(HttpServletResponse.SC_NOT_FOUND); + return false; + } + + if (copySo.isNullResource()) { + String methodsAllowed = DeterminableMethod + .determineMethodsAllowed(copySo); + resp.addHeader("Allow", methodsAllowed); + resp.sendError(WebdavStatus.SC_METHOD_NOT_ALLOWED); + return false; + } + + errorList = new Hashtable(); + + destinationSo = _store.getStoredObject(transaction, + destinationPath); + + if (overwrite) { + + // Delete destination resource, if it exists + if (destinationSo != null) { + _doDelete.deleteResource(transaction, destinationPath, + errorList, req, resp); + + } else { + resp.setStatus(WebdavStatus.SC_CREATED); + } + } else { + + // If the destination exists, then it's a conflict + if (destinationSo != null) { + resp.sendError(WebdavStatus.SC_PRECONDITION_FAILED); + return false; + } else { + resp.setStatus(WebdavStatus.SC_CREATED); + } + + } + copy(transaction, path, destinationPath, errorList, req, resp); + + if (!errorList.isEmpty()) { + sendReport(req, resp, errorList); + } + + } finally { + _resourceLocks.unlockTemporaryLockedObjects(transaction, + destinationPath, lockOwner); + } + } else { + resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR); + return false; + } + return true; + + } + + /** + * copies the specified resource(s) to the specified destination. + * preconditions must be handled by the caller. Standard status codes must + * be handled by the caller. a multi status report in case of errors is + * created here. + * + * @param transaction + * indicates that the method is within the scope of a WebDAV + * transaction + * @param sourcePath + * path from where to read + * @param destinationPath + * path where to write + * @param req + * HttpServletRequest + * @param resp + * HttpServletResponse + * @throws WebdavException + * if an error in the underlying store occurs + * @throws IOException + */ + private void copy(ITransaction transaction, String sourcePath, + String destinationPath, Hashtable errorList, + HttpServletRequest req, HttpServletResponse resp) + throws WebdavException, IOException { + + StoredObject sourceSo = _store.getStoredObject(transaction, sourcePath); + if (sourceSo.isResource()) { + _store.createResource(transaction, destinationPath); + long resourceLength = _store.setResourceContent(transaction, + destinationPath, _store.getResourceContent(transaction, + sourcePath), null, null); + + if (resourceLength != -1) { + StoredObject destinationSo = _store.getStoredObject( + transaction, destinationPath); + destinationSo.setResourceLength(resourceLength); + } + + } else { + + if (sourceSo.isFolder()) { + copyFolder(transaction, sourcePath, destinationPath, errorList, + req, resp); + } else { + resp.sendError(WebdavStatus.SC_NOT_FOUND); + } + } + } + + /** + * helper method of copy() recursively copies the FOLDER at source path to + * destination path + * + * @param transaction + * indicates that the method is within the scope of a WebDAV + * transaction + * @param sourcePath + * where to read + * @param destinationPath + * where to write + * @param errorList + * all errors that ocurred + * @param req + * HttpServletRequest + * @param resp + * HttpServletResponse + * @throws WebdavException + * if an error in the underlying store occurs + */ + private void copyFolder(ITransaction transaction, String sourcePath, + String destinationPath, Hashtable errorList, + HttpServletRequest req, HttpServletResponse resp) + throws WebdavException { + + _store.createFolder(transaction, destinationPath); + boolean infiniteDepth = true; + String depth = req.getHeader("Depth"); + if (depth != null) { + if (depth.equals("0")) { + infiniteDepth = false; + } + } + if (infiniteDepth) { + String[] children = _store + .getChildrenNames(transaction, sourcePath); + children = children == null ? new String[] {} : children; + + StoredObject childSo; + for (int i = children.length - 1; i >= 0; i--) { + children[i] = "/" + children[i]; + try { + childSo = _store.getStoredObject(transaction, + (sourcePath + children[i])); + if (childSo.isResource()) { + _store.createResource(transaction, destinationPath + + children[i]); + long resourceLength = _store.setResourceContent( + transaction, destinationPath + children[i], + _store.getResourceContent(transaction, + sourcePath + children[i]), null, null); + + if (resourceLength != -1) { + StoredObject destinationSo = _store + .getStoredObject(transaction, + destinationPath + children[i]); + destinationSo.setResourceLength(resourceLength); + } + + } else { + copyFolder(transaction, sourcePath + children[i], + destinationPath + children[i], errorList, req, + resp); + } + } catch (AccessDeniedException e) { + errorList.put(destinationPath + children[i], new Integer( + WebdavStatus.SC_FORBIDDEN)); + } catch (ObjectNotFoundException e) { + errorList.put(destinationPath + children[i], new Integer( + WebdavStatus.SC_NOT_FOUND)); + } catch (ObjectAlreadyExistsException e) { + errorList.put(destinationPath + children[i], new Integer( + WebdavStatus.SC_CONFLICT)); + } catch (WebdavException e) { + errorList.put(destinationPath + children[i], new Integer( + WebdavStatus.SC_INTERNAL_SERVER_ERROR)); + } + } + } + } + + +} diff --git a/src/main/java/net/sf/webdav/methods/DoDelete.java b/src/main/java/net/sf/webdav/methods/DoDelete.java new file mode 100644 index 0000000..b1f2484 --- /dev/null +++ b/src/main/java/net/sf/webdav/methods/DoDelete.java @@ -0,0 +1,206 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed 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. + */ +package net.sf.webdav.methods; + +import java.io.IOException; +import java.util.Hashtable; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import net.sf.webdav.ITransaction; +import net.sf.webdav.IWebdavStore; +import net.sf.webdav.StoredObject; +import net.sf.webdav.WebdavStatus; +import net.sf.webdav.exceptions.AccessDeniedException; +import net.sf.webdav.exceptions.LockFailedException; +import net.sf.webdav.exceptions.ObjectAlreadyExistsException; +import net.sf.webdav.exceptions.ObjectNotFoundException; +import net.sf.webdav.exceptions.WebdavException; +import net.sf.webdav.locking.ResourceLocks; + +public class DoDelete extends AbstractMethod { + + private static org.slf4j.Logger LOG = org.slf4j.LoggerFactory + .getLogger(DoDelete.class); + + private IWebdavStore _store; + private ResourceLocks _resourceLocks; + private boolean _readOnly; + + public DoDelete(IWebdavStore store, ResourceLocks resourceLocks, + boolean readOnly) { + _store = store; + _resourceLocks = resourceLocks; + _readOnly = readOnly; + } + + public void execute(ITransaction transaction, HttpServletRequest req, + HttpServletResponse resp) throws IOException, LockFailedException { + LOG.trace("-- " + this.getClass().getName()); + + if (!_readOnly) { + String path = getRelativePath(req); + String parentPath = getParentPath(getCleanPath(path)); + + Hashtable errorList = new Hashtable(); + + if (!checkLocks(transaction, req, resp, _resourceLocks, parentPath)) { + resp.setStatus(WebdavStatus.SC_LOCKED); + return; // parent is locked + } + + if (!checkLocks(transaction, req, resp, _resourceLocks, path)) { + resp.setStatus(WebdavStatus.SC_LOCKED); + return; // resource is locked + } + + String tempLockOwner = "doDelete" + System.currentTimeMillis() + + req.toString(); + if (_resourceLocks.lock(transaction, path, tempLockOwner, false, 0, + TEMP_TIMEOUT, TEMPORARY)) { + try { + errorList = new Hashtable(); + deleteResource(transaction, path, errorList, req, resp); + if (!errorList.isEmpty()) { + sendReport(req, resp, errorList); + } + } catch (AccessDeniedException e) { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + } catch (ObjectAlreadyExistsException e) { + resp.sendError(WebdavStatus.SC_NOT_FOUND, req + .getRequestURI()); + } catch (WebdavException e) { + resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR); + } finally { + _resourceLocks.unlockTemporaryLockedObjects(transaction, + path, tempLockOwner); + } + } else { + resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR); + } + } else { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + } + + } + + /** + * deletes the recources at "path" + * + * @param transaction + * indicates that the method is within the scope of a WebDAV + * transaction + * @param path + * the folder to be deleted + * @param errorList + * all errors that ocurred + * @param req + * HttpServletRequest + * @param resp + * HttpServletResponse + * @throws WebdavException + * if an error in the underlying store occurs + * @throws IOException + * when an error occurs while sending the response + */ + public void deleteResource(ITransaction transaction, String path, + Hashtable errorList, HttpServletRequest req, + HttpServletResponse resp) throws IOException, WebdavException { + + resp.setStatus(WebdavStatus.SC_NO_CONTENT); + + if (!_readOnly) { + + StoredObject so = _store.getStoredObject(transaction, path); + if (so != null) { + + if (so.isResource()) { + _store.removeObject(transaction, path); + } else { + if (so.isFolder()) { + deleteFolder(transaction, path, errorList, req, resp); + _store.removeObject(transaction, path); + } else { + resp.sendError(WebdavStatus.SC_NOT_FOUND); + } + } + } else { + resp.sendError(WebdavStatus.SC_NOT_FOUND); + } + so = null; + + } else { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + } + } + + /** + * + * helper method of deleteResource() deletes the folder and all of its + * contents + * + * @param transaction + * indicates that the method is within the scope of a WebDAV + * transaction + * @param path + * the folder to be deleted + * @param errorList + * all errors that ocurred + * @param req + * HttpServletRequest + * @param resp + * HttpServletResponse + * @throws WebdavException + * if an error in the underlying store occurs + */ + private void deleteFolder(ITransaction transaction, String path, + Hashtable errorList, HttpServletRequest req, + HttpServletResponse resp) throws WebdavException { + + String[] children = _store.getChildrenNames(transaction, path); + children = children == null ? new String[] {} : children; + StoredObject so = null; + for (int i = children.length - 1; i >= 0; i--) { + children[i] = "/" + children[i]; + try { + so = _store.getStoredObject(transaction, path + children[i]); + if (so.isResource()) { + _store.removeObject(transaction, path + children[i]); + + } else { + deleteFolder(transaction, path + children[i], errorList, + req, resp); + + _store.removeObject(transaction, path + children[i]); + + } + } catch (AccessDeniedException e) { + errorList.put(path + children[i], new Integer( + WebdavStatus.SC_FORBIDDEN)); + } catch (ObjectNotFoundException e) { + errorList.put(path + children[i], new Integer( + WebdavStatus.SC_NOT_FOUND)); + } catch (WebdavException e) { + errorList.put(path + children[i], new Integer( + WebdavStatus.SC_INTERNAL_SERVER_ERROR)); + } + } + so = null; + + } + +} diff --git a/src/main/java/net/sf/webdav/methods/DoGet.java b/src/main/java/net/sf/webdav/methods/DoGet.java new file mode 100644 index 0000000..dd75a9c --- /dev/null +++ b/src/main/java/net/sf/webdav/methods/DoGet.java @@ -0,0 +1,307 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed 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. + */ +package net.sf.webdav.methods; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Locale; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import net.sf.webdav.IMimeTyper; +import net.sf.webdav.ITransaction; +import net.sf.webdav.IWebdavStore; +import net.sf.webdav.StoredObject; +import net.sf.webdav.WebdavStatus; +import net.sf.webdav.locking.ResourceLocks; + +public class DoGet extends DoHead { + + private static org.slf4j.Logger LOG = org.slf4j.LoggerFactory + .getLogger(DoGet.class); + + public DoGet(IWebdavStore store, String dftIndexFile, String insteadOf404, + ResourceLocks resourceLocks, IMimeTyper mimeTyper, + int contentLengthHeader) { + super(store, dftIndexFile, insteadOf404, resourceLocks, mimeTyper, + contentLengthHeader); + + } + + protected void doBody(ITransaction transaction, HttpServletResponse resp, + String path) { + + try { + StoredObject so = _store.getStoredObject(transaction, path); + if (so.isNullResource()) { + String methodsAllowed = DeterminableMethod + .determineMethodsAllowed(so); + resp.addHeader("Allow", methodsAllowed); + resp.sendError(WebdavStatus.SC_METHOD_NOT_ALLOWED); + return; + } + OutputStream out = resp.getOutputStream(); + InputStream in = _store.getResourceContent(transaction, path); + try { + int read = -1; + byte[] copyBuffer = new byte[BUF_SIZE]; + + while ((read = in.read(copyBuffer, 0, copyBuffer.length)) != -1) { + out.write(copyBuffer, 0, read); + } + } finally { + // flushing causes a IOE if a file is opened on the webserver + // client disconnected before server finished sending response + try { + in.close(); + } catch (Exception e) { + LOG.warn("Closing InputStream causes Exception!\n" + + e.toString()); + } + try { + out.flush(); + out.close(); + } catch (Exception e) { + LOG.warn("Flushing OutputStream causes Exception!\n" + + e.toString()); + } + } + } catch (Exception e) { + LOG.trace(e.toString()); + } + } + + protected void folderBody(ITransaction transaction, String path, + HttpServletResponse resp, HttpServletRequest req) + throws IOException { + + StoredObject so = _store.getStoredObject(transaction, path); + if (so == null) { + resp.sendError(HttpServletResponse.SC_NOT_FOUND, req + .getRequestURI()); + } else { + + if (so.isNullResource()) { + String methodsAllowed = DeterminableMethod + .determineMethodsAllowed(so); + resp.addHeader("Allow", methodsAllowed); + resp.sendError(WebdavStatus.SC_METHOD_NOT_ALLOWED); + return; + } + + if (so.isFolder()) { + // TODO some folder response (for browsers, DAV tools + // use propfind) in html? + Locale locale = req.getLocale(); + DateFormat shortDF= getDateTimeFormat(req.getLocale()); + resp.setContentType("text/html"); + resp.setCharacterEncoding("UTF8"); + OutputStream out = resp.getOutputStream(); + String[] children = _store.getChildrenNames(transaction, path); + // Make sure it's not null + children = children == null ? new String[] {} : children; + // Sort by name + Arrays.sort(children); + StringBuilder childrenTemp = new StringBuilder(); + childrenTemp.append("Content of folder"); + childrenTemp.append(path); + childrenTemp.append(""); + childrenTemp.append(""); + childrenTemp.append(getHeader(transaction, path, resp, req)); + childrenTemp.append(""); + childrenTemp.append(""); + childrenTemp.append(""); + childrenTemp.append(""); + boolean isEven= false; + for (String child : children) + { + isEven= !isEven; + childrenTemp.append(""); + childrenTemp.append(""); + if (obj != null && obj.isFolder()) + { + childrenTemp.append(""); + } + else + { + childrenTemp.append(""); + } + if (obj != null && obj.getCreationDate() != null) + { + childrenTemp.append(""); + } + else + { + childrenTemp.append(""); + } + if (obj != null && obj.getLastModified() != null) + { + childrenTemp.append(""); + } + else + { + childrenTemp.append(""); + } + childrenTemp.append(""); + } + childrenTemp.append("
NameSizeCreatedModified
Parent
"); + childrenTemp.append(""); + childrenTemp.append(child); + childrenTemp.append("Folder"); + if (obj != null ) + { + childrenTemp.append(obj.getResourceLength()); + } + else + { + childrenTemp.append("Unknown"); + } + childrenTemp.append(" Bytes"); + childrenTemp.append(shortDF.format(obj.getCreationDate())); + childrenTemp.append(""); + childrenTemp.append(shortDF.format(obj.getLastModified())); + childrenTemp.append("
"); + childrenTemp.append(getFooter(transaction, path, resp, req)); + childrenTemp.append(""); + out.write(childrenTemp.toString().getBytes("UTF-8")); + } + } + } + + /** + * Return the CSS styles used to display the HTML representation + * of the webdav content. + * + * @return + */ + protected String getCSS() + { + // The default styles to use + String retVal= "body {\n"+ + " font-family: Arial, Helvetica, sans-serif;\n"+ + "}\n"+ + "h1 {\n"+ + " font-size: 1.5em;\n"+ + "}\n"+ + "th {\n"+ + " background-color: #9DACBF;\n"+ + "}\n"+ + "table {\n"+ + " border-top-style: solid;\n"+ + " border-right-style: solid;\n"+ + " border-bottom-style: solid;\n"+ + " border-left-style: solid;\n"+ + "}\n"+ + "td {\n"+ + " margin: 0px;\n"+ + " padding-top: 2px;\n"+ + " padding-right: 5px;\n"+ + " padding-bottom: 2px;\n"+ + " padding-left: 5px;\n"+ + "}\n"+ + "tr.even {\n"+ + " background-color: #CCCCCC;\n"+ + "}\n"+ + "tr.odd {\n"+ + " background-color: #FFFFFF;\n"+ + "}\n"+ + ""; + try + { + // Try loading one via class loader and use that one instead + ClassLoader cl = getClass().getClassLoader(); + InputStream iStream = cl.getResourceAsStream("webdav.css"); + if(iStream != null) + { + // Found css via class loader, use that one + StringBuffer out = new StringBuffer(); + byte[] b = new byte[4096]; + for (int n; (n = iStream.read(b)) != -1;) + { + out.append(new String(b, 0, n)); +} + retVal= out.toString(); + } + } + catch (Exception ex) + { + LOG.error("Error in reading webdav.css", ex); + } + + return retVal; + } + + /** + * Return the header to be displayed in front of the folder content + * + * @param transaction + * @param path + * @param resp + * @param req + * @return + */ + protected String getHeader(ITransaction transaction, String path, + HttpServletResponse resp, HttpServletRequest req) + { + return "

Content of folder "+path+"

"; + } + + /** + * Return the footer to be displayed after the folder content + * + * @param transaction + * @param path + * @param resp + * @param req + * @return + */ + protected String getFooter(ITransaction transaction, String path, + HttpServletResponse resp, HttpServletRequest req) + { + return ""; + } + + /** + * Return this as the Date/Time format for displaying Creation + Modification dates + * + * @param browserLocale + * @return DateFormat used to display creation and modification dates + */ + protected DateFormat getDateTimeFormat(Locale browserLocale) + { + return SimpleDateFormat.getDateTimeInstance(SimpleDateFormat.SHORT, SimpleDateFormat.MEDIUM, browserLocale); + } + } diff --git a/src/main/java/net/sf/webdav/methods/DoHead.java b/src/main/java/net/sf/webdav/methods/DoHead.java new file mode 100644 index 0000000..7775cd6 --- /dev/null +++ b/src/main/java/net/sf/webdav/methods/DoHead.java @@ -0,0 +1,194 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed 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. + */ +package net.sf.webdav.methods; + +import java.io.IOException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import net.sf.webdav.IMimeTyper; +import net.sf.webdav.StoredObject; +import net.sf.webdav.ITransaction; +import net.sf.webdav.WebdavStatus; +import net.sf.webdav.IWebdavStore; +import net.sf.webdav.exceptions.AccessDeniedException; +import net.sf.webdav.exceptions.LockFailedException; +import net.sf.webdav.exceptions.ObjectAlreadyExistsException; +import net.sf.webdav.exceptions.WebdavException; +import net.sf.webdav.locking.ResourceLocks; + +public class DoHead extends AbstractMethod { + + protected String _dftIndexFile; + protected IWebdavStore _store; + protected String _insteadOf404; + protected ResourceLocks _resourceLocks; + protected IMimeTyper _mimeTyper; + protected int _contentLength; + + private static org.slf4j.Logger LOG = org.slf4j.LoggerFactory + .getLogger(DoHead.class); + + public DoHead(IWebdavStore store, String dftIndexFile, String insteadOf404, + ResourceLocks resourceLocks, IMimeTyper mimeTyper, + int contentLengthHeader) { + _store = store; + _dftIndexFile = dftIndexFile; + _insteadOf404 = insteadOf404; + _resourceLocks = resourceLocks; + _mimeTyper = mimeTyper; + _contentLength = contentLengthHeader; + } + + public void execute(ITransaction transaction, HttpServletRequest req, + HttpServletResponse resp) throws IOException, LockFailedException { + + // determines if the uri exists. + + boolean bUriExists = false; + + String path = getRelativePath(req); + LOG.trace("-- " + this.getClass().getName()); + + StoredObject so; + try { + so = _store.getStoredObject(transaction, path); + if (so == null) { + if (this._insteadOf404 != null && !_insteadOf404.trim().equals("")) { + path = this._insteadOf404; + so = _store.getStoredObject(transaction, this._insteadOf404); + } + } else + bUriExists = true; + } catch (AccessDeniedException e) { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + return; + } + + if (so != null) { + if (so.isFolder()) { + if (_dftIndexFile != null && !_dftIndexFile.trim().equals("")) { + resp.sendRedirect(resp.encodeRedirectURL(req + .getRequestURI() + + this._dftIndexFile)); + return; + } + } else if (so.isNullResource()) { + String methodsAllowed = DeterminableMethod + .determineMethodsAllowed(so); + resp.addHeader("Allow", methodsAllowed); + resp.sendError(WebdavStatus.SC_METHOD_NOT_ALLOWED); + return; + } + + String tempLockOwner = "doGet" + System.currentTimeMillis() + + req.toString(); + + if (_resourceLocks.lock(transaction, path, tempLockOwner, false, 0, + TEMP_TIMEOUT, TEMPORARY)) { + try { + + String eTagMatch = req.getHeader("If-None-Match"); + if (eTagMatch != null) { + if (eTagMatch.equals(getETag(so))) { + resp.setStatus(WebdavStatus.SC_NOT_MODIFIED); + return; + } + } + + if (so.isResource()) { + // path points to a file but ends with / or \ + if (path.endsWith("/") || (path.endsWith("\\"))) { + resp.sendError(HttpServletResponse.SC_NOT_FOUND, + req.getRequestURI()); + } else { + + // setting headers + long lastModified = so.getLastModified().getTime(); + resp.setDateHeader("last-modified", lastModified); + + String eTag = getETag(so); + resp.addHeader("ETag", eTag); + + long resourceLength = so.getResourceLength(); + + if (_contentLength == 1) { + if (resourceLength > 0) { + if (resourceLength <= Integer.MAX_VALUE) { + resp + .setContentLength((int) resourceLength); + } else { + resp.setHeader("content-length", "" + + resourceLength); + // is "content-length" the right header? + // is long a valid format? + } + } + } + + String mimeType = _mimeTyper.getMimeType(transaction, path); + if (mimeType != null) { + resp.setContentType(mimeType); + } else { + int lastSlash = path.replace('\\', '/') + .lastIndexOf('/'); + int lastDot = path.indexOf(".", lastSlash); + if (lastDot == -1) { + resp.setContentType("text/html"); + } + } + + doBody(transaction, resp, path); + } + } else { + folderBody(transaction, path, resp, req); + } + } catch (AccessDeniedException e) { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + } catch (ObjectAlreadyExistsException e) { + resp.sendError(WebdavStatus.SC_NOT_FOUND, req + .getRequestURI()); + } catch (WebdavException e) { + resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR); + } finally { + _resourceLocks.unlockTemporaryLockedObjects(transaction, + path, tempLockOwner); + } + } else { + resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR); + } + } else { + folderBody(transaction, path, resp, req); + } + + if (!bUriExists) + resp.setStatus(WebdavStatus.SC_NOT_FOUND); + + } + + protected void folderBody(ITransaction transaction, String path, + HttpServletResponse resp, HttpServletRequest req) + throws IOException { + // no body for HEAD + } + + protected void doBody(ITransaction transaction, HttpServletResponse resp, + String path) throws IOException { + // no body for HEAD + } +} diff --git a/src/main/java/net/sf/webdav/methods/DoLock.java b/src/main/java/net/sf/webdav/methods/DoLock.java new file mode 100644 index 0000000..d5af92f --- /dev/null +++ b/src/main/java/net/sf/webdav/methods/DoLock.java @@ -0,0 +1,593 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed 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. + */ +package net.sf.webdav.methods; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Hashtable; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.xml.parsers.DocumentBuilder; + +import net.sf.webdav.ITransaction; +import net.sf.webdav.IWebdavStore; +import net.sf.webdav.StoredObject; +import net.sf.webdav.WebdavStatus; +import net.sf.webdav.exceptions.LockFailedException; +import net.sf.webdav.exceptions.WebdavException; +import net.sf.webdav.fromcatalina.XMLWriter; +import net.sf.webdav.locking.IResourceLocks; +import net.sf.webdav.locking.LockedObject; + +import org.w3c.dom.DOMException; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +public class DoLock extends AbstractMethod { + + private static org.slf4j.Logger LOG = org.slf4j.LoggerFactory + .getLogger(DoLock.class); + + private IWebdavStore _store; + private IResourceLocks _resourceLocks; + private boolean _readOnly; + + private boolean _macLockRequest = false; + + private boolean _exclusive = false; + private String _type = null; + private String _lockOwner = null; + + private String _path = null; + private String _parentPath = null; + + private String _userAgent = null; + + public DoLock(IWebdavStore store, IResourceLocks resourceLocks, + boolean readOnly) { + _store = store; + _resourceLocks = resourceLocks; + _readOnly = readOnly; + } + + public void execute(ITransaction transaction, HttpServletRequest req, + HttpServletResponse resp) throws IOException, LockFailedException { + LOG.trace("-- " + this.getClass().getName()); + + if (_readOnly) { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + return; + } else { + _path = getRelativePath(req); + _parentPath = getParentPath(getCleanPath(_path)); + + Hashtable errorList = new Hashtable(); + + if (!checkLocks(transaction, req, resp, _resourceLocks, _path)) { + resp.setStatus(WebdavStatus.SC_LOCKED); + return; // resource is locked + } + + if (!checkLocks(transaction, req, resp, _resourceLocks, _parentPath)) { + resp.setStatus(WebdavStatus.SC_LOCKED); + return; // parent is locked + } + + // Mac OS Finder (whether 10.4.x or 10.5) can't store files + // because executing a LOCK without lock information causes a + // SC_BAD_REQUEST + _userAgent = req.getHeader("User-Agent"); + if (_userAgent != null && _userAgent.indexOf("Darwin") != -1) { + _macLockRequest = true; + + String timeString = new Long(System.currentTimeMillis()) + .toString(); + _lockOwner = _userAgent.concat(timeString); + } + + String tempLockOwner = "doLock" + System.currentTimeMillis() + + req.toString(); + if (_resourceLocks.lock(transaction, _path, tempLockOwner, false, + 0, TEMP_TIMEOUT, TEMPORARY)) { + try { + if (req.getHeader("If") != null) { + doRefreshLock(transaction, req, resp); + } else { + doLock(transaction, req, resp); + } + } catch (LockFailedException e) { + resp.sendError(WebdavStatus.SC_LOCKED); + LOG.error("Lockfailed exception", e); + } finally { + _resourceLocks.unlockTemporaryLockedObjects(transaction, + _path, tempLockOwner); + } + } + } + } + + private void doLock(ITransaction transaction, HttpServletRequest req, + HttpServletResponse resp) throws IOException, LockFailedException { + + StoredObject so = _store.getStoredObject(transaction, _path); + + if (so != null) { + doLocking(transaction, req, resp); + } else { + // resource doesn't exist, null-resource lock + doNullResourceLock(transaction, req, resp); + } + + so = null; + _exclusive = false; + _type = null; + _lockOwner = null; + + } + + private void doLocking(ITransaction transaction, HttpServletRequest req, + HttpServletResponse resp) throws IOException { + + // Tests if LockObject on requested path exists, and if so, tests + // exclusivity + LockedObject lo = _resourceLocks.getLockedObjectByPath(transaction, + _path); + if (lo != null) { + if (lo.isExclusive()) { + sendLockFailError(transaction, req, resp); + return; + } + } + try { + // Thats the locking itself + executeLock(transaction, req, resp); + + } catch (ServletException e) { + resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR); + LOG.trace(e.toString()); + } catch (LockFailedException e) { + sendLockFailError(transaction, req, resp); + } finally { + lo = null; + } + + } + + private void doNullResourceLock(ITransaction transaction, + HttpServletRequest req, HttpServletResponse resp) + throws IOException { + + StoredObject parentSo, nullSo = null; + + try { + parentSo = _store.getStoredObject(transaction, _parentPath); + if (_parentPath != null && parentSo == null) { + _store.createFolder(transaction, _parentPath); + } else if (_parentPath != null && parentSo != null + && parentSo.isResource()) { + resp.sendError(WebdavStatus.SC_PRECONDITION_FAILED); + return; + } + + nullSo = _store.getStoredObject(transaction, _path); + if (nullSo == null) { + // resource doesn't exist + _store.createResource(transaction, _path); + + // Transmit expects 204 response-code, not 201 + if (_userAgent != null && _userAgent.indexOf("Transmit") != -1) { + LOG + .trace("DoLock.execute() : do workaround for user agent '" + + _userAgent + "'"); + resp.setStatus(WebdavStatus.SC_NO_CONTENT); + } else { + resp.setStatus(WebdavStatus.SC_CREATED); + } + + } else { + // resource already exists, could not execute null-resource lock + sendLockFailError(transaction, req, resp); + return; + } + nullSo = _store.getStoredObject(transaction, _path); + // define the newly created resource as null-resource + nullSo.setNullResource(true); + + // Thats the locking itself + executeLock(transaction, req, resp); + + } catch (LockFailedException e) { + sendLockFailError(transaction, req, resp); + } catch (WebdavException e) { + resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR); + LOG.error("Webdav exception", e); + } catch (ServletException e) { + resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR); + LOG.error("Servlet exception", e); + } finally { + parentSo = null; + nullSo = null; + } + } + + private void doRefreshLock(ITransaction transaction, + HttpServletRequest req, HttpServletResponse resp) + throws IOException, LockFailedException { + + String[] lockTokens = getLockIdFromIfHeader(req); + String lockToken = null; + if (lockTokens != null) + lockToken = lockTokens[0]; + + if (lockToken != null) { + // Getting LockObject of specified lockToken in If header + LockedObject refreshLo = _resourceLocks.getLockedObjectByID( + transaction, lockToken); + if (refreshLo != null) { + int timeout = getTimeout(transaction, req); + + refreshLo.refreshTimeout(timeout); + // sending success response + generateXMLReport(transaction, resp, refreshLo); + + refreshLo = null; + } else { + // no LockObject to given lockToken + resp.sendError(WebdavStatus.SC_PRECONDITION_FAILED); + } + + } else { + resp.sendError(WebdavStatus.SC_PRECONDITION_FAILED); + } + } + + // ------------------------------------------------- helper methods + + /** + * Executes the LOCK + */ + private void executeLock(ITransaction transaction, HttpServletRequest req, + HttpServletResponse resp) throws LockFailedException, IOException, + ServletException { + + // Mac OS lock request workaround + if (_macLockRequest) { + LOG.trace("DoLock.execute() : do workaround for user agent '" + + _userAgent + "'"); + + doMacLockRequestWorkaround(transaction, req, resp); + } else { + // Getting LockInformation from request + if (getLockInformation(transaction, req, resp)) { + int depth = getDepth(req); + int lockDuration = getTimeout(transaction, req); + + boolean lockSuccess = false; + if (_exclusive) { + lockSuccess = _resourceLocks.exclusiveLock(transaction, + _path, _lockOwner, depth, lockDuration); + } else { + lockSuccess = _resourceLocks.sharedLock(transaction, _path, + _lockOwner, depth, lockDuration); + } + + if (lockSuccess) { + // Locks successfully placed - return information about + LockedObject lo = _resourceLocks.getLockedObjectByPath( + transaction, _path); + if (lo != null) { + generateXMLReport(transaction, resp, lo); + } else { + resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR); + } + } else { + sendLockFailError(transaction, req, resp); + + throw new LockFailedException(); + } + } else { + // information for LOCK could not be read successfully + resp.setContentType("text/xml; charset=UTF-8"); + resp.sendError(WebdavStatus.SC_BAD_REQUEST); + } + } + } + + /** + * Tries to get the LockInformation from LOCK request + */ + private boolean getLockInformation(ITransaction transaction, + HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + Node lockInfoNode = null; + DocumentBuilder documentBuilder = null; + + documentBuilder = getDocumentBuilder(); + try { + Document document = documentBuilder.parse(new InputSource(req + .getInputStream())); + + // Get the root element of the document + Element rootElement = document.getDocumentElement(); + + lockInfoNode = rootElement; + + if (lockInfoNode != null) { + NodeList childList = lockInfoNode.getChildNodes(); + Node lockScopeNode = null; + Node lockTypeNode = null; + Node lockOwnerNode = null; + + Node currentNode = null; + String nodeName = null; + + for (int i = 0; i < childList.getLength(); i++) { + currentNode = childList.item(i); + + if (currentNode.getNodeType() == Node.ELEMENT_NODE + || currentNode.getNodeType() == Node.TEXT_NODE) { + + nodeName = currentNode.getNodeName(); + + if (nodeName.endsWith("locktype")) { + lockTypeNode = currentNode; + } + if (nodeName.endsWith("lockscope")) { + lockScopeNode = currentNode; + } + if (nodeName.endsWith("owner")) { + lockOwnerNode = currentNode; + } + } else { + return false; + } + } + + if (lockScopeNode != null) { + String scope = null; + childList = lockScopeNode.getChildNodes(); + for (int i = 0; i < childList.getLength(); i++) { + currentNode = childList.item(i); + + if (currentNode.getNodeType() == Node.ELEMENT_NODE) { + scope = currentNode.getNodeName(); + + if (scope.endsWith("exclusive")) { + _exclusive = true; + } else if (scope.equals("shared")) { + _exclusive = false; + } + } + } + if (scope == null) { + return false; + } + + } else { + return false; + } + + if (lockTypeNode != null) { + childList = lockTypeNode.getChildNodes(); + for (int i = 0; i < childList.getLength(); i++) { + currentNode = childList.item(i); + + if (currentNode.getNodeType() == Node.ELEMENT_NODE) { + _type = currentNode.getNodeName(); + + if (_type.endsWith("write")) { + _type = "write"; + } else if (_type.equals("read")) { + _type = "read"; + } + } + } + if (_type == null) { + return false; + } + } else { + return false; + } + + if (lockOwnerNode != null) { + childList = lockOwnerNode.getChildNodes(); + for (int i = 0; i < childList.getLength(); i++) { + currentNode = childList.item(i); + + if (currentNode.getNodeType() == Node.ELEMENT_NODE + || currentNode.getNodeType() == Node.TEXT_NODE) { + _lockOwner = currentNode.getFirstChild() + .getNodeValue(); + } + } + } + if (_lockOwner == null) { + return false; + } + } else { + return false; + } + + } catch (DOMException e) { + resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR); + LOG.error("DOM exception", e); + return false; + } catch (SAXException e) { + resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR); + LOG.error("SAX exception", e); + return false; + } + + return true; + } + + /** + * Ties to read the timeout from request + */ + private int getTimeout(ITransaction transaction, HttpServletRequest req) { + + int lockDuration = DEFAULT_TIMEOUT; + String lockDurationStr = req.getHeader("Timeout"); + + if (lockDurationStr == null) { + lockDuration = DEFAULT_TIMEOUT; + } else { + int commaPos = lockDurationStr.indexOf(','); + // if multiple timeouts, just use the first one + if (commaPos != -1) { + lockDurationStr = lockDurationStr.substring(0, commaPos); + } + if (lockDurationStr.startsWith("Second-")) { + lockDuration = new Integer(lockDurationStr.substring(7)) + .intValue(); + } else { + if (lockDurationStr.equalsIgnoreCase("infinity")) { + lockDuration = MAX_TIMEOUT; + } else { + try { + lockDuration = new Integer(lockDurationStr).intValue(); + } catch (NumberFormatException e) { + lockDuration = MAX_TIMEOUT; + } + } + } + if (lockDuration <= 0) { + lockDuration = DEFAULT_TIMEOUT; + } + if (lockDuration > MAX_TIMEOUT) { + lockDuration = MAX_TIMEOUT; + } + } + return lockDuration; + } + + /** + * Generates the response XML with all lock information + */ + private void generateXMLReport(ITransaction transaction, + HttpServletResponse resp, LockedObject lo) throws IOException { + + HashMap namespaces = new HashMap(); + namespaces.put("DAV:", "D"); + + resp.setStatus(WebdavStatus.SC_OK); + resp.setContentType("text/xml; charset=UTF-8"); + + XMLWriter generatedXML = new XMLWriter(resp.getWriter(), namespaces); + generatedXML.writeXMLHeader(); + generatedXML.writeElement("DAV::prop", XMLWriter.OPENING); + generatedXML.writeElement("DAV::lockdiscovery", XMLWriter.OPENING); + generatedXML.writeElement("DAV::activelock", XMLWriter.OPENING); + + generatedXML.writeElement("DAV::locktype", XMLWriter.OPENING); + generatedXML.writeProperty("DAV::" + _type); + generatedXML.writeElement("DAV::locktype", XMLWriter.CLOSING); + + generatedXML.writeElement("DAV::lockscope", XMLWriter.OPENING); + if (_exclusive) { + generatedXML.writeProperty("DAV::exclusive"); + } else { + generatedXML.writeProperty("DAV::shared"); + } + generatedXML.writeElement("DAV::lockscope", XMLWriter.CLOSING); + + int depth = lo.getLockDepth(); + + generatedXML.writeElement("DAV::depth", XMLWriter.OPENING); + if (depth == INFINITY) { + generatedXML.writeText("Infinity"); + } else { + generatedXML.writeText(String.valueOf(depth)); + } + generatedXML.writeElement("DAV::depth", XMLWriter.CLOSING); + + generatedXML.writeElement("DAV::owner", XMLWriter.OPENING); + generatedXML.writeElement("DAV::href", XMLWriter.OPENING); + generatedXML.writeText(_lockOwner); + generatedXML.writeElement("DAV::href", XMLWriter.CLOSING); + generatedXML.writeElement("DAV::owner", XMLWriter.CLOSING); + + long timeout = lo.getTimeoutMillis(); + generatedXML.writeElement("DAV::timeout", XMLWriter.OPENING); + generatedXML.writeText("Second-" + timeout / 1000); + generatedXML.writeElement("DAV::timeout", XMLWriter.CLOSING); + + String lockToken = lo.getID(); + generatedXML.writeElement("DAV::locktoken", XMLWriter.OPENING); + generatedXML.writeElement("DAV::href", XMLWriter.OPENING); + generatedXML.writeText("opaquelocktoken:" + lockToken); + generatedXML.writeElement("DAV::href", XMLWriter.CLOSING); + generatedXML.writeElement("DAV::locktoken", XMLWriter.CLOSING); + + generatedXML.writeElement("DAV::activelock", XMLWriter.CLOSING); + generatedXML.writeElement("DAV::lockdiscovery", XMLWriter.CLOSING); + generatedXML.writeElement("DAV::prop", XMLWriter.CLOSING); + + resp.addHeader("Lock-Token", ""); + + generatedXML.sendData(); + + } + + /** + * Executes the lock for a Mac OS Finder client + */ + private void doMacLockRequestWorkaround(ITransaction transaction, + HttpServletRequest req, HttpServletResponse resp) + throws LockFailedException, IOException { + LockedObject lo; + int depth = getDepth(req); + int lockDuration = getTimeout(transaction, req); + if (lockDuration < 0 || lockDuration > MAX_TIMEOUT) + lockDuration = DEFAULT_TIMEOUT; + + boolean lockSuccess = false; + lockSuccess = _resourceLocks.exclusiveLock(transaction, _path, + _lockOwner, depth, lockDuration); + + if (lockSuccess) { + // Locks successfully placed - return information about + lo = _resourceLocks.getLockedObjectByPath(transaction, _path); + if (lo != null) { + generateXMLReport(transaction, resp, lo); + } else { + resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR); + } + } else { + // Locking was not successful + sendLockFailError(transaction, req, resp); + } + } + + /** + * Sends an error report to the client + */ + private void sendLockFailError(ITransaction transaction, + HttpServletRequest req, HttpServletResponse resp) + throws IOException { + Hashtable errorList = new Hashtable(); + errorList.put(_path, WebdavStatus.SC_LOCKED); + sendReport(req, resp, errorList); + } + +} diff --git a/src/main/java/net/sf/webdav/methods/DoMkcol.java b/src/main/java/net/sf/webdav/methods/DoMkcol.java new file mode 100644 index 0000000..8eca0ff --- /dev/null +++ b/src/main/java/net/sf/webdav/methods/DoMkcol.java @@ -0,0 +1,178 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed 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. + */ +package net.sf.webdav.methods; + +import java.io.IOException; +import java.util.Hashtable; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import net.sf.webdav.ITransaction; +import net.sf.webdav.IWebdavStore; +import net.sf.webdav.StoredObject; +import net.sf.webdav.WebdavStatus; +import net.sf.webdav.exceptions.AccessDeniedException; +import net.sf.webdav.exceptions.LockFailedException; +import net.sf.webdav.exceptions.WebdavException; +import net.sf.webdav.locking.IResourceLocks; +import net.sf.webdav.locking.LockedObject; + +public class DoMkcol extends AbstractMethod { + + private static org.slf4j.Logger LOG = org.slf4j.LoggerFactory + .getLogger(DoMkcol.class); + + private IWebdavStore _store; + private IResourceLocks _resourceLocks; + private boolean _readOnly; + + public DoMkcol(IWebdavStore store, IResourceLocks resourceLocks, + boolean readOnly) { + _store = store; + _resourceLocks = resourceLocks; + _readOnly = readOnly; + } + + public void execute(ITransaction transaction, HttpServletRequest req, + HttpServletResponse resp) throws IOException, LockFailedException { + LOG.trace("-- " + this.getClass().getName()); + + if (!_readOnly) { + String path = getRelativePath(req); + String parentPath = getParentPath(getCleanPath(path)); + + Hashtable errorList = new Hashtable(); + + if (!checkLocks(transaction, req, resp, _resourceLocks, parentPath)) { + // TODO remove + LOG + .trace("MkCol on locked resource (parentPath) not executable!" + + "\n Sending SC_FORBIDDEN (403) error response!"); + + resp.sendError(WebdavStatus.SC_FORBIDDEN); + return; + } + + String tempLockOwner = "doMkcol" + System.currentTimeMillis() + + req.toString(); + + if (_resourceLocks.lock(transaction, path, tempLockOwner, false, 0, + TEMP_TIMEOUT, TEMPORARY)) { + StoredObject parentSo, so = null; + try { + parentSo = _store.getStoredObject(transaction, parentPath); + if (parentSo == null) { + // parent not exists + resp.sendError(WebdavStatus.SC_CONFLICT); + return; + } + if (parentPath != null && parentSo.isFolder()) { + so = _store.getStoredObject(transaction, path); + if (so == null) { + _store.createFolder(transaction, path); + resp.setStatus(WebdavStatus.SC_CREATED); + } else { + // object already exists + if (so.isNullResource()) { + + LockedObject nullResourceLo = _resourceLocks + .getLockedObjectByPath(transaction, + path); + if (nullResourceLo == null) { + resp + .sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR); + return; + } + String nullResourceLockToken = nullResourceLo + .getID(); + String[] lockTokens = getLockIdFromIfHeader(req); + String lockToken = null; + if (lockTokens != null) + lockToken = lockTokens[0]; + else { + resp.sendError(WebdavStatus.SC_BAD_REQUEST); + return; + } + if (lockToken.equals(nullResourceLockToken)) { + so.setNullResource(false); + so.setFolder(true); + + String[] nullResourceLockOwners = nullResourceLo + .getOwner(); + String owner = null; + if (nullResourceLockOwners != null) + owner = nullResourceLockOwners[0]; + + if (_resourceLocks.unlock(transaction, + lockToken, owner)) { + resp.setStatus(WebdavStatus.SC_CREATED); + } else { + resp + .sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR); + } + + } else { + // TODO remove + LOG + .trace("MkCol on lock-null-resource with wrong lock-token!" + + "\n Sending multistatus error report!"); + + errorList.put(path, WebdavStatus.SC_LOCKED); + sendReport(req, resp, errorList); + } + + } else { + String methodsAllowed = DeterminableMethod + .determineMethodsAllowed(so); + resp.addHeader("Allow", methodsAllowed); + resp + .sendError(WebdavStatus.SC_METHOD_NOT_ALLOWED); + } + } + + } else if (parentPath != null && parentSo.isResource()) { + // TODO remove + LOG + .trace("MkCol on resource is not executable" + + "\n Sending SC_METHOD_NOT_ALLOWED (405) error response!"); + + String methodsAllowed = DeterminableMethod + .determineMethodsAllowed(parentSo); + resp.addHeader("Allow", methodsAllowed); + resp.sendError(WebdavStatus.SC_METHOD_NOT_ALLOWED); + + } else { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + } + } catch (AccessDeniedException e) { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + } catch (WebdavException e) { + resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR); + } finally { + _resourceLocks.unlockTemporaryLockedObjects(transaction, + path, tempLockOwner); + } + } else { + resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR); + } + + } else { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + } + } + +} diff --git a/src/main/java/net/sf/webdav/methods/DoMove.java b/src/main/java/net/sf/webdav/methods/DoMove.java new file mode 100644 index 0000000..188da22 --- /dev/null +++ b/src/main/java/net/sf/webdav/methods/DoMove.java @@ -0,0 +1,121 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed 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. + */ +package net.sf.webdav.methods; + +import java.io.IOException; +import java.util.Hashtable; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import net.sf.webdav.ITransaction; +import net.sf.webdav.IWebdavStore; +import net.sf.webdav.WebdavStatus; +import net.sf.webdav.exceptions.AccessDeniedException; +import net.sf.webdav.exceptions.LockFailedException; +import net.sf.webdav.exceptions.ObjectAlreadyExistsException; +import net.sf.webdav.exceptions.WebdavException; +import net.sf.webdav.locking.ResourceLocks; + +public class DoMove extends AbstractMethod { + + private static org.slf4j.Logger LOG = org.slf4j.LoggerFactory + .getLogger(DoMove.class); + private IWebdavStore _store; + private ResourceLocks _resourceLocks; + private DoDelete _doDelete; + private DoCopy _doCopy; + private boolean _readOnly; + + public DoMove(IWebdavStore _store, ResourceLocks resourceLocks, DoDelete doDelete, + DoCopy doCopy, boolean readOnly) { + this._store = _store; + _resourceLocks = resourceLocks; + _doDelete = doDelete; + _doCopy = doCopy; + _readOnly = readOnly; + } + + public void execute(ITransaction transaction, HttpServletRequest req, + HttpServletResponse resp) throws IOException, LockFailedException { + + if (!_readOnly) { + LOG.trace("-- " + this.getClass().getName()); + + String sourcePath = getRelativePath(req); + Hashtable errorList = new Hashtable(); + + if (!checkLocks(transaction, req, resp, _resourceLocks, sourcePath)) { + resp.setStatus(WebdavStatus.SC_LOCKED); + return; + } + + String destinationPath = parseDestinationHeader(req, resp); + if (destinationPath == null) { + resp.sendError(WebdavStatus.SC_BAD_REQUEST); + return; + } + + if (!checkLocks(transaction, req, resp, _resourceLocks, + destinationPath)) { + resp.setStatus(WebdavStatus.SC_LOCKED); + return; + } + + String tempLockOwner = "doMove" + System.currentTimeMillis() + + req.toString(); + + if (_resourceLocks.lock(transaction, sourcePath, tempLockOwner, + false, 0, TEMP_TIMEOUT, TEMPORARY)) { + try { + boolean ok = _store.moveObject(transaction, destinationPath, sourcePath); + if (!ok) { + if (_doCopy.copyResource(transaction, req, resp)) { + + errorList = new Hashtable(); + _doDelete.deleteResource(transaction, sourcePath, + errorList, req, resp); + if (!errorList.isEmpty()) { + sendReport(req, resp, errorList); + } + } + } + + + } catch (AccessDeniedException e) { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + } catch (ObjectAlreadyExistsException e) { + resp.sendError(WebdavStatus.SC_NOT_FOUND, req + .getRequestURI()); + } catch (WebdavException e) { + resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR); + } finally { + _resourceLocks.unlockTemporaryLockedObjects(transaction, + sourcePath, tempLockOwner); + } + } else { + errorList.put(req.getHeader("Destination"), + WebdavStatus.SC_LOCKED); + sendReport(req, resp, errorList); + } + } else { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + + } + + } + +} diff --git a/src/main/java/net/sf/webdav/methods/DoNotImplemented.java b/src/main/java/net/sf/webdav/methods/DoNotImplemented.java new file mode 100644 index 0000000..e8b8a99 --- /dev/null +++ b/src/main/java/net/sf/webdav/methods/DoNotImplemented.java @@ -0,0 +1,31 @@ +package net.sf.webdav.methods; + +import java.io.IOException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import net.sf.webdav.IMethodExecutor; +import net.sf.webdav.ITransaction; +import net.sf.webdav.WebdavStatus; + +public class DoNotImplemented implements IMethodExecutor { + + private static org.slf4j.Logger LOG = org.slf4j.LoggerFactory + .getLogger(DoNotImplemented.class); + private boolean _readOnly; + + public DoNotImplemented(boolean readOnly) { + _readOnly = readOnly; + } + + public void execute(ITransaction transaction, HttpServletRequest req, + HttpServletResponse resp) throws IOException { + LOG.trace("-- " + req.getMethod()); + + if (_readOnly) { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + } else + resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED); + } +} diff --git a/src/main/java/net/sf/webdav/methods/DoOptions.java b/src/main/java/net/sf/webdav/methods/DoOptions.java new file mode 100644 index 0000000..7df08bf --- /dev/null +++ b/src/main/java/net/sf/webdav/methods/DoOptions.java @@ -0,0 +1,75 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed 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. + */ +package net.sf.webdav.methods; + +import java.io.IOException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import net.sf.webdav.StoredObject; +import net.sf.webdav.ITransaction; +import net.sf.webdav.WebdavStatus; +import net.sf.webdav.IWebdavStore; +import net.sf.webdav.exceptions.AccessDeniedException; +import net.sf.webdav.exceptions.LockFailedException; +import net.sf.webdav.exceptions.WebdavException; +import net.sf.webdav.locking.ResourceLocks; + +public class DoOptions extends DeterminableMethod { + + private static org.slf4j.Logger LOG = org.slf4j.LoggerFactory + .getLogger(DoOptions.class); + + private IWebdavStore _store; + private ResourceLocks _resourceLocks; + + public DoOptions(IWebdavStore store, ResourceLocks resLocks) { + _store = store; + _resourceLocks = resLocks; + } + + public void execute(ITransaction transaction, HttpServletRequest req, + HttpServletResponse resp) throws IOException, LockFailedException { + + LOG.trace("-- " + this.getClass().getName()); + + String tempLockOwner = "doOptions" + System.currentTimeMillis() + + req.toString(); + String path = getRelativePath(req); + if (_resourceLocks.lock(transaction, path, tempLockOwner, false, 0, + TEMP_TIMEOUT, TEMPORARY)) { + StoredObject so = null; + try { + resp.addHeader("DAV", "1, 2"); + + so = _store.getStoredObject(transaction, path); + String methodsAllowed = determineMethodsAllowed(so); + resp.addHeader("Allow", methodsAllowed); + resp.addHeader("MS-Author-Via", "DAV"); + } catch (AccessDeniedException e) { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + } catch (WebdavException e) { + resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR); + } finally { + _resourceLocks.unlockTemporaryLockedObjects(transaction, path, + tempLockOwner); + } + } else { + resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR); + } + } +} diff --git a/src/main/java/net/sf/webdav/methods/DoPropfind.java b/src/main/java/net/sf/webdav/methods/DoPropfind.java new file mode 100644 index 0000000..e279a92 --- /dev/null +++ b/src/main/java/net/sf/webdav/methods/DoPropfind.java @@ -0,0 +1,627 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed 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. + */ +package net.sf.webdav.methods; + +import java.io.IOException; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Vector; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.xml.parsers.DocumentBuilder; + +import net.sf.webdav.IMimeTyper; +import net.sf.webdav.ITransaction; +import net.sf.webdav.IWebdavStore; +import net.sf.webdav.StoredObject; +import net.sf.webdav.WebdavStatus; +import net.sf.webdav.exceptions.AccessDeniedException; +import net.sf.webdav.exceptions.LockFailedException; +import net.sf.webdav.exceptions.WebdavException; +import net.sf.webdav.fromcatalina.URLEncoder; +import net.sf.webdav.fromcatalina.XMLHelper; +import net.sf.webdav.fromcatalina.XMLWriter; +import net.sf.webdav.locking.LockedObject; +import net.sf.webdav.locking.ResourceLocks; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.xml.sax.InputSource; + +public class DoPropfind extends AbstractMethod { + + private static org.slf4j.Logger LOG = org.slf4j.LoggerFactory + .getLogger(DoPropfind.class); + + /** + * Array containing the safe characters set. + */ + protected static URLEncoder URL_ENCODER; + + /** + * PROPFIND - Specify a property mask. + */ + private static final int FIND_BY_PROPERTY = 0; + + /** + * PROPFIND - Display all properties. + */ + private static final int FIND_ALL_PROP = 1; + + /** + * PROPFIND - Return property names. + */ + private static final int FIND_PROPERTY_NAMES = 2; + + private IWebdavStore _store; + private ResourceLocks _resourceLocks; + private IMimeTyper _mimeTyper; + + private int _depth; + + public DoPropfind(IWebdavStore store, ResourceLocks resLocks, + IMimeTyper mimeTyper) { + _store = store; + _resourceLocks = resLocks; + _mimeTyper = mimeTyper; + } + + public void execute(ITransaction transaction, HttpServletRequest req, + HttpServletResponse resp) throws IOException, LockFailedException { + LOG.trace("-- " + this.getClass().getName()); + + // Retrieve the resources + String path = getCleanPath(getRelativePath(req)); + String tempLockOwner = "doPropfind" + System.currentTimeMillis() + + req.toString(); + _depth = getDepth(req); + + if (_resourceLocks.lock(transaction, path, tempLockOwner, false, + _depth, TEMP_TIMEOUT, TEMPORARY)) { + + StoredObject so = null; + try { + so = _store.getStoredObject(transaction, path); + if (so == null) { + resp.setContentType("text/xml; charset=UTF-8"); + resp.sendError(HttpServletResponse.SC_NOT_FOUND, req + .getRequestURI()); + return; + } + + Vector properties = null; + path = getCleanPath(getRelativePath(req)); + + int propertyFindType = FIND_ALL_PROP; + Node propNode = null; + + if (req.getContentLength() > 0) { + DocumentBuilder documentBuilder = getDocumentBuilder(); + try { + Document document = documentBuilder + .parse(new InputSource(req.getInputStream())); + // Get the root element of the document + Element rootElement = document.getDocumentElement(); + + propNode = XMLHelper + .findSubElement(rootElement, "prop"); + if (propNode != null) { + propertyFindType = FIND_BY_PROPERTY; + } else if (XMLHelper.findSubElement(rootElement, + "propname") != null) { + propertyFindType = FIND_PROPERTY_NAMES; + } else if (XMLHelper.findSubElement(rootElement, + "allprop") != null) { + propertyFindType = FIND_ALL_PROP; + } + } catch (Exception e) { + resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR); + return; + } + } else { + // no content, which means it is a allprop request + propertyFindType = FIND_ALL_PROP; + } + + HashMap namespaces = new HashMap(); + namespaces.put("DAV:", "D"); + + if (propertyFindType == FIND_BY_PROPERTY) { + propertyFindType = 0; + properties = XMLHelper.getPropertiesFromXML(propNode); + } + + resp.setStatus(WebdavStatus.SC_MULTI_STATUS); + resp.setContentType("text/xml; charset=UTF-8"); + + // Create multistatus object + XMLWriter generatedXML = new XMLWriter(resp.getWriter(), + namespaces); + generatedXML.writeXMLHeader(); + generatedXML + .writeElement("DAV::multistatus", XMLWriter.OPENING); + if (_depth == 0) { + parseProperties(transaction, req, generatedXML, path, + propertyFindType, properties, _mimeTyper + .getMimeType(transaction, path)); + } else { + recursiveParseProperties(transaction, path, req, + generatedXML, propertyFindType, properties, _depth, + _mimeTyper.getMimeType(transaction, path)); + } + generatedXML + .writeElement("DAV::multistatus", XMLWriter.CLOSING); + + generatedXML.sendData(); + } catch (AccessDeniedException e) { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + } catch (WebdavException e) { + LOG.warn("Sending internal error!"); + resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR); + } catch (ServletException e) { + e.printStackTrace(); // To change body of catch statement use + // File | Settings | File Templates. + } finally { + _resourceLocks.unlockTemporaryLockedObjects(transaction, path, + tempLockOwner); + } + } else { + Hashtable errorList = new Hashtable(); + errorList.put(path, WebdavStatus.SC_LOCKED); + sendReport(req, resp, errorList); + } + } + + /** + * goes recursive through all folders. used by propfind + * + * @param currentPath + * the current path + * @param req + * HttpServletRequest + * @param generatedXML + * @param propertyFindType + * @param properties + * @param depth + * depth of the propfind + * @throws IOException + * if an error in the underlying store occurs + */ + private void recursiveParseProperties(ITransaction transaction, + String currentPath, HttpServletRequest req, XMLWriter generatedXML, + int propertyFindType, Vector properties, int depth, + String mimeType) throws WebdavException { + + parseProperties(transaction, req, generatedXML, currentPath, + propertyFindType, properties, mimeType); + + if (depth > 0) { + // no need to get name if depth is already zero + String[] names = _store.getChildrenNames(transaction, currentPath); + names = names == null ? new String[] {} : names; + String newPath = null; + + for (String name : names) { + newPath = currentPath; + if (!(newPath.endsWith("/"))) { + newPath += "/"; + } + newPath += name; + recursiveParseProperties(transaction, newPath, req, + generatedXML, propertyFindType, properties, depth - 1, + mimeType); + } + } + } + + /** + * Propfind helper method. + * + * @param req + * The servlet request + * @param generatedXML + * XML response to the Propfind request + * @param path + * Path of the current resource + * @param type + * Propfind type + * @param propertiesVector + * If the propfind type is find properties by name, then this Vector + * contains those properties + */ + private void parseProperties(ITransaction transaction, + HttpServletRequest req, XMLWriter generatedXML, String path, + int type, Vector propertiesVector, String mimeType) + throws WebdavException { + + StoredObject so = _store.getStoredObject(transaction, path); + + boolean isFolder = so.isFolder(); + final String creationdate = creationDateFormat(so.getCreationDate()); + final String lastModified = lastModifiedDateFormat(so.getLastModified()); + String resourceLength = String.valueOf(so.getResourceLength()); + + // ResourceInfo resourceInfo = new ResourceInfo(path, resources); + + generatedXML.writeElement("DAV::response", XMLWriter.OPENING); + String status = new String("HTTP/1.1 " + WebdavStatus.SC_OK + " " + + WebdavStatus.getStatusText(WebdavStatus.SC_OK)); + + // Generating href element + generatedXML.writeElement("DAV::href", XMLWriter.OPENING); + + String href = req.getContextPath(); + String servletPath = req.getServletPath(); + if (servletPath != null) { + if ((href.endsWith("/")) && (servletPath.startsWith("/"))) + href += servletPath.substring(1); + else + href += servletPath; + } + if ((href.endsWith("/")) && (path.startsWith("/"))) + href += path.substring(1); + else + href += path; + if ((isFolder) && (!href.endsWith("/"))) + href += "/"; + + generatedXML.writeText(rewriteUrl(href)); + + generatedXML.writeElement("DAV::href", XMLWriter.CLOSING); + + String resourceName = path; + int lastSlash = path.lastIndexOf('/'); + if (lastSlash != -1) + resourceName = resourceName.substring(lastSlash + 1); + + switch (type) { + + case FIND_ALL_PROP: + + generatedXML.writeElement("DAV::propstat", XMLWriter.OPENING); + generatedXML.writeElement("DAV::prop", XMLWriter.OPENING); + + generatedXML.writeProperty("DAV::creationdate", creationdate); + generatedXML.writeElement("DAV::displayname", XMLWriter.OPENING); + generatedXML.writeData(resourceName); + generatedXML.writeElement("DAV::displayname", XMLWriter.CLOSING); + if (!isFolder) { + generatedXML + .writeProperty("DAV::getlastmodified", lastModified); + generatedXML.writeProperty("DAV::getcontentlength", + resourceLength); + String contentType = mimeType; + if (contentType != null) { + generatedXML.writeProperty("DAV::getcontenttype", + contentType); + } + generatedXML.writeProperty("DAV::getetag", getETag(so)); + generatedXML.writeElement("DAV::resourcetype", + XMLWriter.NO_CONTENT); + } else { + generatedXML.writeElement("DAV::resourcetype", + XMLWriter.OPENING); + generatedXML.writeElement("DAV::collection", + XMLWriter.NO_CONTENT); + generatedXML.writeElement("DAV::resourcetype", + XMLWriter.CLOSING); + } + + writeSupportedLockElements(transaction, generatedXML, path); + + writeLockDiscoveryElements(transaction, generatedXML, path); + + generatedXML.writeProperty("DAV::source", ""); + generatedXML.writeElement("DAV::prop", XMLWriter.CLOSING); + generatedXML.writeElement("DAV::status", XMLWriter.OPENING); + generatedXML.writeText(status); + generatedXML.writeElement("DAV::status", XMLWriter.CLOSING); + generatedXML.writeElement("DAV::propstat", XMLWriter.CLOSING); + + break; + + case FIND_PROPERTY_NAMES: + + generatedXML.writeElement("DAV::propstat", XMLWriter.OPENING); + generatedXML.writeElement("DAV::prop", XMLWriter.OPENING); + + generatedXML + .writeElement("DAV::creationdate", XMLWriter.NO_CONTENT); + generatedXML.writeElement("DAV::displayname", XMLWriter.NO_CONTENT); + if (!isFolder) { + generatedXML.writeElement("DAV::getcontentlanguage", + XMLWriter.NO_CONTENT); + generatedXML.writeElement("DAV::getcontentlength", + XMLWriter.NO_CONTENT); + generatedXML.writeElement("DAV::getcontenttype", + XMLWriter.NO_CONTENT); + generatedXML.writeElement("DAV::getetag", XMLWriter.NO_CONTENT); + generatedXML.writeElement("DAV::getlastmodified", + XMLWriter.NO_CONTENT); + } + generatedXML + .writeElement("DAV::resourcetype", XMLWriter.NO_CONTENT); + generatedXML.writeElement("DAV::supportedlock", + XMLWriter.NO_CONTENT); + generatedXML.writeElement("DAV::source", XMLWriter.NO_CONTENT); + + generatedXML.writeElement("DAV::prop", XMLWriter.CLOSING); + generatedXML.writeElement("DAV::status", XMLWriter.OPENING); + generatedXML.writeText(status); + generatedXML.writeElement("DAV::status", XMLWriter.CLOSING); + generatedXML.writeElement("DAV::propstat", XMLWriter.CLOSING); + + break; + + case FIND_BY_PROPERTY: + + Vector propertiesNotFound = new Vector(); + + // Parse the list of properties + + generatedXML.writeElement("DAV::propstat", XMLWriter.OPENING); + generatedXML.writeElement("DAV::prop", XMLWriter.OPENING); + + Enumeration properties = propertiesVector.elements(); + + while (properties.hasMoreElements()) { + + String property = (String) properties.nextElement(); + + if (property.equals("DAV::creationdate")) { + generatedXML.writeProperty("DAV::creationdate", + creationdate); + } else if (property.equals("DAV::displayname")) { + generatedXML.writeElement("DAV::displayname", + XMLWriter.OPENING); + generatedXML.writeData(resourceName); + generatedXML.writeElement("DAV::displayname", + XMLWriter.CLOSING); + } else if (property.equals("DAV::getcontentlanguage")) { + if (isFolder) { + propertiesNotFound.addElement(property); + } else { + generatedXML.writeElement("DAV::getcontentlanguage", + XMLWriter.NO_CONTENT); + } + } else if (property.equals("DAV::getcontentlength")) { + if (isFolder) { + propertiesNotFound.addElement(property); + } else { + generatedXML.writeProperty("DAV::getcontentlength", + resourceLength); + } + } else if (property.equals("DAV::getcontenttype")) { + if (isFolder) { + propertiesNotFound.addElement(property); + } else { + generatedXML.writeProperty("DAV::getcontenttype", + mimeType); + } + } else if (property.equals("DAV::getetag")) { + if (isFolder || so.isNullResource()) { + propertiesNotFound.addElement(property); + } else { + generatedXML.writeProperty("DAV::getetag", getETag(so)); + } + } else if (property.equals("DAV::getlastmodified")) { + if (isFolder) { + propertiesNotFound.addElement(property); + } else { + generatedXML.writeProperty("DAV::getlastmodified", + lastModified); + } + } else if (property.equals("DAV::resourcetype")) { + if (isFolder) { + generatedXML.writeElement("DAV::resourcetype", + XMLWriter.OPENING); + generatedXML.writeElement("DAV::collection", + XMLWriter.NO_CONTENT); + generatedXML.writeElement("DAV::resourcetype", + XMLWriter.CLOSING); + } else { + generatedXML.writeElement("DAV::resourcetype", + XMLWriter.NO_CONTENT); + } + } else if (property.equals("DAV::source")) { + generatedXML.writeProperty("DAV::source", ""); + } else if (property.equals("DAV::supportedlock")) { + + writeSupportedLockElements(transaction, generatedXML, path); + + } else if (property.equals("DAV::lockdiscovery")) { + + writeLockDiscoveryElements(transaction, generatedXML, path); + + } else { + propertiesNotFound.addElement(property); + } + + } + + generatedXML.writeElement("DAV::prop", XMLWriter.CLOSING); + generatedXML.writeElement("DAV::status", XMLWriter.OPENING); + generatedXML.writeText(status); + generatedXML.writeElement("DAV::status", XMLWriter.CLOSING); + generatedXML.writeElement("DAV::propstat", XMLWriter.CLOSING); + + Enumeration propertiesNotFoundList = propertiesNotFound + .elements(); + + if (propertiesNotFoundList.hasMoreElements()) { + + status = new String("HTTP/1.1 " + WebdavStatus.SC_NOT_FOUND + + " " + + WebdavStatus.getStatusText(WebdavStatus.SC_NOT_FOUND)); + + generatedXML.writeElement("DAV::propstat", XMLWriter.OPENING); + generatedXML.writeElement("DAV::prop", XMLWriter.OPENING); + + while (propertiesNotFoundList.hasMoreElements()) { + generatedXML.writeElement((String) propertiesNotFoundList + .nextElement(), XMLWriter.NO_CONTENT); + } + + generatedXML.writeElement("DAV::prop", XMLWriter.CLOSING); + generatedXML.writeElement("DAV::status", XMLWriter.OPENING); + generatedXML.writeText(status); + generatedXML.writeElement("DAV::status", XMLWriter.CLOSING); + generatedXML.writeElement("DAV::propstat", XMLWriter.CLOSING); + + } + + break; + + } + + generatedXML.writeElement("DAV::response", XMLWriter.CLOSING); + + so = null; + } + + private void writeSupportedLockElements(ITransaction transaction, + XMLWriter generatedXML, String path) { + + LockedObject lo = _resourceLocks.getLockedObjectByPath(transaction, + path); + + generatedXML.writeElement("DAV::supportedlock", XMLWriter.OPENING); + + if (lo == null) { + // both locks (shared/exclusive) can be granted + generatedXML.writeElement("DAV::lockentry", XMLWriter.OPENING); + + generatedXML.writeElement("DAV::lockscope", XMLWriter.OPENING); + generatedXML.writeElement("DAV::exclusive", XMLWriter.NO_CONTENT); + generatedXML.writeElement("DAV::lockscope", XMLWriter.CLOSING); + + generatedXML.writeElement("DAV::locktype", XMLWriter.OPENING); + generatedXML.writeElement("DAV::write", XMLWriter.NO_CONTENT); + generatedXML.writeElement("DAV::locktype", XMLWriter.CLOSING); + + generatedXML.writeElement("DAV::lockentry", XMLWriter.CLOSING); + + generatedXML.writeElement("DAV::lockentry", XMLWriter.OPENING); + + generatedXML.writeElement("DAV::lockscope", XMLWriter.OPENING); + generatedXML.writeElement("DAV::shared", XMLWriter.NO_CONTENT); + generatedXML.writeElement("DAV::lockscope", XMLWriter.CLOSING); + + generatedXML.writeElement("DAV::locktype", XMLWriter.OPENING); + generatedXML.writeElement("DAV::write", XMLWriter.NO_CONTENT); + generatedXML.writeElement("DAV::locktype", XMLWriter.CLOSING); + + generatedXML.writeElement("DAV::lockentry", XMLWriter.CLOSING); + + } else { + // LockObject exists, checking lock state + // if an exclusive lock exists, no further lock is possible + if (lo.isShared()) { + + generatedXML.writeElement("DAV::lockentry", XMLWriter.OPENING); + + generatedXML.writeElement("DAV::lockscope", XMLWriter.OPENING); + generatedXML.writeElement("DAV::shared", XMLWriter.NO_CONTENT); + generatedXML.writeElement("DAV::lockscope", XMLWriter.CLOSING); + + generatedXML.writeElement("DAV::locktype", XMLWriter.OPENING); + generatedXML.writeElement("DAV::" + lo.getType(), + XMLWriter.NO_CONTENT); + generatedXML.writeElement("DAV::locktype", XMLWriter.CLOSING); + + generatedXML.writeElement("DAV::lockentry", XMLWriter.CLOSING); + } + } + + generatedXML.writeElement("DAV::supportedlock", XMLWriter.CLOSING); + + lo = null; + } + + private void writeLockDiscoveryElements(ITransaction transaction, + XMLWriter generatedXML, String path) { + + LockedObject lo = _resourceLocks.getLockedObjectByPath(transaction, + path); + + if (lo != null && !lo.hasExpired()) { + + generatedXML.writeElement("DAV::lockdiscovery", XMLWriter.OPENING); + generatedXML.writeElement("DAV::activelock", XMLWriter.OPENING); + + generatedXML.writeElement("DAV::locktype", XMLWriter.OPENING); + generatedXML.writeProperty("DAV::" + lo.getType()); + generatedXML.writeElement("DAV::locktype", XMLWriter.CLOSING); + + generatedXML.writeElement("DAV::lockscope", XMLWriter.OPENING); + if (lo.isExclusive()) { + generatedXML.writeProperty("DAV::exclusive"); + } else { + generatedXML.writeProperty("DAV::shared"); + } + generatedXML.writeElement("DAV::lockscope", XMLWriter.CLOSING); + + generatedXML.writeElement("DAV::depth", XMLWriter.OPENING); + if (_depth == INFINITY) { + generatedXML.writeText("Infinity"); + } else { + generatedXML.writeText(String.valueOf(_depth)); + } + generatedXML.writeElement("DAV::depth", XMLWriter.CLOSING); + + String[] owners = lo.getOwner(); + if (owners != null) { + for (int i = 0; i < owners.length; i++) { + generatedXML.writeElement("DAV::owner", XMLWriter.OPENING); + generatedXML.writeElement("DAV::href", XMLWriter.OPENING); + generatedXML.writeText(owners[i]); + generatedXML.writeElement("DAV::href", XMLWriter.CLOSING); + generatedXML.writeElement("DAV::owner", XMLWriter.CLOSING); + } + } else { + generatedXML.writeElement("DAV::owner", XMLWriter.NO_CONTENT); + } + + int timeout = (int) (lo.getTimeoutMillis() / 1000); + String timeoutStr = new Integer(timeout).toString(); + generatedXML.writeElement("DAV::timeout", XMLWriter.OPENING); + generatedXML.writeText("Second-" + timeoutStr); + generatedXML.writeElement("DAV::timeout", XMLWriter.CLOSING); + + String lockToken = lo.getID(); + + generatedXML.writeElement("DAV::locktoken", XMLWriter.OPENING); + generatedXML.writeElement("DAV::href", XMLWriter.OPENING); + generatedXML.writeText("opaquelocktoken:" + lockToken); + generatedXML.writeElement("DAV::href", XMLWriter.CLOSING); + generatedXML.writeElement("DAV::locktoken", XMLWriter.CLOSING); + + generatedXML.writeElement("DAV::activelock", XMLWriter.CLOSING); + generatedXML.writeElement("DAV::lockdiscovery", XMLWriter.CLOSING); + + } else { + generatedXML.writeElement("DAV::lockdiscovery", + XMLWriter.NO_CONTENT); + } + + lo = null; + } + +} diff --git a/src/main/java/net/sf/webdav/methods/DoProppatch.java b/src/main/java/net/sf/webdav/methods/DoProppatch.java new file mode 100644 index 0000000..afeaca7 --- /dev/null +++ b/src/main/java/net/sf/webdav/methods/DoProppatch.java @@ -0,0 +1,228 @@ +package net.sf.webdav.methods; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Vector; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.xml.parsers.DocumentBuilder; + +import net.sf.webdav.ITransaction; +import net.sf.webdav.IWebdavStore; +import net.sf.webdav.StoredObject; +import net.sf.webdav.WebdavStatus; +import net.sf.webdav.exceptions.AccessDeniedException; +import net.sf.webdav.exceptions.LockFailedException; +import net.sf.webdav.exceptions.WebdavException; +import net.sf.webdav.fromcatalina.XMLHelper; +import net.sf.webdav.fromcatalina.XMLWriter; +import net.sf.webdav.locking.LockedObject; +import net.sf.webdav.locking.ResourceLocks; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.xml.sax.InputSource; + +public class DoProppatch extends AbstractMethod { + + private static org.slf4j.Logger LOG = org.slf4j.LoggerFactory + .getLogger(DoProppatch.class); + + private boolean _readOnly; + private IWebdavStore _store; + private ResourceLocks _resourceLocks; + + public DoProppatch(IWebdavStore store, ResourceLocks resLocks, + boolean readOnly) { + _readOnly = readOnly; + _store = store; + _resourceLocks = resLocks; + } + + public void execute(ITransaction transaction, HttpServletRequest req, + HttpServletResponse resp) throws IOException, LockFailedException { + LOG.trace("-- " + this.getClass().getName()); + + if (_readOnly) { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + return; + } + + String path = getRelativePath(req); + String parentPath = getParentPath(getCleanPath(path)); + + Hashtable errorList = new Hashtable(); + + if (!checkLocks(transaction, req, resp, _resourceLocks, parentPath)) { + resp.setStatus(WebdavStatus.SC_LOCKED); + return; // parent is locked + } + + if (!checkLocks(transaction, req, resp, _resourceLocks, path)) { + resp.setStatus(WebdavStatus.SC_LOCKED); + return; // resource is locked + } + + // TODO for now, PROPPATCH just sends a valid response, stating that + // everything is fine, but doesn't do anything. + + // Retrieve the resources + String tempLockOwner = "doProppatch" + System.currentTimeMillis() + + req.toString(); + + if (_resourceLocks.lock(transaction, path, tempLockOwner, false, 0, + TEMP_TIMEOUT, TEMPORARY)) { + StoredObject so = null; + LockedObject lo = null; + try { + so = _store.getStoredObject(transaction, path); + lo = _resourceLocks.getLockedObjectByPath(transaction, + getCleanPath(path)); + + if (so == null) { + resp.sendError(HttpServletResponse.SC_NOT_FOUND); + return; + // we do not to continue since there is no root + // resource + } + + if (so.isNullResource()) { + String methodsAllowed = DeterminableMethod + .determineMethodsAllowed(so); + resp.addHeader("Allow", methodsAllowed); + resp.sendError(WebdavStatus.SC_METHOD_NOT_ALLOWED); + return; + } + + String[] lockTokens = getLockIdFromIfHeader(req); + boolean lockTokenMatchesIfHeader = (lockTokens != null && lockTokens[0].equals(lo.getID())); + if (lo != null && lo.isExclusive() && !lockTokenMatchesIfHeader) { + // Object on specified path is LOCKED + errorList = new Hashtable(); + errorList.put(path, new Integer(WebdavStatus.SC_LOCKED)); + sendReport(req, resp, errorList); + return; + } + + List toset = null; + List toremove = null; + List tochange = new Vector(); + // contains all properties from + // toset and toremove + + path = getCleanPath(getRelativePath(req)); + + Node tosetNode = null; + Node toremoveNode = null; + + if (req.getContentLength() != 0) { + DocumentBuilder documentBuilder = getDocumentBuilder(); + try { + Document document = documentBuilder + .parse(new InputSource(req.getInputStream())); + // Get the root element of the document + Element rootElement = document.getDocumentElement(); + + tosetNode = XMLHelper.findSubElement(XMLHelper + .findSubElement(rootElement, "set"), "prop"); + toremoveNode = XMLHelper.findSubElement(XMLHelper + .findSubElement(rootElement, "remove"), "prop"); + } catch (Exception e) { + resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR); + return; + } + } else { + // no content: error + resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR); + return; + } + + HashMap namespaces = new HashMap(); + namespaces.put("DAV:", "D"); + + if (tosetNode != null) { + toset = XMLHelper.getPropertiesFromXML(tosetNode); + tochange.addAll(toset); + } + + if (toremoveNode != null) { + toremove = XMLHelper.getPropertiesFromXML(toremoveNode); + tochange.addAll(toremove); + } + + resp.setStatus(WebdavStatus.SC_MULTI_STATUS); + resp.setContentType("text/xml; charset=UTF-8"); + + // Create multistatus object + XMLWriter generatedXML = new XMLWriter(resp.getWriter(), + namespaces); + generatedXML.writeXMLHeader(); + generatedXML + .writeElement("DAV::multistatus", XMLWriter.OPENING); + + generatedXML.writeElement("DAV::response", XMLWriter.OPENING); + String status = new String("HTTP/1.1 " + WebdavStatus.SC_OK + + " " + WebdavStatus.getStatusText(WebdavStatus.SC_OK)); + + // Generating href element + generatedXML.writeElement("DAV::href", XMLWriter.OPENING); + + String href = req.getContextPath(); + if ((href.endsWith("/")) && (path.startsWith("/"))) + href += path.substring(1); + else + href += path; + if ((so.isFolder()) && (!href.endsWith("/"))) + href += "/"; + + generatedXML.writeText(rewriteUrl(href)); + + generatedXML.writeElement("DAV::href", XMLWriter.CLOSING); + + for (Iterator iter = tochange.iterator(); iter + .hasNext();) { + String property = (String) iter.next(); + + generatedXML.writeElement("DAV::propstat", + XMLWriter.OPENING); + + generatedXML.writeElement("DAV::prop", XMLWriter.OPENING); + generatedXML.writeElement(property, XMLWriter.NO_CONTENT); + generatedXML.writeElement("DAV::prop", XMLWriter.CLOSING); + + generatedXML.writeElement("DAV::status", XMLWriter.OPENING); + generatedXML.writeText(status); + generatedXML.writeElement("DAV::status", XMLWriter.CLOSING); + + generatedXML.writeElement("DAV::propstat", + XMLWriter.CLOSING); + } + + generatedXML.writeElement("DAV::response", XMLWriter.CLOSING); + + generatedXML + .writeElement("DAV::multistatus", XMLWriter.CLOSING); + + generatedXML.sendData(); + } catch (AccessDeniedException e) { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + } catch (WebdavException e) { + resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR); + } catch (ServletException e) { + e.printStackTrace(); // To change body of catch statement use + // File | Settings | File Templates. + } finally { + _resourceLocks.unlockTemporaryLockedObjects(transaction, path, + tempLockOwner); + } + } else { + resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR); + } + } +} diff --git a/src/main/java/net/sf/webdav/methods/DoPut.java b/src/main/java/net/sf/webdav/methods/DoPut.java new file mode 100644 index 0000000..49f5a2e --- /dev/null +++ b/src/main/java/net/sf/webdav/methods/DoPut.java @@ -0,0 +1,195 @@ +/* + * Copyright 1999,2004 The Apache Software Foundation. + * + * Licensed 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. + */ +package net.sf.webdav.methods; + +import java.io.IOException; +import java.util.Hashtable; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import net.sf.webdav.ITransaction; +import net.sf.webdav.IWebdavStore; +import net.sf.webdav.StoredObject; +import net.sf.webdav.WebdavStatus; +import net.sf.webdav.exceptions.AccessDeniedException; +import net.sf.webdav.exceptions.LockFailedException; +import net.sf.webdav.exceptions.WebdavException; +import net.sf.webdav.locking.IResourceLocks; +import net.sf.webdav.locking.LockedObject; + +public class DoPut extends AbstractMethod { + + private static org.slf4j.Logger LOG = org.slf4j.LoggerFactory + .getLogger(DoPut.class); + + private IWebdavStore _store; + private IResourceLocks _resourceLocks; + private boolean _readOnly; + private boolean _lazyFolderCreationOnPut; + + private String _userAgent; + + public DoPut(IWebdavStore store, IResourceLocks resLocks, boolean readOnly, + boolean lazyFolderCreationOnPut) { + _store = store; + _resourceLocks = resLocks; + _readOnly = readOnly; + _lazyFolderCreationOnPut = lazyFolderCreationOnPut; + } + + public void execute(ITransaction transaction, HttpServletRequest req, + HttpServletResponse resp) throws IOException, LockFailedException { + LOG.trace("-- " + this.getClass().getName()); + + if (!_readOnly) { + String path = getRelativePath(req); + String parentPath = getParentPath(path); + + _userAgent = req.getHeader("User-Agent"); + + Hashtable errorList = new Hashtable(); + + if (!checkLocks(transaction, req, resp, _resourceLocks, parentPath)) { + resp.setStatus(WebdavStatus.SC_LOCKED); + return; // parent is locked + } + + if (!checkLocks(transaction, req, resp, _resourceLocks, path)) { + resp.setStatus(WebdavStatus.SC_LOCKED); + return; // resource is locked + } + + String tempLockOwner = "doPut" + System.currentTimeMillis() + + req.toString(); + if (_resourceLocks.lock(transaction, path, tempLockOwner, false, 0, + TEMP_TIMEOUT, TEMPORARY)) { + StoredObject parentSo, so = null; + try { + parentSo = _store.getStoredObject(transaction, parentPath); + if (parentPath != null && parentSo != null + && parentSo.isResource()) { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + return; + + } else if (parentPath != null && parentSo == null + && _lazyFolderCreationOnPut) { + _store.createFolder(transaction, parentPath); + + } else if (parentPath != null && parentSo == null + && !_lazyFolderCreationOnPut) { + errorList.put(parentPath, WebdavStatus.SC_NOT_FOUND); + sendReport(req, resp, errorList); + return; + } + + so = _store.getStoredObject(transaction, path); + + if (so == null) { + _store.createResource(transaction, path); + // resp.setStatus(WebdavStatus.SC_CREATED); + } else { + // This has already been created, just update the data + if (so.isNullResource()) { + + LockedObject nullResourceLo = _resourceLocks + .getLockedObjectByPath(transaction, path); + if (nullResourceLo == null) { + resp + .sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR); + return; + } + String nullResourceLockToken = nullResourceLo + .getID(); + String[] lockTokens = getLockIdFromIfHeader(req); + String lockToken = null; + if (lockTokens != null) { + lockToken = lockTokens[0]; + } else { + resp.sendError(WebdavStatus.SC_BAD_REQUEST); + return; + } + if (lockToken.equals(nullResourceLockToken)) { + so.setNullResource(false); + so.setFolder(false); + + String[] nullResourceLockOwners = nullResourceLo + .getOwner(); + String owner = null; + if (nullResourceLockOwners != null) + owner = nullResourceLockOwners[0]; + + if (!_resourceLocks.unlock(transaction, + lockToken, owner)) { + resp + .sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR); + } + } else { + errorList.put(path, WebdavStatus.SC_LOCKED); + sendReport(req, resp, errorList); + } + } + } + // User-Agent workarounds + doUserAgentWorkaround(resp); + + // setting resourceContent + long resourceLength = _store + .setResourceContent(transaction, path, req + .getInputStream(), null, null); + + so = _store.getStoredObject(transaction, path); + if (resourceLength > 0) + so.setResourceLength(resourceLength); + // Now lets report back what was actually saved + + } catch (AccessDeniedException e) { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + } catch (WebdavException e) { + resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR); + } finally { + _resourceLocks.unlockTemporaryLockedObjects(transaction, + path, tempLockOwner); + } + } else { + resp.sendError(WebdavStatus.SC_INTERNAL_SERVER_ERROR); + } + } else { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + } + + } + + /** + * @param resp + */ + private void doUserAgentWorkaround(HttpServletResponse resp) { + if (_userAgent != null && _userAgent.indexOf("WebDAVFS") != -1 + && _userAgent.indexOf("Transmit") == -1) { + LOG.trace("DoPut.execute() : do workaround for user agent '" + + _userAgent + "'"); + resp.setStatus(WebdavStatus.SC_CREATED); + } else if (_userAgent != null && _userAgent.indexOf("Transmit") != -1) { + // Transmit also uses WEBDAVFS 1.x.x but crashes + // with SC_CREATED response + LOG.trace("DoPut.execute() : do workaround for user agent '" + + _userAgent + "'"); + resp.setStatus(WebdavStatus.SC_NO_CONTENT); + } else { + resp.setStatus(WebdavStatus.SC_CREATED); + } + } +} diff --git a/src/main/java/net/sf/webdav/methods/DoUnlock.java b/src/main/java/net/sf/webdav/methods/DoUnlock.java new file mode 100644 index 0000000..a5fe3c9 --- /dev/null +++ b/src/main/java/net/sf/webdav/methods/DoUnlock.java @@ -0,0 +1,98 @@ +package net.sf.webdav.methods; + +import java.io.IOException; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import net.sf.webdav.StoredObject; +import net.sf.webdav.ITransaction; +import net.sf.webdav.WebdavStatus; +import net.sf.webdav.IWebdavStore; +import net.sf.webdav.exceptions.LockFailedException; +import net.sf.webdav.locking.IResourceLocks; +import net.sf.webdav.locking.LockedObject; + +public class DoUnlock extends DeterminableMethod { + + private static org.slf4j.Logger LOG = org.slf4j.LoggerFactory + .getLogger(DoUnlock.class); + + private IWebdavStore _store; + private IResourceLocks _resourceLocks; + private boolean _readOnly; + + public DoUnlock(IWebdavStore store, IResourceLocks resourceLocks, + boolean readOnly) { + _store = store; + _resourceLocks = resourceLocks; + _readOnly = readOnly; + } + + public void execute(ITransaction transaction, HttpServletRequest req, + HttpServletResponse resp) throws IOException, LockFailedException { + LOG.trace("-- " + this.getClass().getName()); + + if (_readOnly) { + resp.sendError(WebdavStatus.SC_FORBIDDEN); + return; + } else { + + String path = getRelativePath(req); + String tempLockOwner = "doUnlock" + System.currentTimeMillis() + + req.toString(); + try { + if (_resourceLocks.lock(transaction, path, tempLockOwner, + false, 0, TEMP_TIMEOUT, TEMPORARY)) { + + String lockId = getLockIdFromLockTokenHeader(req); + LockedObject lo; + if (lockId != null + && ((lo = _resourceLocks.getLockedObjectByID( + transaction, lockId)) != null)) { + + String[] owners = lo.getOwner(); + String owner = null; + if (lo.isShared()) { + // more than one owner is possible + if (owners != null) { + for (int i = 0; i < owners.length; i++) { + // remove owner from LockedObject + lo.removeLockedObjectOwner(owners[i]); + } + } + } else { + // exclusive, only one lock owner + if (owners != null) + owner = owners[0]; + else + owner = null; + } + + if (_resourceLocks.unlock(transaction, lockId, owner)) { + StoredObject so = _store.getStoredObject( + transaction, path); + if (so.isNullResource()) { + _store.removeObject(transaction, path); + } + + resp.setStatus(WebdavStatus.SC_NO_CONTENT); + } else { + LOG.trace("DoUnlock failure at " + lo.getPath()); + resp.sendError(WebdavStatus.SC_METHOD_FAILURE); + } + + } else { + resp.sendError(WebdavStatus.SC_BAD_REQUEST); + } + } + } catch (LockFailedException e) { + e.printStackTrace(); + } finally { + _resourceLocks.unlockTemporaryLockedObjects(transaction, path, + tempLockOwner); + } + } + } + +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..e503ead --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1 @@ +#logging.level.net.sf.webdav=trace \ No newline at end of file diff --git a/src/test/java/com/github/zxbu/webdavteambition/WebdavTeambitionApplicationTests.java b/src/test/java/com/github/zxbu/webdavteambition/WebdavTeambitionApplicationTests.java new file mode 100644 index 0000000..8656acc --- /dev/null +++ b/src/test/java/com/github/zxbu/webdavteambition/WebdavTeambitionApplicationTests.java @@ -0,0 +1,13 @@ +package com.github.zxbu.webdavteambition; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class WebdavTeambitionApplicationTests { + + @Test + void contextLoads() { + } + +}