001: package com.ibm.webdav.protocol.http;
002:
003: /*
004: * (C) Copyright IBM Corp. 2000 All rights reserved.
005: *
006: * The program is provided "AS IS" without any warranty express or
007: * implied, including the warranty of non-infringement and the implied
008: * warranties of merchantibility and fitness for a particular purpose.
009: * IBM will not be liable for any damages suffered by you as a result
010: * of using the Program. In no event will IBM be liable for any
011: * special, indirect or consequential damages or lost profits even if
012: * IBM has been advised of the possibility of their occurrence. IBM
013: * will not be liable for any third party claims against you.
014: *
015: * Portions Copyright (C) Simulacra Media Ltd, 2004.
016: */
017: import java.io.*;
018: import java.net.*;
019: import java.rmi.*;
020: import java.util.*;
021: import java.util.logging.*;
022:
023: import javax.servlet.*;
024: import javax.servlet.http.*;
025:
026: import com.ibm.webdav.*;
027: import com.ibm.webdav.impl.*;
028:
029: /**
030: * WebDAVMethod is the abstract superclass of classes corresponding to the
031: * WebDAV methods using the command pattern. It maintains the state and logic
032: * that is common to the execution of all webDAV methods. These subclasses
033: * are a good place to see how the WebDAV protocol is mapped back onto the
034: * DAV4J API.
035: * <p>
036: * WebDAVMethods are constructed by the HTTP server skeleton ResourceHTTPSkel
037: * in order to dispatch Resource methods to ResourceImpl methods on the server.
038: * This treats HTTP just like any other remote procedure call mechanism, and
039: * unifies the client/server communication.</p>
040: * <p>
041: * In general, the execution of a WebDAV method consists of:
042: * <ol>
043: * <li>create an instance of a Resource implementation corresponding to the resource being
044: * manipulated by the method</li>
045: * <li>set the ResourceImpl request context from the request headers using getRequestHeaders()</li>
046: * <li>get the request entity (if any), parse it, and marshal any arguments
047: * it contains for the method</li>
048: * <li>call the ResoureImpl method corresponding to the WebDAVMethod subclass (WebDAV or HTTP method)</li>
049: * <li>put any response context into the result headers using setResponseHeaders()</li>
050: * <li>output the response if any</li>
051: * <li>catch exceptions and translate them to set the status code and message
052: * for method</li>
053: * </ol>
054: * <p>
055: * Some of these operations are generic to all methods while others are specific
056: * to each subclass. In particular, how the context values,
057: * request entity, generation of the result entity, and status codes are handled
058: * are subclass specific.
059: * </p>
060: * <p>Note it is critical that the execute method sets status codes and the request
061: * and response headers at the right time. The order is, (1) use setStatusCode() and set
062: * other response headers the method needs to set. (2) Call setResponseHeaders() to
063: * copy the resource response context to the response headers. (3) Output any response entity
064: * body. The headers and response code are written the the HTTP response output
065: * stream just before the first byte of the response entity body. Any
066: * headers or status code set after the first byte of the response entity body has
067: * been written will be lost.</p>
068: */
069: public abstract class WebDAVMethod extends Object {
070: protected ResourceImpl resource; // t10he resource the method is operating on
071: protected HttpServletRequest request; // the request from the client
072: protected HttpServletResponse response; // the response from the server
073:
074: // contexts for communicating HTTP and WebDAV headers (method contol couples)
075: protected ResourceContext context = new ResourceContext();
076: protected String methodName = null;
077: private static final Logger logger = Logger
078: .getLogger(WebDAVMethod.class.getName());
079:
080: /** Construct a WebDAVMethod.
081: * @param request the servlet request
082: * @param response the servlet response
083: * @exception com.ibm.webdav.WebDAVException
084: */
085: public WebDAVMethod(HttpServletRequest request,
086: HttpServletResponse response) throws WebDAVException {
087: this .request = request;
088: this .response = response;
089:
090: // this isn't available from the HttpServletRequest directly, but should be
091: // the javax.servlet.http.HttpUtils.getRequestURL() doesn't strip off the query string,
092: // and looses the default port if it was specified.
093: URL requestURL = getRequestURL();
094:
095: resource = new ResourceImpl(requestURL, request
096: .getPathTranslated());
097:
098: if ((resource.exists() && resource.isCollection())
099: || request.getMethod().equals("MKCOL")) {
100: resource = new CollectionImpl(requestURL, request
101: .getPathTranslated(), null);
102: }
103: }
104:
105: /**
106: * Copies input stream to an HTTP output stream.
107: * @param in the source stream from the NamespaceManager
108: * @param out the destination stream from the servlet response
109: * @param length the number of bytes to read from the steam
110: * @param mime the MIME type of the document to determine if its text or binary
111: */
112: protected void copy(InputStream in, HttpServletResponse response,
113: int length, String mime) throws WebDAVException {
114: try {
115: int totalRead = 0;
116: int numRead = 0;
117: int numToRead = 0;
118: boolean isText = mime.regionMatches(0, "text", 0, 4);
119:
120: if (!isText) { // copy binary
121:
122: ServletOutputStream out = (ServletOutputStream) response
123: .getOutputStream();
124: byte[] buf = new byte[4096];
125:
126: while (totalRead < length) {
127: if ((length - totalRead) < buf.length) {
128: numToRead = length - totalRead;
129: } else {
130: numToRead = buf.length;
131: }
132:
133: numRead = in.read(buf, 0, numToRead);
134:
135: if (numRead == -1) {
136: break;
137: }
138:
139: totalRead += numRead;
140: out.write(buf, 0, numRead);
141: }
142: } else { // copy text using the client's preferred encoding scheme
143:
144: Reader isr = new InputStreamReader(in,
145: Resource.defaultCharEncoding);
146:
147: ServletOutputStream out = (ServletOutputStream) response
148: .getOutputStream();
149:
150: // Can't use response.getWriter() because JWS MIME filtering won't work
151: // It gives an IllegalStateException because it thinks you already got the
152: // output stream. This could be related to the AppServer bug that the response
153: // headers appear to be already written. Probably caused by a clone of the
154: // response for the filter.
155: //Writer out = response.getWriter();
156: /*PrintWriter writer = new PrintWriter(
157: new BufferedWriter(
158: new OutputStreamWriter(out,
159: response.getCharacterEncoding()),
160: 1024), false);*/
161: PrintWriter writer = new PrintWriter(
162: new BufferedWriter(new OutputStreamWriter(out,
163: Resource.defaultCharEncoding), 1024),
164: false);
165: numToRead = 4096;
166:
167: char[] buf = new char[numToRead];
168:
169: while ((numRead = isr.read(buf, 0, numToRead)) != -1) {
170: writer.write(buf, 0, numRead);
171: }
172:
173: writer.flush();
174: }
175: } catch (WebDAVException exc) {
176: throw exc;
177: } catch (java.io.IOException exc) {
178: throw new WebDAVException(
179: WebDAVStatus.SC_INTERNAL_SERVER_ERROR, "IO Error");
180: }
181: }
182:
183: /**
184: * Copies an HTTP input stream to an output stream.
185: * @param in the source stream from the servlet request
186: * @param out the destination stream from the NamespaceManager
187: * @param length the number of bytes to read from the steam
188: * @param mime the MIME type of the document to determine if its text or binary
189: */
190: protected void copy(HttpServletRequest request, OutputStream out,
191: int length, String mime) throws WebDAVException {
192: try {
193: int totalRead = 0;
194: int numRead = 0;
195: int numToRead = 0;
196: boolean isText = (length > 0) ? mime.regionMatches(0,
197: "text", 0, 4) : false;
198:
199: if (!isText) { // copy binary
200:
201: ServletInputStream in = (ServletInputStream) request
202: .getInputStream();
203: byte[] buf = new byte[4096];
204:
205: while (totalRead < length) {
206: if ((length - totalRead) < buf.length) {
207: numToRead = length - totalRead;
208: } else {
209: numToRead = buf.length;
210: }
211:
212: numRead = in.read(buf, 0, numToRead);
213:
214: if (numRead == -1) {
215: break;
216: }
217:
218: totalRead += numRead;
219: out.write(buf, 0, numRead);
220: }
221: } else { // copy text server's encoding scheme
222:
223: Reader isr = request.getReader();
224: PrintWriter osw = new PrintWriter(
225: new OutputStreamWriter(out,
226: Resource.defaultCharEncoding), false);
227: char[] buf = new char[4096];
228:
229: while (totalRead < length) {
230: if ((length - totalRead) < buf.length) {
231: numToRead = length - totalRead;
232: } else {
233: numToRead = buf.length;
234: }
235:
236: numRead = isr.read(buf, 0, numToRead);
237:
238: if (numRead == -1) {
239: break;
240: }
241:
242: totalRead += numRead;
243: osw.write(buf, 0, numRead);
244: }
245:
246: osw.flush();
247: }
248: } catch (WebDAVException exc) {
249: throw exc;
250: } catch (java.io.IOException exc) {
251: throw new WebDAVException(
252: WebDAVStatus.SC_INTERNAL_SERVER_ERROR, "IO Error");
253: }
254: }
255:
256: /** Create a WebDAVMethod corresponding to the WebDAV or HTTP method in
257: * the request.
258: * @param request the servlet request
259: * @param response the servlet response
260: * @return a subclass of WebDAVMethod corresponding to the request method
261: * @exception com.ibm.webdav.WebDAVException
262: */
263: public static WebDAVMethod create(HttpServletRequest request,
264: HttpServletResponse response) throws WebDAVException {
265: String methodName = request.getMethod();
266: WebDAVMethod method = null;
267:
268: if (logger.isLoggable(Level.FINE)) {
269: logger
270: .logp(Level.FINE, WebDAVMethod.class.getName(),
271: "create", "Processing " + methodName
272: + " request for "
273: + request.getRequestURI());
274: }
275:
276: // create an instance of a ResourceImpl object that will eventually
277: // field the request. To support subclasses of Resource, create a
278: // a subclass of ResourceHTTPSkel and create an instance of the new
279: // impl subclass here.
280: if (methodName.equals("GET")) {
281: method = new GetMethod(request, response);
282: } else if (methodName.equals("HEAD")) {
283: method = new HeadMethod(request, response);
284: } else if (methodName.equals("PUT")) {
285: method = new PutMethod(request, response);
286: } else if (methodName.equals("DELETE")) {
287: method = new DeleteMethod(request, response);
288: } else if (methodName.equals("COPY")) {
289: method = new CopyMethod(request, response);
290: } else if (methodName.equals("MOVE")) {
291: method = new MoveMethod(request, response);
292: } else if (methodName.equals("PROPFIND")) {
293: method = new PropFindMethod(request, response);
294: } else if (methodName.equals("PROPPATCH")) {
295: method = new PropPatchMethod(request, response);
296: } else if (methodName.equals("MKCOL")) {
297: method = new MkcolMethod(request, response);
298: } else if (methodName.equals("LOCK")) {
299: method = new LockMethod(request, response);
300: } else if (methodName.equals("UNLOCK")) {
301: method = new UnlockMethod(request, response);
302: } else if (methodName.equals("OPTIONS")) {
303: method = new OptionsMethod(request, response);
304: } else if (methodName.equals("POST")) {
305: method = new PostMethod(request, response);
306: } else if (methodName.equals("SEARCH")) {
307: method = new SearchMethod(request, response);
308: } else if (methodName.equals("BIND")) {
309: method = new BindMethod(request, response);
310: } else if (methodName.equals(CheckInMethod.METHOD_NAME)) {
311: method = new CheckInMethod(request, response);
312: } else if (methodName.equals(CheckOutMethod.METHOD_NAME)) {
313: method = new CheckOutMethod(request, response);
314: } else if (methodName.equals(ReportMethod.METHOD_NAME)) {
315: method = new ReportMethod(request, response);
316: } else if (methodName.equals(VersionControlMethod.METHOD_NAME)) {
317: method = new VersionControlMethod(request, response);
318: } else if (methodName.equals(UncheckOutMethod.METHOD_NAME)) {
319: method = new UncheckOutMethod(request, response);
320: } else if (methodName.equals(OrderPatchMethod.METHOD_NAME)) {
321: method = new OrderPatchMethod(request, response);
322: }
323:
324: method.getRequestHeaders();
325:
326: return method;
327: }
328:
329: /** Execute this method. Subclasses are expected to override this method to
330: * handle the request entity, do the work, update the context, return
331: * the response entity result if any, and set the status code.
332: *
333: * @return the WebDAV status code.
334: * @exception com.ibm.webdav.WebDAVException
335: */
336: public abstract WebDAVStatus execute() throws WebDAVException;
337:
338: /** Get the request method name
339: *
340: */
341: public String getMethodName() {
342: return methodName;
343: }
344:
345: /** Initialize the request context from the request headers. This provides
346: * additional parameters for the methods and perhaps other state from the
347: * client for the resource.
348: * @exception com.ibm.webdav.WebDAVException
349: */
350: public void getRequestHeaders() throws WebDAVException {
351: // set the Resource request context from the request headers
352: context = getResource().getContext();
353:
354: Enumeration headerNames = request.getHeaderNames();
355:
356: while (headerNames.hasMoreElements()) {
357: String name = ((String) headerNames.nextElement())
358: .toLowerCase();
359: String value = request.getHeader(name);
360: context.getRequestContext().put(name, value);
361: }
362: }
363:
364: /**
365: * Reconstructs the URL used by the client used to make the
366: * request. This accounts for differences such as addressing
367: * scheme (http, https), but does not attempt to
368: * include query parameters. The port is retained even if the
369: * default port was used.
370: *
371: * <P> This method is useful for creating redirect messages and for
372: * reporting errors.
373: */
374: public URL getRequestURL() {
375: StringBuffer url = new StringBuffer();
376:
377: String scheme = request.getScheme();
378: int port = request.getServerPort();
379: String requestURI = request.getRequestURI();
380: int q = requestURI.indexOf('?');
381:
382: if (q >= 0) {
383: requestURI = requestURI.substring(0, q); // supposed to be done by getRequestURI()!
384: }
385:
386: url.append(scheme); // http, https
387: url.append("://");
388:
389: // note, this is the IP address, not the server name. We have to be sure we normalize
390: // all host names to the IP address to be sure we can compare them in Preconditions
391: url.append(request.getServerName());
392:
393: if (port != -1) {
394: url.append(':');
395: url.append(request.getServerPort());
396: }
397:
398: url.append(requestURI);
399:
400: URL result = null;
401:
402: try {
403: result = new URL(url.toString());
404: } catch (Exception exc) {
405: }
406:
407: return result;
408: }
409:
410: /** Return the Resource the method operates on.
411: */
412: public ResourceImpl getResource() {
413: return resource;
414: }
415:
416: /** Get the client prefered character encoding to be used to encode
417: * text responses. This implementation gets the charset from the Accept
418: * header. TODO: it should probably do something with Accept-Charset too.
419: *
420: * @return the MIME charset
421: */
422: public String getResponseCharset() {
423: String accept = null;
424: String charEncoding = null;
425:
426: try {
427: accept = resource.getRequestContext().accept();
428: } catch (Exception exc) {
429: }
430:
431: if (accept != null) {
432: charEncoding = ((ServletResponse) response)
433: .getCharacterEncoding();
434:
435: /*if (charEncoding != null) {
436: charEncoding = MIME2Java.reverse(charEncoding);
437: }*/
438: }
439:
440: if (charEncoding == null) {
441: charEncoding = Resource.defaultXMLEncoding;
442: }
443:
444: return charEncoding;
445: }
446:
447: /** Get the status code for this method.
448: *
449: * @return a status code as defined by HTTP/1.1 and WebDAV
450: */
451: public int getStatusCode() {
452: return context.getStatusCode().getStatusCode();
453: }
454:
455: /** Set the response headers from the response context. This must be called
456: * after the remote method has been executed, and BEFORE any response entity
457: * is written.
458: * @exception RemoteException
459: */
460: public void setResponseHeaders() throws WebDAVException {
461: // set the response headers from the response context
462: ResourceContext context = resource.getContext();
463:
464: // let the web server calculate the content length header
465: Enumeration propertyNames = context.getResponseContext().keys();
466:
467: while (propertyNames.hasMoreElements()) {
468: String name = (String) propertyNames.nextElement();
469: String value = (String) context.getResponseContext().get(
470: name);
471:
472: if (!name.equals("content-length")) {
473: if (name.equals("content-type")) {
474: // must set Content-Type with this method or
475: // the response state will not be correct
476: response.setContentType(value);
477: } else {
478: response.setHeader(name, value);
479: }
480: }
481: }
482: }
483:
484: /** Set the status code for the method. This method must be called before
485: * any of the response entity is written.
486: *
487: * @param statusCode the status code to set as defined by HTTP/1.1
488: * and WebDAV.
489: *
490: */
491: public void setStatusCode(int statusCode) {
492: context.getStatusCode().setStatusCode(statusCode);
493:
494: // set the returned status code and message
495: // TODO: bug in jsdk2.1, can't set the status message, this method was
496: // deprecated. The javaDoc also ways it sents an HTML response body,
497: // but I think the comment is just wrong.
498: response.setStatus(statusCode, WebDAVStatus
499: .getStatusMessage(statusCode));
500: }
501: }
|