[Dextrose] [sugar PATCH v4] uy#1769: 1-to-N Feature.

Ajay Garg ajay at activitycentral.com
Wed May 2 01:43:01 EDT 2012


The feature specs, and workflow-screenshots, are listed at ::
http://wiki.sugarlabs.org/go/Features/Transfer_to_many_options



Changes of version-4 over version-3 ::
--------------------------------------

NOTE ::

The WebDAV backend-library being used is the one from 
https://launchpad.net/python-webdav-lib/trunk/0.3.0/+download/Python_WebDAV_Library-0.3.0.zip

The reason for using this, is purely based on not reinventing-the-wheel.
Moreover, it served the following three major purposes ::

   (i)   Open-Source, in python.
    
   (ii)  It provided the API for accessing URLs of type ::

         * dav://1.2.3.4/webdav/
         * dav://1.2.3.4/webdav/dir_1
         * dav://1.2.3.4/webdav/dir_1/dir_2
     
   (iii) It provided the API for downloading a resource (file) of type ::

         * dav://1.2.3.4/webdav/1.txt
         * dav://1.2.3.4/webdav/dir_1/2.png
         * dav://1.2.3.4/webdav/dir_1/dir_2/1743.avi


Thanks to Sascha, for the motivation of making it absolutely clear and explicit that 
the reason for using this 3rd-party library be mentioned (since I myself had to struggle 
quite a bit for finding the suitable one :) )




===========================================================================




Changes of version-3 over version-2 ::
--------------------------------------

a)
Added new icon for "Shares" folder.

For this to take effect, patch at http://patchwork.sugarlabs.org/patch/1419/
must be applied.

Thanks to Frederick Grose for the image, and Anish for validating it.




===========================================================================




Changes of version-2 over version-1 ::
--------------------------------------

a)
Fixed the bug, wherein it was required that activities should NOT be launched,
by clicking on the icons in "locally-mounted-remote-shares" listview.

Thanks Anish for providing the solution to this :)



b)
Fixed the bug (Thanks Anish for catching this) ::

* Host adds files to share folder
* Client adds the host's share in journal view, journal entries are
  visible
* Host disconnects from the network
* Client tries to copy/Client tries to access remote share folder
* Exception in logs
* UI Freeze.


The solution is to "catch" the Connection-Failed exception, and display the error-message,
on similar lines as "Entries without a file cannot be copied."



c)
Did "Code Extraction" from "src/webdav/WebdavClient.py" to "src/jarabe/journal/webdavmanager.py".
Now, all files in "src/webdav" and "src/webdav/acp" directories contain code present at 
https://launchpad.net/python-webdav-lib/trunk/0.3.0/+download/Python_WebDAV_Library-0.3.0.zip

Thanks Anish, for providing the motivation.




 configure.ac                          |    4 +-
 src/Makefile.am                       |    4 +-
 src/jarabe/journal/Makefile.am        |    5 +-
 src/jarabe/journal/journalactivity.py |    3 +
 src/jarabe/journal/journaltoolbox.py  |    7 +
 src/jarabe/journal/listview.py        |    9 +
 src/jarabe/journal/model.py           |  205 ++++++++-
 src/jarabe/journal/palettes.py        |   78 +++
 src/jarabe/journal/volumestoolbar.py  |   94 ++++-
 src/jarabe/journal/webdavmanager.py   |  225 +++++++++
 src/jarabe/model/buddy.py             |   11 +
 src/jarabe/model/neighborhood.py      |    3 +
 src/jarabe/view/buddymenu.py          |   11 +
 src/jarabe/view/palettes.py           |   33 ++
 src/webdav/Condition.py               |  475 +++++++++++++++++++
 src/webdav/Connection.py              |  321 +++++++++++++
 src/webdav/Constants.py               |  199 ++++++++
 src/webdav/Makefile.am                |   20 +
 src/webdav/NameCheck.py               |  193 ++++++++
 src/webdav/Utils.py                   |  154 ++++++
 src/webdav/VersionHandler.py          |  198 ++++++++
 src/webdav/WebdavClient.py            |  833 +++++++++++++++++++++++++++++++++
 src/webdav/WebdavRequests.py          |  205 ++++++++
 src/webdav/WebdavResponse.py          |  525 +++++++++++++++++++++
 src/webdav/__init__.py                |   16 +
 src/webdav/acp/Ace.py                 |  293 ++++++++++++
 src/webdav/acp/AceHandler.py          |  182 +++++++
 src/webdav/acp/Acl.py                 |  311 ++++++++++++
 src/webdav/acp/GrantDeny.py           |  241 ++++++++++
 src/webdav/acp/Makefile.am            |   12 +
 src/webdav/acp/Principal.py           |  189 ++++++++
 src/webdav/acp/Privilege.py           |  125 +++++
 src/webdav/acp/__init__.py            |   33 ++
 src/webdav/davlib.py                  |  336 +++++++++++++
 src/webdav/logger.py                  |   51 ++
 src/webdav/qp_xml.py                  |  240 ++++++++++
 src/webdav/uuid_.py                   |  476 +++++++++++++++++++
 37 files changed, 6301 insertions(+), 19 deletions(-)
 create mode 100644 src/jarabe/journal/webdavmanager.py
 create mode 100644 src/webdav/Condition.py
 create mode 100644 src/webdav/Connection.py
 create mode 100644 src/webdav/Constants.py
 create mode 100644 src/webdav/Makefile.am
 create mode 100644 src/webdav/NameCheck.py
 create mode 100644 src/webdav/Utils.py
 create mode 100644 src/webdav/VersionHandler.py
 create mode 100644 src/webdav/WebdavClient.py
 create mode 100644 src/webdav/WebdavRequests.py
 create mode 100644 src/webdav/WebdavResponse.py
 create mode 100644 src/webdav/__init__.py
 create mode 100644 src/webdav/acp/Ace.py
 create mode 100644 src/webdav/acp/AceHandler.py
 create mode 100644 src/webdav/acp/Acl.py
 create mode 100644 src/webdav/acp/GrantDeny.py
 create mode 100644 src/webdav/acp/Makefile.am
 create mode 100644 src/webdav/acp/Principal.py
 create mode 100644 src/webdav/acp/Privilege.py
 create mode 100644 src/webdav/acp/__init__.py
 create mode 100644 src/webdav/davlib.py
 create mode 100644 src/webdav/logger.py
 create mode 100644 src/webdav/qp_xml.py
 create mode 100644 src/webdav/uuid_.py

diff --git a/configure.ac b/configure.ac
index 8e6d871..e04bdec 100644
--- a/configure.ac
+++ b/configure.ac
@@ -61,8 +61,6 @@ extensions/cpsection/modemconfiguration/config.py
 extensions/cpsection/Makefile
 extensions/cpsection/network/Makefile
 extensions/cpsection/power/Makefile
-extensions/cpsection/updater/backends/Makefile
-extensions/cpsection/updater/Makefile
 extensions/deviceicon/Makefile
 extensions/globalkey/Makefile
 extensions/Makefile
@@ -79,6 +77,8 @@ src/jarabe/model/Makefile
 src/jarabe/util/Makefile
 src/jarabe/util/telepathy/Makefile
 src/jarabe/view/Makefile
+src/webdav/acp/Makefile
+src/webdav/Makefile
 src/Makefile
 ])
 
diff --git a/src/Makefile.am b/src/Makefile.am
index 83571a4..765da8b 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -1 +1,3 @@
-SUBDIRS = jarabe
+SUBDIRS =        \
+        jarabe   \
+	webdav
diff --git a/src/jarabe/journal/Makefile.am b/src/jarabe/journal/Makefile.am
index f24dcfe..8efca6d 100644
--- a/src/jarabe/journal/Makefile.am
+++ b/src/jarabe/journal/Makefile.am
@@ -15,5 +15,6 @@ sugar_PYTHON =				\
 	model.py			\
 	objectchooser.py		\
 	palettes.py			\
-	volumestoolbar.py			\
-	processdialog.py
+	volumestoolbar.py		\
+	processdialog.py                \
+	webdavmanager.py
diff --git a/src/jarabe/journal/journalactivity.py b/src/jarabe/journal/journalactivity.py
index fa308cd..1b841e9 100644
--- a/src/jarabe/journal/journalactivity.py
+++ b/src/jarabe/journal/journalactivity.py
@@ -485,6 +485,9 @@ class JournalActivity(JournalWindow):
     def is_editing_mode_present(self):
         return self._editing_mode
 
+    def get_volumes_toolbar(self):
+        return self._volumes_toolbar
+
 
 def get_journal():
     global _journal
diff --git a/src/jarabe/journal/journaltoolbox.py b/src/jarabe/journal/journaltoolbox.py
index fd14826..c3614d7 100644
--- a/src/jarabe/journal/journaltoolbox.py
+++ b/src/jarabe/journal/journaltoolbox.py
@@ -624,6 +624,13 @@ class BatchEraseButton(ToolButton, palettes.ActionItem):
                                      show_not_completed_ops_info=True)
         self.props.tooltip = _('Erase')
 
+        # De-sensitize Batch-Erase button, for locally-mounted-remote-shares.
+        from jarabe.journal.journalactivity import get_mount_point
+        current_mount_point = get_mount_point()
+
+        if model.is_mount_point_for_locally_mounted_remote_share(current_mount_point):
+            self.set_sensitive(False)
+
     def _get_actionable_signal(self):
         return 'clicked'
 
diff --git a/src/jarabe/journal/listview.py b/src/jarabe/journal/listview.py
index 8522dca..10f468f 100644
--- a/src/jarabe/journal/listview.py
+++ b/src/jarabe/journal/listview.py
@@ -634,6 +634,15 @@ class ListView(BaseListView):
         self.emit('volume-error', message, severity)
 
     def __icon_clicked_cb(self, cell, path):
+        # For locally-mounted remote shares, we do not want to launch
+        # by clicking on the icons.
+        # So, check if this is a part of locally-mounted-remote share,
+        # and if yes, return, without doing anything.
+        from jarabe.journal.journalactivity import get_mount_point
+        current_mount_point = get_mount_point()
+        if model.is_mount_point_for_locally_mounted_remote_share(current_mount_point):
+            return
+
         row = self.tree_view.get_model()[path]
         metadata = model.get(row[ListModel.COLUMN_UID])
         misc.resume(metadata)
diff --git a/src/jarabe/journal/model.py b/src/jarabe/journal/model.py
index 83e216f..527f78d 100644
--- a/src/jarabe/journal/model.py
+++ b/src/jarabe/journal/model.py
@@ -20,6 +20,7 @@
 
 import logging
 import os
+import stat
 import errno
 import subprocess
 from datetime import datetime
@@ -36,11 +37,14 @@ import gobject
 import dbus
 import gio
 import gconf
+import string
 
 from sugar import dispatch
 from sugar import mime
 from sugar import util
 
+from jarabe.journal.webdavmanager import get_remote_webdav_share_metadata
+
 DS_DBUS_SERVICE = 'org.laptop.sugar.DataStore'
 DS_DBUS_INTERFACE = 'org.laptop.sugar.DataStore'
 DS_DBUS_PATH = '/org/laptop/sugar/DataStore'
@@ -425,6 +429,123 @@ class InplaceResultSet(BaseResultSet):
         return
 
 
+class RemoteShareResultSet(object):
+    def __init__(self, ip_address, query):
+        self._ip_address = ip_address
+        self._file_list = []
+
+        self.ready = dispatch.Signal()
+        self.progress = dispatch.Signal()
+
+        # First time, query is none.
+        if query is None:
+            return
+
+        query_text = query.get('query', '')
+        if query_text.startswith('"') and query_text.endswith('"'):
+            self._regex = re.compile('*%s*' % query_text.strip(['"']))
+        elif query_text:
+            expression = ''
+            for word in query_text.split(' '):
+                expression += '(?=.*%s.*)' % word
+            self._regex = re.compile(expression, re.IGNORECASE)
+        else:
+            self._regex = None
+
+        if query.get('timestamp', ''):
+            self._date_start = int(query['timestamp']['start'])
+            self._date_end = int(query['timestamp']['end'])
+        else:
+            self._date_start = None
+            self._date_end = None
+
+        self._mime_types = query.get('mime_type', [])
+
+        self._sort = query.get('order_by', ['+timestamp'])[0]
+
+    def setup(self):
+        metadata_list_complete = get_remote_webdav_share_metadata(self._ip_address)
+        for metadata in metadata_list_complete:
+
+            add_to_list = False
+            if self._regex is not None:
+                for f in ['fulltext', 'title',
+                          'description', 'tags']:
+                    if f in metadata and \
+                            self._regex.match(metadata[f]):
+                        add_to_list = True
+                        break
+            else:
+                add_to_list = True
+            if not add_to_list:
+                continue
+
+            add_to_list = False
+            if self._date_start is not None:
+                if metadata['timestamp'] > self._date_start:
+                    add_to_list = True
+            else:
+                add_to_list = True
+            if not add_to_list:
+                continue
+
+            add_to_list = False
+            if self._date_end is not None:
+                if metadata['timestamp'] < self._date_end:
+                    add_to_list = True
+            else:
+                add_to_list = True
+            if not add_to_list:
+                continue
+
+            add_to_list = False
+            if self._mime_types:
+                mime_type = metadata['mime_type']
+                if mime_type in self._mime_types:
+                    add_to_list = True
+            else:
+                add_to_list = True
+            if not add_to_list:
+                continue
+
+            # If control reaches here, the current metadata has passed
+            # out all filter-tests.
+            file_info = (metadata['timestamp'],
+                         metadata['creation_time'],
+                         metadata['filesize'],
+                         metadata)
+            self._file_list.append(file_info)
+
+        if self._sort[1:] == 'filesize':
+            keygetter = itemgetter(2)
+        elif self._sort[1:] == 'creation_time':
+            keygetter = itemgetter(1)
+        else:
+            # timestamp
+            keygetter = itemgetter(0)
+
+        self._file_list.sort(lambda a, b: cmp(b, a),
+                             key=keygetter,
+                             reverse=(self._sort[0] == '-'))
+
+        self.ready.send(self)
+
+    def get_length(self):
+        return len(self._file_list)
+
+    length = property(get_length)
+
+    def seek(self, position):
+        self._position = position
+
+    def read(self):
+        modified_timestamp, creation_timestamp, filesize, metadata =  self._file_list[self._position]
+        return  metadata
+
+    def stop(self):
+        self._stopped = True
+
+
 def _get_file_metadata(path, stat, fetch_preview=True):
     """Return the metadata from the corresponding file.
 
@@ -436,10 +557,16 @@ def _get_file_metadata(path, stat, fetch_preview=True):
     dir_path = os.path.dirname(path)
     metadata = _get_file_metadata_from_json(dir_path, filename, fetch_preview)
     if metadata:
+        # For Documents/Shares/Mounted-Drives.
+        # Special case: for locally-mounted-remote-files, ensure that
+        # "metadata['filesize' is already present before-hand. This
+        # will have to be done at the time of fetching
+        # webdav-properties per resource.
         if 'filesize' not in metadata:
             metadata['filesize'] = stat.st_size
         return metadata
 
+    # For Journal.
     return {'uid': path,
             'title': os.path.basename(path),
             'timestamp': stat.st_mtime,
@@ -529,11 +656,33 @@ def find(query_, page_size):
         raise ValueError('Exactly one mount point must be specified')
 
     if mount_points[0] == '/':
+        """
+        For Journal.
+        """
         return DatastoreResultSet(query, page_size)
+    elif is_mount_point_for_locally_mounted_remote_share(mount_points[0]):
+        """
+        For Locally-Mounted-Remote-Shares.
+        Regex Matching is used, to ensure that the mount-point is an
+        IP-Address.
+        """
+        return RemoteShareResultSet(mount_points[0], query)
     else:
+        """
+        For Documents/Shares/Mounted-Drives.
+        """
         return InplaceResultSet(query, page_size, mount_points[0])
 
 
+def is_mount_point_for_locally_mounted_remote_share(mount_point):
+    import re
+
+    pattern = '[1-9][0-9]{0,2}\.[0-9]{0,3}\.[0-9]{0,3}\.[0-9]{0,3}'
+    if re.match(pattern, mount_point) is None:
+        return False
+    return True
+
+
 def _get_mount_point(path):
     dir_path = os.path.dirname(path)
     while dir_path:
@@ -544,14 +693,45 @@ def _get_mount_point(path):
     return None
 
 
+def is_locally_mounted_remote_share(path):
+    return string.find(path, '/tmp/') == 0
+
+
+def extract_ip_address_from_locally_mounted_remote_share_path(path):
+    """
+    Path is of type ::
+
+        /tmp/127.0.0.1/webdav/a.txt
+    """
+    return path.split('/')[2]
+
+
 def get(object_id):
     """Returns the metadata for an object
     """
-    if os.path.exists(object_id):
-        stat = os.stat(object_id)
+    if (os.path.exists(object_id) or (is_locally_mounted_remote_share(object_id))):
+        """
+        For Documents/Shares/Mounted-Drives/Locally-Mounted-Remote-Shares,
+        where ".Sugar-Metadata" folder exists.
+
+        The only thing is that, for locally-mounted-remote-shares, the
+        "file" is not physically present.
+        """
+        if os.path.exists(object_id):
+            # if the file is physically present, derive file-metadata
+            # by physical examination of the file.
+            stat = os.stat(object_id)
+        else:
+            # if the file is remote, derive file-metadata by fetching
+            # properties remotely (webdav properties).
+            stat = None
+
         metadata = _get_file_metadata(object_id, stat)
         metadata['mountpoint'] = _get_mount_point(object_id)
     else:
+        """
+        For journal, where ".Sugar-Metadata" folder does not exists.
+        """
         metadata = _get_datastore().get_properties(object_id, byte_arrays=True)
         metadata['mountpoint'] = '/'
     return metadata
@@ -561,9 +741,16 @@ def get_file(object_id):
     """Returns the file for an object
     """
     if os.path.exists(object_id):
+        """
+        For Documents/Shares/Mounted-Drives/
+        Locally-Mounted-Remote-Shares-in-case-when-it-is-present-already.
+        """
         logging.debug('get_file asked for file with path %r', object_id)
         return object_id
     else:
+        """
+        For Journal.
+        """
         logging.debug('get_file asked for entry with id %r', object_id)
         file_path = _get_datastore().get_filename(object_id)
         if file_path:
@@ -754,6 +941,20 @@ def _write_entry_on_external_device(metadata, file_path):
         _rename_entry_on_external_device(file_path, destination_path,
                                          metadata_dir_path)
 
+    # For "Shares" folder, we need to set the permissions of the newly
+    # copied file to 0777, else it will not be accessible by "httpd"
+    # service.
+    if metadata['mountpoint'] == '/var/www/web1/web':
+        fd = os.open(destination_path, os.O_RDONLY)
+        os.fchmod(fd, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
+        os.close(fd)
+
+        metadata_file_path = os.path.join(metadata_dir_path, file_name + '.metadata')
+        fd = os.open(metadata_file_path, os.O_RDONLY)
+        os.fchmod(fd, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
+        os.close(fd)
+
+
     object_id = destination_path
     created.send(None, object_id=object_id)
 
diff --git a/src/jarabe/journal/palettes.py b/src/jarabe/journal/palettes.py
index 740b65a..5ff6bd6 100644
--- a/src/jarabe/journal/palettes.py
+++ b/src/jarabe/journal/palettes.py
@@ -45,6 +45,10 @@ from jarabe.model import mimeregistry
 from jarabe.journal import misc
 from jarabe.journal import model
 
+from webdav.Connection import WebdavError
+from jarabe.journal.webdavmanager import get_resource_by_ip_address_and_resource_key
+
+
 friends_model = friends.get_model()
 
 _copy_menu_helper = None
@@ -77,6 +81,9 @@ class ObjectPalette(Palette):
         Palette.__init__(self, primary_text=title,
                          icon=activity_icon)
 
+        from jarabe.journal.journalactivity import get_mount_point
+        current_mount_point = get_mount_point()
+
         if misc.get_activities(metadata) or misc.is_bundle(metadata):
             if metadata.get('activity_id', ''):
                 resume_label = _('Resume')
@@ -86,10 +93,15 @@ class ObjectPalette(Palette):
                 resume_with_label = _('Start with')
             menu_item = MenuItem(resume_label, 'activity-start')
             menu_item.connect('activate', self.__start_activate_cb)
+            if model.is_mount_point_for_locally_mounted_remote_share(current_mount_point):
+                menu_item.set_sensitive(False)
             self.menu.append(menu_item)
             menu_item.show()
 
             menu_item = MenuItem(resume_with_label, 'activity-start')
+            if model.is_mount_point_for_locally_mounted_remote_share(current_mount_point):
+                menu_item.set_sensitive(False)
+
             self.menu.append(menu_item)
             menu_item.show()
             start_with_menu = StartWithMenu(self._metadata)
@@ -101,6 +113,7 @@ class ObjectPalette(Palette):
             self.menu.append(menu_item)
             menu_item.show()
 
+
         menu_item = MenuItem(_('Copy to'))
         icon = Icon(icon_name='edit-copy', xo_color=color,
                     icon_size=gtk.ICON_SIZE_MENU)
@@ -120,16 +133,21 @@ class ObjectPalette(Palette):
         copy_menu.connect('volume-error', self.__volume_error_cb)
         menu_item.set_submenu(copy_menu)
 
+
         if self._metadata['mountpoint'] == '/':
             menu_item = MenuItem(_('Duplicate'))
             icon = Icon(icon_name='edit-duplicate', xo_color=color,
                         icon_size=gtk.ICON_SIZE_MENU)
             menu_item.set_image(icon)
             menu_item.connect('activate', self.__duplicate_activate_cb)
+            if model.is_mount_point_for_locally_mounted_remote_share(current_mount_point):
+                menu_item.set_sensitive(False)
             self.menu.append(menu_item)
             menu_item.show()
 
         menu_item = MenuItem(_('Send to'), 'document-send')
+        if model.is_mount_point_for_locally_mounted_remote_share(current_mount_point):
+            menu_item.set_sensitive(False)
         self.menu.append(menu_item)
         menu_item.show()
 
@@ -140,14 +158,19 @@ class ObjectPalette(Palette):
         if detail == True:
             menu_item = MenuItem(_('View Details'), 'go-right')
             menu_item.connect('activate', self.__detail_activate_cb)
+            if model.is_mount_point_for_locally_mounted_remote_share(current_mount_point):
+                menu_item.set_sensitive(False)
             self.menu.append(menu_item)
             menu_item.show()
 
         menu_item = MenuItem(_('Erase'), 'list-remove')
         menu_item.connect('activate', self.__erase_activate_cb)
+        if model.is_mount_point_for_locally_mounted_remote_share(current_mount_point):
+            menu_item.set_sensitive(False)
         self.menu.append(menu_item)
         menu_item.show()
 
+
     def __start_activate_cb(self, menu_item):
         misc.resume(self._metadata)
 
@@ -541,6 +564,31 @@ class ActionItem(gobject.GObject):
         self._post_operate_per_metadata_per_action(metadata)
 
     def _file_path_valid(self, metadata):
+        from jarabe.journal.journalactivity import get_mount_point
+        current_mount_point = get_mount_point()
+
+        # Now, for locally mounted remote-shares, download the file.
+        # Note that, always download the file, to avoid the problems
+        # of stale-cache.
+        if model.is_mount_point_for_locally_mounted_remote_share(current_mount_point):
+            file_path = metadata['uid']
+            filename = os.path.basename(file_path)
+            ip_address = model.extract_ip_address_from_locally_mounted_remote_share_path(file_path)
+            resource = get_resource_by_ip_address_and_resource_key(ip_address, '/webdav/' + filename)
+            download_file_path = '/tmp/' + ip_address + '/' + filename
+            try:
+                resource.downloadFile(download_file_path)
+                return True
+            except WebdavError, e:
+                error_message = e
+                logging.warn(error_message)
+                if self._batch_mode:
+                    self._handle_error_alert(error_message, metadata)
+                else:
+                    self.emit('volume-error', error_message,
+                            _('Error'))
+                return False
+
         file_path = model.get_file(metadata['uid'])
         if not file_path or not os.path.exists(file_path):
             logging.warn('Entries without a file cannot be copied.')
@@ -708,6 +756,25 @@ class DocumentsMenu(BaseCopyMenuItem):
         self._post_operate_per_metadata_per_action(metadata)
 
 
+class SharesMenu(BaseCopyMenuItem):
+    def __init__(self, metadata_list, show_editing_alert,
+                 show_progress_info_alert, batch_mode):
+        BaseCopyMenuItem.__init__(self, metadata_list, _('Shares'),
+                                  show_editing_alert,
+                                  show_progress_info_alert,
+                                  batch_mode)
+
+    def _operate(self, metadata):
+        if not self._file_path_valid(metadata):
+            return False
+        if not self._metadata_copy_valid(metadata,
+                                         '/var/www/web1/web'):
+            return False
+
+        # This is sync-operation. Call the post-operation now.
+        self._post_operate_per_metadata_per_action(metadata)
+
+
 class FriendsMenu(gtk.Menu):
     __gtype_name__ = 'JournalFriendsMenu'
 
@@ -835,6 +902,17 @@ class CopyMenuHelper(gtk.Menu):
             menu.append(documents_menu)
             documents_menu.show()
 
+        if get_mount_point() != '/var/www/web1/web':
+            documents_menu = SharesMenu(metadata_list,
+                                           show_editing_alert,
+                                           show_progress_info_alert,
+                                           batch_mode)
+            documents_menu.set_image(Icon(icon_name='emblem-neighborhood-shared',
+                                          icon_size=gtk.ICON_SIZE_MENU))
+            documents_menu.connect('volume-error', self.__volume_error_cb)
+            menu.append(documents_menu)
+            documents_menu.show()
+
         if get_mount_point() != '/':
             client = gconf.client_get_default()
             color = XoColor(client.get_string('/desktop/sugar/user/color'))
diff --git a/src/jarabe/journal/volumestoolbar.py b/src/jarabe/journal/volumestoolbar.py
index 94914e6..2a2436c 100644
--- a/src/jarabe/journal/volumestoolbar.py
+++ b/src/jarabe/journal/volumestoolbar.py
@@ -37,8 +37,10 @@ from sugar.graphics.palette import Palette
 from sugar.graphics.xocolor import XoColor
 from sugar import env
 
+from jarabe.frame.notification import NotificationIcon
 from jarabe.journal import model
-from jarabe.view.palettes import JournalVolumePalette, JournalXSPalette
+from jarabe.view.palettes import JournalVolumePalette, JournalXSPalette, RemoteSharePalette
+import jarabe.frame
 
 
 _JOURNAL_0_METADATA_DIR = '.olpc.store'
@@ -209,6 +211,7 @@ class VolumesToolbar(gtk.Toolbar):
 
     def _set_up_volumes(self):
         self._set_up_documents_button()
+        self._set_up_shares_button()
 
         volume_monitor = gio.volume_monitor_get()
         self._mount_added_hid = volume_monitor.connect('mount-added',
@@ -219,12 +222,11 @@ class VolumesToolbar(gtk.Toolbar):
         for mount in volume_monitor.get_mounts():
             self._add_button(mount)
 
-    def _set_up_documents_button(self):
-        documents_path = model.get_documents_path()
-        if documents_path is not None:
-            button = DocumentsButton(documents_path)
+    def _set_up_directory_button(self, dir_path, icon_name, label_text):
+        if dir_path is not None:
+            button = DirectoryButton(dir_path, icon_name)
             button.props.group = self._volume_buttons[0]
-            label = glib.markup_escape_text(_('Documents'))
+            label = glib.markup_escape_text(label_text)
             button.set_palette(Palette(label))
             button.connect('toggled', self._button_toggled_cb)
             button.show()
@@ -234,6 +236,39 @@ class VolumesToolbar(gtk.Toolbar):
             self._volume_buttons.append(button)
             self.show()
 
+    def _set_up_documents_button(self):
+        documents_path = model.get_documents_path()
+        self._set_up_directory_button(documents_path,
+                                      'user-documents',
+                                      _('Documents'))
+
+    def _set_up_shares_button(self):
+        shares_dir_path = '/var/www/web1/web'
+        self._set_up_directory_button(shares_dir_path,
+                                      'emblem-neighborhood-shared',
+                                      _('Shares'))
+
+    def _add_remote_share_button(self, buddy):
+        button = RemoteSharesButton(buddy)
+        button.props.group = self._volume_buttons[0]
+        label = glib.markup_escape_text(_('%s\'s share') % \
+                                        buddy.props.nick)
+        button.set_palette(RemoteSharePalette(buddy, button))
+        button.connect('toggled', self._button_toggled_cb)
+        button.show()
+
+        position = self.get_item_index(self._volume_buttons[-1]) + 1
+        self.insert(button, position)
+        self._volume_buttons.append(button)
+        self.show()
+
+        frame = jarabe.frame.get_view()
+        notif_icon = NotificationIcon()
+        notif_icon.props.icon_name = 'computer-xo'
+        notif_icon.props.xo_color = buddy.props.color
+        frame.add_notification(notif_icon,
+                               gtk.CORNER_BOTTOM_RIGHT)
+
     def __mount_added_cb(self, volume_monitor, mount):
         self._add_button(mount)
 
@@ -271,8 +306,8 @@ class VolumesToolbar(gtk.Toolbar):
     def __volume_error_cb(self, button, strerror, severity):
         self.emit('volume-error', strerror, severity)
 
-    def _button_toggled_cb(self, button):
-        if button.props.active:
+    def _button_toggled_cb(self, button, force_toggle=False):
+        if button.props.active or force_toggle:
             self.emit('volume-changed', button.mount_point)
 
     def _unmount_activated_cb(self, menu_item, mount):
@@ -300,6 +335,19 @@ class VolumesToolbar(gtk.Toolbar):
         if len(self.get_children()) < 2:
             self.hide()
 
+    def _remove_remote_share_button(self, mount_point):
+        # Here, IP_Address is the mount_point.
+        for button in self.get_children():
+            if type(button) == RemoteSharesButton and \
+                    button.mount_point == mount_point:
+                        self._volume_buttons.remove(button)
+                        self.remove(button)
+                        self.get_children()[0].props.active = True
+
+                        if len(sel.get_children()) < 2:
+                            self.hide()
+                        break;
+
     def set_active_volume(self, mount):
         button = self._get_button_for_mount(mount)
         button.props.active = True
@@ -313,6 +361,12 @@ class VolumesToolbar(gtk.Toolbar):
             if button.mount_point != mount_point:
                 button.set_sensitive(sensitive)
 
+    def get_journal_button(self):
+        return self._volume_buttons[0]
+
+    def get_button_toggled_cb(self):
+        return self._button_toggled_cb
+
 
 class BaseButton(RadioToolButton):
     __gsignals__ = {
@@ -427,18 +481,34 @@ class JournalButtonPalette(Palette):
                 {'free_space': free_space / (1024 * 1024)}
 
 
-class DocumentsButton(BaseButton):
+class DirectoryButton(BaseButton):
 
-    def __init__(self, documents_path):
-        BaseButton.__init__(self, mount_point=documents_path)
+    def __init__(self, dir_path, icon_name):
+        BaseButton.__init__(self, mount_point=dir_path)
 
-        self.props.named_icon = 'user-documents'
+        self.props.named_icon = icon_name
 
         client = gconf.client_get_default()
         color = XoColor(client.get_string('/desktop/sugar/user/color'))
         self.props.xo_color = color
 
 
+class RemoteSharesButton(BaseButton):
+
+    def __init__(self, buddy):
+        BaseButton.__init__(self, mount_point=buddy.props.ip_address)
+
+        self._buddy = buddy
+        self.props.named_icon = 'computer-xo'
+        self.props.xo_color = buddy.props.color
+        self._buddy_ip_address = buddy.props.ip_address
+
+    def create_palette(self):
+        palette = RemoteSharePalette(self._buddy)
+        return palette
+
+
+
 class XSButton(ToolButton):
     def __init__(self):
         ToolButton.__init__(self)
diff --git a/src/jarabe/journal/webdavmanager.py b/src/jarabe/journal/webdavmanager.py
new file mode 100644
index 0000000..cc2a0ff
--- /dev/null
+++ b/src/jarabe/journal/webdavmanager.py
@@ -0,0 +1,225 @@
+import os
+import sys
+
+import simplejson
+import shutil
+
+from webdav.Connection import AuthorizationError
+from webdav.WebdavClient import CollectionStorer
+
+def get_key_from_resource(resource):
+    return resource.path
+
+def ensure_correct_remote_webdav_hierarchy(remote_webdav_share_resources,
+                                           remote_webdav_share_collections):
+    pass
+    #assert len(remote_webdav_share_collections.keys()) == 1
+
+class WebDavUrlManager():
+    """
+    This class holds all data, relevant to a WebDavUrl.
+
+    One thing must be noted, that a valid WebDavUrl is the one which
+    may contain zero or more resources (files), or zero or more
+    collections (directories).
+
+    Thus, following are valid WebDavUrls ::
+
+        dav://1.2.3.4/webdav
+        dav://1.2.3.4/webdav/dir_1
+        dav://1.2.3.4/webdav/dir_1/dir_2
+
+    but following are not ::
+
+        dav://1.2.3.4/webdav/a.txt
+        dav://1.2.3.4/webdav/dir_1/b.jpg
+        dav://1.2.3.4/webdab/dir_1/dir_2/c.avi
+    """
+
+    def __init__(self, WebDavUrl, username, password):
+        self._WebDavUrl = WebDavUrl
+        self._username = username
+        self._password = password
+
+        # Since we are only instantiating primitive types in
+        # "__init__", we may safely call the class-function here.
+        self._fetch_resources_and_collections()
+
+    def _get_key_from_resource(self, resource):
+        return resource.path.encode(sys.getfilesystemencoding())
+
+    def _get_number_of_collections(self):
+        return len(self._remote_webdav_share_collections)
+
+    def _get_resources_dict(self):
+        return self._remote_webdav_share_resources
+
+    def _get_collections_dict(self):
+        return self._remote_webdav_share_collections
+
+    def _get_resource_by_key(self, key):
+        return self._remote_webdav_share_resources[key]['resource']
+
+    def _get_metadata_list(self):
+        metadata_list = []
+        for key in self._remote_webdav_share_resources.keys():
+            metadata_list.append(self._remote_webdav_share_resources[key]['metadata'])
+        return metadata_list
+
+    def _get_live_properties(self, resource_key):
+        resource_container = self._remote_webdav_share_resources[resource_key]
+        return resource_container['webdav-properties']
+
+    def _set_metadata_for_resource(self, key, metadata):
+        self._remote_webdav_share_resources[key]['metadata'] = metadata
+
+    def _fetch_resources_and_collections(self):
+        webdavConnection = CollectionStorer(self._WebDavUrl, validateResourceNames=False)
+
+        authFailures = 0
+        while authFailures < 2:
+            try:
+                self._remote_webdav_share_resources = {}
+                self._remote_webdav_share_collections = {}
+
+                for resource, properties in webdavConnection.getCollectionContents():
+                    try:
+                        key = self._get_key_from_resource(resource)
+                        selected_dict = None
+
+                        if properties.getResourceType() == 'resource':
+                            selected_dict = self._remote_webdav_share_resources
+                        else:
+                            selected_dict = self._remote_webdav_share_collections
+
+                        selected_dict[key] = {}
+                        selected_dict[key]['resource'] = resource
+                        selected_dict[key]['webdav-properties'] = properties
+                    except UnicodeEncodeError:
+                        print("Cannot encode resource path or properties.")
+                break # break out of the authorization failure counter
+            except AuthorizationError, e:
+                if self._username is None or self._password is None:
+                    raise Exception("WebDav username or password is None. Please specify appropriate values.")
+
+                if e.authType == "Basic":
+                    webdavConnection.connection.addBasicAuthorization(self._username, self._password)
+                elif e.authType == "Digest":
+                    info = parseDigestAuthInfo(e.authInfo)
+                    webdavConnection.connection.addDigestAuthorization(self._username, self._password, realm=info["realm"], qop=info["qop"], nonce=info["nonce"])
+                else:
+                    raise
+                authFailures += 1
+
+webdav_manager = {}
+
+
+def get_resource_by_ip_address_and_resource_key(ip_address, key):
+    global webdav_manager
+
+    if ip_address in webdav_manager.keys():
+        root_webdav = webdav_manager[ip_address]
+        resources_dict = root_webdav._get_resources_dict()
+        resource_dict = resources_dict[key]
+        resource = resource_dict['resource']
+
+        return resource
+
+
+def get_remote_webdav_share_metadata(ip_address):
+    protocol = 'dav://'
+
+    root_webdav_url = '/webdav'
+
+    complete_root_url = protocol + ip_address + root_webdav_url
+
+    root_webdav = WebDavUrlManager(complete_root_url, 'test', 'olpc')
+
+    # Keep reference to the "WebDavUrlManager", keyed by IP-Address.
+    global webdav_manager
+    webdav_manager[ip_address] = root_webdav
+
+
+    # Assert that the number of collections is only one at this url
+    # (i.e. only ".Sugar-Metadata" is present).
+    assert root_webdav._get_number_of_collections() == 1
+
+    root_sugar_metadata_url = root_webdav_url + '/.Sugar-Metadata'
+
+    complete_root_sugar_metadata_url = protocol + ip_address + root_sugar_metadata_url
+    root_webdav_sugar_metadata = WebDavUrlManager(complete_root_sugar_metadata_url, 'test', 'olpc')
+
+    # assert that the number of collections is zero at this url.
+    assert root_webdav_sugar_metadata._get_number_of_collections() == 0
+
+    # Now. associate sugar-metadata with each of the "root-webdav"
+    # resource.
+    root_webdav_resources = root_webdav._get_resources_dict()
+    root_webdav_sugar_metadata_resources = root_webdav_sugar_metadata._get_resources_dict()
+
+    # Prepare the metadata-download folder.
+    downloaded_data_root_dir = '/tmp/' + ip_address
+    downloaded_metadata_file_dir = downloaded_data_root_dir + '/.Sugar-Metadata'
+    if os.path.isdir(downloaded_data_root_dir):
+        shutil.rmtree(downloaded_data_root_dir)
+    os.makedirs(downloaded_metadata_file_dir)
+
+    for root_webdav_resource_name in root_webdav_resources.keys():
+        """
+        root_webdav_resource_name is of the type ::
+
+            /webdav/a.txt
+        """
+        split_tokens_array = root_webdav_resource_name.split('/')
+
+        # This will provide us with "a.txt"
+        basename = split_tokens_array[len(split_tokens_array) - 1]
+
+        # This will provide us with "a.txt.metadata"
+        sugar_metadata_basename = basename + '.metadata'
+
+        # Thus will provide us with "/webdav/.Sugar-Metadata/a.txt.metadata"
+        sugar_metadata_url = root_sugar_metadata_url + '/' + sugar_metadata_basename
+
+        # Ensure that "sugar_metadata_url" is present as one of the
+        # keys in "root_webdav_sugar_metadata_resources"
+        assert sugar_metadata_url in root_webdav_sugar_metadata_resources.keys()
+
+        # Now download the metadata file, read its contents, and store
+        # the metadata in memory.
+        # It is assumed that the metadata-file is small enough to be
+        # read in one call to "read".
+
+        downloaded_metadata_file_path = downloaded_metadata_file_dir + '/' + sugar_metadata_basename
+        metadata_resource = root_webdav_sugar_metadata._get_resource_by_key(sugar_metadata_url)
+        metadata_resource.downloadFile(downloaded_metadata_file_path)
+
+        file_pointer = open(downloaded_metadata_file_path)
+        metadata = eval(file_pointer.read())
+        file_pointer.close()
+
+        # Very critical.
+        # 1. CRITICAL ONE:
+        #    Fill in the uid.
+        #    Note that the file is not physically present.
+        metadata['uid'] = downloaded_data_root_dir + '/' + basename
+
+        # 2. CRITICAL TWO:
+        #    Fill in the properties, that can only be done by reading
+        #    in the webdav-properties.
+        live_properties = root_webdav._get_live_properties(root_webdav_resource_name)
+        metadata['filesize'] = live_properties.getContentLength()
+        metadata['timestamp'] = live_properties.getLastModified()
+        metadata['creation_time'] = live_properties.getCreationDate()
+
+        # Now, write this to the metadata-file, so that
+        # webdav-properties get gelled into sugar-metadata.
+
+        file_pointer = open(downloaded_metadata_file_path, 'w')
+        file_pointer.write(simplejson.dumps(metadata))
+        file_pointer.close()
+
+        root_webdav._set_metadata_for_resource(root_webdav_resource_name,
+                                               metadata)
+
+    return root_webdav._get_metadata_list()
diff --git a/src/jarabe/model/buddy.py b/src/jarabe/model/buddy.py
index 8f17d7e..c088aa9 100644
--- a/src/jarabe/model/buddy.py
+++ b/src/jarabe/model/buddy.py
@@ -43,6 +43,7 @@ class BaseBuddyModel(gobject.GObject):
         self._color = None
         self._tags = None
         self._current_activity = None
+        self._ip_address = None
 
         gobject.GObject.__init__(self, **kwargs)
 
@@ -87,6 +88,16 @@ class BaseBuddyModel(gobject.GObject):
                                         getter=get_current_activity,
                                         setter=set_current_activity)
 
+    def get_ip_address(self):
+        return self._ip_address
+
+    def set_ip_address(self, ip_address):
+        self._ip_address = ip_address
+
+    ip_address = gobject.property(type=object,
+                                  getter=get_ip_address,
+                                  setter=set_ip_address)
+
     def is_owner(self):
         raise NotImplementedError
 
diff --git a/src/jarabe/model/neighborhood.py b/src/jarabe/model/neighborhood.py
index 4528a15..39f648e 100644
--- a/src/jarabe/model/neighborhood.py
+++ b/src/jarabe/model/neighborhood.py
@@ -930,6 +930,9 @@ class Neighborhood(gobject.GObject):
         if 'key' in properties:
             buddy.props.key = properties['key']
 
+        if 'ip4-address' in properties:
+            buddy.props.ip_address = properties['ip4-address']
+
         nick_key = CONNECTION_INTERFACE_ALIASING + '/alias'
         if nick_key in properties:
             buddy.props.nick = properties[nick_key]
diff --git a/src/jarabe/view/buddymenu.py b/src/jarabe/view/buddymenu.py
index de5a772..dfbcfa3 100644
--- a/src/jarabe/view/buddymenu.py
+++ b/src/jarabe/view/buddymenu.py
@@ -73,6 +73,12 @@ class BuddyMenu(Palette):
         self.menu.append(menu_item)
         menu_item.show()
 
+        access_buddy_remote_share_menu_item = MenuItem(_('Access Share'), 'list-add')
+        access_buddy_remote_share_menu_item.connect('activate',
+                self._access_share_cb)
+        self.menu.append(access_buddy_remote_share_menu_item)
+        access_buddy_remote_share_menu_item.show()
+
         self._invite_menu = MenuItem('')
         self._invite_menu.connect('activate', self._invite_friend_cb)
         self.menu.append(self._invite_menu)
@@ -83,6 +89,11 @@ class BuddyMenu(Palette):
         activity = home_model.get_active_activity()
         self._update_invite_menu(activity)
 
+    def _access_share_cb(self, menuitem):
+        from jarabe.journal.journalactivity import get_journal
+        volumes_toolbar = get_journal().get_volumes_toolbar()
+        volumes_toolbar._add_remote_share_button(self._buddy)
+
     def _add_my_items(self):
         item = MenuItem(_('Shutdown'), 'system-shutdown')
         item.connect('activate', self.__shutdown_activate_cb)
diff --git a/src/jarabe/view/palettes.py b/src/jarabe/view/palettes.py
index 7a17f32..3b26faf 100644
--- a/src/jarabe/view/palettes.py
+++ b/src/jarabe/view/palettes.py
@@ -336,3 +336,36 @@ class JournalXSPalette(Palette):
     def __journal_restore_activate_cb(self, menu_item, xs_hostname):
         dialog = XSRestoreDialog(xs_hostname)
         dialog.show()
+
+
+class RemoteSharePalette(Palette):
+    def __init__(self, buddy, button):
+        Palette.__init__(self, label=('%s\'s share' % buddy.props.nick))
+        self._buddy = buddy
+        self._button = button
+
+        self.props.secondary_text = glib.markup_escape_text(buddy.props.ip_address)
+
+        vbox = gtk.VBox()
+        self.set_content(vbox)
+        vbox.show()
+
+        self.connect('popup', self.__popup_cb)
+
+        menu_item = MenuItem(pgettext('Share', 'Unmount'))
+
+        icon = Icon(icon_name='media-eject', icon_size=gtk.ICON_SIZE_MENU)
+        menu_item.set_image(icon)
+        icon.show()
+
+        menu_item.connect('activate', self.__unmount_activate_cb)
+        self.menu.append(menu_item)
+        menu_item.show()
+
+    def __unmount_activate_cb(self, menu_item):
+        from jarabe.journal.journalactivity import get_journal
+        singleton_volumes_toolbar = get_journal().get_volumes_toolbar()
+        singleton_volumes_toolbar._remove_remote_share_button(self._buddy.props.ip_address)
+
+    def __popup_cb(self, palette):
+        pass
diff --git a/src/webdav/Condition.py b/src/webdav/Condition.py
new file mode 100644
index 0000000..76acf94
--- /dev/null
+++ b/src/webdav/Condition.py
@@ -0,0 +1,475 @@
+# pylint: disable-msg=R0921,W0704,R0901,W0511,R0201
+# Copyright 2008 German Aerospace Center (DLR)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+ 
+
+"""
+This module contains classes for creating a search condition according to the DASL draft.
+The classes will output the WHERE part of a search request to a WebDAV server.
+
+Instances of the classes defined in this module form a tree data structure which represents
+a search condition. This tree is made up of AND-nodes, OR-nodes, Operator- and comparison-
+nodes and from property (i.e. variable) and constant leaf nodes.
+"""
+
+
+import types
+from time import strftime
+from calendar import timegm
+from rfc822 import formatdate
+
+from webdav.Constants import NS_DAV, PROP_LAST_MODIFIED, DATE_FORMAT_ISO8601
+
+
+__version__ = "$Revision$"[11:-2]
+
+
+class ConditionTerm(object):
+    """
+    This is the abstact base class for all condition terms.
+    """
+    def __init__(self):
+        pass
+    
+    def toXML(self):
+        """
+        Abstact method which return a XML string which can be passed to a WebDAV server
+        for a search condition.
+        """
+        raise NotImplementedError
+    
+    # start Tamino workaround for missing like-op:
+    def postFilter(self, resultSet):
+        """
+        Abstact method for temporary workaround for Tamino's absense of the like-operator.
+        This method shall filter the given result set for those resources which match
+        all Contains-trems.
+        """
+        return resultSet
+    # end of workaround
+
+
+class IsCollectionTerm(ConditionTerm):
+    """ Leaf condition. Checks if the matching resources are collections. """
+    
+    def __init__(self):
+        """ Constructor. """
+        
+        ConditionTerm.__init__(self)
+
+    def toXML(self):
+        """
+        Returns XML encoding.
+        """
+        
+        return "<D:is-collection/>"
+
+
+class Literal(ConditionTerm):
+    """
+    A leaf class for condition expressions representing a constant value.
+    """
+    def __init__(self, literal):
+        ConditionTerm.__init__(self)
+        self.literal = literal
+        
+    def toXML(self):
+        '''
+        Returns XML encoding.
+        '''
+        return "<D:literal>" + self.literal + "</D:literal>"
+    
+
+class UnaryTerm(ConditionTerm):
+    """
+    Base class of all nodes with a single child node.
+    """
+    def __init__(self, child):
+        ConditionTerm.__init__(self)
+        self.child = child
+    
+    def toXML(self):
+        '''
+        Returns XML encoding.
+        '''
+        return self.child.toXML()
+    
+    
+class BinaryTerm(ConditionTerm):
+    """
+    Base class of all nodes with two child nodes
+    """
+    def __init__(self, left, right):
+        ConditionTerm.__init__(self)
+        self.left = left
+        self.right = right
+    
+    def toXML(self):
+        '''
+        Returns XML encoding.
+        '''
+        return self.left.toXML() + self.right.toXML()
+
+class TupleTerm(ConditionTerm):
+    """
+    Base class of all nodes with multiple single child nodes.
+    """
+    def __init__(self, terms):
+        ConditionTerm.__init__(self)
+        self.terms = terms
+    
+    def addTerm(self, term):
+        '''
+        Removes a term.
+        
+        @param term: term to add
+        '''
+        self.terms.append(term)
+    
+    def removeTerm(self, term):
+        '''
+        Adds a term.
+        
+        @param term: term to remove
+        '''   
+        try:
+            self.terms.remove(term)
+        except ValueError:
+            pass
+                
+    def toXML(self):
+        '''
+        Returns XML encoding.
+        '''
+        result = ""
+        for term in self.terms:
+            result += term.toXML()
+        return result
+
+
+class AndTerm(TupleTerm):
+    """
+    This class represents and logical AND-condition with multiple sub terms.
+    """
+    def toXML(self):
+        '''
+        Returns XML encoding.
+        '''
+        return "<D:and>" + TupleTerm.toXML(self) + "</D:and>"
+
+    # start Tamino workaround for missing like-op:
+    def postFilter(self, resultSet):
+        '''
+        Filters the given result set. This is a TAMINO WebDav server workaround
+        for the missing 'like' tag.
+        
+        @param resultSet: the result set that needs to be filtered.
+        '''
+        for term in self.terms:
+            filtered = term.postFilter(resultSet)
+            resultSet = filtered
+        return resultSet
+    # end of workaround
+
+class OrTerm(TupleTerm):
+    """
+    This class represents and logical OR-condition with multiple sub terms.
+    """
+    def toXML(self):
+        '''
+        Returns XML encoding.
+        '''
+        return "<D:or>" + TupleTerm.toXML(self) + "</D:or>"
+    
+    # start Tamino workaround for missing like-op:
+    def postFilter(self, resultSet):
+        '''
+        Filters the given result set. This is a TAMINO WebDav server workaround
+        for the missing 'like' tag.
+        
+        @param resultSet: the result set that needs to be filtered.
+        '''
+        raise NotImplementedError
+
+   
+class NotTerm(UnaryTerm):
+    """
+    This class represents a negation term for the contained sub term.
+    """
+    def toXML(self):
+        '''
+        Returns XML encoding.
+        '''
+        # start Tamino workaround for missing like-op:
+        if  isinstance(self.child, ContainsTerm):
+            return ""
+        # end of workaround        
+        return "<D:not>" + UnaryTerm.toXML(self) + "</D:not>"
+
+    # start Tamino workaround for missing like-op:
+    def postFilter(self, resultSet):
+        '''
+        Filters the given result set. This is a TAMINO WebDav server workaround
+        for the missing 'like' tag.
+        
+        @param resultSet: the result set that needs to be filtered.
+        '''
+        if  isinstance(self.child, ContainsTerm):
+            self.child.negate = 1
+            # TODO: pass on filter
+        return self.child.postFilter(resultSet)
+        
+
+class ExistsTerm(UnaryTerm):
+    """
+    Nodes of this class must have a single child with tuple type (of len 2) representing a 
+    WebDAV property.
+    This leaf term evaluates to true if the (child) property exists.
+    """
+    def toXML(self):
+        '''
+        Returns XML encoding.
+        '''
+        return '<D:is-defined><D:prop xmlns="%s"><%s' % self.child + ' /></D:prop></D:is-defined>'
+
+class ContentContainsTerm(UnaryTerm):
+    """
+    This class can be used to search for a given phrase in resources' contents.
+    """
+    def toXML(self):
+        '''
+        Returns XML encoding.
+        '''
+        return "<D:contains>" + self.child + "</D:contains>"
+
+
+
+class BinaryRelationTerm(BinaryTerm):
+    """
+    This is the abstact base class for the following relation operands.
+    """
+    def __init__(self, left, right):
+        BinaryTerm.__init__(self, left, right)
+        if isinstance(self.left, types.StringType):     # Must be namespace + name pair
+            self.left = ('DAV:', self.left)
+        if not isinstance(self.right, Literal):
+            self.right = Literal(self.right)             # Must be Literal instance
+
+    def toXML(self):
+        '''
+        Returns XML encoding.
+        '''
+        ## TODO: extract name space and create shortcut for left element
+        return '<D:prop xmlns="%s"><%s /></D:prop>' % self.left + self.right.toXML()
+
+        
+class StringRelationTerm(BinaryRelationTerm):
+    """
+    This is the abstact base class for the following string relation classes.
+    """
+    def __init__(self, left, right, caseless=None):
+        """
+        @param left: webdav property (namespace, name)
+        @param right: string/unicode literal
+        qparam caseless: 1 for case sensitive comparison
+        """
+        BinaryRelationTerm.__init__(self, left, Literal(right))
+        self.caseless = caseless
+        if self.caseless:
+            self.attrCaseless = "yes"
+        else:
+            self.attrCaseless = "no"
+        
+class NumberRelationTerm(BinaryRelationTerm):
+    """
+    This is the abstact base class for the following number comparison classes.
+    """
+    def __init__(self, left, right):
+        """
+        @param left: webdav property (namespace, name)
+        @param right: constant number
+        """
+        ## TODO: implemet typed literal
+        BinaryRelationTerm.__init__(self, left, Literal(str(right)))
+
+class DateRelationTerm(BinaryRelationTerm):
+    """
+    This is the abstact base class for the following date comparison classes.
+    """
+    def __init__(self, left, right):
+        """
+        @param left: webdav property (namespace, name)
+        @param right: string literal containing a date in ISO8601 format
+        """
+        ## TODO: implemet typed literal
+        assert len(right) == 9, "No time is specified for literal: " + str(right)
+        BinaryRelationTerm.__init__(self, left, right)        
+        if self.left == (NS_DAV, PROP_LAST_MODIFIED):
+            rfc822Time = formatdate(timegm(right))      # must not use locale setting                  
+            self.right = Literal(rfc822Time)
+        else:               
+            self.right = Literal(strftime(DATE_FORMAT_ISO8601, right))
+
+
+class MatchesTerm(StringRelationTerm):
+    """
+    Nodes of this class evaluate to true if the (child) property's value matches the (child) string.
+    """
+    def toXML(self):
+        '''
+        Returns XML encoding.
+        '''
+        return '<D:eq caseless="%s">' % self.attrCaseless + StringRelationTerm.toXML(self) + "</D:eq>"
+
+class ContainsTerm(StringRelationTerm):        
+    """
+    Nodes of this class evaluate to true if the (left child) property's value contains the
+    (right child) string.
+    """
+    def __init__(self, left, right, isTaminoWorkaround=False):
+        right = unicode(right)
+        StringRelationTerm.__init__(self, left, "%" + right + "%")
+        # Tamino workaround: operator like is not yet implemented:
+        self.negate = 0
+        self.isTaminoWorkaround = isTaminoWorkaround 
+            
+    def toXML(self):
+        '''
+        Returns XML encoding.
+        '''
+        # Tamino workaround: operator like is not yet implemented:
+        # Produce a is-defined-condition instead
+        if self.isTaminoWorkaround:
+            return "<D:isdefined><D:prop xmlns='%s'><%s" % self.left + " /></D:prop></D:isdefined>"
+        else:
+            return '<D:like caseless="%s">' % self.attrCaseless + StringRelationTerm.toXML(self) + "</D:like>"
+        
+    # start Tamino workaround for missing like-op:
+    def postFilter(self, resultSet):
+        '''
+        Filters the given result set. This is a TAMINO WebDav server workaround
+        for the missing 'like' tag.
+        
+        @param resultSet: the result set that needs to be filtered.
+        '''
+        newResult = {}
+        word = self.right.literal[1:-1]       # remove leading and trailing '%' characters (see __init__())
+        for url, properties in resultSet.items():
+            value = properties.get(self.left)
+            if self.negate:
+                if not value or value.textof().find(word) < 0:
+                    newResult[url] = properties                
+            else:
+                if value and value.textof().find(word) >= 0:
+                    newResult[url] = properties
+        return newResult
+    # end of workaround
+
+class IsEqualTerm(NumberRelationTerm):
+    """
+    Nodes of this class evaluate to true if the (left child) numerical property's value is equal
+    to the (right child) number.
+    """
+    def toXML(self):
+        '''
+        Returns XML encoding.
+        '''
+        return "<D:eq>" + NumberRelationTerm.toXML(self) + "</D:eq>"
+
+class IsGreaterTerm(NumberRelationTerm):
+    """
+    Nodes of this class evaluate to true if the (left child) numerical property's value is greater
+    than the (right child) number.
+    """
+    def toXML(self):
+        '''
+        Returns XML encoding.
+        '''
+        return "<D:gt>" + NumberRelationTerm.toXML(self) + "</D:gt>"
+
+class IsGreaterOrEqualTerm(NumberRelationTerm):
+    """
+    Nodes of this class evaluate to true if the (left child) numerical property's value is greater
+    than or equal to the (right child) number.
+    """
+    def toXML(self):
+        '''
+        Returns XML encoding.
+        '''
+        return "<D:gte>" + NumberRelationTerm.toXML(self) + "</D:gte>"
+
+class IsSmallerTerm(NumberRelationTerm):
+    """
+    Nodes of this class evaluate to true if the (left child) numerical property's value is less
+    than the (right child) number.
+    """
+    def toXML(self):
+        '''
+        Returns XML encoding.
+        '''
+        return "<D:lt>" + NumberRelationTerm.toXML(self) + "</D:lt>"
+
+class IsSmallerOrEqualTerm(NumberRelationTerm):
+    """
+    Nodes of this class evaluate to true if the (left child) numerical property's value is less
+    than or equal to the (right child) number.
+    """
+    def toXML(self):
+        '''
+        Returns XML encoding.
+        '''
+        return "<D:lte>" + NumberRelationTerm.toXML(self) + "</D:lte>"
+
+
+class OnTerm(DateRelationTerm):
+    """
+    Nodes of this class evaluate to true if the (left child) property's value is a date
+    equal to the (right child) date.
+    """
+    def toXML(self):
+        '''
+        Returns XML encoding.
+        '''
+        return "<D:eq>" + DateRelationTerm.toXML(self) + "</D:eq>"
+
+class AfterTerm(DateRelationTerm):
+    """
+    Nodes of this class evaluate to true if the (left child) property's value is a date
+    succeeding the (right child) date.
+    """
+    def toXML(self):
+        '''
+        Returns XML encoding.
+        '''
+        return "<D:gt>" + DateRelationTerm.toXML(self) + "</D:gt>"
+
+class BeforeTerm(DateRelationTerm):
+    """
+    Nodes of this class evaluate to true if the (left child) property's value is a date
+    preceeding the (right child) date.
+    """
+    def toXML(self):
+        '''
+        Returns XML encoding.
+        '''
+        return "<D:lt>" + DateRelationTerm.toXML(self) + "</D:lt>"
+
+
+    
+# Simple module test
+if  __name__ == '__main__':
+    # use the example from the webdav specification
+    condition = AndTerm( (MatchesTerm('getcontenttype', 'image/gif'), \
+                IsGreaterTerm('getcontentlength', 4096)) )
+    print "Where: " + condition.toXML()
diff --git a/src/webdav/Connection.py b/src/webdav/Connection.py
new file mode 100644
index 0000000..66f7833
--- /dev/null
+++ b/src/webdav/Connection.py
@@ -0,0 +1,321 @@
+# pylint: disable-msg=W0142,W0102,R0901,R0904,E0203,E1101,C0103
+#
+# Copyright 2008 German Aerospace Center (DLR)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+"""
+The contained class extends the HTTPConnection class for WebDAV support.
+"""
+
+
+from httplib import HTTPConnection, CannotSendRequest, BadStatusLine, ResponseNotReady
+from copy import copy
+import base64   # for basic authentication
+try:
+    import hashlib
+except ImportError: # for Python 2.4 compatibility
+    import md5
+    hashlib = md5
+import mimetypes
+import os       # file handling
+import urllib
+import types
+import socket   # to "catch" socket.error
+from threading import RLock
+try:
+    from uuid import uuid4
+except ImportError: # for Python 2.4 compatibility
+    from uuid_ import uuid4
+from xml.parsers.expat import ExpatError
+
+from davlib import DAV
+from qp_xml import Parser
+
+from webdav.WebdavResponse import MultiStatusResponse, ResponseFormatError
+from webdav import Constants
+from webdav.logger import getDefaultLogger
+
+
+__version__ = "$LastChangedRevision$"
+
+
+class Connection(DAV):
+    """
+    This class handles a connection to a WebDAV server.
+    This class is used internally. Client code should prefer classes
+    L{WebdavClient.ResourceStorer} and L{WebdavClient.CollectionStorer}.
+    
+    @author: Roland Betz
+    """
+    
+    # Constants
+    #  The following switch activates a workaround for the Tamino webdav server:
+    #  Tamino expects URLs which are passed in a HTTP header to be Latin-1 encoded
+    #  instead of Utf-8 encoded.
+    #  Set this switch to zero in order to communicate with conformant servers.
+    blockSize = 30000
+    MaxRetries = 10
+    
+    def __init__(self, *args, **kwArgs):
+        DAV.__init__(self, *args, **kwArgs)
+        self.__authorizationInfo = None
+        self.logger = getDefaultLogger()
+        self.isConnectedToCatacomb = True
+        self.serverTypeChecked = False
+        self._lock = RLock()
+         
+    def _request(self, method, url, body=None, extra_hdrs={}):
+        
+        self._lock.acquire()
+        try:
+            # add the authorization header
+            extraHeaders = copy(extra_hdrs)
+            if self.__authorizationInfo:
+
+                # update (digest) authorization data
+                if hasattr(self.__authorizationInfo, "update"):
+                    self.__authorizationInfo.update(method=method, uri=url)
+                
+                extraHeaders["AUTHORIZATION"] = self.__authorizationInfo.authorization
+            
+            # encode message parts
+            body = _toUtf8(body)
+            url = _urlEncode(url)
+            for key, value in extraHeaders.items():
+                extraHeaders[key] = _toUtf8(value)
+                if key == "Destination": # copy/move header
+                    if self.isConnectedToCatacomb:
+                        extraHeaders[key] = _toUtf8(value.replace(Constants.SHARP, Constants.QUOTED_SHARP))
+                        
+                    else: # in case of TAMINO 4.4
+                        extraHeaders[key] = _urlEncode(value)
+            # pass message to httplib class
+            for retry in range(0, Connection.MaxRetries):    # retry loop
+                try:
+                    self.logger.debug("REQUEST Send %s for %s" % (method, url))
+                    self.logger.debug("REQUEST Body: " + repr(body))
+                    for hdr in extraHeaders.items():
+                        self.logger.debug("REQUEST Header: " + repr(hdr))
+                    self.request(method, url, body, extraHeaders)
+                    response = self.getresponse()
+                    break  # no retry needed
+                except (CannotSendRequest, socket.error, BadStatusLine, ResponseNotReady), exc:
+                    # Workaround, start: reconnect and retry...
+                    self.logger.debug("Exception: " + str(exc) + " Retry ... ")
+                    self.close()
+                    try:
+                        self.connect()
+                    except (CannotSendRequest, socket.error, BadStatusLine, ResponseNotReady), exc:
+                        raise WebdavError("Cannot perform request. Connection failed.")
+                    if retry == Connection.MaxRetries - 1:
+                        raise WebdavError("Cannot perform request.")
+            return self.__evaluateResponse(method, response)
+        finally:
+            self._lock.release()
+        
+    def __evaluateResponse(self, method, response):
+        """ Evaluates the response of the WebDAV server. """
+        
+        status, reason = response.status, response.reason
+        self.logger.debug("Method: " + method + " Status %d: " % status + reason)
+        
+        if status >= Constants.CODE_LOWEST_ERROR:     # error has occured ?
+            self.logger.debug("ERROR Response: " + response.read())
+            
+            # identify authentication CODE_UNAUTHORIZED, throw appropriate exception
+            if status == Constants.CODE_UNAUTHORIZED:
+                raise AuthorizationError(reason, status, response.msg["www-authenticate"])
+            
+            response.close()
+            raise WebdavError(reason, status)
+        
+        if status == Constants.CODE_MULTISTATUS:
+            content = response.read()
+            ## check for UTF-8 encoding
+            try:
+                response.root = Parser().parse(content)
+            except ExpatError, error:
+                errorMessage = "Invalid XML document has been returned.\nReason: '%s'" % str(error.args)
+                raise WebdavError(errorMessage)
+            try:
+                response.msr = MultiStatusResponse(response.root)
+            except ResponseFormatError:
+                raise WebdavError("Invalid WebDAV response.")
+            response.close()
+            self.logger.debug("RESPONSE (Multi-Status): " + unicode(response.msr))
+        elif method == 'LOCK' and status == Constants.CODE_SUCCEEDED:
+            response.parse_lock_response()
+            response.close()
+        elif method != 'GET' and method != 'PUT':
+            self.logger.debug("RESPONSE Body: " + response.read())
+            response.close()
+        return response
+        
+    def addBasicAuthorization(self, user, password, realm=None):
+        if user and len(user) > 0:
+            self.__authorizationInfo = _BasicAuthenticationInfo(realm=realm, user=user, password=password)
+                   
+    def addDigestAuthorization(self, user, password, realm, qop, nonce, uri = None, method = None):
+        if user and len(user) > 0:
+            # username, realm, password, uri, method, qop are required
+            self.__authorizationInfo = _DigestAuthenticationInfo(realm=realm, user=user, password=password, uri=uri, method=method, qop=qop, nonce=nonce)
+
+    def putFile(self, path, srcfile, header={}):
+        self._lock.acquire()
+        try:
+            # Assemble header
+            try:
+                size = os.path.getsize(srcfile.name)    
+            except os.error, error:
+                raise WebdavError("Cannot determine file size.\nReason: ''" % str(error.args))
+            header["Content-length"] = str(size)
+            
+            contentType, contentEnc = mimetypes.guess_type(path)
+            if contentType:
+                header['Content-Type'] = contentType
+            if contentEnc:
+                header['Content-Encoding'] = contentEnc
+            if self.__authorizationInfo:
+                # update (digest) authorization data
+                if hasattr(self.__authorizationInfo, "update"):
+                    self.__authorizationInfo.update(method="PUT", uri=path)
+                header["AUTHORIZATION"] = self.__authorizationInfo.authorization
+                
+            # send first request
+            path = _urlEncode(path)
+            try:
+                HTTPConnection.request(self, 'PUT', path, "", header)
+                self._blockCopySocket(srcfile, self, Connection.blockSize)
+                srcfile.close()
+                response = self.getresponse()
+            except (CannotSendRequest, socket.error, BadStatusLine, ResponseNotReady), exc:
+                self.logger.debug("Exception: " + str(exc) + " Retry ... ")
+                raise WebdavError("Cannot perform request.")
+            status, reason = (response.status, response.reason)
+            self.logger.debug("Status %d: %s" % (status, reason))
+            try:
+                if status >= Constants.CODE_LOWEST_ERROR:     # error has occured ?
+                    raise WebdavError(reason, status)
+            finally:
+                self.logger.debug("RESPONSE Body: " + response.read())
+                response.close()        
+            return response
+        finally:
+            self._lock.release()
+                  
+    def _blockCopySocket(self, source, toSocket, blockSize):
+        transferedBytes = 0
+        block = source.read(blockSize)
+        #while source.readinto(block, blockSize):
+        while len(block):
+            toSocket.send(block)
+            self.logger.debug("Wrote %d bytes." % len(block))
+            transferedBytes += len(block)
+            block = source.read(blockSize)        
+        self.logger.info("Transfered %d bytes." % transferedBytes)
+
+    def __str__(self):
+        return self.protocol + "://" + self.host + ':' + str(self.port)
+        
+
+class _BasicAuthenticationInfo(object):
+    def __init__(self, **kwArgs):
+        self.__dict__.update(kwArgs)
+        self.cookie = base64.encodestring("%s:%s" % (self.user, self.password) ).strip()
+        self.authorization = "Basic " + self.cookie
+        self.password = None     # protect password security
+        
+class _DigestAuthenticationInfo(object):
+    
+    __nc = "0000000" # in hexadecimal without leading 0x
+    
+    def __init__(self, **kwArgs):
+
+        self.__dict__.update(kwArgs)
+        
+        if self.qop is None:
+            raise WebdavError("Digest without qop is not implemented.")
+        if self.qop == "auth-int":
+            raise WebdavError("Digest with qop-int is not implemented.")
+    
+    def update(self, **kwArgs):
+        """ Update input data between requests"""
+    
+        self.__dict__.update(kwArgs)
+
+    def _makeDigest(self):
+        """ Creates the digest information. """
+        
+        # increment nonce count
+        self._incrementNc()
+        
+        # username, realm, password, uri, method, qop are required
+        
+        a1 = "%s:%s:%s" % (self.user, self.realm, self.password)
+        ha1 = hashlib.md5(a1).hexdigest()
+
+        #qop == auth
+        a2 = "%s:%s" % (self.method, self.uri)
+        ha2 = hashlib.md5(a2).hexdigest()
+        
+        cnonce = str(uuid4())
+        
+        responseData = "%s:%s:%s:%s:%s:%s" % (ha1, self.nonce, _DigestAuthenticationInfo.__nc, cnonce, self.qop, ha2)
+        digestResponse = hashlib.md5(responseData).hexdigest()
+        
+        authorization = "Digest username=\"%s\", realm=\"%s\", nonce=\"%s\", uri=\"%s\", algorithm=MD5, response=\"%s\", qop=auth, nc=%s, cnonce=\"%s\"" \
+                        % (self.user, self.realm, self.nonce, self.uri, digestResponse, _DigestAuthenticationInfo.__nc, cnonce)
+        return authorization
+    
+    authorization = property(_makeDigest)
+    
+    def _incrementNc(self):
+        _DigestAuthenticationInfo.__nc = self._dec2nc(self._nc2dec() + 1)
+    
+    def _nc2dec(self):
+        return int(_DigestAuthenticationInfo.__nc, 16)
+    
+    def _dec2nc(self, decimal):
+        return hex(decimal)[2:].zfill(8)
+    
+
+class WebdavError(IOError):
+    def __init__(self, reason, code=0):
+        IOError.__init__(self, code)
+        self.code = code
+        self.reason = reason
+    def __str__(self):
+        return self.reason
+
+
+class AuthorizationError(WebdavError):
+    def __init__(self, reason, code, authHeader):
+        WebdavError.__init__(self, reason, code)
+        
+        self.authType = authHeader.split(" ")[0]
+        self.authInfo = authHeader
+
+
+def _toUtf8(body):
+    if not body is None:
+        if type(body) == types.UnicodeType:
+            body = body.encode('utf-8')
+    return body
+
+
+def _urlEncode(url):
+    if type(url) == types.UnicodeType:
+        url = url.encode('utf-8')
+    return urllib.quote(url)
diff --git a/src/webdav/Constants.py b/src/webdav/Constants.py
new file mode 100644
index 0000000..56dfd77
--- /dev/null
+++ b/src/webdav/Constants.py
@@ -0,0 +1,199 @@
+# pylint: disable-msg=C0103
+#
+# Copyright 2008 German Aerospace Center (DLR)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+"""
+Contains XML tag names for the WebDAV protocol (RFC 2815)
+and further WebDAV related constants.
+"""
+
+
+__version__ = "$Revision$"[11:-2]
+
+
+QUOTED_SHARP = "%23"
+SHARP = "#"
+
+# Date formats
+DATE_FORMAT_ISO8601 = r"%Y-%m-%dT%H:%M:%SZ"
+DATE_FORMAT_HTTP = r"%a, %d %b %Y %H:%M:%S GMT"  # not used, substituted by rfc822 function
+
+NS_DAV = 'DAV:'
+NS_TAMINO = 'http://namespaces.softwareag.com/tamino/response2'
+
+TAG_PROPERTY_FIND   = 'propfind'
+TAG_PROPERTY_NAME   = 'propname'
+TAG_PROPERTY_UPDATE = 'propertyupdate'
+TAG_PROPERTY_SET    = 'set'
+TAG_PROPERTY_REMOVE = 'remove'
+TAG_ALL_PROPERTY    = 'allprop'
+TAG_PROP            = 'prop'
+
+TAG_MULTISTATUS         = 'multistatus'
+TAG_RESPONSE            = 'response'
+TAG_HREF                = 'href'
+TAG_PROPERTY_STATUS     = 'propstat'
+TAG_STATUS              = 'status'
+TAG_RESPONSEDESCRIPTION = 'responsdescription'
+
+PROP_CREATION_DATE    = 'creationdate'
+PROP_DISPLAY_NAME     = 'displayname'
+PROP_CONTENT_LANGUAGE = 'getcontentlanguage'
+PROP_CONTENT_LENGTH   = 'getcontentlength'
+PROP_CONTENT_TYPE     = 'getcontenttype'
+PROP_ETAG             = 'getetag'
+PROP_MODIFICATION_DATE = 'modificationdate' # this property is supported by
+# Tamino 4.4 but not by Catacomb; the date format is ISO8601
+PROP_LAST_MODIFIED    = 'getlastmodified'
+PROP_LOCK_DISCOVERY   = 'lockdiscovery'
+PROP_RESOURCE_TYPE    = 'resourcetype'
+PROP_SOURCE           = 'source'
+PROP_SUPPORTED_LOCK   = 'supportedlock'
+PROP_OWNER            = 'owner'
+
+PROP_RESOURCE_TYPE_RESOURCE    = 'resource'
+PROP_RESOURCE_TYPE_COLLECTION  = 'collection'
+
+TAG_LINK             = 'link'
+TAG_LINK_SOURCE      = 'src'
+TAG_LINK_DESTINATION = 'dst'
+
+TAG_LOCK_ENTRY     = 'lockentry'
+TAG_LOCK_SCOPE     = 'lockscope'
+TAG_LOCK_TYPE      = 'locktype'
+TAG_LOCK_INFO      = 'lockinfo'
+TAG_ACTIVE_LOCK    = 'activelock'
+TAG_LOCK_DEPTH     = 'depth'
+TAG_LOCK_TOKEN     = 'locktoken'
+TAG_LOCK_TIMEOUT   = 'timeout'
+TAG_LOCK_EXCLUSIVE = 'exclusive'
+TAG_LOCK_SHARED    = 'shared'
+TAG_LOCK_OWNER     = 'owner'
+
+# HTTP error code constants
+CODE_MULTISTATUS = 207
+CODE_SUCCEEDED = 200
+CODE_CREATED = 201
+CODE_NOCONTENT = 204
+
+CODE_LOWEST_ERROR = 300
+
+CODE_UNAUTHORIZED = 401
+CODE_FORBIDDEN = 403
+CODE_NOT_FOUND = 404
+CODE_CONFLICT = 409
+CODE_PRECONDITION_FAILED = 412
+CODE_LOCKED = 423   # no permission
+CODE_FAILED_DEPENDENCY = 424
+
+CODE_OUTOFMEM = 507
+
+# ?
+CONFIG_UNICODE_URL = 1
+
+# constants for WebDAV DASL according to draft
+
+TAG_SEARCH_REQUEST = 'searchrequest'
+TAG_SEARCH_BASIC   = 'basicsearch'
+TAG_SEARCH_SELECT  = 'select'
+TAG_SEARCH_FROM    = 'from'
+TAG_SEARCH_SCOPE   = 'scope'
+TAG_SEARCH_WHERE   = 'where'
+
+# constants for WebDAV ACP (according to draft-ietf-webdav-acl-09) below ...
+
+TAG_ACL                 = 'acl'
+TAG_ACE                 = 'ace'
+TAG_GRANT               = 'grant'
+TAG_DENY                = 'deny'
+TAG_PRIVILEGE           = 'privilege'
+TAG_PRINCIPAL           = 'principal'
+TAG_ALL                 = 'all'
+TAG_AUTHENTICATED       = 'authenticated'
+TAG_UNAUTHENTICATED     = 'unauthenticated'
+TAG_OWNER               = 'owner'
+TAG_PROPERTY            = 'property'
+TAG_SELF                = 'self'
+TAG_INHERITED           = 'inherited'
+TAG_PROTECTED           = 'protected'
+TAG_SUPPORTED_PRIVILEGE = 'supported-privilege'
+TAG_DESCRIPTION         = 'description'
+
+# privileges for WebDAV ACP:
+TAG_READ = 'read'
+TAG_WRITE = 'write'
+TAG_WRITE_PROPERTIES = 'write-properties'
+TAG_WRITE_CONTENT = 'write-content'
+TAG_UNLOCK = 'unlock'
+TAG_READ_ACL = 'read-acl'
+TAG_READ_CURRENT_USER_PRIVILEGE_SET = 'read-current-user-privilege-set'
+TAG_WRITE_ACL = 'write-acl'
+TAG_ALL = 'all'
+TAG_BIND = 'bind'
+TAG_UNBIND = 'unbind'
+# Tamino-specific privileges
+TAG_TAMINO_SECURITY = 'security' 
+# Limestone-specific privileges
+TAG_BIND_COLLECTION = 'bind-collection'
+TAG_UNBIND_COLLECTION = 'unbind-collection'
+TAG_READ_PRIVATE_PROPERTIES = 'read-private-properties'
+TAG_WRITE_PRIVATE_PROPERTIES = 'write-private-properties'
+
+# properties for WebDAV ACP:
+PROP_CURRENT_USER_PRIVILEGE_SET = 'current-user-privilege-set'
+PROP_SUPPORTED_PRIVILEGE_SET    = 'supported-privilege-set'
+PROP_PRINCIPAL_COLLECTION_SET = 'principal-collection-set' 
+
+# reports for WebDAV ACP
+REPORT_ACL_PRINCIPAL_PROP_SET   = 'acl-principal-prop-set'
+
+
+
+# constants for WebDAV Delta-V
+
+#   WebDAV Delta-V method names
+METHOD_REPORT = 'REPORT'
+METHOD_VERSION_CONTROL = 'VERSION-CONTROL'
+METHOD_UNCHECKOUT = 'UNCHECKOUT'
+METHOD_CHECKOUT = 'CHECKOUT'
+METHOD_CHECKIN = 'CHECKIN'
+METHOD_UPDATE = 'UPDATE'
+
+#   Special properties
+PROP_SUCCESSOR_SET = (NS_DAV, 'successor-set')
+PROP_PREDECESSOR_SET = (NS_DAV, 'predecessor-set')
+PROP_VERSION_HISTORY = (NS_DAV, 'version-history')
+PROP_CREATOR = (NS_DAV, 'creator-displayname')
+PROP_VERSION_NAME = (NS_DAV, 'version-name')
+PROP_CHECKEDIN = (NS_DAV, 'checked-in')
+PROP_CHECKEDOUT = (NS_DAV, 'checked-out')
+PROP_COMMENT = (NS_DAV, 'comment')
+
+#   XML tags for request body
+TAG_VERSION_TREE = 'version-tree'
+TAG_LOCATE_BY_HISTORY = 'locate-by-history'
+TAG_UPDATE = 'update'
+TAG_VERSION = 'version'
+
+# HTTP header constants
+HTTP_HEADER_DEPTH_INFINITY  = 'infinity'
+HTTP_HEADER_IF  = 'if'
+HTTP_HEADER_DAV = 'dav'
+HTTP_HEADER_DASL = 'dasl'
+HTTP_HEADER_OPTION_ACL = 'access-control'
+HTTP_HEADER_OPTION_DAV_BASIC_SEARCH = 'DAV:basicsearch'
+HTTP_HEADER_SERVER = 'server'
+HTTP_HEADER_SERVER_TAMINO = 'Apache/2.0.54 (Win32)'
diff --git a/src/webdav/Makefile.am b/src/webdav/Makefile.am
new file mode 100644
index 0000000..3356daf
--- /dev/null
+++ b/src/webdav/Makefile.am
@@ -0,0 +1,20 @@
+SUBDIRS = acp
+
+sugardir = $(pythondir)/webdav
+sugar_PYTHON =                  \
+             Connection.py      \
+             davlib.py          \
+             logger.py          \
+             NameCheck.py       \
+             Utils.py           \
+             VersionHandler.py  \
+             WebdavRequests.py  \
+             Condition.py       \
+             Constants.py       \
+             __init__.py        \
+             qp_xml.py          \
+             uuid_.py           \
+             WebdavClient.py    \
+             WebdavResponse.py
+
+
diff --git a/src/webdav/NameCheck.py b/src/webdav/NameCheck.py
new file mode 100644
index 0000000..7976973
--- /dev/null
+++ b/src/webdav/NameCheck.py
@@ -0,0 +1,193 @@
+# pylint: disable-msg=R0904,W0142,W0511,W0104,C0321,E1103,W0212
+# 
+# Copyright 2008 German Aerospace Center (DLR)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+"""
+Check name of new collections/resources for "illegal" characters.
+"""
+
+
+import re
+import unicodedata
+
+
+__version__ = "$LastChangedRevision$"
+
+
+_unicodeUmlaut = [unicodedata.lookup("LATIN CAPITAL LETTER A WITH DIAERESIS"),
+                  unicodedata.lookup("LATIN SMALL LETTER A WITH DIAERESIS"),
+                  unicodedata.lookup("LATIN CAPITAL LETTER O WITH DIAERESIS"),
+                  unicodedata.lookup("LATIN SMALL LETTER O WITH DIAERESIS"),
+                  unicodedata.lookup("LATIN CAPITAL LETTER U WITH DIAERESIS"),
+                  unicodedata.lookup("LATIN SMALL LETTER U WITH DIAERESIS"),
+                  unicodedata.lookup("LATIN SMALL LETTER SHARP S")]
+ 
+# Define characters and character base sets
+_german  = u"".join(_unicodeUmlaut)
+_alpha = "A-Za-z"
+_num = "0-9"
+_alphaNum = _alpha + _num
+_space = " "
+_under = "_"
+_dash = "\-"
+_dot = "\."
+_exclam = "\!"
+_tilde   = "\~"
+_dollar  = "\$"
+_plus = "+"
+_equal = "="
+_sharp = "#"
+
+# Define character groups
+_letterNum = _alphaNum + _german
+_letter = _alpha + _german
+
+# Define character sets for names
+firstPropertyChar = _letter + _under
+propertyChar = firstPropertyChar + _num + _dash + _dot
+firstResourceChar = firstPropertyChar + _num + _tilde + _exclam + _dollar + \
+                     _dot + _dash + _plus + _equal + _sharp
+resourceChar = firstResourceChar + _space
+
+# Define regular expressions for name validation
+_propertyFirstRe = re.compile(u"^["+ firstPropertyChar +"]")
+
+_propertyRe = re.compile(u"[^"+ propertyChar +"]")
+_resourceFirstRe = re.compile(u"^["+ firstResourceChar +"]")
+_resourceRe = re.compile(u"[^"+ resourceChar +"]")
+                    
+                    
+def isValidPropertyName(name):
+    """
+    Check if the given property name is valid.
+    
+    @param name: Property name.
+    @type name: C{unicode}
+    
+    @return: Boolean indicating whether the given property name is valid or not. 
+    @rtype: C{bool}
+    """
+
+    illegalChar = _propertyRe.search(name)
+    return illegalChar == None  and  _propertyFirstRe.match(name) != None
+    
+    
+def isValidResourceName(name):
+    """
+    Check if the given resource name is valid.
+    
+    @param name: Resource name.
+    @type name: C{unicode}
+    
+    @return: Boolean indicating whether the given resource name is valid or not. 
+    @rtype: C{bool}
+    """
+    
+    illegalChar = _resourceRe.search(name)
+    return illegalChar == None  and  _resourceFirstRe.match(name) != None
+
+
+def validatePropertyName(name):
+    """
+    Check if the given property name is valid.
+    
+    @param name: Property name.
+    @type name: C{unicode}
+    @raise WrongNameError: if validation fails (see L{datafinder.common.NameCheck.WrongNameError})
+    """
+    
+    illegalChar = _propertyRe.search(name)
+    if illegalChar:
+        raise WrongNameError(illegalChar.start(), name[illegalChar.start()])
+    if not _propertyFirstRe.match(name):
+        if len(name) > 0:
+            raise WrongNameError(0, name[0])
+        else:
+            raise WrongNameError(0, 0)
+       
+    
+def validateResourceName(name):
+    """
+    Check if the given resource name is valid.
+    
+    @param name: name of resource/collection
+    @type name: C{unicode}
+    @raise WrongNameError: if validation fails (@see L{datafinder.common.NameCheck.WrongNameError})
+    """
+
+    illegalChar = _resourceRe.search(name)
+    if illegalChar:
+        raise WrongNameError(illegalChar.start(), name[illegalChar.start()])
+    if not _resourceFirstRe.match(name):
+        if len(name) > 0:
+            raise WrongNameError(0, name[0])
+        else:
+            raise WrongNameError(0, 0)
+
+
+def getResourceNameErrorPosition(name):
+    """
+    Get position of illegal character (and the error-message).
+    This method can be used to get this information if L{isValidPropertyName}
+    or L{isValidResourceName} failed.
+    
+    @param name: Resource name.
+    @type name: C{unicode}
+    
+    @return: Tuple of error position and message.
+    @rtype: C{tuple} of C{int} and C{unicode}
+    """
+
+    result = (-1, None)
+    illegalChar = _resourceRe.search(name)
+    if illegalChar:
+        result = (illegalChar.start(), \
+                  u"Illegal character '%s' at index %d." % \
+                      (name[illegalChar.start()], illegalChar.start()))
+    elif not _resourceFirstRe.match(name):
+        result = (0, u"Illegal character '%s' at index %d." % (name[0], 0))
+    return result
+
+    
+class WrongNameError(ValueError):
+    """
+    Exception raised if an "illegal" character was found.
+    
+    @ivar character: character that caused the exception
+    @type character: C{unicode}
+    @ivar position: position of C{character}
+    @type position: C{int}
+    """
+    
+    def __init__(self, position, character):
+        """
+        Constructor.
+        
+        @param character: Character that caused the exception.
+        @type character: C{unicode}
+        @param position: Position of C{character}
+        @type position: C{int}
+        """
+        
+        ValueError.__init__(self)        
+        self.character = character
+        self.position = position
+    
+    def __str__(self):
+        """ Returns string representation. """
+        
+        return ValueError.__str__(self) + \
+            "Character '%s' at index %d." % (self.character, self.position)
diff --git a/src/webdav/Utils.py b/src/webdav/Utils.py
new file mode 100644
index 0000000..ec05755
--- /dev/null
+++ b/src/webdav/Utils.py
@@ -0,0 +1,154 @@
+# pylint: disable-msg=W0141,R0912
+#
+# Copyright 2008 German Aerospace Center (DLR)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+"""
+The module contains functions to support use of the WebDav functionalities.
+"""
+
+
+import os
+import sys
+
+from webdav.WebdavClient import CollectionStorer, ResourceStorer
+from webdav.Constants import NS_DAV, PROP_RESOURCE_TYPE, CODE_NOT_FOUND, PROP_RESOURCE_TYPE_RESOURCE
+from webdav.Connection import WebdavError
+
+
+__version__ = "$Revision$"[11:-2]
+
+
+def resourceExists(node, name = None, resourceType = PROP_RESOURCE_TYPE_RESOURCE):
+    """
+    Check if resource exists.
+    
+    Usage:
+      - resourceExists(ResourceStorer-object):
+        check if resource exists
+      - resourceExists(CollectionStorer-object, name):
+        check if resource name exists in collection
+    
+    @param node: node that has to be checked or node of collection
+    @type node: L{ResourceStorer<webdav.WebdavClient.ResourceStorer>}
+    @param name: name of resource (in collection node) that has to be checked
+    @type name: string
+    
+    @return: boolean
+    
+    @raise WebdavError: all WebDAV errors except WebDAV error 404 (not found)
+    """
+    
+    exists = False
+    if not node:
+        return exists
+    try:
+        myResourceType = ""
+        if name:
+            # make sure it's unicode:
+            if not isinstance(name, unicode):
+                name = name.decode(sys.getfilesystemencoding())
+            url = node.url
+            if url.endswith("/"):
+                url = url  + name
+            else:
+                url = url + "/" + name
+            newNode = ResourceStorer(url, node.connection)
+            element = newNode.readProperty(NS_DAV, PROP_RESOURCE_TYPE)
+        else: # name is "None":
+            element = node.readProperty(NS_DAV, PROP_RESOURCE_TYPE)
+        
+        if len(element.children) > 0:
+            myResourceType = element.children[0].name
+        if resourceType == myResourceType or resourceType == PROP_RESOURCE_TYPE_RESOURCE:
+            exists = True
+        else:
+            exists = False
+    except WebdavError, wderr:
+        if wderr.code == CODE_NOT_FOUND:
+            # node doesn't exist -> exists = False:
+            exists = False
+        else:
+            # another exception occured -> "re-raise" it:
+            raise
+    return exists
+
+
+def downloadCollectionContent(destinationPath, collectionToDownload):
+    """
+    Downloads the resources contained to the given directory.
+    
+    @param destinationPath: Path to download the files to, will be created if it not exists.
+    @type destinationPath: C{String}
+    @param collectionToDownload: Collection to download the content from.
+    @type collectionToDownload: instance of L{CollectionStorer<webdav.WebdavClient.CollectionStorer>}
+    
+    @raise  WebdavError: If something goes wrong.
+    """
+    
+    from time import mktime, gmtime
+
+    downloadCount = 0
+
+    listOfItems = collectionToDownload.getCollectionContents()
+    
+    if not os.path.exists(destinationPath):
+        try:
+            os.makedirs(destinationPath)
+        except OSError:
+            errorMessage = "Cannot create download destination directory '%s'." % destinationPath
+            raise WebdavError(errorMessage)
+        
+    try:
+        itemsInPath = os.listdir(destinationPath)
+    except OSError:
+        errorMessage = "Cannot read the content of download destination directory '%s'." % destinationPath
+        raise WebdavError(errorMessage)
+    
+    for item in listOfItems:
+        # skip collections
+        if not isinstance(item[0], CollectionStorer):
+            itemSavePath = os.path.join(destinationPath, item[0].name)
+            existsItemSavePath = os.path.exists(itemSavePath)
+            
+            # update?
+            if existsItemSavePath:
+                try:
+                    isUpdateNecessary = mktime(item[1].getLastModified()) > mktime(gmtime(os.path.getmtime(itemSavePath)))
+                except (ValueError, OverflowError):
+                    isUpdateNecessary = True
+                # windows is not case sensitive
+                for realItem in itemsInPath:
+                    if realItem.lower() == item[0].name.lower():
+                        itemsInPath.remove(realItem)
+            else:
+                isUpdateNecessary = True
+            
+            # download
+            if not existsItemSavePath or (existsItemSavePath and isUpdateNecessary):
+                item[0].downloadFile(itemSavePath)
+                downloadCount = downloadCount + 1
+    
+    # delete old items
+    try:
+        for item in itemsInPath:
+            os.remove(os.path.join(destinationPath, item))
+    except OSError, e:
+        if e.errno == 13:    # permission error
+            sys.stderr.write("permission problem on '%s' in %s\n" % (e.filename, e.strerror))
+        else:
+            raise  
+        
+    return downloadCount
diff --git a/src/webdav/VersionHandler.py b/src/webdav/VersionHandler.py
new file mode 100644
index 0000000..a1962c6
--- /dev/null
+++ b/src/webdav/VersionHandler.py
@@ -0,0 +1,198 @@
+# pylint: disable-msg=W0612,W0142
+#
+# Copyright 2008 German Aerospace Center (DLR)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+"""
+The WebDAV client module forwards Delta-V related method invocations to
+the following VersionHandler class.
+"""
+
+__version__ = '$Revision$'[11:-2]
+
+
+import types
+
+from webdav import Constants
+from davlib import XML_CONTENT_TYPE, XML_DOC_HEADER
+
+
+class VersionHandler(object):
+    """
+    Implements a client interface for WebDAV Delta-V methods
+    For the Delta-V see RFC 3253 at http://www.ietf.org/rfc/rfc3253.txt
+    """
+    
+    # restrict instance variables
+    __slots__ = ('path', 'connection')
+ 
+    
+    def __init__(self, connection, path):
+        """
+        Construct a VersionHandler with a URL path and a WebDAV connection.
+        This constructor must not be called outside class ResourceStorer.
+        
+        @param connection: L{webdav.Connection} instance
+        @param path: resource's path part of URL
+        """
+        #assert isinstance(connection, Connection), \
+        #    "Class of connection is %s." % connection.__class__.__name__
+        self.connection = connection
+        self.path = path
+ 
+ 
+    def activateVersionControl(self):
+        """
+        Turns version control on for this resource.
+        The resource becomes a version controlled resource (VCR)
+        """
+        response = self.connection._request(Constants.METHOD_VERSION_CONTROL, self.path, None, {})
+        # set auto-versioning to DAV:locked-checkout
+        ## parse response body in case of an error
+
+    def uncheckout(self, lockToken=None):
+        """
+        Undos a previous check-out operation on this VCR.
+        The VCR is reverted to the state before the checkout/lock operation.
+        Beware: Property or content changes will be lost !
+        A (optional) lock has to be removed seperatedly.
+        
+        @param lockToken: returned by a preceeding lock() method invocation or None
+        """
+        headers = {}
+        if lockToken:
+            headers = lockToken.toHeader() 
+        response = self.connection._request(Constants.METHOD_UNCHECKOUT, self.path, None, headers)
+        ## parse response body in case of an error
+    
+    def listAllVersions(self):
+        """
+        List version history.
+        
+        @return: List of versions for this VCR. Each version entry is a tuple adhering
+            to the format (URL-path, name, creator, tuple of successor URL-paths).
+            If there are no branches then there is at most one successor within the tuple. 
+        """
+        # implementation is similar to the propfind method
+        headers                 = {}
+        headers['Content-Type'] = XML_CONTENT_TYPE
+        body = _createReportVersionTreeBody()
+        response = self.connection._request(Constants.METHOD_REPORT, self.path, body, headers)
+        # response is multi-status
+        result = []
+        for path, properties in response.msr.items():           
+            # parse the successor-set value from XML into alist
+            result.append( (path, str(properties[Constants.PROP_VERSION_NAME]), \
+                            str(properties[Constants.PROP_CREATOR]), \
+                            _extractSuccessorList(properties[Constants.PROP_SUCCESSOR_SET])) )
+        ## TODO: sort for path and produce list
+        result.sort()
+        return result
+
+    # warning: not tested yet
+    def readVersionProperties(self):
+        """
+        Provide version related information on this VCR.
+        This include a reference to the latest version resource,
+        check-out state information and a comment.
+        
+        @return: map of version properties with values.
+        """
+        versionProperties = (Constants.PROP_CHECKEDIN, Constants.PROP_CHECKEDOUT, Constants.PROP_COMMENT)
+        return self.connection.readProperties(*versionProperties)
+    
+
+    def revertToVersion(self, oldVersion):
+        """
+        Revert this VCR to the given version.
+        Beware: All versions succeeding the given version are made unavailable.
+        
+        @param oldVersion: URL-path of a previous version of this VCR.
+        """
+        ## send an update request
+        assert isinstance(oldVersion, types.StringType) or isinstance(oldVersion, types.UnicodeType)
+        response = self.connection._request(Constants.METHOD_UPDATE, self.path,
+                        _createUpdateBody(oldVersion), {})
+        return response
+
+
+    # the following is not needed when using auto-versioning
+    
+    # warning: not tested yet
+    def checkout(self):
+        """
+        Marks resource as checked-out
+        This is usually followed by a GET (download) operation.
+        """
+        response = self.connection._request(Constants.METHOD_CHECKOUT, self.path, None, {})
+        ## parse response body in case of an error
+
+    # warning: not tested yet
+    def checkin(self):
+        """
+        Creates a new version from the VCR's content.
+        This opeartion is usually preceeded by one or more write operations.
+        """
+        response = self.connection._request(Constants.METHOD_CHECKIN, self.path, None, {})
+        versionUrl = response.getheader('Location')
+        return versionUrl
+        ## parse response body in case of an error
+
+
+
+
+# Helper functions
+def _createReportVersionTreeBody():
+    """
+    TBD
+    
+    @return: ...
+    @rtype: string
+    """
+    versions = 'D:' + Constants.TAG_VERSION_TREE
+    prop = 'D:' + Constants.TAG_PROP
+    nameList = [Constants.PROP_SUCCESSOR_SET, Constants.PROP_VERSION_NAME, Constants.PROP_CREATOR]
+    return XML_DOC_HEADER + \
+        '<%s xmlns:D="DAV:"><%s>' % (versions, prop) + \
+        reduce(lambda xml, name: xml + "<D:%s/>" % name[1], [''] + nameList) + \
+        '</%s></%s>' % (prop, versions)
+
+def _createUpdateBody(path):
+    """
+    TBD
+    
+    @return: ...
+    @rtype: string
+    """
+    update = 'D:' + Constants.TAG_UPDATE
+    version = 'D:' + Constants.TAG_VERSION
+    href = 'D:' + Constants.TAG_HREF
+    #PROP = 'D:' + TAG_PROP
+    return XML_DOC_HEADER + \
+        '<%s xmlns:D="DAV:"><%s><%s>' % (update, version, href) + \
+        path + \
+        '</%s></%s></%s>' % (href, version, update)
+
+def _extractSuccessorList(element):
+    """
+    TBD
+    
+    @return: ...
+    @rtype: tuple of strings
+    """
+    result = []
+    for href in element.children:
+        result.append(href.textof())
+    return tuple(result)
diff --git a/src/webdav/WebdavClient.py b/src/webdav/WebdavClient.py
new file mode 100644
index 0000000..4cf8c90
--- /dev/null
+++ b/src/webdav/WebdavClient.py
@@ -0,0 +1,833 @@
+# pylint: disable-msg=R0904,W0142,W0511,W0104,C0321,E1103,W0212
+# 
+# Copyright 2008 German Aerospace Center (DLR)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+ 
+
+"""
+This module contains the classes ResourceStorer and CollectionStorer for accessing WebDAV resources.
+"""
+
+
+from davlib import XML_CONTENT_TYPE
+
+from urlparse import urlsplit
+import re
+import types
+import sys
+import os
+import shutil
+
+from webdav import Constants
+from webdav.WebdavResponse import LiveProperties
+from webdav.WebdavRequests import createFindBody, createUpdateBody, createDeleteBody, createSearchBody
+from webdav.Condition import ConditionTerm
+from webdav.Connection import Connection, WebdavError, AuthorizationError
+from webdav.VersionHandler import VersionHandler
+
+from webdav.acp.Privilege import Privilege
+from webdav.acp.Acl import ACL
+from webdav.NameCheck import validateResourceName, WrongNameError
+
+
+__version__ = '$Revision$'[11:-2]
+
+
+def switchUnicodeUrlOn(switch):
+    """
+    Configure whether to use unicode (UTF-8) encoded URLs (default) or
+    Latin-1 encoded URLs.
+    
+    @param switch: 1 if unicode URLs shall be used
+    """
+
+    assert switch == 0 or switch == 1, "Pass boolean argument, please."
+    Constants.CONFIG_UNICODE_URL = switch
+
+
+def parseDigestAuthInfo(authInfo):
+    """ 
+    Parses the authentication information returned from a server and returns
+    a dictionary containing realm, qop, and nonce.
+    
+    @see: L{AuthorizationError<webdav.Connection.AuthorizationError>} 
+    or the main function of this module.
+    """
+    
+    info = dict()
+    info["realm"] = re.search('realm="([^"]+)"', authInfo).group(1)
+    info["qop"] = re.search('qop="([^"]+)"', authInfo).group(1)
+    info["nonce"] = re.search('nonce="([^"]+)"', authInfo).group(1)
+    return info
+
+
+class ResourceStorer(object):
+    """
+    This class provides client access to a WebDAV resource
+    identified by an URI. It provides all WebDAV class 2 features which include
+    uploading data, getting and setting properties qualified by a XML name space,
+    locking and unlocking the resource. 
+    This class does not cache resource data. This has to be performed by its clients.
+    
+    @author: Roland Betz
+    """
+        
+    # Instance properties
+    url = property(lambda self: str(self.connection) + self.path, None, None, "Resource's URL")
+
+    def __init__(self, url, connection=None, validateResourceNames=True):
+        """
+        Creates an instance for the given URL
+        User must invoke validate() after construction to check the resource on the server.
+        
+        @param url: Unique resource location for this storer.
+        @type  url: C{string}
+        @param connection: this optional parameter contains a Connection object 
+            for the host part of the given URL. Passing a connection saves 
+            memory by sharing this connection. (defaults to None)
+        @type  connection: L{webdav.Connection}
+        @raise WebdavError: If validation of resource name path parts fails.
+        """
+
+        assert connection == None or isinstance(connection, Connection)
+        parts = urlsplit(url, allow_fragments=False)
+        self.path = parts[2]
+        self.validateResourceNames = validateResourceNames
+        
+        # validate URL path
+        for part in self.path.split('/'):
+            if part != '' and not "ino:" in part: # explicitly allowing this character sequence as a part of a path (Tamino 4.4)
+                if self.validateResourceNames:
+                    try:
+                        validateResourceName(part)
+                    except WrongNameError:
+                        raise WebdavError("Found invalid resource name part.")
+                self.name = part
+        # was: filter(lambda part: part and validateResourceName(part), self.path.split('/'))
+        # but filter is deprecated
+        
+        self.defaultNamespace = None     # default XML name space of properties
+        if connection:
+            self.connection = connection
+        else:
+            conn = parts[1].split(":")
+            if len(conn) == 1:
+                self.connection = Connection(conn[0], protocol = parts[0])  # host and protocol
+            else:
+                self.connection = Connection(conn[0], int(conn[1]), protocol = parts[0])  # host and port and protocol
+        self.versionHandler = VersionHandler(self.connection, self.path)
+        
+
+    def validate(self):
+        """
+        Check whether URL contains a WebDAV resource
+        Uses the WebDAV OPTIONS method.
+        
+        @raise WebdavError: L{WebdavError} if URL does not contain a WebDAV resource
+        """
+        #davHeader = response.getheader(HTTP_HEADER_DAV)
+        davHeader = self.getSpecificOption(Constants.HTTP_HEADER_DAV)
+        self.connection.logger.debug("HEADER DAV: %s" % davHeader)
+        if not(davHeader) or davHeader.find("2") < 0:   # DAV class 2 supported ?
+            raise WebdavError("URL does not support WebDAV", 0)
+
+    def options(self):
+        """
+        Send an OPTIONS request to server and return all HTTP headers.
+        
+        @return: map of all HTTP headers returned by the OPTIONS method.
+        """
+        response = self.connection.options(self.path)
+        result = {}
+        result.update(response.msg)
+        self.connection.logger.debug("OPTION returns: " + str(result.keys()))
+        return result
+        
+    def _getAclSupportAvailable(self):
+        """
+        Returns True if the current connection has got ACL support.
+        
+        @return: ACL support (True / False)
+        @rtype: C{bool}
+        """
+        options = self.getSpecificOption(Constants.HTTP_HEADER_DAV)
+        if options.find(Constants.HTTP_HEADER_OPTION_ACL) >= 0:
+            return True
+        else:
+            return False
+        
+    aclSupportAvailable = property(_getAclSupportAvailable)
+        
+    def _getDaslBasicsearchSupportAvailable(self):
+        """
+        Returns True if the current connection supports DASL basic search.
+        
+        @return: DASL basic search support (True / False)
+        @rtype: C{bool}
+        """
+        options = self.getSpecificOption(Constants.HTTP_HEADER_DASL)
+        if not options or \
+           not options.find(Constants.HTTP_HEADER_OPTION_DAV_BASIC_SEARCH) >= 0:
+            return False
+        else:
+            return True
+    
+    daslBasicsearchSupportAvailable = property(_getDaslBasicsearchSupportAvailable)
+    
+    def isConnectedToCatacombServer(self):
+        """
+        Returns True if connected to a Catacomb WebDav server.
+        
+        @return: if connected to Catacomb Webdav server (True / False)
+        @rtype: C{bool}
+        """
+        if not self.connection.serverTypeChecked:
+            options = self.getSpecificOption(Constants.HTTP_HEADER_SERVER)
+            if options.find(Constants.HTTP_HEADER_SERVER_TAMINO) >= 0:
+                self.connection.isConnectedToCatacomb = False
+            else:
+                self.connection.isConnectedToCatacomb = True
+            self.connection.serverTypeChecked = True
+        return self.connection.isConnectedToCatacomb
+        
+    def getSpecificOption(self, option):
+        """
+        Returns specified WebDav options.
+        @param option: name of the option
+        
+        @return: String containing the value of the option.
+        @rtype: C{string}
+        """
+        options = ''
+        try:
+            options = self.options().get(option)
+        except KeyError:
+            return options
+        return options  
+        
+    ### delegate some method invocations    
+    def __getattr__(self, name):
+        """
+        Build-in method:
+        Forwards unknow lookups (methods) to delegate object 'versionHandler'.
+        
+        @param name: name of unknown attribute
+        """
+        # delegate Delta-V methods
+        return getattr(self.versionHandler, name)
+    
+    def copy(self, toUrl, infinity=True):
+        """
+        Copies this resource.
+        
+        @param toUrl: target URI path
+        @param infinity: Flag that indicates that the complete content of collection is copied. (default)  
+        @type depth: C{boolean}
+        """
+        self.connection.logger.debug("Copy to " + repr(toUrl));
+        _checkUrl(toUrl)
+        if infinity:
+            response = self.connection.copy(self.path, toUrl)
+        else:
+            response = self.connection.copy(self.path, toUrl, 0)
+        if  response.status == Constants.CODE_MULTISTATUS and response.msr.errorCount > 0:
+            raise WebdavError("Request failed: " + response.msr.reason, response.msr.code)        
+        
+    def delete(self, lockToken=None):
+        """
+        Deletes this resource.
+        
+        @param lockToken: String returned by last lock operation or null.
+        @type  lockToken: L{LockToken}
+        """
+        assert lockToken == None or isinstance(lockToken, LockToken), \
+                "Invalid lockToken argument %s" % type(lockToken)
+        header = {}
+        if lockToken:
+            header = lockToken.toHeader()
+        response = self.connection.delete(self.path, header)
+        if  response.status == Constants.CODE_MULTISTATUS and response.msr.errorCount > 0:
+            raise WebdavError("Request failed: " + response.msr.reason, response.msr.code)        
+
+    def move(self, toUrl):
+        """
+        Moves this resource to the given path or renames it.
+        
+        @param toUrl: new (URI) path
+        """
+        self.connection.logger.debug("Move to " + repr(toUrl));
+        _checkUrl(toUrl)
+        response = self.connection.move(self.path, toUrl)
+        if  response.status == Constants.CODE_MULTISTATUS and response.msr.errorCount > 0:
+            raise WebdavError("Request failed: " + response.msr.reason, response.msr.code)        
+
+ 
+    def lock(self, owner):
+        """
+        Locks this resource for exclusive write access. This means that for succeeding
+        write operations the returned lock token has to be passed.
+        If the methode does not throw an exception the lock has been granted.
+        
+        @param owner: describes the lock holder
+        @return: lock token string (automatically generated)
+        @rtype: L{LockToken}
+        """
+        response = self.connection.lock(self.path, owner)
+        if  response.status == Constants.CODE_MULTISTATUS and response.msr.errorCount > 0:
+            raise WebdavError("Request failed: " + response.msr.reason, response.msr.code)        
+        return LockToken(self.url, response.locktoken)
+
+    def unlock(self, lockToken):
+        """
+        Removes the lock from this resource.
+        
+        @param lockToken: which has been return by the lock() methode
+        @type  lockToken: L{LockToken}
+        """
+        self.connection.unlock(self.path, lockToken.token)
+
+
+    def deleteContent(self, lockToken=None):
+        """
+        Delete binary data at permanent storage.
+        
+        @param lockToken: None or lock token from last lock request
+        @type  lockToken: L{LockToken}
+        """
+        assert lockToken == None or isinstance(lockToken, LockToken), \
+                "Invalid lockToken argument %s" % type(lockToken)
+        header = {}
+        if lockToken:
+            header = lockToken.toHeader()
+        self.connection.put(self.path, "", extra_hdrs=header)
+
+    def uploadContent(self, content, lockToken=None):
+        """
+        Write binary data to permanent storage.
+        
+        @param content: containing binary data
+        @param lockToken: None or lock token from last lock request
+        @type  lockToken: L{LockToken}
+        """
+        assert not content or isinstance(content, types.UnicodeType) or\
+                isinstance(content, types.StringType), "Content is not a string: " + content.__class__.__name__
+        assert lockToken == None or isinstance(lockToken, LockToken), \
+                "Invalid lockToken argument %s" % type(lockToken)
+        header = {}
+        if lockToken:
+            header = lockToken.toHeader()
+        response = None
+        if not content is None:
+            header["Content-length"] = len(content)
+        else:
+            header["Content-length"] = 0
+
+        try: 
+            response = self.connection.put(self.path, content, extra_hdrs=header)
+        finally:
+            if response:        
+                self.connection.logger.debug(response.read())
+                response.close()
+
+    def uploadFile(self, newFile, lockToken=None):
+        """
+        Write binary data to permanent storage.
+        
+        @param newFile: File containing binary data.
+        @param lockToken: None or lock token from last lock request
+        @type  lockToken: L{LockToken}
+        """
+        assert isinstance(newFile, types.FileType), "Argument is no file: " + file.__class__.__name__
+        assert lockToken == None or isinstance(lockToken, LockToken), \
+                "Invalid lockToken argument %s" % type(lockToken)
+        header = {}
+        if lockToken:
+            header = lockToken.toHeader()
+        self.connection.putFile(self.path, newFile, header=header)
+
+    def downloadContent(self):
+        """
+        Read binary data from permanent storage.
+        """
+        response = self.connection.get(self.path)
+        # TODO: Other interface ? return self.connection.getfile()
+        return response
+
+    def downloadFile(self, localFileName):
+        """
+        Copy binary data from permanent storage to a local file.
+        
+        @param localFileName: file to write binary data to
+        """
+        localFile = open(localFileName, 'wb')
+        remoteFile = self.downloadContent()
+        _blockCopyFile(remoteFile, localFile, Connection.blockSize)
+        remoteFile.close()
+        localFile.close()
+
+    def readProperties(self, *names):
+        """
+        Reads the given properties.
+        
+        @param names: a list of property names.
+                      A property name is a (XmlNameSpace, propertyName) tuple.
+        @return: a map from property names to DOM Element or String values.
+        """
+        assert names, "Property names are missing."
+        body = createFindBody(names, self.defaultNamespace)
+        response = self.connection.propfind(self.path, body, depth=0)
+        properties = response.msr.values()[0]
+        if  properties.errorCount > 0:
+            raise WebdavError("Property is missing on '%s': %s" % (self.path, properties.reason), properties.code)
+        return properties
+
+    def readProperty(self, nameSpace, name):
+        """
+        Reads the given property.
+        
+        @param nameSpace: XML-namespace
+        @type nameSpace: string
+        @param name: A property name.
+        @type name: string
+        
+        @return: a map from property names to DOM Element or String values.
+        """
+        results = self.readProperties((nameSpace, name))
+        if  len(results) == 0:
+            raise WebdavError("Property is missing: " + results.reason)
+        return results.values()[0]
+
+    def readAllProperties(self):
+        """
+        Reads all properties of this resource.
+        
+        @return: a map from property names to DOM Element or String values.
+        """
+        response = self.connection.allprops(self.path, depth=0)
+        return response.msr.values()[0]
+
+    def readAllPropertyNames(self):
+        """
+        Returns the names of all properties attached to this resource.
+        
+        @return: List of property names
+        """
+        response = self.connection.propnames(self.path, depth=0)
+        return response.msr.values()[0]
+
+    def readStandardProperties(self):
+        """
+        Read all WebDAV live properties.
+        
+        @return: A L{LiveProperties} instance which contains a getter method for each live property.
+        """
+        body = createFindBody(LiveProperties.NAMES, Constants.NS_DAV)
+        response = self.connection.propfind(self.path, body, depth=0)
+        properties = response.msr.values()[0]
+        return LiveProperties(properties)
+
+    def writeProperties(self, properties, lockToken=None):
+        """
+        Sets or updates the given properties.
+        
+        @param lockToken: if the resource has been locked this is the lock token.
+        @type  lockToken: L{LockToken}
+        @param properties: a map from property names to a String or
+                           DOM element value for each property to add or update.
+        """
+        assert isinstance(properties, types.DictType)
+        assert lockToken == None or isinstance(lockToken, LockToken), \
+                "Invalid lockToken argument %s" % type(lockToken)
+        header = {}
+        if lockToken:
+            header = lockToken.toHeader()
+        body = createUpdateBody(properties, self.defaultNamespace)
+        response = self.connection.proppatch(self.path, body, header)
+        if  response.msr.errorCount > 0:
+            raise WebdavError("Request failed: " + response.msr.reason, response.msr.code)
+
+    def deleteProperties(self, lockToken=None, *names):
+        """
+        Removes the given properties from this resource.
+        
+        @param lockToken: if the resource has been locked this is the lock token.
+        @type  lockToken: L{LockToken}
+        @param names: a collection of property names.
+               A property name is a (XmlNameSpace, propertyName) tuple.
+        """
+        assert lockToken == None or isinstance(lockToken, LockToken), \
+                "Invalid lockToken argument %s" % type(lockToken)
+        header = {}
+        if lockToken:
+            header = lockToken.toHeader()
+        body = createDeleteBody(names, self.defaultNamespace)
+        response = self.connection.proppatch(self.path, body, header)
+        if  response.msr.errorCount > 0:
+            raise WebdavError("Request failed: " + response.msr.reason, response.msr.code)
+
+    # ACP extension
+    def setAcl(self, acl, lockToken=None):
+        """
+        Sets ACEs in the non-inherited and non-protected ACL or the resource.
+        This is the implementation of the ACL method of the WebDAV ACP.
+        
+        @param acl: ACL to be set on resource as ACL object.
+        @param lockToken: If the resource has been locked this is the lock token (defaults to None).
+        @type  lockToken: L{LockToken}
+        """
+        assert lockToken == None or isinstance(lockToken, LockToken), \
+                "Invalid lockToken argument %s" % type(lockToken)
+        headers = {}
+        if lockToken:
+            headers = lockToken.toHeader()
+        headers['Content-Type'] = XML_CONTENT_TYPE
+        body                    = acl.toXML()
+        response = self.connection._request('ACL', self.path, body, headers)
+        return response
+        ## TODO: parse DAV:error response
+
+    def getAcl(self):
+        """
+        Returns this resource's ACL in an ACL instance.
+        
+        @return: Access Control List.
+        @rtype: L{ACL<webdav.acp.Acl.ACL>}
+        """
+        xmlAcl = self.readProperty(Constants.NS_DAV, Constants.TAG_ACL)
+        return ACL(xmlAcl)
+
+    def getCurrentUserPrivileges(self):
+        """
+        Returns a tuple of the current user privileges.
+        
+        @return: list of Privilege instances
+        @rtype: list of L{Privilege<webdav.acp.Privilege.Privilege>}
+        """
+        privileges = self.readProperty(Constants.NS_DAV, Constants.PROP_CURRENT_USER_PRIVILEGE_SET)
+        result = []
+        for child in privileges.children:
+            result.append(Privilege(domroot=child))
+        return result
+    
+    def getPrincipalCollections(self):
+        """
+        Returns a list principal collection URLs.
+        
+        @return: list of principal collection URLs
+        @rtype: C{list} of C{unicode} elements
+        """
+        webdavQueryResult = self.readProperty(Constants.NS_DAV, Constants.PROP_PRINCIPAL_COLLECTION_SET)
+        principalCollectionList = []
+        for child in webdavQueryResult.children:
+            principalCollectionList.append(child.first_cdata)            
+        return principalCollectionList
+    
+    def getOwnerUrl(self):
+        """ Explicitly retireve the Url of the owner. """
+        
+        result = self.readProperty(Constants.NS_DAV, Constants.PROP_OWNER)
+        if result and len(result.children):
+            return result.children[0].textof()
+        return None
+
+class CollectionStorer(ResourceStorer):
+    """
+    This class provides client access to a WebDAV collection resource identified by an URI.
+    This class does not cache resource data. This has to be performed by its clients.
+    
+    @author: Roland Betz
+    """
+        
+    def __init__(self, url, connection=None, validateResourceNames=True):
+        """
+        Creates a CollectionStorer instance for a URL and an optional Connection object.
+        User must invoke validate() after constuction to check the resource on the server.
+        
+        @see: L{webdav.WebdavClient.ResourceStorer.__init__}
+        @param url: unique resource location for this storer
+        @param connection: this optional parameter contains a Connection object for the host part
+            of the given URL. Passing a connection saves memory by sharing this connection.    
+        """
+        if  url[-1] != '/':     # Collection URL must end with slash
+            url += '/'
+        ResourceStorer.__init__(self, url, connection, validateResourceNames)
+    
+    def getResourceStorer(self, name):
+        """
+        Return a ResourceStorer instance for a child resource (member) of this Collection.
+        
+        @param name: leaf name of child resource
+        @return: L{ResourceStorer} instance
+        """
+        assert isinstance(name, types.StringType) or isinstance(name, types.UnicodeType)
+        return ResourceStorer(self.url + name, self.connection, self.validateResourceNames)
+             
+    def validate(self):
+        """
+        Check whether this URL contains a WebDAV collection.
+        Uses the WebDAV OPTION method.
+        
+        @raise WebdavError: L{WebdavError} if URL does not contain a WebDAV collection resource.
+        """
+        super(CollectionStorer, self).validate()
+        isCollection = self.readProperty(Constants.NS_DAV, Constants.PROP_RESOURCE_TYPE)
+        if not (isCollection and isCollection.children):
+            raise WebdavError("Not a collection URL.", 0)        
+        
+    def addCollection(self, name, lockToken=None):
+        """
+        Make a new WebDAV collection resource within this collection.
+        
+        @param name: of the new collection
+        @param lockToken: None or token returned by last lock operation
+        @type  lockToken: L{LockToken}
+        """
+        assert isinstance(name, types.StringType) or isinstance(name, types.UnicodeType)
+        assert lockToken == None or isinstance(lockToken, LockToken), \
+                "Invalid lockToken argument %s" % type(lockToken)
+        header = {}
+        if lockToken:
+            header = lockToken.toHeader()
+        if self.validateResourceNames:
+            validateResourceName(name)
+        if  name[-1] != '/':     # Collection URL must end with slash
+            name += '/'
+        self.connection.mkcol(self.path + name, header)
+        return CollectionStorer(self.url + name, self.connection, self.validateResourceNames) 
+        
+    def addResource(self, name, content=None, properties=None, lockToken=None):
+        """
+        Create a new empty WebDAV resource contained in this collection with the given
+        properties.
+        
+        @param name: leaf name of the new resource    
+        @param content: None or initial binary content of resource
+        @param properties: name/value-map containing properties
+        @param lockToken: None or token returned by last lock operation
+        @type  lockToken: L{LockToken}
+        """
+        assert isinstance(name, types.StringType) or isinstance(name, types.UnicodeType)
+        assert lockToken == None or isinstance(lockToken, LockToken), \
+                "Invalid lockToken argument %s" % type(lockToken)
+        if self.validateResourceNames:
+            validateResourceName(name) # check for invalid characters        
+        resource_ = ResourceStorer(self.url + name, self.connection, self.validateResourceNames)
+        resource_.uploadContent(content, lockToken)
+        if properties:
+            resource_.writeProperties(properties, lockToken)
+        return resource_
+        
+    def deleteResource(self, name, lockToken=None):
+        """
+        Delete a collection which is contained within this collection
+        
+        @param name: leaf name of a contained collection resource
+        @param lockToken: None or token returned by last lock operation
+        @type  lockToken: L{LockToken}
+        """
+        assert isinstance(name, types.StringType) or isinstance(name, types.UnicodeType)
+        assert lockToken == None or isinstance(lockToken, LockToken), \
+                "Invalid lockToken argument %s" % type(lockToken)
+        header = {}
+        if lockToken:
+            header = lockToken.toHeader()
+        if self.validateResourceNames:
+            validateResourceName(name)
+        response = self.connection.delete(self.path + name, header)
+        if  response.status == Constants.CODE_MULTISTATUS and response.msr.errorCount > 0:
+            raise WebdavError("Request failed: %s" % response.msr.reason, response.msr.code)        
+        
+    def lockAll(self, owner):
+        """
+        Locks this collection resource for exclusive write access. This means that for 
+        succeeding write operations the returned lock token has to be passed.
+        The operation is applied recursively to all contained resources.
+        If the methode does not throw an exception then the lock has been granted.
+        
+        @param owner: describes the lock holder
+        @return: Lock token string (automatically generated).
+        @rtype: L{LockToken}
+        """
+        assert isinstance(owner, types.StringType) or isinstance(owner, types.UnicodeType)
+        response = self.connection.lock(self.path, owner, depth=Constants.HTTP_HEADER_DEPTH_INFINITY)
+        return LockToken(self.url, response.locktoken)
+        
+    def listResources(self):
+        """
+        Describe all members within this collection.
+        
+        @return: map from URI to a L{LiveProperties} instance containing the WebDAV
+                 live attributes of the contained resource
+        """
+        # *LiveProperties.NAMES denotes the list of all live properties as an
+        # argument to the method call.
+        response = self.connection.getprops(self.path,
+                                            depth=1,
+                                            ns=Constants.NS_DAV,
+                                            *LiveProperties.NAMES)
+        result = {}
+        for path, properties in response.msr.items():
+            if path == self.path:      # omit this collection resource
+                continue
+            ## some servers do not append a trailing slash to collection paths
+            if self.path.endswith('/') and self.path[0:-1] == path:
+                continue
+            result[path] = LiveProperties(properties=properties)
+        return result
+
+    def getCollectionContents(self):
+        """
+        Return a list of the tuple (resources or collection) / properties)
+
+        @return: a list of the tuple (resources or collection) / properties)
+        @rtype: C{list} 
+        """
+        self.validate()
+        collectionContents = []
+        result = self.listResources()
+        for url, properties_ in result.items():
+            if not self.path == url:
+                if properties_.getResourceType() == 'resource':
+                    myWebDavStorer = ResourceStorer(url, self.connection, self.validateResourceNames)
+                else:
+                    myWebDavStorer = CollectionStorer(url, self.connection, self.validateResourceNames)
+                collectionContents.append((myWebDavStorer, properties_))
+        return collectionContents
+
+    def findProperties(self, *names):
+        """
+        Retrieve given properties for this collection and all directly contained resources.
+        
+        @param names: a list of property names
+        @return: a map from resource URI to a map from property name to value.
+        """
+        assert isinstance(names, types.ListType) or isinstance(names, types.TupleType), \
+                "Argument name has type %s" % str(type(names))
+        body = createFindBody(names, self.defaultNamespace)
+        response = self.connection.propfind(self.path, body, depth=1)
+        return response.msr
+        
+    def deepFindProperties(self, *names):
+        """
+        Retrieve given properties for this collection and all contained (nested) resources.
+        
+        Note:
+        =====
+          This operation can take a long time if used with recursive=true and is therefore
+          disabled on some WebDAV servers.
+        
+        @param names: a list of property names
+        @return: a map from resource URI to a map from property name to value.
+        """
+        assert isinstance(names, types.ListType.__class__) or isinstance(names, types.TupleType), \
+                "Argument name has type %s" % str(type(names))
+        body = createFindBody(names, self.defaultNamespace)
+        response = self.connection.propfind(self.path, body, depth=Constants.HTTP_HEADER_DEPTH_INFINITY)
+        return response.msr
+        
+    def findAllProperties(self):
+        """
+        Retrieve all properties for this collection and all directly contained resources.
+        
+        @return: a map from resource URI to a map from property name to value.
+        """
+        response = self.connection.allprops(self.path, depth=1)
+        return response.msr
+            
+        
+    # DASL extension
+    def search(self, conditions, selects):
+        """
+        Search for contained resources which match the given search condition.
+        
+        @param conditions: tree of ConditionTerm instances representing a logical search term
+        @param selects: list of property names to retrieve for the found resources
+        """
+        assert isinstance(conditions, ConditionTerm)
+        headers = { 'Content-Type' : XML_CONTENT_TYPE, "depth": Constants.HTTP_HEADER_DEPTH_INFINITY}
+        body = createSearchBody(selects, self.path, conditions)
+        response = self.connection._request('SEARCH', self.path, body, headers)
+        return response.msr
+        
+
+class LockToken(object):
+    """
+    This class provides help on handling WebDAV lock tokens.
+    
+    @author: Roland Betz
+    """
+    # restrict instance variables
+    __slots__ = ('url', 'token')
+
+    def __init__(self, url, token):
+        assert isinstance(url, types.StringType) or isinstance(url, types.UnicodeType), \
+            "Invalid url argument %s" % type(url)
+        assert isinstance(token, types.StringType) or isinstance(token, types.UnicodeType), \
+            "Invalid lockToken argument %s" % type(token)
+        self.url = url
+        self.token = token
+
+    def value(self):
+        """
+        Descriptive string containing the lock token's URL and the token itself.
+        
+        @return: Descriptive lock token with URL.
+        @rtype: C{string}
+        """
+        return "<" + self.url + "> (<" + self.token + ">)"
+
+    def toHeader(self):
+        """
+        Header fragment for WebDAV request.
+        
+        @return: Dictionary containing an entry for the lock token query.
+        @rtype: C{dictionary}
+        """
+        return {Constants.HTTP_HEADER_IF: self.value()}
+    
+    def __str__(self):
+        return self.value()
+
+
+def _blockCopyFile(source, dest, blockSize):
+    """
+    Copies a file in chunks of C{blockSize}.
+    
+    @param source: Source file.
+    @type  source: FileIO buffer.
+    @param dest: Destination file.
+    @type  dest: FileIO buffer.
+    @param blockSize: Size of block in bytes.
+    @type  blockSize: C{int}
+    """
+    transferedBytes = 0
+    block = source.read(blockSize)
+    while len(block):
+        dest.write(block)
+        transferedBytes += len(block);
+        block = source.read(blockSize)        
+
+def _checkUrl(url):
+    """
+    Checks the given URL for validity.
+    
+    @param url: URL to check.
+    @type  url: C{string}
+    
+    @raise ValueError: If the URL does not contain valid/usable content.
+    """
+    
+    parts = urlsplit(url, allow_fragments=False)
+    if len(parts[0]) == 0 or len(parts[1]) == 0 or len(parts[2]) == 0:
+        raise ValueError("Invalid URL: " + repr(url))
diff --git a/src/webdav/WebdavRequests.py b/src/webdav/WebdavRequests.py
new file mode 100644
index 0000000..79e586a
--- /dev/null
+++ b/src/webdav/WebdavRequests.py
@@ -0,0 +1,205 @@
+# pylint: disable-msg=W0511,W0212,E1111
+#
+# Copyright 2008 German Aerospace Center (DLR)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+ 
+ 
+"""
+This module handles WebDav server requests.
+"""
+
+
+import types
+from webdav import Constants
+import qp_xml
+from tempfile import TemporaryFile
+
+from davlib import XML_DOC_HEADER
+
+from webdav.NameCheck import validatePropertyName
+
+
+__version__ = "$LastChangedRevision$"
+
+
+## TODO: create a property list class
+    
+class XmlNameSpaceMangler(object):
+    '''
+    Handles WebDav requests.
+    '''
+    
+    # restrict instance variables
+    __slots__ = ('shortcuts', 'defaultNameSpace')
+    
+    def __init__(self, nameList, defaultNameSpace = None):
+        '''
+        
+        @param nameList:
+        @param defaultNameSpace:
+        '''
+        
+        assert isinstance(nameList, types.ListType) or isinstance(nameList, types.TupleType), \
+            "1. argument has wrong type %s" % type(nameList)
+        self.shortcuts = {}
+        self.defaultNameSpace = defaultNameSpace
+        for name in nameList:
+            if  not isinstance(name, types.TupleType):
+                name = (defaultNameSpace, name)            
+            assert isinstance(name, types.TupleType) and len(name) == 2, \
+                         "Name is not a namespace, name tuple: %s" % type(name)
+            validatePropertyName(name[1])
+            if  name[0] and not self.shortcuts.has_key(name[0]):
+                self.shortcuts[name[0]] = 'ns%d' % len(self.shortcuts)
+    
+    def getNameSpaces(self):
+        '''
+        Returns the namespace.
+        '''
+        
+        result = ""        
+        for namespace, short in self.shortcuts.items():
+            result += ' xmlns:%s="%s"' % (short, namespace)
+        return result
+    
+    def getUpdateElements(self, valueMap):
+        '''
+        
+        @param valueMap:
+        '''
+        
+        elements = ""
+        for name in valueMap.keys():
+            fullname = name
+            if  isinstance(name, types.StringType):
+                fullname = (self.defaultNameSpace, name)        
+            if  not fullname[0]:
+                tag = fullname[1]        
+            else:
+                tag = self.shortcuts[fullname[0]] + ':' + fullname[1]
+            value = valueMap[name]
+            if value:
+                if isinstance(value, qp_xml._element):
+                    tmpFile = TemporaryFile('w+')
+                    value = qp_xml.dump(tmpFile, value)
+                    tmpFile.flush()
+                    tmpFile.seek(0)
+                    tmpFile.readline()
+                    value = tmpFile.read()
+                else:
+                    value = "<![CDATA[%s]]>" % value
+            else:
+                value = ""
+            elements += "<%s>%s</%s>" % (tag, value, tag)
+        return elements
+    
+    def getNameElements(self, nameList):
+        '''
+        
+        @param nameList:
+        '''
+        
+        elements = ""
+        for name in nameList:
+            if  isinstance(name, types.StringType):
+                name = (self.defaultNameSpace, name)        
+            if  not name[0]:
+                tag = name[1]        
+            else:
+                tag = self.shortcuts[name[0]] + ':' + name[1]
+            elements += "<%s />" % tag
+        return elements
+                         
+
+
+def createUpdateBody(propertyDict, defaultNameSpace = None):
+    '''
+    
+    @param propertyDict:
+    @param defaultNameSpace:
+    '''
+    
+    updateTag = 'D:' + Constants.TAG_PROPERTY_UPDATE
+    setTag = 'D:' + Constants.TAG_PROPERTY_SET
+    propTag = 'D:' + Constants.TAG_PROP
+    mangler = XmlNameSpaceMangler(propertyDict.keys(), defaultNameSpace)
+    return XML_DOC_HEADER + \
+        '<%s xmlns:D="DAV:"><%s><%s %s>' % (updateTag, setTag, propTag, mangler.getNameSpaces()) + \
+        mangler.getUpdateElements(propertyDict) + \
+        '</%s></%s></%s>' % (propTag, setTag, updateTag)
+
+        
+def createDeleteBody(nameList, defaultNameSpace = None):
+    '''
+    
+    @param nameList:
+    @param defaultNameSpace:
+    '''
+    
+    updateTag = 'D:' + Constants.TAG_PROPERTY_UPDATE
+    removeTag = 'D:' + Constants.TAG_PROPERTY_REMOVE
+    propTag = 'D:' + Constants.TAG_PROP
+    mangler = XmlNameSpaceMangler(nameList, defaultNameSpace)
+    return XML_DOC_HEADER + \
+        '<%s xmlns:D="DAV:"><%s><%s %s>' % (updateTag, removeTag, propTag, mangler.getNameSpaces()) + \
+        mangler.getNameElements(nameList) + \
+        '</%s></%s></%s>' % (propTag, removeTag, updateTag)
+        
+        
+def createFindBody(nameList, defaultNameSpace = None):
+    '''
+    
+    @param nameList:
+    @param defaultNameSpace:
+    '''
+    
+    findTag = 'D:' + Constants.TAG_PROPERTY_FIND
+    propTag = 'D:' + Constants.TAG_PROP
+    mangler = XmlNameSpaceMangler(nameList, defaultNameSpace)
+    return XML_DOC_HEADER + \
+        '<%s xmlns:D="DAV:"><%s %s>' % (findTag, propTag, mangler.getNameSpaces()) + \
+        mangler.getNameElements(nameList) + \
+        '</%s></%s>' % (propTag, findTag)
+        
+        
+def createSearchBody(selects, path, conditions, defaultNameSpace = None):
+    '''
+    Creates DASL XML body.
+    
+    @param selects: list of property names to retrieve for the found resources
+    @param path: list of conditions
+    @param conditions: tree of ConditionTerm instances representing a logical search term
+    @param defaultNameSpace: default namespace
+    '''
+    
+    searchTag = 'D:' + Constants.TAG_SEARCH_REQUEST
+    basicTag = 'D:' + Constants.TAG_SEARCH_BASIC
+    selectTag = 'D:' + Constants.TAG_SEARCH_SELECT
+    fromTag = 'D:' + Constants.TAG_SEARCH_FROM
+    scopeTag = 'D:' + Constants.TAG_SEARCH_SCOPE
+    whereTag = 'D:' + Constants.TAG_SEARCH_WHERE
+    propTag = 'D:' + Constants.TAG_PROP
+    hrefTag = 'D:' + Constants.TAG_HREF
+    depthTag = 'D:' + Constants.TAG_LOCK_DEPTH
+    depthValue = Constants.HTTP_HEADER_DEPTH_INFINITY
+    mangler = XmlNameSpaceMangler(selects, defaultNameSpace)
+    return XML_DOC_HEADER + \
+        '<%s xmlns:D="DAV:"><%s>' % (searchTag, basicTag) + \
+        '<%s><%s %s>%s</%s></%s>' % (selectTag, propTag, mangler.getNameSpaces(), 
+                                     mangler.getNameElements(selects), propTag, selectTag) + \
+        '<%s><%s><%s>%s</%s><%s>%s</%s></%s></%s>' % (fromTag, scopeTag, hrefTag, path, hrefTag, 
+                                                      depthTag, depthValue, depthTag, scopeTag, fromTag) + \
+        '<%s>%s</%s>' % (whereTag, conditions.toXML(),whereTag) + \
+        '</%s></%s>' % (basicTag, searchTag)
+        
\ No newline at end of file
diff --git a/src/webdav/WebdavResponse.py b/src/webdav/WebdavResponse.py
new file mode 100644
index 0000000..c84943d
--- /dev/null
+++ b/src/webdav/WebdavResponse.py
@@ -0,0 +1,525 @@
+# pylint: disable-msg=R0903,W0142,W0221,W0212,W0104,W0511,C0103,R0901
+#
+# Copyright 2008 German Aerospace Center (DLR)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+"""
+Handles WebDAV responses.
+"""
+
+
+from davlib import _parse_status
+import qp_xml
+from webdav import Constants
+import time
+import rfc822
+import urllib
+# Handling Jython 2.5 bug concerning the date pattern
+# conversion in time.strptime
+try:
+    from java.lang import IllegalArgumentException
+except ImportError:
+    class IllegalArgumentException(object):
+        pass
+
+
+__version__ = "$LastChangedRevision$"
+
+
+class HttpStatus(object):
+    """
+    TBD
+    
+    @ivar code:
+    @type code:
+    @ivar reason:
+    @type reason:
+    @ivar errorCount:
+    @type errorCount: int
+    """
+    
+    def __init__(self, elem):
+        """
+        TBD
+        
+        @param elem: ...
+        @type elem: instance of L{Element}
+        """
+        self.code, self.reason = _parse_status(elem)
+        self.errorCount = (self.code >= Constants.CODE_LOWEST_ERROR)
+    def __str__(self):
+        return "HTTP status %d: %s" % (self.code, self.reason)
+
+        
+class MultiStatusResponse(dict):
+    """
+    TBD
+    
+    @ivar status:
+    @type status:
+    @ivar reason:
+    @type reason:
+    @ivar errorCount:
+    @type errorCount:
+    """
+    
+    # restrict instance variables
+    __slots__ = ('errorCount', 'reason', 'status')
+
+    def __init__(self, domroot):
+        dict.__init__(self)
+        self.errorCount = 0
+        self.reason = None
+        self.status = Constants.CODE_MULTISTATUS
+        if (domroot.ns != Constants.NS_DAV) or (domroot.name != Constants.TAG_MULTISTATUS):
+            raise ResponseFormatError(domroot, 'Invalid response: <DAV:multistatus> expected.')
+        self._scan(domroot)
+    
+    def getCode(self):
+        if self.errorCount == 0:
+            return Constants.CODE_SUCCEEDED
+        if  len(self) > self.errorCount:
+            return Constants.CODE_MULTISTATUS
+        return self.values()[0].code
+
+    def getReason(self):
+        result = ""
+        for response in self.values():
+            if response.code > Constants.CODE_LOWEST_ERROR:
+                result += response.reason
+        return result                    
+    
+    def __str__(self):
+        result = ""
+        for key, value in self.items():
+            if  isinstance(value, PropertyResponse):
+                result += "Resource at %s has %d properties and %d errors.\n" % (key, len(value), value.errorCount)
+            else:
+                result += "Resource at %s returned " % key + str(value)
+        return result
+    
+    def _scan(self, root):
+        for child in root.children:
+            if child.ns != Constants.NS_DAV:
+                continue
+            if child.name == Constants.TAG_RESPONSEDESCRIPTION:
+                self.reason = child.textof()
+            elif child.name == Constants.TAG_RESPONSE:
+                self._scanResponse(child)
+            ### unknown child element
+        
+    def _scanResponse(self, elem):
+        hrefs = []
+        response = None
+        for child in elem.children:
+            if child.ns != Constants.NS_DAV:
+                continue
+            if child.name == Constants.TAG_HREF:
+                try:
+                    href = _unquoteHref(child.textof())
+                except UnicodeDecodeError:
+                    raise ResponseFormatError(child, "Invalid 'href' data encoding.")
+                hrefs.append(href)
+            elif child.name == Constants.TAG_STATUS:
+                self._scanStatus(child, *hrefs)
+            elif child.name == Constants.TAG_PROPERTY_STATUS:
+                if not response:
+                    if len(hrefs) != 1:
+                        raise ResponseFormatError(child, 'Invalid response: One <DAV:href> expected.')
+                    response = PropertyResponse()                    
+                    self[hrefs[0]] = response                    
+                response._scan(child)
+            elif child.name == Constants.TAG_RESPONSEDESCRIPTION:
+                for href in hrefs:
+                    self[href].reasons.append(child.textOf())            
+            ### unknown child element
+        if response and response.errorCount > 0:
+            self.errorCount += 1
+                                    
+    def _scanStatus(self, elem, *hrefs):
+        if  len(hrefs) == 0:
+            raise ResponseFormatError(elem, 'Invalid response: <DAV:href> expected.')
+        status = HttpStatus(elem)
+        for href in hrefs:
+            self[href] = status
+            if  status.errorCount:
+                self.errorCount += 1
+
+    # Instance properties
+    code = property(getCode, None, None, "HTTP response code")
+
+
+
+class PropertyResponse(dict):
+    """
+    TBD
+    
+    @ivar errors:
+    @type errors: list of ...
+    @ivar reasons:
+    @type reasons: list of ...
+    @ivar failedProperties:
+    @type failedProperties: dict of ...
+    """
+
+    # restrict instance variables
+    __slots__ = ('errors', 'reasons', 'failedProperties')
+
+    def __init__(self):
+        dict.__init__(self)
+        self.errors = []
+        self.reasons = []
+        self.failedProperties = {}
+
+    def __str__(self):
+        result = ""
+        for value in self.values():
+            result += value.name + '= ' + value.textof() + '\n'
+        result += self.getReason()
+        return result
+        
+    def getCode(self):
+        if  len(self.errors) == 0:
+            return Constants.CODE_SUCCEEDED
+        if  len(self) > 0:
+            return Constants.CODE_MULTISTATUS
+        return self.errors[-1].code       
+
+    def getReason(self):
+        result = ""
+        if len(self.errors) > 0:
+            result = "Failed for: "   + repr(self.failedProperties.keys()) + "\n"
+        for error in self.errors:
+            result += "%s (%d).  " % (error.reason, error.code)
+        for reason in self.reasons:
+            result += "%s.  " % reason
+        return result
+        
+    def _scan(self, element):
+        status = None
+        statusElement = element.find(Constants.TAG_STATUS, Constants.NS_DAV)
+        if statusElement:
+            status = HttpStatus(statusElement)
+            if status.errorCount:
+                self.errors.append(status)
+        
+        propElement = element.find(Constants.TAG_PROP, Constants.NS_DAV)
+        if propElement:
+            for prop in propElement.children:
+                if status.errorCount:
+                    self.failedProperties[(prop.ns, prop.name)]= status
+                else:
+                    prop.__class__ = Element     # bad, bad trick
+                    self[prop.fullname] = prop
+        reasonElement = element.find(Constants.TAG_RESPONSEDESCRIPTION, Constants.NS_DAV)
+        if reasonElement:
+            self.reasons.append(reasonElement.textOf())
+        
+    # Instance properties
+    code = property(getCode, None, None, "HTTP response code")
+    errorCount = property(lambda self: len(self.errors), None, None, "HTTP response code")
+    reason = property(getReason, None, None, "HTTP response code")
+
+
+
+        
+class LiveProperties(object):
+    """
+    This class provides convenient access to the WebDAV 'live' properties of a resource.
+    WebDav 'live' properties are defined in RFC 2518, Section 13. 
+    Each property is converted from string to its natural data type.
+    
+    @version: $Revision$
+    @author: Roland Betz
+    """
+    
+    # restrict instance variables
+    __slots__ = ('properties')
+
+    NAMES = (Constants.PROP_CREATION_DATE, Constants.PROP_DISPLAY_NAME,
+             Constants.PROP_CONTENT_LENGTH, Constants.PROP_CONTENT_TYPE, Constants.PROP_ETAG,
+             Constants.PROP_LAST_MODIFIED, Constants.PROP_OWNER,
+             Constants.PROP_LOCK_DISCOVERY, Constants.PROP_RESOURCE_TYPE, Constants.PROP_SUPPORTED_LOCK )
+     
+    def __init__(self, properties=None, propElement=None):
+        """
+        Construct <code>StandardProperties</code> from a map of properties containing
+        live properties or from a XML 'prop' element containing live properties
+        
+        @param properties: map as implemented by class L{PropertyResponse}
+        @param propElement: an C{Element} value
+        """
+        assert isinstance(properties, PropertyResponse) or \
+               isinstance(propElement, qp_xml._element), \
+                "Argument properties has type %s" % str(type(properties))
+        self.properties = {}
+        for name, value in properties.items():
+            if  name[0] == Constants.NS_DAV  and  name[1] in self.NAMES:
+                self.properties[name[1]] = value
+
+    def getContentLanguage(self):
+        """
+        Return the language of a resource's textual content or null
+        
+        @return: string
+        """
+        
+        result = ""
+        if not self.properties.get(Constants.PROP_CONTENT_LANGUAGE, None) is None:
+            result = self.properties.get(Constants.PROP_CONTENT_LANGUAGE).textof()
+        return result
+
+    def getContentLength(self):
+        """
+        Returns the length of the resource's content in bytes.
+        
+        @return: number of bytes
+        """
+        
+        result = 0
+        if not self.properties.get(Constants.PROP_CONTENT_LENGTH, None) is None:
+            result = int(self.properties.get(Constants.PROP_CONTENT_LENGTH).textof())
+        return result
+
+    def getContentType(self):
+        """
+        Return the resource's content MIME type.
+        
+        @return: MIME type string
+        """
+        
+        result = ""
+        if not self.properties.get(Constants.PROP_CONTENT_TYPE, None) is None:
+            result = self.properties.get(Constants.PROP_CONTENT_TYPE).textof()
+        return result
+
+    def getCreationDate(self):
+        """
+        Return date of creation as time tuple.
+                
+        @return: time tuple
+        @rtype: C{time.struct_time}
+        
+        @raise ValueError: If string is not in the expected format (ISO 8601).
+        """
+        
+        datetimeString = ""
+        if not self.properties.get(Constants.PROP_CREATION_DATE, None) is None:
+            datetimeString = self.properties.get(Constants.PROP_CREATION_DATE).textof()
+
+        result = rfc822.parsedate(datetimeString)
+        if result is None:
+            result = _parseIso8601String(datetimeString)
+
+        return time.mktime(result)
+    
+    def getEntityTag(self):
+        """
+        Return a entity tag which is unique for a particular version of a resource.
+        Different resources or one resource before and after modification have different etags. 
+        
+        @return: entity tag string
+        """
+        
+        result = ""
+        if not self.properties.get(Constants.PROP_ETAG, None) is None:
+            result = self.properties.get(Constants.PROP_ETAG).textof()
+        return result
+            
+    def getDisplayName(self):
+        """
+        Returns a resource's display name.
+        
+        @return: string
+        """
+        
+        result = ""
+        if not self.properties.get(Constants.PROP_DISPLAY_NAME, None) is None:
+            result = self.properties.get(Constants.PROP_DISPLAY_NAME).textof()
+        return result
+
+    def getLastModified(self):
+        """
+        Return last modification of resource as time tuple.
+        
+        @return: Modification date time.
+        @rtype:  C{time.struct_time}
+        
+        @raise ValueError: If the date time string is not in the expected format (RFC 822 / ISO 8601).
+        """
+        
+        datetimeString = None
+        if not self.properties.get(Constants.PROP_LAST_MODIFIED, None) is None:
+            datetimeString = self.properties.get(Constants.PROP_LAST_MODIFIED).textof()
+        result = rfc822.parsedate(datetimeString)
+        if result is None:
+            result = _parseIso8601String(datetimeString)
+        return time.mktime(result)
+
+    def getLockDiscovery(self):
+        """
+        Return all current lock's applied to a resource or null if it is not locked.
+        
+        @return: a lockdiscovery DOM element according to RFC 2815
+        """
+        
+        xml = self.properties.get(Constants.PROP_LOCK_DISCOVERY)
+        return _scanLockDiscovery(xml)
+
+    def getResourceType(self):
+        """
+        Return a resource's WebDAV type.
+        
+        @return: 'collection' or 'resource'
+        """
+        
+        xml = self.properties.get(Constants.PROP_RESOURCE_TYPE)
+        if xml and xml.children:
+            return xml.children[0].name
+        return "resource"
+
+    def getSupportedLock(self):
+        """
+        Return a DOM element describing all supported lock options for a resource.
+        Usually this is shared and exclusive write lock.
+        
+        @return: supportedlock DOM element according to RFC 2815
+        """
+        
+        xml = self.properties.get(Constants.PROP_SUPPORTED_LOCK)
+        return xml
+
+    def getOwnerAsUrl(self):
+        """
+        Return a resource's owner in form of a URL.
+        
+        @return: string
+        """
+        
+        xml = self.properties.get(Constants.PROP_OWNER)
+        if xml and len(xml.children):
+            return xml.children[0].textof()
+        return None
+
+    def __str__(self):
+        result = ""
+        result += " Name=" + self.getDisplayName()
+        result += "\n Type=" + self.getResourceType()
+        result += "\n Length=" + str(self.getContentLength())
+        result += "\n Content Type="+ self.getContentType()
+        result += "\n ETag=" + self.getEntityTag()
+        result += "\n Created=" + time.strftime("%c GMT", self.getCreationDate())
+        result += "\n Modified=" + time.strftime("%c GMT", self.getLastModified())
+        return result
+
+
+def _parseIso8601String(date):
+    """ 
+    Parses the given ISO 8601 string and returns a time tuple.
+    The strings should be formatted according to RFC 3339 (see section 5.6).
+    But currently there are two exceptions:
+        1. Time offset is limited to "Z".
+        2. Fragments of seconds are ignored.
+    """
+    
+    if "." in date and "Z" in date: # Contains fragments of second?
+        secondFragmentPos = date.rfind(".")
+        timeOffsetPos = date.rfind("Z")
+        date = date[:secondFragmentPos] + date[timeOffsetPos:]
+    try:
+        timeTuple = time.strptime(date, Constants.DATE_FORMAT_ISO8601)
+    except IllegalArgumentException: # Handling Jython 2.5 bug concerning the date pattern accordingly
+        import _strptime # Using the Jython fall back solution directly
+        timeTuple = _strptime.strptime(date, Constants.DATE_FORMAT_ISO8601)
+    return timeTuple
+
+
+class ResponseFormatError(IOError):
+    """
+    An instance of this class is raised when the web server returned a webdav
+    reply which does not adhere to the standard and cannot be recognized.
+    """
+    def __init__(self, element, message= None):
+        IOError.__init__(self, "ResponseFormatError at element %s: %s"  % (element.name, message))
+        self.element = element
+        self.message = message
+        
+    
+class Element(qp_xml._element):
+    """
+    This class improves the DOM interface (i.e. element interface) provided by the qp_xml module
+    TODO: substitute qp_xml by 'real' implementation. e.g. domlette
+    """
+    def __init__(self, namespace, name, cdata=''):
+        qp_xml._element.__init__(self, ns=namespace, name=name, lang=None, parent=None,
+                    children=[], ns_scope={}, attrs={},
+                    first_cdata=cdata, following_cdata='')
+                    
+    def __str__(self):
+        return self.textof()
+        
+    def __getattr__(self, name):
+        if  (name == 'fullname'):
+            return (self.__dict__['ns'], self.__dict__['name'])
+        raise AttributeError, name
+
+    def add(self, child):
+        self.children.append(child)
+        return child
+
+def _scanLockDiscovery(root):
+    assert root.name == Constants.PROP_LOCK_DISCOVERY, "Invalid lock discovery XML element"
+    active = root.find(Constants.TAG_ACTIVE_LOCK, Constants.NS_DAV)
+    if active:
+        return _scanActivelock(active)
+    return None
+    
+def _scanActivelock(root):
+    assert root.name == Constants.TAG_ACTIVE_LOCK, "Invalid active lock XML element"
+    token = _scanOrError(root, Constants.TAG_LOCK_TOKEN)
+    value = _scanOrError(token, Constants.TAG_HREF)
+    owner = _scanOwner(root)
+    depth = _scanOrError(root, Constants.TAG_LOCK_DEPTH)
+    return (value.textof(), owner, depth.textof())
+
+def _scanOwner(root):
+    owner = root.find(Constants.TAG_LOCK_OWNER, Constants.NS_DAV)
+    if owner:
+        href = owner.find(Constants.TAG_HREF, Constants.NS_DAV)
+        if href:
+            return href.textof()
+        return owner.textof()
+    return None
+    
+def _scanOrError(elem, childName):
+    child = elem.find(childName, Constants.NS_DAV)
+    if not child:
+        raise ResponseFormatError(elem, "Invalid response: <"+childName+"> expected")
+    return child
+    
+         
+def _unquoteHref(href):
+    #print "*** Response HREF=", repr(href)
+    if type(href) == type(u""):
+        try: 
+            href = href.encode('ascii')
+        except UnicodeError:    # URL contains unescaped non-ascii character
+            # handle bug in Tamino webdav server
+            return urllib.unquote(href)
+    href = urllib.unquote(href)
+    if Constants.CONFIG_UNICODE_URL:
+        return unicode(href, 'utf-8')
+    else:
+        return unicode(href, 'latin-1')
diff --git a/src/webdav/__init__.py b/src/webdav/__init__.py
new file mode 100644
index 0000000..3e46609
--- /dev/null
+++ b/src/webdav/__init__.py
@@ -0,0 +1,16 @@
+# Copyright 2008 German Aerospace Center (DLR)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+__version__ = "$LastChangedRevision$"
diff --git a/src/webdav/acp/Ace.py b/src/webdav/acp/Ace.py
new file mode 100644
index 0000000..8321d41
--- /dev/null
+++ b/src/webdav/acp/Ace.py
@@ -0,0 +1,293 @@
+# Copyright 2008 German Aerospace Center (DLR)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+"""
+ACE object handling according to WebDAV ACP specification.
+"""
+
+
+from webdav.acp.Principal import Principal
+from webdav.acp.GrantDeny import GrantDeny
+from webdav import Constants
+from webdav.Connection import WebdavError
+
+
+__version__ = "$LastChangedRevision$"
+
+
+class ACE(object):
+    """
+    This class provides functionality for handling ACEs
+    
+    @ivar principal:   A principal (user or group)
+    @type principal:   L{Principal} object
+    @ivar invert:      Flag indicating whether ACE should invert the principal.
+    @type invert:      C{bool}
+    @ivar grantDenies: Grant or deny clauses for privileges
+    @type grantDenies: C{list} of L{GrantDeny} objects
+    @ivar protected:   Flag indicating whether ACE is protected.
+    @type protected:   C{bool}
+    @ivar inherited:   URL indicating the source from where the ACE is inherited.
+    @type inherited:   C{string}
+    """
+    
+    # restrict instance variables
+    __slots__ = ('principal', 'invert', 'grantDenies', 'protected', 'inherited')
+    
+    def __init__(self, domroot=None, principal=None, grantDenies=None):
+        """
+        Constructor should be called with either no parameters (create blank ACE),
+        one parameter (a DOM tree or principal), or two parameters (principal and 
+        sequence of GrantDenies).
+        
+        @param domroot:     A DOM tree (default: None).
+        @type  domroot:     L{webdav.WebdavResponse.Element} object
+        @param principal:   A principal (user or group), (default: None).
+        @type  principal:   L{Principal} object
+        @param grantDenies: Grant and deny clauses for privileges (default: None).
+        @type  grantDenies: sequence of L{GrantDeny} objects
+        
+        @raise WebdavError: When non-valid parameters are passed a L{WebdavError} is raised.
+        """
+        self.principal   = Principal()
+        self.protected   = None
+        self.inherited   = None
+        self.invert      = None
+        self.grantDenies = []
+
+        if domroot:
+            self.principal   = Principal(domroot=domroot.find(Constants.TAG_PRINCIPAL, Constants.NS_DAV))
+            self.inherited   = domroot.find(Constants.TAG_INHERITED, Constants.NS_DAV)
+            if self.inherited:
+                self.inherited = self.inherited.children[0].textof()
+            if domroot.find(Constants.TAG_PROTECTED, Constants.NS_DAV):
+                self.protected = 1
+            for child in domroot.children:
+                if child.ns == Constants.NS_DAV \
+                        and (child.name == Constants.TAG_GRANT or child.name == Constants.TAG_DENY):
+                    self.grantDenies.append(GrantDeny(domroot=child))
+        elif isinstance(principal, Principal):
+            newPrincipal = Principal()
+            newPrincipal.copy(principal)
+            self.principal = newPrincipal
+            if (isinstance(grantDenies, list) or isinstance(grantDenies, tuple)):
+                self.addGrantDenies(grantDenies)
+        elif domroot == None and grantDenies == None:
+            # no param ==> blank ACE
+            pass
+        else:
+            # This shouldn't happen, someone screwed up with the params ...
+            raise WebdavError('non-valid parameters handed to ACE constructor')
+
+    def __cmp__(self, other):
+        if not isinstance(other, ACE):
+            return 1
+        if self.principal == other.principal \
+                and self.invert == other.invert \
+                and self.protected == other.protected \
+                and self.inherited == other.inherited:
+            equal = 1
+            for grantDeny in self.grantDenies:
+                inList = 0
+                for otherGrantDeny in other.grantDenies:
+                    if grantDeny == otherGrantDeny:
+                        inList = 1
+                if inList == 0:
+                    equal = 0
+            return not equal
+        else:
+            return 1
+
+    def __repr__(self):
+        repr = '<class ACE: '
+        if self.invert:
+            repr += 'inverted principal, ' % (self.invert)
+        if self.principal:
+            repr += 'principal: %s, ' % (self.principal)
+        if self.protected:
+            repr += 'protected, '
+        if self.inherited:
+            repr += 'inherited href: %s, ' % (self.inherited)
+        first = 1
+        repr += 'grantDenies: ['
+        for grantDeny in self.grantDenies:
+            if first:
+                repr += '%s' % grantDeny
+                first = 0
+            else:
+                repr += ', %s' % grantDeny
+        return '%s]>' % (repr)
+
+    def copy(self, other):
+        '''Copy an ACE object.
+        
+        @param other: Another ACE to copy.
+        @type  other: L{ACE} object
+        
+        @raise WebdavError: When an object that is not an L{ACE} is passed 
+            a L{WebdavError} is raised.
+        '''
+        if not isinstance(other, ACE):
+            raise WebdavError('Non-ACE object passed to copy method: %s.' % other.__class__)
+        self.invert    = other.invert
+        self.protected = other.protected
+        self.inherited = other.inherited
+        self.principal = Principal()
+        if other.principal:
+            self.principal.copy(other.principal)
+        if other.grantDenies:
+            self.addGrantDenies(other.grantDenies)
+
+    def isValid(self):
+        """
+        Returns true/false (1/0) whether necessarry props 
+        principal and grantDenies are set and whether the ACE contains one 
+        grant or deny clauses.
+        
+        @return: Validity of ACE.
+        @rtype:  C{bool}
+        """
+        return self.principal and len(self.grantDenies) == 1
+
+    def isGrant(self):
+        '''
+        Returns true/false (1/0) if ACE contains only grant clauses.
+        
+        @return: Value whether the ACE is of grant type.
+        @rtype:  C{bool}
+        '''
+        if self.isMixed() or len(self.grantDenies) < 1:
+            return 0
+        else:
+            return self.grantDenies[0].isGrant()
+
+    def isDeny(self):
+        '''
+        Returns true/false (1/0) if ACE contains only deny clauses.
+        
+        @return: Value whether the ACE is of deny type.
+        @rtype:  C{bool}
+        '''
+        if self.isMixed() or len(self.grantDenies) < 1:
+            return 0
+        else:
+            return self.grantDenies[0].isDeny()
+
+    def isMixed(self):
+        '''
+        Returns true/false (1/0) if ACE contains both types (grant and deny) of clauses.
+        
+        @return: Value whether the ACE is of mixed (grant and deny) type.
+        @rtype:  C{bool}
+        '''
+        mixed = 0
+        if len(self.grantDenies):
+            first = self.grantDenies[0].grantDeny
+            for grantDeny in self.grantDenies:
+                if grantDeny.grantDeny != first:
+                    mixed = 1
+        return mixed
+
+    def toXML(self, defaultNameSpace=None):
+        """
+        Returns ACE content as a string of valid XML as described in WebDAV ACP.
+        
+        @param defaultNameSpace: Name space (default: None).
+        @type  defaultNameSpace: C(string)
+        """
+        assert self.isValid(), "ACE is not initialized or does not contain valid content!"
+        
+        ACE = 'D:' + Constants.TAG_ACE
+        res = self.principal.toXML(self.invert)
+        for grantDeny in self.grantDenies:
+            res += grantDeny.toXML()
+        if self.protected:
+            res += '<D:protected/>'
+        if self.inherited:
+            res += '<D:inherited><D:href>%s</D:href></D:inherited>' % (self.inherited)
+        return '<%s>%s</%s>' % (ACE, res, ACE)
+
+    def setPrincipal(self, principal):
+        '''
+        Sets the passed principal on the ACE.
+        
+        @param principal: A principal.
+        @type  principal: L{Principal} object
+        '''
+        self.principal = Principal()
+        self.principal.copy(principal)
+
+    def setInherited(self, href):
+        '''
+        Sets the passed URL on the ACE to denote from where it is inherited.
+        
+        @param href: A URL.
+        @type  href: C{string}
+        '''
+        self.inherited = href
+
+    def addGrantDeny(self, grantDeny):
+        '''
+        Adds the passed GrantDeny object to list if it's not in it, yet.
+        
+        @param grantDeny: A grant or deny clause.
+        @type  grantDeny: L{GrantDeny} object
+        '''
+        # only add it if it's not in the list, yet ...
+        inList = 0
+        for element in self.grantDenies:
+            if element == grantDeny:
+                inList = 1
+        if not inList:
+            newGrantDeny = GrantDeny()
+            newGrantDeny.copy(grantDeny)
+            self.grantDenies.append(newGrantDeny)
+
+    def addGrantDenies(self, grantDenies):
+        '''Adds the list of passed grant/deny objects to list.
+        
+        @param grantDenies: Grant or deny clauses.
+        @type  grantDenies: sequence of L{GrantDeny} objects
+        '''
+        map(lambda grantDeny: self.addGrantDeny(grantDeny), grantDenies)
+
+    def delGrantDeny(self, grantDeny):
+        '''Deletes the passed GrantDeny object from list.
+        
+        @param grantDeny: A grant or deny clause.
+        @type  grantDeny: L{GrantDeny} object
+        
+        @raise WebdavError: A L{WebdavError} is raised if the clause to be 
+            deleted is not present.
+        '''
+        # only add it if it's not in the list, yet ...
+        count = 0
+        index = 0
+        for element in self.grantDenies:
+            count += 1
+            if element == grantDeny:
+                index = count
+        if index:
+            self.grantDenies.pop(index - 1)
+        else:
+            raise WebdavError('GrantDeny to be deleted not in list: %s.' % grantDeny)
+
+    def delGrantDenies(self, grantDenies):
+        '''Deletes the list of passed grant/deny objects from list.
+        
+        @param grantDenies: Grant or deny clauses.
+        @type  grantDenies: sequence of L{GrantDeny} objects
+        '''
+        map(lambda grantDeny: self.delGrantDeny(grantDeny), grantDenies)
diff --git a/src/webdav/acp/AceHandler.py b/src/webdav/acp/AceHandler.py
new file mode 100644
index 0000000..e07b74d
--- /dev/null
+++ b/src/webdav/acp/AceHandler.py
@@ -0,0 +1,182 @@
+# Copyright 2008 German Aerospace Center (DLR)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+"""
+Handling of WebDAV Access Protocol Extensions and ACL preparation for UI.
+"""
+
+
+from webdav import Constants
+from webdav.WebdavClient import ResourceStorer
+from webdav.Connection import WebdavError
+
+
+__version__ = "$LastChangedRevision$"
+
+
+def extractSupportedPrivilegeSet(userPrivileges):
+    """
+    Returns a dictionary of supported privileges.
+    
+    @param userPrivileges: A DOM tree.
+    @type  userPrivileges: L{webdav.WebdavResponse.Element} object
+    
+    @raise WebdavError: When unknown elements appear in the 
+        C{DAV:supported-privilege} appear a L{WebdavError} is raised.
+    
+    @return: A dictionary with privilege names as keys and privilege descriptions as values.
+    @rtype:  C{dictionary}
+    """
+    result = {}
+    for element in userPrivileges.children:
+        if element.name == Constants.TAG_SUPPORTED_PRIVILEGE:
+            privName        = ''
+            privDescription = ''
+            for privilege in element.children:
+                if privilege.name == Constants.TAG_PRIVILEGE:
+                    privName = privilege.children[0].name
+                elif privilege.name == Constants.TAG_DESCRIPTION:
+                    privDescription = privilege.textof()
+                else:
+                    raise WebdavError('Unknown element in DAV:supported-privilege: ' + privilege.name)
+                
+                if privName and privDescription:
+                    result[privName] = privDescription
+                    privName        = ''
+                    privDescription = ''
+        else:
+            raise WebdavError('Invalid element tag in DAV:supported-privilege-set: ' + element.name)
+    return result
+
+
+def _insertAclDisplaynames(acl):
+    """
+    Modifies the ACL by adding the human readable names 
+    (DAV:displayname property) of each principal found in an ACL.
+    
+    This should be done with the REPORT method, but it is not supported by 
+    Jacarta Slide, yet. (As of Aug. 1, 2003 in CVS repository)
+    
+    So we are going to do it differently by foot the harder way ...
+    
+    @param acl: An ACL object for which the displaynames should be retrieved.
+    @type  acl: L{ACL} object
+    """
+    ## This is redundant code to be still kept for the REPORT method way of doing it ...
+    ## property = '''<D:prop><D:displayname/></D:prop>'''
+    ## return self.getReport(REPORT_ACL_PRINCIPAL_PROP_SET, property)
+    for ace in acl.aces:
+        if not ace.principal.property:
+            principalConnection = \
+                ResourceStorer(ace.principal.principalURL)
+            ace.principal.displayname = \
+                principalConnection.readProperty(Constants.NS_DAV, Constants.PROP_DISPLAY_NAME)
+
+
+def prepareAcls(acls):
+    """
+    Returns all ACLs describing the behaviour of the resource. The information 
+    in the ACL is modified to contain all information needed to display in the UI.
+    
+    @param acls: ACL objects.
+    @type  acls: C{list} of L{ACL} objects
+    
+    @return: (non-valid) ACLs that contain both grant and deny clauses in an ACE.
+        Displaynames are added to the Principals where needed.
+    @rtype:  C{list} of L{ACL} objects
+    """
+    for acl in acls.keys():
+        acls[acl] = acls[acl].joinGrantDeny()
+        _insertAclDisplaynames(acls[acl])
+    return acls
+
+
+def prepareAcl(acl):
+    """
+    Returns an ACL describing the behaviour of the resource. The information 
+    in the ACL is modified to contain all information needed to display in the UI.
+    
+    @param acl: An ACL object.
+    @type  acl: L{ACL} object
+    
+    @return: A (non-valid) ACL that contains both grant and deny clauses in an ACE.
+        Displaynames are added to the Principals where needed.
+    @rtype:  L{ACL} object
+    """
+    acl = acl.joinGrantDeny()
+    _insertAclDisplaynames(acl)
+    return acl
+
+
+def refineAclForSet(acl):
+    """
+    Sets the ACL composed from the UI on the WebDAV server. For that purpose the 
+    ACL object gets refined first to form a well accepted ACL to be set by the 
+    ACL WebDAV method.
+
+    @param acl: An ACL object to be refined.
+    @type  acl: L{ACL} object
+    
+    @return: A valid ACL that contains only grant or deny clauses in an ACE.
+        Inherited and protected ACEs are stripped out.
+    @rtype:  L{ACL} object
+    """
+    acl = acl.splitGrantDeny()
+    acl = acl.stripAces()
+    return acl
+
+
+##~ unsupported or unfinished methods:
+##~ 
+##~ def report(self, report, request=None, lockToken=None):
+##~     """
+##~     This method implements the WebDAV ACP method: REPORT for given report 
+##~     types.
+##~     
+##~     Parameters:
+##~     
+##~       'report' -- Report type as a string.
+##~       
+##~       'request' -- XML content of the request for the report (defaults to None).
+##~       
+##~       'lockToken' -- Lock token to be set (defaults to None).
+##~     """
+##~     raise WebdavError('Reports are not supported by our Jacarta Slide, yet (as of Aug. 1, 2003 in CVS).')
+##~     
+##~     headers                 = createCondition(lockToken)
+##~     headers['Content-Type'] = XML_CONTENT_TYPE
+##~     body                    = '<D:%s xmlns:D="DAV:">%s</D:%s>' % (report, request, report)
+##~     #print "Body: ", body
+##~     response = self.connection._request('REPORT', self.path, body, headers)
+##~     return response
+##~     ## TODO: parse DAV:error response
+##~ 
+##~ 
+##~ def getAllAcls(self):
+##~    """
+##~     Returns a dictionary of ACL resources with respective ACL objects 
+##~     that apply to the given resource.
+##~     
+##~     ### This method needs to be extended for inherited ACLs when Tamino
+##~     support tells me (Guy) how to get to them.
+##~     """
+##~     acls = {self.path: self.getAcl()}
+##~     for ace in acls[self.path].aces:
+##~         if ace.inherited:
+##~             if not ace.inherited in acls:
+##~                 acls[ace.inherited] = self.getAcl()
+##~    
+##~     # append some more stuff here to acls for possible inherited ACLs
+##~     return acls
diff --git a/src/webdav/acp/Acl.py b/src/webdav/acp/Acl.py
new file mode 100644
index 0000000..8f2b36f
--- /dev/null
+++ b/src/webdav/acp/Acl.py
@@ -0,0 +1,311 @@
+# pylint: disable-msg=W0622
+#
+# Copyright 2008 German Aerospace Center (DLR)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+
+"""
+ACL object handling according to WebDAV ACP specification.
+"""
+
+
+from webdav.acp.Ace import ACE
+from webdav import Constants
+from webdav.Connection import WebdavError
+from webdav.davlib import XML_DOC_HEADER
+
+
+__version__ = "$LastChangedRevision$"
+
+
+class ACL(object):
+    """
+    This class provides access to Access Control List funcionality
+    as specified in the WebDAV ACP.
+    
+    @ivar aces:          ACEs in ACL
+    @type aces:          C{list} of L{ACE} objects
+    @ivar withInherited: Flag indicating whether ACL contains inherited ACEs.
+    @type withInherited: C{bool}
+    """
+
+    # restrict instance variables
+    __slots__ = ('aces', 'withInherited')
+    
+    def __init__(self, domroot=None, aces=None):
+        """
+        Constructor should be called with either no parameters (create blank ACE),
+        or one parameter (a DOM tree or ACE list).
+        
+        @param domroot: A DOM tree (default: None).
+        @type  domroot: L{webdav.WebdavResponse.Element} object
+        @param aces:    ACE objects (default: None)
+        @type  aces:    C{list} of L{ACE} objects
+        
+        @raise WebdavError: When non-valid parameters are passed a L{WebdavError} is raised.
+        """
+        self.withInherited = None
+        self.aces          = []
+        
+        if domroot:
+            for child in domroot.children:
+                if child.name == Constants.TAG_ACE and child.ns == Constants.NS_DAV:
+                    self.addAce(ACE(child))
+                else:
+                    # This shouldn't happen, someone screwed up with the params ...
+                    raise WebdavError('Non-ACE tag handed to ACL constructor: ' + child.ns + child.name)
+        elif isinstance(aces, list) or isinstance(aces, tuple):
+            self.addAces(aces)
+        elif domroot == None and aces == None:
+            # no param ==> blank object
+            pass
+        else:
+            # This shouldn't happen, someone screwed up with the params ...
+            raise WebdavError('non-valid parameters handed to ACL constructor')
+
+    def __cmp__(self, other):
+        if not isinstance(other, ACL):
+            return 1
+        if self.withInherited == other.withInherited:
+            equal = 1
+            for ace in self.aces:
+                inList = 0
+                for otherAce in other.aces:
+                    if ace == otherAce:
+                        inList = 1
+                if inList == 0:
+                    equal = 0
+            return not equal
+        else:
+            return 1
+
+    def __repr__(self):
+        repr = '<class ACL: '
+        if self.withInherited:
+            repr += 'with inherited, '
+        first = 1
+        repr += 'aces: ['
+        for ace in self.aces:
+            if first:
+                repr += '%s' % ace
+                first = 0
+            else:
+                repr += ', %s' % ace
+        return '%s]>' % (repr)
+
+    def copy(self, other):
+        '''Copy an ACL object.
+        
+        @param other: Another ACL to copy.
+        @type  other: L{ACL} object
+        
+        @raise WebdavError: When an object that is not an L{ACL} is passed 
+            a L{WebdavError} is raised.
+        '''
+        if not isinstance(other, ACL):
+            raise WebdavError('Non-ACL object passed to copy method: %s' % other.__class__)
+        self.withInherited = other.withInherited
+        if other.aces:
+            self.addAces(other.aces)
+
+    def toXML(self):
+        """
+        Returns ACL content as a string of valid XML as described in WebDAV ACP.
+        """
+        aclTag = 'D:' + Constants.TAG_ACL
+        return XML_DOC_HEADER +\
+            '<' + aclTag + ' xmlns:D="DAV:">' + reduce(lambda xml, ace: xml + ace.toXML() + '\n', [''] + self.aces) +\
+            '</' + aclTag + '>'
+
+    def addAce(self, ace):
+        '''
+        Adds the passed ACE object to list if it's not in it, yet.
+        
+        @param ace: An ACE.
+        @type  ace: L{ACE} object
+        '''
+        newAce = ACE()
+        newAce.copy(ace)
+        # only add it if it's not in the list, yet ...
+        inList = 0
+        for element in self.aces:
+            if element == ace:
+                inList = 1
+        if not inList:
+            self.aces.append(newAce)
+
+    def addAces(self, aces):
+        '''Adds the list of passed ACE objects to list.
+        
+        @param aces: ACEs
+        @type  aces: sequence of L{ACE} objects
+        '''
+        for ace in aces:
+            self.addAce(ace)
+
+    def delAce(self, ace):
+        '''Deletes the passed ACE object from list.
+        
+        @param ace: An ACE.
+        @type  ace: L{ACE} object
+        
+        @raise WebdavError: When the ACE to be deleted is not within the ACL 
+            a L{WebdavError} is raised.
+        '''
+        # find where it is and delete it ...
+        count = 0
+        index = 0
+        for element in self.aces:
+            count += 1
+            if element == ace:
+                index = count
+        if index:
+            self.aces.pop(index - 1)
+        else:
+            raise WebdavError('ACE to be deleted not in list: %s.' % ace)
+
+    def delAces(self, aces):
+        '''Deletes the list of passed ACE objects from list.
+        
+        @param aces: ACEs
+        @type  aces: sequence of L{ACE} objects
+        '''
+        for ace in aces:
+            self.delAce(ace)
+
+    def delPrincipalsAces(self, principal):
+        """
+        Deletes all ACEs in ACL by given principal.
+        
+        @param principal: A principal.
+        @type  principal: L{Principal} object
+        """
+        # find where it is and delete it ...
+        index = 0
+        while index < len(self.aces):
+            if self.aces[index].principal.principalURL == principal.principalURL:
+                self.aces.pop(index)
+            else:
+                index += 1
+
+    def joinGrantDeny(self):
+        """
+        Returns a "refined" ACL of the ACL for ease of use in the UI. 
+        The purpose is to post the user an ACE that can contain both, granted 
+        and denied, privileges. So possible pairs of grant and deny ACEs are joined 
+        to return them in one ACE. This resulting ACE then of course IS NOT valid 
+        for setting ACLs anymore. They will have to be reconverted to yield valid 
+        ACLs for the ACL method.
+        
+        @return: A (non-valid) ACL that contains both grant and deny clauses in an ACE.
+        @rtype:  L{ACL} object
+        """
+        joinedAces = {}
+        for ace in self.aces:
+            if not ace.principal.principalURL is None:
+                principalKey = ace.principal.principalURL
+            elif not ace.principal.property is None:
+                principalKey = ace.principal.property
+            else:
+                principalKey = None
+            if ace.inherited:
+                principalKey = ace.inherited + ":" + principalKey
+            if principalKey in joinedAces:
+                joinedAces[principalKey].addGrantDenies(ace.grantDenies)
+            else:
+                joinedAces[principalKey] = ACE()
+                joinedAces[principalKey].copy(ace)
+        newAcl = ACL()
+        newAcl.addAces(joinedAces.values())
+        return newAcl
+
+    def splitGrantDeny(self):
+        """
+        Returns a "refined" ACL of the ACL for ease of use in the UI. 
+        The purpose is to post the user an ACE that can contain both, granted 
+        and denied, privileges. So possible joined grant and deny clauses in ACEs 
+        splitted to return them in separate ACEs. This resulting ACE then is valid 
+        for setting ACLs again. This method is to be seen in conjunction with the 
+        method joinGrantDeny as it reverts its effect.
+        
+        @return: A valid ACL that contains only ACEs with either grant or deny clauses.
+        @rtype:  L{ACL} object
+        """
+        acesGrant = {}
+        acesDeny  = {}
+        for ace in self.aces:
+            for grantDeny in ace.grantDenies:
+                if grantDeny.isGrant():
+                    if ace.principal.principalURL in acesGrant:
+                        ace.addGrantDeny(grantDeny)
+                    else:
+                        acesGrant[ace.principal.principalURL] = ACE()
+                        acesGrant[ace.principal.principalURL].copy(ace)
+                        acesGrant[ace.principal.principalURL].grantDenies = []
+                        acesGrant[ace.principal.principalURL].addGrantDeny(grantDeny)
+                else:
+                    if ace.principal.principalURL in acesDeny:
+                        ace.addGrantDeny(grantDeny)
+                    else:
+                        acesDeny[ace.principal.principalURL] = ACE()
+                        acesDeny[ace.principal.principalURL].copy(ace)
+                        acesDeny[ace.principal.principalURL].grantDenies = []
+                        acesDeny[ace.principal.principalURL].addGrantDeny(grantDeny)
+        newAcl = ACL()
+        newAcl.addAces(acesGrant.values())
+        newAcl.addAces(acesDeny.values())
+        return newAcl
+
+    def isValid(self):
+        """
+        Returns true (1) if all contained ACE objects are valid, 
+        otherwise false (0) is returned.
+        
+        @return: Validity of ACL.
+        @rtype:  C{bool}
+        """
+        valid = 1
+        if len(self.aces):
+            for ace in self.aces:
+                if not ace.isValid():
+                    valid = 0
+        return valid
+
+    def stripAces(self, inherited=True, protected=True):
+        """
+        Returns an ACL object with all ACEs stripped that are inherited 
+        and/or protected.
+        
+        @param inherited: Flag to indicate whether inherited ACEs should 
+            be stripped (default: True).
+        @type  inherited: C{bool}
+        @param protected: Flag to indicate whether protected ACEs should 
+            be stripped (default: True).
+        @type  protected: C{bool}
+        
+        @return: An ACL without the stripped ACEs.
+        @rtype:  L{ACL} object
+        """
+        newAcl = ACL()
+        if len(self.aces):
+            for ace in self.aces:
+                keep = 1
+                if inherited and ace.inherited:
+                    keep = 0
+                elif protected and ace.protected:
+                    keep = 0
+                if keep:
+                    newAcl.addAce(ace)
+        return newAcl
diff --git a/src/webdav/acp/GrantDeny.py b/src/webdav/acp/GrantDeny.py
new file mode 100644
index 0000000..52c9b93
--- /dev/null
+++ b/src/webdav/acp/GrantDeny.py
@@ -0,0 +1,241 @@
+# Copyright 2008 German Aerospace Center (DLR)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+"""
+Handling of grant and deny clauses in ACEs according to WebDAV ACP specification.
+"""
+
+
+from webdav.acp.Privilege import Privilege
+from webdav import Constants
+from webdav.Connection import WebdavError
+
+
+__version__ = "$LastChangedRevision$"
+
+
+class GrantDeny(object):
+    """
+    This class provides functionality for handling
+    grant and deny clauses in ACEs.
+    
+    @ivar grantDeny:  Flag indicating whether clause grants or denies.
+    @type grantDeny:  C{bool}
+    @ivar privileges: Privileges to be granted or denied.
+    @type privileges: C{list} of L{Privilege} objects
+    """
+    
+    def __init__(self, domroot=None):
+        """
+        Constructor should be called with either no parameters 
+        (create blank GrantDeny), or one parameter (a DOM tree).
+        
+        @param domroot:     A DOM tree (default: None).
+        @type  domroot:     L{webdav.WebdavResponse.Element} object
+        
+        @raise WebdavError: When non-valid parameters are passed a L{WebdavError} is raised.
+        """
+        self.grantDeny  = 0   # 0: deny, 1: grant
+        self.privileges = []
+        
+        if domroot:
+            self.grantDeny = (domroot.name == Constants.TAG_GRANT)
+            for child in domroot.children:
+                if child.name == Constants.TAG_PRIVILEGE and child.ns == Constants.NS_DAV:
+                    self.privileges.append(Privilege(domroot=child))
+                else:
+                    # This shouldn't happen, someone screwed up with the params ...
+                    raise WebdavError('Non-privilege tag handed to GrantDeny constructor: %s' \
+                        % child.name)
+        elif domroot == None:
+            # no param ==> blank object
+            pass
+        else:
+            # This shouldn't happen, someone screwed up with the params ...
+            raise WebdavError('Non-valid parameters handed to GrantDeny constructor.')
+
+    def __cmp__(self, other):
+        """ Compares two GrantDeny instances. """
+        if not isinstance(other, GrantDeny):
+            return 1
+        if self.grantDeny == other.grantDeny:
+            equal = 1
+            for priv in self.privileges:
+                inList = 0
+                for otherPriv in other.privileges:
+                    if priv == otherPriv:
+                        inList = 1
+                if inList == 0:
+                    equal = 0
+            return not equal
+        else:
+            return 1
+
+    def __repr__(self):
+        """ Returns the representation of an instance. """
+        representation = '<class GrantDeny: '
+        if self.grantDeny:
+            representation += 'grant privileges: ['
+        else:
+            representation += 'deny privileges: ['
+        first = 1
+        for priv in self.privileges:
+            if first:
+                representation += '%s' % priv
+                first = 0
+            else:
+                representation += ', %s' % priv
+        return '%s]>' % (representation)
+
+    def copy(self, other):
+        """
+        Copy a GrantDeny object.
+        
+        @param other: Another grant or deny clause to copy.
+        @type  other: L{GrantDeny} object
+        
+        @raise WebdavError: When an object that is not an L{GrantDeny} is passed 
+            a L{WebdavError} is raised.
+        """
+        if not isinstance(other, GrantDeny):
+            raise WebdavError('Non-GrantDeny object passed to copy method: %s' \
+                % other)
+        self.grantDeny = other.grantDeny
+        if other.privileges:
+            self.addPrivileges(other.privileges)
+
+    def isGrant(self):
+        """
+        Returns whether the set of privileges is of type "grant"
+        indicating true or false.
+        
+        @return: Value whether the clause is of grant type.
+        @rtype:  C{bool}
+        """
+        return self.grantDeny
+
+    def isDeny(self):
+        """
+        Returns whether the set of privileges is of type "deny"
+        indicating true or false.
+        
+        @return: Value whether the clause is of deny type.
+        @rtype:  C{bool}
+        """
+        return not self.grantDeny
+
+    def setGrantDeny(self, grantDeny):
+        """
+        Sets the set of privileges to given value for grantDeny.
+        
+        @param grantDeny: Grant/deny value for clause (grant: True/1, deny: False/0).
+        @type  grantDeny: C{bool}
+        """
+        if grantDeny == 0 or grantDeny == 1:
+            self.grantDeny = grantDeny
+
+    def setGrant(self):
+        """ Sets the set of privileges to type "grant". """
+        self.grantDeny = 1
+
+    def setDeny(self):
+        """ Sets the set of privileges to type "deny". """
+        self.grantDeny = 0
+
+    def isAll(self):
+        """
+        Checks whether the privileges contained are equal
+        to aggregate DAV:all privilege.
+        
+        @return: Value whether all un-aggregated privileges are present.
+        @rtype:  C{bool}
+        """
+        
+        if len(self.privileges) == 1 and self.privileges[0].name == Constants.TAG_ALL:
+            return 1
+        return 0
+
+    def addPrivilege(self, privilege):
+        """
+        Adds the passed privilege to list if it's not in it, yet.
+        
+        @param privilege: A privilege.
+        @type  privilege: L{Privilege} object
+        """
+        inList = False
+        for priv in self.privileges:
+            if priv == privilege:
+                inList = True
+        if not inList:
+            newPrivilege = Privilege()
+            newPrivilege.copy(privilege)
+            self.privileges.append(newPrivilege)
+
+    def addPrivileges(self, privileges):
+        """
+        Adds the list of passed privileges to list.
+        
+        @param privileges: Several privileges.
+        @type  privileges: sequence of L{Privilege} objects
+        """
+        for priv in privileges:
+            self.addPrivilege(priv)
+
+    def delPrivilege(self, privilege):
+        """
+        Deletes the passed privilege from list if it's in it.
+        
+        @param privilege: A privilege.
+        @type  privilege: L{Privilege} object
+        
+        @raise WebdavError: A L{WebdavError} is raised if the privilege to be 
+            deleted is not present.
+        """
+        count = 0
+        index = 0
+        for priv in self.privileges:
+            count += 1
+            if priv == privilege:
+                index = count
+        if index:
+            self.privileges.pop(index - 1)
+        else:
+            raise WebdavError('Privilege to be deleted not in list: %s' % privilege)
+
+    def delPrivileges(self, privileges):
+        """
+        Deletes the list of passed privileges from list.
+        
+        @param privileges: Several privileges.
+        @type  privileges: sequence of L{Privilege} objects
+        """
+        for priv in privileges:
+            self.delPrivilege(priv)
+
+    def toXML(self):
+        """
+        Returns string of GrantDeny content to valid XML as described in WebDAV ACP.
+        """
+        assert self.privileges, "GrantDeny object is not initialized or does not contain content!"
+        
+        if self.isGrant():
+            tag = 'D:' + Constants.TAG_GRANT
+        else:
+            tag = 'D:' + Constants.TAG_DENY
+            
+        res = ''
+        for privilege in self.privileges:
+            res += privilege.toXML()
+        return '<%s>%s</%s>' % (tag, res, tag)
diff --git a/src/webdav/acp/Makefile.am b/src/webdav/acp/Makefile.am
new file mode 100644
index 0000000..506eb92
--- /dev/null
+++ b/src/webdav/acp/Makefile.am
@@ -0,0 +1,12 @@
+sugardir = $(pythondir)/webdav/acp
+sugar_PYTHON =                  \
+             AceHandler.py      \
+             Ace.py             \
+             Acl.py             \
+             GrantDeny.py       \
+             __init__.py        \
+             Principal.py       \
+             Privilege.py
+             
+
+
diff --git a/src/webdav/acp/Principal.py b/src/webdav/acp/Principal.py
new file mode 100644
index 0000000..a0d5ec9
--- /dev/null
+++ b/src/webdav/acp/Principal.py
@@ -0,0 +1,189 @@
+# Copyright 2008 German Aerospace Center (DLR)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+"""
+Handling of principals for ACEs according to WebDAV ACP specification.
+"""
+
+
+from webdav import Constants
+from webdav.Connection import WebdavError
+
+
+__version__ = "$LastChangedRevision$"
+
+
+class Principal(object):
+    """
+    This class provides functionality for handling
+    principals according to the WebDAV ACP.
+     
+    @ivar displayname:  Name of the principal for output
+    @type displayname:  C{string}
+    @ivar principalURL: URL under which the principal can be referenced on the server.
+    @type principalURL: C{string}
+    @ivar property:     Information on type of a pseudo/jproperty principal, e. g. 
+        DAV:owner, DAV:authenticated, etc.
+    @type property:     C{string}
+    
+    @cvar _TAG_LIST_PRINCIPALS: List of allowed XML tags within a principal declaration.
+    @type _TAG_LIST_PRINCIPALS: C{tuple} of C{string}s
+    @cvar _TAG_LIST_STATUS:     List of XML tags for the status of a pseudo principal.
+    @type _TAG_LIST_STATUS:     C{tuple} of C{string}s
+    """
+    
+    # some local constants for this class to make things easier/more readable:
+    _TAG_LIST_PRINCIPALS = (Constants.TAG_HREF,     # directly by URL
+                            Constants.TAG_ALL, Constants.TAG_AUTHENTICATED, Constants.TAG_UNAUTHENTICATED, 
+                                                    # by log-in status
+                            Constants.TAG_PROPERTY, # for property info, e. g. 'owner'
+                            Constants.TAG_SELF,     # only if the resource is the principal itself
+                            Constants.TAG_PROP)     # contains property info like 'displayname'
+    _TAG_LIST_STATUS     = (Constants.TAG_ALL, Constants.TAG_AUTHENTICATED, Constants.TAG_UNAUTHENTICATED)
+    
+    # restrict instance variables
+    __slots__ = ('displayname', 'principalURL', 'property')
+    
+    def __init__(self, domroot=None, displayname=None, principalURL=None):
+        """
+        Constructor should be called with either no parameters (create blank Principal),
+        one parameter (a DOM tree), or two parameters (displayname and URL or property tag).
+        
+        @param domroot:      A DOM tree (default: None).
+        @type  domroot:      L{webdav.WebdavResponse.Element} object
+        @param displayname:  The display name of a principal (default: None).
+        @type  displayname:  C{string}
+        @param principalURL: The URL representing a principal (default: None).
+        @type  principalURL: C{string}
+        
+        @raise WebdavError: When non-valid parameters or sets of parameters are 
+            passed a L{WebdavError} is raised.
+        """
+        self.displayname  = None
+        self.principalURL = None
+        self.property     = None
+
+        if domroot:
+            for child in domroot.children:
+                if child.ns == Constants.NS_DAV and (child.name in self._TAG_LIST_PRINCIPALS):
+                    if child.name == Constants.TAG_PROP:
+                        self.displayname = \
+                            child.find(Constants.PROP_DISPLAY_NAME, Constants.NS_DAV)
+                    elif child.name == Constants.TAG_HREF:
+                        self.principalURL = child.textof()
+                        if self.principalURL and self.property in self._TAG_LIST_STATUS:
+                            raise WebdavError('Principal cannot contain a URL and "%s"' % (self.property))
+                    elif child.name == Constants.TAG_PROPERTY:
+                        if child.count() == 1:
+                            if self.property:
+                                raise WebdavError('Property for principal has already been set: old "%s", new "%s"' \
+                                    % (self.property, child.pop().name))
+                            elif self.principalURL:
+                                raise WebdavError('Principal cannot contain a URL and "%s"' % (self.property))
+                            else:
+                                self.property = child.pop().name
+                        else:
+                            raise WebdavError("There should be only one value in the property for a principal, we have: %s" \
+                                % child.name)
+                    else:
+                        if self.property:
+                            raise WebdavError('Property for principal has already been set: old "%s", new "%s"' \
+                                % (self.property, child.name))
+                        else:
+                            self.property = child.name
+                        if self.principalURL and self.property in self._TAG_LIST_STATUS:
+                            raise WebdavError('Principal cannot contain a URL and "%s"' % (self.property))
+                else: # This shouldn't happen, something's wrong with the DOM tree
+                    raise WebdavError('Non-valid tag in principal DOM tree for constructor: %s' % child.name)
+        elif displayname == None or principalURL == None:
+            if displayname:
+                self.displayname  = displayname
+            if principalURL:
+                self.principalURL = principalURL
+        else:
+            # This shouldn't happen, someone screwed up with the params ...
+            raise WebdavError('Non-valid parameters handed to Principal constructor.')
+
+    def __cmp__(self, other):
+        if not isinstance(other, Principal):
+            return 1
+        if self.displayname == other.displayname \
+                and self.principalURL == other.principalURL \
+                and self.property == other.property:
+            return 0
+        else:
+            return 1
+
+    def __repr__(self):
+        return '<class Principal: displayname: "%s", principalURL: "%s", property: "%s">' \
+            % (self.displayname, self.principalURL, self.property)
+
+    def copy(self, other):
+        """Copy Principal object.
+        
+        @param other: Another principal to copy.
+        @type  other: L{Principal} object
+        
+        @raise WebdavError: When an object that is not a L{Principal} is passed 
+            a L{WebdavError} is raised.
+        """
+        if not isinstance(other, Principal):
+            raise WebdavError('Non-Principal object passed to copy method: ' % other.__class__)
+        self.displayname  = other.displayname
+        self.principalURL = other.principalURL
+        self.property     = other.property
+
+    def isValid(self):
+        """
+        Checks whether necessarry props for principal are set.
+        
+        @return: Validity of principal.
+        @rtype:  C{bool}
+        """
+        return (self.displayname and
+                (self.principalURL or self.property) and
+                not (self.principalURL and self.property))
+        
+    def toXML(self, invert=False, displayname=False, defaultNameSpace=None):
+        """Returns string of Principal content in valid XML as described in WebDAV ACP.
+        
+        @param defaultNameSpace: Name space (default: None).
+        @type  defaultNameSpace: C(string)
+        @param invert:           True if principal should be inverted (default: False).
+        @type  invert:           C{bool}
+        @param displayname:      True if displayname should be in output (default: False).
+        @type  displayname:      C{bool}
+        """
+        # this check is needed for setting principals only:
+        # assert self.isValid(), "principal is not initialized or does not contain valid content!"
+        
+        PRINCIPAL = 'D:' + Constants.TAG_PRINCIPAL
+        res = ''
+        if self.principalURL:
+            res += '<D:%s>%s</D:%s>' % (Constants.TAG_HREF, self.principalURL, Constants.TAG_HREF)
+        elif self.property in self._TAG_LIST_STATUS \
+                or self.property == Constants.TAG_SELF:
+            res += '<D:%s/>' % (self.property)
+        elif self.property:
+            res += '<D:%s><D:%s/></D:%s>' \
+                % (Constants.TAG_PROPERTY, self.property, Constants.TAG_PROPERTY)
+        if self.displayname and displayname:
+            res += '<D:%s><D:%s>%s</D:%s></D:%s>' \
+                % (Constants.TAG_PROP, Constants.PROP_DISPLAY_NAME,
+                    self.displayname,
+                    Constants.PROP_DISPLAY_NAME, Constants.TAG_PROP)
+        if invert:
+            res = '<D:invert>%s</D:invert>' % (res)
+        return '<%s>%s</%s>' % (PRINCIPAL, res, PRINCIPAL)
diff --git a/src/webdav/acp/Privilege.py b/src/webdav/acp/Privilege.py
new file mode 100644
index 0000000..abfdcf9
--- /dev/null
+++ b/src/webdav/acp/Privilege.py
@@ -0,0 +1,125 @@
+# Copyright 2008 German Aerospace Center (DLR)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+"""
+Handling for privileges for grant and deny clauses in ACEs 
+according to WebDAV ACP specification.
+"""
+
+
+from webdav import Constants
+from webdav.Connection import WebdavError
+
+
+__version__ = "$LastChangedRevision$"
+
+
+class Privilege(object):
+    """This class provides functionality for handling privileges for ACEs.
+     
+    @ivar name: Name of the privilege.
+    @type name: C{string}
+    
+    @cvar __privileges: List of allowed XML tags for privileges.
+    @type __privileges: C{tuple} of C{string}s
+    """
+
+
+    __privileges = list()
+        
+    
+    def __init__(self, privilege=None, domroot=None):
+        """
+        Constructor should be called with either no parameters (create blank Privilege),
+        one parameter (a DOM tree or privilege name to initialize it directly).
+        
+        @param domroot: A DOM tree (default: None).
+        @type  domroot: L{webdav.WebdavResponse.Element} object
+        @param privilege: The valid name of a privilege (default: None).
+        @type  privilege: C{string}
+        
+        @raise WebdavError: When non-valid parameters or sets of parameters are 
+                            passed a L{WebdavError} is raised.
+        """
+        
+        self.name = None
+        
+        if domroot:
+            if len(domroot.children) != 1:
+                raise WebdavError('Wrong number of elements for Privilege constructor, we have: %i' \
+                    % (len(domroot.children)))
+            else:
+                child = domroot.children[0]
+                if child.ns == Constants.NS_DAV and child.name in self.__privileges:
+                    self.name = child.name
+                else:
+                    raise WebdavError('Not a valid privilege tag, we have: %s%s' \
+                        % (child.ns, child.name))
+        elif privilege:
+            if privilege in self.__privileges:
+                self.name = privilege
+            else:
+                raise WebdavError('Not a valid privilege tag, we have: %s.' % str(privilege))
+
+    @classmethod
+    def registerPrivileges(cls, privileges):
+        """
+        Registers supported privilege tags.
+        
+        @param privileges: List of privilege tags.
+        @type privileges: C{list} of C{unicode}
+        """
+        
+        for privilege in privileges:
+            cls.__privileges.append(privilege)
+    
+    def __cmp__(self, other):
+        """ Compares two Privilege instances. """
+        if not isinstance(other, Privilege):
+            return 1
+        if self.name != other.name:
+            return 1
+        else:
+            return 0
+
+    def __repr__(self):
+        """ Returns the string representation of an instance. """
+        return '<class Privilege: name: "%s">' % (self.name)
+
+    def copy(self, other):
+        """
+        Copy Privilege object.
+        
+        @param other: Another privilege to copy.
+        @type  other: L{Privilege} object
+        
+        @raise WebdavError: When an object that is not a L{Privilege} is passed 
+            a L{WebdavError} is raised.
+        """
+        if not isinstance(other, Privilege):
+            raise WebdavError('Non-Privilege object passed to copy method: %s' % other.__class__)
+        self.name = other.name
+
+    def toXML(self):
+        """
+        Returns privilege content as string in valid XML as described in WebDAV ACP.
+        
+        @param defaultNameSpace: Name space (default: None).
+        @type  defaultNameSpace: C(string)
+        """
+        assert self.name != None, "privilege is not initialized or does not contain valid content!"
+        
+        privilege = 'D:' + Constants.TAG_PRIVILEGE
+        return '<%s><D:%s/></%s>' % (privilege, self.name, privilege)
diff --git a/src/webdav/acp/__init__.py b/src/webdav/acp/__init__.py
new file mode 100644
index 0000000..b5af299
--- /dev/null
+++ b/src/webdav/acp/__init__.py
@@ -0,0 +1,33 @@
+# Copyright 2008 German Aerospace Center (DLR)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+from webdav import Constants 
+from webdav.acp.Acl import ACL
+from webdav.acp.Ace import ACE
+from webdav.acp.GrantDeny import GrantDeny
+from webdav.acp.Privilege import Privilege
+from webdav.acp.Principal import Principal
+
+
+__version__ = "$LastChangedRevision$"
+
+
+privileges = [Constants.TAG_READ, Constants.TAG_WRITE, Constants.TAG_WRITE_PROPERTIES, 
+              Constants.TAG_WRITE_CONTENT, Constants.TAG_UNLOCK, Constants.TAG_READ_ACL, 
+              Constants.TAG_READ_CURRENT_USER_PRIVILEGE_SET, Constants.TAG_WRITE_ACL, Constants.TAG_ALL, 
+              Constants.TAG_BIND, Constants.TAG_UNBIND, Constants.TAG_TAMINO_SECURITY,
+              Constants.TAG_BIND_COLLECTION, Constants.TAG_UNBIND_COLLECTION, Constants.TAG_READ_PRIVATE_PROPERTIES,
+              Constants.TAG_WRITE_PRIVATE_PROPERTIES]
+Privilege.registerPrivileges(privileges)
diff --git a/src/webdav/davlib.py b/src/webdav/davlib.py
new file mode 100644
index 0000000..f4dac91
--- /dev/null
+++ b/src/webdav/davlib.py
@@ -0,0 +1,336 @@
+# pylint: disable-msg=W0402,W0231,W0141,R0903,C0321,W0701,R0904,C0103,W0201,W0102,R0913,W0622,E1101,C0111,C0121,R0901
+# DAV client library
+#
+# Copyright (C) 1998-2000 Guido van Rossum. All Rights Reserved.
+# Written by Greg Stein. Given to Guido. Licensed using the Python license.
+#
+# This module is maintained by Greg and is available at:
+#    http://www.lyra.org/greg/python/davlib.py
+#
+# Since this isn't in the Python distribution yet, we'll use the CVS ID
+# for tracking:
+#   $Id: davlib.py 3182 2008-02-22 15:57:55 +0000 (Fr, 22 Feb 2008) schlauch $
+#
+
+import httplib
+import urllib
+import string
+import types
+import mimetypes
+import qp_xml
+
+
+INFINITY = 'infinity'
+XML_DOC_HEADER = '<?xml version="1.0" encoding="utf-8"?>'
+XML_CONTENT_TYPE = 'text/xml; charset="utf-8"'
+
+# block size for copying files up to the server
+BLOCKSIZE = 16384
+
+
+class HTTPProtocolChooser(httplib.HTTPSConnection):
+    def __init__(self, *args, **kw):
+        self.protocol = kw.pop('protocol')
+        if self.protocol == "https":
+            self.default_port = 443
+        else:
+            self.default_port = 80
+            
+        apply(httplib.HTTPSConnection.__init__, (self,) + args, kw)
+
+    def connect(self):
+        if self.protocol == "https":
+            httplib.HTTPSConnection.connect(self)
+        else:
+            httplib.HTTPConnection.connect(self)
+
+
+class HTTPConnectionAuth(HTTPProtocolChooser):
+    def __init__(self, *args, **kw):
+        apply(HTTPProtocolChooser.__init__, (self,) + args, kw)
+
+        self.__username = None
+        self.__password = None
+        self.__nonce = None
+        self.__opaque = None
+
+    def setauth(self, username, password):
+        self.__username = username
+        self.__password = password
+
+
+def _parse_status(elem):
+    text = elem.textof()
+    idx1 = string.find(text, ' ')
+    idx2 = string.find(text, ' ', idx1+1)
+    return int(text[idx1:idx2]), text[idx2+1:]
+
+class _blank:
+    def __init__(self, **kw):
+        self.__dict__.update(kw)
+class _propstat(_blank): pass
+class _response(_blank): pass
+class _multistatus(_blank): pass
+
+def _extract_propstat(elem):
+    ps = _propstat(prop={}, status=None, responsedescription=None)
+    for child in elem.children:
+        if child.ns != 'DAV:':
+            continue
+        if child.name == 'prop':
+            for prop in child.children:
+                ps.prop[(prop.ns, prop.name)] = prop
+        elif child.name == 'status':
+            ps.status = _parse_status(child)
+        elif child.name == 'responsedescription':
+            ps.responsedescription = child.textof()
+        ### unknown element name
+
+    return ps
+
+def _extract_response(elem):
+    resp = _response(href=[], status=None, responsedescription=None, propstat=[])
+    for child in elem.children:
+        if child.ns != 'DAV:':
+            continue
+        if child.name == 'href':
+            resp.href.append(child.textof())
+        elif child.name == 'status':
+            resp.status = _parse_status(child)
+        elif child.name == 'responsedescription':
+            resp.responsedescription = child.textof()
+        elif child.name == 'propstat':
+            resp.propstat.append(_extract_propstat(child))
+        ### unknown child element
+
+    return resp
+
+def _extract_msr(root):
+    if root.ns != 'DAV:' or root.name != 'multistatus':
+        raise 'invalid response: <DAV:multistatus> expected'
+
+    msr = _multistatus(responses=[ ], responsedescription=None)
+
+    for child in root.children:
+        if child.ns != 'DAV:':
+            continue
+        if child.name == 'responsedescription':
+            msr.responsedescription = child.textof()
+        elif child.name == 'response':
+            msr.responses.append(_extract_response(child))
+        ### unknown child element
+
+    return msr
+
+def _extract_locktoken(root):
+    if root.ns != 'DAV:' or root.name != 'prop':
+        raise 'invalid response: <DAV:prop> expected'
+    elem = root.find('lockdiscovery', 'DAV:')
+    if not elem:
+        raise 'invalid response: <DAV:lockdiscovery> expected'
+    elem = elem.find('activelock', 'DAV:')
+    if not elem:
+        raise 'invalid response: <DAV:activelock> expected'
+    elem = elem.find('locktoken', 'DAV:')
+    if not elem:
+        raise 'invalid response: <DAV:locktoken> expected'
+    elem = elem.find('href', 'DAV:')
+    if not elem:
+        raise 'invalid response: <DAV:href> expected'
+    return elem.textof()
+
+
+class DAVResponse(httplib.HTTPResponse):
+    def parse_multistatus(self):
+        self.root = qp_xml.Parser().parse(self)
+        self.msr = _extract_msr(self.root)
+
+    def parse_lock_response(self):
+        self.root = qp_xml.Parser().parse(self)
+        self.locktoken = _extract_locktoken(self.root)
+
+
+class DAV(HTTPConnectionAuth):
+
+    response_class = DAVResponse
+
+    def get(self, url, extra_hdrs={ }):
+        return self._request('GET', url, extra_hdrs=extra_hdrs)
+
+    def head(self, url, extra_hdrs={ }):
+        return self._request('HEAD', url, extra_hdrs=extra_hdrs)
+
+    def post(self, url, data={ }, body=None, extra_hdrs={ }):
+        headers = extra_hdrs.copy()
+
+        assert body or data, "body or data must be supplied"
+        assert not (body and data), "cannot supply both body and data"
+        if data:
+            body = ''
+            for key, value in data.items():
+                if isinstance(value, types.ListType):
+                    for item in value:
+                        body = body + '&' + key + '=' + urllib.quote(str(item))
+                else:
+                    body = body + '&' + key + '=' + urllib.quote(str(value))
+            body = body[1:]
+            headers['Content-Type'] = 'application/x-www-form-urlencoded'
+
+        return self._request('POST', url, body, headers)
+
+    def options(self, url='*', extra_hdrs={ }):
+        return self._request('OPTIONS', url, extra_hdrs=extra_hdrs)
+
+    def trace(self, url, extra_hdrs={ }):
+        return self._request('TRACE', url, extra_hdrs=extra_hdrs)
+
+    def put(self, url, contents,
+            content_type=None, content_enc=None, extra_hdrs={ }):
+
+        if not content_type:
+            content_type, content_enc = mimetypes.guess_type(url)
+
+        headers = extra_hdrs.copy()
+        if content_type:
+            headers['Content-Type'] = content_type
+        if content_enc:
+            headers['Content-Encoding'] = content_enc
+        return self._request('PUT', url, contents, headers)
+
+    def delete(self, url, extra_hdrs={ }):
+        return self._request('DELETE', url, extra_hdrs=extra_hdrs)
+
+    def propfind(self, url, body=None, depth=None, extra_hdrs={ }):
+        headers = extra_hdrs.copy()
+        headers['Content-Type'] = XML_CONTENT_TYPE
+        if depth is not None:
+            headers['Depth'] = str(depth)
+        return self._request('PROPFIND', url, body, headers)
+
+    def proppatch(self, url, body, extra_hdrs={ }):
+        headers = extra_hdrs.copy()
+        headers['Content-Type'] = XML_CONTENT_TYPE
+        return self._request('PROPPATCH', url, body, headers)
+
+    def mkcol(self, url, extra_hdrs={ }):
+        return self._request('MKCOL', url, extra_hdrs=extra_hdrs)
+
+    def move(self, src, dst, extra_hdrs={ }):
+        headers = extra_hdrs.copy()
+        headers['Destination'] = dst
+        return self._request('MOVE', src, extra_hdrs=headers)
+
+    def copy(self, src, dst, depth=None, extra_hdrs={ }):
+        headers = extra_hdrs.copy()
+        headers['Destination'] = dst
+        if depth is not None:
+            headers['Depth'] = str(depth)
+        return self._request('COPY', src, extra_hdrs=headers)
+
+    def lock(self, url, owner='', timeout=None, depth=None,
+             scope='exclusive', type='write', extra_hdrs={ }):
+        headers = extra_hdrs.copy()
+        headers['Content-Type'] = XML_CONTENT_TYPE
+        if depth is not None:
+            headers['Depth'] = str(depth)
+        if timeout is not None:
+            headers['Timeout'] = timeout
+        body = XML_DOC_HEADER + \
+               '<DAV:lockinfo xmlns:DAV="DAV:">' + \
+               '<DAV:lockscope><DAV:%s/></DAV:lockscope>' % scope + \
+               '<DAV:locktype><DAV:%s/></DAV:locktype>' % type + \
+               '<DAV:owner>' + owner + '</DAV:owner>' + \
+               '</DAV:lockinfo>'
+        return self._request('LOCK', url, body, extra_hdrs=headers)
+
+    def unlock(self, url, locktoken, extra_hdrs={ }):
+        headers = extra_hdrs.copy()
+        if locktoken[0] != '<':
+            locktoken = '<' + locktoken + '>'
+        headers['Lock-Token'] = locktoken
+        return self._request('UNLOCK', url, extra_hdrs=headers)
+
+    def _request(self, method, url, body=None, extra_hdrs={}):
+        "Internal method for sending a request."
+
+        self.request(method, url, body, extra_hdrs)
+        return self.getresponse()
+
+
+    #
+    # Higher-level methods for typical client use
+    #
+
+    def allprops(self, url, depth=None):
+        body = XML_DOC_HEADER + \
+               '<DAV:propfind xmlns:DAV="DAV:"><DAV:allprop/></DAV:propfind>'
+        return self.propfind(url, body, depth=depth)
+
+    def propnames(self, url, depth=None):
+        body = XML_DOC_HEADER + \
+               '<DAV:propfind xmlns:DAV="DAV:"><DAV:propname/></DAV:propfind>'
+        return self.propfind(url, body, depth)
+
+    def getprops(self, url, *names, **kw):
+        assert names, 'at least one property name must be provided'
+        if kw.has_key('ns'):
+            xmlns = ' xmlns:NS="' + kw['ns'] + '"'
+            ns = 'NS:'
+            del kw['ns']
+        else:
+            xmlns = ns = ''
+        if kw.has_key('depth'):
+            depth = kw['depth']
+            del kw['depth']
+        else:
+            depth = 0
+        assert not kw, 'unknown arguments'
+        body = XML_DOC_HEADER + \
+               '<DAV:propfind xmlns:DAV="DAV:"' + xmlns + '><DAV:prop><' + ns + \
+               string.joinfields(names, '/><' + ns) + \
+               '/></DAV:prop></DAV:propfind>'
+        return self.propfind(url, body, depth)
+
+    def delprops(self, url, *names, **kw):
+        assert names, 'at least one property name must be provided'
+        if kw.has_key('ns'):
+            xmlns = ' xmlns:NS="' + kw['ns'] + '"'
+            ns = 'NS:'
+            del kw['ns']
+        else:
+            xmlns = ns = ''
+        assert not kw, 'unknown arguments'
+        body = XML_DOC_HEADER + \
+               '<DAV:propertyupdate xmlns:DAV="DAV:"' + xmlns + \
+               '><DAV:remove><DAV:prop><' + ns + \
+               string.joinfields(names, '/><' + ns) + \
+               '/></DAV:prop></DAV:remove></DAV:propertyupdate>'
+        return self.proppatch(url, body)
+
+    def setprops(self, url, *xmlprops, **props):
+        assert xmlprops or props, 'at least one property must be provided'
+        xmlprops = list(xmlprops)
+        if props.has_key('ns'):
+            xmlns = ' xmlns:NS="' + props['ns'] + '"'
+            ns = 'NS:'
+            del props['ns']
+        else:
+            xmlns = ns = ''
+        for key, value in props.items():
+            if value:
+                xmlprops.append('<%s%s>%s</%s%s>' % (ns, key, value, ns, key))
+            else:
+                xmlprops.append('<%s%s/>' % (ns, key))
+        elems = string.joinfields(xmlprops, '')
+        body = XML_DOC_HEADER + \
+               '<DAV:propertyupdate xmlns:DAV="DAV:"' + xmlns + \
+               '><DAV:set><DAV:prop>' + \
+               elems + \
+               '</DAV:prop></DAV:set></DAV:propertyupdate>'
+        return self.proppatch(url, body)
+
+    def get_lock(self, url, owner='', timeout=None, depth=None):
+        response = self.lock(url, owner, timeout, depth)
+        response.parse_lock_response()
+        return response.locktoken
+    
\ No newline at end of file
diff --git a/src/webdav/logger.py b/src/webdav/logger.py
new file mode 100644
index 0000000..d2538ef
--- /dev/null
+++ b/src/webdav/logger.py
@@ -0,0 +1,51 @@
+# Copyright 2008 German Aerospace Center (DLR)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+"""" 
+Module provides access to a configured logger instance.
+The logger writes C{sys.stdout}.
+"""
+
+
+import logging
+import sys
+
+
+__version__ = "$LastChangedRevision$"[11:-2]
+
+
+_defaultLoggerName = "webdavLogger"
+_fileLogFormat = "%(asctime)s: %(levelname)s: %(message)s"
+    
+
+def getDefaultLogger(handler=None):
+    """ 
+    Returns a configured logger object.
+    
+    @return: Logger instance.
+    @rtype: C{logging.Logger}
+    """
+    
+    myLogger = logging.getLogger(_defaultLoggerName)
+    if len(myLogger.handlers) == 0: 
+        myLogger.level = logging.DEBUG
+        formatter = logging.Formatter(_fileLogFormat)
+        if handler is None:
+            stdoutHandler = logging.StreamHandler(sys.stdout)
+            stdoutHandler.setFormatter(formatter)
+            myLogger.addHandler(stdoutHandler)
+        else:
+            myLogger.addHandler(handler)
+    return myLogger
diff --git a/src/webdav/qp_xml.py b/src/webdav/qp_xml.py
new file mode 100644
index 0000000..f167e1b
--- /dev/null
+++ b/src/webdav/qp_xml.py
@@ -0,0 +1,240 @@
+# pylint: disable-msg=W0311,E1101,E1103,W0201,C0103,W0622,W0402,W0706,R0911,W0613,W0612,R0912,W0141,C0111,C0121
+
+# qp_xml: Quick Parsing for XML
+#
+# Written by Greg Stein. Public Domain.
+# No Copyright, no Rights Reserved, and no Warranties.
+#
+# This module is maintained by Greg and is available as part of the XML-SIG
+# distribution. This module and its changelog can be fetched at:
+#    http://www.lyra.org/cgi-bin/viewcvs.cgi/xml/xml/utils/qp_xml.py
+#
+# Additional information can be found on Greg's Python page at:
+#    http://www.lyra.org/greg/python/
+#
+# This module was added to the XML-SIG distribution on February 14, 2000.
+# As part of that distribution, it falls under the XML distribution license.
+#
+
+import string
+from xml.parsers import expat
+
+
+error = __name__ + '.error'
+
+
+#
+# The parsing class. Instantiate and pass a string/file to .parse()
+#
+class Parser:
+  def __init__(self):
+    self.reset()
+
+  def reset(self):
+    self.root = None
+    self.cur_elem = None
+
+  def find_prefix(self, prefix):
+    elem = self.cur_elem
+    while elem:
+      if elem.ns_scope.has_key(prefix):
+        return elem.ns_scope[prefix]
+      elem = elem.parent
+
+    if prefix == '':
+      return ''        # empty URL for "no namespace"
+
+    return None
+
+  def process_prefix(self, name, use_default):
+    idx = string.find(name, ':')
+    if idx == -1:
+      if use_default:
+        return self.find_prefix(''), name
+      return '', name    # no namespace
+
+    if string.lower(name[:3]) == 'xml':
+      return '', name    # name is reserved by XML. don't break out a NS.
+
+    ns = self.find_prefix(name[:idx])
+    if ns is None:
+      raise error, 'namespace prefix ("%s") not found' % name[:idx]
+
+    return ns, name[idx+1:]
+
+  def start(self, name, attrs):
+    elem = _element(name=name, lang=None, parent=None,
+                    children=[], ns_scope={}, attrs={},
+                    first_cdata='', following_cdata='')
+
+    if self.cur_elem:
+      elem.parent = self.cur_elem
+      elem.parent.children.append(elem)
+      self.cur_elem = elem
+    else:
+      self.cur_elem = self.root = elem
+
+    work_attrs = [ ]
+
+    # scan for namespace declarations (and xml:lang while we're at it)
+    for name, value in attrs.items():
+      if name == 'xmlns':
+        elem.ns_scope[''] = value
+      elif name[:6] == 'xmlns:':
+        elem.ns_scope[name[6:]] = value
+      elif name == 'xml:lang':
+        elem.lang = value
+      else:
+        work_attrs.append((name, value))
+
+    # inherit xml:lang from parent
+    if elem.lang is None and elem.parent:
+      elem.lang = elem.parent.lang
+
+    # process prefix of the element name
+    elem.ns, elem.name = self.process_prefix(elem.name, 1)
+
+    # process attributes' namespace prefixes
+    for name, value in work_attrs:
+      elem.attrs[self.process_prefix(name, 0)] = value
+
+  def end(self, name):
+    parent = self.cur_elem.parent
+
+    del self.cur_elem.ns_scope
+    del self.cur_elem.parent
+
+    self.cur_elem = parent
+
+  def cdata(self, data):
+    elem = self.cur_elem
+    if elem.children:
+      last = elem.children[-1]
+      last.following_cdata = last.following_cdata + data
+    else:
+      elem.first_cdata = elem.first_cdata + data
+
+  def parse(self, input):
+    self.reset()
+
+    p = expat.ParserCreate()
+    p.StartElementHandler = self.start
+    p.EndElementHandler = self.end
+    p.CharacterDataHandler = self.cdata
+
+    try:
+      if type(input) == type(''):
+        p.Parse(input, 1)
+      else:
+        while 1:
+          s = input.read(_BLOCKSIZE)
+          if not s:
+            p.Parse('', 1)
+            break
+
+          p.Parse(s, 0)
+
+    finally:
+      if self.root:
+        _clean_tree(self.root)
+
+    return self.root
+
+
+#
+# handy function for dumping a tree that is returned by Parser
+#
+def dump(f, root):
+  f.write('<?xml version="1.0"?>\n')
+  namespaces = _collect_ns(root)
+  _dump_recurse(f, root, namespaces, dump_ns=1)
+  f.write('\n')
+
+
+#
+# This function returns the element's CDATA. Note: this is not recursive --
+# it only returns the CDATA immediately within the element, excluding the
+# CDATA in child elements.
+#
+def textof(elem):
+  return elem.textof()
+
+
+#########################################################################
+#
+# private stuff for qp_xml
+#
+
+_BLOCKSIZE = 16384    # chunk size for parsing input
+
+class _element:
+  def __init__(self, **kw):
+    self.__dict__.update(kw)
+
+  def textof(self):
+    '''Return the CDATA of this element.
+
+    Note: this is not recursive -- it only returns the CDATA immediately
+    within the element, excluding the CDATA in child elements.
+    '''
+    s = self.first_cdata
+    for child in self.children:
+      s = s + child.following_cdata
+    return s
+
+  def find(self, name, ns=''):
+    for elem in self.children:
+      if elem.name == name and elem.ns == ns:
+        return elem
+    return None
+
+
+def _clean_tree(elem):
+  elem.parent = None
+  del elem.parent
+  map(_clean_tree, elem.children)
+
+
+def _collect_recurse(elem, dict):
+  dict[elem.ns] = None
+  for ns, name in elem.attrs.keys():
+    dict[ns] = None
+  for child in elem.children:
+    _collect_recurse(child, dict)
+
+def _collect_ns(elem):
+  "Collect all namespaces into a NAMESPACE -> PREFIX mapping."
+  d = { '' : None }
+  _collect_recurse(elem, d)
+  del d['']    # make sure we don't pick up no-namespace entries
+  keys = d.keys()
+  for i in range(len(keys)):
+    d[keys[i]] = i
+  return d
+
+def _dump_recurse(f, elem, namespaces, lang=None, dump_ns=0):
+  if elem.ns:
+    f.write('<ns%d:%s' % (namespaces[elem.ns], elem.name))
+  else:
+    f.write('<' + elem.name)
+  for (ns, name), value in elem.attrs.items():
+    if ns:
+      f.write(' ns%d:%s="%s"' % (namespaces[ns], name, value))
+    else:
+      f.write(' %s="%s"' % (name, value))
+  if dump_ns:
+    for ns, id in namespaces.items():
+      f.write(' xmlns:ns%d="%s"' % (id, ns))
+  if elem.lang != lang:
+    f.write(' xml:lang="%s"' % elem.lang)
+  if elem.children or elem.first_cdata:
+    f.write('>' + elem.first_cdata)
+    for child in elem.children:
+      _dump_recurse(f, child, namespaces, elem.lang)
+      f.write(child.following_cdata)
+    if elem.ns:
+      f.write('</ns%d:%s>' % (namespaces[elem.ns], elem.name))
+    else:
+      f.write('</%s>' % elem.name)
+  else:
+    f.write('/>')
diff --git a/src/webdav/uuid_.py b/src/webdav/uuid_.py
new file mode 100644
index 0000000..3b590e8
--- /dev/null
+++ b/src/webdav/uuid_.py
@@ -0,0 +1,476 @@
+r"""UUID objects (universally unique identifiers) according to RFC 4122.
+
+This module provides immutable UUID objects (class UUID) and the functions
+uuid1(), uuid3(), uuid4(), uuid5() for generating version 1, 3, 4, and 5
+UUIDs as specified in RFC 4122.
+
+If all you want is a unique ID, you should probably call uuid1() or uuid4().
+Note that uuid1() may compromise privacy since it creates a UUID containing
+the computer's network address.  uuid4() creates a random UUID.
+
+Typical usage:
+
+    >>> import uuid
+
+    # make a UUID based on the host ID and current time
+    >>> uuid.uuid1()
+    UUID('a8098c1a-f86e-11da-bd1a-00112444be1e')
+
+    # make a UUID using an MD5 hash of a namespace UUID and a name
+    >>> uuid.uuid3(uuid.NAMESPACE_DNS, 'python.org')
+    UUID('6fa459ea-ee8a-3ca4-894e-db77e160355e')
+
+    # make a random UUID
+    >>> uuid.uuid4()
+    UUID('16fd2706-8baf-433b-82eb-8c7fada847da')
+
+    # make a UUID using a SHA-1 hash of a namespace UUID and a name
+    >>> uuid.uuid5(uuid.NAMESPACE_DNS, 'python.org')
+    UUID('886313e1-3b8a-5372-9b90-0c9aee199e5d')
+
+    # make a UUID from a string of hex digits (braces and hyphens ignored)
+    >>> x = uuid.UUID('{00010203-0405-0607-0809-0a0b0c0d0e0f}')
+
+    # convert a UUID to a string of hex digits in standard form
+    >>> str(x)
+    '00010203-0405-0607-0809-0a0b0c0d0e0f'
+
+    # get the raw 16 bytes of the UUID
+    >>> x.bytes
+    '\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f'
+
+    # make a UUID from a 16-byte string
+    >>> uuid.UUID(bytes=x.bytes)
+    UUID('00010203-0405-0607-0809-0a0b0c0d0e0f')
+"""
+
+__author__ = 'Ka-Ping Yee <ping at zesty.ca>'
+__date__ = '$Date: 2006/06/12 23:15:40 $'.split()[1].replace('/', '-')
+__version__ = '$Revision: 1.30 $'.split()[1]
+
+RESERVED_NCS, RFC_4122, RESERVED_MICROSOFT, RESERVED_FUTURE = [
+    'reserved for NCS compatibility', 'specified in RFC 4122',
+    'reserved for Microsoft compatibility', 'reserved for future definition']
+
+class UUID(object):
+    """Instances of the UUID class represent UUIDs as specified in RFC 4122.
+    UUID objects are immutable, hashable, and usable as dictionary keys.
+    Converting a UUID to a string with str() yields something in the form
+    '12345678-1234-1234-1234-123456789abc'.  The UUID constructor accepts
+    four possible forms: a similar string of hexadecimal digits, or a
+    string of 16 raw bytes as an argument named 'bytes', or a tuple of
+    six integer fields (with 32-bit, 16-bit, 16-bit, 8-bit, 8-bit, and
+    48-bit values respectively) as an argument named 'fields', or a single
+    128-bit integer as an argument named 'int'.
+
+    UUIDs have these read-only attributes:
+
+        bytes       the UUID as a 16-byte string
+
+        fields      a tuple of the six integer fields of the UUID,
+                    which are also available as six individual attributes
+                    and two derived attributes:
+
+            time_low                the first 32 bits of the UUID
+            time_mid                the next 16 bits of the UUID
+            time_hi_version         the next 16 bits of the UUID
+            clock_seq_hi_variant    the next 8 bits of the UUID
+            clock_seq_low           the next 8 bits of the UUID
+            node                    the last 48 bits of the UUID
+
+            time                    the 60-bit timestamp
+            clock_seq               the 14-bit sequence number
+
+        hex         the UUID as a 32-character hexadecimal string
+
+        int         the UUID as a 128-bit integer
+
+        urn         the UUID as a URN as specified in RFC 4122
+
+        variant     the UUID variant (one of the constants RESERVED_NCS,
+                    RFC_4122, RESERVED_MICROSOFT, or RESERVED_FUTURE)
+
+        version     the UUID version number (1 through 5, meaningful only
+                    when the variant is RFC_4122)
+    """
+
+    def __init__(self, hex=None, bytes=None, fields=None, int=None,
+                       version=None):
+        r"""Create a UUID from either a string of 32 hexadecimal digits,
+        a string of 16 bytes as the 'bytes' argument, a tuple of six
+        integers (32-bit time_low, 16-bit time_mid, 16-bit time_hi_version,
+        8-bit clock_seq_hi_variant, 8-bit clock_seq_low, 48-bit node) as
+        the 'fields' argument, or a single 128-bit integer as the 'int'
+        argument.  When a string of hex digits is given, curly braces,
+        hyphens, and a URN prefix are all optional.  For example, these
+        expressions all yield the same UUID:
+
+        UUID('{12345678-1234-5678-1234-567812345678}')
+        UUID('12345678123456781234567812345678')
+        UUID('urn:uuid:12345678-1234-5678-1234-567812345678')
+        UUID(bytes='\x12\x34\x56\x78'*4)
+        UUID(fields=(0x12345678, 0x1234, 0x5678, 0x12, 0x34, 0x567812345678))
+        UUID(int=0x12345678123456781234567812345678)
+
+        Exactly one of 'hex', 'bytes', 'fields', or 'int' must be given.
+        The 'version' argument is optional; if given, the resulting UUID
+        will have its variant and version number set according to RFC 4122,
+        overriding bits in the given 'hex', 'bytes', 'fields', or 'int'.
+        """
+
+        if [hex, bytes, fields, int].count(None) != 3:
+            raise TypeError('need just one of hex, bytes, fields, or int')
+        if hex is not None:
+            hex = hex.replace('urn:', '').replace('uuid:', '')
+            hex = hex.strip('{}').replace('-', '')
+            if len(hex) != 32:
+                raise ValueError('badly formed hexadecimal UUID string')
+            int = long(hex, 16)
+        if bytes is not None:
+            if len(bytes) != 16:
+                raise ValueError('bytes is not a 16-char string')
+            int = long(('%02x'*16) % tuple(map(ord, bytes)), 16)
+        if fields is not None:
+            if len(fields) != 6:
+                raise ValueError('fields is not a 6-tuple')
+            (time_low, time_mid, time_hi_version,
+             clock_seq_hi_variant, clock_seq_low, node) = fields
+            if not 0 <= time_low < 1<<32L:
+                raise ValueError('field 1 out of range (need a 32-bit value)')
+            if not 0 <= time_mid < 1<<16L:
+                raise ValueError('field 2 out of range (need a 16-bit value)')
+            if not 0 <= time_hi_version < 1<<16L:
+                raise ValueError('field 3 out of range (need a 16-bit value)')
+            if not 0 <= clock_seq_hi_variant < 1<<8L:
+                raise ValueError('field 4 out of range (need an 8-bit value)')
+            if not 0 <= clock_seq_low < 1<<8L:
+                raise ValueError('field 5 out of range (need an 8-bit value)')
+            if not 0 <= node < 1<<48L:
+                raise ValueError('field 6 out of range (need a 48-bit value)')
+            clock_seq = (clock_seq_hi_variant << 8L) | clock_seq_low
+            int = ((time_low << 96L) | (time_mid << 80L) |
+                   (time_hi_version << 64L) | (clock_seq << 48L) | node)
+        if int is not None:
+            if not 0 <= int < 1<<128L:
+                raise ValueError('int is out of range (need a 128-bit value)')
+        if version is not None:
+            if not 1 <= version <= 5:
+                raise ValueError('illegal version number')
+            # Set the variant to RFC 4122.
+            int &= ~(0xc000 << 48L)
+            int |= 0x8000 << 48L
+            # Set the version number.
+            int &= ~(0xf000 << 64L)
+            int |= version << 76L
+        self.__dict__['int'] = int
+
+    def __cmp__(self, other):
+        if isinstance(other, UUID):
+            return cmp(self.int, other.int)
+        return NotImplemented
+
+    def __hash__(self):
+        return hash(self.int)
+
+    def __int__(self):
+        return self.int
+
+    def __repr__(self):
+        return 'UUID(%r)' % str(self)
+
+    def __setattr__(self, name, value):
+        raise TypeError('UUID objects are immutable')
+
+    def __str__(self):
+        hex = '%032x' % self.int
+        return '%s-%s-%s-%s-%s' % (
+            hex[:8], hex[8:12], hex[12:16], hex[16:20], hex[20:])
+
+    def get_bytes(self):
+        bytes = ''
+        for shift in range(0, 128, 8):
+            bytes = chr((self.int >> shift) & 0xff) + bytes
+        return bytes
+
+    bytes = property(get_bytes)
+
+    def get_fields(self):
+        return (self.time_low, self.time_mid, self.time_hi_version,
+                self.clock_seq_hi_variant, self.clock_seq_low, self.node)
+
+    fields = property(get_fields)
+
+    def get_time_low(self):
+        return self.int >> 96L
+
+    time_low = property(get_time_low)
+
+    def get_time_mid(self):
+        return (self.int >> 80L) & 0xffff
+
+    time_mid = property(get_time_mid)
+
+    def get_time_hi_version(self):
+        return (self.int >> 64L) & 0xffff
+
+    time_hi_version = property(get_time_hi_version)
+
+    def get_clock_seq_hi_variant(self):
+        return (self.int >> 56L) & 0xff
+
+    clock_seq_hi_variant = property(get_clock_seq_hi_variant)
+
+    def get_clock_seq_low(self):
+        return (self.int >> 48L) & 0xff
+
+    clock_seq_low = property(get_clock_seq_low)
+
+    def get_time(self):
+        return (((self.time_hi_version & 0x0fffL) << 48L) |
+                (self.time_mid << 32L) | self.time_low)
+
+    time = property(get_time)
+
+    def get_clock_seq(self):
+        return (((self.clock_seq_hi_variant & 0x3fL) << 8L) |
+                self.clock_seq_low)
+
+    clock_seq = property(get_clock_seq)
+
+    def get_node(self):
+        return self.int & 0xffffffffffff
+
+    node = property(get_node)
+
+    def get_hex(self):
+        return '%032x' % self.int
+
+    hex = property(get_hex)
+
+    def get_urn(self):
+        return 'urn:uuid:' + str(self)
+
+    urn = property(get_urn)
+
+    def get_variant(self):
+        if not self.int & (0x8000 << 48L):
+            return RESERVED_NCS
+        elif not self.int & (0x4000 << 48L):
+            return RFC_4122
+        elif not self.int & (0x2000 << 48L):
+            return RESERVED_MICROSOFT
+        else:
+            return RESERVED_FUTURE
+
+    variant = property(get_variant)
+
+    def get_version(self):
+        # The version bits are only meaningful for RFC 4122 UUIDs.
+        if self.variant == RFC_4122:
+            return int((self.int >> 76L) & 0xf)
+
+    version = property(get_version)
+
+def _ifconfig_getnode():
+    """Get the hardware address on Unix by running ifconfig."""
+    import os
+    for dir in ['', '/sbin/', '/usr/sbin']:
+        try:
+            pipe = os.popen(os.path.join(dir, 'ifconfig'))
+        except IOError:
+            continue
+        for line in pipe:
+            words = line.lower().split()
+            for i in range(len(words)):
+                if words[i] in ['hwaddr', 'ether']:
+                    return int(words[i + 1].replace(':', ''), 16)
+
+def _ipconfig_getnode():
+    """Get the hardware address on Windows by running ipconfig.exe."""
+    import os, re
+    dirs = ['', r'c:\windows\system32', r'c:\winnt\system32']
+    try:
+        import ctypes
+        buffer = ctypes.create_string_buffer(300)
+        ctypes.windll.kernel32.GetSystemDirectoryA(buffer, 300)
+        dirs.insert(0, buffer.value.decode('mbcs'))
+    except:
+        pass
+    for dir in dirs:
+        try:
+            pipe = os.popen(os.path.join(dir, 'ipconfig') + ' /all')
+        except IOError:
+            continue
+        for line in pipe:
+            value = line.split(':')[-1].strip().lower()
+            if re.match('([0-9a-f][0-9a-f]-){5}[0-9a-f][0-9a-f]', value):
+                return int(value.replace('-', ''), 16)
+
+def _netbios_getnode():
+    """Get the hardware address on Windows using NetBIOS calls.
+    See http://support.microsoft.com/kb/118623 for details."""
+    import win32wnet, netbios
+    ncb = netbios.NCB()
+    ncb.Command = netbios.NCBENUM
+    ncb.Buffer = adapters = netbios.LANA_ENUM()
+    adapters._pack()
+    if win32wnet.Netbios(ncb) != 0:
+        return
+    adapters._unpack()
+    for i in range(adapters.length):
+        ncb.Reset()
+        ncb.Command = netbios.NCBRESET
+        ncb.Lana_num = ord(adapters.lana[i])
+        if win32wnet.Netbios(ncb) != 0:
+            continue
+        ncb.Reset()
+        ncb.Command = netbios.NCBASTAT
+        ncb.Lana_num = ord(adapters.lana[i])
+        ncb.Callname = '*'.ljust(16)
+        ncb.Buffer = status = netbios.ADAPTER_STATUS()
+        if win32wnet.Netbios(ncb) != 0:
+            continue
+        status._unpack()
+        bytes = map(ord, status.adapter_address)
+        return ((bytes[0]<<40L) + (bytes[1]<<32L) + (bytes[2]<<24L) +
+                (bytes[3]<<16L) + (bytes[4]<<8L) + bytes[5])
+
+# Thanks to Thomas Heller for ctypes and for his help with its use here.
+
+# If ctypes is available, use it to find system routines for UUID generation.
+_uuid_generate_random = _uuid_generate_time = _UuidCreate = None
+try:
+    import ctypes, ctypes.util
+    _buffer = ctypes.create_string_buffer(16)
+
+    # The uuid_generate_* routines are provided by libuuid on at least
+    # Linux and FreeBSD, and provided by libc on Mac OS X.
+    for libname in ['uuid', 'c']:
+        try:
+            lib = ctypes.CDLL(ctypes.util.find_library(libname))
+        except:
+            continue
+        if hasattr(lib, 'uuid_generate_random'):
+            _uuid_generate_random = lib.uuid_generate_random
+        if hasattr(lib, 'uuid_generate_time'):
+            _uuid_generate_time = lib.uuid_generate_time
+
+    # On Windows prior to 2000, UuidCreate gives a UUID containing the
+    # hardware address.  On Windows 2000 and later, UuidCreate makes a
+    # random UUID and UuidCreateSequential gives a UUID containing the
+    # hardware address.  These routines are provided by the RPC runtime.
+    try:
+        lib = ctypes.windll.rpcrt4
+    except:
+        lib = None
+    _UuidCreate = getattr(lib, 'UuidCreateSequential',
+                          getattr(lib, 'UuidCreate', None))
+except:
+    pass
+
+def _unixdll_getnode():
+    """Get the hardware address on Unix using ctypes."""
+    _uuid_generate_time(_buffer)
+    return UUID(bytes=_buffer.raw).node
+
+def _windll_getnode():
+    """Get the hardware address on Windows using ctypes."""
+    if _UuidCreate(_buffer) == 0:
+        return UUID(bytes=_buffer.raw).node
+
+def _random_getnode():
+    """Get a random node ID, with eighth bit set as suggested by RFC 4122."""
+    import random
+    return random.randrange(0, 1<<48L) | 0x010000000000L
+
+_node = None
+
+def getnode():
+    """Get the hardware address as a 48-bit integer.  The first time this
+    runs, it may launch a separate program, which could be quite slow.  If
+    all attempts to obtain the hardware address fail, we choose a random
+    48-bit number with its eighth bit set to 1 as recommended in RFC 4122."""
+
+    global _node
+    if _node is not None:
+        return _node
+
+    import sys
+    if sys.platform == 'win32':
+        getters = [_windll_getnode, _netbios_getnode, _ipconfig_getnode]
+    else:
+        getters = [_unixdll_getnode, _ifconfig_getnode]
+
+    for getter in getters + [_random_getnode]:
+        try:
+            _node = getter()
+        except:
+            continue
+        if _node is not None:
+            return _node
+
+def uuid1(node=None, clock_seq=None):
+    """Generate a UUID from a host ID, sequence number, and the current time.
+    If 'node' is not given, getnode() is used to obtain the hardware
+    address.  If 'clock_seq' is given, it is used as the sequence number;
+    otherwise a random 14-bit sequence number is chosen."""
+
+    # When the system provides a version-1 UUID generator, use it (but don't
+    # use UuidCreate here because its UUIDs don't conform to RFC 4122).
+    if _uuid_generate_time and node is clock_seq is None:
+        _uuid_generate_time(_buffer)
+        return UUID(bytes=_buffer.raw)
+
+    import time
+    nanoseconds = int(time.time() * 1e9)
+    # 0x01b21dd213814000 is the number of 100-ns intervals between the
+    # UUID epoch 1582-10-15 00:00:00 and the Unix epoch 1970-01-01 00:00:00.
+    timestamp = int(nanoseconds/100) + 0x01b21dd213814000L
+    if clock_seq is None:
+        import random
+        clock_seq = random.randrange(1<<14L) # instead of stable storage
+    time_low = timestamp & 0xffffffffL
+    time_mid = (timestamp >> 32L) & 0xffffL
+    time_hi_version = (timestamp >> 48L) & 0x0fffL
+    clock_seq_low = clock_seq & 0xffL
+    clock_seq_hi_variant = (clock_seq >> 8L) & 0x3fL
+    if node is None:
+        node = getnode()
+    return UUID(fields=(time_low, time_mid, time_hi_version,
+                        clock_seq_hi_variant, clock_seq_low, node), version=1)
+
+def uuid3(namespace, name):
+    """Generate a UUID from the MD5 hash of a namespace UUID and a name."""
+    import md5
+    hash = md5.md5(namespace.bytes + name).digest()
+    return UUID(bytes=hash[:16], version=3)
+
+def uuid4():
+    """Generate a random UUID."""
+
+    # When the system provides a version-4 UUID generator, use it.
+    if _uuid_generate_random:
+        _uuid_generate_random(_buffer)
+        return UUID(bytes=_buffer.raw)
+
+    # Otherwise, get randomness from urandom or the 'random' module.
+    try:
+        import os
+        return UUID(bytes=os.urandom(16), version=4)
+    except:
+        import random
+        bytes = [chr(random.randrange(256)) for i in range(16)]
+        return UUID(bytes=bytes, version=4)
+
+def uuid5(namespace, name):
+    """Generate a UUID from the SHA-1 hash of a namespace UUID and a name."""
+    import sha
+    hash = sha.sha(namespace.bytes + name).digest()
+    return UUID(bytes=hash[:16], version=5)
+
+# The following standard UUIDs are for use with uuid3() or uuid5().
+
+NAMESPACE_DNS = UUID('6ba7b810-9dad-11d1-80b4-00c04fd430c8')
+NAMESPACE_URL = UUID('6ba7b811-9dad-11d1-80b4-00c04fd430c8')
+NAMESPACE_OID = UUID('6ba7b812-9dad-11d1-80b4-00c04fd430c8')
+NAMESPACE_X500 = UUID('6ba7b814-9dad-11d1-80b4-00c04fd430c8')
-- 
1.7.4.4



More information about the Dextrose mailing list