001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software GmbH & Co. KG, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 *
027 * This file is based on:
028 * - org.apache.catalina.servlets.WebdavServlet
029 * - org.apache.catalina.servlets.DefaultServlet
030 * from the Apache Tomcat project.
031 *
032 * Licensed to the Apache Software Foundation (ASF) under one or more
033 * contributor license agreements.  See the NOTICE file distributed with
034 * this work for additional information regarding copyright ownership.
035 * The ASF licenses this file to You under the Apache License, Version 2.0
036 * (the "License"); you may not use this file except in compliance with
037 * the License.  You may obtain a copy of the License at
038 *
039 *      http://www.apache.org/licenses/LICENSE-2.0
040 *
041 * Unless required by applicable law or agreed to in writing, software
042 * distributed under the License is distributed on an "AS IS" BASIS,
043 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
044 * See the License for the specific language governing permissions and
045 * limitations under the License.
046 */
047
048package org.opencms.webdav;
049
050import org.opencms.file.CmsVfsResourceAlreadyExistsException;
051import org.opencms.file.CmsVfsResourceNotFoundException;
052import org.opencms.i18n.CmsEncoder;
053import org.opencms.main.CmsException;
054import org.opencms.main.CmsLog;
055import org.opencms.main.OpenCms;
056import org.opencms.repository.A_CmsRepository;
057import org.opencms.repository.CmsRepositoryLockInfo;
058import org.opencms.repository.I_CmsRepositoryItem;
059import org.opencms.repository.I_CmsRepositorySession;
060import org.opencms.security.CmsSecurityException;
061import org.opencms.util.CmsRequestUtil;
062
063import java.io.BufferedInputStream;
064import java.io.ByteArrayInputStream;
065import java.io.ByteArrayOutputStream;
066import java.io.File;
067import java.io.FileInputStream;
068import java.io.IOException;
069import java.io.InputStream;
070import java.io.InputStreamReader;
071import java.io.OutputStreamWriter;
072import java.io.PrintWriter;
073import java.io.RandomAccessFile;
074import java.io.Reader;
075import java.io.UnsupportedEncodingException;
076import java.io.Writer;
077import java.security.MessageDigest;
078import java.security.NoSuchAlgorithmException;
079import java.text.DateFormat;
080import java.text.SimpleDateFormat;
081import java.util.ArrayList;
082import java.util.BitSet;
083import java.util.Date;
084import java.util.Hashtable;
085import java.util.Iterator;
086import java.util.List;
087import java.util.Locale;
088import java.util.Map;
089import java.util.Map.Entry;
090import java.util.Stack;
091import java.util.StringTokenizer;
092import java.util.TimeZone;
093import java.util.Vector;
094
095import javax.servlet.ServletException;
096import javax.servlet.ServletOutputStream;
097import javax.servlet.UnavailableException;
098import javax.servlet.http.HttpServlet;
099import javax.servlet.http.HttpServletRequest;
100import javax.servlet.http.HttpServletResponse;
101
102import org.apache.commons.codec.binary.Base64;
103import org.apache.commons.codec.net.URLCodec;
104import org.apache.commons.logging.Log;
105
106import org.dom4j.Document;
107import org.dom4j.DocumentException;
108import org.dom4j.DocumentHelper;
109import org.dom4j.Element;
110import org.dom4j.Namespace;
111import org.dom4j.Node;
112import org.dom4j.QName;
113import org.dom4j.io.SAXReader;
114import org.xml.sax.InputSource;
115
116/**
117 * Servlet which adds support for WebDAV level 2.<p>
118 *
119 * @since 6.5.6
120 */
121public class CmsWebdavServlet extends HttpServlet {
122
123    /** Basic authorization prefix constant. */
124    public static final String AUTHORIZATION_BASIC_PREFIX = "BASIC ";
125
126    /** Size of file transfer buffer in bytes. */
127    public static final int BUFFER_SIZE = 4096;
128
129    /** Credentials separator constant. */
130    public static final String SEPARATOR_CREDENTIALS = ":";
131
132    /** Date format for the last modified date. */
133    protected static final DateFormat HTTP_DATE_FORMAT;
134
135    /** Date format for the creation date. */
136    protected static final DateFormat ISO8601_FORMAT;
137
138    /** MD5 message digest provider. */
139    protected static MessageDigest m_md5Helper;
140
141    /** The MD5 helper object for this class. */
142    protected static final CmsMD5Encoder MD5_ENCODER = new CmsMD5Encoder();
143
144    /** WebDAV method: COPY. */
145    protected static final String METHOD_COPY = "COPY";
146
147    /** HTTP Method: DELETE. */
148    protected static final String METHOD_DELETE = "DELETE";
149
150    /** HTTP Method: GET. */
151    protected static final String METHOD_GET = "GET";
152
153    /** HTTP Method: HEAD. */
154    protected static final String METHOD_HEAD = "HEAD";
155
156    /** WebDAV method: LOCK. */
157    protected static final String METHOD_LOCK = "LOCK";
158
159    /** WebDAV method: MKCOL. */
160    protected static final String METHOD_MKCOL = "MKCOL";
161
162    /** WebDAV method: MOVE. */
163    protected static final String METHOD_MOVE = "MOVE";
164
165    /** HTTP Method: OPTIONS. */
166    protected static final String METHOD_OPTIONS = "OPTIONS";
167
168    /** HTTP Method: POST. */
169    protected static final String METHOD_POST = "POST";
170
171    /** WebDAV method: PROPFIND. */
172    protected static final String METHOD_PROPFIND = "PROPFIND";
173
174    /** WebDAV method: PROPPATCH. */
175    protected static final String METHOD_PROPPATCH = "PROPPATCH";
176
177    /** HTTP Method: PUT. */
178    protected static final String METHOD_PUT = "PUT";
179
180    /** HTTP Method: TRACE. */
181    protected static final String METHOD_TRACE = "TRACE";
182
183    /** WebDAV method: UNLOCK. */
184    protected static final String METHOD_UNLOCK = "UNLOCK";
185
186    /** MIME multipart separation string. */
187    protected static final String MIME_SEPARATION = "CATALINA_MIME_BOUNDARY";
188
189    /** Chars which are safe for urls. */
190    protected static final BitSet URL_SAFE_CHARS;
191
192    /** Name of the servlet attribute to get the path to the temp directory. */
193    private static final String ATT_SERVLET_TEMPDIR = "javax.servlet.context.tempdir";
194
195    /** The text to use as basic realm. */
196    private static final String BASIC_REALM = "OpenCms WebDAV Servlet";
197
198    /** Default namespace. */
199    private static final String DEFAULT_NAMESPACE = "DAV:";
200
201    /** The text to send if the depth is inifinity. */
202    private static final String DEPTH_INFINITY = "Infinity";
203
204    /** PROPFIND - Display all properties. */
205    private static final int FIND_ALL_PROP = 1;
206
207    /** PROPFIND - Specify a property mask. */
208    private static final int FIND_BY_PROPERTY = 0;
209
210    /** PROPFIND - Return property names. */
211    private static final int FIND_PROPERTY_NAMES = 2;
212
213    /** Full range marker. */
214    private static ArrayList<CmsWebdavRange> FULL_RANGE = new ArrayList<CmsWebdavRange>();
215
216    /** The name of the header "allow". */
217    private static final String HEADER_ALLOW = "Allow";
218
219    /** The name of the header "authorization". */
220    private static final String HEADER_AUTHORIZATION = "Authorization";
221
222    /** The name of the header "content-length". */
223    private static final String HEADER_CONTENTLENGTH = "content-length";
224
225    /** The name of the header "Content-Range". */
226    private static final String HEADER_CONTENTRANGE = "Content-Range";
227
228    /** The name of the header "Depth". */
229    private static final String HEADER_DEPTH = "Depth";
230
231    /** The name of the header "Destination". */
232    private static final String HEADER_DESTINATION = "Destination";
233
234    /** The name of the header "ETag". */
235    private static final String HEADER_ETAG = "ETag";
236
237    /** The name of the header "If-Range". */
238    private static final String HEADER_IFRANGE = "If-Range";
239
240    /** The name of the header "Last-Modified". */
241    private static final String HEADER_LASTMODIFIED = "Last-Modified";
242
243    /** The name of the header "Lock-Token". */
244    private static final String HEADER_LOCKTOKEN = "Lock-Token";
245
246    /** The name of the header "Overwrite". */
247    private static final String HEADER_OVERWRITE = "Overwrite";
248
249    /** The name of the header "Range". */
250    private static final String HEADER_RANGE = "Range";
251
252    /** The name of the init parameter in the web.xml to allow listing. */
253    private static final String INIT_PARAM_LIST = "listings";
254
255    /** The name of the init parameter in the web.xml to set read only. */
256    private static final String INIT_PARAM_READONLY = "readonly";
257
258    /** The name of the init-param where the repository class is defined. */
259    private static final String INIT_PARAM_REPOSITORY = "repository";
260
261    /** Create a new lock. */
262    private static final int LOCK_CREATION = 0;
263
264    /** Refresh lock. */
265    private static final int LOCK_REFRESH = 1;
266
267    /** The log object for this class. */
268    private static final Log LOG = CmsLog.getLog(CmsWebdavServlet.class);
269
270    /** The repository used from this servlet. */
271    private static A_CmsRepository m_repository;
272
273    /** The unique serial id for this class. */
274    private static final long serialVersionUID = -122598983283724306L;
275
276    /** The name of the tag "activelock" in the WebDAV protocol. */
277    private static final String TAG_ACTIVELOCK = "activelock";
278
279    /** The name of the tag "collection" in the WebDAV protocol. */
280    private static final String TAG_COLLECTION = "collection";
281
282    /** The name of the tag "getcontentlanguage" in the WebDAV protocol. */
283    private static final String TAG_CONTENTLANGUAGE = "getcontentlanguage";
284
285    /** The name of the tag "getcontentlength" in the WebDAV protocol. */
286    private static final String TAG_CONTENTLENGTH = "getcontentlength";
287
288    /** The name of the tag "getcontenttype" in the WebDAV protocol. */
289    private static final String TAG_CONTENTTYPE = "getcontenttype";
290
291    /** The name of the tag "creationdate" in the WebDAV protocol. */
292    private static final String TAG_CREATIONDATE = "creationdate";
293
294    /** The name of the tag "depth" in the WebDAV protocol. */
295    private static final String TAG_DEPTH = "depth";
296
297    /** The name of the tag "displayname" in the WebDAV protocol. */
298    private static final String TAG_DISPLAYNAME = "displayname";
299
300    /** The name of the tag "getetag" in the WebDAV protocol. */
301    private static final String TAG_ETAG = "getetag";
302
303    /** The name of the tag "href" in the WebDAV protocol. */
304    private static final String TAG_HREF = "href";
305
306    /** The name of the tag "getlastmodified" in the WebDAV protocol. */
307    private static final String TAG_LASTMODIFIED = "getlastmodified";
308
309    /** The name of the tag "lockdiscovery" in the WebDAV protocol. */
310    private static final String TAG_LOCKDISCOVERY = "lockdiscovery";
311
312    /** The name of the tag "lockentry" in the WebDAV protocol. */
313    private static final String TAG_LOCKENTRY = "lockentry";
314
315    /** The name of the tag "lockscope" in the WebDAV protocol. */
316    private static final String TAG_LOCKSCOPE = "lockscope";
317
318    /** The name of the tag "locktoken" in the WebDAV protocol. */
319    private static final String TAG_LOCKTOKEN = "locktoken";
320
321    /** The name of the tag "locktype" in the WebDAV protocol. */
322    private static final String TAG_LOCKTYPE = "locktype";
323
324    /** The name of the tag "multistatus" in the WebDAV protocol. */
325    private static final String TAG_MULTISTATUS = "multistatus";
326
327    /** The name of the tag "owner" in the WebDAV protocol. */
328    private static final String TAG_OWNER = "owner";
329
330    /** The name of the tag "prop" in the WebDAV protocol. */
331    private static final String TAG_PROP = "prop";
332
333    /** The name of the tag "propstat" in the WebDAV protocol. */
334    private static final String TAG_PROPSTAT = "propstat";
335
336    /** The name of the tag "resourcetype" in the WebDAV protocol. */
337    private static final String TAG_RESOURCETYPE = "resourcetype";
338
339    /** The name of the tag "response" in the WebDAV protocol. */
340    private static final String TAG_RESPONSE = "response";
341
342    /** The name of the tag "source" in the WebDAV protocol. */
343    private static final String TAG_SOURCE = "source";
344
345    /** The name of the tag "status" in the WebDAV protocol. */
346    private static final String TAG_STATUS = "status";
347
348    /** The name of the tag "supportedlock" in the WebDAV protocol. */
349    private static final String TAG_SUPPORTEDLOCK = "supportedlock";
350
351    /** The name of the tag "timeout" in the WebDAV protocol. */
352    private static final String TAG_TIMEOUT = "timeout";
353
354    /** The text to send if the timeout is infinite. */
355    private static final String TIMEOUT_INFINITE = "Infinite";
356
357    /** The input buffer size to use when serving resources. */
358    protected int m_input = 2048;
359
360    /** The output buffer size to use when serving resources. */
361    protected int m_output = 2048;
362
363    /** Should we generate directory listings? */
364    private boolean m_listings;
365
366    /** Read only flag. By default, it's set to true. */
367    private boolean m_readOnly = true;
368
369    /** Secret information used to generate reasonably secure lock ids. */
370    private String m_secret = "catalina";
371
372    /** The session which handles the action made with WebDAV. */
373    private I_CmsRepositorySession m_session;
374
375    /** The name of the user found in the authorization header. */
376    private String m_username;
377
378    static {
379        URL_SAFE_CHARS = new BitSet();
380        URL_SAFE_CHARS.set('a', 'z' + 1);
381        URL_SAFE_CHARS.set('A', 'Z' + 1);
382        URL_SAFE_CHARS.set('0', '9' + 1);
383        URL_SAFE_CHARS.set('-');
384        URL_SAFE_CHARS.set('_');
385        URL_SAFE_CHARS.set('.');
386        URL_SAFE_CHARS.set('*');
387        URL_SAFE_CHARS.set('/');
388        URL_SAFE_CHARS.set(':');
389
390        ISO8601_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
391        ISO8601_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT"));
392
393        HTTP_DATE_FORMAT = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.US);
394        HTTP_DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT"));
395    }
396
397    /**
398     * Adds an xml element to the given parent and sets the appropriate namespace and
399     * prefix.<p>
400     *
401     * @param parent the parent node to add the element
402     * @param name the name of the new element
403     *
404     * @return the created element with the given name which was added to the given parent
405     */
406    public static Element addElement(Element parent, String name) {
407
408        return parent.addElement(new QName(name, Namespace.get("D", DEFAULT_NAMESPACE)));
409    }
410
411    /**
412     * Initialize this servlet.<p>
413     *
414     * @throws ServletException if something goes wrong
415     */
416    @Override
417    public void init() throws ServletException {
418
419        if (LOG.isInfoEnabled()) {
420            LOG.info(Messages.get().getBundle().key(Messages.LOG_INIT_WEBDAV_SERVLET_0));
421        }
422
423        String value = null;
424
425        // init parameter: listings
426        try {
427            value = getServletConfig().getInitParameter(INIT_PARAM_LIST);
428            if (value != null) {
429                m_listings = Boolean.valueOf(value).booleanValue();
430            }
431        } catch (Exception e) {
432            if (LOG.isErrorEnabled()) {
433                LOG.error(
434                    Messages.get().getBundle().key(Messages.LOG_READ_INIT_PARAM_ERROR_2, INIT_PARAM_LIST, value),
435                    e);
436            }
437        }
438
439        if (LOG.isInfoEnabled()) {
440            LOG.info(
441                Messages.get().getBundle().key(
442                    Messages.LOG_READ_INIT_PARAM_2,
443                    INIT_PARAM_LIST,
444                    Boolean.valueOf(m_listings)));
445        }
446
447        // init parameter: read only
448        try {
449            value = getServletConfig().getInitParameter(INIT_PARAM_READONLY);
450            if (value != null) {
451                m_readOnly = Boolean.valueOf(value).booleanValue();
452            }
453        } catch (Exception e) {
454            if (LOG.isErrorEnabled()) {
455                LOG.error(
456                    Messages.get().getBundle().key(Messages.LOG_READ_INIT_PARAM_ERROR_2, INIT_PARAM_READONLY, value),
457                    e);
458            }
459        }
460
461        if (LOG.isInfoEnabled()) {
462            LOG.info(
463                Messages.get().getBundle().key(
464                    Messages.LOG_READ_INIT_PARAM_2,
465                    INIT_PARAM_READONLY,
466                    Boolean.valueOf(m_readOnly)));
467        }
468
469        // Load the MD5 helper used to calculate signatures.
470        try {
471            m_md5Helper = MessageDigest.getInstance("MD5");
472        } catch (NoSuchAlgorithmException e) {
473
474            if (LOG.isErrorEnabled()) {
475                LOG.error(Messages.get().getBundle().key(Messages.ERR_MD5_NOT_AVAILABLE_0), e);
476            }
477
478            throw new UnavailableException(Messages.get().getBundle().key(Messages.ERR_MD5_NOT_AVAILABLE_0));
479        }
480
481        // Instantiate repository from init-param
482        String repositoryName = getInitParameter(INIT_PARAM_REPOSITORY);
483        if (repositoryName == null) {
484
485            if (LOG.isErrorEnabled()) {
486                LOG.error(Messages.get().getBundle().key(Messages.ERR_INIT_PARAM_MISSING_1, INIT_PARAM_REPOSITORY));
487            }
488
489            throw new ServletException(
490                Messages.get().getBundle().key(Messages.ERR_INIT_PARAM_MISSING_1, INIT_PARAM_REPOSITORY));
491        }
492
493        m_repository = OpenCms.getRepositoryManager().getRepository(repositoryName, A_CmsRepository.class);
494        if (m_repository == null) {
495
496            if (LOG.isErrorEnabled()) {
497                LOG.error(Messages.get().getBundle().key(Messages.ERR_REPOSITORY_NOT_FOUND_1, repositoryName));
498            }
499
500            throw new ServletException(
501                Messages.get().getBundle().key(Messages.ERR_REPOSITORY_NOT_FOUND_1, repositoryName));
502        }
503
504        if (LOG.isInfoEnabled()) {
505            LOG.info(Messages.get().getBundle().key(Messages.LOG_USE_REPOSITORY_1, repositoryName));
506        }
507    }
508
509    /**
510     * Copy the contents of the specified input stream to the specified
511     * output stream, and ensure that both streams are closed before returning
512     * (even in the face of an exception).<p>
513     *
514     * @param item the RepositoryItem
515     * @param is the input stream to copy from
516     * @param writer the writer to write to
517     *
518     * @throws IOException if an input/output error occurs
519     */
520    protected void copy(I_CmsRepositoryItem item, InputStream is, PrintWriter writer) throws IOException {
521
522        IOException exception = null;
523
524        InputStream resourceInputStream = null;
525        if (!item.isCollection()) {
526            resourceInputStream = new ByteArrayInputStream(item.getContent());
527        } else {
528            resourceInputStream = is;
529        }
530
531        Reader reader = new InputStreamReader(resourceInputStream);
532
533        // Copy the input stream to the output stream
534        exception = copyRange(reader, writer);
535
536        // Clean up the reader
537        try {
538            reader.close();
539        } catch (Exception e) {
540            if (LOG.isErrorEnabled()) {
541                LOG.error(Messages.get().getBundle().key(Messages.ERR_CLOSE_READER_0), e);
542            }
543        }
544
545        // Rethrow any exception that has occurred
546        if (exception != null) {
547            throw exception;
548        }
549    }
550
551    /**
552     * Copy the contents of the specified input stream to the specified
553     * output stream, and ensure that both streams are closed before returning
554     * (even in the face of an exception).<p>
555     *
556     * @param item the RepositoryItem
557     * @param is the input stream to copy from
558     * @param ostream the output stream to write to
559     *
560     * @throws IOException if an input/output error occurs
561     */
562    protected void copy(I_CmsRepositoryItem item, InputStream is, ServletOutputStream ostream) throws IOException {
563
564        IOException exception = null;
565        InputStream resourceInputStream = null;
566
567        // Optimization: If the binary content has already been loaded, send
568        // it directly
569        if (!item.isCollection()) {
570            byte[] buffer = item.getContent();
571            if (buffer != null) {
572                ostream.write(buffer, 0, buffer.length);
573                return;
574            }
575            resourceInputStream = new ByteArrayInputStream(item.getContent());
576        } else {
577            resourceInputStream = is;
578        }
579
580        InputStream istream = new BufferedInputStream(resourceInputStream, m_input);
581
582        // Copy the input stream to the output stream
583        exception = copyRange(istream, ostream);
584
585        // Clean up the input stream
586        try {
587            istream.close();
588        } catch (Exception e) {
589            if (LOG.isErrorEnabled()) {
590                LOG.error(Messages.get().getBundle().key(Messages.ERR_CLOSE_INPUT_STREAM_0), e);
591            }
592        }
593
594        // Rethrow any exception that has occurred
595        if (exception != null) {
596            throw exception;
597        }
598    }
599
600    /**
601     * Copy the contents of the specified input stream to the specified
602     * output stream, and ensure that both streams are closed before returning
603     * (even in the face of an exception).<p>
604     *
605     * @param item the RepositoryItem
606     * @param writer the writer to write to
607     * @param range the range the client wants to retrieve
608     *
609     * @throws IOException if an input/output error occurs
610     */
611    protected void copy(I_CmsRepositoryItem item, PrintWriter writer, CmsWebdavRange range) throws IOException {
612
613        IOException exception = null;
614
615        InputStream resourceInputStream = new ByteArrayInputStream(item.getContent());
616
617        Reader reader = new InputStreamReader(resourceInputStream);
618        exception = copyRange(reader, writer, range.getStart(), range.getEnd());
619
620        // Clean up the input stream
621        try {
622            reader.close();
623        } catch (Exception e) {
624            if (LOG.isErrorEnabled()) {
625                LOG.error(Messages.get().getBundle().key(Messages.ERR_CLOSE_READER_0), e);
626            }
627        }
628
629        // Rethrow any exception that has occurred
630        if (exception != null) {
631            throw exception;
632        }
633    }
634
635    /**
636     * Copy the contents of the specified input stream to the specified
637     * output stream, and ensure that both streams are closed before returning
638     * (even in the face of an exception).<p>
639     *
640     * @param item the RepositoryItem
641     * @param writer the writer to write to
642     * @param ranges iterator of the ranges the client wants to retrieve
643     * @param contentType the content type of the resource
644     *
645     * @throws IOException if an input/output error occurs
646     */
647    protected void copy(
648        I_CmsRepositoryItem item,
649        PrintWriter writer,
650        Iterator<CmsWebdavRange> ranges,
651        String contentType) throws IOException {
652
653        IOException exception = null;
654
655        while ((exception == null) && (ranges.hasNext())) {
656
657            InputStream resourceInputStream = new ByteArrayInputStream(item.getContent());
658
659            Reader reader = new InputStreamReader(resourceInputStream);
660            CmsWebdavRange currentRange = ranges.next();
661
662            // Writing MIME header.
663            writer.println();
664            writer.println("--" + MIME_SEPARATION);
665            if (contentType != null) {
666                writer.println("Content-Type: " + contentType);
667            }
668            writer.println(
669                "Content-Range: bytes "
670                    + currentRange.getStart()
671                    + "-"
672                    + currentRange.getEnd()
673                    + "/"
674                    + currentRange.getLength());
675            writer.println();
676
677            // Printing content
678            exception = copyRange(reader, writer, currentRange.getStart(), currentRange.getEnd());
679
680            try {
681                reader.close();
682            } catch (Exception e) {
683                if (LOG.isErrorEnabled()) {
684                    LOG.error(Messages.get().getBundle().key(Messages.ERR_CLOSE_READER_0), e);
685                }
686            }
687
688        }
689
690        writer.println();
691        writer.print("--" + MIME_SEPARATION + "--");
692
693        // Rethrow any exception that has occurred
694        if (exception != null) {
695            throw exception;
696        }
697    }
698
699    /**
700     * Copy the contents of the specified input stream to the specified
701     * output stream, and ensure that both streams are closed before returning
702     * (even in the face of an exception).<p>
703     *
704     * @param item the RepositoryItem
705     * @param ostream the output stream to write to
706     * @param range the range the client wants to retrieve
707     *
708     * @throws IOException if an input/output error occurs
709     */
710    protected void copy(I_CmsRepositoryItem item, ServletOutputStream ostream, CmsWebdavRange range)
711    throws IOException {
712
713        IOException exception = null;
714
715        InputStream resourceInputStream = new ByteArrayInputStream(item.getContent());
716        InputStream istream = new BufferedInputStream(resourceInputStream, m_input);
717        exception = copyRange(istream, ostream, range.getStart(), range.getEnd());
718
719        // Clean up the input stream
720        try {
721            istream.close();
722        } catch (Exception e) {
723            if (LOG.isErrorEnabled()) {
724                LOG.error(Messages.get().getBundle().key(Messages.ERR_CLOSE_INPUT_STREAM_0), e);
725            }
726        }
727
728        // Rethrow any exception that has occurred
729        if (exception != null) {
730            throw exception;
731        }
732    }
733
734    /**
735     * Copy the contents of the specified input stream to the specified
736     * output stream, and ensure that both streams are closed before returning
737     * (even in the face of an exception).<p>
738     *
739     * @param item the RepositoryItem
740     * @param ostream the output stream to write to
741     * @param ranges iterator of the ranges the client wants to retrieve
742     * @param contentType the content type of the resource
743     *
744     * @throws IOException if an input/output error occurs
745     */
746    protected void copy(
747        I_CmsRepositoryItem item,
748        ServletOutputStream ostream,
749        Iterator<CmsWebdavRange> ranges,
750        String contentType) throws IOException {
751
752        IOException exception = null;
753
754        while ((exception == null) && (ranges.hasNext())) {
755
756            InputStream resourceInputStream = new ByteArrayInputStream(item.getContent());
757            InputStream istream = new BufferedInputStream(resourceInputStream, m_input);
758
759            CmsWebdavRange currentRange = ranges.next();
760
761            // Writing MIME header.
762            ostream.println();
763            ostream.println("--" + MIME_SEPARATION);
764            if (contentType != null) {
765                ostream.println("Content-Type: " + contentType);
766            }
767            ostream.println(
768                "Content-Range: bytes "
769                    + currentRange.getStart()
770                    + "-"
771                    + currentRange.getEnd()
772                    + "/"
773                    + currentRange.getLength());
774            ostream.println();
775
776            // Printing content
777            exception = copyRange(istream, ostream, currentRange.getStart(), currentRange.getEnd());
778
779            try {
780                istream.close();
781            } catch (Exception e) {
782                if (LOG.isErrorEnabled()) {
783                    LOG.error(Messages.get().getBundle().key(Messages.ERR_CLOSE_INPUT_STREAM_0), e);
784                }
785            }
786
787        }
788
789        ostream.println();
790        ostream.print("--" + MIME_SEPARATION + "--");
791
792        // Rethrow any exception that has occurred
793        if (exception != null) {
794            throw exception;
795        }
796    }
797
798    /**
799     * Copy the contents of the specified input stream to the specified
800     * output stream, and ensure that both streams are closed before returning
801     * (even in the face of an exception).<p>
802     *
803     * @param istream the input stream to read from
804     * @param ostream the output stream to write to
805     *
806     * @return the exception which occurred during processing
807     */
808    protected IOException copyRange(InputStream istream, ServletOutputStream ostream) {
809
810        // Copy the input stream to the output stream
811        IOException exception = null;
812        byte[] buffer = new byte[m_input];
813        int len = buffer.length;
814        while (true) {
815            try {
816                len = istream.read(buffer);
817                if (len == -1) {
818                    break;
819                }
820                ostream.write(buffer, 0, len);
821            } catch (IOException e) {
822                exception = e;
823                len = -1;
824                break;
825            }
826        }
827        return exception;
828    }
829
830    /**
831     * Copy the contents of the specified input stream to the specified
832     * output stream, and ensure that both streams are closed before returning
833     * (even in the face of an exception).<p>
834     *
835     * @param istream the input stream to read from
836     * @param ostream the output stream to write to
837     * @param start the start of the range which will be copied
838     * @param end the end of the range which will be copied
839     *
840     * @return the exception which occurred during processing
841     */
842    protected IOException copyRange(InputStream istream, ServletOutputStream ostream, long start, long end) {
843
844        if (LOG.isDebugEnabled()) {
845            LOG.debug(Messages.get().getBundle().key(Messages.LOG_SERVE_BYTES_2, new Long(start), new Long(end)));
846        }
847
848        try {
849            istream.skip(start);
850        } catch (IOException e) {
851            return e;
852        }
853
854        IOException exception = null;
855        long bytesToRead = (end - start) + 1;
856
857        byte[] buffer = new byte[m_input];
858        int len = buffer.length;
859        while ((bytesToRead > 0) && (len >= buffer.length)) {
860            try {
861                len = istream.read(buffer);
862                if (bytesToRead >= len) {
863                    ostream.write(buffer, 0, len);
864                    bytesToRead -= len;
865                } else {
866                    ostream.write(buffer, 0, (int)bytesToRead);
867                    bytesToRead = 0;
868                }
869            } catch (IOException e) {
870                exception = e;
871                len = -1;
872            }
873
874            if (len < buffer.length) {
875                break;
876            }
877        }
878
879        return exception;
880    }
881
882    /**
883     * Copy the contents of the specified input stream to the specified
884     * output stream, and ensure that both streams are closed before returning
885     * (even in the face of an exception).<p>
886     *
887     * @param reader the reader to read from
888     * @param writer the writer to write to
889     *
890     * @return the exception which occurred during processing
891     */
892    protected IOException copyRange(Reader reader, PrintWriter writer) {
893
894        // Copy the input stream to the output stream
895        IOException exception = null;
896        char[] buffer = new char[m_input];
897        int len = buffer.length;
898        while (true) {
899            try {
900                len = reader.read(buffer);
901                if (len == -1) {
902                    break;
903                }
904                writer.write(buffer, 0, len);
905            } catch (IOException e) {
906                exception = e;
907                len = -1;
908                break;
909            }
910        }
911        return exception;
912    }
913
914    /**
915     * Copy the contents of the specified input stream to the specified
916     * output stream, and ensure that both streams are closed before returning
917     * (even in the face of an exception).<p>
918     *
919     * @param reader the reader to read from
920     * @param writer the writer to write to
921     * @param start the start of the range which will be copied
922     * @param end the end of the range which will be copied
923     *
924     * @return the exception which occurred during processing
925     */
926    protected IOException copyRange(Reader reader, PrintWriter writer, long start, long end) {
927
928        try {
929            reader.skip(start);
930        } catch (IOException e) {
931            return e;
932        }
933
934        IOException exception = null;
935        long bytesToRead = (end - start) + 1;
936
937        char[] buffer = new char[m_input];
938        int len = buffer.length;
939        while ((bytesToRead > 0) && (len >= buffer.length)) {
940            try {
941                len = reader.read(buffer);
942                if (bytesToRead >= len) {
943                    writer.write(buffer, 0, len);
944                    bytesToRead -= len;
945                } else {
946                    writer.write(buffer, 0, (int)bytesToRead);
947                    bytesToRead = 0;
948                }
949            } catch (IOException e) {
950                exception = e;
951                len = -1;
952            }
953
954            if (len < buffer.length) {
955                break;
956            }
957        }
958
959        return exception;
960    }
961
962    /**
963     * Process a COPY WebDAV request for the specified resource.<p>
964     *
965     * @param req the servlet request we are processing
966     * @param resp the servlet response we are creating
967     */
968    protected void doCopy(HttpServletRequest req, HttpServletResponse resp) {
969
970        // Check if webdav is set to read only
971        if (m_readOnly) {
972
973            resp.setStatus(CmsWebdavStatus.SC_FORBIDDEN);
974
975            if (LOG.isDebugEnabled()) {
976                LOG.debug(Messages.get().getBundle().key(Messages.LOG_WEBDAV_READ_ONLY_0));
977            }
978
979            return;
980        }
981
982        // Get the source path to copy
983        String src = getRelativePath(req);
984
985        // Check if source exists
986        if (!m_session.exists(src)) {
987
988            resp.setStatus(CmsWebdavStatus.SC_NOT_FOUND);
989
990            if (LOG.isDebugEnabled()) {
991                LOG.debug(Messages.get().getBundle().key(Messages.LOG_ITEM_NOT_FOUND_1, src));
992            }
993
994            return;
995        }
996
997        // Get the destination path to copy to
998        String dest = parseDestinationHeader(req);
999        if (dest == null) {
1000
1001            resp.setStatus(CmsWebdavStatus.SC_BAD_REQUEST);
1002
1003            if (LOG.isDebugEnabled()) {
1004                LOG.debug(Messages.get().getBundle().key(Messages.LOG_PARSE_DEST_HEADER_0));
1005            }
1006
1007            return;
1008        }
1009
1010        // source and destination are the same
1011        if (dest.equals(src)) {
1012
1013            resp.setStatus(CmsWebdavStatus.SC_FORBIDDEN);
1014
1015            if (LOG.isDebugEnabled()) {
1016                LOG.debug(Messages.get().getBundle().key(Messages.LOG_SRC_DEST_EQUALS_0));
1017            }
1018
1019            return;
1020        }
1021
1022        // Parsing overwrite header
1023        boolean overwrite = parseOverwriteHeader(req);
1024
1025        // If the destination exists, then it's a conflict
1026        if ((m_session.exists(dest)) && (!overwrite)) {
1027
1028            resp.setStatus(CmsWebdavStatus.SC_PRECONDITION_FAILED);
1029
1030            if (LOG.isDebugEnabled()) {
1031                LOG.debug(Messages.get().getBundle().key(Messages.LOG_DEST_PATH_EXISTS_1, dest));
1032            }
1033
1034            return;
1035        }
1036
1037        if ((!m_session.exists(dest)) && (overwrite)) {
1038            resp.setStatus(CmsWebdavStatus.SC_CREATED);
1039        }
1040
1041        // Copying source to destination
1042        try {
1043
1044            if (LOG.isDebugEnabled()) {
1045                LOG.debug(Messages.get().getBundle().key(Messages.LOG_COPY_ITEM_2, src, dest));
1046            }
1047
1048            m_session.copy(src, dest, overwrite);
1049        } catch (CmsSecurityException sex) {
1050            resp.setStatus(CmsWebdavStatus.SC_FORBIDDEN);
1051
1052            if (LOG.isDebugEnabled()) {
1053                LOG.debug(Messages.get().getBundle().key(Messages.LOG_NO_PERMISSION_0));
1054            }
1055
1056            return;
1057        } catch (CmsVfsResourceAlreadyExistsException raeex) {
1058
1059            // should never happen
1060            resp.setStatus(CmsWebdavStatus.SC_PRECONDITION_FAILED);
1061
1062            if (LOG.isDebugEnabled()) {
1063                LOG.debug(Messages.get().getBundle().key(Messages.LOG_ITEM_EXISTS_1, dest));
1064            }
1065
1066            return;
1067        } catch (CmsVfsResourceNotFoundException rnfex) {
1068
1069            // should never happen
1070            resp.setStatus(CmsWebdavStatus.SC_NOT_FOUND);
1071
1072            if (LOG.isDebugEnabled()) {
1073                LOG.debug(Messages.get().getBundle().key(Messages.LOG_ITEM_NOT_FOUND_1, src));
1074            }
1075
1076            return;
1077        } catch (CmsException ex) {
1078            resp.setStatus(CmsWebdavStatus.SC_INTERNAL_SERVER_ERROR);
1079
1080            if (LOG.isErrorEnabled()) {
1081                LOG.error(Messages.get().getBundle().key(Messages.LOG_REPOSITORY_ERROR_2, "COPY", src), ex);
1082            }
1083            return;
1084        }
1085
1086        if (LOG.isDebugEnabled()) {
1087            LOG.debug(Messages.get().getBundle().key(Messages.LOG_COPY_SUCCESS_0));
1088        }
1089    }
1090
1091    /**
1092     * Process a DELETE WebDAV request for the specified resource.<p>
1093     *
1094     * @param req the servlet request we are processing
1095     * @param resp the servlet response we are creating
1096     *
1097     * @throws IOException if an input/output error occurs
1098     */
1099    @Override
1100    protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws IOException {
1101
1102        // Get the path to delete
1103        String path = getRelativePath(req);
1104
1105        // Check if webdav is set to read only
1106        if (m_readOnly) {
1107
1108            resp.setStatus(CmsWebdavStatus.SC_FORBIDDEN);
1109
1110            if (LOG.isDebugEnabled()) {
1111                LOG.debug(Messages.get().getBundle().key(Messages.LOG_WEBDAV_READ_ONLY_0));
1112            }
1113
1114            return;
1115        }
1116
1117        // Check if path exists
1118        boolean exists = m_session.exists(path);
1119        if (!exists) {
1120
1121            resp.setStatus(CmsWebdavStatus.SC_NOT_FOUND);
1122
1123            if (LOG.isDebugEnabled()) {
1124                LOG.debug(Messages.get().getBundle().key(Messages.LOG_ITEM_NOT_FOUND_1, path));
1125            }
1126
1127            return;
1128        }
1129
1130        // Check if resource is locked
1131        if (isLocked(req)) {
1132
1133            resp.setStatus(CmsWebdavStatus.SC_LOCKED);
1134
1135            if (LOG.isDebugEnabled()) {
1136                LOG.debug(Messages.get().getBundle().key(Messages.LOG_ITEM_LOCKED_1, path));
1137            }
1138
1139            return;
1140        }
1141
1142        // Check if resources found in the tree of the path are locked
1143        Hashtable<String, Integer> errorList = new Hashtable<String, Integer>();
1144
1145        checkChildLocks(req, path, errorList);
1146        if (!errorList.isEmpty()) {
1147            sendReport(req, resp, errorList);
1148
1149            if (LOG.isDebugEnabled()) {
1150                Iterator<String> iter = errorList.keySet().iterator();
1151                while (iter.hasNext()) {
1152                    String errorPath = iter.next();
1153                    LOG.debug(Messages.get().getBundle().key(Messages.LOG_CHILD_LOCKED_1, errorPath));
1154                }
1155            }
1156
1157            return;
1158        }
1159
1160        // Delete the resource
1161        try {
1162
1163            if (LOG.isDebugEnabled()) {
1164                LOG.debug(Messages.get().getBundle().key(Messages.LOG_DELETE_ITEM_0));
1165            }
1166
1167            m_session.delete(path);
1168        } catch (CmsVfsResourceNotFoundException rnfex) {
1169
1170            // should never happen
1171            resp.setStatus(CmsWebdavStatus.SC_NOT_FOUND);
1172            return;
1173        } catch (CmsSecurityException sex) {
1174            resp.setStatus(CmsWebdavStatus.SC_FORBIDDEN);
1175
1176            if (LOG.isDebugEnabled()) {
1177                LOG.debug(Messages.get().getBundle().key(Messages.LOG_NO_PERMISSION_0));
1178            }
1179
1180            return;
1181        } catch (CmsException ex) {
1182            resp.setStatus(CmsWebdavStatus.SC_INTERNAL_SERVER_ERROR);
1183
1184            if (LOG.isErrorEnabled()) {
1185                LOG.error(Messages.get().getBundle().key(Messages.LOG_REPOSITORY_ERROR_2, "DELETE", path), ex);
1186            }
1187
1188            return;
1189        }
1190
1191        if (LOG.isDebugEnabled()) {
1192            LOG.debug(Messages.get().getBundle().key(Messages.LOG_DELETE_SUCCESS_0));
1193        }
1194
1195        resp.setStatus(CmsWebdavStatus.SC_NO_CONTENT);
1196    }
1197
1198    /**
1199     * Process a GET request for the specified resource.<p>
1200     *
1201     * @param request the servlet request we are processing
1202     * @param response the servlet response we are creating
1203     *
1204     * @throws IOException if an input/output error occurs
1205     */
1206    @Override
1207    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
1208
1209        // Serve the requested resource, including the data content
1210        serveResource(request, response, true);
1211    }
1212
1213    /**
1214     * Process a HEAD request for the specified resource.<p>
1215     *
1216     * @param request the servlet request we are processing
1217     * @param response the servlet response we are creating
1218     *
1219     * @throws IOException if an input/output error occurs
1220     */
1221    @Override
1222    protected void doHead(HttpServletRequest request, HttpServletResponse response) throws IOException {
1223
1224        // Serve the requested resource, without the data content
1225        serveResource(request, response, false);
1226    }
1227
1228    /**
1229     * Process a LOCK WebDAV request for the specified resource.<p>
1230     *
1231     * @param req the servlet request we are processing
1232     * @param resp the servlet response we are creating
1233     *
1234     * @throws IOException if an input/output error occurs
1235     */
1236    @SuppressWarnings("unchecked")
1237    protected void doLock(HttpServletRequest req, HttpServletResponse resp) throws IOException {
1238
1239        String path = getRelativePath(req);
1240
1241        // Check if webdav is set to read only
1242        if (m_readOnly) {
1243
1244            resp.setStatus(CmsWebdavStatus.SC_FORBIDDEN);
1245
1246            if (LOG.isDebugEnabled()) {
1247                LOG.debug(Messages.get().getBundle().key(Messages.LOG_WEBDAV_READ_ONLY_0));
1248            }
1249
1250            return;
1251        }
1252
1253        // Check if resource is locked
1254        if (isLocked(req)) {
1255
1256            resp.setStatus(CmsWebdavStatus.SC_LOCKED);
1257
1258            if (LOG.isDebugEnabled()) {
1259                LOG.debug(Messages.get().getBundle().key(Messages.LOG_ITEM_LOCKED_1, path));
1260            }
1261
1262            return;
1263        }
1264
1265        CmsRepositoryLockInfo lock = new CmsRepositoryLockInfo();
1266
1267        // Parsing depth header
1268        String depthStr = req.getHeader(HEADER_DEPTH);
1269        if (depthStr == null) {
1270            lock.setDepth(CmsRepositoryLockInfo.DEPTH_INFINITY_VALUE);
1271        } else {
1272            if (depthStr.equals("0")) {
1273                lock.setDepth(0);
1274            } else {
1275                lock.setDepth(CmsRepositoryLockInfo.DEPTH_INFINITY_VALUE);
1276            }
1277        }
1278
1279        // Parsing timeout header
1280        int lockDuration = CmsRepositoryLockInfo.TIMEOUT_INFINITE_VALUE;
1281        lock.setExpiresAt(System.currentTimeMillis() + (lockDuration * 1000));
1282
1283        int lockRequestType = LOCK_CREATION;
1284
1285        Element lockInfoNode = null;
1286
1287        try {
1288            SAXReader saxReader = new SAXReader();
1289            Document document = saxReader.read(new InputSource(req.getInputStream()));
1290
1291            // Get the root element of the document
1292            Element rootElement = document.getRootElement();
1293            lockInfoNode = rootElement;
1294        } catch (Exception e) {
1295            lockRequestType = LOCK_REFRESH;
1296        }
1297
1298        if (lockInfoNode != null) {
1299
1300            // Reading lock information
1301            Iterator<Element> iter = lockInfoNode.elementIterator();
1302
1303            Element lockScopeNode = null;
1304            Element lockTypeNode = null;
1305            Element lockOwnerNode = null;
1306
1307            while (iter.hasNext()) {
1308                Element currentElem = iter.next();
1309                switch (currentElem.getNodeType()) {
1310                    case Node.TEXT_NODE:
1311                        break;
1312                    case Node.ELEMENT_NODE:
1313                        String nodeName = currentElem.getName();
1314                        if (nodeName.endsWith(TAG_LOCKSCOPE)) {
1315                            lockScopeNode = currentElem;
1316                        }
1317                        if (nodeName.endsWith(TAG_LOCKTYPE)) {
1318                            lockTypeNode = currentElem;
1319                        }
1320                        if (nodeName.endsWith(TAG_OWNER)) {
1321                            lockOwnerNode = currentElem;
1322                        }
1323                        break;
1324                    default:
1325                        break;
1326                }
1327            }
1328
1329            if (lockScopeNode != null) {
1330
1331                iter = lockScopeNode.elementIterator();
1332                while (iter.hasNext()) {
1333                    Element currentElem = iter.next();
1334                    switch (currentElem.getNodeType()) {
1335                        case Node.TEXT_NODE:
1336                            break;
1337                        case Node.ELEMENT_NODE:
1338                            String tempScope = currentElem.getName();
1339                            if (tempScope.indexOf(':') != -1) {
1340                                lock.setScope(tempScope.substring(tempScope.indexOf(':') + 1));
1341                            } else {
1342                                lock.setScope(tempScope);
1343                            }
1344                            break;
1345                        default:
1346                            break;
1347                    }
1348                }
1349
1350                if (lock.getScope() == null) {
1351
1352                    // Bad request
1353                    resp.setStatus(CmsWebdavStatus.SC_BAD_REQUEST);
1354                }
1355
1356            } else {
1357
1358                // Bad request
1359                resp.setStatus(CmsWebdavStatus.SC_BAD_REQUEST);
1360            }
1361
1362            if (lockTypeNode != null) {
1363
1364                iter = lockTypeNode.elementIterator();
1365                while (iter.hasNext()) {
1366                    Element currentElem = iter.next();
1367                    switch (currentElem.getNodeType()) {
1368                        case Node.TEXT_NODE:
1369                            break;
1370                        case Node.ELEMENT_NODE:
1371                            String tempType = currentElem.getName();
1372                            if (tempType.indexOf(':') != -1) {
1373                                lock.setType(tempType.substring(tempType.indexOf(':') + 1));
1374                            } else {
1375                                lock.setType(tempType);
1376                            }
1377                            break;
1378                        default:
1379                            break;
1380                    }
1381                }
1382
1383                if (lock.getType() == null) {
1384
1385                    // Bad request
1386                    resp.setStatus(CmsWebdavStatus.SC_BAD_REQUEST);
1387                }
1388
1389            } else {
1390
1391                // Bad request
1392                resp.setStatus(CmsWebdavStatus.SC_BAD_REQUEST);
1393            }
1394
1395            if (lockOwnerNode != null) {
1396
1397                iter = lockOwnerNode.elementIterator();
1398                while (iter.hasNext()) {
1399                    Element currentElem = iter.next();
1400                    switch (currentElem.getNodeType()) {
1401                        case Node.TEXT_NODE:
1402                            lock.setOwner(lock.getOwner() + currentElem.getStringValue());
1403                            break;
1404                        case Node.ELEMENT_NODE:
1405                            lock.setOwner(lock.getOwner() + currentElem.getStringValue());
1406                            break;
1407                        default:
1408                            break;
1409                    }
1410                }
1411
1412                if (lock.getOwner() == null) {
1413
1414                    // Bad request
1415                    resp.setStatus(CmsWebdavStatus.SC_BAD_REQUEST);
1416                }
1417
1418            } else {
1419                lock.setOwner("");
1420            }
1421
1422        }
1423
1424        lock.setPath(path);
1425        lock.setUsername(m_username);
1426
1427        if (lockRequestType == LOCK_REFRESH) {
1428
1429            CmsRepositoryLockInfo currentLock = m_session.getLock(path);
1430            if (currentLock == null) {
1431                lockRequestType = LOCK_CREATION;
1432            }
1433        }
1434
1435        if (lockRequestType == LOCK_CREATION) {
1436
1437            try {
1438
1439                if (LOG.isDebugEnabled()) {
1440                    LOG.debug(Messages.get().getBundle().key(Messages.LOG_LOCK_ITEM_1, lock.getOwner()));
1441                }
1442
1443                boolean result = m_session.lock(path, lock);
1444                if (result) {
1445
1446                    // Add the Lock-Token header as by RFC 2518 8.10.1
1447                    // - only do this for newly created locks
1448                    resp.addHeader(HEADER_LOCKTOKEN, "<opaquelocktoken:" + generateLockToken(req, lock) + ">");
1449
1450                    if (LOG.isDebugEnabled()) {
1451                        LOG.debug(Messages.get().getBundle().key(Messages.LOG_LOCK_ITEM_FAILED_0));
1452                    }
1453
1454                } else {
1455
1456                    resp.setStatus(CmsWebdavStatus.SC_LOCKED);
1457
1458                    if (LOG.isDebugEnabled()) {
1459                        LOG.debug(Messages.get().getBundle().key(Messages.LOG_LOCK_ITEM_SUCCESS_0));
1460                    }
1461
1462                    return;
1463                }
1464            } catch (CmsVfsResourceNotFoundException rnfex) {
1465                resp.setStatus(CmsWebdavStatus.SC_NOT_FOUND);
1466
1467                if (LOG.isDebugEnabled()) {
1468                    LOG.debug(Messages.get().getBundle().key(Messages.LOG_ITEM_NOT_FOUND_1, path));
1469                }
1470
1471                return;
1472            } catch (CmsSecurityException sex) {
1473                resp.setStatus(CmsWebdavStatus.SC_FORBIDDEN);
1474
1475                if (LOG.isDebugEnabled()) {
1476                    LOG.debug(Messages.get().getBundle().key(Messages.LOG_NO_PERMISSION_0));
1477                }
1478
1479                return;
1480            } catch (CmsException ex) {
1481                resp.setStatus(CmsWebdavStatus.SC_INTERNAL_SERVER_ERROR);
1482
1483                if (LOG.isErrorEnabled()) {
1484                    LOG.error(Messages.get().getBundle().key(Messages.LOG_REPOSITORY_ERROR_2, "LOCK", path), ex);
1485                }
1486
1487                return;
1488            }
1489        }
1490
1491        // Set the status, then generate the XML response containing
1492        // the lock information
1493        Document doc = DocumentHelper.createDocument();
1494        Element propElem = doc.addElement(new QName(TAG_PROP, Namespace.get(DEFAULT_NAMESPACE)));
1495
1496        Element lockElem = addElement(propElem, TAG_LOCKDISCOVERY);
1497        addLockElement(lock, lockElem, generateLockToken(req, lock));
1498
1499        resp.setStatus(CmsWebdavStatus.SC_OK);
1500        resp.setContentType("text/xml; charset=UTF-8");
1501
1502        Writer writer = resp.getWriter();
1503        doc.write(writer);
1504        writer.close();
1505    }
1506
1507    /**
1508     * Process a MKCOL WebDAV request for the specified resource.<p>
1509     *
1510     * @param req the servlet request we are processing
1511     * @param resp the servlet response we are creating
1512     *
1513     * @throws IOException if an input/output error occurs
1514     */
1515    protected void doMkcol(HttpServletRequest req, HttpServletResponse resp) throws IOException {
1516
1517        String path = getRelativePath(req);
1518
1519        // Check if Webdav is read only
1520        if (m_readOnly) {
1521
1522            if (LOG.isDebugEnabled()) {
1523                LOG.debug(Messages.get().getBundle().key(Messages.LOG_WEBDAV_READ_ONLY_0));
1524            }
1525
1526            resp.setStatus(CmsWebdavStatus.SC_FORBIDDEN);
1527            return;
1528        }
1529
1530        // Check if resource is locked
1531        if (isLocked(req)) {
1532
1533            if (LOG.isDebugEnabled()) {
1534                LOG.debug(Messages.get().getBundle().key(Messages.LOG_ITEM_LOCKED_1, path));
1535            }
1536
1537            resp.setStatus(CmsWebdavStatus.SC_LOCKED);
1538            return;
1539        }
1540
1541        boolean exists = m_session.exists(path);
1542
1543        // Can't create a collection if a resource already exists at the given path
1544        if (exists) {
1545
1546            if (LOG.isDebugEnabled()) {
1547                LOG.debug(Messages.get().getBundle().key(Messages.LOG_ITEM_EXISTS_1, path));
1548            }
1549
1550            // Get allowed methods
1551            StringBuffer methodsAllowed = determineMethodsAllowed(getRelativePath(req));
1552            resp.addHeader(HEADER_ALLOW, methodsAllowed.toString());
1553            resp.setStatus(CmsWebdavStatus.SC_METHOD_NOT_ALLOWED);
1554            return;
1555        }
1556
1557        if (req.getInputStream().available() > 0) {
1558            try {
1559                new SAXReader().read(req.getInputStream());
1560
1561                // TODO: Process this request body (from Apache Tomcat)
1562                resp.setStatus(CmsWebdavStatus.SC_NOT_IMPLEMENTED);
1563                return;
1564
1565            } catch (DocumentException de) {
1566
1567                // Parse error - assume invalid content
1568                resp.setStatus(CmsWebdavStatus.SC_BAD_REQUEST);
1569
1570                if (LOG.isDebugEnabled()) {
1571                    LOG.debug(Messages.get().getBundle().key(Messages.LOG_INVALID_CONTENT_0));
1572                }
1573
1574                return;
1575            }
1576        }
1577
1578        // call session to create collection
1579        try {
1580
1581            if (LOG.isDebugEnabled()) {
1582                LOG.debug(Messages.get().getBundle().key(Messages.LOG_CREATE_COLLECTION_0));
1583            }
1584
1585            m_session.create(path);
1586        } catch (CmsVfsResourceAlreadyExistsException raeex) {
1587
1588            // should never happen, because it was checked if the item exists before
1589            resp.setStatus(CmsWebdavStatus.SC_PRECONDITION_FAILED);
1590
1591            if (LOG.isDebugEnabled()) {
1592                LOG.debug(Messages.get().getBundle().key(Messages.LOG_ITEM_EXISTS_1, path));
1593            }
1594
1595            return;
1596        } catch (CmsSecurityException sex) {
1597
1598            resp.setStatus(CmsWebdavStatus.SC_FORBIDDEN);
1599
1600            if (LOG.isDebugEnabled()) {
1601                LOG.debug(Messages.get().getBundle().key(Messages.LOG_NO_PERMISSION_0));
1602            }
1603
1604            return;
1605        } catch (CmsException ex) {
1606
1607            resp.setStatus(CmsWebdavStatus.SC_INTERNAL_SERVER_ERROR);
1608
1609            if (LOG.isErrorEnabled()) {
1610                LOG.error(Messages.get().getBundle().key(Messages.LOG_REPOSITORY_ERROR_2, "MKCOL", path), ex);
1611            }
1612
1613            return;
1614        }
1615
1616        if (LOG.isDebugEnabled()) {
1617            LOG.debug(Messages.get().getBundle().key(Messages.LOG_CREATE_SUCCESS_0));
1618        }
1619
1620        resp.setStatus(CmsWebdavStatus.SC_CREATED);
1621    }
1622
1623    /**
1624     * Process a MOVE WebDAV request for the specified resource.<p>
1625     *
1626     * @param req the servlet request we are processing
1627     * @param resp the servlet response we are creating
1628     */
1629    protected void doMove(HttpServletRequest req, HttpServletResponse resp) {
1630
1631        // Get source path
1632        String src = getRelativePath(req);
1633
1634        // Check if Webdav is read only
1635        if (m_readOnly) {
1636
1637            resp.setStatus(CmsWebdavStatus.SC_FORBIDDEN);
1638
1639            if (LOG.isDebugEnabled()) {
1640                LOG.debug(Messages.get().getBundle().key(Messages.LOG_WEBDAV_READ_ONLY_0));
1641            }
1642
1643            return;
1644        }
1645
1646        // Check if resource is locked
1647        if (isLocked(req)) {
1648
1649            resp.setStatus(CmsWebdavStatus.SC_LOCKED);
1650
1651            if (LOG.isDebugEnabled()) {
1652                LOG.debug(Messages.get().getBundle().key(Messages.LOG_ITEM_LOCKED_1, src));
1653            }
1654
1655            return;
1656        }
1657
1658        // Parsing destination header
1659        String dest = parseDestinationHeader(req);
1660        if (dest == null) {
1661
1662            resp.setStatus(CmsWebdavStatus.SC_BAD_REQUEST);
1663
1664            if (LOG.isDebugEnabled()) {
1665                LOG.debug(Messages.get().getBundle().key(Messages.LOG_PARSE_DEST_HEADER_0));
1666            }
1667
1668            return;
1669        }
1670
1671        // source and destination are the same
1672        if (dest.equals(src)) {
1673
1674            resp.setStatus(CmsWebdavStatus.SC_FORBIDDEN);
1675
1676            if (LOG.isDebugEnabled()) {
1677                LOG.debug(Messages.get().getBundle().key(Messages.LOG_SRC_DEST_EQUALS_0));
1678            }
1679
1680            return;
1681        }
1682
1683        // Parsing overwrite header
1684        boolean overwrite = parseOverwriteHeader(req);
1685
1686        // Check if source exists
1687        if (!m_session.exists(src)) {
1688
1689            resp.setStatus(CmsWebdavStatus.SC_NOT_FOUND);
1690
1691            if (LOG.isDebugEnabled()) {
1692                LOG.debug(Messages.get().getBundle().key(Messages.LOG_ITEM_NOT_FOUND_1, src));
1693            }
1694
1695            return;
1696        }
1697
1698        // If the destination exists, then it's a conflict
1699        if ((m_session.exists(dest)) && (!overwrite)) {
1700
1701            resp.setStatus(CmsWebdavStatus.SC_PRECONDITION_FAILED);
1702
1703            if (LOG.isDebugEnabled()) {
1704                LOG.debug(Messages.get().getBundle().key(Messages.LOG_DEST_PATH_EXISTS_1, dest));
1705            }
1706
1707            return;
1708        }
1709
1710        if ((!m_session.exists(dest)) && (overwrite)) {
1711            resp.setStatus(CmsWebdavStatus.SC_CREATED);
1712        }
1713
1714        // trigger move in session handler
1715        try {
1716
1717            if (LOG.isDebugEnabled()) {
1718                LOG.debug(Messages.get().getBundle().key(Messages.LOG_MOVE_ITEM_2, src, dest));
1719            }
1720
1721            m_session.move(src, dest, overwrite);
1722        } catch (CmsVfsResourceNotFoundException rnfex) {
1723            resp.setStatus(CmsWebdavStatus.SC_NOT_FOUND);
1724
1725            if (LOG.isDebugEnabled()) {
1726                LOG.debug(Messages.get().getBundle().key(Messages.LOG_ITEM_NOT_FOUND_1, src));
1727            }
1728
1729            return;
1730        } catch (CmsSecurityException sex) {
1731            resp.setStatus(CmsWebdavStatus.SC_FORBIDDEN);
1732
1733            if (LOG.isDebugEnabled()) {
1734                LOG.debug(Messages.get().getBundle().key(Messages.LOG_NO_PERMISSION_0));
1735            }
1736
1737            return;
1738        } catch (CmsVfsResourceAlreadyExistsException raeex) {
1739            resp.setStatus(CmsWebdavStatus.SC_PRECONDITION_FAILED);
1740
1741            if (LOG.isDebugEnabled()) {
1742                LOG.debug(Messages.get().getBundle().key(Messages.LOG_ITEM_EXISTS_1, dest));
1743            }
1744
1745            return;
1746        } catch (CmsException ex) {
1747            resp.setStatus(CmsWebdavStatus.SC_INTERNAL_SERVER_ERROR);
1748
1749            if (LOG.isErrorEnabled()) {
1750                LOG.error(Messages.get().getBundle().key(Messages.LOG_REPOSITORY_ERROR_2, "MOVE", src), ex);
1751            }
1752
1753            return;
1754        }
1755
1756        if (LOG.isDebugEnabled()) {
1757            LOG.debug(Messages.get().getBundle().key(Messages.LOG_MOVE_ITEM_SUCCESS_0));
1758        }
1759    }
1760
1761    /**
1762     * Process a OPTIONS WebDAV request for the specified resource.<p>
1763     *
1764     * @param req the servlet request we are processing
1765     * @param resp the servlet response we are creating
1766     */
1767    @Override
1768    protected void doOptions(HttpServletRequest req, HttpServletResponse resp) {
1769
1770        resp.addHeader("DAV", "1,2");
1771
1772        StringBuffer methodsAllowed = determineMethodsAllowed(getRelativePath(req));
1773
1774        resp.addHeader(HEADER_ALLOW, methodsAllowed.toString());
1775        resp.addHeader("MS-Author-Via", "DAV");
1776    }
1777
1778    /**
1779     * Process a PROPFIND WebDAV request for the specified resource.<p>
1780     *
1781     * @param req the servlet request we are processing
1782     * @param resp the servlet response we are creating
1783     *
1784     * @throws IOException if an input/output error occurs
1785     */
1786    protected void doPropfind(HttpServletRequest req, HttpServletResponse resp) throws IOException {
1787
1788        String path = getRelativePath(req);
1789
1790        if (!m_listings) {
1791
1792            // Get allowed methods
1793            StringBuffer methodsAllowed = determineMethodsAllowed(getRelativePath(req));
1794
1795            resp.addHeader(HEADER_ALLOW, methodsAllowed.toString());
1796            resp.setStatus(CmsWebdavStatus.SC_METHOD_NOT_ALLOWED);
1797            return;
1798        }
1799
1800        // Properties which are to be displayed.
1801        List<String> properties = new Vector<String>();
1802
1803        // Propfind depth
1804        int depth = CmsRepositoryLockInfo.DEPTH_INFINITY_VALUE;
1805
1806        // Propfind type
1807        int type = FIND_ALL_PROP;
1808
1809        String depthStr = req.getHeader(HEADER_DEPTH);
1810
1811        if (depthStr == null) {
1812            depth = CmsRepositoryLockInfo.DEPTH_INFINITY_VALUE;
1813        } else {
1814            if (depthStr.equals("0")) {
1815                depth = 0;
1816            } else if (depthStr.equals("1")) {
1817                depth = 1;
1818            } else if (depthStr.equalsIgnoreCase(DEPTH_INFINITY)) {
1819                depth = CmsRepositoryLockInfo.DEPTH_INFINITY_VALUE;
1820            }
1821        }
1822
1823        Element propNode = null;
1824
1825        try {
1826            SAXReader saxReader = new SAXReader();
1827            Document document = saxReader.read(req.getInputStream());
1828
1829            // Get the root element of the document
1830            Element rootElement = document.getRootElement();
1831            @SuppressWarnings("unchecked")
1832            Iterator<Element> iter = rootElement.elementIterator();
1833
1834            while (iter.hasNext()) {
1835                Element currentElem = iter.next();
1836                switch (currentElem.getNodeType()) {
1837                    case Node.TEXT_NODE:
1838                        break;
1839                    case Node.ELEMENT_NODE:
1840                        if (currentElem.getName().endsWith("prop")) {
1841                            type = FIND_BY_PROPERTY;
1842                            propNode = currentElem;
1843                        }
1844                        if (currentElem.getName().endsWith("propname")) {
1845                            type = FIND_PROPERTY_NAMES;
1846                        }
1847                        if (currentElem.getName().endsWith("allprop")) {
1848                            type = FIND_ALL_PROP;
1849                        }
1850                        break;
1851                    default:
1852                        break;
1853                }
1854            }
1855        } catch (Exception e) {
1856            // Most likely there was no content : we use the defaults.
1857        }
1858
1859        if (propNode != null) {
1860            if (type == FIND_BY_PROPERTY) {
1861                @SuppressWarnings("unchecked")
1862                Iterator<Element> iter = propNode.elementIterator();
1863                while (iter.hasNext()) {
1864                    Element currentElem = iter.next();
1865                    switch (currentElem.getNodeType()) {
1866                        case Node.TEXT_NODE:
1867                            break;
1868                        case Node.ELEMENT_NODE:
1869                            String nodeName = currentElem.getName();
1870                            String propertyName = null;
1871                            if (nodeName.indexOf(':') != -1) {
1872                                propertyName = nodeName.substring(nodeName.indexOf(':') + 1);
1873                            } else {
1874                                propertyName = nodeName;
1875                            }
1876                            // href is a live property which is handled differently
1877                            properties.add(propertyName);
1878                            break;
1879                        default:
1880                            break;
1881                    }
1882                }
1883            }
1884        }
1885
1886        boolean exists = m_session.exists(path);
1887        if (!exists) {
1888
1889            resp.setStatus(CmsWebdavStatus.SC_NOT_FOUND);
1890
1891            if (LOG.isDebugEnabled()) {
1892                LOG.debug(Messages.get().getBundle().key(Messages.LOG_ITEM_NOT_FOUND_1, path));
1893            }
1894
1895            return;
1896        }
1897
1898        I_CmsRepositoryItem item = null;
1899        try {
1900            item = m_session.getItem(path);
1901        } catch (CmsException e) {
1902            resp.setStatus(CmsWebdavStatus.SC_NOT_FOUND);
1903            return;
1904        }
1905
1906        resp.setStatus(CmsWebdavStatus.SC_MULTI_STATUS);
1907        resp.setContentType("text/xml; charset=UTF-8");
1908
1909        // Create multistatus object
1910        Document doc = DocumentHelper.createDocument();
1911        Element multiStatusElem = doc.addElement(new QName(TAG_MULTISTATUS, Namespace.get("D", DEFAULT_NAMESPACE)));
1912
1913        if (depth == 0) {
1914            parseProperties(req, multiStatusElem, item, type, properties);
1915        } else {
1916            // The stack always contains the object of the current level
1917            Stack<I_CmsRepositoryItem> stack = new Stack<I_CmsRepositoryItem>();
1918            stack.push(item);
1919
1920            // Stack of the objects one level below
1921            Stack<I_CmsRepositoryItem> stackBelow = new Stack<I_CmsRepositoryItem>();
1922
1923            while ((!stack.isEmpty()) && (depth >= 0)) {
1924
1925                I_CmsRepositoryItem currentItem = stack.pop();
1926                parseProperties(req, multiStatusElem, currentItem, type, properties);
1927
1928                if ((currentItem.isCollection()) && (depth > 0)) {
1929
1930                    try {
1931                        List<I_CmsRepositoryItem> list = m_session.list(currentItem.getName());
1932                        Iterator<I_CmsRepositoryItem> iter = list.iterator();
1933                        while (iter.hasNext()) {
1934                            I_CmsRepositoryItem element = iter.next();
1935                            stackBelow.push(element);
1936                        }
1937
1938                    } catch (CmsException e) {
1939
1940                        resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
1941
1942                        if (LOG.isErrorEnabled()) {
1943                            LOG.error(
1944                                Messages.get().getBundle().key(Messages.LOG_LIST_ITEMS_ERROR_1, currentItem.getName()),
1945                                e);
1946                        }
1947
1948                        return;
1949                    }
1950                }
1951
1952                if (stack.isEmpty()) {
1953                    depth--;
1954                    stack = stackBelow;
1955                    stackBelow = new Stack<I_CmsRepositoryItem>();
1956                }
1957            }
1958        }
1959
1960        Writer writer = resp.getWriter();
1961        doc.write(writer);
1962        writer.close();
1963    }
1964
1965    /**
1966     * Process a PROPPATCH WebDAV request for the specified resource.<p>
1967     *
1968     * Not implemented yet.<p>
1969     *
1970     * @param req the servlet request we are processing
1971     * @param resp the servlet response we are creating
1972     */
1973    protected void doProppatch(HttpServletRequest req, HttpServletResponse resp) {
1974
1975        // Check if Webdav is read only
1976        if (m_readOnly) {
1977
1978            resp.setStatus(CmsWebdavStatus.SC_FORBIDDEN);
1979
1980            if (LOG.isDebugEnabled()) {
1981                LOG.debug(Messages.get().getBundle().key(Messages.LOG_WEBDAV_READ_ONLY_0));
1982            }
1983
1984            return;
1985        }
1986
1987        // Check if resource is locked
1988        if (isLocked(req)) {
1989
1990            resp.setStatus(CmsWebdavStatus.SC_LOCKED);
1991
1992            if (LOG.isDebugEnabled()) {
1993                LOG.debug(Messages.get().getBundle().key(Messages.LOG_ITEM_LOCKED_1, getRelativePath(req)));
1994            }
1995
1996            return;
1997        }
1998
1999        resp.setStatus(HttpServletResponse.SC_NOT_IMPLEMENTED);
2000    }
2001
2002    /**
2003     * Process a POST request for the specified resource.<p>
2004     *
2005     * @param req the servlet request we are processing
2006     * @param resp the servlet response we are creating
2007     *
2008     * @throws IOException if an input/output error occurs
2009     */
2010    @Override
2011    protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IOException {
2012
2013        String path = getRelativePath(req);
2014
2015        // Check if webdav is set to read only
2016        if (m_readOnly) {
2017
2018            resp.setStatus(HttpServletResponse.SC_FORBIDDEN);
2019
2020            if (LOG.isDebugEnabled()) {
2021                LOG.debug(Messages.get().getBundle().key(Messages.LOG_WEBDAV_READ_ONLY_0));
2022            }
2023
2024            return;
2025        }
2026
2027        // Check if resource is locked
2028        if (isLocked(req)) {
2029
2030            resp.setStatus(CmsWebdavStatus.SC_LOCKED);
2031
2032            if (LOG.isDebugEnabled()) {
2033                LOG.debug(Messages.get().getBundle().key(Messages.LOG_ITEM_LOCKED_1, path));
2034            }
2035
2036            return;
2037        }
2038
2039        boolean exists = m_session.exists(path);
2040        boolean result = true;
2041
2042        // Temp. content file used to support partial PUT
2043        File contentFile = null;
2044
2045        CmsWebdavRange range = parseContentRange(req, resp);
2046
2047        InputStream resourceInputStream = null;
2048
2049        // Append data specified in ranges to existing content for this
2050        // resource - create a temp. file on the local filesystem to
2051        // perform this operation
2052        // Assume just one range is specified for now
2053        if (range != null) {
2054            contentFile = executePartialPut(req, range, path);
2055            resourceInputStream = new FileInputStream(contentFile);
2056        } else {
2057            resourceInputStream = req.getInputStream();
2058        }
2059
2060        try {
2061
2062            // FIXME: Add attributes(from Apache Tomcat)
2063            if (LOG.isDebugEnabled()) {
2064                LOG.debug(Messages.get().getBundle().key(Messages.LOG_SAVE_ITEM_0));
2065            }
2066
2067            m_session.save(path, resourceInputStream, exists);
2068        } catch (Exception e) {
2069
2070            if (LOG.isErrorEnabled()) {
2071                LOG.error(Messages.get().getBundle().key(Messages.LOG_REPOSITORY_ERROR_2, "PUT", path), e);
2072            }
2073
2074            result = false;
2075            resp.setStatus(HttpServletResponse.SC_CONFLICT);
2076        }
2077
2078        // Bugzilla 40326: at this point content file should be safe to delete
2079        // as it's no longer referenced.  Let's not rely on deleteOnExit because
2080        // it's a memory leak, as noted in this Bugzilla issue.
2081        if (contentFile != null) {
2082            try {
2083                contentFile.delete();
2084            } catch (Exception e) {
2085                if (LOG.isErrorEnabled()) {
2086                    LOG.error(Messages.get().getBundle().key(Messages.LOG_DELETE_TEMP_FILE_0), e);
2087                }
2088            }
2089        }
2090
2091        if (result) {
2092
2093            if (LOG.isDebugEnabled()) {
2094                LOG.debug(Messages.get().getBundle().key(Messages.LOG_SAVE_SUCCESS_0));
2095            }
2096
2097            if (exists) {
2098                resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
2099            } else {
2100                resp.setStatus(HttpServletResponse.SC_CREATED);
2101            }
2102        }
2103    }
2104
2105    /**
2106     * Process a UNLOCK WebDAV request for the specified resource.<p>
2107     *
2108     * @param req the servlet request we are processing
2109     * @param resp the servlet response we are creating
2110     */
2111    protected void doUnlock(HttpServletRequest req, HttpServletResponse resp) {
2112
2113        String path = getRelativePath(req);
2114
2115        // Check if Webdav is read only
2116        if (m_readOnly) {
2117
2118            resp.setStatus(CmsWebdavStatus.SC_FORBIDDEN);
2119
2120            if (LOG.isDebugEnabled()) {
2121                LOG.debug(Messages.get().getBundle().key(Messages.LOG_WEBDAV_READ_ONLY_0));
2122            }
2123
2124            return;
2125        }
2126
2127        // Check if resource is locked
2128        if (isLocked(req)) {
2129
2130            resp.setStatus(CmsWebdavStatus.SC_LOCKED);
2131
2132            if (LOG.isDebugEnabled()) {
2133                LOG.debug(Messages.get().getBundle().key(Messages.LOG_ITEM_LOCKED_1, path));
2134            }
2135
2136            return;
2137        }
2138
2139        if (LOG.isDebugEnabled()) {
2140            LOG.debug(Messages.get().getBundle().key(Messages.LOG_UNLOCK_ITEM_0));
2141        }
2142
2143        m_session.unlock(path);
2144
2145        resp.setStatus(CmsWebdavStatus.SC_NO_CONTENT);
2146    }
2147
2148    /**
2149     * Handle a partial PUT.<p>
2150     *
2151     * New content specified in request is appended to
2152     * existing content in oldRevisionContent (if present). This code does
2153     * not support simultaneous partial updates to the same resource.<p>
2154     *
2155     * @param req the servlet request we are processing
2156     * @param range the range of the content in the file
2157     * @param path the path where to find the resource
2158     *
2159     * @return the new content file with the appended data
2160     *
2161     * @throws IOException if an input/output error occurs
2162     */
2163    protected File executePartialPut(HttpServletRequest req, CmsWebdavRange range, String path) throws IOException {
2164
2165        // Append data specified in ranges to existing content for this
2166        // resource - create a temp. file on the local filesystem to
2167        // perform this operation
2168        File tempDir = (File)getServletContext().getAttribute(ATT_SERVLET_TEMPDIR);
2169
2170        // Convert all '/' characters to '.' in resourcePath
2171        String convertedResourcePath = path.replace('/', '.');
2172        File contentFile = new File(tempDir, convertedResourcePath);
2173        contentFile.createNewFile();
2174
2175        RandomAccessFile randAccessContentFile = new RandomAccessFile(contentFile, "rw");
2176
2177        InputStream oldResourceStream = null;
2178        try {
2179            I_CmsRepositoryItem item = m_session.getItem(path);
2180
2181            oldResourceStream = new ByteArrayInputStream(item.getContent());
2182        } catch (CmsException e) {
2183            if (LOG.isErrorEnabled()) {
2184                LOG.error(Messages.get().getBundle().key(Messages.LOG_ITEM_NOT_FOUND_1, path), e);
2185            }
2186        }
2187
2188        // Copy data in oldRevisionContent to contentFile
2189        if (oldResourceStream != null) {
2190
2191            int numBytesRead;
2192            byte[] copyBuffer = new byte[BUFFER_SIZE];
2193            while ((numBytesRead = oldResourceStream.read(copyBuffer)) != -1) {
2194                randAccessContentFile.write(copyBuffer, 0, numBytesRead);
2195            }
2196
2197            oldResourceStream.close();
2198        }
2199
2200        randAccessContentFile.setLength(range.getLength());
2201
2202        // Append data in request input stream to contentFile
2203        randAccessContentFile.seek(range.getStart());
2204        int numBytesRead;
2205        byte[] transferBuffer = new byte[BUFFER_SIZE];
2206        BufferedInputStream requestBufInStream = new BufferedInputStream(req.getInputStream(), BUFFER_SIZE);
2207        while ((numBytesRead = requestBufInStream.read(transferBuffer)) != -1) {
2208            randAccessContentFile.write(transferBuffer, 0, numBytesRead);
2209        }
2210        randAccessContentFile.close();
2211        requestBufInStream.close();
2212
2213        return contentFile;
2214    }
2215
2216    /**
2217     * Get the ETag associated with a file.<p>
2218     *
2219     * @param item the WebDavItem
2220     *
2221     * @return the created ETag for the resource attributes
2222     */
2223    protected String getETag(I_CmsRepositoryItem item) {
2224
2225        return "\"" + item.getContentLength() + "-" + item.getLastModifiedDate() + "\"";
2226    }
2227
2228    /**
2229     * Parse the range header.<p>
2230     *
2231     * @param request the servlet request we are processing
2232     * @param response the servlet response we are creating
2233     * @param item the WebdavItem with the information
2234     *
2235     * @return Vector of ranges
2236     */
2237    protected ArrayList<CmsWebdavRange> parseRange(
2238        HttpServletRequest request,
2239        HttpServletResponse response,
2240        I_CmsRepositoryItem item) {
2241
2242        // Checking If-Range
2243        String headerValue = request.getHeader(HEADER_IFRANGE);
2244
2245        if (headerValue != null) {
2246
2247            long headerValueTime = (-1L);
2248            try {
2249                headerValueTime = request.getDateHeader(HEADER_IFRANGE);
2250            } catch (Exception e) {
2251                // noop
2252            }
2253
2254            String eTag = getETag(item);
2255            long lastModified = item.getLastModifiedDate();
2256
2257            if (headerValueTime == (-1L)) {
2258
2259                // If the ETag the client gave does not match the entity
2260                // etag, then the entire entity is returned.
2261                if (!eTag.equals(headerValue.trim())) {
2262                    return FULL_RANGE;
2263                }
2264
2265            } else {
2266
2267                // If the timestamp of the entity the client got is older than
2268                // the last modification date of the entity, the entire entity
2269                // is returned.
2270                if (lastModified > (headerValueTime + 1000)) {
2271                    return FULL_RANGE;
2272                }
2273            }
2274        }
2275
2276        long fileLength = item.getContentLength();
2277
2278        if (fileLength == 0) {
2279            return null;
2280        }
2281
2282        // Retrieving the range header (if any is specified
2283        String rangeHeader = request.getHeader(HEADER_RANGE);
2284
2285        if (rangeHeader == null) {
2286            return null;
2287        }
2288
2289        // bytes is the only range unit supported (and I don't see the point
2290        // of adding new ones).
2291        if (!rangeHeader.startsWith("bytes")) {
2292            response.addHeader(HEADER_CONTENTRANGE, "bytes */" + fileLength);
2293            response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
2294            return null;
2295        }
2296
2297        rangeHeader = rangeHeader.substring(6);
2298
2299        // Vector which will contain all the ranges which are successfully parsed.
2300        ArrayList<CmsWebdavRange> result = new ArrayList<CmsWebdavRange>();
2301        StringTokenizer commaTokenizer = new StringTokenizer(rangeHeader, ",");
2302
2303        // Parsing the range list
2304        while (commaTokenizer.hasMoreTokens()) {
2305            String rangeDefinition = commaTokenizer.nextToken().trim();
2306
2307            CmsWebdavRange currentRange = new CmsWebdavRange();
2308            currentRange.setLength(fileLength);
2309
2310            int dashPos = rangeDefinition.indexOf('-');
2311
2312            if (dashPos == -1) {
2313                response.addHeader(HEADER_CONTENTRANGE, "bytes */" + fileLength);
2314                response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
2315                return null;
2316            }
2317
2318            if (dashPos == 0) {
2319
2320                try {
2321                    long offset = Long.parseLong(rangeDefinition);
2322                    currentRange.setStart(fileLength + offset);
2323                    currentRange.setEnd(fileLength - 1);
2324                } catch (NumberFormatException e) {
2325                    response.addHeader(HEADER_CONTENTRANGE, "bytes */" + fileLength);
2326                    response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
2327                    return null;
2328                }
2329
2330            } else {
2331
2332                try {
2333                    currentRange.setStart(Long.parseLong(rangeDefinition.substring(0, dashPos)));
2334                    if (dashPos < (rangeDefinition.length() - 1)) {
2335                        currentRange.setEnd(
2336                            Long.parseLong(rangeDefinition.substring(dashPos + 1, rangeDefinition.length())));
2337                    } else {
2338                        currentRange.setEnd(fileLength - 1);
2339                    }
2340                } catch (NumberFormatException e) {
2341                    response.addHeader(HEADER_CONTENTRANGE, "bytes */" + fileLength);
2342                    response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
2343                    return null;
2344                }
2345
2346            }
2347
2348            if (!currentRange.validate()) {
2349                response.addHeader(HEADER_CONTENTRANGE, "bytes */" + fileLength);
2350                response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
2351                return null;
2352            }
2353
2354            result.add(currentRange);
2355        }
2356
2357        return result;
2358    }
2359
2360    /**
2361     * Return an InputStream to an HTML representation of the contents
2362     * of this directory.<p>
2363     *
2364     * @param contextPath context path to which our internal paths are relative
2365     * @param path the path of the resource to render the html for
2366     *
2367     * @return an input stream with the rendered html
2368     *
2369     * @throws IOException if an input/output error occurs
2370     */
2371    protected InputStream renderHtml(String contextPath, String path) throws IOException {
2372
2373        String name = path;
2374        // Prepare a writer to a buffered area
2375        ByteArrayOutputStream stream = new ByteArrayOutputStream();
2376        OutputStreamWriter osWriter = null;
2377        try {
2378            osWriter = new OutputStreamWriter(stream, "UTF8");
2379        } catch (Exception e) {
2380
2381            // Should never happen
2382            osWriter = new OutputStreamWriter(stream);
2383        }
2384        PrintWriter writer = new PrintWriter(osWriter);
2385
2386        StringBuffer sb = new StringBuffer();
2387
2388        // rewriteUrl(contextPath) is expensive. cache result for later reuse
2389        String rewrittenContextPath = rewriteUrl(contextPath);
2390
2391        // Render the page header
2392        sb.append("<html>\r\n");
2393        sb.append("<head>\r\n");
2394        sb.append("<title>");
2395        sb.append(Messages.get().getBundle().key(Messages.GUI_DIRECTORY_TITLE_1, name));
2396        sb.append("</title>\r\n");
2397
2398        // TODO: add opencms css style
2399        sb.append("<STYLE><!--");
2400        sb.append(
2401            "H1 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:22px;} "
2402                + "H2 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:16px;} "
2403                + "H3 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:14px;} "
2404                + "BODY {font-family:Tahoma,Arial,sans-serif;color:black;background-color:white;} "
2405                + "B {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;} "
2406                + "P {font-family:Tahoma,Arial,sans-serif;background:white;color:black;font-size:12px;}"
2407                + "A {color : black;}"
2408                + "A.name {color : black;}"
2409                + "HR {color : #525D76;}");
2410        sb.append("--></STYLE> ");
2411
2412        sb.append("</head>\r\n");
2413        sb.append("<body>");
2414        sb.append("<h1>");
2415        sb.append(Messages.get().getBundle().key(Messages.GUI_DIRECTORY_TITLE_1, name));
2416
2417        sb.append("</h1>");
2418        sb.append("<HR size=\"1\" noshade=\"noshade\">");
2419
2420        sb.append("<table width=\"100%\" cellspacing=\"0\"" + " cellpadding=\"5\" align=\"center\">\r\n");
2421
2422        // Render the column headings
2423        sb.append("<tr>\r\n");
2424        sb.append("<td align=\"left\"><font size=\"+1\"><strong>");
2425        sb.append(Messages.get().getBundle().key(Messages.GUI_DIRECTORY_FILENAME_0));
2426        sb.append("</strong></font></td>\r\n");
2427        sb.append("<td align=\"center\"><font size=\"+1\"><strong>");
2428        sb.append(Messages.get().getBundle().key(Messages.GUI_DIRECTORY_SIZE_0));
2429        sb.append("</strong></font></td>\r\n");
2430        sb.append("<td align=\"right\"><font size=\"+1\"><strong>");
2431        sb.append(Messages.get().getBundle().key(Messages.GUI_DIRECTORY_LASTMODIFIED_0));
2432        sb.append("</strong></font></td>\r\n");
2433        sb.append("</tr>");
2434
2435        boolean shade = false;
2436
2437        // Render the link to our parent (if required)
2438        String parentDirectory = name;
2439        if (parentDirectory.endsWith("/")) {
2440            parentDirectory = parentDirectory.substring(0, parentDirectory.length() - 1);
2441        }
2442        int slash = parentDirectory.lastIndexOf('/');
2443        if (slash >= 0) {
2444
2445            String parent = parentDirectory.substring(0, slash);
2446
2447            sb.append("<tr");
2448            if (shade) {
2449                sb.append(" bgcolor=\"#eeeeee\"");
2450            }
2451            sb.append(">\r\n");
2452            shade = !shade;
2453
2454            sb.append("<td align=\"left\">&nbsp;&nbsp;\r\n");
2455            sb.append("<a href=\"");
2456            sb.append(rewrittenContextPath);
2457            if (parent.equals("")) {
2458                parent = "/";
2459            }
2460            sb.append(rewriteUrl(parent));
2461            if (!parent.endsWith("/")) {
2462                sb.append("/");
2463            }
2464            sb.append("\"><tt>");
2465            sb.append("..");
2466            sb.append("</tt></a></td>\r\n");
2467
2468            sb.append("<td align=\"right\"><tt>");
2469            sb.append("&nbsp;");
2470            sb.append("</tt></td>\r\n");
2471
2472            sb.append("<td align=\"right\"><tt>");
2473            sb.append("&nbsp;");
2474            sb.append("</tt></td>\r\n");
2475
2476            sb.append("</tr>\r\n");
2477        }
2478
2479        try {
2480
2481            // Render the directory entries within this directory
2482            List<I_CmsRepositoryItem> list = m_session.list(path);
2483            Iterator<I_CmsRepositoryItem> iter = list.iterator();
2484            while (iter.hasNext()) {
2485
2486                I_CmsRepositoryItem childItem = iter.next();
2487
2488                String resourceName = childItem.getName();
2489                if (resourceName.endsWith("/")) {
2490                    resourceName = resourceName.substring(0, resourceName.length() - 1);
2491                }
2492                slash = resourceName.lastIndexOf('/');
2493                if (slash > -1) {
2494                    resourceName = resourceName.substring(slash + 1, resourceName.length());
2495                }
2496
2497                sb.append("<tr");
2498                if (shade) {
2499                    sb.append(" bgcolor=\"#eeeeee\"");
2500                }
2501                sb.append(">\r\n");
2502                shade = !shade;
2503
2504                sb.append("<td align=\"left\">&nbsp;&nbsp;\r\n");
2505                sb.append("<a href=\"");
2506                sb.append(rewrittenContextPath);
2507                //resourceName = rewriteUrl(name + resourceName);
2508                sb.append(rewriteUrl(name + resourceName));
2509                if (childItem.isCollection()) {
2510                    sb.append("/");
2511                }
2512                sb.append("\"><tt>");
2513                sb.append(CmsEncoder.escapeXml(resourceName));
2514                if (childItem.isCollection()) {
2515                    sb.append("/");
2516                }
2517                sb.append("</tt></a></td>\r\n");
2518
2519                sb.append("<td align=\"right\"><tt>");
2520                if (childItem.isCollection()) {
2521                    sb.append("&nbsp;");
2522                } else {
2523                    sb.append(renderSize(childItem.getContentLength()));
2524                }
2525                sb.append("</tt></td>\r\n");
2526
2527                sb.append("<td align=\"right\"><tt>");
2528                sb.append(HTTP_DATE_FORMAT.format(new Date(childItem.getLastModifiedDate())));
2529                sb.append("</tt></td>\r\n");
2530
2531                sb.append("</tr>\r\n");
2532            }
2533
2534        } catch (CmsException e) {
2535
2536            // Something went wrong
2537            e.printStackTrace();
2538        }
2539
2540        // Render the page footer
2541        sb.append("</table>\r\n");
2542
2543        sb.append("<HR size=\"1\" noshade=\"noshade\">");
2544        sb.append("</body>\r\n");
2545        sb.append("</html>\r\n");
2546
2547        // Return an input stream to the underlying bytes
2548        writer.write(sb.toString());
2549        writer.flush();
2550        return (new ByteArrayInputStream(stream.toByteArray()));
2551    }
2552
2553    /**
2554     * Render the specified file size (in bytes).<p>
2555     *
2556     * @param size file size (in bytes)
2557     *
2558     * @return a string with the given size formatted to output to user
2559     */
2560    protected String renderSize(long size) {
2561
2562        long leftSide = size / 1024;
2563        long rightSide = (size % 1024) / 103; // Makes 1 digit
2564        if ((leftSide == 0) && (rightSide == 0) && (size > 0)) {
2565            rightSide = 1;
2566        }
2567
2568        return ("" + leftSide + "." + rightSide + " kb");
2569    }
2570
2571    /**
2572     * URL rewriter.<p>
2573     *
2574     * @param path path which has to be rewritten
2575     *
2576     * @return a string with the encoded path
2577     *
2578     * @throws UnsupportedEncodingException if something goes wrong while encoding the url
2579     */
2580    protected String rewriteUrl(String path) throws UnsupportedEncodingException {
2581
2582        return new String(URLCodec.encodeUrl(URL_SAFE_CHARS, path.getBytes("ISO-8859-1")));
2583    }
2584
2585    /**
2586     * Serve the specified resource, optionally including the data content.<p>
2587     *
2588     * @param request the servlet request we are processing
2589     * @param response the servlet response we are creating
2590     * @param content should the content be included?
2591     *
2592     * @throws IOException if an input/output error occurs
2593     */
2594    protected void serveResource(HttpServletRequest request, HttpServletResponse response, boolean content)
2595    throws IOException {
2596
2597        // Identify the requested resource path
2598        String path = getRelativePath(request);
2599        if (LOG.isDebugEnabled()) {
2600            if (content) {
2601                LOG.debug(Messages.get().getBundle().key(Messages.LOG_SERVE_ITEM_1, path));
2602            } else {
2603                LOG.debug(Messages.get().getBundle().key(Messages.LOG_SERVE_ITEM_HEADER_1, path));
2604            }
2605        }
2606
2607        I_CmsRepositoryItem item = null;
2608        try {
2609            item = m_session.getItem(path);
2610        } catch (CmsException ex) {
2611            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
2612
2613            if (LOG.isDebugEnabled()) {
2614                LOG.debug(Messages.get().getBundle().key(Messages.LOG_ITEM_NOT_FOUND_1, path));
2615            }
2616
2617            return;
2618        }
2619
2620        // If the resource is not a collection, and the resource path
2621        // ends with "/" or "\", return NOT FOUND
2622        if (!item.isCollection()) {
2623            if (path.endsWith("/") || (path.endsWith("\\"))) {
2624
2625                response.setStatus(HttpServletResponse.SC_NOT_FOUND);
2626
2627                if (LOG.isDebugEnabled()) {
2628                    LOG.debug(Messages.get().getBundle().key(Messages.LOG_ITEM_NOT_FOUND_1, path));
2629                }
2630
2631                return;
2632            }
2633        }
2634
2635        // Find content type.
2636        String contentType = item.getMimeType();
2637        if (contentType == null) {
2638            contentType = getServletContext().getMimeType(item.getName());
2639        }
2640
2641        ArrayList<CmsWebdavRange> ranges = null;
2642        long contentLength = -1L;
2643
2644        if (item.isCollection()) {
2645
2646            // Skip directory listings if we have been configured to suppress them
2647            if (!m_listings) {
2648                response.setStatus(HttpServletResponse.SC_NOT_FOUND);
2649                return;
2650            }
2651            contentType = "text/html;charset=UTF-8";
2652
2653        } else {
2654
2655            // Parse range specifier
2656            ranges = parseRange(request, response, item);
2657
2658            // ETag header
2659            response.setHeader(HEADER_ETAG, getETag(item));
2660
2661            // Last-Modified header
2662            response.setHeader(HEADER_LASTMODIFIED, HTTP_DATE_FORMAT.format(new Date(item.getLastModifiedDate())));
2663
2664            // Get content length
2665            contentLength = item.getContentLength();
2666
2667            // Special case for zero length files, which would cause a
2668            // (silent) ISE when setting the output buffer size
2669            if (contentLength == 0L) {
2670                content = false;
2671            }
2672
2673        }
2674
2675        ServletOutputStream ostream = null;
2676        PrintWriter writer = null;
2677
2678        if (content) {
2679
2680            // Trying to retrieve the servlet output stream
2681            try {
2682                ostream = response.getOutputStream();
2683            } catch (IllegalStateException e) {
2684
2685                // If it fails, we try to get a Writer instead if we're
2686                // trying to serve a text file
2687                if ((contentType == null) || (contentType.startsWith("text")) || (contentType.endsWith("xml"))) {
2688                    writer = response.getWriter();
2689                } else {
2690                    throw e;
2691                }
2692            }
2693
2694        }
2695
2696        if ((item.isCollection())
2697            || (((ranges == null) || (ranges.isEmpty())) && (request.getHeader(HEADER_RANGE) == null))
2698            || (ranges == FULL_RANGE)) {
2699
2700            // Set the appropriate output headers
2701            if (contentType != null) {
2702                if (LOG.isDebugEnabled()) {
2703                    LOG.debug(Messages.get().getBundle().key(Messages.LOG_SERVE_ITEM_CONTENT_TYPE_1, contentType));
2704                }
2705                response.setContentType(contentType);
2706            }
2707
2708            if ((!item.isCollection()) && (contentLength >= 0)) {
2709                if (LOG.isDebugEnabled()) {
2710                    LOG.debug(
2711                        Messages.get().getBundle().key(
2712                            Messages.LOG_SERVE_ITEM_CONTENT_LENGTH_1,
2713                            new Long(contentLength)));
2714                }
2715
2716                if (contentLength < Integer.MAX_VALUE) {
2717                    response.setContentLength((int)contentLength);
2718                } else {
2719
2720                    // Set the content-length as String to be able to use a long
2721                    response.setHeader(HEADER_CONTENTLENGTH, "" + contentLength);
2722                }
2723            }
2724
2725            InputStream renderResult = null;
2726            if (item.isCollection()) {
2727
2728                if (content) {
2729                    // Serve the directory browser
2730                    renderResult = renderHtml(request.getContextPath() + request.getServletPath(), item.getName());
2731                }
2732
2733            }
2734
2735            // Copy the input stream to our output stream (if requested)
2736            if (content) {
2737                try {
2738                    response.setBufferSize(m_output);
2739                } catch (IllegalStateException e) {
2740                    // Silent catch
2741                }
2742                if (ostream != null) {
2743                    copy(item, renderResult, ostream);
2744                } else {
2745                    copy(item, renderResult, writer);
2746                }
2747            }
2748
2749        } else {
2750
2751            if ((ranges == null) || (ranges.isEmpty())) {
2752                return;
2753            }
2754
2755            // Partial content response.
2756            response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
2757
2758            if (ranges.size() == 1) {
2759
2760                CmsWebdavRange range = ranges.get(0);
2761                response.addHeader(
2762                    HEADER_CONTENTRANGE,
2763                    "bytes " + range.getStart() + "-" + range.getEnd() + "/" + range.getLength());
2764                long length = (range.getEnd() - range.getStart()) + 1;
2765                if (length < Integer.MAX_VALUE) {
2766                    response.setContentLength((int)length);
2767                } else {
2768                    // Set the content-length as String to be able to use a long
2769                    response.setHeader(HEADER_CONTENTLENGTH, "" + length);
2770                }
2771
2772                if (contentType != null) {
2773                    if (LOG.isDebugEnabled()) {
2774                        LOG.debug(Messages.get().getBundle().key(Messages.LOG_SERVE_ITEM_CONTENT_TYPE_1, contentType));
2775                    }
2776                    response.setContentType(contentType);
2777                }
2778
2779                if (content) {
2780                    try {
2781                        response.setBufferSize(m_output);
2782                    } catch (IllegalStateException e) {
2783                        // Silent catch
2784                    }
2785                    if (ostream != null) {
2786                        copy(item, ostream, range);
2787                    } else {
2788                        copy(item, writer, range);
2789                    }
2790                }
2791
2792            } else {
2793
2794                response.setContentType("multipart/byteranges; boundary=" + MIME_SEPARATION);
2795
2796                if (content) {
2797                    try {
2798                        response.setBufferSize(m_output);
2799                    } catch (IllegalStateException e) {
2800                        // Silent catch
2801                    }
2802                    if (ostream != null) {
2803                        copy(item, ostream, ranges.iterator(), contentType);
2804                    } else {
2805                        copy(item, writer, ranges.iterator(), contentType);
2806                    }
2807                }
2808
2809            }
2810
2811        }
2812    }
2813
2814    /**
2815     * Handles the special WebDAV methods.<p>
2816     *
2817     * @param req the servlet request we are processing
2818     * @param resp the servlet response we are creating
2819     *
2820     * @throws IOException if an input/output error occurs
2821     * @throws ServletException if a servlet-specified error occurs
2822     */
2823    @Override
2824    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
2825
2826        String method = req.getMethod();
2827
2828        if (LOG.isDebugEnabled()) {
2829            String path = getRelativePath(req);
2830            LOG.debug("[" + method + "] " + path);
2831        }
2832
2833        // check authorization
2834        String auth = req.getHeader(HEADER_AUTHORIZATION);
2835        if ((auth == null) || !auth.toUpperCase().startsWith(AUTHORIZATION_BASIC_PREFIX)) {
2836
2837            // no authorization data is available
2838            requestAuthorization(resp);
2839
2840            if (LOG.isDebugEnabled()) {
2841                LOG.debug(Messages.get().getBundle().key(Messages.LOG_NO_AUTHORIZATION_0));
2842            }
2843
2844            return;
2845        }
2846
2847        // get encoded user and password, following after "BASIC "
2848        String base64Token = auth.substring(6);
2849
2850        // decode it, using base 64 decoder
2851        String token = new String(Base64.decodeBase64(base64Token.getBytes()));
2852        String password = null;
2853        int pos = token.indexOf(SEPARATOR_CREDENTIALS);
2854        if (pos != -1) {
2855            m_username = token.substring(0, pos);
2856            password = token.substring(pos + 1);
2857        }
2858
2859        // get session
2860        try {
2861            m_session = m_repository.login(m_username, password);
2862        } catch (CmsException ex) {
2863            m_session = null;
2864        }
2865
2866        if (m_session == null) {
2867
2868            if (LOG.isDebugEnabled()) {
2869                LOG.debug(Messages.get().getBundle().key(Messages.LOG_LOGIN_FAILED_1, m_username));
2870            }
2871
2872            resp.setStatus(HttpServletResponse.SC_FORBIDDEN);
2873            return;
2874        }
2875
2876        if (method.equals(METHOD_PROPFIND)) {
2877            doPropfind(req, resp);
2878        } else if (method.equals(METHOD_PROPPATCH)) {
2879            doProppatch(req, resp);
2880        } else if (method.equals(METHOD_MKCOL)) {
2881            doMkcol(req, resp);
2882        } else if (method.equals(METHOD_COPY)) {
2883            doCopy(req, resp);
2884        } else if (method.equals(METHOD_MOVE)) {
2885            doMove(req, resp);
2886        } else if (method.equals(METHOD_LOCK)) {
2887            doLock(req, resp);
2888        } else if (method.equals(METHOD_UNLOCK)) {
2889            doUnlock(req, resp);
2890        } else {
2891
2892            // DefaultServlet processing
2893            super.service(req, resp);
2894        }
2895
2896    }
2897
2898    /**
2899     * Generate a dom element from the given information with all needed subelements to
2900     * add to the parent.<p>
2901     *
2902     * @param lock the lock with the information to create the subelements
2903     * @param parent the parent element where to add the created element
2904     * @param lockToken the lock token to use
2905     */
2906    private void addLockElement(CmsRepositoryLockInfo lock, Element parent, String lockToken) {
2907
2908        Element activeLockElem = addElement(parent, TAG_ACTIVELOCK);
2909        addElement(addElement(activeLockElem, TAG_LOCKTYPE), lock.getType());
2910        addElement(addElement(activeLockElem, TAG_LOCKSCOPE), lock.getScope());
2911
2912        if (lock.getDepth() == CmsRepositoryLockInfo.DEPTH_INFINITY_VALUE) {
2913            addElement(activeLockElem, TAG_DEPTH).addText(DEPTH_INFINITY);
2914        } else {
2915            addElement(activeLockElem, TAG_DEPTH).addText("0");
2916        }
2917
2918        Element ownerElem = addElement(activeLockElem, TAG_OWNER);
2919        addElement(ownerElem, TAG_HREF).addText(lock.getOwner());
2920
2921        if (lock.getExpiresAt() == CmsRepositoryLockInfo.TIMEOUT_INFINITE_VALUE) {
2922            addElement(activeLockElem, TAG_TIMEOUT).addText(TIMEOUT_INFINITE);
2923        } else {
2924            long timeout = (lock.getExpiresAt() - System.currentTimeMillis()) / 1000;
2925            addElement(activeLockElem, TAG_TIMEOUT).addText("Second-" + timeout);
2926        }
2927
2928        Element lockTokenElem = addElement(activeLockElem, TAG_LOCKTOKEN);
2929        addElement(lockTokenElem, TAG_HREF).addText("opaquelocktoken:" + lockToken);
2930    }
2931
2932    /**
2933     * Checks if the items in the path or in a subpath are locked.<p>
2934     *
2935     * @param req the servlet request we are processing
2936     * @param path the path to check the items for locks
2937     * @param errorList the error list where to put the found errors
2938     */
2939    private void checkChildLocks(HttpServletRequest req, String path, Hashtable<String, Integer> errorList) {
2940
2941        List<I_CmsRepositoryItem> list = null;
2942        try {
2943            list = m_session.list(path);
2944        } catch (CmsException e) {
2945            if (LOG.isErrorEnabled()) {
2946                LOG.error(Messages.get().getBundle().key(Messages.LOG_LIST_ITEMS_ERROR_1, path), e);
2947            }
2948            errorList.put(path, new Integer(CmsWebdavStatus.SC_INTERNAL_SERVER_ERROR));
2949            return;
2950        }
2951
2952        Iterator<I_CmsRepositoryItem> iter = list.iterator();
2953        while (iter.hasNext()) {
2954            I_CmsRepositoryItem element = iter.next();
2955
2956            if (isLocked(element.getName())) {
2957                errorList.put(element.getName(), new Integer(CmsWebdavStatus.SC_LOCKED));
2958            } else {
2959                if (element.isCollection()) {
2960                    checkChildLocks(req, element.getName(), errorList);
2961                }
2962            }
2963        }
2964    }
2965
2966    /**
2967     * Determines the methods normally allowed for the resource.<p>
2968     *
2969     * @param path the path to the resource
2970     *
2971     * @return a StringBuffer with the WebDAV methods allowed for the resource at the given path
2972     */
2973    private StringBuffer determineMethodsAllowed(String path) {
2974
2975        StringBuffer methodsAllowed = new StringBuffer();
2976        boolean exists = true;
2977        I_CmsRepositoryItem item = null;
2978        try {
2979            item = m_session.getItem(path);
2980        } catch (CmsException e) {
2981            exists = false;
2982        }
2983
2984        if (!exists) {
2985
2986            methodsAllowed.append(METHOD_OPTIONS);
2987            methodsAllowed.append(", ").append(METHOD_PUT);
2988            methodsAllowed.append(", ").append(METHOD_MKCOL);
2989            methodsAllowed.append(", ").append(METHOD_LOCK);
2990            return methodsAllowed;
2991        }
2992
2993        // add standard http methods
2994        methodsAllowed.append(METHOD_OPTIONS);
2995        methodsAllowed.append(", ").append(METHOD_GET);
2996        methodsAllowed.append(", ").append(METHOD_HEAD);
2997        methodsAllowed.append(", ").append(METHOD_POST);
2998        methodsAllowed.append(", ").append(METHOD_DELETE);
2999        methodsAllowed.append(", ").append(METHOD_TRACE);
3000
3001        // add special WebDAV methods
3002        methodsAllowed.append(", ").append(METHOD_LOCK);
3003        methodsAllowed.append(", ").append(METHOD_UNLOCK);
3004        methodsAllowed.append(", ").append(METHOD_MOVE);
3005        methodsAllowed.append(", ").append(METHOD_COPY);
3006        methodsAllowed.append(", ").append(METHOD_PROPPATCH);
3007
3008        if (m_listings) {
3009            methodsAllowed.append(", ").append(METHOD_PROPFIND);
3010        }
3011
3012        if (item != null) {
3013            if (!item.isCollection()) {
3014                methodsAllowed.append(", ").append(METHOD_PUT);
3015            }
3016        }
3017        return methodsAllowed;
3018    }
3019
3020    /**
3021     * Print the lock discovery information associated with a path.<p>
3022     *
3023     * @param path the path to the resource
3024     * @param elem the dom element where to add the lock discovery elements
3025     * @param req the servlet request we are processing
3026     *
3027     * @return true if at least one lock was displayed
3028     */
3029    private boolean generateLockDiscovery(String path, Element elem, HttpServletRequest req) {
3030
3031        CmsRepositoryLockInfo lock = m_session.getLock(path);
3032
3033        if (lock != null) {
3034
3035            Element lockElem = addElement(elem, TAG_LOCKDISCOVERY);
3036            addLockElement(lock, lockElem, generateLockToken(req, lock));
3037
3038            return true;
3039        }
3040
3041        return false;
3042    }
3043
3044    /**
3045     * Generates a lock token out of the lock and some information out of the
3046     * request to make it unique.<p>
3047     *
3048     * @param req the servlet request we are processing
3049     * @param lock the lock with the information for the lock token
3050     *
3051     * @return the generated lock token
3052     */
3053    private String generateLockToken(HttpServletRequest req, CmsRepositoryLockInfo lock) {
3054
3055        String lockTokenStr = req.getServletPath()
3056            + "-"
3057            + req.getUserPrincipal()
3058            + "-"
3059            + lock.getOwner()
3060            + "-"
3061            + lock.getPath()
3062            + "-"
3063            + m_secret;
3064
3065        return MD5_ENCODER.encode(m_md5Helper.digest(lockTokenStr.getBytes()));
3066    }
3067
3068    /**
3069     * Return the relative path associated with this servlet.<p>
3070     *
3071     * @param request the servlet request we are processing
3072     *
3073     * @return the relative path of the resource
3074     */
3075    private String getRelativePath(HttpServletRequest request) {
3076
3077        String result = request.getPathInfo();
3078        if (result == null) {
3079            //result = request.getServletPath();
3080        }
3081        if ((result == null) || (result.equals(""))) {
3082            result = "/";
3083        }
3084        return (result);
3085    }
3086
3087    /**
3088     * Check to see if a resource is currently write locked.<p>
3089     *
3090     * @param req the servlet request we are processing
3091     *
3092     * @return true if the resource is locked otherwise false
3093     */
3094    private boolean isLocked(HttpServletRequest req) {
3095
3096        return isLocked(getRelativePath(req));
3097    }
3098
3099    /**
3100     * Check to see if a resource is currently write locked.<p>
3101     *
3102     * @param path the path where to find the resource to check the lock
3103     *
3104     * @return true if the resource is locked otherwise false
3105     */
3106    private boolean isLocked(String path) {
3107
3108        // get lock for path
3109        CmsRepositoryLockInfo lock = m_session.getLock(path);
3110        if (lock == null) {
3111            return false;
3112        }
3113
3114        // check if found lock fits to the lock token from request
3115        //        String currentToken = "<opaquelocktoken:" + generateLockToken(req, lock) + ">";
3116        //        if (currentToken.equals(parseLockTokenHeader(req))) {
3117        //            return false;
3118        //        }
3119
3120        if (lock.getUsername().equals(m_username)) {
3121            return false;
3122        }
3123
3124        return true;
3125    }
3126
3127    /**
3128     * Return a context-relative path, beginning with a "/".<p>
3129     *
3130     * That represents the canonical version of the specified path after ".."
3131     * and "." elements are resolved out. If the specified path attempts to go
3132     * outside the boundaries of the current context (i.e. too many ".." path
3133     * elements are present), return <code>null</code> instead.<p>
3134     *
3135     * @param path the path to be normalized
3136     *
3137     * @return the normalized path
3138     */
3139    private String normalize(String path) {
3140
3141        if (path == null) {
3142            return null;
3143        }
3144
3145        // Create a place for the normalized path
3146        String normalized = path;
3147
3148        if (normalized.equals("/.")) {
3149            return "/";
3150        }
3151
3152        // Normalize the slashes and add leading slash if necessary
3153        if (normalized.indexOf('\\') >= 0) {
3154            normalized = normalized.replace('\\', '/');
3155        }
3156
3157        if (!normalized.startsWith("/")) {
3158            normalized = "/" + normalized;
3159        }
3160
3161        // Resolve occurrences of "//" in the normalized path
3162        while (true) {
3163            int index = normalized.indexOf("//");
3164            if (index < 0) {
3165                break;
3166            }
3167            normalized = normalized.substring(0, index) + normalized.substring(index + 1);
3168        }
3169
3170        // Resolve occurrences of "/./" in the normalized path
3171        while (true) {
3172            int index = normalized.indexOf("/./");
3173            if (index < 0) {
3174                break;
3175            }
3176            normalized = normalized.substring(0, index) + normalized.substring(index + 2);
3177        }
3178
3179        // Resolve occurrences of "/../" in the normalized path
3180        while (true) {
3181            int index = normalized.indexOf("/../");
3182            if (index < 0) {
3183                break;
3184            }
3185            if (index == 0) {
3186                return (null); // Trying to go outside our context
3187            }
3188
3189            int index2 = normalized.lastIndexOf('/', index - 1);
3190            normalized = normalized.substring(0, index2) + normalized.substring(index + 3);
3191        }
3192
3193        // Return the normalized path that we have completed
3194        return (normalized);
3195    }
3196
3197    /**
3198     * Parse the content-range header.<p>
3199     *
3200     * @param request the servlet request we are processing
3201     * @param response the servlet response we are creating
3202     *
3203     * @return the range of the content read from the header
3204     */
3205    private CmsWebdavRange parseContentRange(HttpServletRequest request, HttpServletResponse response) {
3206
3207        // Retrieving the content-range header (if any is specified
3208        String rangeHeader = request.getHeader(HEADER_CONTENTRANGE);
3209
3210        if (rangeHeader == null) {
3211            return null;
3212        }
3213
3214        // bytes is the only range unit supported
3215        if (!rangeHeader.startsWith("bytes")) {
3216            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
3217            return null;
3218        }
3219
3220        rangeHeader = rangeHeader.substring(6).trim();
3221
3222        int dashPos = rangeHeader.indexOf('-');
3223        int slashPos = rangeHeader.indexOf('/');
3224
3225        if (dashPos == -1) {
3226            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
3227            return null;
3228        }
3229
3230        if (slashPos == -1) {
3231            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
3232            return null;
3233        }
3234
3235        CmsWebdavRange range = new CmsWebdavRange();
3236
3237        try {
3238            range.setStart(Long.parseLong(rangeHeader.substring(0, dashPos)));
3239            range.setEnd(Long.parseLong(rangeHeader.substring(dashPos + 1, slashPos)));
3240            range.setLength(Long.parseLong(rangeHeader.substring(slashPos + 1, rangeHeader.length())));
3241        } catch (NumberFormatException e) {
3242            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
3243            return null;
3244        }
3245
3246        if (!range.validate()) {
3247            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
3248            return null;
3249        }
3250
3251        return range;
3252    }
3253
3254    /**
3255     * Reads the information about a destination path out of the header of the
3256     * request.<p>
3257     *
3258     * @param req the servlet request we are processing
3259     *
3260     * @return the destination path
3261     */
3262    private String parseDestinationHeader(HttpServletRequest req) {
3263
3264        // Parsing destination header
3265        String destinationPath = req.getHeader(HEADER_DESTINATION);
3266
3267        if (destinationPath == null) {
3268            return null;
3269        }
3270
3271        // Remove url encoding from destination
3272        destinationPath = CmsEncoder.decode(destinationPath, "UTF8");
3273
3274        int protocolIndex = destinationPath.indexOf("://");
3275        if (protocolIndex >= 0) {
3276
3277            // if the Destination URL contains the protocol, we can safely
3278            // trim everything upto the first "/" character after "://"
3279            int firstSeparator = destinationPath.indexOf("/", protocolIndex + 4);
3280            if (firstSeparator < 0) {
3281                destinationPath = "/";
3282            } else {
3283                destinationPath = destinationPath.substring(firstSeparator);
3284            }
3285        } else {
3286            String hostName = req.getServerName();
3287            if ((hostName != null) && (destinationPath.startsWith(hostName))) {
3288                destinationPath = destinationPath.substring(hostName.length());
3289            }
3290
3291            int portIndex = destinationPath.indexOf(":");
3292            if (portIndex >= 0) {
3293                destinationPath = destinationPath.substring(portIndex);
3294            }
3295
3296            if (destinationPath.startsWith(":")) {
3297                int firstSeparator = destinationPath.indexOf("/");
3298                if (firstSeparator < 0) {
3299                    destinationPath = "/";
3300                } else {
3301                    destinationPath = destinationPath.substring(firstSeparator);
3302                }
3303            }
3304        }
3305
3306        // Normalise destination path (remove '.' and '..')
3307        destinationPath = normalize(destinationPath);
3308
3309        String contextPath = req.getContextPath();
3310        if ((contextPath != null) && (destinationPath.startsWith(contextPath))) {
3311            destinationPath = destinationPath.substring(contextPath.length());
3312        }
3313
3314        String pathInfo = req.getPathInfo();
3315        if (pathInfo != null) {
3316            String servletPath = req.getServletPath();
3317            if ((servletPath != null) && (destinationPath.startsWith(servletPath))) {
3318                destinationPath = destinationPath.substring(servletPath.length());
3319            }
3320        }
3321
3322        return destinationPath;
3323    }
3324
3325    /**
3326     * Reads the information about overwriting out of the header of the
3327     * request.<p>
3328     *
3329     * @param req the servlet request we are processing
3330     *
3331     * @return true if overwrite was set in the header otherwise false
3332     */
3333    private boolean parseOverwriteHeader(HttpServletRequest req) {
3334
3335        boolean overwrite = true;
3336        String overwriteHeader = req.getHeader(HEADER_OVERWRITE);
3337
3338        if (overwriteHeader != null) {
3339            if (overwriteHeader.equalsIgnoreCase("T")) {
3340                overwrite = true;
3341            } else {
3342                overwrite = false;
3343            }
3344        }
3345
3346        return overwrite;
3347    }
3348
3349    /**
3350     * Propfind helper method.<p>
3351     *
3352     * @param req the servlet request
3353     * @param elem the parent element where to add the generated subelements
3354     * @param item the current item where to parse the properties
3355     * @param type the propfind type
3356     * @param propertiesVector if the propfind type is find properties by
3357     *          name, then this Vector contains those properties
3358     */
3359    private void parseProperties(
3360        HttpServletRequest req,
3361        Element elem,
3362        I_CmsRepositoryItem item,
3363        int type,
3364        List<String> propertiesVector) {
3365
3366        String path = item.getName();
3367        Element responseElem = addElement(elem, TAG_RESPONSE);
3368
3369        String status = "HTTP/1.1 "
3370            + CmsWebdavStatus.SC_OK
3371            + " "
3372            + CmsWebdavStatus.getStatusText(CmsWebdavStatus.SC_OK);
3373
3374        // Generating href element
3375        Element hrefElem = addElement(responseElem, TAG_HREF);
3376
3377        String href = req.getContextPath() + req.getServletPath();
3378        if ((href.endsWith("/")) && (path.startsWith("/"))) {
3379            href += path.substring(1);
3380        } else {
3381            href += path;
3382        }
3383
3384        try {
3385            hrefElem.addText(rewriteUrl(href));
3386        } catch (UnsupportedEncodingException ex) {
3387            return;
3388        }
3389
3390        String resourceName = path;
3391
3392        Element propstatElem = addElement(responseElem, TAG_PROPSTAT);
3393        Element propElem = addElement(propstatElem, TAG_PROP);
3394
3395        switch (type) {
3396
3397            case FIND_ALL_PROP:
3398
3399                addElement(propElem, TAG_CREATIONDATE).addText(ISO8601_FORMAT.format(new Date(item.getCreationDate())));
3400                addElement(propElem, TAG_DISPLAYNAME).addCDATA(resourceName);
3401
3402                // properties only for files (no collections)
3403                if (!item.isCollection()) {
3404
3405                    addElement(propElem, TAG_LASTMODIFIED).addText(
3406                        HTTP_DATE_FORMAT.format(new Date(item.getLastModifiedDate())));
3407
3408                    addElement(propElem, TAG_CONTENTLENGTH).addText(String.valueOf(item.getContentLength()));
3409
3410                    String contentType = getServletContext().getMimeType(item.getName());
3411                    if (contentType != null) {
3412                        addElement(propElem, TAG_CONTENTTYPE).addText(contentType);
3413                    }
3414                    addElement(propElem, TAG_ETAG).addText(getETag(item));
3415                    addElement(propElem, TAG_RESOURCETYPE);
3416                } else {
3417                    addElement(addElement(propElem, TAG_RESOURCETYPE), TAG_COLLECTION);
3418                }
3419
3420                addElement(propElem, TAG_SOURCE).addText("");
3421
3422                Element suppLockElem = addElement(propElem, TAG_SUPPORTEDLOCK);
3423                Element lockEntryElem = addElement(suppLockElem, TAG_LOCKENTRY);
3424                addElement(addElement(lockEntryElem, TAG_LOCKSCOPE), CmsRepositoryLockInfo.SCOPE_EXCLUSIVE);
3425                addElement(addElement(lockEntryElem, TAG_LOCKTYPE), CmsRepositoryLockInfo.TYPE_WRITE);
3426                lockEntryElem = addElement(suppLockElem, TAG_LOCKENTRY);
3427                addElement(addElement(lockEntryElem, TAG_LOCKSCOPE), CmsRepositoryLockInfo.SCOPE_SHARED);
3428                addElement(addElement(lockEntryElem, TAG_LOCKTYPE), CmsRepositoryLockInfo.TYPE_WRITE);
3429
3430                generateLockDiscovery(path, propElem, req);
3431
3432                addElement(propstatElem, TAG_STATUS).addText(status);
3433
3434                break;
3435
3436            case FIND_PROPERTY_NAMES:
3437
3438                addElement(propElem, TAG_CREATIONDATE);
3439                addElement(propElem, TAG_DISPLAYNAME);
3440                if (!item.isCollection()) {
3441
3442                    addElement(propElem, TAG_CONTENTLANGUAGE);
3443                    addElement(propElem, TAG_CONTENTLENGTH);
3444                    addElement(propElem, TAG_CONTENTTYPE);
3445                    addElement(propElem, TAG_ETAG);
3446                }
3447                addElement(propElem, TAG_LASTMODIFIED);
3448                addElement(propElem, TAG_RESOURCETYPE);
3449                addElement(propElem, TAG_SOURCE);
3450                addElement(propElem, TAG_LOCKDISCOVERY);
3451
3452                addElement(propstatElem, TAG_STATUS).addText(status);
3453
3454                break;
3455
3456            case FIND_BY_PROPERTY:
3457
3458                List<String> propertiesNotFound = new Vector<String>();
3459
3460                // Parse the list of properties
3461                Iterator<String> iter = propertiesVector.iterator();
3462                while (iter.hasNext()) {
3463                    String property = iter.next();
3464
3465                    if (property.equals(TAG_CREATIONDATE)) {
3466                        addElement(propElem, TAG_CREATIONDATE).addText(
3467                            ISO8601_FORMAT.format(new Date(item.getCreationDate())));
3468                    } else if (property.equals(TAG_DISPLAYNAME)) {
3469                        addElement(propElem, TAG_DISPLAYNAME).addCDATA(resourceName);
3470                    } else if (property.equals(TAG_CONTENTLANGUAGE)) {
3471                        if (item.isCollection()) {
3472                            propertiesNotFound.add(property);
3473                        } else {
3474                            addElement(propElem, TAG_CONTENTLANGUAGE);
3475                        }
3476                    } else if (property.equals(TAG_CONTENTLENGTH)) {
3477                        if (item.isCollection()) {
3478                            propertiesNotFound.add(property);
3479                        } else {
3480                            addElement(propElem, TAG_CONTENTLENGTH).addText((String.valueOf(item.getContentLength())));
3481                        }
3482                    } else if (property.equals(TAG_CONTENTTYPE)) {
3483                        if (item.isCollection()) {
3484                            propertiesNotFound.add(property);
3485                        } else {
3486                            String contentType = item.getMimeType();
3487                            if (contentType == null) {
3488                                contentType = getServletContext().getMimeType(item.getName());
3489                            }
3490
3491                            if (contentType != null) {
3492                                addElement(propElem, TAG_CONTENTTYPE).addText(contentType);
3493                            }
3494                        }
3495                    } else if (property.equals(TAG_ETAG)) {
3496                        if (item.isCollection()) {
3497                            propertiesNotFound.add(property);
3498                        } else {
3499                            addElement(propElem, TAG_ETAG).addText(getETag(item));
3500                        }
3501                    } else if (property.equals(TAG_LASTMODIFIED)) {
3502                        addElement(propElem, TAG_LASTMODIFIED).addText(
3503                            HTTP_DATE_FORMAT.format(new Date(item.getLastModifiedDate())));
3504                    } else if (property.equals(TAG_RESOURCETYPE)) {
3505                        if (item.isCollection()) {
3506                            addElement(addElement(propElem, TAG_RESOURCETYPE), TAG_COLLECTION);
3507                        } else {
3508                            addElement(propElem, TAG_RESOURCETYPE);
3509                        }
3510                    } else if (property.equals(TAG_SOURCE)) {
3511                        addElement(propElem, TAG_SOURCE).addText("");
3512                    } else if (property.equals(TAG_SUPPORTEDLOCK)) {
3513                        suppLockElem = addElement(propElem, TAG_SUPPORTEDLOCK);
3514                        lockEntryElem = addElement(suppLockElem, TAG_LOCKENTRY);
3515                        addElement(addElement(lockEntryElem, TAG_LOCKSCOPE), CmsRepositoryLockInfo.SCOPE_EXCLUSIVE);
3516                        addElement(addElement(lockEntryElem, TAG_LOCKTYPE), CmsRepositoryLockInfo.TYPE_WRITE);
3517                        lockEntryElem = addElement(suppLockElem, TAG_LOCKENTRY);
3518                        addElement(addElement(lockEntryElem, TAG_LOCKSCOPE), CmsRepositoryLockInfo.SCOPE_SHARED);
3519                        addElement(addElement(lockEntryElem, TAG_LOCKTYPE), CmsRepositoryLockInfo.TYPE_WRITE);
3520                    } else if (property.equals(TAG_LOCKDISCOVERY)) {
3521                        if (!generateLockDiscovery(path, propElem, req)) {
3522                            addElement(propElem, TAG_LOCKDISCOVERY);
3523                        }
3524                    } else {
3525                        propertiesNotFound.add(property);
3526                    }
3527                }
3528
3529                addElement(propstatElem, TAG_STATUS).addText(status);
3530
3531                if (propertiesNotFound.size() > 0) {
3532                    status = "HTTP/1.1 "
3533                        + CmsWebdavStatus.SC_NOT_FOUND
3534                        + " "
3535                        + CmsWebdavStatus.getStatusText(CmsWebdavStatus.SC_NOT_FOUND);
3536
3537                    propstatElem = addElement(responseElem, TAG_PROPSTAT);
3538                    propElem = addElement(propstatElem, TAG_PROP);
3539
3540                    Iterator<String> notFoundIter = propertiesNotFound.iterator();
3541                    while (notFoundIter.hasNext()) {
3542                        addElement(propElem, notFoundIter.next());
3543                    }
3544
3545                    addElement(propstatElem, TAG_STATUS).addText(status);
3546                }
3547
3548                break;
3549
3550            default:
3551
3552                if (LOG.isErrorEnabled()) {
3553                    LOG.error(Messages.get().getBundle().key(Messages.LOG_INVALID_PROPFIND_TYPE_0));
3554                }
3555                break;
3556        }
3557    }
3558
3559    /**
3560     * Sends a response back to authenticate the user.<p>
3561     *
3562     * @param resp the servlet response we are processing
3563     *
3564     * @throws IOException if errors while writing to response occurs
3565     */
3566    private void requestAuthorization(HttpServletResponse resp) throws IOException {
3567
3568        // Authorisation is required for the requested action.
3569        resp.setHeader(CmsRequestUtil.HEADER_WWW_AUTHENTICATE, "Basic realm=\"" + BASIC_REALM + "\"");
3570
3571        resp.sendError(HttpServletResponse.SC_UNAUTHORIZED);
3572    }
3573
3574    /**
3575     * Send a multistatus element containing a complete error report to the
3576     * client.<p>
3577     *
3578     * @param req the servlet request we are processing
3579     * @param resp the servlet response we are processing
3580     * @param errors the errors to be displayed
3581     *
3582     * @throws IOException if errors while writing to response occurs
3583     */
3584    private void sendReport(HttpServletRequest req, HttpServletResponse resp, Map<String, Integer> errors)
3585    throws IOException {
3586
3587        resp.setStatus(CmsWebdavStatus.SC_MULTI_STATUS);
3588
3589        String absoluteUri = req.getRequestURI();
3590        String relativePath = getRelativePath(req);
3591
3592        Document doc = DocumentHelper.createDocument();
3593        Element multiStatusElem = doc.addElement(new QName(TAG_MULTISTATUS, Namespace.get(DEFAULT_NAMESPACE)));
3594
3595        Iterator<Entry<String, Integer>> it = errors.entrySet().iterator();
3596        while (it.hasNext()) {
3597            Entry<String, Integer> e = it.next();
3598            String errorPath = e.getKey();
3599            int errorCode = e.getValue().intValue();
3600
3601            Element responseElem = addElement(multiStatusElem, TAG_RESPONSE);
3602
3603            String toAppend = errorPath.substring(relativePath.length());
3604            if (!toAppend.startsWith("/")) {
3605                toAppend = "/" + toAppend;
3606            }
3607            addElement(responseElem, TAG_HREF).addText(absoluteUri + toAppend);
3608            addElement(responseElem, TAG_STATUS).addText(
3609                "HTTP/1.1 " + errorCode + " " + CmsWebdavStatus.getStatusText(errorCode));
3610        }
3611
3612        Writer writer = resp.getWriter();
3613        doc.write(writer);
3614        writer.close();
3615    }
3616}