2021年3月14日日曜日

4: WebDAVサーバーを自力で実装する: 最小限バックグラウンド

<このシリーズの前の記事 | このシリーズの目次 |

WebDAVプロトコルを実装するのは、技術的に難しくありません。サードパーティライブラリは特に必要ありません。

話題


About: Javaプログラミング言語
About: WebDAV

この記事の目次


開始コンテキスト


  • 読者は、HTTPプロトコルの基本的知識を持っている。

ターゲットコンテキスト



  • 読者は、WebDAVサーバーを自力で実装するための最小限バックグラウンドを知る。

オリエンテーション


本記事はJavaのためのシリーズ内にありますが、ここで紹介される知識は、いかなるプログラミング言語においても役立ちます。

残りの知識は、あるRFCドキュメントから得ることができます。


本体


1: WebDAVプロトコルは、HTTPプロトコルの拡張である


Hypothesizer 7
WebDAVプロトコルを実装するにはTCP/IPソケットプログラミングをしなければならないのだろう、と私は想像していた(根拠なく)。

それは真実ではない: WebDAVプロトコルは、実際にはHTTPプロトコルの拡張である。

それはどういう意味なのか?えーと、もっと最もよく知られているところでは、'GET'および'POST'メソッドがHTTPプロトコルで使われるが、それ以外のいくつかのメソッドもWebDAVプロトコルでは使われる。

したがって、私が行なわなければならないのは、それらのメソッドのHTTPリクエスト群をレシーブして適切なHTTPレスポンス群をリターンするハンドラーを書くことだけである。

各リクエストデータは、HTTPリクエストヘッダおよびHTTPリクエストボディによって表わされるが、HTTPリクエストボディは、実際にはXMLデータである。

各レスポンスデータは、HTTPレスポンスヘッダー(ステータスコードを含む)およびHTTPレスポンスボディによって表わされるが、HTTPレスポンスボディはXMLデータである、推測できるとおり。

とてもシンプルでしょう?


2: いくつかの遂行レベルがある


Hypothesizer 7
可能なリクエスト群の全体をどのWebDAVサーバーも遂行しなければならないというわけではない。

特に、あるWebDAVサーバーは'LOCK'メソッドを遂行する必要はなく、それ('LOCK'を遂行しないもの)が、レベル'1'だ。

レベル'2'は'LOCK'メソッドを遂行するが、必ずしも全ての'SHOULD'仕様(任意のメソッドについてであり、'LOCK'メソッドについてだけではない)をサポートしない。

「'SHOULD'仕様」とは何のことだ?えーと、当該RFCドキュメントは、ある事柄は実装されることが'MUST'であり、他の事柄は実装されることが'SHOULD'であると言っており、したがって、それらの'SHOULD'要件は、必ずしも実装される必要はない。

レベル'3'は、全ての'SHOULD'仕様(全ての'MUST'仕様は言うまでもなく)を遂行する。


3: メソッド群


Hypothesizer 7
以下が、実装できるメソッド群だ: 'OPTIONS'、'PROPFIND'、'PROPPATCH'、'GET'、'HEAD'、'PUT'、'POST'、'DELETE'、'COPY'、'MOVE'、'MKCOL'、'LOCK'、'UNLOCK'。

特に、最初の5メソッドが重要である。

'OPTIONS'は、指定されたリソース(既に存在するかもしれないししないかもしれない)に対してどんな種類のリクエストたちが当該サーバーによって遂行されるかを知る(クライアントの観点から言って)ためのものだ(レスポンスはリソースに依存するかもしれない)。

'PROPFIND'は、指定されたリソースのいくつかのプロパティー(例えば、最終変更日時)を知る(クライアントの観点から言って)ためのものだ。

'PROPPATCH'は、指定されたリソースのいくつかのプロパティ(例えば、最終変更日時)をセットする(クライアントの観点から言って)ためのものだ。

'GET'は、指定されたリソースのコンテンツを取り出す(クライアントの観点から言って)ためのものだ。

'PUT'は、指定されたデータを指定されたリソース(既に存在するかもしないかもしれない)のコンテンツとして送る(クライアントの観点から言って)ためのものだ。

上記5メソッドをいかに実装するかを、次セクションにて見てみよう。


4: 上記の重要5メソッドを実装する


Hypothesizer 7
準備として、当該HTTPサーバーのスケルトンを持っていなければならない。

いかなるプログラミング言語によるいかなる適切なものも勿論使えるのだが、ここでは、私は、Javaで'com.sun.net.httpserver.HttpServer'を用いる。以下が、私のスケルトンだ。

theBiasPlanet/webDavServer/programs/WebDavServerConsoleProgram.java

@Java ソースコード
package theBiasPlanet.webDavServer.programs;

import java.net.InetSocketAddress;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Scanner;
import java.util.concurrent.Executors;
import com.sun.net.httpserver.HttpServer;
import theBiasPlanet.coreUtilities.messagingHandling.Publisher;
import theBiasPlanet.webDavServer.controllers.WebDavRequestHandler;

public class WebDavServerConsoleProgram {
	static private HttpServer s_httpServer = null;
	
	private WebDavServerConsoleProgram () {
	}
	
	public static void main (String [] a_argumentsArray) throws Exception {
		Publisher.setLoggingLevel (3);
		Publisher.setSuccinctness (true);
		if (a_argumentsArray.length < 4) {
				throw new Exception ("The arguments have to be these.\nThe argument 1: the host address\nThe argument 2: the port number\nThe argument 3: the back logging size\nThe argument 4: the contents base directory absolute path");
		}
		String l_hostAddress = a_argumentsArray [0];
		int l_portNumber = Integer.parseInt (a_argumentsArray [1]);
		int l_backLoggingSize = Integer.parseInt (a_argumentsArray [2]);
		Path l_contentsBaseDirectoryAbsolutePath = Paths.get (a_argumentsArray [3]);
		
		initialize (l_hostAddress, l_portNumber, l_backLoggingSize, l_contentsBaseDirectoryAbsolutePath);
		Scanner l_userInputScanner = new Scanner (System.in);
		System.out.println ("### Enter any line to stop the server.");
		l_userInputScanner.nextLine ();
		s_httpServer.stop (0);
		System.exit (0);
	}
	
	public static void initialize (String a_hostAddress, int a_portNumber, int a_backLoggingSize, Path a_contentsBaseDirectoryAbsolutePath) throws Exception {
		s_httpServer = HttpServer.create (new InetSocketAddress (a_hostAddress, a_portNumber), a_backLoggingSize);
		s_httpServer.createContext ("/", new WebDavRequestHandler (a_contentsBaseDirectoryAbsolutePath));
		s_httpServer.setExecutor (Executors.newFixedThreadPool (10));
		s_httpServer.start ();
	}
}

theBiasPlanet/webDavServer/controllers/WebDavRequestHandler.java

@Java ソースコード
package theBiasPlanet.webDavServer.controllers;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.URI;
import java.net.URLDecoder;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Stack;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.parsers.ParserConfigurationException;
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.SAXNotRecognizedException;
import org.xml.sax.SAXNotSupportedException;
import org.xml.sax.SAXParseException;
import org.xml.sax.ext.DefaultHandler2;
import theBiasPlanet.coreUtilities.constantsGroups.CharactersSetNamesConstantsGroup;
import theBiasPlanet.coreUtilities.constantsGroups.DefaultValuesConstantsGroup;
import theBiasPlanet.coreUtilities.constantsGroups.GeneralConstantsConstantsGroup;
import theBiasPlanet.coreUtilities.constantsGroups.InputPropertiesConstantsGroup;
import theBiasPlanet.coreUtilities.filesHandling.FilesHandler;
import theBiasPlanet.coreUtilities.messagingHandling.Publisher;
import theBiasPlanet.coreUtilities.stringsHandling.StringHandler;
import theBiasPlanet.coreUtilities.xmlDataHandling.SaxParser;
import theBiasPlanet.coreUtilities.xmlDataHandling.XmlDatumHandler;

public class WebDavRequestHandler implements HttpHandler {
	private static class WebDavRequestDatumSaxHandler extends DefaultHandler2 {
		private HashMap <String, String> i_itemPathStringToValueMap = new HashMap <String, String> ();
		private Stack <String> i_currentItemPathString = new Stack <String> ();
		private StringBuilder i_currentItemValueBuilder = new StringBuilder ();
		private boolean i_isNotEmpty = false;
		
		private WebDavRequestDatumSaxHandler () {
		}
		
		@Override
		protected void finalize () {
		}
		
		@Override
		public void startDTD (String a_rootBareElementName, String a_publicId, String a_systemId) throws SAXException {
		}
		
		@Override
		public void startDocument () throws SAXException {
		}
		
		@Override
		public void endDocument () throws SAXException {
		}
		
		@Override
		public void startElement (String a_namespaceUri,String a_localName, String a_qualifiedName, Attributes a_attributes) throws SAXException {
			if (!i_isNotEmpty) {
				i_isNotEmpty = true;
			}
			i_currentItemPathString.push (a_localName);
		}
		
		@Override
		public void endElement (String a_namespaceUri, String a_localName, String a_qualifiedName) throws SAXException {
			String l_currentItemPathString = i_currentItemPathString.toString ();
			i_itemPathStringToValueMap.put (l_currentItemPathString, i_currentItemValueBuilder.toString ());
			Publisher.logDebugInformation (String.format ("### a request body item: %s -> %s", l_currentItemPathString, i_currentItemValueBuilder.toString ()));
			i_currentItemPathString.pop ();
			i_currentItemValueBuilder.delete (GeneralConstantsConstantsGroup.c_iterationStartNumber, i_currentItemValueBuilder.length ());
		}
		 
		@Override
		public void characters (char[] a_characters, int a_start, int a_length) throws SAXException {
			i_currentItemValueBuilder.append (a_characters, a_start, a_length);
		}
		
		@Override
		public void startCDATA () throws SAXException {
		}
		
		@Override
		public void endCDATA () throws SAXException {
		}
		
		@Override
		public void startPrefixMapping(String a_prefix, String a_namespaceUri) throws SAXException {
		}
		
		@Override
		public void warning (SAXParseException a_exception) throws SAXException {
		}
		
		@Override
		public void error (SAXParseException a_exception) throws SAXException {
		}
		
		@Override
		public void fatalError (SAXParseException a_exception) throws SAXException {
		}
		
		public void initialize () {
			i_itemPathStringToValueMap.clear ();
			i_currentItemPathString.clear ();
			i_currentItemValueBuilder.delete (GeneralConstantsConstantsGroup.c_iterationStartNumber, i_currentItemValueBuilder.length ());
			i_isNotEmpty = false;
		}
		
		public HashMap <String, String> getItemPathStringToValueMap () {
			return i_itemPathStringToValueMap;
		}
		
		public String getItemValue (String a_itemPathString) {
			return i_itemPathStringToValueMap.get (a_itemPathString);
		}
		
		public boolean isEmpty () {
			return !i_isNotEmpty;
		}
	}
	private static final int c_bufferSize = DefaultValuesConstantsGroup.c_smallBufferSize;
	private Path i_contentsBaseDirectoryAbsolutePath = null;
	private ZoneId s_coordinatedUniversalTimeZone = ZoneId.of ("UTC");
	private DateTimeFormatter i_rfc1123DateAndTimeFormatter = DateTimeFormatter.ofPattern ("EEE, dd MMM yyyy HH:mm:ss zzz");
	private SaxParser i_saxParser = null;
	private WebDavRequestDatumSaxHandler i_webDavRequestDatumSaxHandler = new WebDavRequestDatumSaxHandler ();
	private static Pattern c_propertyNameRegularExpression = Pattern.compile (".*, prop, (.*)](.*)");
	private static final String c_webDavResponseBodyXmlDeclaration =
		"<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n";
	private static final String c_webDavResponseBodyMultiStatusHeader =
		"<D:multistatus xmlns:D=\"DAV:\">\n";
	private static final String c_webDavResponseBodyMultiStatusFooter =
		"</D:multistatus>\n";
	private static final String c_webDavResponseBodyResponseHeader =
		"	<D:response>\n";
	private static final String c_webDavResponseBodyResponseFooter =
		"	</D:response>\n";
	private static final String c_webDavResponseBodyHrefTag =
		"		<D:href>%s</D:href>\n";
	private static final String c_webDavResponseBodyPropstatHeader =
		"		<D:propstat>\n";
	private static final String c_webDavResponseBodyPropstatFooter =
		"		</D:propstat>\n";
	private static final String c_webDavResponseBodyPropHeader =
		"			<D:prop>\n";
	private static final String c_webDavResponseBodyPropFooter =
		"			</D:prop>\n";
	private static final String c_webDavResponseBodyStatutsTag =
		"			<D:status>HTTP/1.1 %d </D:status>\n";
	private static final String c_webDavResponseBodyErrorHeader =
		"<D:error xmlns:D=\"DAV:\">\n";
	private static final String c_webDavResponseBodyErrorFooter =
		"</D:error>\n";
	
	public WebDavRequestHandler (Path a_contentsBaseDirectoryAbsolutePath) throws NoSuchAlgorithmException, ParserConfigurationException, SAXException, SAXNotRecognizedException, SAXNotSupportedException {
		i_contentsBaseDirectoryAbsolutePath = a_contentsBaseDirectoryAbsolutePath;
		i_saxParser = new SaxParser (null, false);
	}
	
	@Override
	protected void finalize () {
	}
	
	@Override
	public void handle (HttpExchange a_httpExchange) throws IOException {
		URI l_httpRequestUri = a_httpExchange.getRequestURI ();
		String l_requestMethod = a_httpExchange.getRequestMethod ();
		Publisher.logDebugInformation (String.format ("\u001B[33m### %s is called with %s.\u001B[0m", URLDecoder.decode (l_httpRequestUri.toString (), CharactersSetNamesConstantsGroup.c_utf8CharactersSetName), l_requestMethod));
		Headers l_httpRequestHeader = a_httpExchange.getRequestHeaders ();
		Publisher.logDebugInformation ("### the request header Start");
		for (Map.Entry <String, List <String>> l_httpRequestHeaderItemNameToValuesMapEntry: l_httpRequestHeader.entrySet ()) {
			Publisher.logDebugInformation (String.format ("### %s: %s", l_httpRequestHeaderItemNameToValuesMapEntry.getKey (), StringHandler.getString (l_httpRequestHeaderItemNameToValuesMapEntry.getValue ())));
		}
		Publisher.logDebugInformation ("### the request header End");
		int l_httpResponseStatus = 404;
		try {
			if ("OPTIONS".equals (l_requestMethod)) {
				l_httpResponseStatus = handleOptionsRequest (a_httpExchange);
			}
			else if ("PROPFIND".equals (l_requestMethod)) {
				l_httpResponseStatus = handlePropfindRequest (a_httpExchange);
			}
			else if ("HEAD".equals (l_requestMethod)) {
				l_httpResponseStatus = handleHeadRequest (a_httpExchange);
			}
			else if ("GET".equals (l_requestMethod)) {
				l_httpResponseStatus = handleGetRequest (a_httpExchange);
			}
			else if ("PUT".equals (l_requestMethod)) {
				l_httpResponseStatus = handlePutRequest (a_httpExchange);
			}
			else if ("PROPPATCH".equals (l_requestMethod)) {
				l_httpResponseStatus = handleProppatchRequest (a_httpExchange);
			}
			else {
				l_httpResponseStatus = 405;
				Headers l_httpResponseHeader = a_httpExchange.getResponseHeaders ();
				String l_resourceUriString = URLDecoder.decode (l_httpRequestUri.toString (), CharactersSetNamesConstantsGroup.c_utf8CharactersSetName);
				if (l_resourceUriString.endsWith (String.valueOf (GeneralConstantsConstantsGroup.c_linuxDirectoriesDelimiter))) {
					l_httpResponseHeader.set ("Allow", "OPTIONS, PROPFIND");
				}
				else {
					l_httpResponseHeader.set ("Allow", "OPTIONS, PROPFIND, GET, PUT, PROPPATCH");
				}
				a_httpExchange.sendResponseHeaders (l_httpResponseStatus, -1);
			}
		}
		catch (SAXException l_exception) {
			Publisher.logErrorInformation (l_exception);
		}
		Publisher.logDebugInformation (String.format ("\u001B[33m### the response status: %d\u001B[0m", l_httpResponseStatus));
	}
	
	~
	
	private void sendResponseFragment (OutputStream a_outputStream, String a_responseFragment, boolean a_isEscaped) throws IOException {
		if (a_isEscaped) {
			a_outputStream.write (XmlDatumHandler.getEscapedXmlText (a_responseFragment).getBytes ());
		}
		else {
			a_outputStream.write (a_responseFragment.getBytes ());
		}
	}
}

'WebDavRequestDatumSaxHandler'クラスは、任意のXMLリクエストボディを解析するためのものであり、XMLリクエストボディのアイテム群は、'getItemValue (String a_itemPathString)'メソッドまたは'getItemPathStringToValueMap ()'メソッドを介して取り出すことができる。

'sendResponseFragment (OutputStream a_outputStream, String a_responseFragment, boolean a_isEscaped)'メソッドは、指定されたレスポンスボディフラグメントを送るユーティリティメソッドであり、今後使われることになる。

そこでは、いくつかのユーティリティクラスたち('theBiasPlanet.coreUtilities.filesHandling.FilesHandler'のような)が使われているが、ここでは説明されない、なぜなら、それらはここでの問題ではなく、それらのメソッドたちが何を(どのようにかはともかく)行なうかは明白だから。

ここでは、私は、レベル'1'でいく。


4-1: 'OPTIONS'


Hypothesizer 7
'OPTIONS'を実装するのはとても容易だ: リクエストボディもレスポンスボディもない。

私は、任意のリソース(たとえ、リソースが存在しないとしても)に対して、以下のレスポンスヘッダーをリターンすることにする。

@出力
DAV: 1
MS-Author-Via: DAV

いくつかのMicrosoftクライアントは「MS-Author-Via」を必要とするようだ。

以下が、私のコードだ。

@Java ソースコード
	private int handleOptionsRequest (HttpExchange a_httpExchange) throws IOException, SAXException {
		URI l_httpRequestUri = a_httpExchange.getRequestURI ();
		Headers l_httpRequestHeader = a_httpExchange.getRequestHeaders ();
		Headers l_httpResponseHeader = a_httpExchange.getResponseHeaders ();
		InputStream l_httpRequestInputStream = a_httpExchange.getRequestBody ();
		OutputStream l_httpResponseOutputStream = a_httpExchange.getResponseBody ();
		int l_httpResponseStatus = 404;
		
		String l_resourceUriString = URLDecoder.decode (l_httpRequestUri.toString (), CharactersSetNamesConstantsGroup.c_utf8CharactersSetName);
		i_webDavRequestDatumSaxHandler.initialize ();
		try {
			i_saxParser.parse (new BufferedReader (new InputStreamReader (l_httpRequestInputStream, CharactersSetNamesConstantsGroup.c_utf8CharactersSetName), DefaultValuesConstantsGroup.c_smallBufferSize), i_webDavRequestDatumSaxHandler);
		}
		catch (SAXParseException l_exception) {
			// comes here if the input stream is empty
		}
		l_httpResponseHeader.set ("DAV", "1");
		// Microsoft wants this
		l_httpResponseHeader.set ("MS-Author-Via", "DAV");
		l_httpResponseStatus = 200;
		a_httpExchange.sendResponseHeaders (l_httpResponseStatus, -1);
		
		return l_httpResponseStatus;
	}


4-2: 'PROPFIND'


Hypothesizer 7
実のところ、これが最も重要な部分だ: クライアントは'PUT'をコールする前後に本メソッドを(おそらくは)使うだろうから、'PUT'が発生するようにするためには、本メソッドを私は実装する必要がある。

本メソッドについては、もしも、指定されたリソースが存在していなければ、単に'404'レスポンスをリターンし、その他の場合は、'207'または'403'レスポンスをリターンする。

リクエストは、'Depth'ヘッダーをもっているはずであり、それは、'0'(指定されたリソースのみが問合わされていることを意味する)、'1'(指定されたリソース(それがディレクトリー(当該RFC内では'container(コンテナ)'と呼ばれている)であると仮定して)およびその中に格納されたリソース群が問合わされていることを意味する)、'Infinity'(指定されたリソース(それがディレクトリーだと仮定して)およびその指定されたリソース配下の全てのリソースが問合わされることを意味する)であるだろう。

'Infinity'は、合法に拒否することができ、その場合、サーバーは、'403'レスポンスを、以下のレスポンスボディでリターンすることになる。

@出力
<?xml version="1.0" encoding="utf-8" ?>
<D:error xmlns:D="DAV:">
	<D:propfind-finite-depth>
		<D:href>%t<codeLine><![CDATA[he resource URI%</D:href>
	</D:propfind-finite-depth>
</D:error>

ここでは、私のサーバーはそうする、また実際には、サブディレクトリーを全然許さない(単に、プログラムをシンプルにするという理由で)。

リクエストボディは、以下の4タイプの内の1つである: 1) 何もなし; 2) 許されるプロパティの名前たちを要求する; 3) 全プロパティを要求する; 4) 指定されたプロパティたちを要求する。

1)は、実際には、意味において、3)と同じである。

2)は、以下のようになる。

@出力
<?xml version="1.0" encoding="utf-8" ?>
<D:propfind xmlns:D="DAV:">
	<D:propname/>
</D:propfind>

私のサーバーは、以下のようなレスポンスボディを送る。

@出力
<?xml version="1.0" encoding="utf-8" ?>
<D:multistatus xmlns:D="DAV:">
	<!-- This item repeats per resource -->
	<D:response>
		<D:href>%the resource URI%</D:href>
		<D:propstat>
			<D:prop>
				<D:resourcetype/>
				<D:getcontenttype/>
				<D:supportedlock/>
				<D:creationdate/>
				<D:getlastmodified/>
				<D:getcontentlength/>
				<D:displayname/>
			</D:prop>
			<D:status>HTTP/1.1 200 </D:status>
		</D:propstat>
	</D:response>
	~
</D:multistatus>

'getcontenttype'はリソースがディレクトリでないときにのみ存在することに注意する。

3)は、以下のようになる。

@出力
<?xml version="1.0" encoding="utf-8" ?>
<D:propfind xmlns:D="DAV:">
	<D:allprop/>
</D:propfind>

私のサーバーは、以下のようなレスポンスボディを送る。

@出力
<?xml version="1.0" encoding="utf-8" ?>
<D:multistatus xmlns:D="DAV:">
	<!-- This item repeats per resource -->
	<D:response>
		<D:href>%the resource URI%</D:href>
		<D:propstat>
			<D:prop>
				<D:resourcetype>
					<D:collection/>
				</D:resourcetype>
				<D:getcontenttype>%the resource contents type%</D:getcontenttype>
				<D:supportedlock>
				</D:supportedlock>
				<D:creationdate>%the resource created date and time in the RFC1123 format in UTC%</D:creationdate>
				<D:getlastmodified>%the resource last modified date and time in the RFC1123 format in UTC%</D:getlastmodified>
				<D:getcontentlength>%the resource contents length%</D:getcontentlength>
				<D:displayname>%the resource display name%</D:displayname>
			</D:prop>
			<D:status>HTTP/1.1 200 </D:status>
		</D:propstat>
	</D:response>
	~
</D:multistatus>

注意事項として、当該リソースがディレクトリーでなければ、「resourcetype」は空になり、「getcontenttype」は、当該リソースがディレクトリーでない場合のみに存在し、%the resource contents type%は'application/octet-stream'のようになり、RFC1123フォーマットは'Wed, 04 Nov 2020 03:53:17 UTC'のようになる。

4)は、以下のようになる。

@出力
<?xml version="1.0" encoding="utf-8" ?>
<D:propfind xmlns:D="DAV:">
	<D:prop>
		<D:creationdate/>
		<D:getlastmodified/>
		<D:anUnsupportedPropertyName/>
	</D:prop>
</D:propfind>

私のサーバーは、以下のようなレスポンスボディを送る。

@出力
<?xml version="1.0" encoding="utf-8" ?>
<D:multistatus xmlns:D="DAV:">
	<!-- This item repeats per resource -->
	<D:response>
		<D:href>%the resource URI%</D:href>
		<D:propstat>
			<D:prop>
				<D:creationdate>%the resource created date and time in the RFC1123 format in UTC%</D:creationdate>
				<D:getlastmodified>%the resource last modified date and time in the RFC1123 format in UTC%</D:getlastmodified>
			</D:prop>
			<D:status>HTTP/1.1 200 </D:status>
		</D:propstat>
		<D:propstat>
			<D:prop>
				<D:anUnsupportedPropertyName/>
			</D:prop>
			<D:status>HTTP/1.1 404 </D:status>
		</D:propstat>
	</D:response>
	~
</D:multistatus>

注意事項として、「anUnsupportedPropertyName」は「404」で応答されているが、理由は、それが本サーバーではサポートされていないことだ。

リソースたちが実際にどのように存在しているか(例えば、普通にあるファイルシステムに、またはあるリレーショナルデータベースに、またはメモリ内だけに、存在しているかもしれないし、実際には全然どこにも存在していないかもしれない)は、当該サーバーの裁量によるが、多分、'PUT'メソッドで投ぜられた任意のリソースは、しばらくの間、少なくとも存在するふりをしなければならないだろう、なぜなら、クライアントはその'PUT'メソッドコールの直後にそのリソースに不可避にアクセスを試みる傾向があるからだ、いくつかのプロパティをセットしたり、単にその'PUT'メソッドコールの結果を確認したりするために。

ここでは、リソース群は、あるファイルシステムのある事前に指定されたディレクトリ内に存在している('i_contentsBaseDirectoryAbsolutePath'フィールドに指定されているとおり)。

以下が、私のコードだ(長めではあるが、実際には真っ直ぐなものである)。

@Java ソースコード
	private int handlePropfindRequest  (HttpExchange a_httpExchange) throws IOException, SAXException {
		URI l_httpRequestUri = a_httpExchange.getRequestURI ();
		Headers l_httpRequestHeader = a_httpExchange.getRequestHeaders ();
		Headers l_httpResponseHeader = a_httpExchange.getResponseHeaders ();
		InputStream l_httpRequestInputStream = a_httpExchange.getRequestBody ();
		OutputStream l_httpResponseOutputStream = a_httpExchange.getResponseBody ();
		int l_httpResponseStatus = 404;
		
		String l_resourceUriString = URLDecoder.decode (l_httpRequestUri.toString (), CharactersSetNamesConstantsGroup.c_utf8CharactersSetName);
		i_webDavRequestDatumSaxHandler.initialize ();
		try {
			i_saxParser.parse (new BufferedReader (new InputStreamReader (l_httpRequestInputStream, CharactersSetNamesConstantsGroup.c_utf8CharactersSetName), DefaultValuesConstantsGroup.c_smallBufferSize), i_webDavRequestDatumSaxHandler);
		}
		catch (SAXParseException l_exception) {
			// comes here if the input stream is empty
		}
		String l_queryDepthString = l_httpRequestHeader.getFirst ("Depth");
		if (l_queryDepthString.equals ("Infinity")) {
			l_httpResponseStatus = 403;
			l_httpResponseHeader.set ("Content-Type", "text/xml;charset=UTF-8");
			sendResponseFragment (l_httpResponseOutputStream, c_webDavResponseBodyXmlDeclaration, false);
			sendResponseFragment (l_httpResponseOutputStream, c_webDavResponseBodyErrorHeader, false);
			sendResponseFragment (l_httpResponseOutputStream, "	<D:propfind-finite-depth>", false);
			sendResponseFragment (l_httpResponseOutputStream, String.format (c_webDavResponseBodyHrefTag, l_resourceUriString), false);
			sendResponseFragment (l_httpResponseOutputStream, "	</D:propfind-finite-depth>", false);
			sendResponseFragment (l_httpResponseOutputStream, c_webDavResponseBodyErrorFooter, false);
		}
		else {
			boolean l_allPropertiesAreRequested = false;
			boolean l_propertyNamesAreRequested = false;
			if (i_webDavRequestDatumSaxHandler.isEmpty ()) {
				l_allPropertiesAreRequested = true;
			}
			else {
				if (i_webDavRequestDatumSaxHandler.getItemValue ("[propfind, allprop]") != null) {
					l_allPropertiesAreRequested = true;
				}
				if (i_webDavRequestDatumSaxHandler.getItemValue ("[propfind, propname]") != null) {
					l_propertyNamesAreRequested = true;
				}
			}
			boolean l_resourceExists = false;
			if (l_resourceUriString.equals (String.valueOf (GeneralConstantsConstantsGroup.c_linuxDirectoriesDelimiter))) {
				l_resourceExists = true;
			}
			else {
				InputStream l_contentsInputStream = null;
				try {
					l_contentsInputStream = Files.newInputStream (i_contentsBaseDirectoryAbsolutePath.resolve (Paths.get (l_resourceUriString.substring (1))),  StandardOpenOption.READ);
					l_resourceExists = true;
				}
				catch (IOException | SecurityException l_exception) {
				}
				finally {
					if (l_contentsInputStream != null) {
						l_contentsInputStream.close ();
						l_contentsInputStream = null;
					}
				}
			}
			if (l_resourceExists) {
				
				l_httpResponseHeader.set ("Content-Type", "text/xml;charset=UTF-8");
				l_httpResponseStatus = 207;
				a_httpExchange.sendResponseHeaders (l_httpResponseStatus, 0);
				sendResponseFragment (l_httpResponseOutputStream, c_webDavResponseBodyXmlDeclaration, false);
				sendResponseFragment (l_httpResponseOutputStream, c_webDavResponseBodyMultiStatusHeader, false);
				ArrayList <Path> l_childResourcePaths = new ArrayList <Path> ();
				ListIterator <Path> l_childResourcePathsIterator = null;
				while (l_resourceUriString != null) {
					Path l_resourcePath = i_contentsBaseDirectoryAbsolutePath.resolve (Paths.get (l_resourceUriString.substring (1)));
					boolean l_resourceIsDirectory = l_resourceUriString.endsWith (String.valueOf (GeneralConstantsConstantsGroup.c_linuxDirectoriesDelimiter)) ? true: false;
					LocalDateTime l_resourceCreatedDateAndTime = null;
					LocalDateTime l_resourceLastModifiedDateAndTime = null;
					long l_resourceSize = GeneralConstantsConstantsGroup.c_unspecifiedInteger;
					
					l_resourceCreatedDateAndTime = FilesHandler.getFileCreatedDateAndTime (l_resourcePath);
					l_resourceLastModifiedDateAndTime = FilesHandler.getFileLastModifiedDateAndTime (l_resourcePath);
					l_resourceSize = Files.size (l_resourcePath);
					
					sendResponseFragment (l_httpResponseOutputStream, c_webDavResponseBodyResponseHeader, false);
					sendResponseFragment (l_httpResponseOutputStream, String.format (c_webDavResponseBodyHrefTag, l_resourceUriString), false);
					if (l_propertyNamesAreRequested) {
						sendResponseFragment (l_httpResponseOutputStream, c_webDavResponseBodyPropstatHeader, false);
						sendResponseFragment (l_httpResponseOutputStream, c_webDavResponseBodyPropHeader, false);
						sendResponseFragment (l_httpResponseOutputStream, "				<D:resourcetype/>\n", false);
						if (!l_resourceIsDirectory) {
							sendResponseFragment (l_httpResponseOutputStream, "				<D:getcontenttype/>\n", false);
						}
						sendResponseFragment (l_httpResponseOutputStream, "				<D:supportedlock/>\n", false);
						sendResponseFragment (l_httpResponseOutputStream, "				<D:creationdate/>\n", false);
						sendResponseFragment (l_httpResponseOutputStream, "				<D:getlastmodified/>\n", false);
						sendResponseFragment (l_httpResponseOutputStream, "				<D:getcontentlength/>\n", false);
						sendResponseFragment (l_httpResponseOutputStream, "				<D:displayname/>\n", false);
						sendResponseFragment (l_httpResponseOutputStream, c_webDavResponseBodyPropFooter, false);
						sendResponseFragment (l_httpResponseOutputStream, String.format (c_webDavResponseBodyStatutsTag, 200), false);
						sendResponseFragment (l_httpResponseOutputStream, c_webDavResponseBodyPropstatFooter, false);
					}
					else {
						ArrayList <String> l_supportedPropertyNames = new ArrayList <String> ();
						ArrayList <String> l_unsupportedPropertyNames = new ArrayList <String> ();
						if (l_allPropertiesAreRequested) {
							l_supportedPropertyNames.add ("resourcetype");
							if (!l_resourceIsDirectory) {
								l_supportedPropertyNames.add ("getcontenttype");
							}
							l_supportedPropertyNames.add ("supportedlock");
							l_supportedPropertyNames.add ("creationdate");
							l_supportedPropertyNames.add ("getlastmodified");
							l_supportedPropertyNames.add ("getcontentlength");
							l_supportedPropertyNames.add ("displayname");
						}
						else {
							HashMap <String, String> l_itemPathStringToValueMap = i_webDavRequestDatumSaxHandler.getItemPathStringToValueMap ();
							for (Map.Entry <String, String> l_itemPathStringToValueMapEntry: l_itemPathStringToValueMap.entrySet ()) {
								String l_itemPathString = l_itemPathStringToValueMapEntry.getKey ();
								if (l_itemPathString.equals ("[propfind, prop, resourcetype]") || l_itemPathString.equals ("[propfind, prop, supportedlock]") || l_itemPathString.equals ("[propfind, prop, creationdate]") || l_itemPathString.equals ("[propfind, prop, getlastmodified]") || l_itemPathString.equals ("[propfind, prop, getcontentlength]") || l_itemPathString.equals ("[propfind, prop, displayname]")) {
									l_supportedPropertyNames.add (StringHandler.getStackStringLastItem (l_itemPathString));
								}
								else if (l_itemPathString.equals ("[propfind, prop, getcontenttype]") && !l_resourceIsDirectory) {
									l_supportedPropertyNames.add (StringHandler.getStackStringLastItem (l_itemPathString));
								}
								else if (l_itemPathString.startsWith ("[propfind, prop, ")) {
									l_unsupportedPropertyNames.add (StringHandler.getStackStringLastItem (l_itemPathString));
								}
							}
						}
						sendResourcePropertiesResponseFragments (l_httpResponseOutputStream, l_supportedPropertyNames, l_unsupportedPropertyNames, l_resourceIsDirectory, l_resourceUriString, l_resourceCreatedDateAndTime, l_resourceLastModifiedDateAndTime, l_resourceSize);
					}
					sendResponseFragment (l_httpResponseOutputStream, c_webDavResponseBodyResponseFooter, false);
					if (l_resourceIsDirectory) {
						if (l_queryDepthString.equals ("1")) {
							try {
								Files.newDirectoryStream (l_resourcePath, a_path -> Files.isRegularFile (a_path)).forEach (a_path -> l_childResourcePaths.add (i_contentsBaseDirectoryAbsolutePath.relativize (a_path).normalize ()));		
								l_childResourcePathsIterator = l_childResourcePaths.listIterator ();
							}
							catch (IOException l_exception) {
								break;
							}
						}
						else {
							break;
						}
					}
					if (l_childResourcePathsIterator != null && l_childResourcePathsIterator.hasNext ()) {
						l_resourceUriString = GeneralConstantsConstantsGroup.c_linuxDirectoriesDelimiter + l_childResourcePathsIterator.next ().toString ();
					}
					else {
						l_resourceUriString = null;
					}
				}
				sendResponseFragment (l_httpResponseOutputStream, "</D:multistatus>\n", false);
			}
			else {
				l_httpResponseStatus = 404;
				a_httpExchange.sendResponseHeaders (l_httpResponseStatus, 0);
			}
		}
		
		l_httpResponseOutputStream.close ();
		return l_httpResponseStatus;
	}
	
	private void sendResourcePropertiesResponseFragments (OutputStream a_httpResponseOutputStream, ArrayList <String> a_supportedPropertyNames, ArrayList <String> a_unsupportedPropertyNames, boolean a_resourceIsDirectory, String a_resourceUriString, LocalDateTime a_resourceCreatedDateAndTime, LocalDateTime a_resourceLastModifiedDateAndTime, long a_resourceSize) throws IOException {
		if (a_supportedPropertyNames != null && a_supportedPropertyNames.size () > 0) {
			sendResponseFragment (a_httpResponseOutputStream, c_webDavResponseBodyPropstatHeader, false);
			sendResponseFragment (a_httpResponseOutputStream, c_webDavResponseBodyPropHeader, false);
			for (String l_supportedPropertyName: a_supportedPropertyNames) {
				if (l_supportedPropertyName.equals ("resourcetype")) {
					sendResponseFragment (a_httpResponseOutputStream, "				<D:resourcetype>\n", false);
					if (a_resourceIsDirectory) {
						sendResponseFragment (a_httpResponseOutputStream, "					<D:collection/>\n", false);
					}
					sendResponseFragment (a_httpResponseOutputStream, "				</D:resourcetype>\n", false);
				}
				if (l_supportedPropertyName.equals ("getcontenttype")) {
					sendResponseFragment (a_httpResponseOutputStream, String.format ("				<D:getcontenttype>%s</D:getcontenttype>\n", StringHandler.getFileContentType (a_resourceUriString)), false);
				}
				else if (l_supportedPropertyName.equals ("supportedlock")) {
					sendResponseFragment (a_httpResponseOutputStream, "				<D:supportedlock>\n", false);
					sendResponseFragment (a_httpResponseOutputStream, "				</D:supportedlock>\n", false);
				}
				else if (l_supportedPropertyName.equals ("creationdate")) {
					sendResponseFragment (a_httpResponseOutputStream, String.format ("				<D:creationdate>%s</D:creationdate>\n", i_rfc1123DateAndTimeFormatter.format (a_resourceCreatedDateAndTime.atZone (ZoneId.systemDefault ()).withZoneSameInstant (s_coordinatedUniversalTimeZone))), false);
				}
				else if (l_supportedPropertyName.equals ("getlastmodified")) {
					sendResponseFragment (a_httpResponseOutputStream, String.format ("				<D:getlastmodified>%s</D:getlastmodified>\n", i_rfc1123DateAndTimeFormatter.format (a_resourceLastModifiedDateAndTime.atZone (ZoneId.systemDefault ()).withZoneSameInstant (s_coordinatedUniversalTimeZone))), false);
				}
				else if (l_supportedPropertyName.equals ("getcontentlength")) {
					sendResponseFragment (a_httpResponseOutputStream, String.format ("				<D:getcontentlength>%d</D:getcontentlength>\n", a_resourceSize), false);
				}
				else if (l_supportedPropertyName.equals ("displayname")) {
					Path l_fileOrDirectoryLastPath = Paths.get (a_resourceUriString).getFileName ();
					sendResponseFragment (a_httpResponseOutputStream, String.format ("				<D:displayname>%s</D:displayname>\n", l_fileOrDirectoryLastPath != null? l_fileOrDirectoryLastPath.toString (): ""), false);
				}
			}
			sendResponseFragment (a_httpResponseOutputStream, c_webDavResponseBodyPropFooter, false);
			sendResponseFragment (a_httpResponseOutputStream, String.format (c_webDavResponseBodyStatutsTag, 200), false);
			sendResponseFragment (a_httpResponseOutputStream, c_webDavResponseBodyPropstatFooter, false);
		}
		if (a_unsupportedPropertyNames != null && a_unsupportedPropertyNames.size () > 0) {
			sendResponseFragment (a_httpResponseOutputStream, c_webDavResponseBodyPropstatHeader, false);
			sendResponseFragment (a_httpResponseOutputStream, c_webDavResponseBodyPropHeader, false);
			for (String l_unsupportedPropertyName: a_unsupportedPropertyNames) {
				sendResponseFragment (a_httpResponseOutputStream, String.format ("				<D:%s/>\n", l_unsupportedPropertyName), false);
			}
			sendResponseFragment (a_httpResponseOutputStream, c_webDavResponseBodyPropFooter, false);
			sendResponseFragment (a_httpResponseOutputStream, String.format (c_webDavResponseBodyStatutsTag, 404), false);
			sendResponseFragment (a_httpResponseOutputStream, c_webDavResponseBodyPropstatFooter, false);
		}
	}


4-3: 'PROPPATCH'


Hypothesizer 7
クライアントは、本メソッドを、'PUT'をコールした後に(おそらくは)使うので、'PUT'コールが成功したとクライアントに納得させるために、本メソッドを実装する必要がある、たとえ、私のサーバーはそんなプロパティ群には興味がないとしても。

もしも、指定されたリソースが存在しなければ、単に'404'レスポンスをリターンし、そうでなければ、'207'レスポンスをリターンする。

リクエストボディは、以下のようになる。

@出力
<?xml version="1.0" encoding="utf-8" ?>
<D:propertyupdate xmlns:D="DAV:">
	<D:set>
		<D:prop>
			<D:creationdate>%a date and time in the RFC1123 format%</D:creationdate>
			<D:getlastmodified>%a date and time in the RFC1123 format%</D:getlastmodified>
		</D:prop>
	</D:set>
</D:propertyupdate>

私のサーバーは、以下のようなレスポンスボディを送る。

@出力
<?xml version="1.0" encoding="utf-8" ?>
<D:multistatus xmlns:D="DAV:">
	<D:response>
		<D:href>%the resource URI%</D:href>
		<D:propstat>
			<D:prop>
				<D:creationdate/>
				<D:getlastmodified/>
			</D:prop>
			<D:status>HTTP/1.1 200 </D:status>
		</D:propstat>
	</D:response>
</D:multistatus>

'set'アイテムと並んでまたはその代わりに'remove'アイテムがあるかもしれない、実際には、作成日時や最終変更日時を削除するリクエストがあるとは予期しないが。

Windows 'Explorer'はいくつかの'Win32~'プロパティを送るだろうが、私のサーバーは、それらを処理せず、しかし、処理したふりをする。

以下が、私のコードだ。

@Java ソースコード
	private int handleProppatchRequest (HttpExchange a_httpExchange) throws IOException, SAXException {
		URI l_httpRequestUri = a_httpExchange.getRequestURI ();
		Headers l_httpRequestHeader = a_httpExchange.getRequestHeaders ();
		Headers l_httpResponseHeader = a_httpExchange.getResponseHeaders ();
		InputStream l_httpRequestInputStream = a_httpExchange.getRequestBody ();
		OutputStream l_httpResponseOutputStream = a_httpExchange.getResponseBody ();
		int l_httpResponseStatus = 404;
		
		String l_resourceUriString = URLDecoder.decode (l_httpRequestUri.toString (), CharactersSetNamesConstantsGroup.c_utf8CharactersSetName);
		i_webDavRequestDatumSaxHandler.initialize ();
		try {
			i_saxParser.parse (new BufferedReader (new InputStreamReader (l_httpRequestInputStream, CharactersSetNamesConstantsGroup.c_utf8CharactersSetName), DefaultValuesConstantsGroup.c_smallBufferSize), i_webDavRequestDatumSaxHandler);
		}
		catch (SAXParseException l_exception) {
			// comes here if the input stream is empty
		}
		InputStream l_contentsInputStream = null;
		try {
			l_contentsInputStream = Files.newInputStream (i_contentsBaseDirectoryAbsolutePath.resolve (Paths.get (l_resourceUriString.substring (1))),  StandardOpenOption.READ);
			l_httpResponseStatus = 207;
		}
		catch (IOException | SecurityException l_exception) {
			l_httpResponseStatus = 404;
		}
		finally {
			if (l_contentsInputStream != null) {
				l_contentsInputStream.close ();
				l_contentsInputStream = null;
			}
		}
		a_httpExchange.sendResponseHeaders (l_httpResponseStatus, 0);
		sendResponseFragment (l_httpResponseOutputStream, c_webDavResponseBodyXmlDeclaration, false);
		sendResponseFragment (l_httpResponseOutputStream, c_webDavResponseBodyMultiStatusHeader, false);
		sendResponseFragment (l_httpResponseOutputStream, c_webDavResponseBodyResponseHeader, false);
		sendResponseFragment (l_httpResponseOutputStream, String.format (c_webDavResponseBodyHrefTag, l_resourceUriString), false);
		HashMap <String, String> l_supportedPropertyNameToValueMap = new HashMap <String, String> ();
		ArrayList <String> l_unsupportedPropertyNames = new ArrayList <String> ();
		HashMap <String, String> l_itemPathStringToValueMap = i_webDavRequestDatumSaxHandler.getItemPathStringToValueMap ();
		for (Map.Entry <String, String> l_itemPathStringToValueMapEntry: l_itemPathStringToValueMap.entrySet ()) {
			String l_itemPathString = l_itemPathStringToValueMapEntry.getKey ();
			if (l_itemPathString.startsWith ("[propertyupdate, set, prop, ") || l_itemPathString.startsWith ("[propertyupdate, remove, prop, ")) {
				String l_propertyName = null;
				Matcher l_propertyNameMatcher = c_propertyNameRegularExpression.matcher (l_itemPathString);
				if (l_propertyNameMatcher != null && l_propertyNameMatcher.lookingAt ()) {
					if (l_propertyNameMatcher.group (2).equals (GeneralConstantsConstantsGroup.c_emptyString)) {
						l_propertyName = l_propertyNameMatcher.group (1);
						// Windows Explorer sends 'Win32='s.
						if (l_propertyName.equals ("creationdate") || l_propertyName.equals ("getlastmodified") || l_propertyName.equals ("Win32CreationTime") || l_propertyName.equals ("Win32LastModifiedTime") || l_propertyName.equals ("Win32LastAccessTime") || l_propertyName.equals ("Win32FileAttributes")) {
							l_supportedPropertyNameToValueMap.put (l_propertyName, l_itemPathStringToValueMapEntry.getValue ());
						}
						else {
							l_unsupportedPropertyNames.add (l_propertyName);
						}
					}
				}
			}
		}
		int l_propertyStatusCode = 403;
		if (l_unsupportedPropertyNames.size () == 0) {
			l_propertyStatusCode = 200;
		}
		else {
			l_propertyStatusCode = 403;
		}
		sendResponseFragment (l_httpResponseOutputStream, c_webDavResponseBodyPropstatHeader, false);
		sendResponseFragment (l_httpResponseOutputStream, c_webDavResponseBodyPropHeader, false);
		for (Map.Entry <String, String> l_supportedPropertyNameToValueMapEntry: l_supportedPropertyNameToValueMap.entrySet ()) {
			String l_propertyName = l_supportedPropertyNameToValueMapEntry.getKey ();
			Path l_resourcePath = i_contentsBaseDirectoryAbsolutePath.resolve (Paths.get (l_resourceUriString.substring (1)));
			sendResponseFragment (l_httpResponseOutputStream, String.format ("				<D:%s/>\n", l_propertyName), false);
			if (l_propertyStatusCode == 200) {
				if (l_propertyName.equals ("creationdate")) {
					try {
						FilesHandler.setFileCreatedDateAndTime (l_resourcePath, ZonedDateTime.parse (l_supportedPropertyNameToValueMapEntry.getValue (), i_rfc1123DateAndTimeFormatter).withZoneSameInstant (ZoneId.systemDefault ()).toLocalDateTime ());
					}
					catch (NoSuchFileException l_exception) {
						Publisher.logErrorInformation (l_exception);
					}
				}
				if (l_propertyName.equals ("getlastmodified")) {
					try {
						FilesHandler.setFileLastModifiedDateAndTime (l_resourcePath, ZonedDateTime.parse (l_supportedPropertyNameToValueMapEntry.getValue (), i_rfc1123DateAndTimeFormatter).withZoneSameInstant (ZoneId.systemDefault ()).toLocalDateTime ());
					}
					catch (NoSuchFileException l_exception) {
						Publisher.logErrorInformation (l_exception);
					}
				}
				// 'Win32~'s are really ignored while pretend to be processed.
			}
		}
		for (String l_unsupportedPropertyName: l_unsupportedPropertyNames) {
			sendResponseFragment (l_httpResponseOutputStream, String.format ("				<D:%s/>\n", l_unsupportedPropertyName), false);
		}
		sendResponseFragment (l_httpResponseOutputStream, c_webDavResponseBodyPropFooter, false);
		sendResponseFragment (l_httpResponseOutputStream, String.format (c_webDavResponseBodyStatutsTag, l_propertyStatusCode), false);
		sendResponseFragment (l_httpResponseOutputStream, c_webDavResponseBodyPropstatFooter, false);
		sendResponseFragment (l_httpResponseOutputStream, c_webDavResponseBodyResponseFooter, false);
		sendResponseFragment (l_httpResponseOutputStream, c_webDavResponseBodyMultiStatusFooter, false);
		
		l_httpResponseOutputStream.close ();
		return l_httpResponseStatus;
	}

留意事項として、'Win32~'プロパティに対して'<D:%s/>'を送るのは本当は正しくない、'Win32~'は'Z="urn:schemas-microsoft-com:"'に属していないので。


4-4: 'GET'


Hypothesizer 7
'GET'を実装するのは、またしてもとても容易だ: サーバーは単に、当該リソースが存在すれば、当該リソースのコンテンツをリターンし、そうでなければ、'404'レスポンスをリターンするだけだ。

以下が、私のコードだ。

@Java ソースコード
	private int handleGetRequest (HttpExchange a_httpExchange) throws IOException, SAXException {
		URI l_httpRequestUri = a_httpExchange.getRequestURI ();
		Headers l_httpRequestHeader = a_httpExchange.getRequestHeaders ();
		Headers l_httpResponseHeader = a_httpExchange.getResponseHeaders ();
		InputStream l_httpRequestInputStream = a_httpExchange.getRequestBody ();
		OutputStream l_httpResponseOutputStream = a_httpExchange.getResponseBody ();
		int l_httpResponseStatus = 404;
		
		String l_resourceUriString = URLDecoder.decode (l_httpRequestUri.toString (), CharactersSetNamesConstantsGroup.c_utf8CharactersSetName);
		i_webDavRequestDatumSaxHandler.initialize ();
		try {
			i_saxParser.parse (new BufferedReader (new InputStreamReader (l_httpRequestInputStream, CharactersSetNamesConstantsGroup.c_utf8CharactersSetName), DefaultValuesConstantsGroup.c_smallBufferSize), i_webDavRequestDatumSaxHandler);
		}
		catch (SAXParseException l_exception) {
			// comes here if the input stream is empty
		}
		if (l_resourceUriString.endsWith (String.valueOf (GeneralConstantsConstantsGroup.c_linuxDirectoriesDelimiter))) {
			l_httpResponseStatus = 200;
			a_httpExchange.sendResponseHeaders (l_httpResponseStatus, 0);
		}
		else {
			InputStream l_contentsInputStream = null;
			try {
				l_contentsInputStream = Files.newInputStream (i_contentsBaseDirectoryAbsolutePath.resolve (Paths.get (l_resourceUriString.substring (1))),  StandardOpenOption.READ);
				l_httpResponseStatus = 200;
				a_httpExchange.sendResponseHeaders (l_httpResponseStatus, 0);
				byte [] l_bytesArray = new byte [c_bufferSize];
				int l_readFunctionReturn = GeneralConstantsConstantsGroup.c_unspecifiedInteger;
				while ( (l_readFunctionReturn = l_contentsInputStream.read (l_bytesArray, GeneralConstantsConstantsGroup.c_iterationStartNumber, c_bufferSize)) != InputPropertiesConstantsGroup.c_noMoreData) {
					l_httpResponseOutputStream.write (l_bytesArray, GeneralConstantsConstantsGroup.c_iterationStartNumber, l_readFunctionReturn);
					l_httpResponseOutputStream.flush ();
				}
			}
			catch (IOException | SecurityException l_exception) {
				l_httpResponseStatus = 404;
				a_httpExchange.sendResponseHeaders (l_httpResponseStatus, 0);
			}
			finally {
				if (l_contentsInputStream != null) {
					l_contentsInputStream.close ();
					l_contentsInputStream = null;
				}
			}
		}
		
		l_httpResponseOutputStream.close ();
		return l_httpResponseStatus;
	}


4-5: 'PUT'


Hypothesizer 7
'PUT'を実装するのは、またしてもまたしてもとても容易だ: サーバーは当該コンテンツをリクエストボディとして受け取り、そのコンテンツを好きなようにできる、'200'(当該リソースが上書きされたことを意味する)または'201'(当該リソースが新たに作成されたことを意味する)レスポンスをリターンして。

以下が、私のコードだ。

@Java ソースコード
	private int handlePutRequest (HttpExchange a_httpExchange) throws IOException {
		URI l_httpRequestUri = a_httpExchange.getRequestURI ();
		Headers l_httpRequestHeader = a_httpExchange.getRequestHeaders ();
		Headers l_httpResponseHeader = a_httpExchange.getResponseHeaders ();
		InputStream l_httpRequestInputStream = a_httpExchange.getRequestBody ();
		OutputStream l_httpResponseOutputStream = a_httpExchange.getResponseBody ();
		int l_httpResponseStatus = 404;
		
		boolean l_requestIsAccepted = false;
		
		l_requestIsAccepted = true;
		
		String l_resourceUriString = URLDecoder.decode (l_httpRequestUri.toString (), CharactersSetNamesConstantsGroup.c_utf8CharactersSetName);
		if (l_requestIsAccepted) {
			OutputStream l_contentsOutputStream = null;
			boolean l_outputFileExisted = false;
			try {
				Path l_resourcePath = i_contentsBaseDirectoryAbsolutePath.resolve (Paths.get (l_resourceUriString.substring (1)));
				try {
					l_contentsOutputStream = Files.newOutputStream (l_resourcePath, StandardOpenOption.CREATE);
				}
				catch (IOException l_exception) {
					l_contentsOutputStream = Files.newOutputStream (l_resourcePath, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
					l_outputFileExisted = true;
				}
				byte [] l_bytesArray = new byte [c_bufferSize];
				int l_readFunctionReturn = GeneralConstantsConstantsGroup.c_unspecifiedInteger;
				while ( (l_readFunctionReturn = l_httpRequestInputStream.read (l_bytesArray, GeneralConstantsConstantsGroup.c_iterationStartNumber, c_bufferSize)) != InputPropertiesConstantsGroup.c_noMoreData) {
					l_contentsOutputStream.write (l_bytesArray, GeneralConstantsConstantsGroup.c_iterationStartNumber, l_readFunctionReturn);
					l_contentsOutputStream.flush ();
				}
			}
			catch (IOException | SecurityException l_exception) {
			}
			finally {
				if (l_contentsOutputStream != null) {
					l_contentsOutputStream.close ();
					l_contentsOutputStream = null;
				}
			}
			if (l_outputFileExisted) {
				l_httpResponseStatus = 200;
			}
			else {
				l_httpResponseStatus = 201;
			}
		}
		else {
			l_httpResponseStatus = 412;
		}
		a_httpExchange.sendResponseHeaders (l_httpResponseStatus, -1);
		
		return l_httpResponseStatus;
	}


5: 上記のものは、仕様に100%準拠してはいないが、一部の状況では十分に動作する


Hypothesizer 7
以上だ。

. . . 本当に?えーと、上記のものは、仕様に100%準拠してはいないが(当該RFCが'MUST'だと言っている全てをサーバーは行なっているわけではない)、私の目的(ありうる限りの任意のWebDAVクライアントに仕えることではなく、あるWebDAVクライアントとコンテンツをやり取りすることだ)においては十分動作する。

実のところ、ここに私のプロジェクトがある(それは、この記事にしたがってビルドすることができる)。

以下が、そのサーバーを起動するコマンドだ。

@bash or cmd ソースコード
gradle i_executeJarTask -Pc_mainClassName="theBiasPlanet.webDavServer.programs.WebDavServerConsoleProgram" -Pc_commandLineArguments="localhost 8080 10 %the contents base directory absolute path%"

例えば、Linuxでの'cadver'は、以下に対して、問題なく動作する。

@bash ソースコード
cadaver  http://localhost:8080/
ls /
get TestFile.txt TestFile.txt.copied
put TestFile.txt.copied TestFile.txt.copied2

Windowsでの'WinSCP'は、ログインすること、ファイル群を取得すること、ファイル群を投入することに対して、問題なく動作する。

Windowsでの'Explorer'は、ログインすること、ファイル群を取得することに対して問題なく動作するが、ファイル群を投入することに対してはそうでない、なぜなら、それは、私のサーバーが実装していない'LOCK'を不可避にコールするからである。


6: さらに先に行くには


Hypothesizer 7
ロックメカニズムがほしければ、私は、'LOCK'および'UNLOCK'メソッドを実装するだろう('PROPFIND'および'PUT'のハンドラーたちを微調整してロックを認識するようにさせることも必要になるだろう。

コピー、移動、削除といった、より多くの機能が必要であれば、私は、対応するメソッド群を実装するだろう。

いずれにせよ、行なうべきことは、基本的に言って、HTTPプロトコル上でXMLデータをやりとりすることだけである。

詳細は、当該RFCドキュメントに記載されている。


参考資料


  • The IETF Trust. (2007). RFC 4918 - HTTP Extensions for Web Distributed Authoring and Versioning (WebDAV). Retrieved from https://tools.ietf.org/html/rfc4918
  • WinSCP.net. (2020). WinSCP :: Official Site :: Free SFTP and FTP client for Windows. Retrieved from https://winscp.net/
<このシリーズの前の記事 | このシリーズの目次 |