All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.globalmentor.servlet.http.AbstractHTTPServlet Maven / Gradle / Ivy

The newest version!
/*
 * Copyright © 1996-2008 GlobalMentor, Inc. 
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.globalmentor.servlet.http;

import java.io.*;
import java.net.*;
import java.security.Principal;
import java.util.*;
import java.util.regex.Pattern;

import javax.servlet.*;
import javax.servlet.http.*;
import javax.xml.parsers.DocumentBuilder;

import static com.globalmentor.java.CharSequences.*;
import static com.globalmentor.java.Characters.*;
import static com.globalmentor.java.Classes.getLocalName;
import static com.globalmentor.net.HTTP.*;
import static com.globalmentor.net.URIs.*;
import static com.globalmentor.net.http.webdav.WebDAV.*;
import static com.globalmentor.servlet.http.HTTPServlets.*;
import static com.globalmentor.text.Text.*;
import static java.nio.charset.StandardCharsets.*;

import com.globalmentor.collections.Collections;
import com.globalmentor.io.*;
import com.globalmentor.java.Characters;
import com.globalmentor.net.*;
import com.globalmentor.net.http.*;
import com.globalmentor.text.SyntaxException;
import com.globalmentor.xml.XMLSerializer;
import com.globalmentor.xml.spec.XML;

import org.w3c.dom.*;
import org.xml.sax.SAXException;

/**
 * The base servlet class for implementing an HTTP server that access resources.
 * @see RFC 2616
 * @see Caching Tutorial
 * @author Garret Wilson
 */
public abstract class AbstractHTTPServlet extends BaseHTTPServlet { //TODO address http://lists.w3.org/Archives/Public/w3c-dist-auth/1999OctDec/0343.html

	private static final long serialVersionUID = 2887805688495237552L;

	/** Whether a directory listing is allowed for GET on a collection. */
	protected static final boolean LIST_DIRECTORIES = false; //TODO fix

	/** Whether access is restricted to read methods. */
	private boolean readOnly = false;

	/** @return Whether access is restricted to read methods. */
	protected boolean isReadOnly() {
		return readOnly;
	}

	/**
	 * Sets whether access is restricted to read methods.
	 * @param readOnly The new read-only status.
	 */
	protected void setReadOnly(final boolean readOnly) {
		this.readOnly = readOnly;
	}

	/**
	 * An array of regular expressions matching user agents not correctly supporting redirects.
	 * @see https://lists.w3.org/Archives/Public/w3c-dist-auth/2002AprJun/0190.html
	 * @see https://skrb.org/ietf/http_errata.html#saferedirect
	 * @see https://httpd.apache.org/docs/2.0/env.html#special
	 */
	private static final Pattern[] REDIRECT_UNSUPPORTED_AGENTS = new Pattern[] {Pattern.compile("^gnome-vfs.*"), //Gnome; see http://bugzilla.gnome.org/show_bug.cgi?id=92908 ; https://bugzilla.redhat.com/beta/show_bug.cgi?id=106290
			//TODO del				"gnome-vfs/*"	//see http://mail.gnome.org/archives/gnome-vfs-list/2002-December/msg00028.html
			Pattern.compile("Microsoft Data Access Internet Publishing Provider.*"), //http://lists.w3.org/Archives/Public/w3c-dist-auth/2002AprJun/0190.html
			Pattern.compile("Microsoft-WebDAV-MiniRedir/5\\.1\\.2600.*"), //http://mailman.lyra.org/pipermail/dav-dev/2003-June/004777.html
			Pattern.compile("^DAVAccess/1\\.[01234]\\.[1234].*"), //iCal; see http://macintouch.com/panreader02.html
			Pattern.compile("^Dreamweaver-WebDAV-SCM1\\.0[23].*"), //Dreamweaver MX, 2003; see http://archive.webct.com/docs/mail/nov03/0018.html
			Pattern.compile("^neon.*"), //neon; see http://archive.webct.com/docs/mail/nov03/0018.html ; http://www.mail-archive.com/[email protected]/msg53373.html
			Pattern.compile("^WebDAVFS.*"), //Macintosh OS X Jaquar; see http://www.askbjoernhansen.com/archives/2002/08/27/000115.html
			//TODO del				"^WebDAVFS/1.[012]",	//Macintosh; see http://www.macosxhints.com/article.php?story=20021114063433862
			Pattern.compile("^WebDrive.*") //http://lists.w3.org/Archives/Public/w3c-dist-auth/2002AprJun/0190.html
	};

	/**
	 * Determines if the user agent sending the request supports redirects. An agent is assumed to support redirects unless its name is recognized as an agent not
	 * supporting redirects.
	 * @param request The HTTP request.
	 * @return true if the agent sending the request is not known to be redirect-averse.
	 * @see #REDIRECT_UNSUPPORTED_AGENTS
	 */
	protected static boolean isRedirectSupported(final HttpServletRequest request) { //TODO maybe transfer this to BasicServlet
		final String userAgent = getUserAgent(request); //get the user agent sending this request
		for(final Pattern pattern : REDIRECT_UNSUPPORTED_AGENTS) { //look at each agent not supporting redirects
			if(pattern.matcher(userAgent).matches()) { //if this is a buggy user agent
				return false; //show that we recognize this user agent as one not correctly supporting redirects
			}
		}
		return true; //if we didn't recognize the user agent, we assume it supports redirects
	}

	/**
	 * Services the OPTIONS method.
	 * @param request The HTTP request.
	 * @param response The HTTP response.
	 * @throws ServletException if there is a problem servicing the request.
	 * @throws IOException if there is an error reading or writing data.
	 */
	public void doOptions(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {
		final URI resourceURI = getResourceURI(request); //get the URI of the requested resource
		//TODO del getLogger().trace("doing options for URI {}", resourceURI);
		final Set allowedMethodSet = getAllowedMethods(request, resourceURI); //get the allowed methods
		response.addHeader(ALLOW_HEADER, Collections.toString(allowedMethodSet, COMMA_CHAR)); //put the allowed methods in the "allow" header, separated by commas
		response.setContentLength(0); //set the content length to zero, according to the HTTP specification for OPTIONS
	}

	/**
	 * Services the HEAD method.
	 * @param request The HTTP request.
	 * @param response The HTTP response.
	 * @throws ServletException if there is a problem servicing the request.
	 * @throws IOException if there is an error reading or writing data.
	 */
	public void doHead(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {
		serveResource(request, response, false); //serve the resource without its content
	}

	/**
	 * Services the GET method.
	 * @param request The HTTP request.
	 * @param response The HTTP response.
	 * @throws ServletException if there is a problem servicing the request.
	 * @throws IOException if there is an error reading or writing data.
	 */
	public void doGet(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {
		serveResource(request, response, true); //serve the resource with its content
		//TODO ignore any broken pipe error
	}

	/**
	 * Services the POST method. This version delegates to doGet().
	 * @param request The HTTP request.
	 * @param response The HTTP response.
	 * @throws ServletException if there is a problem servicing the request.
	 * @throws IOException if there is an error reading or writing data.
	 * @see #doGet(HttpServletRequest, HttpServletResponse)
	 */
	public void doPost(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {
		doGet(request, response); //delegate to the GET method servicing
	}

	/**
	 * Services the PUT method.
	 * @param request The HTTP request.
	 * @param response The HTTP response.
	 * @throws ServletException if there is a problem servicing the request.
	 * @throws IOException if there is an error reading or writing data.
	 */
	public void doPut(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {
		//	TODO del getLogger().trace("getting resource URI");
		final URI resourceURI = getResourceURI(request); //get the URI of the requested resource
		getLogger().trace("checking destination existence");
		final boolean exists = exists(request, resourceURI); //see whether the resource already exists
		getLogger().trace("exists? {}", exists);
		final InputStream inputStream = request.getInputStream(); //get an input stream from the request
		final OutputStream outputStream; //we'll determine the output stream to use
		if(exists) { //if this resource exists
			final R resource = getResource(request, resourceURI); //get the resource information
			outputStream = getOutputStream(request, resource); //get an output stream to the resource
		} else { //if the resource doesn't exist
			try {
				outputStream = createResource(request, resourceURI); //create a new resource
			} catch(final IllegalArgumentException illegalArgumentException) { //if this is an invalid resource URI
				//			TODO del getLogger().warn("Illegal argument.", illegalArgumentException);
				throw new HTTPForbiddenException(illegalArgumentException); //forbid creation of resources with invalid URIs
			}
		}
		try {
			getLogger().trace("trying to write");
			IOStreams.copy(inputStream, outputStream); //copy the file from the request to the resource
			getLogger().trace("written");
		} finally {
			/*TODO del; doesn't work
						if(outputStream instanceof FileOutputStream) {	//TODO fix; testing to put file in known state; this is at best a temporary fix, as the file output stream may by wrapped by a buffered stream; better to create a FileOutputStream wrapper
			getLogger().trace("syncing file output stream {}", resource.getReferenceURI());
							((FileOutputStream)outputStream).getFD().sync();
						}
			*/
			/*TODO test
						if(outputStream instanceof FileOutputStream) {	//TODO fix; testing to put file in known state; this is at best a temporary fix, as the file output stream may by wrapped by a buffered stream; better to create a FileOutputStream wrapper
			getLogger().trace("forcing file output stream {}", resourceURI);
							((FileOutputStream)outputStream).getChannel().force(true);
						}
			*/
			//TODO del getLogger().trace("closing output stream for resource URI {}", resourceURI);
			outputStream.close(); //always close the output stream
			//TODO del getLogger().trace("closed output stream; now content of resource is {}", getContentLength(request, getResource(request, resourceURI)));
		}

		/*TODO del when works
				final R resource;	//we'll get the existing resource, if there is one 
				if(exists) {	//if this resource exists
					resource=getResource(request, resourceURI);	//get the resource information
		    }
				else {	//if the resource doesn't exist
		//		TODO del getLogger().trace("trying to create resource");
					try
					{
						resource=createResource(resourceURI);	//create the resource TODO make sure no default resource content is created here
					}
					catch(final IllegalArgumentException illegalArgumentException) {	//if this is an invalid resource URI
		//			TODO del getLogger().warn("Illegal argument.", illegalArgumentException);
							throw new HTTPForbiddenException(illegalArgumentException);	//forbid creation of resources with invalid URIs
					}
				}
				try
				{
					final InputStream inputStream=request.getInputStream();	//get an input stream from the request
					final OutputStream outputStream=getOutputStream(request, resource);	//get an output stream to the resource
					try
					{
		getLogger().trace("trying to write");
						OutputStreamUtilities.copy(inputStream, outputStream);	//copy the file from the request to the resource
					}
					catch(final IllegalArgumentException illegalArgumentException) {	//if this is an invalid resource URI
		//			TODO del getLogger().warn("Illegal argument.", illegalArgumentException);
							throw new HTTPForbiddenException(illegalArgumentException);	//forbid creation of resources with invalid URIs
					}
					finally
					{
		*/
		/*TODO del; doesn't work
						if(outputStream instanceof FileOutputStream) {	//TODO fix; testing to put file in known state; this is at best a temporary fix, as the file output stream may by wrapped by a buffered stream; better to create a FileOutputStream wrapper
		getLogger().trace("syncing file output stream {}", resource.getReferenceURI());
							((FileOutputStream)outputStream).getFD().sync();
						}
		*/
		/*TODO del when works
						if(outputStream instanceof FileOutputStream) {	//TODO fix; testing to put file in known state; this is at best a temporary fix, as the file output stream may by wrapped by a buffered stream; better to create a FileOutputStream wrapper
		getLogger().trace("forcing file output stream {}", resource.getReferenceURI());
							((FileOutputStream)outputStream).getChannel().force(true);
						}
		
		getLogger().trace("closing output stream of resource type {}", resource.getClass());
						outputStream.close();	//always close the output stream
		getLogger().trace("closed output stream; now content of resource is {}", getContentLength(request, resource));
					}
				}
		*/
		/*TODO fix if needed
		catch(final IOException ioException) {	//if we have any problems saving the resource contents
			if(!exists) {	//if the resource didn't exist before
				deleteResource(resource);	//delete the resource, as we weren't able to save its contents
			}
			throw ioException;	//rethrow the exception
		}
		*/
		getLogger().trace("done PUT; determining response");
		if(exists) { //if the resource already existed
			getLogger().trace("PUT already existed; returning SC_NO_CONTENT");
			response.setStatus(HttpServletResponse.SC_NO_CONTENT); //indicate success by showing that there is no content to return
			response.setContentLength(0); //TODO check; this seems to be needed---should we throw an HTTPException or set the response instead?
		} else { //if the resource did not exist already
			getLogger().trace("PUT resource didn't already exist; returning SC_CREATED");
			response.setStatus(HttpServletResponse.SC_CREATED); //indicate that we created the resource
			response.setContentLength(0); //TODO check; this seems to be needed---should we throw an HTTPException or set the response instead?
		}
	}

	/**
	 * Services the DELETE method.
	 * @param request The HTTP request.
	 * @param response The HTTP response.
	 * @throws ServletException if there is a problem servicing the request.
	 * @throws IOException if there is an error reading or writing data.
	 */
	public void doDelete(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {
		final URI resourceURI = getResourceURI(request); //get the URI of the requested resource
		final boolean exists = exists(request, resourceURI); //see whether the resource already exists
		final R resource; //we'll get the existing resource, if there is one 
		if(exists) { //if this resource exists
			resource = getResource(request, resourceURI); //get the resource information
			deleteResource(request, resource); //delete the resource
		} else { //if the resource does not exist
			throw new HTTPNotFoundException(resourceURI.toString()); //show that we didn't find a resource for which to find properties					
		}
	}

	/**
	 * Services the GET, HEAD, and POST methods. The response will be compressed if supported by the user agent. This method delegates to
	 * {@link #serveResource(HttpServletRequest, HttpServletResponse, Resource, boolean)} after verifying that the resource exists.
	 * @param request The HTTP request.
	 * @param response The HTTP response.
	 * @param serveContent true if the contents of the resource should be returned.
	 * @throws ServletException if there is a problem servicing the request.
	 * @throws IOException if there is an error reading or writing data.
	 */
	protected void serveResource(final HttpServletRequest request, final HttpServletResponse response, final boolean serveContent)
			throws ServletException, IOException {
		final URI resourceURI = getResourceURI(request); //get the URI of the requested resource
		//	TODO del getLogger().trace("serving resource {}", resourceURI);
		if(exists(request, resourceURI)) { //if this resource exists
			//TODO del getLogger().trace("resource exists? {}", resourceURI);
			//TODO check if headers
			final R resource = getResource(request, resourceURI); //get a resource description
			serveResource(request, response, resource, serveContent); //serve the resource
		} else { //if the resource does not exist
			throw new HTTPNotFoundException(resourceURI.toString()); //show that we didn't find a resource for which to find properties					
		}
	}

	/**
	 * Serves a resource that has been verified to exist The response will be compressed if supported by the user agent.
	 * @param request The HTTP request.
	 * @param response The HTTP response.
	 * @param resource The resource being served.
	 * @param serveContent true if the contents of the resource should be returned.
	 * @throws ServletException if there is a problem servicing the request.
	 * @throws IOException if there is an error reading or writing data.
	 */
	protected void serveResource(final HttpServletRequest request, final HttpServletResponse response, final R resource, final boolean serveContent)
			throws ServletException, IOException {
		if(isCollection(request, resource.getURI())) { //if the resource is a collection
			//TODO del getLogger().trace("is collection {}", resourceURI);
			if(LIST_DIRECTORIES) { //if we should list directories
				final Writer writer = response.getWriter();
				response.setContentType("text/plain");
				final List resourceList = getChildResources(request, resource);
				for(final R childResource : resourceList) {
					writer.append(childResource.toString()).append('\n');
				}

				//TODO fix          contentType = "text/html;charset=UTF-8";
			}
			/*TODO determine what to do here; if we throw a not-found exception, we prevent default resources from being returned for a collection.
			  		else {	//if we're not allowed to list directories
			  			throw new HTTPNotFoundException(resource.getURI().toString());	//show that we didn't find a resource to return				
			  		}
			*/
		}
		//TODO del check; prevents default resources being returned; maybe throw not found exception somewhere here if it's clear there's nothing there  	else	//if this resource is not a collection
		{
			//TODO del getLogger().trace("is not a collection; ready to send back file {}", resourceURI);
			final Date lastModifiedDate = getLastModifiedDate(request, resource); //get the last modified date of the resource
			if(lastModifiedDate != null) { //if we know when the resource was last modified; check this before adding headers, especially because we use weak validators (RFC 2616 10.3.5)---Last-Modified time is implicitly weak (RDF 2616 13.3.3)
				//TODO del getLogger().trace("last modified date: {}", new HTTPDateFormat().format(lastModifiedDate));
				final Date roundedLastModifiedDate = new Date((lastModifiedDate.getTime() / 1000) * 1000); //round the date to the nearest millisecond before using it to compare, because the incoming date only has a one-second precision and comparing with the incoming rounded date would result in data being sent back unnecessarily; see Hunter, Jason, _Java Servlet Programming_, Second Edition, page 59
				try {
					final Date ifModifiedSinceDate = getIfModifiedSinceDate(request); //get the If-Modified-Since date
					/*TODO del 
						if(ifModifiedSinceDate!=null)
						{
							getLogger().trace("If-Modified-Since: {}", new HTTPDateFormat().format(ifModifiedSinceDate));
							getLogger().trace("ready to compare ifModifiedSince {} and roundedLastModified {} lastModified {}", ifModifiedSinceDate.getTime(), roundedLastModifiedDate.getTime(), lastModifiedDate.getTime());
						}
					*/
					if(ifModifiedSinceDate != null && ifModifiedSinceDate.compareTo(roundedLastModifiedDate) <= 0) { //if there is an If-Modified-Since date and the resource was not modified since that date
						//TODO del getLogger().trace("Not modified---use the value in the cache!");
						throw new HTTPNotModifiedException(); //stop serving content and indicate that the resource has not been modified
					}
					//TODO add support for If-Unmodified-Since
				} catch(final SyntaxException syntaxException) { //TODO fix better
					throw new IllegalArgumentException(syntaxException);
				}
			}
			//    	TODO del getLogger().trace("ready to send back a file");
			final MediaType contentType = getContentType(request, resource); //get the content type of the resource
			if(contentType != null) { //if we know the content type
				//      	TODO del getLogger().trace("setting content type to: {}", contentType);
				//TODO del getLogger().trace("setting content type to: {}", contentType);	//TODO del
				response.setContentType(contentType.toString()); //tell the response which content type we're serving
			}
			if(HEAD_METHOD.equals(request.getMethod())) { //if this is a HEAD request, send back the content-length, but not for other methods, as we may compress the actual content TODO make sure this is the correct; RFC 2616 is ambiguous as to whether a HEAD content length should be the compressed length or the uncompresed length
				final long contentLength = getContentLength(request, resource); //get the content length of the resource
				if(contentLength >= 0) { //if we found a content length for the resource
					//      	TODO del getLogger().trace("setting content length to: {}", contentLength);
					assert contentLength < Integer.MAX_VALUE : "Resource size " + contentLength + " is too large.";
					response.setContentLength((int)contentLength); //tell the response the size of the resource      		
				}
			}
			if(lastModifiedDate != null) { //if we know when the resource was last modified
				setLastModified(response, lastModifiedDate); //set the last modified date header
			}
			if(serveContent) { //if we should serve content
				//TODO fix ranges
				final OutputStream outputStream; //we'll determine the output stream
				if(contentType != null && isText(contentType)) { //if this is a text content type TODO later add other content types, if they can be compressed
					//TODO del      			getLogger().trace("compressing content type: {}", contentType);
					outputStream = getCompressedOutputStream(request, response); //get the output stream, compressing it if we can TODO do we want to check for an IllegalStateException, and send back text if we can?
				} else { //if we don't know the content type, or it isn't text
					//TODO del      			getLogger().trace("not compressing content type: {}", contentType);
					outputStream = response.getOutputStream(); //get the output stream without compression, as this could be a binary resource, making compression counter-productive TODO do we want to check for an IllegalStateException, and send back text if we can?      			
				}
				try (final InputStream inputStream = new BufferedInputStream(getInputStream(request, resource))) { //get an input stream to the resource
					IOStreams.copy(inputStream, outputStream); //copy the input stream to the output stream
				}
				outputStream.close(); //if there are no errors, close the output stream, which will write the remaining compressed data, if this is a compressed output stream
			}
		}
	}

	/**
	 * Determines the URI of the requested resource.
	 * 

* If it is determined that the requested resource is located in another location, this method may throw an HTTPRedirectException with the new * location. This method may choose to continue processing the request (e.g. if a client cannot handle redirects) using a different URI; in this case the new * URI will be returned. *

* @param request The HTTP request indicating the requested resource. * @return The URI of the requested resource, which may be different from the URL specified in the request. * @throws HTTPRedirectException if the request should be redirected to another URI. * @see HttpServletRequest#getRequestURL() */ protected URI getResourceURI(final HttpServletRequest request) throws HTTPRedirectException { final URI requestedResourceURI = super.getResourceURI(request); //get the default resource URI for this request final URI resourceURI = getResourceURI(request, requestedResourceURI, request.getMethod(), null); //get the correct URI for the resource if(!resourceURI.equals(requestedResourceURI)) { //if the real resource URI is different from the one requested if(isRedirectSupported(request)) { //if redirection is supported by the user agent sending the request throw new HTTPMovedPermanentlyException(resourceURI); //report back that this resource has permanently moved to its correct location URI } } return resourceURI; //return the resource URI } /** * Determines if the request can be redirected to another URI. This usually occurs when a request for "path/to/collection" should really be to * "path/to/collection/", the former doesn't exist yet the latter is a collection, and the server wishes to automatically redirect to the latter. Some clients * (notably Microsoft WebDAV support) may not redirect correctly, or the server may not want to redirect for some other reason. This method determines whether * a redirect should occur. This version ensures that the user agent sending the request can property follow redirects. * @param request The HTTP request indicating the requested resource. * @param requestedResourceURI The requested absolute URI of the resource. * @param redirectResourceURI The URI to which a redirect may occur. * @return true if a redirect can be sent to redirect the client from the requested URI to the new URI. */ /*TODO del protected boolean canRedirect(final HttpServletRequest request, final URI requestedResourceURI, final URI redirectResourceURI) { return isRedirectSupported(request); //see if redirection is supported by the user agent sending the request } */ /** * Determines the URI of a requested resource, using an optional resource as an analogy. This method determines if a non-collection resource (i.e. one not * ending in '/') should represent a collection if one of the following conditions apply: *
    *
  • The given method is a collection-specific method.
  • *
  • The requested non-collection URI does not exist, but there exists a collection at the location of the URI with an appended '/'.
  • *
  • The analogous resource, if present, is a collection.
  • *
* @param request The HTTP request indicating the requested resource. * @param requestedResourceURI The requested absolute URI of the resource. * @param method The HTTP request method. * @param analogousResourceURI The URI of a resource to use by analogy, or null if no analogous resource is known. This parameter is useful when * used with the COPY or MOVE method. * @return The canonical URI of the requested resource, which may be different than the requested resource URI. */ protected URI getResourceURI(final HttpServletRequest request, final URI requestedResourceURI, final String method, final URI analogousResourceURI) { //TODO now that we pass the request, remove the method parameter because it is redundance URI resourceURI = requestedResourceURI; //start off assuming we'll use the requested URI // TODO del getLogger().trace("requested URI {}", requestedResourceURI); final String requestResourceURIString = requestedResourceURI.toString(); //get the string version of the request URI if(!endsWith(requestResourceURIString, PATH_SEPARATOR)) { //if the URI is not a collection URI final URI collectionURI = URI.create(requestResourceURIString + PATH_SEPARATOR); //add a trailing slash to get a collection URI //if this is a method referring to a collection if(MKCOL_METHOD.equals(method)) { //TODO make sure this goes in the correct servlet resourceURI = collectionURI; //use the collection URI } else if(analogousResourceURI != null) { //if there is an analogous resource //if the analogous resource ends with '/' if(endsWith(analogousResourceURI.toString(), PATH_SEPARATOR)) { resourceURI = collectionURI; //use the collection URI } } else { //if there is no analogous resource (don't do the liberal non-existence check if there is an analogous resource, because this could prevent MOVE or COPY from a non-collection to another non-collection when a similarly-named collection exists) try { //getLogger().trace("can we substitute {} for {}", collectionURI, requestedResourceURI); if(canSubstitute(request, requestedResourceURI, collectionURI)) { //if we can substitute the collection URI for the requested URI resourceURI = collectionURI; //use the collection URI } } catch(final IOException ioException) { //if there is an error checking existence or whether the resource is a collection getLogger().warn("I/O error.", ioException); //don't do anything major, now---the request will fail, later, and there's no forwarding to be done for error-prone resources } } } //TODO del getLogger().trace("using URI {}", resourceURI); return resourceURI; //return the resource URI we decided on } /** * Determines if another URI can be substituted for the requested URI. This usually occurs when a request for "path/to/collection" should really be to * "path/to/collection/", the former doesn't exist yet the latter is a collection, and the server wishes to automatically redirect to the latter. Note that it * may later be determined that redirect should not occur for whatever reason, and the resource at the substitute URI maybe used anyway in the background. * This version allows substitution if the requested URI does not exist, but the substitute URI is a collection. * @param request The HTTP request indicating the requested resource. * @param requestedResourceURI The requested absolute URI of the resource. * @param substituteResourceURI The URI to the URI which may be substituted for the first URI. * @return true if the provided URI may be substituted for the requested URI. * @throws IOException if there is an error checking whether URI substitution can occur. */ protected boolean canSubstitute(final HttpServletRequest request, final URI requestedResourceURI, final URI substituteResourceURI) throws IOException { //getLogger().trace("requested resource exists: {}", exists(request, requestedResourceURI)); //getLogger().trace("substitute resource is collection: {}", isCollection(request, substituteResourceURI)); return !exists(request, requestedResourceURI) && isCollection(request, substituteResourceURI); //if the resource doesn't exist, but the substitute resource is a collection, we can substitute } /** * Determines the URI of a requested resource from its requested URI. *

* If it is determined that the requested resource is located in another location, the new URI will be returned. *

* @param requestURI The absolute URI of the requested resource. * @return The URI of the requested resource, which may be different from the URI specified in the request. * @throws IllegalArgumentException if the given URI is not absolute with an absolute path. * @see HttpServletRequest#getRequestURL() */ /*TODO fix protected URI getResourceURI(final URI requestURI) { getLogger().trace("request URI {}", requestURI); final String requestURIString=requestURI.toString(); //get the string version of the request URI if(!endsWith(requestURIString, PATH_SEPARATOR)) { //if the URI is not a collection URI final URI redirectURI=URI.create(requestURIString+PATH_SEPARATOR); //add a trailing slash to get a collection URI final boolean isCollectionMethod=MKCOL_METHOD.equals(request.getMethod()); //see if this is a method referring to a collection final boolean redirect; //determine if we need to redirect if(isCollectionMethod) { //if this is a collection-specific method redirect=true; //redirect to the real collection URI } else { //if this is not a collection-specific method redirect=!exists(requestURI) && isCollection(redirectURI); //redirect if there is no such file but redirecting would take the client to a collection } if(redirect) { //if we should redirect getLogger().trace("sending redirect {}", redirectURI); if(isRedirectSupported(request)) { //if redirection is supported by the user agent sending the request throw new HTTPMovedPermanentlyException(redirectURI); //report back that this resource has permanently moved to its correct location URI } else { //if we can't redirect return redirectURI; //we'll just pretend they requested the correct URI } } } return requestURI; //return the requested URI } */ /** * Retrieves an XML document from the body of an HTTP request. * @param request The request from which to get the XML document. * @param documentBuilder The document builder to use for parsing the XML. * @return A document representing the XML information, or null if nothing but whitespace was included in the request. * @throws IOException if there is an error reading the XML. * @throws DOMException if there is an error creating the document. * @throws SAXException if there is an error parsing the document. */ protected Document getXML(final HttpServletRequest request, final DocumentBuilder documentBuilder) throws IOException, DOMException, SAXException { // TODO del getLogger().trace("getting XML"); final int contentLength = request.getContentLength(); //get the length of the request // TODO del getLogger().trace("content length {}", contentLength); //TODO del; no content length means no content assert contentLength>=0 : "Missing content length"; if(contentLength > 0) { //if content is present //TODO fix chunked coding final InputStream inputStream = request.getInputStream(); //get an input stream to the request content //TODO del getLogger().trace("Ready to get XML bytes of Content-Length: {}", contentLength); final byte[] content = InputStreams.readBytes(inputStream, contentLength); //read the request TODO check for the content being shorter than expected //TODO del getLogger().trace("got bytes: {}", new String(content, "UTF-8")); boolean hasContent = false; //we'll start out assuming there actually is no content for(final byte b : content) { //look at each byte in the content if(!Characters.isWhitespace((char)b)) { //if this byte doesn't represent whitespace (ignoring the encoding is fine, because another encoding would require content, so if we find non-whitespace it means there is some content) hasContent = true; //we have content break; //stop looking for content } } if(hasContent) { //if we have content final InputStream xmlInputStream = new ByteArrayInputStream(content); //create a new input stream from the bytes we read //TODO del getLogger().trace("ready to parse"); return documentBuilder.parse(xmlInputStream); //parse the bytes we read } } /*TODO del; we accept no content as no XML else if(contentLength<0) { //if no content length was given throw new HTTPLengthRequiredException(); //indicate that we require a content length } */ return null; //show that there is no content to return } /** * Places an XML document into the body of an HTTP response. The XML will be sent back compressed if supported by the user agent. * @param request The request for which this XML represents a response. * @param response The response into which to place the XML document. * @param document The XML document to place into the response. * @throws IOException if there is an error writing the XML. */ protected void setXML(final HttpServletRequest request, final HttpServletResponse response, final Document document) throws IOException { try (final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) { //create a byte array output stream to hold our outgoing data new XMLSerializer(true).serialize(document, byteArrayOutputStream, UTF_8); //serialize the document to the byte array with no byte order mark final byte[] bytes = byteArrayOutputStream.toByteArray(); //get the bytes we serialized //set the content type to text/xml; charset=UTF-8 response.setContentType(XML.MEDIA_TYPE.withCharset(UTF_8).toString()); //TODO del; this prevents compression response.setContentLength(bytes.length); //tell the response how many bytes to expect final OutputStream outputStream = getCompressedOutputStream(request, response); //get an output stream to the response, compressing the output if possible try (final InputStream inputStream = new ByteArrayInputStream(bytes)) { //get an input stream to the bytes IOStreams.copy(inputStream, outputStream); //write the bytes to the response } outputStream.close(); //if there are no errors, close the output stream, which will write the remaining compressed data, if this is a compressed output stream } } /** * Determines the HTTP methods allowed for the requested resource. * @param request The HTTP request indicating the requested resource. * @param resourceURI The URI of a resource for which options should be obtained. * @return A set of methods allowed for this resource. * @throws IOException if there is an error accessing the resource. */ protected Set getAllowedMethods(final HttpServletRequest request, final URI resourceURI) throws IOException { final Set allowedMethods = new HashSet(); //create a new set of method strings allowedMethods.add(OPTIONS_METHOD); //we always allow options if(exists(request, resourceURI)) { //if the resource exists allowedMethods.add(GET_METHOD); allowedMethods.add(HEAD_METHOD); allowedMethods.add(POST_METHOD); if(LIST_DIRECTORIES) { //if we allow directory listings // TODO implement methodSet.add(PROPFIND); } if(!isCollection(request, resourceURI)) { //if the resource is not a collection allowedMethods.add(PUT_METHOD); //allow saving a resource to this location } } else { //if the resource does not exist // TODO implement methodSet.add(LOCK); allowedMethods.add(PUT_METHOD); } // TODO implement methodSet.add(TRACE); return allowedMethods; //return the allowed methods } /** * Checks whether the given principal is authorized to invoke the given method on the given resource. This version restricts the HTTP methods PUT and DELETE * if the servlet is read-only. * @param request The HTTP request. * @param resourceURI The URI of the resource requested. * @param method The HTTP method requested on the resource. * @param principal The principal requesting authentication, or null if the principal is not known. * @param realm The realm with which the resource is associated, or null if the realm is not known. * @return true if the given principal is authorized to perform the given method on the resource represented by the given URI. * @throws HTTPInternalServerErrorException if there is an error determining if the principal is authorized. * @see #isReadOnly() */ protected boolean isAuthorized(final HttpServletRequest request, final URI resourceURI, final String method, final Principal principal, final String realm) throws HTTPInternalServerErrorException { if(!super.isAuthorized(request, resourceURI, method, principal, realm)) { //if the request does not pass default authorization return false; //don't allow authorization } if(isReadOnly()) { //if this servlet is read-only if(PUT_METHOD.equals(method) || DELETE_METHOD.equals(method)) { //disallow the PUT and DELETE methods return false; //don't allow write methods } } return true; //report that the request is authorized } /** * Determines the realm applicable for the resource indicated by the given URI This version returns the local class name of the servlet unless the servlet is * read-only, in case it returns null. * @param resourceURI The URI of the resource requested. * @return The realm appropriate for the resource, or null if the given resource is not in a known realm. * @throws HTTPInternalServerErrorException if there is an error getting the realm. * @see #isReadOnly() */ protected String getRealm(final URI resourceURI) throws HTTPInternalServerErrorException { return isReadOnly() ? null : getLocalName(getClass()); //return the local name of the servlet class unless the servlet is read-only } /** * Determines if the resource at a given URI exists. * @param request The HTTP request in response to which existence of the resource is being determined. * @param resourceURI The URI of the requested resource. * @return true if the resource exists, else false. * @throws IOException if there is an error accessing the resource. */ protected abstract boolean exists(final HttpServletRequest request, final URI resourceURI) throws IOException; /** * Determines if the resource at a given URI is an existing collection. * @param request The HTTP request in response to which the collection is being checked. * @param resourceURI The URI of the requested resource. * @return true if the resource is a collection, else false. * @throws IOException if there is an error accessing the resource. * @see #exists(HttpServletRequest, URI) */ protected abstract boolean isCollection(final HttpServletRequest request, final URI resourceURI) throws IOException; /** * Determines the requested resource. * @param request The HTTP request in response to which the resource is being retrieved. * @param resourceURI The URI of the requested resource. * @return An object providing an encapsulation of the requested resource, but not necessarily the contents of the resource. * @throws IllegalArgumentException if the given resource URI does not represent a valid resource. * @throws IOException if there is an error accessing the resource. */ protected abstract R getResource(final HttpServletRequest request, final URI resourceURI) throws IllegalArgumentException, IOException; /** * Determines the content type of the given resource. This default version returns the MIME content type servlet known by the servlet context. * @param request The HTTP request in response to which the content type is being retrieved. * @param resource The resource for which the content type should be determined. * @throws IOException if there is an error accessing the resource. * @return The content type of the given resource, or null if no content type could be determined. * @see ServletContext#getMimeType(java.lang.String) */ protected MediaType getContentType(final HttpServletRequest request, final R resource) throws IOException { return findRawName(resource.getURI()).map(getServletContext()::getMimeType) //ask the servlet context for the MIME type .map(MediaType::parse).orElse(null); //create a content type object if a content type string was returned } /** * Determines the content length of the given resource. * @param request The HTTP request in response to which the content length is being retrieved. * @param resource The resource for which the content length should be determined. * @return The content length of the given resource, or -1 if no content length could be determined. * @throws IOException Thrown if there is an error accessing the resource. */ protected abstract long getContentLength(final HttpServletRequest request, final R resource) throws IOException; /** * Determines the last modified date of the given resource. * @param request The HTTP request in response to which the last modified date is being retrieved. * @param resource The resource for which the last modified date should be determined. * @return The last modified date of the given resource, or null if no there is no known last modified date. * @throws IOException Thrown if there is an error accessing the resource. */ protected abstract Date getLastModifiedDate(final HttpServletRequest request, final R resource) throws IOException; /** * Retrieves an input stream to the given resource. * @param request The HTTP request in response to which the input stream is being retrieved. * @param resource The resource for which an input stream should be retrieved. * @return An input stream to the given resource. * @throws IOException Thrown if there is an error accessing the resource, such as a missing file or a resource that has no contents. */ protected abstract InputStream getInputStream(final HttpServletRequest request, final R resource) throws IOException; //TODO do we want to pass the resource or just the URI here? /** * Retrieves an output stream to the given resource. * @param request The HTTP request in response to which the output stream is being retrieved. * @param resource The resource for which an output stream should be retrieved. * @return An output stream to the given resource. * @throws IOException Thrown if there is an error accessing the resource. */ protected abstract OutputStream getOutputStream(final HttpServletRequest request, final R resource) throws IOException; //TODO do we want to pass the resource or just the URI here? /** * Creates a resource and returns an output stream for storing content. * @param request The HTTP request in response to which a resource is being created. If the resource already exists, it will be replaced. For collections, * {@link #createCollection(HttpServletRequest, URI)} should be used instead. * @param resourceURI The URI of the resource to create. * @return An output stream for storing content in the resource. * @throws IllegalArgumentException if the given resource URI does not represent a valid resource. * @throws IOException Thrown if there is an error creating the resource. * @throws HTTPConflictException if an intermediate collection required for creating this collection does not exist. * @see #createCollection(HttpServletRequest, URI) */ protected abstract OutputStream createResource(final HttpServletRequest request, final URI resourceURI) throws IllegalArgumentException, IOException, HTTPConflictException; /** * Creates a resource. For collections, {@link #createCollection(URI)} should be used instead. * @param resourceURI The URI of the resource to create. * @return The description of a newly created resource, or null if the resource is not allowed to be created. * @throws IllegalArgumentException if the given resource URI does not represent a valid resource in a valid burrow. * @throws IOException Thrown if there is an error creating the resource. * @throws HTTPConflictException if an intermediate collection required for creating this collection does not exist. * @see #createCollection(URI) */ //TODO del when works protected abstract R createResource(final URI resourceURI) throws IllegalArgumentException, IOException, HTTPConflictException; /** * Creates a collection resource. * @param request The HTTP request in response to which a collection is being created. * @param resourceURI The URI of the resource to create. * @return The description of a newly created resource, or null if the resource is not allowed to be created. * @throws IllegalArgumentException if the given resource URI does not represent a valid resource. * @throws IOException Thrown if there is an error creating the resource. * @throws HTTPConflictException if an intermediate collection required for creating this collection does not exist. * @see #createResource(HttpServletRequest, URI) */ protected abstract R createCollection(final HttpServletRequest request, final URI resourceURI) throws IllegalArgumentException, IOException, HTTPConflictException; /** * Deletes a resource. * @param request The HTTP request in response to which a resource is being deleted. * @param resource The resource to delete. * @throws IOException Thrown if the resource could not be deleted. */ protected abstract void deleteResource(final HttpServletRequest request, final R resource) throws IOException; /** * Retrieves an list of child resources of the given resource. * @param request The HTTP request in response to which child resources are being retrieved.. * @param resource The resource for which children should be returned. * @return A list of child resources. * @throws IOException Thrown if there is an error retrieving the list of child resources. */ protected abstract List getChildResources(final HttpServletRequest request, final R resource) throws IOException; //TODO do we want to pass the resource or just the URI here? }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy