2020-11-08

4: Implement WebDAV Server on One's Own: a Minimal Background

<The previous article in this series | The table of contents of this series |

The WebDAV protocol is not technically difficult to implement. Any 3rd-party library is not particularly necessary.

Topics


About: The Java programming language
About: WebDAV

The table of contents of this article


Starting Context


  • The reader has a basic knowledge on the HTTP protocol.

Target Context


  • The reader will know a minimal background for implementing a WebDAV server on his or her own.

Orientation


Although this article is in the series for Java, the knowledge introduced here will be useful for any programming language.

The remaining knowledge can be gotten from a RFC document.


Main Body


1: The WebDAV Protocol Is an Extension of the HTTP Protocol


Hypothesizer 7
I had imagined (groundlessly) that I would have to do a TCP/IP socket programming, in order to implement the WebDAV protocol.

That is not true: the WebDAV protocol is really an extension of the HTTP protocol.

What does that mean? Well, while most famously the 'GET' and 'POST' methods are used in the HTTP protocol, also some additional methods are used in the WebDAV protocol.

So, all what I need to do is to write a handler that receives HTTP requests of those methods and returns appropriate HTTP responses.

Each request data is represented in the HTTP request header and the HTTP request body, which is really a XML datum.

Each response data is represented in the HTTP response header (including the status code) and the HTTP response body, which is a XML datum, as can be guessed.

Quite simple, huh?


2: There Are Some Fulfillment Levels


Hypothesizer 7
It is not that any WebDAV server has to fulfill the whole set of possible requests.

Especially, a WebDAV server does not need to fulfill the 'LOCK' method, which (not fulfilling 'LOCK') is the level, '1'.

The level, '2', fulfills the 'LOCK' method, but not necessarily supports all the 'SHOULD' specifications (which is about any method, not only about the 'LOCK' method).

What does "'SHOULD' specifications" mean? Well, the RFC document says that some things 'MUST' be implemented while some other things 'SHOULD' be implemented; so those 'SHOULD' requirements do not necessarily have to be implemented.

The level, '3', fulfills all the 'SHOULD' specifications (to say nothing of all the 'MUST' specifications).


3: The Methods


Hypothesizer 7
These are the methods that can be implemented: 'OPTIONS', 'PROPFIND', 'PROPPATCH', 'GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'COPY', 'MOVE', 'MKCOL', 'LOCK', and 'UNLOCK'.

Especially, the first 5 methods are important.

'OPTIONS' is for knowing (in the viewpoint of the client) what kinds of requests are fulfilled for the specified resource (which may or may not exist already) by the server (the response may depend on the resource).

'PROPFIND' is for knowing (in the viewpoint of the client) some properties (for example, the last modified date and time) of the specified resource.

'PROPPATCH' is for setting (in the viewpoint of the client) some properties (for example, the last modified date and time) of the specified resource.

'GET' is for retrieving (in the viewpoint of the client) the contents of the specified resource.

'PUT' is for sending (in the viewpoint of the client) the specified datum as the contents of the specified resource, which may or may not exist already.

Let me see how to implement the 5 methods in the following section.


4: Implementing the 5 Important Methods


Hypothesizer 7
In preparation, I have to have the skeleton of the HTTP server.

Anything appropriate in any programming language can be used of course, but here, I use 'com.sun.net.httpserver.HttpServer' in Java. This is my skeleton.

theBiasPlanet/webDavServer/programs/WebDavServerConsoleProgram.java

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

The 'WebDavRequestDatumSaxHandler' class is for parsing any XML request body, whose items can be retrieved via the 'getItemValue (String a_itemPathString)' method or the 'getItemPathStringToValueMap ()' method.

The 'sendResponseFragment (OutputStream a_outputStream, String a_responseFragment, boolean a_isEscaped)' method is a utility method that sends the specified response body fragment, to be used hereafter.

While there are some utility classes used there (like 'theBiasPlanet.coreUtilities.filesHandling.FilesHandler'), they will not be explained here, because they are not any issue here and what (if not how) their methods do are obvious.

Here, I will go with the level, '1'.


4-1: 'OPTIONS'


Hypothesizer 7
Implementing 'OPTIONS' is quite easy: there is no request body or response body.

My server is going to return this response header for any resource (even if the resource does not exist).

@Output
DAV: 1
MS-Author-Via: DAV

Some Microsoft clients seems to require "MS-Author-Via".

This is my code.

@Java Source Code
	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
In fact, this is the most important part: as the client will (most probably) use this method before and after it calls 'PUT', I need to implement this method in order to let 'PUT' happen.

As for this method, if the specified resource does not exist, I return just a '404' response; otherwise, I return a '207' or '403' response.

The request should have a 'Depth' header, which will be '0' (which means that only the specified resource is being queried), '1' (which means that the specified resource (supposing that it is a directory, which is called 'container' in the RFC) and the resources contained in it are being queried), or 'Infinity' (which means that the specified resource (supposing that it is a directory) and all the resources under the specified resource are being queried).

The 'Infinity' can be legitimately rejected, which means that the server returns a '403' response with this response body.

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

Here, my server will do so, and in fact, it will not allow any sub directory (just for making the program simple).

The request body is one of these 4 types: 1) nothing; 2) requiring the allowed property names; 3) requiring all the properties; 4) requiring the specified properties.

1) is really the same with 3) in the meaning.

2) is like this.

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

My server is going to send a response body like this.

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

Note that 'getcontenttype' exists only when the resource is not any directory.

3) is like this.

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

My server is going to send a response body like this.

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

Note that "resourcetype" is empty if the resource is not any directory; "getcontenttype" exists only if the resource is not any directory; %the resource contents type% is like 'application/octet-stream'; and the RFC1123 format is like 'Wed, 04 Nov 2020 03:53:17 UTC'.

4) is like this.

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

My server is going to send a response body like this.

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

Note that "anUnsupportedPropertyName" is responded with "404" because it is not supported by this server.

How the resources really exist (for example, they may exist in a file system as usual, in a relational database, just in the memory, or in nowhere really at all) is arbitrary by the server, but probably, any resource put by the 'PUT' method will at least have to pretend to exist for a while, because the client tends to inevitably try to access the resource just after the 'PUT' method call, in order to set some properties or just confirm the result of the 'PUT' method call.

Here, the resources exist in a pre-specified directory (as specified in the 'i_contentsBaseDirectoryAbsolutePath' field) of a file system.

This is my code (which is rather long, but is really straightforward).

@Java Source Code
	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
As the client will (most probably) use this method after it calls 'PUT', I need to implement this method in order to assure the client that the 'PUT' call has succeeded, even if my server is not interested in such properties.

If the specified resource does not exist, I return just a '404' response; otherwise, I return a '207' response.

The request body is like this.

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

My server is going to send a response body like this.

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

There may be a 'remove' item along with or instead of the 'set', although I do not really anticipate any request for removing the created date and time or the last modified date and time.

Windows 'Explorer' will send some 'Win32~' properties, which my server does not process, but pretends to have processed.

This is my code.

@Java Source Code
	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;
	}

Mind that sending '<D:%s/>' for any 'Win32~' property is not really correct, as 'Win32~' does not belong to 'Z="urn:schemas-microsoft-com:"'.


4-4: 'GET'


Hypothesizer 7
Implementing 'GET' is again quite easy: the server just returns the contents of the resource if the resource exists, or returns a '404' response.

This is my code.

@Java Source Code
	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
Implementing 'PUT' is again and again quite easy: the server receives the contents as the request body and can do whatever with the contents, returning a '200' (which means that the resource has been overwritten) or '201' (which means that the resource has been newly created) response.

This is my code.

@Java Source Code
	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: That Is Not 100% Compliant with the Specifications, but Works Enough in Some Situations


Hypothesizer 7
That is all.

. . . Really? Well, that is not 100% compliant with the specifications (the server is not doing everything that the RFC says 'MUST'), but works enough in my purpose, which is not to serve any possible WebDAV client in any possible way, but to exchange contents with a WebDAV client.

In fact, here is my project (which can be built according to this article).

This is the command to start the server.

@bash or cmd Source Code
gradle i_executeJarTask -Pc_mainClassName="theBiasPlanet.webDavServer.programs.WebDavServerConsoleProgram" -Pc_commandLineArguments="localhost 8080 10 %the contents base directory absolute path%"

For example, 'cadver' in Linux works fine for this.

@bash Source Code
cadaver  http://localhost:8080/
ls /
get TestFile.txt TestFile.txt.copied
put TestFile.txt.copied TestFile.txt.copied2

'WinSCP' in Windows works fine for logging in, getting files, and putting files.

'Explorer' in Windows works fine for logging in and getting files, but not for putting files, because it inevitably calls 'LOCK', which is not implemented in my server.


6: To Go Further


Hypothesizer 7
If I want a locking mechanism, I will implement the 'LOCK' and 'UNLOCK' methods (also 'PROPFIND' and 'PUT' handlers will have to be tweaked to be aware of locks).

If I have to have more functionalities like copying, moving, and deleting, I will implement the corresponding methods.

Anyway, what to be done is basically just exchanging XML data on the HTTP protocol.

The details are described in the RFC document.


References


  • 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/
<The previous article in this series | The table of contents of this series |