Index: quintagroup.transmogrifier/branches/ofs/MANIFEST.in
===================================================================
--- quintagroup.transmogrifier/branches/ofs/MANIFEST.in	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/MANIFEST.in	(revision 1597)
@@ -0,0 +1,2 @@
+recursive-include quintagroup *
+recursive-exclude quintagroup *.py[co]
Index: quintagroup.transmogrifier/branches/ofs/README.txt
===================================================================
--- quintagroup.transmogrifier/branches/ofs/README.txt	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/README.txt	(revision 1597)
@@ -0,0 +1,42 @@
+***************************************
+Export/import transmogrifier blueprints
+***************************************
+
+.. contents::
+
+This package contains blueprints for collective.transmogrifier
+pipelines, that may be used to export/import Plone site content.
+It also overrides GenericSetup ``Content`` step so this package
+can be used out-the-box to migrate site content.
+
+Running test
+************
+
+In current state before running all test you need to go to
+collective.transmogrifier package and edit configure.zcml file, replacing
+
+::
+
+    <adapter factory=".transmogrifier.Transmogrifier" /> 
+
+line with
+
+::
+    <adapter 
+        factory=".transmogrifier.Transmogrifier" 
+        provides=".interfaces.ITransmogrifier" 
+        />
+
+Credits
+*******
+
+Design and development
+
+    - Bohdan Koval _ at Quintagroup_
+    - Andriy Mylenkyy _ at Quintagroup_
+    - Vitaliy Podoba
+    - Volodymyr Cherepanyak _ at Quintagroup_
+    - Myroslav Opyr _ at Quintagroup_
+
+
+.. _Quintagroup: http://www.quintagroup.com/
Index: quintagroup.transmogrifier/branches/ofs/TODO.txt
===================================================================
--- quintagroup.transmogrifier/branches/ofs/TODO.txt	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/TODO.txt	(revision 1597)
@@ -0,0 +1,7 @@
+TODO
+====
+
+- Remove importing of object UIDs
+
+- Rewrite ReferenceExporter adapter as pipeline section and merge code from
+  ReferenceImporter adapter to reference importer section.
Index: quintagroup.transmogrifier/branches/ofs/docs/HISTORY.txt
===================================================================
--- quintagroup.transmogrifier/branches/ofs/docs/HISTORY.txt	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/docs/HISTORY.txt	(revision 1597)
@@ -0,0 +1,27 @@
+Changelog
+=========
+
+0.3 (Unreleased)
+----------------
+
+ - added sections for export/import of portlet assignments [koval]
+
+0.2 (2010-01-04)
+----------------
+
+ - the "quintagroup.transmogrifier" namespace removed [koval]
+
+ - added interface import/export steps [mylan]
+
+ - merged plone21 branch [piv]
+
+ - various fixes [piv, mylan, koval]
+
+ - history update, release [chervol]
+
+
+0.1 (2009-11-19)
+----------------
+
+ - Initial release
+
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/__init__.py
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/__init__.py	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/__init__.py	(revision 1597)
@@ -0,0 +1,6 @@
+# See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages
+try:
+    __import__('pkg_resources').declare_namespace(__name__)
+except ImportError:
+    from pkgutil import extend_path
+    __path__ = extend_path(__path__, __name__)
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/__init__.py
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/__init__.py	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/__init__.py	(revision 1597)
@@ -0,0 +1,2 @@
+# this is a package
+
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/adapters/__init__.py
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/adapters/__init__.py	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/adapters/__init__.py	(revision 1597)
@@ -0,0 +1,1 @@
+# i'm a package
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/adapters/configure.zcml
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/adapters/configure.zcml	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/adapters/configure.zcml	(revision 1597)
@@ -0,0 +1,21 @@
+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    xmlns:five="http://namespaces.zope.org/five">
+
+    <!-- 'marshall' adapters -->
+    <adapter
+        factory=".exporting.ReferenceExporter"
+        name="marshall"
+        />
+
+    <adapter
+        factory=".importing.ReferenceImporter"
+        name="marshall"
+        />
+
+    <adapter
+        factory=".exporting.CriterionExporter"
+        name="marshall"
+        />
+
+</configure>
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/adapters/exporting.py
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/adapters/exporting.py	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/adapters/exporting.py	(revision 1597)
@@ -0,0 +1,82 @@
+from xml.dom import minidom
+
+from zope.interface import implements
+from zope.component import adapts
+
+from Products.Archetypes import atapi
+from Products.Archetypes.interfaces import IBaseObject
+from Products.ATContentTypes.interface import IATTopicCriterion
+from Products.Marshall.registry import getComponent
+from Products.Archetypes import config as atcfg
+
+from collective.transmogrifier.interfaces import ITransmogrifier
+
+from quintagroup.transmogrifier.interfaces import IExportDataCorrector
+
+class ReferenceExporter(object):
+    """ Add reference fields to XML, generated by Marshall product.
+    """
+    implements(IExportDataCorrector)
+    adapts(IBaseObject, ITransmogrifier)
+
+    def __init__(self, context, transmogrifier):
+        self.context = context
+        self.transmogrifier = transmogrifier
+
+    def __call__(self, data):
+        data['data'] = self.exportReferences(data['data'])
+        return data
+
+    def exportReferences(self, xml):
+        """ Marshall 1.0.0 doesn't export references, do it manually.
+        """
+        doc = minidom.parseString(xml)
+        root = doc.documentElement
+        for fname in self.context.Schema().keys():
+            if not isinstance(self.context.Schema()[fname], atapi.ReferenceField):
+                continue
+            values = self.context[fname]
+            if not values:
+                continue
+            elem = doc.createElement("field")
+            attr = doc.createAttribute("name")
+            attr.value = fname
+            elem.setAttributeNode(attr)
+            if type(values) not in (tuple, list):
+                values = [values,]
+            for value in values:
+                ref = doc.createElement('reference')
+                uid = doc.createElement('uid')
+                value = doc.createTextNode(str(value))
+                uid.appendChild(value)
+                ref.appendChild(uid)
+                elem.appendChild(ref)
+            root.appendChild(elem)
+        return doc.toxml('utf-8')
+
+class CriterionExporter(ReferenceExporter):
+    """ Special marshalling adapter for topic criterias.
+    """
+
+    implements(IExportDataCorrector)
+    adapts(IATTopicCriterion, ITransmogrifier)
+
+    def __init__(self, context, transmogrifier):
+        self.context = context
+        self.transmogrifier = transmogrifier
+        self.marshaller = getComponent('atxml')
+
+    def __call__(self, data):
+        if data['data'] is not None:
+            return super(CriterionExporter, self).__call__(data)
+        # Marshall fails when object has UID that is equal to None
+        # fix it here by setting it to empty string and after marshalling setting back to old value
+        old_uid_attr = getattr(self.context, atcfg.UUID_ATTR)
+        setattr(self.context, atcfg.UUID_ATTR, "")
+
+        ct, length, xml = self.marshaller.marshall(self.context)
+        xml = self.exportReferences(xml)
+        data['data'] = xml
+
+        setattr(self.context, atcfg.UUID_ATTR, old_uid_attr)
+        return data
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/adapters/importing.py
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/adapters/importing.py	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/adapters/importing.py	(revision 1597)
@@ -0,0 +1,74 @@
+import re
+from xml.dom import minidom
+
+from zope.interface import implements
+from zope.component import adapts
+
+from Products.Archetypes.interfaces import IBaseObject
+from Products.Archetypes import atapi
+
+from collective.transmogrifier.interfaces import ITransmogrifier
+
+from quintagroup.transmogrifier.interfaces import IImportDataCorrector
+
+EXISTING_UIDS = {}
+REFERENCE_QUEUE = {}
+
+class ReferenceImporter(object):
+    """ Demarshall content from xml file by using of Marshall product.
+    """
+    implements(IImportDataCorrector)
+    adapts(IBaseObject, ITransmogrifier)
+
+    def __init__(self, context, transmogrifier):
+        self.context = context
+        self.transmogrifier = transmogrifier
+
+    def __call__(self, data):
+        # uid = self.context.UID()
+        uid = self.getUID(data['data'])
+        if uid:
+            EXISTING_UIDS[uid] = None
+        data['data'] = self.importReferences(data['data'])
+        return data
+
+    def getUID(self, xml):
+        """ Find 'uid' element and get it's value.
+        """
+        start = re.search(r'<uid>', xml)
+        end = re.search(r'</uid>', xml)
+        if start and end:
+            uid = xml[start.end():end.start()]
+            return uid.strip()
+        return None
+
+    def importReferences(self, data):
+        """ Marshall 1.0.0 doesn't import references, do it manually.
+        """
+        doc = minidom.parseString(data)
+        root = doc.documentElement
+        for fname in self.context.Schema().keys():
+            if not isinstance(self.context.Schema()[fname], atapi.ReferenceField):
+                continue
+            uids = []
+            validUIDs = True
+            elements = [i for i in root.getElementsByTagName('field') if i.getAttribute('name') == fname]
+            if not elements:
+                # if needed elements are absent skip this field
+                # update as much as posible fields and don't raise exceptions
+                continue
+            elem = elements[0]
+            for uid_elem in elem.getElementsByTagName('uid'):
+                value = str(uid_elem.firstChild.nodeValue)
+                uids.append(value)
+                if value not in EXISTING_UIDS:
+                    validUIDs = False
+            if validUIDs:
+                mutator = self.context.Schema()[fname].getMutator(self.context)
+                mutator(uids)
+            else:
+                suid = str(root.getElementsByTagName('uid')[0].firstChild.nodeValue.strip())
+                REFERENCE_QUEUE[suid] = {}
+                REFERENCE_QUEUE[suid][fname] = uids
+            root.removeChild(elem)
+        return doc.toxml('utf-8')
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/binary.py
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/binary.py	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/binary.py	(revision 1597)
@@ -0,0 +1,206 @@
+import traceback
+from xml.dom import minidom
+
+from zope.interface import classProvides, implements
+
+from ZODB.POSException import ConflictError
+
+from Products.Archetypes.interfaces import IBaseObject
+
+from collective.transmogrifier.interfaces import ISection, ISectionBlueprint
+from collective.transmogrifier.utils import defaultMatcher, Condition
+
+class FileExporterSection(object):
+    classProvides(ISectionBlueprint)
+    implements(ISection)
+
+    def __init__(self, transmogrifier, name, options, previous):
+        self.previous = previous
+        self.context = transmogrifier.context
+
+        self.pathkey = defaultMatcher(options, 'path-key', name, 'path')
+        self.fileskey = options.get('files-key', '_files').strip()
+        # only this section can add 'excluded_field' for marshalling
+        #self.excludekey = defaultMatcher(options, 'exclude-key', name, 'excluded_fields')
+        self.excludekey = options.get('exclude-key', '_excluded_fields').strip()
+
+        self.exclude_fieldtypes = filter(None, [i.strip() for i in
+                                         options.get('exclude-fieldtypes', '').splitlines()])
+        self.doc = minidom.Document()
+        self.condition = Condition(options.get('condition', 'python:True'),
+                                   transmogrifier, name, options)
+
+    def __iter__(self):
+        for item in self.previous:
+            pathkey = self.pathkey(*item.keys())[0]
+
+            if not pathkey:
+                yield item; continue
+
+            path = item[pathkey]
+            obj = self.context.unrestrictedTraverse(path, None)
+            if obj is None:         # path doesn't exist
+                yield item; continue
+
+            if IBaseObject.providedBy(obj):
+                schema = obj.Schema()
+                binary_fields = {}
+                binary_field_names = []
+                for field in schema.keys():
+                    if obj.isBinary(field):
+                        binary_field_names.append(field)
+                        if not self.condition(item, context=obj, fname=field):
+                            continue
+                        fname, ct, data = self.extractFile(obj, field)
+                        if fname == '' or data == '':
+                            # empty file fields have empty filename and empty data
+                            # skip them
+                            continue
+                        binary_fields[field] = dict(filename=fname, mimetype=ct)
+                        files = item.setdefault(self.fileskey, {})
+                        #key = "field-%s" % field
+                        files[fname] = {
+                            # now we export FileField as file with it's original name,
+                            # but it may cause name collapse
+                            'name': fname,
+                            'data': data,
+                            'content_type': ct,
+                        }
+                if binary_fields:
+                    files['file-fields'] = {
+                        'name': '.file-fields.xml',
+                        'data': self.createManifest(binary_fields),
+                    }
+                if binary_field_names:
+                    item[self.excludekey] = binary_field_names
+
+            yield item
+
+    def extractFile(self, obj, field):
+        """ Return tuple of (filename, content_type, data)
+        """
+        field = obj.getField(field)
+        # temporarily:
+        # dirty call, I know, just lazy to get method arguments
+        # TextField overrided getBaseUnit method but didn't follow API
+        try:
+            base_unit = field.getBaseUnit(obj, full=True)
+        except TypeError, e:
+            base_unit = field.getBaseUnit(obj)
+        fname = base_unit.getFilename() 
+        ct = base_unit.getContentType()
+        value = base_unit.getRaw()
+
+        return fname, ct, value
+
+    def createManifest(self, binary_fields):
+        doc = self.doc
+
+        root = doc.createElement('manifest')
+        for fname, info in binary_fields.items():
+            # create field node
+            field = doc.createElement('field')
+
+            # set name attribute
+            attr = doc.createAttribute('name')
+            attr.value = fname
+            field.setAttributeNode(attr)
+
+            # create filename node
+            filename = doc.createElement('filename')
+            filename.appendChild(doc.createTextNode(info['filename']))
+            field.appendChild(filename)
+
+            # create mimetype node
+            mimetype = doc.createElement('mimetype')
+            mimetype.appendChild(doc.createTextNode(info['mimetype']))
+            field.appendChild(mimetype)
+
+            root.appendChild(field)
+
+        doc.appendChild(root)
+
+        try:
+            data = doc.toprettyxml(indent='  ', encoding='utf-8')
+        except UnicodeDecodeError, e:
+            # all comments are strings encoded in 'utf-8' and they will properly
+            # saved in xml file, but if we explicitly give 'utf-8' encoding
+            # UnicodeDecodeError will be raised when they have non-ascii chars
+            data = doc.toprettyxml(indent='  ')
+
+        doc.unlink()
+        return data
+
+
+class FileImporterSection(object):
+    classProvides(ISectionBlueprint)
+    implements(ISection)
+
+    def __init__(self, transmogrifier, name, options, previous):
+        self.previous = previous
+        self.context = transmogrifier.context
+
+        self.pathkey = defaultMatcher(options, 'path-key', name, 'path')
+        self.fileskey = defaultMatcher(options, 'files-key', name, 'files')
+        self.contextkey = defaultMatcher(options, 'context-key', name, 'import_context')
+
+        self.condition = Condition(options.get('condition', 'python:True'),
+                                   transmogrifier, name, options)
+
+    def __iter__(self):
+        for item in self.previous:
+            pathkey = self.pathkey(*item.keys())[0]
+            fileskey = self.fileskey(*item.keys())[0]
+            contextkey = self.contextkey(*item.keys())[0]
+
+            if not (pathkey and fileskey):
+                yield item; continue
+            if 'file-fields' not in item[fileskey]:
+                yield item; continue
+
+            path = item[pathkey]
+            obj = self.context.unrestrictedTraverse(path, None)
+            if obj is None:         # path doesn't exist
+                yield item; continue
+
+            if IBaseObject.providedBy(obj):
+                try:
+                    manifest = item[fileskey]['file-fields']['data']
+                    for field, info in self.parseManifest(manifest).items():
+                        fname = info['filename']
+                        ct = info['mimetype']
+                        if fname in item[fileskey]:
+                            data = item[fileskey][fname]['data']
+                        elif contextkey:
+                            data = context.readDataFile("%s/%s" % (path, fname))
+                            if data is None:
+                                continue
+                        if not self.condition(item, context=obj, fname=field,
+                            filename=fname, data=data, mimetype=ct):
+                            continue
+                        mutator = obj.getField(field).getMutator(obj)
+                        mutator(data, filename=fname, mimetype=ct)
+                except ConflictError:
+                    raise
+                except Exception, e:
+                    print "Exception in fileimporter section:"
+                    print '-'*60
+                    traceback.print_exc()
+                    print '-'*60
+
+            yield item
+
+    def parseManifest(self, manifest):
+        doc = minidom.parseString(manifest)
+        fields = {}
+        for elem in doc.getElementsByTagName('field'):
+            field = fields.setdefault(str(elem.getAttribute('name')), {})
+            for child in elem.childNodes:
+                if child.nodeType != child.ELEMENT_NODE:
+                    continue
+                if child.tagName == u'filename':
+                    field['filename'] = child.firstChild.nodeValue.strip().encode('utf-8')
+                elif child.tagName == u'mimetype':
+                    field['mimetype'] = str(child.firstChild.nodeValue.strip())
+
+        return fields
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/binary.txt
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/binary.txt	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/binary.txt	(revision 1597)
@@ -0,0 +1,94 @@
+File fields exporter and importer sections
+==========================================
+
+File fields importer and exporter sections are used to extract data from
+Archetypes file fields. The exporter section blueprint name is
+``quintagroup.transmogrifier.fileexporter`` and the importer section blueprint
+name is ``quintagroup.transmogrifier.fileimporter``.
+
+Exporter needs relative path to the object, that have file fields. Improter
+needs path and data for updating fields.
+
+Both sections has ``path-key`` option which specify key in item where path to
+object is stored (``_path`` is default) and ``files-key`` option that gives
+key where XML data will be or is stored. Exporter section also has ``exclude-key``
+option, that specifies key where list of file field names will be stored
+(default is ``_excluded_fields``). These list will be used in marshaller
+section.
+
+Also this section provides condition option which, if specified, exports/imports
+this or another binary field only if condition expression evaluates to true.
+
+>>> import pprint
+>>> binary = """
+... [transmogrifier]
+... pipeline =
+...     binarysource
+...     fileexporter
+...     fileimporter
+...     printer
+...     dataprinter
+... 
+... [binarysource]
+... blueprint = quintagroup.transmogrifier.tests.binarysource
+... 
+... [fileexporter]
+... blueprint = quintagroup.transmogrifier.fileexporter
+... condition = python:fname != 'image'
+... 
+... [dataprinter]
+... blueprint = quintagroup.transmogrifier.tests.dataprinter
+... print = 
+...     _files
+...     file-fields
+...     data
+... 
+... [fileimporter]
+... blueprint = quintagroup.transmogrifier.fileimporter
+... 
+... [printer]
+... blueprint = collective.transmogrifier.sections.tests.pprinter
+... """
+>>> registerConfig(u'quintagroup.transmogrifier.tests.marshall', binary)
+>>> transmogrifier(u'quintagroup.transmogrifier.tests.marshall') # doctest: +REPORT_NDIFF, +ELLIPSIS
+{}
+{'_path': 'not/existing/bar'}
+{'_path': 'spam/eggs/notatcontent'}
+{'_excluded_fields': ['image', 'file'],
+ '_files': {'archive.tar.gz': {'content_type': 'application/x-tar',
+                               'data': 'binary data',
+                               'name': 'archive.tar.gz'},
+            'file-fields': {'data': ...,
+                            'name': '.file-fields.xml'}},
+ '_path': 'spam/eggs/foo'}
+<?xml version="1.0" encoding="utf-8"?>
+<manifest>
+  <field name="file">
+    <filename>
+      archive.tar.gz
+    </filename>
+    <mimetype>
+      application/x-tar
+    </mimetype>
+  </field>
+</manifest>
+<BLANKLINE>
+>>> pprint.pprint(plone.updated)
+('archive.tar.gz', 'application/x-tar', 'binary data')
+
+TODO: write test for getting data for fields from import context
+
+The ``condition`` expression hass access to the following:
+
+=================== ==========================================================
+ ``item``            the current pipeline item
+ ``transmogrifier``  the transmogrifier
+ ``name``            the name of the splitter section
+ ``options``         the splitter options
+ ``modules``         sys.modules
+ ``context``         the current content object
+ ``fname``           the name of the field being processed
+ ``filename``        the file name binary field is loaded into (import only)
+ ``data``            data read from the file (import only)
+ ``mimetype``        data mimetype (import only)
+=================== ==========================================================
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/catalogsource.py
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/catalogsource.py	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/catalogsource.py	(revision 1597)
@@ -0,0 +1,131 @@
+from zope.interface import classProvides, implements
+from zope.annotation.interfaces import IAnnotations
+
+from collective.transmogrifier.interfaces import ISection, ISectionBlueprint
+
+from Products.CMFCore import utils
+
+from quintagroup.transmogrifier.logger import VALIDATIONKEY
+
+class CatalogSourceSection(object):
+    classProvides(ISectionBlueprint)
+    implements(ISection)
+
+    def __init__(self, transmogrifier, name, options, previous):
+        self.previous = previous
+        self.context = transmogrifier.context
+
+        # next is for communication with 'logger' section
+        self.anno = IAnnotations(transmogrifier)
+        self.storage = self.anno.setdefault(VALIDATIONKEY, [])
+
+        self.pathkey = options.pop('path-key', '_path')
+        self.entrieskey = options.pop('entries-key', '_entries')
+
+        # remove 'blueprint' option - it cannot be a query
+        options.pop('blueprint')
+
+        self.query = {}
+        for k, v in options.items():
+            for p in v.split(';'):
+                params = p.split('=', 1)
+                if len(params) == 1:
+                    self.query[k] = p.strip()
+                else :
+                    q = self.query.setdefault(k, {})
+                    q[params[0].strip()] = params[1].strip()
+
+        self.catalog = utils.getToolByName(self.context, 'portal_catalog')
+
+    def __iter__(self):
+        for item in self.previous:
+            yield item
+
+        exported = []
+
+        results = list(self.catalog(**self.query))
+        results.sort(key=lambda x: x.getPath())
+        for brain in results:
+            # discussion items are indexed and they must be replaced to
+            # content objects to which they correspond
+            # we need to skip them
+            if brain.portal_type == 'Discussion Item':
+                path =  '/'.join(brain.getPath().split('/')[:-2])
+                cp, id_ = path.rsplit('/', 1)
+                brain = self.catalog(path=cp, id=id_)[0]
+            else:
+                path = brain.getPath()
+
+            # folderish objects are tried to export twice:
+            # when their contained items are exported and when they are
+            # returned in catalog search results
+            if path in exported:
+                continue
+            exported.append(path)
+
+            # export also all parents of current object
+            containers = []
+            container_path = path.rsplit('/', 1)[0]
+            while container_path:
+                if container_path in exported:
+                    container_path = container_path.rsplit('/', 1)[0]
+                    continue
+                contained = self.getContained(container_path)
+                if contained:
+                    exported.append(container_path)
+                    containers.append({
+                        self.pathkey: '/'.join(container_path.split('/')[2:]),
+                        self.entrieskey: contained,
+                    })
+                container_path = container_path.rsplit('/', 1)[0]
+            containers.reverse()
+            # order metter for us
+            for i in containers:
+                self.storage.append(i[self.pathkey])
+                yield i
+
+            item = {
+                self.pathkey: '/'.join(path.split('/')[2:]),
+            }
+            if brain.is_folderish:
+                contained = self.getContained(path)
+                if contained:
+                    item[self.entrieskey] = contained
+
+            self.storage.append(item[self.pathkey])
+            yield item
+
+        # cleanup
+        if VALIDATIONKEY in self.anno:
+            del self.anno[VALIDATIONKEY]
+
+    def getContained(self, path):
+        """ Return list of (object_id, portal_type) for objects that are returned by catalog
+            and contained in folder with given 'path'.
+        """
+        results = []
+        seen = []
+        raw_results = self.catalog(path=path, **self.query)
+        for brain in raw_results:
+            current = brain.getPath()
+            relative = current[len(path):]
+            relative = relative.strip('/')
+            if not relative:
+                # it's object with path that was given in catalog query
+                continue
+            elif '/' in relative:
+                # object stored in subfolders, we need append to results their parent folder
+                parent_path = '/'.join([path, relative.split('/', 1)[0]])
+                if parent_path not in seen:                    
+                    res = self.catalog(path=path) #, meta_type='Folder')
+                    for i in res:
+                        if i.getPath() == parent_path:
+                            results.append(i)
+                            seen.append(parent_path)
+                            break
+            elif current not in seen:
+                # object is directly stored in folder, that has path given in query
+                seen.append(current)
+                results.append(brain)
+        contained = [(i.getId, str(i.portal_type)) for i in results]
+        return tuple(contained)
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/catalogsource.txt
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/catalogsource.txt	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/catalogsource.txt	(revision 1597)
@@ -0,0 +1,54 @@
+Catalog source section
+======================
+
+This is a source section that returns items queried from catalog. The catalog
+source section blueprint name is ``quintagroup.transmogrifier.catalogsource``.
+
+Catalog source section has next options:
+  - ``path-key' (default ``_path``) - key where object path is stored
+  - ``entries-key`` (default ``_entries``) - key where data for manifest section
+     is stored (tuple of contained items)
+
+All other options are assumed to be catalog query parameters, option name
+corresponds to index and its value is value for search or string representation
+of record object. Record object is a dictionary, which items are separeted by
+``;`` and key is separeted from value with ``=``.
+
+Test parsing os catalog query, which is given in section options (here we
+manually initialize section with that query).
+
+>>> from pprint import pprint
+>>> from quintagroup.transmogrifier.catalogsource import CatalogSourceSection
+>>> options = {'modified': 'query = 2008/11/02 12:00:00 GMT+0 ; range = min',
+...            'Type': 'Document',
+...            'blueprint': 'quintagroup.transmogrifier.catalogsource'}
+>>> section = CatalogSourceSection(transmogrifier, 'catalogsource', options, object())
+>>> pprint(section.query)
+{'Type': 'Document',
+ 'modified': {'query': '2008/11/02 12:00:00 GMT+0', 'range': 'min'}}
+
+Now test functionality of this section.
+
+>>> catalogsource = """
+... [transmogrifier]
+... pipeline =
+...     catalogsource
+...     printer
+...
+... [catalogsource]
+... blueprint = quintagroup.transmogrifier.catalogsource
+... modified = query = 2008-11-02T12:00:00Z; range = min
+... 
+... [printer]
+... blueprint = collective.transmogrifier.sections.tests.pprinter
+... """
+>>> registerConfig(u'quintagroup.transmogrifier.tests.catalogsource',
+...                catalogsource)
+>>> transmogrifier(u'quintagroup.transmogrifier.tests.catalogsource') #doctest: +REPORT_NDIFF
+{'_entries': (('folder2', 'Folder'), ('folder1', 'Folder')), '_path': ''}
+{'_entries': (('document2', 'Document'), ('document3', 'Document')),
+ '_path': 'folder1'}
+{'_path': 'folder1/document2'}
+{'_path': 'folder1/document3'}
+{'_entries': (('document4', 'Document'),), '_path': 'folder2'}
+{'_path': 'folder2/document4'}
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/comments.py
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/comments.py	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/comments.py	(revision 1597)
@@ -0,0 +1,183 @@
+from xml.dom import minidom
+
+from zope.interface import classProvides, implements
+
+from Acquisition import aq_base
+from Products.CMFCore import utils
+from Products.CMFDefault import DiscussionItem
+from Products.CMFDefault.exceptions import DiscussionNotAllowed
+
+from collective.transmogrifier.interfaces import ISection, ISectionBlueprint
+from collective.transmogrifier.utils import defaultMatcher
+
+class CommentsExporterSection(object):
+    classProvides(ISectionBlueprint)
+    implements(ISection)
+
+    def __init__(self, transmogrifier, name, options, previous):
+        self.previous = previous
+        self.context = transmogrifier.context
+
+        self.pathkey = defaultMatcher(options, 'path-key', name, 'path')
+        self.fileskey = options.get('files-key', '_files').strip()
+
+        self.doc = minidom.Document()
+
+    def __iter__(self):
+        for item in self.previous:
+            pathkey = self.pathkey(*item.keys())[0]
+
+            if not pathkey:
+                yield item; continue
+
+            path = item[pathkey]
+            obj = self.context.unrestrictedTraverse(path, None)
+            if obj is None:         # path doesn't exist
+                yield item; continue
+
+            # check if object has comments
+            discussion_container = getattr(aq_base(obj), 'talkback', None)
+            if discussion_container is not None:
+                data = self.extractComments(discussion_container)
+                if data:
+                    files = item.setdefault(self.fileskey, {})
+                    item[self.fileskey]['comments'] = {
+                        'name': '.comments.xml',
+                        'data': data,
+                    }
+
+            yield item
+
+    def extractComments(self, container):
+        doc = self.doc
+
+        items = container.objectItems()
+        if not items:
+            return None
+
+        root = doc.createElement('discussion')
+        doc.appendChild(root)
+        for item_id, item in items:
+            hdrlist = item.getMetadataHeaders()
+            # get creator (it is displayed in "Posted by")
+            hdrlist.append(('Creator', item.Creator()))
+            # get modification date (also is displayed)
+            hdrlist.append(('Modification_date', item.ModificationDate()))
+            # get relation
+            hdrlist.append(('In_reply_to', str(item.in_reply_to)))
+            # get comment text
+            hdrlist.append(('Text', item.text))
+
+            item_elem = doc.createElement('item')
+            attr = doc.createAttribute('id')
+            attr.value = item_id
+            item_elem.setAttributeNode(attr)
+
+            for k, v in hdrlist:
+                field = doc.createElement('field')
+                attr = doc.createAttribute('name')
+                attr.value = k
+                field.setAttributeNode(attr)
+                text = doc.createTextNode(v)
+                field.appendChild(text)
+                item_elem.appendChild(field)
+
+            root.appendChild(item_elem)
+
+
+        try:
+            data = self.doc.toprettyxml(indent='  ', encoding='utf-8')
+        except UnicodeError, e:
+            # all comments are strings encoded in 'utf-8' and they will properly
+            # saved in xml file, but if we explicitly give 'utf-8' encoding
+            # UnicodeDecodeError will be raised when they have non-ascii chars
+            data = self.doc.toprettyxml(indent='  ')
+
+        self.doc.unlink()
+        return data
+
+class CommentsImporterSection(object):
+    classProvides(ISectionBlueprint)
+    implements(ISection)
+
+    def __init__(self, transmogrifier, name, options, previous):
+        self.previous = previous
+        self.context = transmogrifier.context
+
+        self.pathkey = defaultMatcher(options, 'path-key', name, 'path')
+        self.fileskey = defaultMatcher(options, 'files-key', name, 'files')
+
+        self.dtool = utils.getToolByName(self.context, 'portal_discussion')
+
+    def __iter__(self):
+        for item in self.previous:
+            pathkey = self.pathkey(*item.keys())[0]
+            fileskey = self.fileskey(*item.keys())[0]
+
+            if not (pathkey and fileskey):
+                yield item; continue
+
+            if 'comments' not in item[fileskey]:
+                yield item; continue
+
+            path = item[pathkey]
+            obj = self.context.unrestrictedTraverse(path, None)
+            if obj is None:         # path doesn't exist
+                yield item; continue
+
+            # allow discussion if it wasn't allowed (because we have comments)
+            try:
+                discussion_container = self.dtool.getDiscussionFor(obj)
+            except DiscussionNotAllowed:
+                obj.allow_discussion = True
+                discussion_container = self.dtool.getDiscussionFor(obj)
+
+            data = item[fileskey]['comments']['data']
+            discussion_container._container.clear()
+            for id_, props in self.parseXML(data).items():
+                comment = DiscussionItem.DiscussionItem(id_)
+                discussion_container._container[id_] = comment
+                comment = comment.__of__(discussion_container)
+                self.updateDiscussionItem(comment, props)
+
+            yield item
+
+    def parseXML(self, data):
+        doc = minidom.parseString(data)
+        root = doc.documentElement
+
+        items = {}
+        for child in root.childNodes:
+            if child.nodeName != 'item':
+                continue
+            id_ = str(child.getAttribute('id'))
+            item = items[id_] = {}
+            for field in child.childNodes:
+                if field.nodeName != 'field':
+                    continue
+                name = field.getAttribute('name')
+                # this will be unicode string, encode it?
+                value = ''
+                for node in field.childNodes:
+                    if node.nodeName != '#text':
+                        continue
+                    lines = [line.lstrip() for line in node.nodeValue.splitlines()]
+                    value += '\n'.join(lines)
+                item[name] = value.strip()
+
+        return items
+
+    def updateDiscussionItem(self, item, props):
+        in_reply_to = props['In_reply_to']
+        # if 'In_reply_to' field is string "None" we need to set attribute to None
+        if in_reply_to == 'None':
+            item.in_reply_to = None
+        else:
+            item.in_reply_to = in_reply_to
+
+        item.addCreator(props['Creator'])
+        item.setFormat('text/plain')
+        item.setMetadata(props)
+        item._edit(text=props['Text'])
+        item.setModificationDate(props['Modification_date'])
+        item.indexObject()
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/comments.txt
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/comments.txt	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/comments.txt	(revision 1597)
@@ -0,0 +1,102 @@
+Comments exporter and importer sections
+=======================================
+
+Comments exporter and importer sections are used to migrate comments for site
+content. The comments exporter section blueprint name is
+``quintagroup.transmogrifier.commentsexporter`` and importer section blueprint
+name is ``quintagroup.transmogrifier.commentsimporter``.
+
+Comments are represented in XML format. Both sections need relative path to the
+object with comments and importer also needs XML data for adding comments.
+
+Both sections has ``path-key`` option which specify key in item where path to
+object is stored (``_path`` is default) and ``files-key`` option that gives
+key where XML data will be or is stored.
+
+>>> import pprint
+>>> comments = """
+... [transmogrifier]
+... pipeline =
+...     commentssource
+...     commentsexporter
+...     printer
+...     dataprinter
+...     commentsimporter
+...     
+... [commentssource]
+... blueprint = quintagroup.transmogrifier.tests.commentssource
+... 
+... [commentsexporter]
+... blueprint = quintagroup.transmogrifier.commentsexporter
+... 
+... [printer]
+... blueprint = collective.transmogrifier.sections.tests.pprinter
+... 
+... [dataprinter]
+... blueprint = quintagroup.transmogrifier.tests.dataprinter
+... print = 
+...     _files
+...     comments
+...     data
+... 
+... [commentsimporter]
+... blueprint = quintagroup.transmogrifier.commentsimporter
+... """
+>>> registerConfig(u'quintagroup.transmogrifier.tests.comments', comments)
+>>> transmogrifier(u'quintagroup.transmogrifier.tests.comments') # doctest: +ELLIPSIS, +REPORT_NDIFF
+{}
+{'_path': 'not/existing/bar'}
+{'_path': 'spam/eggs/notdiscussable'}
+{'_files': {'comments': {'data': ...
+                         'name': '.comments.xml'}},
+ '_path': 'spam/eggs/foo'}
+...
+<discussion>
+  <item id="1">
+    <field name="Creator">
+      creator
+    </field>
+    <field name="Modification_date">
+      date
+    </field>
+    <field name="In_reply_to">
+      None
+    </field>
+    <field name="Text">
+      comment to content
+    </field>
+  </item>
+  <item id="2">
+    <field name="Creator">
+      creator
+    </field>
+    <field name="Modification_date">
+      date
+    </field>
+    <field name="In_reply_to">
+      1
+    </field>
+    <field name="Text">
+      reply to first comment
+    </field>
+  </item>
+  <item id="3">
+    <field name="Creator">
+      creator
+    </field>
+    <field name="Modification_date">
+      date
+    </field>
+    <field name="In_reply_to">
+      None
+    </field>
+    <field name="Text">
+      other comment to content
+    </field>
+  </item>
+</discussion>
+<BLANKLINE>
+>>> pprint.pprint(plone._container)
+{'1': <DicussionItem creator date None comment to content>,
+ '2': <DicussionItem creator date 1 reply to first comment>,
+ '3': <DicussionItem creator date None other comment to content>}
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/configure.zcml
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/configure.zcml	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/configure.zcml	(revision 1597)
@@ -0,0 +1,213 @@
+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    xmlns:five="http://namespaces.zope.org/five"
+    xmlns:transmogrifier="http://namespaces.plone.org/transmogrifier"
+    xmlns:gs="http://namespaces.zope.org/genericsetup"
+    xmlns:browser="http://namespaces.zope.org/browser"
+    i18n_domain="quintagroup.transmogrifier">
+
+    <include package="collective.transmogrifier" file="meta.zcml" />
+    <include package="collective.transmogrifier" />
+    <include package="plone.app.transmogrifier" />
+
+    <!-- register our blueprints -->
+    <utility
+        component=".sitewalker.SiteWalkerSection"
+        name="quintagroup.transmogrifier.sitewalker"
+        />
+
+    <utility
+        component=".manifest.ManifestExporterSection"
+        name="quintagroup.transmogrifier.manifestexporter"
+        />
+
+    <utility
+        component=".manifest.ManifestImporterSection"
+        name="quintagroup.transmogrifier.manifestimporter"
+        />
+
+    <utility
+        component=".marshall.MarshallerSection"
+        name="quintagroup.transmogrifier.marshaller"
+        />
+
+    <utility
+        component=".marshall.DemarshallerSection"
+        name="quintagroup.transmogrifier.demarshaller"
+        />
+
+    <utility
+        component=".propertymanager.PropertiesExporterSection"
+        name="quintagroup.transmogrifier.propertiesexporter"
+        />
+
+    <utility
+        component=".propertymanager.PropertiesImporterSection"
+        name="quintagroup.transmogrifier.propertiesimporter"
+        />
+
+    <utility
+        component=".comments.CommentsExporterSection"
+        name="quintagroup.transmogrifier.commentsexporter"
+        />
+
+    <utility
+        component=".comments.CommentsImporterSection"
+        name="quintagroup.transmogrifier.commentsimporter"
+        />
+
+    <utility
+        component=".datacorrector.DataCorrectorSection"
+        name="quintagroup.transmogrifier.datacorrector"
+        />
+
+    <!-- registration of adapters for data corrector section -->
+    <include package=".adapters" />
+
+    <utility
+        component=".writer.WriterSection"
+        name="quintagroup.transmogrifier.writer"
+        />
+
+    <utility
+        component=".reader.ReaderSection"
+        name="quintagroup.transmogrifier.reader"
+        />
+
+    <utility
+        component=".substitution.SubstitutionSection"
+        name="quintagroup.transmogrifier.substitution"
+        />
+
+    <utility
+        component=".xslt.XSLTSection"
+        name="quintagroup.transmogrifier.xslt"
+        />
+
+    <utility
+        component=".logger.LoggerSection"
+        name="quintagroup.transmogrifier.logger"
+        />
+
+    <utility
+        component=".references.ReferencesImporterSection"
+        name="quintagroup.transmogrifier.referencesimporter"
+        />
+
+    <utility
+        component=".binary.FileExporterSection"
+        name="quintagroup.transmogrifier.fileexporter"
+        />
+
+    <utility
+        component=".binary.FileImporterSection"
+        name="quintagroup.transmogrifier.fileimporter"
+        />
+
+    <utility
+        component=".catalogsource.CatalogSourceSection"
+        name="quintagroup.transmogrifier.catalogsource"
+        />
+
+    <utility
+        component=".flushcache.FlushCacheSection"
+        name="quintagroup.transmogrifier.flushcache"
+        provides="collective.transmogrifier.interfaces.ISectionBlueprint"
+        />
+
+    <utility
+        component=".interfacemanager.InterfacesExporterSection"
+        name="quintagroup.transmogrifier.interfacesexporter"
+        />
+
+    <utility
+        component=".interfacemanager.InterfacesImporterSection"
+        name="quintagroup.transmogrifier.interfacesimporter"
+        />
+
+    <utility
+        component=".portlets.PortletsExporterSection"
+        name="quintagroup.transmogrifier.portletsexporter"
+        />
+
+    <utility
+        component=".portlets.PortletsImporterSection"
+        name="quintagroup.transmogrifier.portletsimporter"
+        />
+
+    <adapter factory=".portlets.PortletAssignmentExportImportHandler" />
+
+    <transmogrifier:registerConfig
+        name="export"
+        title="Export pipeline configuration"
+        description="This is a Plone content export pipeline configuration."
+        configuration="export.cfg"
+        />
+
+    <!-- We registry this export step in zcml and it will override Plone's standard 
+         Content export step. Than we need to go in ZMI to Manage tab of portal_setup
+         tool and delete old step. (I also detected that steps registered in zcml
+         overriede those registered in profile without deleting them on manage tab)
+    -->
+    <gs:exportStep
+        name="content_quinta"
+        title="Content (transmogrifier)"
+        description="Export the site's structure and content."
+        handler=".exportimport.exportSiteStructure"
+        />
+
+    <transmogrifier:registerConfig
+        name="import"
+        title="Import pipeline configuration"
+        description="This is a Plone content import pipeline configuration."
+        configuration="import.cfg"
+        />
+
+    <!-- With import step we can't do the same. Plone's portal_setup tool currently
+         works such that if we delete standard Content import step on Manage tab
+         it will be recreated when rendering Import tab. (also this step is overriden
+         when we import all steps, but old handler is used if we select only content
+         step).
+
+         <gs:importStep
+            name="content_quinta"
+            title="Content (transmogrifier)"
+            description="Import the site's structure and content."
+            handler=".exportimport.importSiteStructure"
+            />
+
+         That's why we can go another way and register adapter which is looked up in
+         standard handler.
+
+         <adapter
+            for="Products.CMFPlone.interfaces.IPloneSiteRoot"
+            provides="Products.GenericSetup.interfaces.IFilesystemImporter"
+            factory=".exportimport.PloneSiteImporter"
+            />
+    -->
+
+    <gs:importStep
+        name="content_quinta"
+        title="Content (transmogrifier)"
+        description="Import the site's structure and content."
+        handler=".exportimport.importSiteStructure"
+        />
+
+    <browser:page
+        name="pipeline-config"
+        for="Products.CMFPlone.interfaces.IPloneSiteRoot"
+        permission="zope2.ViewManagementScreens"
+        class=".configview.PipelineConfigView"
+        template="pipelineconfig.pt"
+        />
+
+    <gs:registerProfile
+        name="default"
+        title="quintagroup.transmogrifier (adds Content Migration configlet)"
+        directory="profiles/default"
+        description="Profile that adds Content Migration configlet."
+        for="Products.CMFPlone.interfaces.IPloneSiteRoot"
+        provides="Products.GenericSetup.interfaces.EXTENSION"
+        />
+
+</configure>
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/configview.py
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/configview.py	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/configview.py	(revision 1597)
@@ -0,0 +1,70 @@
+from zope.annotation import IAnnotations
+
+from Products.Five.browser import BrowserView
+
+from collective.transmogrifier.transmogrifier import configuration_registry
+
+ANNOKEY = 'quintagroup.transmogrifier.config'
+
+class PipelineConfigView(BrowserView):
+    """ View for setting persistent pipeline config.
+    """
+
+    def __init__(self, context, request):
+        super(PipelineConfigView, self).__init__(context, request)
+        self.anno = IAnnotations(context)
+        self.status = None
+
+    def __call__(self):
+        action = self.request.form.get('action')
+        if action is not None:
+            stat = []
+            # handle export config
+            export_config = self.request.form['export'].strip()
+            expkey = ANNOKEY+'.export'
+            oldconfig = self.getConfig('export')
+            if export_config and self._configChanged(oldconfig, export_config):
+                self.anno[expkey] = export_config
+                stat.append('updated export')
+            elif not export_config and expkey in self.anno:
+                del self.anno[expkey]
+                stat.append('removed export')
+            # handle import config
+            import_config = self.request.form['import'].strip()
+            impkey = ANNOKEY+'.import'
+            oldconfig = self.getConfig('import')
+            if import_config and self._configChanged(oldconfig, import_config):
+                self.anno[impkey] = import_config
+                stat.append('updated import')
+            elif not import_config and impkey in self.anno:
+                del self.anno[impkey]
+                stat.append('removed import')
+            if stat:
+                self.status = 'Changes: %s configuration.' % ' and '.join(stat)
+            else:
+                self.status = 'No changes'
+
+        return self.index()
+
+    def getConfig(self, type_):
+        key = '%s.%s' % (ANNOKEY, type_)
+        if key in self.anno:
+            return self.anno[key]
+        else:
+            fname = configuration_registry.getConfiguration(type_)['configuration']
+            return file(fname).read()
+
+    def _configChanged(self, old, new):
+        """ Compare configs with normalization of line endings.
+        """
+        if old == new:
+            return False
+        if old == new.replace('\r\n', '\n'):
+            return False
+        if old.strip() == new.replace('\r\n', '\n'):
+            return False
+        return True
+
+    def isDefault(self, type_):
+        key = '%s.%s' % (ANNOKEY, type_)
+        return key not in self.anno
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/datacorrector.py
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/datacorrector.py	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/datacorrector.py	(revision 1597)
@@ -0,0 +1,60 @@
+from zope.interface import classProvides, implements
+from zope.component import queryMultiAdapter
+
+from collective.transmogrifier.interfaces import ISection, ISectionBlueprint
+from collective.transmogrifier.utils import defaultMatcher
+
+from quintagroup.transmogrifier.interfaces import IExportDataCorrector
+from quintagroup.transmogrifier.interfaces import IImportDataCorrector
+
+class DataCorrectorSection(object):
+    classProvides(ISectionBlueprint)
+    implements(ISection)
+
+    def __init__(self, transmogrifier, name, options, previous):
+        self.previous = previous
+        self.transmogrifier = transmogrifier
+        self.context = transmogrifier.context
+
+        self.pathkey = defaultMatcher(options, 'path-key', name, 'path')
+        self.fileskey = defaultMatcher(options, 'files-key', name, 'files')
+
+        # 'type' options specifies adapter interface
+        self.type_ = options.get('type', 'export')
+        if self.type_ == 'export':
+            self.interface = IExportDataCorrector
+        elif self.type_ == 'import':
+            self.interface = IImportDataCorrector
+        else:
+            self.interface = None
+
+        # 'sources' specifies names of adapters
+        self.sources = options.get('sources', '')
+        self.sources = filter(None, [i.strip() for i in self.sources.splitlines()])
+
+    def __iter__(self):
+        for item in self.previous:
+            pathkey = self.pathkey(*item.keys())[0]
+            fileskey = self.fileskey(*item.keys())[0]
+
+            if not (pathkey and fileskey and self.sources):
+                yield item; continue
+
+            path = item[pathkey]
+            obj = self.context.unrestrictedTraverse(path, None)
+            if obj is None:         # path doesn't exist
+                yield item; continue
+
+            file_store = item[fileskey]
+            if not file_store:
+                yield item; continue
+
+            for name in self.sources:
+                if not name in file_store:
+                    continue
+                adapter = queryMultiAdapter((obj, self.transmogrifier),
+                                            self.interface, name)
+                if adapter:
+                    file_store[name] = adapter(file_store[name])
+
+            yield item
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/datacorrector.txt
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/datacorrector.txt	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/datacorrector.txt	(revision 1597)
@@ -0,0 +1,58 @@
+Data corrector section
+======================
+
+A data corrector section lets you use context sensitive components (adapters)
+to do needed corrections in data generated in previous sections. The data
+corrector section blueprint name is ``quintagroup.transmogrifier.datacorrector``.
+
+Data corrector needs next information: the path to the object (context), the
+key in item on which is stored contaired with data from previous sections and
+sources -  list of keys in this container which points to data, that need to
+be corrected. Every key in this list is also a name of adapter to lookup.
+
+Adapter are of two types: ``export`` and ``import``. What to use is controlled
+by ``type`` option, ``export`` is default value.
+
+Paths to objects are always interpreted as relative to the context.
+
+>>> datacorrector = """
+... [transmogrifier]
+... pipeline =
+...     source
+...     exportcorrector
+...     importcorrector
+...     printer
+...     
+... [source]
+... blueprint = quintagroup.transmogrifier.tests.datacorrectorsource
+... 
+... [exportcorrector]
+... blueprint = quintagroup.transmogrifier.datacorrector
+... sources = marshall
+... 
+... [importcorrector]
+... blueprint = quintagroup.transmogrifier.datacorrector
+... type = import
+... sources = 
+...     manifest
+...     unchanged
+...     nodata
+... 
+... [printer]
+... blueprint = collective.transmogrifier.sections.tests.pprinter
+... 
+... """
+>>> registerConfig(u'quintagroup.transmogrifier.tests.datacorrector', datacorrector)
+>>> transmogrifier(u'quintagroup.transmogrifier.tests.datacorrector') # doctest: +ELLIPSIS, +REPORT_NDIFF
+{}
+{'_files': {'marshall': "item hasn't path"}}
+{'_path': 'spam/eggs/foo'}
+{'_path': 'not/existing/bar'}
+{'_files': {'marshall': "object isn't adaptable"},
+ '_path': 'spam/eggs/notadaptable'}
+{'_files': {'marshall': 'modified export data',
+            'unchanged': 'this must be unchanged'},
+ '_path': 'spam/eggs/foo'}
+{'_files': {'manifest': 'modified import data',
+            'unchanged': 'this must be unchanged'},
+ '_path': 'spam/eggs/foo'}
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/export.cfg
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/export.cfg	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/export.cfg	(revision 1597)
@@ -0,0 +1,49 @@
+[transmogrifier]
+pipeline =
+    sitewalker
+    manifestexporter
+    fileexporter
+    marshaller
+    propertiesexporter
+    commentsexporter
+    datacorrector
+    portletsexporter
+    writer
+    EXPORTING
+
+[sitewalker]
+blueprint = quintagroup.transmogrifier.sitewalker
+
+[manifestexporter]
+blueprint = quintagroup.transmogrifier.manifestexporter
+
+[fileexporter]
+blueprint = quintagroup.transmogrifier.fileexporter
+
+[marshaller]
+blueprint = quintagroup.transmogrifier.marshaller
+
+[propertiesexporter]
+blueprint = quintagroup.transmogrifier.propertiesexporter
+exclude = title
+
+[commentsexporter]
+blueprint = quintagroup.transmogrifier.commentsexporter
+
+[datacorrector]
+blueprint = quintagroup.transmogrifier.datacorrector
+sources =
+    marshall
+
+[portletsexporter]
+blueprint = quintagroup.transmogrifier.portletsexporter
+
+[writer]
+blueprint = quintagroup.transmogrifier.writer
+prefix = structure
+
+[EXPORTING]
+blueprint = quintagroup.transmogrifier.logger
+keys = 
+    _type
+    _path
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/exportimport.py
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/exportimport.py	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/exportimport.py	(revision 1597)
@@ -0,0 +1,165 @@
+# -*- coding: utf-8 -*-
+import os
+import tempfile
+
+from zope.interface import implements
+from zope.annotation import IAnnotations
+
+from collective.transmogrifier.interfaces import ITransmogrifier
+from collective.transmogrifier.transmogrifier import _load_config, constructPipeline
+from collective.transmogrifier.transmogrifier import configuration_registry
+
+from Products.GenericSetup.context import TarballExportContext, TarballImportContext
+from Products.GenericSetup.interfaces import IFilesystemImporter
+
+from quintagroup.transmogrifier.writer import WriterSection
+from quintagroup.transmogrifier.reader import ReaderSection
+from quintagroup.transmogrifier.configview import ANNOKEY
+
+EXPORT_CONFIG = 'export'
+IMPORT_CONFIG = 'import'
+
+CONFIGFILE = None
+def registerPersistentConfig(site, type_):
+    """ Try to get persistent pipeline configuration of given type (export or import)
+        and register it for use with transmogrifier.
+    """
+    global CONFIGFILE
+    anno = IAnnotations(site)
+    key = '%s.%s' % (ANNOKEY, type_)
+    config = anno.has_key(key) and anno[key] or None
+
+    # unregister old config
+    name = 'persitent-%s' % type_
+    if name in configuration_registry._config_ids:
+        configuration_registry._config_ids.remove(name)
+        del configuration_registry._config_info[name]
+
+    # register new
+    if config is not None:
+        title = description = u'Persistent %s pipeline'
+        tf = tempfile.NamedTemporaryFile('w+t', suffix='.cfg')
+        tf.write(config)
+        tf.seek(0)
+        CONFIGFILE = tf
+        configuration_registry.registerConfiguration(name, title, description, tf.name)
+        return name
+    else:
+        return None
+
+def exportSiteStructure(context):
+
+    transmogrifier = ITransmogrifier(context.getSite())
+
+    # we don't use transmogrifer's __call__ method, because we need to do
+    # some modification in pipeline sections
+
+    config_name = registerPersistentConfig(context.getSite(), 'export')
+    if config_name is None:
+        transmogrifier._raw = _load_config(EXPORT_CONFIG)
+    else:
+        transmogrifier._raw = _load_config(config_name)
+        global CONFIGFILE
+        CONFIGFILE = None
+    transmogrifier._data = {}
+
+    options = transmogrifier._raw['transmogrifier']
+    sections = options['pipeline'].splitlines()
+    pipeline = constructPipeline(transmogrifier, sections)
+
+    last_section = pipeline.gi_frame.f_locals['self']
+
+    # if 'quintagroup.transmogrifier.writer' section's export context is
+    # tarball replace it with given function argument
+    while hasattr(last_section, 'previous'):
+        if isinstance(last_section, WriterSection) and \
+            isinstance(last_section.export_context, TarballExportContext):
+            last_section.export_context = context
+        last_section = last_section.previous
+        # end cycle if we get empty starter section
+        if type(last_section) == type(iter(())):
+            break
+        last_section = last_section.gi_frame.f_locals['self']
+
+    # Pipeline execution
+    for item in pipeline:
+        pass # discard once processed
+
+def importSiteStructure(context):
+
+    # Only run step if a flag file is present
+    if context.readDataFile('quintagroup.transmogrifier-import.txt') is None:
+        if getattr(context, '_archive', None) is None:
+            return
+
+    transmogrifier = ITransmogrifier(context.getSite())
+
+    # we don't use transmogrifer's __call__ method, because we need to do
+    # some modification in pipeline sections
+
+    config_name = registerPersistentConfig(context.getSite(), 'import')
+    if config_name is None:
+        transmogrifier._raw = _load_config(IMPORT_CONFIG)
+    else:
+        transmogrifier._raw = _load_config(config_name)
+        global CONFIGFILE
+        CONFIGFILE = None
+    transmogrifier._data = {}
+
+    # this function is also called when adding Plone site, so call standard handler
+    path = ''
+    prefix = 'structure'
+    if 'reader' in transmogrifier._raw:
+        path = transmogrifier._raw['reader'].get('path', '')
+        prefix = transmogrifier._raw['reader'].get('prefix', 'structure')
+    if not context.readDataFile('.objects.xml', subdir=os.path.join(path, prefix)):
+        try:
+            from Products.GenericSetup.interfaces import IFilesystemImporter
+            IFilesystemImporter(context.getSite()).import_(context, 'structure', True)
+        except ImportError:
+            pass
+        return
+
+    options = transmogrifier._raw['transmogrifier']
+    sections = options['pipeline'].splitlines()
+    pipeline = constructPipeline(transmogrifier, sections)
+
+    last_section = pipeline.gi_frame.f_locals['self']
+
+    # if 'quintagroup.transmogrifier.writer' section's export context is
+    # tarball replace it with given function argument
+    while hasattr(last_section, 'previous'):
+        if isinstance(last_section, ReaderSection) and \
+            isinstance(last_section.import_context, TarballImportContext):
+            last_section.import_context = context
+        last_section = last_section.previous
+        # end cycle if we get empty starter section
+        if type(last_section) == type(iter(())):
+            break
+        last_section = last_section.gi_frame.f_locals['self']
+
+    # Pipeline execution
+    for item in pipeline:
+        pass # discard once processed
+
+
+class PloneSiteImporter(object):
+    """ Importer of plone site.
+    """
+    implements(IFilesystemImporter)
+
+    def __init__(self, context):
+        self.context = context
+
+    def import_(self, import_context, subdir="structure", root=False):
+        # When performing import steps we need to use standart importing adapter,
+        # if 'object.xml' file is absent in 'structure' directory of the profile.
+        # This may be because it is the base plone profile or extension profile, that has
+        # structure part in other format.
+
+        objects_xml = import_context.readDataFile('.objects.xml', subdir)
+        if objects_xml is not None:
+            importSiteStructure(import_context)
+        else:
+            from Products.CMFCore.exportimport.content import StructureFolderWalkingAdapter
+            StructureFolderWalkingAdapter(self.context).import_(import_context, "structure", True)
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/flushcache.py
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/flushcache.py	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/flushcache.py	(revision 1597)
@@ -0,0 +1,22 @@
+import transaction
+from zope.interface import classProvides, implements
+from collective.transmogrifier.interfaces import ISectionBlueprint
+from collective.transmogrifier.interfaces import ISection
+
+class FlushCacheSection(object):
+    classProvides(ISectionBlueprint)
+    implements(ISection)
+    
+    def __init__(self, transmogrifier, name, options, previous):
+        self.every = int(options.get('every', 1000))
+        self.previous = previous
+        self.context = transmogrifier.context
+    
+    def __iter__(self):
+        count = 0
+        for item in self.previous:
+            count = (count + 1) % self.every
+            if count == 0:
+                [self.context.Control_Panel.Database[x]._getDB().cacheMinimize() \
+                 for x in self.context.Control_Panel.Database.getDatabaseNames()]
+            yield item
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/flushcache.txt
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/flushcache.txt	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/flushcache.txt	(revision 1597)
@@ -0,0 +1,34 @@
+Flush cache section
+===================
+
+A flushcache pipeline section flushes zope cache every so often, which has an
+effect of freeing up memory. This section is useful while performing migration
+on a weak system with a low memory. The flushcache section blueprint name is
+``quintagroup.transmogrifier.flushcache``.
+
+A flushcache section takes an optional ``every`` option, which defaults to
+1000; cache is being flushed every ``every`` items passing through the pipe.
+A flushcache section doesn't alter the items in any way:
+
+>>> flushcache = """
+... [transmogrifier]
+... pipeline =
+...     source
+...     flushcache
+...     
+... [source]
+... blueprint = collective.transmogrifier.sections.tests.rangesource
+... size = 10
+... 
+... [flushcache]
+... blueprint = quintagroup.transmogrifier.flushcache
+... every = 3
+... """
+>>> registerConfig(u'quintagroup.transmogrifier.tests.flushcache', flushcache)
+
+We'll show caching is flushed using dedicated variable on dummy plone instance
+called 'cacheMinimized'. Which is a number of caches performed.
+
+>>> transmogrifier(u'quintagroup.transmogrifier.tests.flushcache')
+>>> transmogrifier.context.cacheMinimized
+3
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/import.cfg
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/import.cfg	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/import.cfg	(revision 1597)
@@ -0,0 +1,60 @@
+[transmogrifier]
+pipeline =
+    reader
+    manifestimporter
+    constructor
+    datacorrector
+    fileimporter
+    demarshaller
+    referencesimporter
+    propertiesimporter
+    commentsimporter
+    portletsimporter
+    IMPORTING
+
+[reader]
+blueprint = quintagroup.transmogrifier.reader
+prefix = structure
+.objects.xml = manifest
+.marshall.xml = marshall
+.properties.xml = propertymanager
+.comments.xml = comments
+.file-fields.xml = file-fields
+.interfaces.xml = interfaces
+.portlets.xml = portlets
+
+[manifestimporter]
+blueprint = quintagroup.transmogrifier.manifestimporter
+
+[constructor]
+blueprint = collective.transmogrifier.sections.constructor
+
+[datacorrector]
+blueprint = quintagroup.transmogrifier.datacorrector
+type = import
+sources =
+    marshall
+
+[fileimporter]
+blueprint = quintagroup.transmogrifier.fileimporter
+
+[demarshaller]
+blueprint = quintagroup.transmogrifier.demarshaller
+
+[referencesimporter]
+blueprint = quintagroup.transmogrifier.referencesimporter
+
+[propertiesimporter]
+blueprint = quintagroup.transmogrifier.propertiesimporter
+
+[commentsimporter]
+blueprint = quintagroup.transmogrifier.commentsimporter
+
+[portletsimporter]
+blueprint = quintagroup.transmogrifier.portletsimporter
+
+[IMPORTING]
+blueprint = quintagroup.transmogrifier.logger
+keys = 
+    _type
+    _path
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/interfacemanager.py
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/interfacemanager.py	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/interfacemanager.py	(revision 1597)
@@ -0,0 +1,175 @@
+import os.path
+from xml.dom import minidom
+
+from zope.interface import classProvides, implements
+from zope.interface import directlyProvidedBy, alsoProvides
+from zope.annotation.interfaces import IAnnotations
+
+from Products.CMFCore import utils
+
+from collective.transmogrifier.interfaces import ISection, ISectionBlueprint
+from collective.transmogrifier.utils import defaultMatcher
+
+from quintagroup.transmogrifier.logger import VALIDATIONKEY
+
+class InterfacesExporterSection(object):
+    classProvides(ISectionBlueprint)
+    implements(ISection)
+
+    def __init__(self, transmogrifier, name, options, previous):
+        self.previous = previous
+        self.context = transmogrifier.context
+
+        self.pathkey = defaultMatcher(options, 'path-key', name, 'path')
+        self.fileskey = options.get('files-key', '_files').strip()
+
+        self.excludekey = defaultMatcher(options, 'exclude-key', name, 'excluded_interfaces')
+        self.exclude = filter(None, [i.strip() for i in
+                              options.get('exclude', '').splitlines()])
+
+        self.includekey = defaultMatcher(options, 'include-key', name, 'included_interfaces')
+        self.include = filter(None, [i.strip() for i in
+                              options.get('include', '').splitlines()])
+
+        self.doc = minidom.Document()
+
+    def __iter__(self):
+        for item in self.previous:
+            pathkey = self.pathkey(*item.keys())[0]
+
+            if not pathkey:
+                yield item; continue
+
+            path = item[pathkey]
+            obj = self.context.unrestrictedTraverse(path, None)
+            if obj is None:         # path doesn't exist
+                yield item; continue
+
+            ifaces = self.getInterfaces(obj)
+
+            if ifaces:
+                files = item.setdefault('_files', {})
+                item[self.fileskey]['interfaces'] = {
+                    'name': '.interfaces.xml',
+                    'data': ifaces,
+                }
+
+            yield item
+
+    def getInterfaces(self, obj):
+        if not obj:
+            return None
+
+        doc = self.doc
+        root = doc.createElement('interfaces')
+
+        ifaces = [i.__identifier__ for i in directlyProvidedBy(obj)]
+        if self.include:
+            ifaces = filter(lambda i: i in self.include, ifaces)
+        elif self.exclude:
+            ifaces = filter(lambda i: not i in self.include, ifaces)
+
+        if ifaces == []:
+            return None
+
+        for iface in ifaces:
+            # create record
+            record = doc.createElement('record')
+
+            # add object interface
+            text = doc.createTextNode(iface)
+            record.appendChild(text)
+
+            root.appendChild(record)
+
+        doc.appendChild(root)
+
+        try:
+            data = doc.toprettyxml(indent='  ', encoding='utf-8')
+        except UnicodeDecodeError, e:
+            # all comments are strings encoded in 'utf-8' and they will properly
+            # saved in xml file, but if we explicitly give 'utf-8' encoding
+            # UnicodeDecodeError will be raised when they have non-ascii chars
+            data = doc.toprettyxml(indent='  ')
+
+        doc.unlink()
+        return data
+
+
+class InterfacesImporterSection(object):
+    classProvides(ISectionBlueprint)
+    implements(ISection)
+
+    def __init__(self, transmogrifier, name, options, previous):
+        self.previous = previous
+        self.context = transmogrifier.context
+
+        self.pathkey = defaultMatcher(options, 'path-key', name, 'path')
+        self.fileskey = defaultMatcher(options, 'files-key', name, 'files')
+
+        self.excludekey = defaultMatcher(options, 'exclude-key', name, 'excluded_properties')
+        self.exclude = filter(None, [i.strip() for i in
+                            options.get('exclude', '').splitlines()])
+
+        self.includekey = defaultMatcher(options, 'include-key', name, 'included_properties')
+        self.include = filter(None, [i.strip() for i in
+                              options.get('include', '').splitlines()])
+
+        self.catalog = utils.getToolByName(self.context, 'portal_catalog')
+
+    def __iter__(self):
+
+        for item in self.previous:
+            pathkey = self.pathkey(*item.keys())[0]
+            fileskey = self.fileskey(*item.keys())[0]
+
+            if not (pathkey and fileskey):
+                yield item; continue
+            if 'interfaces' not in item[fileskey]:
+                yield item; continue
+
+            path = item[pathkey]
+            obj = self.context.unrestrictedTraverse(path, None)
+            if obj is None:         # path doesn't exist
+                yield item; continue
+
+            ifaces = self.extractIfaces(obj, item[fileskey]['interfaces']['data'])
+
+            if ifaces == []:         # no interfaces
+                yield item; continue
+
+            alsoProvides(obj, *ifaces)
+
+            yield item
+
+        self.catalog.reindexIndex('object_provides', None)
+
+
+    def extractIfaces(self, obj, data):
+        doc = minidom.parseString(data)
+        ifaces = []
+        for record in doc.getElementsByTagName('record'):
+            iface_name = str(record.firstChild.nodeValue.strip())
+
+            # filter interfaces
+            if self.include and not iface_name in self.include:
+                continue
+            elif self.exclude and iface_name in self.exclude:
+                continue
+                
+            iface = self.getIfaceById(iface_name)
+            if iface:
+                ifaces.append(iface)
+        return ifaces
+
+
+    def getIfaceById(self, name):
+        components = name.split('.'); components.reverse()
+        try:
+            obj = __import__(components.pop())
+        except (ImportError, ValueError):
+            return None
+        while obj is not None and components:
+            obj = getattr(obj, components.pop(), None)
+        return obj
+
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/interfacemanager.txt
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/interfacemanager.txt	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/interfacemanager.txt	(revision 1597)
@@ -0,0 +1,80 @@
+Interfaces exporter and importer sections
+=========================================
+
+Interfaces exporter and importer sections are used to migrate interfaces for
+objects.
+The interfaces exporter section blueprint name is
+``quintagroup.transmogrifier.interfacesexporter`` and importer section blueprint
+name is ``quintagroup.transmogrifier.interfacesimporter``.
+
+Interfaces are represented in XML format. Both sections need relative
+path to the object with interfaces and importer also needs XML data for updating
+interfaces provided by object.
+
+Both sections has ``path-key`` option which specifies key in item where path to
+object is stored (``_path`` is default) and ``files-key`` option that gives
+key where XML data will be or is stored. Also there are ``include`` and
+``exclude`` options that give global list of interface identifiers to include or
+exclude on serializing. There are correspondent ``include-key``, ``exclude-key``
+options, that specifies key where same lists per item is stored (default is
+``_included_interfaces`` & ``_excluded_interfaces``). These options give you
+alternative way to manage interfaces to serialize and apply to the object.
+``include`` option - include only interfaces, listed in, and ``exclude`` option
+behave in opposite way. If both options present - only ``include`` option get
+into consideration. If both options is absent - all interfaces will be
+serialized and/or migrated.
+Exporter section exports only Interfaces directly provided by object, so in such
+way we prevent confusion with interfaces, implemented by class itself.
+
+>>> import pprint
+>>> interfacemanager = """
+... [transmogrifier]
+... pipeline =
+...     interfacemanagersource
+...     interfacesexporter
+...     printer
+...     dataprinter
+...     interfacesimporter
+...     
+... [interfacemanagersource]
+... blueprint = quintagroup.transmogrifier.tests.interfacemanagersource
+... 
+... [interfacesexporter]
+... blueprint = quintagroup.transmogrifier.interfacesexporter
+... 
+... [printer]
+... blueprint = collective.transmogrifier.sections.tests.pprinter
+... 
+... [dataprinter]
+... blueprint = quintagroup.transmogrifier.tests.dataprinter
+... print = 
+...     _files
+...     interfaces
+...     data
+... 
+... [interfacesimporter]
+... blueprint = quintagroup.transmogrifier.interfacesimporter
+... include =
+...     zope.annotation.interfaces.IAttributeAnnotatable
+... """
+>>> registerConfig(u'quintagroup.transmogrifier.tests.interfacemanager', interfacemanager)
+>>> transmogrifier(u'quintagroup.transmogrifier.tests.interfacemanager') # doctest: +ELLIPSIS, +REPORT_NDIFF
+{}
+{'_path': 'not/existing/bar'}
+{'_path': 'spam/eggs/notatcontent'}
+{'_files': {'interfaces': {'data': ...
+                           'name': '.interfaces.xml'}},
+ '_path': 'spam/eggs/foo'}
+<?xml version="1.0" encoding="utf-8"?>
+<interfaces>
+  <record>
+    zope.annotation.interfaces.IAttributeAnnotatable
+  </record>
+  <record>
+    zope.interface.Interface
+  </record>
+</interfaces>
+<BLANKLINE>
+
+>>> pprint.pprint(updated)
+['zope.annotation.interfaces.IAttributeAnnotatable']
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/interfaces.py
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/interfaces.py	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/interfaces.py	(revision 1597)
@@ -0,0 +1,17 @@
+from zope.interface import Interface
+
+class IExportDataCorrector(Interface):
+    """ Inteface for components that do some data correction on export.
+    """
+
+    def __call__(data):
+        """ Correct data given in 'data' argument and return it.
+        """
+
+class IImportDataCorrector(Interface):
+    """ Inteface for components that do some data correction on import.
+    """
+
+    def __call__(data):
+        """ Correct data given in 'data' argument and return it.
+        """
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/logger.py
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/logger.py	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/logger.py	(revision 1597)
@@ -0,0 +1,66 @@
+import logging
+from time import time
+
+from zope.interface import classProvides, implements
+from zope.annotation.interfaces import IAnnotations
+
+from collective.transmogrifier.interfaces import ISectionBlueprint
+from collective.transmogrifier.interfaces import ISection
+from collective.transmogrifier.utils import Matcher
+
+VALIDATIONKEY = 'quintagroup.transmogrifier.logger'
+
+class LoggerSection(object):
+    classProvides(ISectionBlueprint)
+    implements(ISection)
+
+    def __init__(self, transmogrifier, name, options, previous):
+        self.transmogrifier = transmogrifier
+        keys = options.get('keys') or ''
+        self.pathkey = options.get('path-key', '_path').strip()
+        self.keys = Matcher(*keys.splitlines())
+        self.previous = previous
+        self.logger = name
+        self.storage = IAnnotations(transmogrifier).setdefault(VALIDATIONKEY, [])
+
+    def __iter__(self):
+        start_time = time()
+        count = 0
+        problematic = 0
+        for item in self.previous:
+            # source sections add store path of current generated item in annotation
+            # it gives posibility to monitor what items go through all pipeline
+            # sections between source section and this section and what don't
+            if self.pathkey in item and item[self.pathkey] in self.storage:
+                self.storage.remove(item[self.pathkey])
+            count += 1
+            # print item data stored on keys given as option
+            items = []
+            for key in item.keys():
+                if self.keys(key)[0] is not None:
+                    items.append("%s=%s" % (key, item[key]))
+            if items:
+                msg = ", ".join(items)
+                logging.getLogger(self.logger).info(msg)
+            yield item
+        
+        working_time = int(round(time() - start_time))
+
+        # log items that maybe have some problems
+        if self.storage:
+            problematic = len(self.storage)
+            logging.getLogger(self.logger).warning('\nNext objects didn\'t go through full pipeline:\n%s' % \
+                '\n'.join(['\t'+i for i in self.storage]))
+        # delete validation data from annotations
+        anno = IAnnotations(self.transmogrifier)
+        if VALIDATIONKEY in anno:
+            del anno[VALIDATIONKEY]
+
+        seconds = working_time % 60
+        minutes = working_time / 60 % 60
+        hours = working_time / 3600
+        stats = "\nPipeline processing time: %02d:%02d:%02d\n" % (hours, minutes, seconds)
+        stats += "\t%4d items were generated in source sections\n" % (count + problematic)
+        stats += "\t%4d went through full pipeline\n" % count
+        stats += "\t%4d were discarded in some section" % problematic
+        logging.getLogger(self.logger).info(stats)
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/manifest.py
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/manifest.py	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/manifest.py	(revision 1597)
@@ -0,0 +1,154 @@
+import os.path
+from xml.dom import minidom
+
+from zope.interface import classProvides, implements
+from zope.annotation.interfaces import IAnnotations
+
+from collective.transmogrifier.interfaces import ISection, ISectionBlueprint
+from collective.transmogrifier.utils import defaultMatcher
+
+from quintagroup.transmogrifier.logger import VALIDATIONKEY
+
+class ManifestExporterSection(object):
+    classProvides(ISectionBlueprint)
+    implements(ISection)
+
+    def __init__(self, transmogrifier, name, options, previous):
+        self.previous = previous
+        self.context = transmogrifier.context
+
+        self.entrieskey = defaultMatcher(options, 'entries-key', name, 'entries')
+        self.fileskey = options.get('files-key', '_files').strip()
+
+        self.doc = minidom.Document()
+
+    def __iter__(self):
+        for item in self.previous:
+            entrieskey = self.entrieskey(*item.keys())[0]
+            if not entrieskey:
+                yield item; continue
+
+            manifest = self.createManifest(item[entrieskey])
+
+            if manifest:
+                files = item.setdefault('_files', {})
+                item[self.fileskey]['manifest'] = {
+                    'name': '.objects.xml',
+                    'data': manifest,
+                }
+
+            yield item
+
+    def createManifest(self, entries):
+        if not entries:
+            return None
+
+        doc = self.doc
+        root = doc.createElement('manifest')
+
+        for obj_id, obj_type in entries:
+            # create record
+            record = doc.createElement('record')
+
+            # set type attribute
+            attr = doc.createAttribute('type')
+            attr.value = obj_type
+            record.setAttributeNode(attr)
+
+            # add object id
+            text = doc.createTextNode(obj_id)
+            record.appendChild(text)
+
+            root.appendChild(record)
+
+        doc.appendChild(root)
+
+        try:
+            data = doc.toprettyxml(indent='  ', encoding='utf-8')
+        except UnicodeDecodeError, e:
+            # all comments are strings encoded in 'utf-8' and they will properly
+            # saved in xml file, but if we explicitly give 'utf-8' encoding
+            # UnicodeDecodeError will be raised when they have non-ascii chars
+            data = doc.toprettyxml(indent='  ')
+
+        doc.unlink()
+        return data
+
+class ManifestImporterSection(object):
+    classProvides(ISectionBlueprint)
+    implements(ISection)
+
+    def __init__(self, transmogrifier, name, options, previous):
+        self.previous = previous
+        self.context = transmogrifier.context
+
+        self.pathkey = defaultMatcher(options, 'path-key', name, 'path')
+        self.fileskey = defaultMatcher(options, 'files-key', name, 'files')
+        self.typekey = options.get('type-key', '_type').strip()
+        self.enable_source_behaviour = options.get('enable-source-behaviour', 'true') == 'true' and True or False
+
+        # communication with logger
+        self.anno = IAnnotations(transmogrifier)
+        self.storage = self.anno.setdefault(VALIDATIONKEY, [])
+
+        # we need this dictionary to store manifest data, because reader section
+        # uses recursion when walking through content folders
+        self.manifests = {}
+
+    def __iter__(self):
+        for item in self.previous:
+            pathkey = self.pathkey(*item.keys())[0]
+            fileskey = self.fileskey(*item.keys())[0]
+
+            # skip items without path
+            if not pathkey: continue
+
+            path  = item[pathkey]
+
+            if path != '':
+                parent, item_id = os.path.split(path)
+                manifest = self.manifests.get(parent, {})
+
+                # skip that are not listed in their parent's manifest
+                if item_id not in manifest: continue
+
+                item[self.typekey] = manifest.pop(item_id)
+                # remove empty manifest dict
+                if not manifest:
+                    del self.manifests[parent]
+
+            # this item is folderish - parse manifest
+            if fileskey and 'manifest' in item[fileskey]:
+                self.extractManifest(path, item[fileskey]['manifest']['data'])
+
+            yield item
+
+        # now we yield items that were defined in manifests but not generated by
+        # previous sections - it is possible
+        if self.manifests and self.enable_source_behaviour:
+            containers = self.manifests.keys()
+            containers.sort()
+            for i in containers:
+                manifest = self.manifests[i]
+                ids = manifest.keys()
+                ids.sort()
+                for id_ in ids:
+                    if i == '':
+                        path = id_
+                    else:
+                        path = '/'.join([i, id_])
+                    self.storage.append(path)
+                    yield {pathkey: path, self.typekey: manifest[id_]}
+
+        # cleanup
+        if VALIDATIONKEY in self.anno:
+            del self.anno[VALIDATIONKEY]
+
+    def extractManifest(self, path, data):
+        doc = minidom.parseString(data)
+        objects = {}
+        for record in doc.getElementsByTagName('record'):
+            type_ = str(record.getAttribute('type'))
+            object_id = str(record.firstChild.nodeValue.strip())
+            objects[object_id] = type_
+        self.manifests[path] = objects
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/manifest.txt
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/manifest.txt	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/manifest.txt	(revision 1597)
@@ -0,0 +1,97 @@
+Manifest exporter and importer sections
+=======================================
+
+A manifest exporter and importer sections are used to generate and parse
+manifest files - listings of objects contained in some folder in XML format.
+These listings are needed to set portal types for pipeline items and also act
+as filter on them - items not listed in manifest will be removed. Manifest
+import section is also a source section - it will generate item for every
+record seen in manifest which didn't have any corresponding item in the items
+stream from previous source sections. The manifest exporter section blueprint name is
+``quintagroup.transmogrifier.manifestsexporter`` and importer section blueprint
+name is ``quintagroup.transmogrifier.manifestimporter``.
+
+Both sections has ``files-key`` option that gives key in item where XML data
+will be or is stored (default is ``_files``). Exporter section has
+``entries-key`` option, which specifies key in item where list of 
+(object_id, portal_type) pairs that represents folder contents is stored 
+(default is ``_entries``). Importer section has ``path-key`` option which
+specifies key in item where path to object is stored (default is ``_path``)
+and ``type-key`` option that gives key to store item's portal type (default is
+``_type``).
+
+>>> manifest = """
+... [transmogrifier]
+... pipeline =
+...     manifestsource
+...     manifestexporter
+...     manifestimporter
+...     printer
+...     dataprinter
+... 
+... [manifestsource]
+... blueprint = quintagroup.transmogrifier.tests.manifestsource
+... 
+... [manifestexporter]
+... blueprint = quintagroup.transmogrifier.manifestexporter
+... 
+... [manifestimporter]
+... blueprint = quintagroup.transmogrifier.manifestimporter
+... enable-source-behaviour = true
+...
+... [printer]
+... blueprint = collective.transmogrifier.sections.tests.pprinter
+... 
+... [dataprinter]
+... blueprint = quintagroup.transmogrifier.tests.dataprinter
+... print = 
+...     _files
+...     manifest
+...     data
+... """
+>>> registerConfig(u'quintagroup.transmogrifier.tests.manifest',
+...                manifest)
+>>> transmogrifier(u'quintagroup.transmogrifier.tests.manifest') # doctest: +ELLIPSIS, +REPORT_NDIFF
+{'_entries': (('news', 'Folder'),
+              ('events', 'Folder'),
+              ('front-page', 'Document'),
+              ('only-in-manifest', 'Document')),
+ '_files': {'manifest': {'data': ...
+                         'name': '.objects.xml'}},
+ '_path': ''}
+<?xml version="1.0" encoding="utf-8"?>
+<manifest>
+  <record type="Folder">
+    news
+  </record>
+  <record type="Folder">
+    events
+  </record>
+  <record type="Document">
+    front-page
+  </record>
+  <record type="Document">
+    only-in-manifest
+  </record>
+</manifest>
+<BLANKLINE>
+{'_entries': (('aggregator', 'Topic'), ('once-more', 'File')),
+ '_files': {'manifest': {'data': ...
+                         'name': '.objects.xml'}},
+ '_path': 'news',
+ '_type': 'Folder'}
+<?xml version="1.0" encoding="utf-8"?>
+<manifest>
+  <record type="Topic">
+    aggregator
+  </record>
+  <record type="File">
+    once-more
+  </record>
+</manifest>
+<BLANKLINE>
+{'_type': 'Topic', '_path': 'news/aggregator'}
+{'_type': 'Folder', '_path': 'events'}
+{'_type': 'Document', '_path': 'front-page'}
+{'_type': 'Document', '_path': 'only-in-manifest'}
+{'_type': 'File', '_path': 'news/once-more'}
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/marshall.py
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/marshall.py	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/marshall.py	(revision 1597)
@@ -0,0 +1,151 @@
+import traceback
+from DateTime import DateTime
+
+from zope.interface import classProvides, implements
+from zope import event
+
+from ZODB.POSException import ConflictError
+
+from collective.transmogrifier.interfaces import ISection, ISectionBlueprint
+from collective.transmogrifier.utils import defaultMatcher
+
+from Products.CMFCore import utils
+from Products.Marshall import registry
+from Products.Archetypes.interfaces import IBaseObject
+from Products.Archetypes.event import ObjectInitializedEvent
+from Products.Archetypes.event import ObjectEditedEvent
+
+# override Marshall atxml namespaces
+from quintagroup.transmogrifier import namespaces
+
+
+class MarshallerSection(object):
+    classProvides(ISectionBlueprint)
+    implements(ISection)
+
+    def __init__(self, transmogrifier, name, options, previous):
+        self.previous = previous
+        self.context = transmogrifier.context
+
+        self.pathkey = defaultMatcher(options, 'path-key', name, 'path')
+        self.fileskey = options.get('files-key', '_files').strip()
+
+        self.excludekey = defaultMatcher(options, 'exclude-key', name, 'excluded_fields')
+        self.exclude = filter(None, [i.strip() for i in 
+                              options.get('exclude', '').splitlines()])
+
+        self.atxml = registry.getComponent("atxml")
+
+    def __iter__(self):
+        for item in self.previous:
+            pathkey = self.pathkey(*item.keys())[0]
+
+            if not pathkey:
+                yield item; continue
+
+            path = item[pathkey]
+            obj = self.context.unrestrictedTraverse(path, None)
+            if obj is None:         # path doesn't exist
+                yield item; continue
+
+            if IBaseObject.providedBy(obj):
+                # get list of excluded fields given in options and in item
+                excludekey = self.excludekey(*item.keys())[0]
+                atns_exclude = tuple(self.exclude)
+                if excludekey:
+                    atns_exclude = tuple(set(item[excludekey]) | set(atns_exclude))
+
+                try:
+                    content_type, length, data = self.atxml.marshall(obj, atns_exclude=atns_exclude)
+                except ConflictError:
+                    raise
+                except:
+                    data = None
+
+                if data or data is None:
+                    # None value has special meaning for IExportDataCorrector adapter for topic criterias
+                    files = item.setdefault(self.fileskey, {})
+                    item[self.fileskey]['marshall'] = {
+                        'name': '.marshall.xml',
+                        'data': data,
+                    }
+
+            yield item
+
+class DemarshallerSection(object):
+    classProvides(ISectionBlueprint)
+    implements(ISection)
+
+    def __init__(self, transmogrifier, name, options, previous):
+        self.previous = previous
+        self.context = transmogrifier.context
+
+        self.pathkey = defaultMatcher(options, 'path-key', name, 'path')
+        self.fileskey = defaultMatcher(options, 'files-key', name, 'files')
+
+        # Marshall doesn't support excluding fields on demarshalling,
+        # we can do this with xml.dom.minodom, if it'll be needed in the future
+        # self.excludekey = defaultMatcher(options, 'exclude-key', name, 'excluded_fields')
+
+        # self.exclude = filter(None, [i.strip() for i in 
+        #                     options.get('exclude', '').splitlines()])
+
+        self.atxml = registry.getComponent("atxml")
+
+    def __iter__(self):
+        for item in self.previous:
+            pathkey = self.pathkey(*item.keys())[0]
+            fileskey = self.fileskey(*item.keys())[0]
+
+            if not (pathkey and fileskey):
+                yield item; continue
+            if 'marshall' not in item[fileskey]:
+                yield item; continue
+
+            path = item[pathkey]
+            obj = self.context.unrestrictedTraverse(path, None)
+            if obj is None:         # path doesn't exist
+                yield item; continue
+
+            if IBaseObject.providedBy(obj):
+                try:
+                    data = item[fileskey]['marshall']['data']
+                    self.atxml.demarshall(obj, data)
+
+                    # When we change workflow state of content through Plone interface,
+                    # effective date field will be updated to current date (the same
+                    # as modification date) if it was empty, so after demarshalling
+                    # (workflow history was updated) we need to do it manually.
+                    effective_field = obj.getField('effectiveDate')
+                    if effective_field is not None and effective_field.getAccessor(obj)() is None:
+                        date = obj.getField('modification_date').getAccessor(obj)()
+                        effective_field.getMutator(obj)(date)
+
+                    # we don't want to call reindexObject because modification_date
+                    # will be updated, so we call only indexObject (reindexObject does
+                    # some things with uid catalog too)
+                    is_new_object = obj.checkCreationFlag()
+                    obj.indexObject()
+                    # firing of events
+                    obj.unmarkCreationFlag()
+                    if is_new_object:
+                        event.notify(ObjectInitializedEvent(obj))
+                        obj.at_post_create_script()
+                    else:
+                        event.notify(ObjectEditedEvent(obj))
+                        obj.at_post_edit_script()
+                except ConflictError:
+                    raise
+                except Exception, e:
+                    print 'Exception in demarshaller section:'
+                    print '-'*60
+                    traceback.print_exc()
+                    print '-'*60
+
+            yield item
+
+        # updating security settings on demarshalled content
+        wtool = utils.getToolByName(self.context, 'portal_workflow')
+        wtool.updateRoleMappings()
+        catalog = utils.getToolByName(self.context, 'portal_catalog')
+        catalog.reindexIndex('allowedRolesAndUsers', None)
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/marshall.txt
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/marshall.txt	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/marshall.txt	(revision 1597)
@@ -0,0 +1,71 @@
+Marshaller and demarshaller sections
+====================================
+
+Marshaller and demarshaller sections are used to serialize content in XML data
+of update it from it with the help of Marshall 1.0.0 Plone product. The
+marshaller section blueprint name is ``quintagroup.transmogrifier.marshaller``
+and the demarshaller section blueprint name is
+``quintagroup.transmogrifier.demarshaller``.
+
+Marshaller needs relative path to the object to serialize in XML format.
+Demarshaller needs path and XML data for updating object. Both sections use
+Marshall products internals and serialize/deserialize content with the use of
+``atxml`` component (this is not Zope 3 component, Marshall calls it's handlers
+components).
+
+Both sections has ``path-key`` option which specify key in item where path to
+object is stored (``_path`` is default) and ``files-key`` option that gives
+key where XML data will be or is stored. Marshaller section also has ``exclude``
+option that give global list of Archetypes fields to exclude on serializing and
+``exclude-key`` option, that specifies key where same list per item is stored
+(default is ``_excluded_fields``). Fields that are in any of these lists are
+excluded.
+
+>>> import pprint
+>>> marshall = """
+... [transmogrifier]
+... pipeline =
+...     marshallsource
+...     marshall
+...     demarshaller
+...     printer
+...     
+... [marshallsource]
+... blueprint = quintagroup.transmogrifier.tests.marshallsource
+... 
+... [marshall]
+... blueprint = quintagroup.transmogrifier.marshaller
+... exclude =
+...     text
+... 
+... [demarshaller]
+... blueprint = quintagroup.transmogrifier.demarshaller
+... 
+... [printer]
+... blueprint = collective.transmogrifier.sections.tests.pprinter
+... """
+>>> registerConfig(u'quintagroup.transmogrifier.tests.marshall', marshall)
+>>> transmogrifier(u'quintagroup.transmogrifier.tests.marshall') # doctest: +REPORT_NDIFF
+{}
+{'_excluded_fields': ('file', 'image'),
+ '_files': {'marshall': {'data': 'marshalled', 'name': '.marshall.xml'}},
+ '_path': 'spam/eggs/foo'}
+{'_files': {'marshall': {'data': None, 'name': '.marshall.xml'}},
+ '_path': 'topic/criterion'}
+{'_path': 'not/existing/bar'}
+{'_files': {'marshall': {'data': 'xml', 'name': '.marshall.xml'}},
+ '_path': 'spam/eggs/notatcontent'}
+>>> pprint.pprint(plone.marshalled)
+(('spam/eggs/foo', ('text', 'image', 'file')), ('topic/criterion', ('text',)))
+>>> pprint.pprint(plone.demarshalled)
+('spam/eggs/foo', 'topic/criterion')
+>>> pprint.pprint(plone.indexed+plone.criterion.indexed)
+('spam/eggs/foo', 'topic/criterion')
+>>> plone.updatedRoles
+True
+>>> plone.reindexed
+True
+>>> plone.fields['effectiveDate']
+'changed'
+>>> plone.criterion.fields['effectiveDate']
+'not changed'
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/meta.zcml
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/meta.zcml	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/meta.zcml	(revision 1597)
@@ -0,0 +1,12 @@
+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    xmlns:meta="http://namespaces.zope.org/meta">
+
+    <meta:directive
+        namespace="http://namespaces.plone.org/transmogrifier"
+        name="stylesheet"
+        schema=".xslt.IStylesheetDirective"
+        handler=".xslt.stylesheet"
+        />
+
+</configure>
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/namespaces/__init__.py
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/namespaces/__init__.py	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/namespaces/__init__.py	(revision 1597)
@@ -0,0 +1,27 @@
+# import Products.Marshall namespaces to ensure it's registered
+# before we override it
+from Products.Marshall import namespaces
+from Products.Marshall.handlers.atxml import ATXMLMarshaller, XmlNamespace
+
+from atns import Archetypes
+from dcns import DublinCore
+from cmfns import CMF
+
+
+def replaceNamespace(ns):
+    """Replace namespaces by prefix"""
+    if not isinstance(ns, XmlNamespace):
+        ns = ns()
+
+    nses = ATXMLMarshaller.namespaces
+    prefixes = [nses.index(n) for n in nses if n.prefix == ns.prefix]
+    if len(prefixes) == 0:
+        return False
+
+    idx = prefixes[0]
+    nses.pop(idx)
+    nses.insert(idx, ns)
+
+replaceNamespace(DublinCore)
+replaceNamespace(Archetypes)
+replaceNamespace(CMF)
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/namespaces/atns.py
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/namespaces/atns.py	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/namespaces/atns.py	(revision 1597)
@@ -0,0 +1,109 @@
+"""
+    Archetypes Marshall namespace but which can safely handle
+    Control Characters for you
+"""
+
+from Products.Archetypes.interfaces.base import IBaseUnit
+
+from Products.Marshall import config
+from Products.Marshall.handlers.atxml import XmlNamespace
+from Products.Marshall.handlers.atxml import SchemaAttribute
+from Products.Marshall.handlers.atxml import getRegisteredNamespaces
+from Products.Marshall.exceptions import MarshallingException
+from Products.Marshall import utils
+
+from Products.Marshall.namespaces import atns as base
+
+from quintagroup.transmogrifier.namespaces.util import has_ctrlchars
+
+
+class ATAttribute(base.ATAttribute):
+
+    def serialize(self, dom, parent_node, instance, options={}):
+        
+        values = self.get(instance)
+        if not values:
+            return
+
+        is_ref = self.isReference(instance)
+        
+        for value in values:
+            node = dom.createElementNS(self.namespace.xmlns, "field")
+            name_attr = dom.createAttribute("name")
+            name_attr.value = self.name
+            node.setAttributeNode(name_attr)
+            
+            # try to get 'utf-8' encoded string
+            if isinstance(value, unicode):
+                value = value.encode('utf-8')
+            elif IBaseUnit.isImplementedBy(value):
+                value = value.getRaw(encoding='utf-8')
+            else:
+                value = str(value)
+
+            if is_ref:
+                if config.HANDLE_REFS:
+                    ref_node = dom.createElementNS(self.namespace.xmlns,
+                                                    'reference')
+                    uid_node = dom.createElementNS(self.namespace.xmlns,
+                                                    'uid')
+                    value = response.createTextNode(value)
+                    uid_node.append(value)
+                    ref_node.append(uid_node)
+                    node.append(ref_node)
+            elif isinstance(value, str) and has_ctrlchars(value):
+                value = value.encode('base64')
+                attr = dom.createAttributeNS(self.namespace.xmlns,
+                                             'transfer_encoding')
+                attr.value = 'base64'
+                node.setAttributeNode(attr)
+                value_node = dom.createCDATASection(value)
+                node.appendChild(value_node)
+            else:
+                value_node = dom.createTextNode(value)
+                node.appendChild(value_node)
+        
+            node.normalize()
+            parent_node.appendChild(node)
+
+        return True
+
+    def processXmlValue(self, context, value):
+        if value is None:
+            return
+
+        value = value.strip()
+        if not value:
+            return
+
+        # decode node value if needed
+        te = context.node.get('transfer_encoding', None)
+        if te is not None:
+            value = value.decode(te)
+
+        data = context.getDataFor(self.namespace.xmlns)
+        if data.has_key(self.name):
+            svalues = data[self.name]
+            if not isinstance(svalues, list):
+                data[self.name] = svalues = [svalues]
+            svalues.append(value)
+            return
+        else:
+            data[self.name] = value
+
+class Archetypes(base.Archetypes):
+
+    def getAttributeByName(self, schema_name, context=None):
+        if context is not None and schema_name not in self.at_fields:
+            if not context.instance.Schema().has_key(schema_name):
+                return
+                raise AssertionError, \
+                      "invalid attribute %s"%(schema_name)
+        
+        if schema_name in self.at_fields:
+            return self.at_fields[schema_name]
+
+        attribute = ATAttribute(schema_name)
+        attribute.setNamespace(self)
+        
+        return attribute
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/namespaces/cmfns.py
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/namespaces/cmfns.py	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/namespaces/cmfns.py	(revision 1597)
@@ -0,0 +1,77 @@
+"""
+    CMF Marshall namespace is overrided here in order to fix
+    LocalRolesAttribute class. It's not working in Marshall Product.
+"""
+
+from Products.Marshall import utils
+from Products.Marshall.namespaces import cmfns as base
+
+
+class LocalRolesAttribute(base.LocalRolesAttribute):
+
+    def getAttributeNames(self):
+        return (self.name, 'security')
+    
+    def processXml(self, context, node):
+        nsprefix = node.tag[:node.tag.find('}')+1]
+        local_roles = node.findall(nsprefix+self.name)
+        
+        if len(local_roles) == 0:
+            return
+
+        data = context.getDataFor(self.namespace.xmlns)
+        values = data.setdefault(self.name, [])
+        
+        for lrole in local_roles:
+            values.append((lrole.get('user_id'), lrole.get('role')))
+        
+        return True
+
+class WorkflowAttribute(base.WorkflowAttribute):
+    
+    def processXml(self, context, node):
+        data = context.getDataFor(self.namespace.xmlns)
+        wf_data = data.setdefault(self.name, {})
+        nsprefix = node.tag[:node.tag.find('}')+1]
+
+        for wf_node in node.findall(nsprefix+'workflow'):
+            # workflow
+            wf_id = wf_node.attrib.get(nsprefix+'id') or \
+                    wf_node.attrib.get('id')
+            if wf_id is None:
+                continue
+
+            # history
+            wf = wf_data.setdefault(wf_id, [])
+            hist_nodes = wf_node.findall(nsprefix+'history')
+            for hist_node in hist_nodes:
+                record = {}
+                wf.append(record)
+
+                #var
+                var_nodes = hist_node.findall(nsprefix+'var')
+                vid = vtype = value = None
+
+                for var_node in var_nodes:
+                    vid = var_node.attrib.get(nsprefix+'id') or \
+                          var_node.attrib.get('id')
+                    vtype = var_node.attrib.get(nsprefix+'type',None) or \
+                            var_node.attrib.get('type')
+                    value = var_node.attrib.get(nsprefix+'value',None) or \
+                            var_node.attrib.get('value') or ''
+
+                    if not (vid and vtype and value is not None):
+                        continue
+
+                    value = base.demarshall_value(value, vtype)
+                    record[vid] = value
+
+        return True
+
+class CMF(base.CMF):
+    
+    attributes = (
+        base.TypeAttribute('type'),
+        WorkflowAttribute('workflow_history'),
+        LocalRolesAttribute('local_role')
+        )
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/namespaces/dcns.py
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/namespaces/dcns.py	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/namespaces/dcns.py	(revision 1597)
@@ -0,0 +1,80 @@
+"""
+    DublinCore Marshall namespace but which can safely handle
+    Control Characters for you
+"""
+
+from Products.Archetypes.interfaces.base import IBaseUnit
+
+from Products.Marshall.namespaces import dcns as base
+from Products.Marshall.namespaces.dcns import normalizer
+
+from quintagroup.transmogrifier.namespaces.util import has_ctrlchars
+
+
+class DCAttribute(base.DCAttribute):
+
+    def serialize(self, dom, parent_node, instance):
+        values = self.get(instance)
+        if not values:
+            return False
+        
+        for value in values:
+            elname = "%s:%s"%(self.namespace.prefix, self.name)
+            node = dom.createElementNS(base.DublinCore.xmlns, elname)
+
+            # try to get 'utf-8' encoded string
+            if isinstance(value, unicode):
+                value = value.encode('utf-8')
+            elif IBaseUnit.isImplementedBy(value):
+                value = value.getRaw(encoding='utf-8')
+            else:
+                value = str(value)
+
+            if isinstance(value, str) and has_ctrlchars(value):
+                value = value.encode('base64')
+                attr = dom.createAttributeNS(base.DublinCore.xmlns,
+                                             'transfer_encoding')
+                attr.value = 'base64'
+                node.setAttributeNode(attr)
+                value_node = dom.createCDATASection(value)
+            else:
+                value_node = dom.createTextNode(value)
+
+            node.appendChild(value_node)
+            node.normalize()
+            parent_node.appendChild(node)
+        return True
+
+    def processXmlValue(self, context, value):
+        value = value and value.strip()
+        if not value:
+            return
+
+        # decode node value if needed
+        te = context.node.get('transfer_encoding', None)
+        if te is not None:
+            value = value.decode(te)
+
+        data = context.getDataFor(self.namespace.xmlns)
+        if self.many:
+            data.setdefault(self.name, []).append(value)
+        else:
+            data[self.name]=value
+
+class DublinCore(base.DublinCore):
+    
+    attributes = (
+        DCAttribute('title', 'Title', 'setTitle',
+                    process=(normalizer.space, normalizer.newline)),
+        
+        DCAttribute('description', 'Description', 'setDescription',
+                    process=(normalizer.space,)),
+
+        DCAttribute('subject', 'Subject', 'setSubject', many=True),                
+        DCAttribute('contributor', 'Contributors', 'setContributors',
+                    many=True),
+        # this attr diverges from cmfdefault.dublincore
+        DCAttribute('creator', 'Creators', 'setCreators', many=True),
+        DCAttribute('rights', 'Rights', 'setRights'),
+        DCAttribute('language', 'Language', 'setLanguage')
+        )
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/namespaces/util.py
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/namespaces/util.py	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/namespaces/util.py	(revision 1597)
@@ -0,0 +1,18 @@
+"""
+    Helpful functions for namespaces
+"""
+
+import re
+
+# control characters from 0-31 and 127 (delete) excluding:
+#   9 (\t, tab)
+#   10 (\n, new line character)
+#   13 (\r, carriage return)
+#   127 (\x1f, delete)
+# which are handled properly by python xml libraries such
+# as xml.dom.minidom and elementtree
+_ctrl_chars = re.compile(r'[\x00-\x08\x0b-\x0c\x0e-\x1f\x7f]')
+def has_ctrlchars(value):
+    if _ctrl_chars.search(value):
+        return True
+    return False
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/patches.py
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/patches.py	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/patches.py	(revision 1597)
@@ -0,0 +1,133 @@
+import os
+import time
+from tarfile import TarInfo, DIRTYPE
+from StringIO import StringIO
+
+# TarballExportContext don't write dirs in tarball and we need to fix this
+
+#security.declareProtected( ManagePortal, 'writeDataFile' )
+def writeDataFile( self, filename, text, content_type, subdir=None ):
+
+    """ See IExportContext.
+    """
+    mod_time = time.time()
+    if subdir is not None:
+        elements = subdir.split('/')
+        parents = filter(None, elements)
+        while parents:
+            dirname = os.path.join(*parents)
+            try:
+                self._archive.getmember(dirname+'/')
+            except KeyError:
+                info = TarInfo(dirname)
+                info.size = 0
+                info.mode = 509
+                info.mtime = mod_time
+                info.type = DIRTYPE
+                self._archive.addfile(info, StringIO())
+            parents = parents[:-1]
+
+        filename = '/'.join( ( subdir, filename ) )
+
+    stream = StringIO( text )
+    info = TarInfo( filename )
+    info.size = len( text )
+    info.mode = 436
+    info.mtime = mod_time
+    self._archive.addfile( info, stream )
+
+from Products.GenericSetup.context import TarballExportContext
+TarballExportContext.writeDataFile = writeDataFile
+
+from Products.GenericSetup.context import SKIPPED_FILES, SKIPPED_SUFFIXES
+
+def listDirectory(self, path, skip=SKIPPED_FILES,
+                    skip_suffixes=SKIPPED_SUFFIXES):
+
+    """ See IImportContext.
+    """
+    if path is None:  # root is special case:  no leading '/'
+        path = ''
+    elif path:
+        if not self.isDirectory(path):
+            return None
+
+        if not path.endswith('/'):
+            path = path + '/'
+
+    pfx_len = len(path)
+
+    names = []
+    for name in self._archive.getnames():
+        if name == path or not name.startswith(path):
+            continue
+        name = name[pfx_len:]
+        if name.count('/') > 1:
+            continue
+        if '/' in name and not name.endswith('/'):
+            continue
+        if name in skip:
+            continue
+        if [s for s in skip_suffixes if name.endswith(s)]:
+            continue
+        # directories have trailing '/' character and we need to remove it
+        name = name.rstrip('/')
+        names.append(name)
+
+    return names
+
+from Products.GenericSetup.context import TarballImportContext
+TarballImportContext.listDirectory = listDirectory
+
+# patch for this bug in tarfile module - http://bugs.python.org/issue1719898
+from tarfile import TarInfo, nts, GNUTYPE_SPARSE, normpath, DIRTYPE
+def frombuf(cls, buf):
+    """Construct a TarInfo object from a 512 byte string buffer.
+    """
+    tarinfo = cls()
+    tarinfo.name   = nts(buf[0:100])
+    tarinfo.mode   = int(buf[100:108], 8)
+    tarinfo.uid    = int(buf[108:116],8)
+    tarinfo.gid    = int(buf[116:124],8)
+
+    # There are two possible codings for the size field we
+    # have to discriminate, see comment in tobuf() below.
+    if buf[124] != chr(0200):
+        tarinfo.size = long(buf[124:136], 8)
+    else:
+        tarinfo.size = 0L
+        for i in range(11):
+            tarinfo.size <<= 8
+            tarinfo.size += ord(buf[125 + i])
+
+    tarinfo.mtime  = long(buf[136:148], 8)
+    tarinfo.chksum = int(buf[148:156], 8)
+    tarinfo.type   = buf[156:157]
+    tarinfo.linkname = nts(buf[157:257])
+    tarinfo.uname  = nts(buf[265:297])
+    tarinfo.gname  = nts(buf[297:329])
+    try:
+        tarinfo.devmajor = int(buf[329:337], 8)
+        tarinfo.devminor = int(buf[337:345], 8)
+    except ValueError:
+        tarinfo.devmajor = tarinfo.devmajor = 0
+    tarinfo.prefix = buf[345:500]
+
+    # The prefix field is used for filenames > 100 in
+    # the POSIX standard.
+    # name = prefix + '/' + name
+    if tarinfo.type != GNUTYPE_SPARSE:
+        tarinfo.name = normpath(os.path.join(nts(tarinfo.prefix), tarinfo.name))
+
+    # Some old tar programs represent a directory as a regular
+    # file with a trailing slash.
+    if tarinfo.isreg() and tarinfo.name.endswith("/"):
+        tarinfo.type = DIRTYPE
+
+    # Directory names should have a '/' at the end.
+    if tarinfo.isdir():
+        tarinfo.name += "/"
+    return tarinfo
+
+frombuf = classmethod(frombuf)
+TarInfo.frombuf = frombuf
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/pipelineconfig.pt
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/pipelineconfig.pt	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/pipelineconfig.pt	(revision 1597)
@@ -0,0 +1,52 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"
+      xmlns:tal="http://xml.zope.org/namespaces/tal">
+<head>
+  <title>Content migration configuration</title>
+  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+</head>
+<body>
+
+    <h1>Configuration of transmogrifier pipelines for content migration</h1>
+
+    <div style="background-color:#eee; padding:0.5em; line-height:1.2em;">
+        The default configuration is registered in ZCML and it's initially displayed
+        in this form. If you click 'Save' button for the first time it will be 
+        persisted in datebase and will override the default configuration. To swich
+        back to default configuration simply leave blank input area and click 'Save'.
+    </div>
+
+    <pre tal:content="view/status" tal:condition="view/status"
+         style="background-color:#ddd; padding:0.5em;
+         border: 1px black dashed; line-height:1.2em;">
+        Status
+    </pre>
+
+    <form action="#" method="POST"
+        tal:attributes="action request/getURL">
+
+        <table>
+            <tr>
+            <td>
+                <label>Export configuration <span style="color: red;" tal:condition="python:view.isDefault('export')">(default)</span></label>
+                <br/>
+                <textarea name="export" cols="80" rows="30"
+                    tal:content="python:view.getConfig('export')">
+                    config
+                </textarea>
+            </td>
+            <td>
+                <label>Import configuration <span style="color: red;" tal:condition="python:view.isDefault('import')">(default)</span></label>
+                <br/>
+                <textarea name="import" cols="80" rows="30" 
+                    tal:content="python:view.getConfig('import')">
+                    config
+                </textarea>
+            </td>
+        </table>
+        <input type="submit" name="action" value="Save" />
+    </form>
+
+</body>
+</html>
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/portlets.py
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/portlets.py	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/portlets.py	(revision 1597)
@@ -0,0 +1,289 @@
+import logging
+from xml.dom import minidom
+
+from zope.interface import classProvides, implements, providedBy
+from zope.component import getUtilitiesFor, queryMultiAdapter, getUtility, \
+    getMultiAdapter, adapts
+from zope.component.interfaces import IFactory
+from zope.app.container.interfaces import INameChooser
+from zope.schema._bootstrapinterfaces import ConstraintNotSatisfied
+from zope.schema.interfaces import ICollection
+
+
+from plone.portlets.interfaces import ILocalPortletAssignable, IPortletManager,\
+    IPortletAssignmentMapping, IPortletAssignment, ILocalPortletAssignmentManager
+from plone.portlets.constants import USER_CATEGORY, GROUP_CATEGORY, \
+    CONTENT_TYPE_CATEGORY, CONTEXT_CATEGORY
+from plone.app.portlets.interfaces import IPortletTypeInterface
+from plone.app.portlets.exportimport.interfaces import IPortletAssignmentExportImportHandler
+from plone.app.portlets.exportimport.portlets import PropertyPortletAssignmentExportImportHandler
+from plone.app.portlets.interfaces import IPortletTypeInterface
+
+from collective.transmogrifier.interfaces import ISection, ISectionBlueprint
+from collective.transmogrifier.utils import defaultMatcher
+
+class PortletsExporterSection(object):
+    classProvides(ISectionBlueprint)
+    implements(ISection)
+
+    def __init__(self, transmogrifier, name, options, previous):
+        self.previous = previous
+        self.context = transmogrifier.context
+
+        self.pathkey = defaultMatcher(options, 'path-key', name, 'path')
+        self.fileskey = options.get('files-key', '_files').strip()
+
+        self.doc = minidom.Document()
+
+    def __iter__(self):
+        self.portlet_schemata = dict([(iface, name,) for name, iface in 
+            getUtilitiesFor(IPortletTypeInterface)])
+        self.portlet_managers = list(getUtilitiesFor(IPortletManager))
+
+        for item in self.previous:
+            pathkey = self.pathkey(*item.keys())[0]
+
+            if not pathkey:
+                yield item; continue
+
+            path = item[pathkey]
+            obj = self.context.unrestrictedTraverse(path, None)
+            if obj is None:         # path doesn't exist
+                yield item; continue
+
+            if ILocalPortletAssignable.providedBy(obj):
+                data = None
+
+                root = self.doc.createElement('portlets')
+
+                for elem in self.exportAssignments(obj):
+                    root.appendChild(elem)
+                for elem in self.exportBlacklists(obj):
+                    root.appendChild(elem)
+                if root.hasChildNodes():
+                    self.doc.appendChild(root)
+                    data = self.doc.toprettyxml(indent='  ', encoding='utf-8')
+                    self.doc.unlink()
+
+                if data:
+                    files = item.setdefault(self.fileskey, {})
+                    item[self.fileskey]['portlets'] = {
+                        'name': '.portlets.xml',
+                        'data': data,
+                    }
+            yield item
+
+    def exportAssignments(self, obj):
+        assignments = []
+        for manager_name, manager in self.portlet_managers:
+            mapping = queryMultiAdapter((obj, manager), IPortletAssignmentMapping)
+            if mapping is None:
+                continue
+
+            mapping = mapping.__of__(obj)
+
+            for name, assignment in mapping.items():
+                type_ = None
+                for schema in providedBy(assignment).flattened():
+                    type_ = self.portlet_schemata.get(schema, None)
+                    if type_ is not None:
+                        break
+
+                if type_ is not None:
+                    child = self.doc.createElement('assignment')
+                    child.setAttribute('manager', manager_name)
+                    child.setAttribute('category', CONTEXT_CATEGORY)
+                    child.setAttribute('key', '/'.join(obj.getPhysicalPath()))
+                    child.setAttribute('type', type_)
+                    child.setAttribute('name', name)
+
+                    assignment = assignment.__of__(mapping)
+                    # use existing adapter for exporting a portlet assignment
+                    handler = IPortletAssignmentExportImportHandler(assignment)
+                    handler.export_assignment(schema, self.doc, child)
+
+                    assignments.append(child)
+
+        return assignments
+
+    def exportBlacklists(self, obj):
+        assignments = []
+        for manager_name, manager in self.portlet_managers:
+            assignable = queryMultiAdapter((obj, manager), ILocalPortletAssignmentManager)
+            if assignable is None:
+                continue
+            for category in (USER_CATEGORY, GROUP_CATEGORY, CONTENT_TYPE_CATEGORY, CONTEXT_CATEGORY,):
+                child = self.doc.createElement('blacklist')
+                child.setAttribute('manager', manager_name)
+                child.setAttribute('category', category)
+            
+                status = assignable.getBlacklistStatus(category)
+                if status == True:
+                    child.setAttribute('status', u'block')
+                elif status == False:
+                    child.setAttribute('status', u'show')
+                else:
+                    child.setAttribute('status', u'acquire')
+                    
+                assignments.append(child)
+
+        return assignments
+
+
+class PortletsImporterSection(object):
+    classProvides(ISectionBlueprint)
+    implements(ISection)
+
+    def __init__(self, transmogrifier, name, options, previous):
+        self.previous = previous
+        self.context = transmogrifier.context
+
+        self.pathkey = defaultMatcher(options, 'path-key', name, 'path')
+        self.fileskey = defaultMatcher(options, 'files-key', name, 'files')
+        self.purge = options.get('purge', 'false').strip().lower() == 'true' and True or False
+
+    def __iter__(self):
+
+        for item in self.previous:
+            pathkey = self.pathkey(*item.keys())[0]
+            fileskey = self.fileskey(*item.keys())[0]
+
+            if not (pathkey and fileskey):
+                yield item; continue
+            if 'portlets' not in item[fileskey]:
+                yield item; continue
+
+            path = item[pathkey]
+            obj = self.context.unrestrictedTraverse(path, None)
+            if obj is None:         # path doesn't exist
+                yield item; continue
+
+            # Purge assignments if 'purge' option set to true
+            if self.purge:
+                for name, portletManager in getUtilitiesFor(IPortletManager):
+                    assignable = queryMultiAdapter((obj, portletManager), IPortletAssignmentMapping)
+                    if assignable is not None:
+                        for key in list(assignable.keys()):
+                            del assignable[key]
+
+            if ILocalPortletAssignable.providedBy(obj):
+                data = None
+                data = item[fileskey]['portlets']['data']
+                doc = minidom.parseString(data)
+                root = doc.documentElement
+                for elem in root.childNodes:
+                    if elem.nodeName == 'assignment':
+                        self.importAssignment(obj, elem)
+                    elif elem.nodeName == 'blacklist':
+                        self.importBlacklist(obj, elem)
+
+            yield item
+
+    def importAssignment(self, obj, node):
+        """ Import an assignment from a node
+        """
+        # 1. Determine the assignment mapping and the name
+        manager_name = node.getAttribute('manager')
+        category = node.getAttribute('category')
+
+        manager = getUtility(IPortletManager, manager_name)
+        mapping = getMultiAdapter((obj, manager), IPortletAssignmentMapping)
+        if mapping is None:
+            return
+
+        # 2. Either find or create the assignment
+        assignment = None
+        name = node.getAttribute('name')
+        if name:
+            assignment = mapping.get(name, None)
+
+        type_ = node.getAttribute('type')
+
+        if assignment is None:
+            portlet_factory = getUtility(IFactory, name=type_)
+            assignment = portlet_factory()
+
+            if not name:
+                chooser = INameChooser(mapping)
+                name = chooser.chooseName(None, assignment)
+
+            mapping[name] = assignment
+
+        # aq-wrap it so that complex fields will work
+        assignment = assignment.__of__(obj)
+
+        # 3. Use an adapter to update the portlet settings
+        portlet_interface = getUtility(IPortletTypeInterface, name=type_)
+        assignment_handler = IPortletAssignmentExportImportHandler(assignment)
+        assignment_handler.import_assignment(portlet_interface, node)
+
+    def importBlacklist(self, obj, node):
+        """ Import a blacklist from a node
+        """
+        manager = node.getAttribute('manager')
+        category = node.getAttribute('category')
+        status = node.getAttribute('status')
+        
+        manager = getUtility(IPortletManager, name=manager)
+        
+        assignable = queryMultiAdapter((obj, manager), ILocalPortletAssignmentManager)
+        
+        if status.lower() == 'block':
+            assignable.setBlacklistStatus(category, True)
+        elif status.lower() == 'show':
+            assignable.setBlacklistStatus(category, False)
+        elif status.lower() == 'acquire':
+            assignable.setBlacklistStatus(category, None)
+
+
+logger = logging.getLogger('quintagroup.transmogrifier.portletsimporter')
+
+class PortletAssignmentExportImportHandler(PropertyPortletAssignmentExportImportHandler):
+    """ This adapter is needed because original fails to handle text from 
+        pretty printed XML file.
+    """
+    adapts(IPortletAssignment)
+
+    def extract_text(self, node):
+        text = super(PortletAssignmentExportImportHandler, self).extract_text(node)
+        # strip text to remove newlines and space character from the beginning 
+        # and the end
+        return text.strip()
+
+    def import_node(self, interface, child):
+        """Import a single <property /> node
+        """
+        property_name = child.getAttribute('name')
+
+        field = interface.get(property_name, None)
+        if field is None:
+            return
+
+        field = field.bind(self.assignment)
+        value = None
+
+        # If we have a collection, we need to look at the value_type.
+        # We look for <element>value</element> child nodes and get the
+        # value from there
+        if ICollection.providedBy(field):
+            value_type = field.value_type
+            value = []
+            for element in child.childNodes:
+                if element.nodeName != 'element':
+                    continue
+                element_value = self.extract_text(element)
+                value.append(self.from_unicode(value_type, element_value))
+            value = self.field_typecast(field, value)
+
+        # Otherwise, just get the value of the <property /> node
+        else:
+            value = self.extract_text(child)
+            value = self.from_unicode(field, value)
+
+        try:
+            field.validate(value)
+        except ConstraintNotSatisfied, e:
+            logger.warning('"%s" value doesn\'t satisfy constaints for "%s:%s" field' % \
+                (value, self.assignment.__name__, field.__name__))
+
+        field.set(self.assignment, value)
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/portlets.txt
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/portlets.txt	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/portlets.txt	(revision 1597)
@@ -0,0 +1,226 @@
+Portlets exporter and importer sections
+=======================================
+
+Portlets exporter and importer sections are used to migrate Plone 3 contextual 
+portlet assignments and blacklist settings.
+The portlets exporter section blueprint name is
+``quintagroup.transmogrifier.portletsexporter`` and importer section blueprint
+name is ``quintagroup.transmogrifier.portletsimporter``.
+
+Portlet assignments and blacklists are stored in XML format. Both sections need 
+relative path to the object with portlets assigned and importer also needs XML 
+data for updating them.
+
+Both sections has ``path-key`` option which specifies key in item where path to
+object is stored (``_path`` is default) and ``files-key`` option that gives
+key where XML data will be or is stored.
+
+>>> import pprint
+>>> portlets = """
+... [transmogrifier]
+... pipeline =
+...     portletssource
+...     portletsexporter
+...     printer
+...     dataprinter
+...     portletsimporter
+...     
+... [portletssource]
+... blueprint = quintagroup.transmogrifier.tests.portletssource
+... 
+... [portletsexporter]
+... blueprint = quintagroup.transmogrifier.portletsexporter
+... 
+... [printer]
+... blueprint = collective.transmogrifier.sections.tests.pprinter
+... 
+... [dataprinter]
+... blueprint = quintagroup.transmogrifier.tests.dataprinter
+... print = 
+...     _files
+...     portlets
+...     data
+... 
+... [portletsimporter]
+... blueprint = quintagroup.transmogrifier.portletsimporter
+... files-key = files
+... """
+>>> registerConfig(u'quintagroup.transmogrifier.tests.portlets', portlets)
+>>> transmogrifier(u'quintagroup.transmogrifier.tests.portlets') # doctest: +ELLIPSIS, +REPORT_NDIFF
+{}
+{'_path': 'not/existing/bar'}
+{'_path': 'spam/eggs/notassignable'}
+{'_files': {'portlets': {'data': ...
+                         'name': '.portlets.xml'}},
+ '_path': 'assignable'}
+<?xml version="1.0" encoding="utf-8"?>
+<portlets>
+  <assignment category="context" key="/assignable" manager="plone.leftcolumn" name="rss" type="portlets.rss">
+    <property name="count">
+      10
+    </property>
+    <property name="url">
+      http://sumno.com/feeds/main-page/
+    </property>
+    <property name="portlet_title">
+      RSS feed
+    </property>
+    <property name="timeout">
+      60
+    </property>
+  </assignment>
+  <blacklist category="user" manager="plone.leftcolumn" status="block"/>
+  <blacklist category="group" manager="plone.leftcolumn" status="acquire"/>
+  <blacklist category="content_type" manager="plone.leftcolumn" status="acquire"/>
+  <blacklist category="context" manager="plone.leftcolumn" status="acquire"/>
+</portlets>
+<BLANKLINE>
+{'_files': {'portlets': {'data': ...
+			 'name': '.portlets.xml'}},
+ '_path': 'other-assignable',
+ 'files': {'portlets': {'data': ...
+			'name': '.portlets.xml'}}}
+<?xml version="1.0" encoding="utf-8"?>
+<portlets>
+  <assignment category="context" key="/other-assignable" manager="plone.leftcolumn" name="rss" type="portlets.rss">
+    <property name="count">
+      10
+    </property>
+    <property name="url">
+      http://sumno.com/feeds/main-page/
+    </property>
+    <property name="portlet_title">
+      RSS feed
+    </property>
+    <property name="timeout">
+      60
+    </property>
+  </assignment>
+  <blacklist category="user" manager="plone.leftcolumn" status="block"/>
+  <blacklist category="group" manager="plone.leftcolumn" status="acquire"/>
+  <blacklist category="content_type" manager="plone.leftcolumn" status="acquire"/>
+  <blacklist category="context" manager="plone.leftcolumn" status="acquire"/>
+</portlets>
+<BLANKLINE>
+
+
+New portlet must be added to existent ones.
+
+>>> 'habra-rss' in mapping
+True
+>>> mapping['habra-rss'].count
+20
+>>> mapping['habra-rss'].url
+u'http://habrahabr.ru/rss/'
+>>> mapping['habra-rss'].portlet_title
+u'Habrahabr RSS feed'
+>>> mapping['habra-rss'].timeout
+120
+>>> 'rss' in mapping
+True
+>>> mapping['rss'].count
+10
+>>> mapping['rss'].url
+u'http://sumno.com/feeds/main-page/'
+>>> mapping['rss'].portlet_title
+u'RSS feed'
+>>> mapping['rss'].timeout
+60
+
+
+Export sections has also ``purge`` option which specifies if portlets must
+be purged before exporting. The ``purge`` option set to ``False`` by default
+and not purge existent portlets. To force purging - it must be set to ``True``.
+
+>>> portlets2 = """
+... [transmogrifier]
+... pipeline =
+...     portletssource
+...     portletsexporter
+...     printer
+...     dataprinter
+...     portletsimporter
+...     
+... [portletssource]
+... blueprint = quintagroup.transmogrifier.tests.portletssource2
+... 
+... [portletsexporter]
+... blueprint = quintagroup.transmogrifier.portletsexporter
+... 
+... [printer]
+... blueprint = collective.transmogrifier.sections.tests.pprinter
+... 
+... [dataprinter]
+... blueprint = quintagroup.transmogrifier.tests.dataprinter
+... print = 
+...     _files
+...     portlets
+...     data
+... 
+... [portletsimporter]
+... blueprint = quintagroup.transmogrifier.portletsimporter
+... files-key = files
+... purge = True
+... """
+>>> registerConfig(u'quintagroup.transmogrifier.tests.portlets2', portlets2)
+>>> transmogrifier(u'quintagroup.transmogrifier.tests.portlets2') # doctest: +ELLIPSIS, +REPORT_NDIFF
+{}
+{'_files': {'portlets': {'data': ...
+			 'name': '.portlets.xml'}},
+ '_path': 'other-assignable',
+ 'files': {'portlets': {'data': ...
+			'name': '.portlets.xml'}}}
+<?xml version="1.0" encoding="utf-8"?>
+<portlets>
+  <assignment category="context" key="/other-assignable" manager="plone.leftcolumn" name="rss" type="portlets.rss">
+    <property name="count">
+      10
+    </property>
+    <property name="url">
+      http://sumno.com/feeds/main-page/
+    </property>
+    <property name="portlet_title">
+      RSS feed
+    </property>
+    <property name="timeout">
+      60
+    </property>
+  </assignment>
+  <assignment category="context" key="/other-assignable" manager="plone.leftcolumn" name="habra-rss" type="portlets.rss">
+    <property name="count">
+      20
+    </property>
+    <property name="url">
+      http://habrahabr.ru/rss/
+    </property>
+    <property name="portlet_title">
+      Habrahabr RSS feed
+    </property>
+    <property name="timeout">
+      120
+    </property>
+  </assignment>
+  <blacklist category="user" manager="plone.leftcolumn" status="block"/>
+  <blacklist category="group" manager="plone.leftcolumn" status="acquire"/>
+  <blacklist category="content_type" manager="plone.leftcolumn" status="acquire"/>
+  <blacklist category="context" manager="plone.leftcolumn" status="acquire"/>
+</portlets>
+<BLANKLINE>
+
+Now only 'sumno-rss-2' portlet must be present in mapping after portlets purging.
+
+>>> 'habra-rss' in mapping
+False
+>>> 'rss' in mapping
+False
+>>> 'sumno-rss-2' in mapping
+True
+>>> mapping['sumno-rss-2'].count
+30
+>>> mapping['sumno-rss-2'].url
+u'http://sumno.com/rss'
+>>> mapping['sumno-rss-2'].portlet_title
+u'Sumno RSS feed'
+>>> mapping['sumno-rss-2'].timeout
+360
+
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/profiles/default/controlpanel.xml
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/profiles/default/controlpanel.xml	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/profiles/default/controlpanel.xml	(revision 1597)
@@ -0,0 +1,8 @@
+<?xml version="1.0"?>
+<object name="portal_controlpanel" meta_type="Plone Control Panel Tool">
+ <configlet title="Content migration" action_id="transmogrifier" appId="None"
+    category="Products" condition_expr=""
+    url_expr="string:${portal_url}/@@pipeline-config" visible="True">
+  <permission>Manage portal</permission>
+ </configlet>
+</object>
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/profiles/default/metadata.xml
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/profiles/default/metadata.xml	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/profiles/default/metadata.xml	(revision 1597)
@@ -0,0 +1,4 @@
+<?xml version="1.0"?>
+<metadata>
+  <version>0.1</version>
+</metadata>
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/profiles/default/quintagroup.transmogrifier-import.txt
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/profiles/default/quintagroup.transmogrifier-import.txt	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/profiles/default/quintagroup.transmogrifier-import.txt	(revision 1597)
@@ -0,0 +1,1 @@
+To check if we really need to perform content import.
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/propertymanager.py
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/propertymanager.py	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/propertymanager.py	(revision 1597)
@@ -0,0 +1,261 @@
+from copy import deepcopy
+from xml.dom import minidom
+
+from zope.interface import classProvides, implements
+
+from collective.transmogrifier.interfaces import ISection, ISectionBlueprint
+from collective.transmogrifier.utils import defaultMatcher
+
+from OFS.interfaces import IPropertyManager
+from Products.GenericSetup.utils import PropertyManagerHelpers, NodeAdapterBase
+
+class Helper(PropertyManagerHelpers, NodeAdapterBase):
+    """ We need this class because PropertyManagerHelpers in _initProperties
+        method uses _convertToBoolean and _getNodeText methods from
+        NodeAdapterBase class.
+    """
+    
+    _encoding = 'utf-8'
+    
+    def __init__(self):
+        pass
+
+    def _getNodeText(self, node):
+        # We override method in NodeAdapterBase, because it return bad property value.
+        # When properties are extracted newline charcters and indentation were added to
+        # them, but these aren't stripped on import. Maybe this method doesn't handle
+        # properly multiline string values, but it is needed for importing.
+        text = ''
+        for child in node.childNodes:
+            if child.nodeName != '#text':
+                continue
+            text += child.nodeValue.strip()
+        return text
+
+    def _extractProperties(self):
+        fragment = self._doc.createDocumentFragment()
+
+        for prop_map in self.context._propertyMap():
+            prop_id = prop_map['id']
+            if prop_id == 'i18n_domain':
+                continue
+
+            # Don't export read-only nodes
+            if 'w' not in prop_map.get('mode', 'wd'):
+                continue
+
+            node = self._doc.createElement('property')
+            node.setAttribute('name', prop_id)
+
+            prop = self.context.getProperty(prop_id)
+            if isinstance(prop, (tuple, list)):
+                for value in prop:
+                    if isinstance(value, str):
+                        value = value.decode(self._encoding)
+                    child = self._doc.createElement('element')
+                    child.appendChild(self._doc.createTextNode(value))
+                    node.appendChild(child)
+            else:
+                if prop_map.get('type') == 'boolean':
+                    prop = unicode(bool(prop))
+                elif isinstance(prop, str):
+                    prop = prop.decode(self._encoding)
+                elif not isinstance(prop, basestring):
+                    prop = unicode(prop)
+                child = self._doc.createTextNode(prop)
+                node.appendChild(child)
+
+            if 'd' in prop_map.get('mode', 'wd') and not prop_id == 'title':
+                prop_type = prop_map.get('type', 'string')
+                node.setAttribute('type', unicode(prop_type))
+                select_variable = prop_map.get('select_variable', None)
+                if select_variable is not None:
+                    node.setAttribute('select_variable', select_variable)
+
+            if hasattr(self, '_i18n_props') and prop_id in self._i18n_props:
+                node.setAttribute('i18n:translate', '')
+
+            fragment.appendChild(node)
+
+        return fragment
+
+    def _initProperties(self, node):
+        obj = self.context
+        if node.hasAttribute('i18n:domain'):
+            i18n_domain = str(node.getAttribute('i18n:domain'))
+            obj._updateProperty('i18n_domain', i18n_domain)
+        for child in node.childNodes:
+            if child.nodeName != 'property':
+                continue
+            prop_id = str(child.getAttribute('name'))
+            prop_map = obj.propdict().get(prop_id, None)
+
+            if prop_map is None:
+                if child.hasAttribute('type'):
+                    val = str(child.getAttribute('select_variable'))
+                    prop_type = str(child.getAttribute('type'))
+                    obj._setProperty(prop_id, val, prop_type)
+                    prop_map = obj.propdict().get(prop_id, None)
+                else:
+                    raise ValueError("undefined property '%s'" % prop_id)
+
+            if not 'w' in prop_map.get('mode', 'wd'):
+                raise BadRequest('%s cannot be changed' % prop_id)
+
+            elements = []
+            remove_elements = []
+            for sub in child.childNodes:
+                if sub.nodeName == 'element':
+                    if len(sub.childNodes) > 0:
+                        value = sub.childNodes[0].nodeValue.strip()
+                        if isinstance(value, unicode):
+                            value = value.encode(self._encoding)
+                        if self._convertToBoolean(sub.getAttribute('remove')
+                                                  or 'False'):
+                            remove_elements.append(value)
+                            if value in elements:
+                                elements.remove(value)
+                        else:
+                            elements.append(value)
+                            if value in remove_elements:
+                                remove_elements.remove(value)
+
+            if elements or prop_map.get('type') == 'multiple selection':
+                prop_value = tuple(elements) or ()
+            elif prop_map.get('type') == 'boolean':
+                prop_value = self._convertToBoolean(self._getNodeText(child))
+            else:
+                # if we pass a *string* to _updateProperty, all other values
+                # are converted to the right type
+                prop_value = self._getNodeText(child).encode(self._encoding)
+
+            if not self._convertToBoolean(child.getAttribute('purge')
+                                          or 'True'):
+                # If the purge attribute is False, merge sequences
+                prop = obj.getProperty(prop_id)
+                if isinstance(prop, (tuple, list)):
+                    prop_value = (tuple([p for p in prop
+                                         if p not in prop_value and
+                                            p not in remove_elements]) +
+                                  tuple(prop_value))
+
+            obj._updateProperty(prop_id, prop_value)
+
+class PropertiesExporterSection(object):
+    classProvides(ISectionBlueprint)
+    implements(ISection)
+
+    def __init__(self, transmogrifier, name, options, previous):
+        self.previous = previous
+        self.context = transmogrifier.context
+
+        self.pathkey = defaultMatcher(options, 'path-key', name, 'path')
+        self.fileskey = options.get('files-key', '_files').strip()
+
+        self.excludekey = defaultMatcher(options, 'exclude-key', name, 'excluded_properties')
+        self.exclude = filter(None, [i.strip() for i in 
+                              options.get('exclude', '').splitlines()])
+
+        self.helper = Helper()
+        self.doc = minidom.Document()
+        self.helper._doc = self.doc
+
+    def __iter__(self):
+        helper = self.helper
+        doc = self.doc
+
+        for item in self.previous:
+            pathkey = self.pathkey(*item.keys())[0]
+
+            if not pathkey:
+                yield item; continue
+
+            path = item[pathkey]
+            obj = self.context.unrestrictedTraverse(path, None)
+            if obj is None:         # path doesn't exist
+                yield item; continue
+
+            if IPropertyManager.providedBy(obj):
+                data = None
+                excludekey = self.excludekey(*item.keys())[0]
+                excluded_props = tuple(self.exclude)
+                if excludekey:
+                    excluded_props = tuple(set(item[excludekey]) | set(excluded_props))
+
+                helper.context = obj
+                node = doc.createElement('properties')
+                for elem in helper._extractProperties().childNodes:
+                    if elem.nodeName != 'property':
+                        continue
+                    if elem.getAttribute('name') not in excluded_props:
+                        node.appendChild(deepcopy(elem))
+                if node.hasChildNodes():
+                    doc.appendChild(node)
+                    data = doc.toprettyxml(indent='  ', encoding='utf-8')
+                    doc.unlink()
+
+                if data:
+                    files = item.setdefault(self.fileskey, {})
+                    item[self.fileskey]['propertymanager'] = {
+                        'name': '.properties.xml',
+                        'data': data,
+                    }
+
+            yield item
+
+class PropertiesImporterSection(object):
+    classProvides(ISectionBlueprint)
+    implements(ISection)
+
+    def __init__(self, transmogrifier, name, options, previous):
+        self.previous = previous
+        self.context = transmogrifier.context
+
+        self.pathkey = defaultMatcher(options, 'path-key', name, 'path')
+        self.fileskey = defaultMatcher(options, 'files-key', name, 'files')
+
+        self.excludekey = defaultMatcher(options, 'exclude-key', name, 'excluded_properties')
+        self.exclude = filter(None, [i.strip() for i in 
+                            options.get('exclude', '').splitlines()])
+
+        self.helper = Helper()
+        self.helper._encoding = 'utf-8'
+
+    def __iter__(self):
+        helper = self.helper
+
+        for item in self.previous:
+            pathkey = self.pathkey(*item.keys())[0]
+            fileskey = self.fileskey(*item.keys())[0]
+
+            if not (pathkey and fileskey):
+                yield item; continue
+            if 'propertymanager' not in item[fileskey]:
+                yield item; continue
+
+            path = item[pathkey]
+            obj = self.context.unrestrictedTraverse(path, None)
+            if obj is None:         # path doesn't exist
+                yield item; continue
+
+            if IPropertyManager.providedBy(obj):
+                data = None
+                excludekey = self.excludekey(*item.keys())[0]
+                excluded_props = self.exclude
+                if excludekey:
+                    excluded_props = tuple(set(item[excludekey]) | set(excluded_props))
+
+                data = item[fileskey]['propertymanager']['data']
+                doc = minidom.parseString(data)
+                root = doc.documentElement
+                children = [k for k in root.childNodes]
+                for child in children:
+                    if child.nodeName != 'property':
+                        continue
+                    if child.getAttribute('name') in excluded_props:
+                        root.removeChild(child)
+
+                helper.context = obj
+                helper._initProperties(root)
+
+            yield item
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/propertymanager.txt
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/propertymanager.txt	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/propertymanager.txt	(revision 1597)
@@ -0,0 +1,76 @@
+Properties exporter and importer sections
+=========================================
+
+Properties exporter and importer sections are used to migrate properties for
+objects that inherit from OFS.PropertyManager.PropertyManager mixin class.
+The properties exporter section blueprint name is
+``quintagroup.transmogrifier.propertiesexporter`` and importer section blueprint
+name is ``quintagroup.transmogrifier.propertiesimporter``.
+
+Properties are represented in XML format and this is done with help of
+GenericSetup.utils.PropertyManagerHelpers class. Both sections need relative
+path to the object with properties and importer also needs XML data for updating
+properties.
+
+Both sections has ``path-key`` option which specifies key in item where path to
+object is stored (``_path`` is default) and ``files-key`` option that gives
+key where XML data will be or is stored. Exporter section also has ``exclude``
+option that give global list of property ids to exclude on serializing and
+``exclude-key`` option, that specifies key where same list per item is stored
+(default is ``_excluded_properties``). Properties that are present in any of
+these lists are excluded.
+
+>>> import pprint
+>>> propertymanager = """
+... [transmogrifier]
+... pipeline =
+...     propertymanagersource
+...     propertiesexporter
+...     printer
+...     dataprinter
+...     propertiesimporter
+...     
+... [propertymanagersource]
+... blueprint = quintagroup.transmogrifier.tests.propertymanagersource
+... 
+... [propertiesexporter]
+... blueprint = quintagroup.transmogrifier.propertiesexporter
+... exclude = 
+...     title
+...     notexistentprop
+... 
+... [printer]
+... blueprint = collective.transmogrifier.sections.tests.pprinter
+... 
+... [dataprinter]
+... blueprint = quintagroup.transmogrifier.tests.dataprinter
+... print = 
+...     _files
+...     propertymanager
+...     data
+... 
+... [propertiesimporter]
+... blueprint = quintagroup.transmogrifier.propertiesimporter
+... exclude = author
+... """
+>>> registerConfig(u'quintagroup.transmogrifier.tests.propertymanager', propertymanager)
+>>> transmogrifier(u'quintagroup.transmogrifier.tests.propertymanager') # doctest: +ELLIPSIS, +REPORT_NDIFF
+{}
+{'_path': 'not/existing/bar'}
+{'_path': 'spam/eggs/notatcontent'}
+{'_excluded_properties': ('encoding',),
+ '_files': {'propertymanager': {'data': ...
+                                'name': '.properties.xml'}},
+ '_path': 'spam/eggs/foo'}
+<?xml version="1.0" encoding="utf-8"?>
+<properties>
+  <property name="description">
+    value
+  </property>
+  <property name="author">
+    value
+  </property>
+</properties>
+<BLANKLINE>
+>>> pprint.pprint(plone.updated)
+('spam/eggs/foo', 'description', 'value')
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/reader.py
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/reader.py	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/reader.py	(revision 1597)
@@ -0,0 +1,95 @@
+import os.path
+
+from zope.interface import classProvides, implements
+from zope.annotation.interfaces import IAnnotations
+
+from collective.transmogrifier.interfaces import ISection, ISectionBlueprint
+from collective.transmogrifier.utils import defaultMatcher
+
+from Products.GenericSetup import context
+from Products.CMFCore import utils
+
+# import monkey pathes for GS TarballContext
+import quintagroup.transmogrifier.patches
+from quintagroup.transmogrifier.logger import VALIDATIONKEY
+
+class ReaderSection(object):
+    classProvides(ISectionBlueprint)
+    implements(ISection)
+
+    def __init__(self, transmogrifier, name, options, previous):
+        self.previous = previous
+        self.context = transmogrifier.context
+        self.options = options
+
+        self.anno = IAnnotations(transmogrifier)
+        self.storage = self.anno.setdefault(VALIDATIONKEY, [])
+
+        self.pathkey = options.get('path-key', '_path').strip()
+        self.fileskey = options.get('files-key', '_files').strip()
+        self.contextkey = options.get('context-key', '_import_context').strip()
+
+        if 'prefix' in options:
+            self.prefix = options['prefix'].strip()
+            self.prefix = self.prefix.strip('/')
+        else:
+            self.prefix = ''
+
+        context_type = options.get('context', 'tarball').strip()
+        if context_type not in ['directory', 'tarball', 'snapshot']:
+            context_type = 'tarball'
+
+        path = options.get('path', '').strip()
+
+        setup_tool = utils.getToolByName(self.context, 'portal_setup')
+        if context_type == 'directory':
+            self.import_context = context.DirectoryImportContext(setup_tool, path)
+        elif context_type == 'tarball':
+            if os.path.exists(path):
+                archive = file(path, 'rb')
+                archive_bits = archive.read()
+                archive.close()
+            else:
+                archive_bits = ''
+            self.import_context = context.TarballImportContext(setup_tool, archive_bits)
+        elif context_type == 'snapshot':
+            self.import_context = context.SnapshotImportContext(setup_tool, path)
+
+    def walk(self, top):
+        names = self.import_context.listDirectory(top)
+        if names is None:
+            names = []
+        yield self.readFiles(top, names)
+        for name in names:
+            name = os.path.join(top, name)
+            if self.import_context.isDirectory(name):
+                for i in self.walk(name):
+                    yield i
+
+    def readFiles(self, top, names):
+        path = top[len(self.prefix):]
+        path = path.lstrip('/')
+        item = {self.pathkey: path}
+        for name in names:
+            full_name = os.path.join(top, name)
+            if self.import_context.isDirectory(full_name): continue
+            section = self.options.get(name, name).strip()
+            files = item.setdefault(self.fileskey, {})
+            files[section] = {
+                'name': name,
+                'data': self.import_context.readDataFile(name, top)
+            }
+        return item
+
+    def __iter__(self):
+        for item in self.previous:
+            yield item
+
+        for item in self.walk(self.prefix):
+            # add import context to item (some next section may use it)
+            item[self.contextkey] = self.import_context
+            self.storage.append(item[self.pathkey])
+            yield item
+
+        if VALIDATIONKEY in self.anno:
+            del self.anno[VALIDATIONKEY]
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/reader.txt
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/reader.txt	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/reader.txt	(revision 1597)
@@ -0,0 +1,100 @@
+Reader section
+==============
+
+A reader source pipeline section walks through different GenericSetup
+import contexts and yields items for every folder. Item contains it's
+path stored on a key given in ``path-key`` option and dictionary of
+data of files, contained in directory to which this item corresponds,
+stored on a key given in ``files-key`` option. The reader section blueprint
+name is ``quintagroup.transmogrifier.reader``.
+
+Which import context to use is controlled by ``context`` option. Possible
+values are: directory, tarball (default if option is absent or it's illegal)
+and snapshot. The only required option in this section is ``path`` which
+is a filesystem path of directory or archive for first two contexts, or
+snapshot's id for last context. Other usefull option is ``prefix`` which
+is additional path that must be added to ``path`` (it was designed to
+be used for tarball context, where content lives in 'structure' subdirectory,
+but may be used for other two too). Also note that ``prefix`` isn't added
+to generated item's path.
+
+Reader reads files and adds to ``item[files-key]`` dictionary with file's data
+and name. This dictionary is stored on a key, that identifies to which section
+this file corresponds. Mapping between filenames and sections is defined in
+reader section options. Option name is filename and option value is section name.
+
+>>> reader = """
+... [transmogrifier]
+... pipeline =
+...     reader
+...     printer
+... 
+... [reader]
+... blueprint = quintagroup.transmogrifier.reader
+... prefix = structure
+... .objects.xml = manifest
+... .marshall.xml = marshall
+... .properties.xml = propertymanager
+... .comments.xml = comments
+... 
+... [printer]
+... blueprint = collective.transmogrifier.sections.tests.pprinter
+... """
+>>> registerConfig(u'quintagroup.transmogrifier.tests.reader', reader)
+>>> transmogrifier(u'quintagroup.transmogrifier.tests.reader') # doctest: +ELLIPSIS, +REPORT_NDIFF
+{'_files': {'other.file': {'data': 'some data', 'name': 'other.file'},
+            'propertymanager': {'data': 'some data',
+                                'name': '.properties.xml'}},
+ '_import_context': <Tarball ...>,
+ '_path': ''}
+{'_files': {'manifest': {'data': 'some data', 'name': '.objects.xml'}},
+ '_import_context': <Tarball ...>,
+ '_path': 'news'}
+{'_import_context': <Tarball ...>,
+ '_path': 'news/recent'}
+{'_files': {'manifest': {'data': 'some data', 'name': '.objects.xml'}},
+ '_import_context': <Tarball ...>,
+ '_path': 'pages'}
+{'_files': {'comments': {'data': 'some data', 'name': '.comments.xml'},
+            'marshall': {'data': 'some data', 'name': '.marshall.xml'}},
+ '_import_context': <Tarball ...>,
+ '_path': 'pages/front-page'}
+
+Now we test if reader context is properly controlled by ``context`` option.
+
+>>> dirreader = """
+... [transmogrifier]
+... pipeline =
+...     reader
+...     printer
+... 
+... [reader]
+... blueprint = quintagroup.transmogrifier.reader
+... context = directory
+... path = /path/to/directory
+... 
+... [printer]
+... blueprint = collective.transmogrifier.sections.tests.pprinter
+... """
+>>> registerConfig(u'quintagroup.transmogrifier.tests.dirreader', dirreader)
+>>> transmogrifier(u'quintagroup.transmogrifier.tests.dirreader') # doctest: +ELLIPSIS, +REPORT_NDIFF
+{'_import_context': <Directory args=(..., '/path/to/directory')>,
+ '_path': ''}
+>>> dbreader = """
+... [transmogrifier]
+... pipeline =
+...     reader
+...     printer
+... 
+... [reader]
+... blueprint = quintagroup.transmogrifier.reader
+... context = snapshot
+... path = snapshot-20081008163000
+... 
+... [printer]
+... blueprint = collective.transmogrifier.sections.tests.pprinter
+... """
+>>> registerConfig(u'quintagroup.transmogrifier.tests.dbreader', dbreader)
+>>> transmogrifier(u'quintagroup.transmogrifier.tests.dbreader') # doctest: +ELLIPSIS, +REPORT_NDIFF
+{'_import_context': <Snapshot args=(..., 'snapshot-20081008163000')>,
+ '_path': ''}
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/references.py
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/references.py	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/references.py	(revision 1597)
@@ -0,0 +1,39 @@
+from zope.interface import classProvides, implements
+
+from collective.transmogrifier.interfaces import ISection, ISectionBlueprint
+from collective.transmogrifier.utils import defaultMatcher
+
+from Products.CMFCore.utils import getToolByName
+from Products.Archetypes import config as atcfg
+
+# most reference importing actions are done in adapter, what can't be done is saved in
+# next global variables (this must be changed)
+from quintagroup.transmogrifier.adapters.importing import EXISTING_UIDS, REFERENCE_QUEUE
+
+class ReferencesImporterSection(object):
+    classProvides(ISectionBlueprint)
+    implements(ISection)
+
+    def __init__(self, transmogrifier, name, options, previous):
+        self.previous = previous
+        self.context = transmogrifier.context
+
+    def __iter__(self):
+        for item in self.previous:
+            yield item
+        # finalization of importing references
+        rc = getToolByName(self.context, atcfg.REFERENCE_CATALOG)
+        uc = getToolByName(self.context, atcfg.UID_CATALOG)
+        uids = uc.uniqueValuesFor('UID')
+        existing = set(uids)
+        for suid, rel_fields in REFERENCE_QUEUE.items():
+            instance = rc.lookupObject(suid)
+            for fname, tuids in rel_fields.items():
+                # now we update reference field only if all target UIDs are valid
+                # but may be it is better to update with as much as possible valid
+                # target UIDs (do existing.intersection(set(tuids)))
+                if set(tuids).issubset(existing):
+                    mutator = instance.Schema()[fname].getMutator(instance)
+                    mutator(tuids)
+        EXISTING_UIDS.clear()
+        REFERENCE_QUEUE.clear()
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/sitewalker.py
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/sitewalker.py	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/sitewalker.py	(revision 1597)
@@ -0,0 +1,62 @@
+from zope.interface import classProvides, implements
+from zope.annotation.interfaces import IAnnotations
+
+from collective.transmogrifier.interfaces import ISection, ISectionBlueprint
+from collective.transmogrifier.utils import Condition
+
+from Products.CMFCore.interfaces import IFolderish
+from Products.Archetypes.interfaces import IBaseFolder
+
+from quintagroup.transmogrifier.logger import VALIDATIONKEY
+
+class SiteWalkerSection(object):
+    classProvides(ISectionBlueprint)
+    implements(ISection)
+
+    def __init__(self, transmogrifier, name, options, previous):
+        self.previous = previous
+        self.context = transmogrifier.context
+        self.pathkey = options.get('path-key', '_path').strip()
+        self.typekey = options.get('type-key', '_type').strip()
+        self.entrieskey = options.get('entries-key', '_entries').strip()
+        # this is used for communication with 'logger' section
+        self.anno = IAnnotations(transmogrifier)
+        self.storage = self.anno.setdefault(VALIDATIONKEY, [])
+
+        self.condition = Condition(options.get('condition', 'python:True'),
+                                   transmogrifier, name, options)
+
+    def getContained(self, obj):
+        contained = [(k, v) for k, v in obj.contentItems()
+                        if self.condition(None, context=v)]
+        return tuple(contained)
+
+    def walk(self, obj):
+        if IFolderish.providedBy(obj) or IBaseFolder.providedBy(obj):
+            contained = self.getContained(obj)
+            yield obj, tuple([(k, v.getPortalTypeName()) for k, v in contained])
+            for k, v in contained:
+                for x in self.walk(v):
+                    yield x
+        else:
+            yield obj, ()
+
+    def __iter__(self):
+        for item in self.previous:
+            yield item
+
+        for obj, contained in self.walk(self.context):
+            item = {
+                self.pathkey: '/'.join(obj.getPhysicalPath()[2:]),
+                self.typekey: obj.getPortalTypeName(),
+            }
+            if contained:
+                item[self.entrieskey] = contained
+            # add item path to stack
+            self.storage.append(item[self.pathkey])
+        
+            yield item
+
+        # cleanup
+        if VALIDATIONKEY in self.anno:
+            del self.anno[VALIDATIONKEY]
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/sitewalker.txt
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/sitewalker.txt	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/sitewalker.txt	(revision 1597)
@@ -0,0 +1,54 @@
+Site walker source section
+==========================
+
+A Plone site walker source pipeline section lets you create pipeline items from
+contents of Plone site folders. The site walker source section blueprint name is
+``quitagroup.transmogrifier.sitewalker``.
+
+A site walker source section will recurse through site's content tree, starting 
+form folder, which is the context of transmogrifier, and will yield an item for
+each content object. For folder it will also add a tuple of (id, portal_type)
+pairs of contained items.
+
+You can also specify an optional ``condition`` option; if given, content object
+is going into pipeline only if the condition, which is also a TALES is true.
+
+>>> sitewalker = """
+... [transmogrifier]
+... pipeline =
+...     sitewalker
+...     printer
+...
+... [sitewalker]
+... blueprint = quintagroup.transmogrifier.sitewalker
+... condition = python:context.getPortalTypeName() not in ('File', 'Image')
+...
+... [printer]
+... blueprint = collective.transmogrifier.sections.tests.pprinter
+... """
+>>> registerConfig(u'quintagroup.transmogrifier.tests.sitewalker',
+...                sitewalker)
+>>> transmogrifier(u'quintagroup.transmogrifier.tests.sitewalker')
+{'_entries': (('document1', 'Document'),
+              ('folder1', 'Folder'),
+              ('document3', 'Document')),
+ '_path': '',
+ '_type': 'MockPortal'}
+{'_type': 'Document', '_path': 'document1'}
+{'_entries': (('folder2', 'Folder'), ('document2', 'Document')),
+ '_path': 'folder1',
+ '_type': 'Folder'}
+{'_type': 'Folder', '_path': 'folder1/folder2'}
+{'_type': 'Document', '_path': 'folder1/document2'}
+{'_type': 'Document', '_path': 'document3'}
+
+
+The ``condition`` expression has an access to the following:
+
+=================== ==========================================================
+ ``context``         the current content object
+ ``transmogrifier``  the transmogrifier
+ ``name``            the name of the splitter section
+ ``options``         the splitter options
+ ``modules``         sys.modules
+=================== ==========================================================
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/substitution.py
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/substitution.py	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/substitution.py	(revision 1597)
@@ -0,0 +1,24 @@
+from zope.interface import classProvides, implements
+from collective.transmogrifier.interfaces import ISectionBlueprint
+from collective.transmogrifier.interfaces import ISection
+
+class SubstitutionSection(object):
+    classProvides(ISectionBlueprint)
+    implements(ISection)
+
+    def __init__(self, transmogrifier, name, options, previous):
+        self.key = options['key'].strip()
+        self.oldkey = "_old" + self.key
+        self.options = options
+        self.previous = previous
+
+    def __iter__(self):
+        key = self.key
+        for item in self.previous:
+            if key in item:
+                old = item[key]
+                new = self.options.get(item[key])
+                if new is not None and old != new:
+                    item[key] = new.strip()
+                    item[self.oldkey] = old
+            yield item
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/substitution.txt
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/substitution.txt	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/substitution.txt	(revision 1597)
@@ -0,0 +1,37 @@
+Substitution section
+====================
+
+A substitution pipeline section lets you substitute item's value stored
+on some key with value given in options. The substitution section blueprint
+name is ``quintagroup.transmogrifier.substitution``.
+
+Substition is performed in this way: if section has option with name that
+is equal to value stored on this key, than value of this option is used
+as new value for this key. Old value is stored on ``_old${key}`` key.
+Which key to use is determined by ``key`` option.
+
+>>> substitution = """
+... [transmogrifier]
+... pipeline =
+...     source
+...     substitution
+...     printer
+... 
+... [source]
+... blueprint = quintagroup.transmogrifier.tests.substitutionsource
+... 
+... [substitution]
+... blueprint = quintagroup.transmogrifier.substitution
+... key = _type
+... Blog = Weblog
+... PloneFormMailer = FormFolder
+... 
+... [printer]
+... blueprint = collective.transmogrifier.sections.tests.pprinter
+... """
+>>> registerConfig(u'quintagroup.transmogrifier.tests.substitution', substitution)
+>>> transmogrifier(u'quintagroup.transmogrifier.tests.substitution') # doctest: +ELLIPSIS, +REPORT_NDIFF
+{}
+{'_old_type': 'Blog', '_type': 'Weblog'}
+{'_old_type': 'PloneFormMailer', '_type': 'FormFolder'}
+{'_type': 'Document'}
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/tests.py
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/tests.py	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/tests.py	(revision 1597)
@@ -0,0 +1,1256 @@
+import unittest
+import pprint
+import os
+
+from zope.testing import doctest, cleanup
+from zope.component import provideUtility, provideAdapter, adapts
+from zope.interface import classProvides, implements
+
+from collective.transmogrifier.interfaces import ISectionBlueprint, ISection
+from collective.transmogrifier.tests import tearDown
+from collective.transmogrifier.sections.tests import sectionsSetUp
+from collective.transmogrifier.sections.tests import SampleSource
+
+from Products.Five import zcml
+
+import quintagroup.transmogrifier
+from quintagroup.transmogrifier.xslt import stylesheet_registry
+
+class DataPrinter(object):
+    classProvides(ISectionBlueprint)
+    implements(ISection)
+
+    def __init__(self, transmogrifier, name, options, previous):
+        self.previous = previous
+        self.printkey = [i.strip() for i in options['print'].splitlines() if i.strip()]
+        if 'prettyprint' in options:
+            self.pprint = pprint.PrettyPrinter().pprint
+
+    def __iter__(self):
+        for item in self.previous:
+            if self.printkey:
+                data = item
+                for i in self.printkey:
+                    if i in data:
+                        data = data[i]
+                    else:
+                        data = None
+                        break
+                if data is not None:
+                    if hasattr(self, 'pprint'):
+                        self.pprint(data)
+                    else:
+                        print data
+            yield item
+
+ctSectionsSetup = sectionsSetUp
+def sectionsSetUp(test):
+    ctSectionsSetup(test)
+    import Products.Five
+    import Products.GenericSetup
+    import zope.annotation
+    zcml.load_config('meta.zcml', Products.Five)
+    zcml.load_config('permissions.zcml', Products.Five)
+    zcml.load_config('meta.zcml', Products.GenericSetup)
+    zcml.load_config('configure.zcml', zope.annotation)
+    zcml.load_config('configure.zcml', quintagroup.transmogrifier)
+
+    from Products.CMFCore import utils
+    def getToolByName(context, tool_id, default=None):
+        return context
+    utils.getToolByName = getToolByName
+
+    import Acquisition
+    def aq_base(obj):
+        return obj
+    Acquisition.aq_base = aq_base
+
+    provideUtility(DataPrinter,
+        name=u'quintagroup.transmogrifier.tests.dataprinter')
+
+def siteWalkerSetUp(test):
+    sectionsSetUp(test)
+
+    from Products.CMFCore.interfaces import IFolderish
+    from Products.Archetypes.interfaces import IBaseFolder
+
+    class MockContent(object):
+        path = ()
+
+        def getPhysicalPath(self):
+            return self.path
+
+        def getPortalTypeName(self):
+            return self.__class__.__name__
+
+    class Document(MockContent):
+        pass
+
+    class Folder(MockContent, dict):
+        implements(IBaseFolder)
+
+        contentItems = dict.items
+        contentValues = dict.values
+
+    class MockPortal(MockContent, dict):
+        implements(IFolderish)
+
+        contentItems = dict.items
+        contentValues = dict.values
+
+    portal = MockPortal()
+
+    test.globs['plone'] = portal
+    test.globs['transmogrifier'].context = test.globs['plone']
+
+    portal.path = ('', 'plone')
+    portal['document1'] = Document()
+    portal['document1'].path = ('', 'plone', 'document1')
+    portal['folder1'] = Folder()
+    portal['folder1'].path = ('', 'plone', 'folder1')
+    portal['folder1']['document2'] = Document()
+    portal['folder1']['document2'].path = ('', 'plone', 'folder1', 'document2')
+    portal['folder1']['folder2'] = Folder()
+    portal['folder1']['folder2'].path = ('', 'plone', 'folder1', 'folder2')
+    portal['document3'] = Document()
+    portal['document3'].path = ('', 'plone', 'document3')
+
+def manifestSetUp(test):
+    sectionsSetUp(test)
+
+    root = dict(
+        _path='',
+        _entries=(
+            ('news', 'Folder'),
+            ('events', 'Folder'),
+            ('front-page', 'Document'),
+            ('only-in-manifest', 'Document')
+        )
+    )
+
+    news = dict(
+        _path='news',
+        _entries=(
+            ('aggregator', 'Topic'),
+            ('once-more', 'File')
+        )
+    )
+
+    aggregator = dict(
+        _path='news/aggregator',
+    )
+
+    events = dict(
+        _path='events'
+    )
+
+    front_page = dict(
+        _path='front-page',
+    )
+
+    members = dict(
+        _path='Members'
+    )
+
+    class ManifestSource(SampleSource):
+        classProvides(ISectionBlueprint)
+        implements(ISection)
+
+        def __init__(self, *args, **kw):
+            super(ManifestSource, self).__init__(*args, **kw)
+            self.sample = (root, dict(), news, aggregator, events, front_page, members)
+
+    provideUtility(ManifestSource,
+        name=u'quintagroup.transmogrifier.tests.manifestsource')
+
+def marshallSetUp(test):
+    sectionsSetUp(test)
+
+    from Products.Archetypes.interfaces import IBaseObject
+
+    class Field(object):
+        def __init__(self, name):
+            self.name = name
+            self.obj = None
+
+        def getAccessor(self, obj):
+            self.obj = obj
+            return self
+
+        def getMutator(self, obj):
+            self.obj = obj
+            return self
+
+        def __call__(self, value=None):
+            if value is None:
+                return self.obj.fields[self.name]
+            else:
+                self.obj.fields[self.name] = value
+
+    class MockBase(object):
+        def __init__(self, effective=None):
+            self.fields = {
+                'effectiveDate': effective,
+                'modification_date': 'changed',
+            }
+
+        def checkCreationFlag(self):
+            return True
+
+        def unmarkCreationFlag(self):
+            pass
+
+        def at_post_create_script(self):
+            pass
+
+        def at_post_edit_script(self):
+            pass
+
+        indexed = ()
+        def indexObject(self):
+            self.indexed += (self._last_path,)
+
+        def getField(self, fname):
+            return Field(fname)
+
+    class MockCriterion(MockBase):
+        implements(IBaseObject)
+        _last_path = None
+        indexed = ()
+        def indexObject(self):
+            self.indexed += (self._last_path,)
+
+    class MockPortal(MockBase):
+        implements(IBaseObject)
+
+        criterion = MockCriterion('not changed')
+
+        _last_path = None
+        def unrestrictedTraverse(self, path, default):
+            if path[0] == '/':
+                return default # path is absolute
+            if isinstance(path, unicode):
+                return default
+            if path == 'not/existing/bar':
+                return default
+            if path == 'topic/criterion':
+                self._last_path = path
+                self.criterion._last_path = path
+                return self.criterion
+            if path.endswith('/notatcontent'):
+                return object()
+            self._last_path = path
+            return self
+
+        def getId(self):
+            return "plone"
+
+        indexed = ()
+        def indexObject(self):
+            self.indexed += (self._last_path,)
+
+        updatedRoles = False
+        def updateRoleMappings(self):
+            self.updatedRoles = True
+
+        reindexed = False
+        def reindexIndex(self, name, extra=None):
+            self.reindexed = True
+
+        marshalled = ()
+        def marshall(self, instance, **kwargs):
+            self.marshalled += ((self._last_path, kwargs.get('atns_exclude')),)
+            # Marshall often fails to export topic criteria
+            if isinstance(instance, MockCriterion):
+                return None, None, None
+            else:
+                return None, None, "marshalled"
+
+        demarshalled = ()
+        def demarshall(self, instance, data):
+            # we don't need to test Marshall product, only check if we call it's components
+            self.demarshalled += (self._last_path,)
+
+    portal = MockPortal()
+    test.globs['plone'] = portal
+    test.globs['transmogrifier'].context = test.globs['plone']
+
+    from Products.Marshall import registry
+    def getComponent(name):
+        return portal
+    registry.getComponent = getComponent
+
+    class MarshallSource(SampleSource):
+        classProvides(ISectionBlueprint)
+        implements(ISection)
+
+        def __init__(self, *args, **kw):
+            super(MarshallSource, self).__init__(*args, **kw)
+            self.sample = (
+                dict(),
+                dict(_path='spam/eggs/foo', _excluded_fields=('file', 'image')),
+                dict(_path='topic/criterion'),
+                dict(_path='not/existing/bar'),
+                dict(_path='spam/eggs/notatcontent', 
+                     _files=dict(marshall=dict(data='xml', name='.marshall.xml'))),
+            )
+    provideUtility(MarshallSource,
+        name=u'quintagroup.transmogrifier.tests.marshallsource')
+
+def propertyManagerSetUp(test):
+    sectionsSetUp(test)
+
+    from OFS.interfaces import IPropertyManager
+
+    class MockPortal(object):
+        implements(IPropertyManager)
+
+        _properties = (
+            {'id':'title', 'type': 'string', 'mode': 'w'},
+            {'id':'description', 'type': 'string', 'mode': 'w'},
+            {'id':'encoding', 'type': 'string', 'mode': 'w'},
+            {'id':'author', 'type': 'string', 'mode': 'w'}
+        )
+
+        _last_path = None
+        def unrestrictedTraverse(self, path, default):
+            if path[0] == '/':
+                return default # path is absolute
+            if isinstance(path, unicode):
+                return default
+            if path == 'not/existing/bar':
+                return default
+            if path.endswith('/notatcontent'):
+                return object()
+            self._last_path = path
+            return self
+
+        def _propertyMap(self):
+            return self._properties
+
+        def getProperty(self, id, d=None):
+            return 'value'
+
+        def propdict(self):
+            d={}
+            for p in self._properties:
+                d[p['id']]=p
+            return d
+
+        updated = ()
+        def _updateProperty(self, id, value):
+            self.updated += ((self._last_path, id, value))
+
+    portal = MockPortal()
+    test.globs['plone'] = portal
+    test.globs['transmogrifier'].context = test.globs['plone']
+
+    class PropertyManagerSource(SampleSource):
+        classProvides(ISectionBlueprint)
+        implements(ISection)
+
+        def __init__(self, *args, **kw):
+            super(PropertyManagerSource, self).__init__(*args, **kw)
+            self.sample = (
+                dict(),
+                dict(_path='not/existing/bar'),
+                dict(_path='spam/eggs/notatcontent'),
+                dict(_path='spam/eggs/foo', _excluded_properties=('encoding',)),
+            )
+
+    provideUtility(PropertyManagerSource,
+        name=u'quintagroup.transmogrifier.tests.propertymanagersource')
+
+def commentsSetUp(test):
+    sectionsSetUp(test)
+
+    class MockDiscussionItem(object):
+        creator = 'creator'
+        modified = 'date'
+
+        def __init__(self, reply, text=""):
+            self.in_reply_to = reply
+            self.text = text
+
+        def __of__(self, container):
+            return self
+
+        def getMetadataHeaders(self):
+            return []
+
+        def setMetadata(self, headers):
+            pass
+
+        def Creator(self):
+            return self.creator
+
+        def addCreator(self, creator):
+            self.creator = creator
+
+        def ModificationDate(self):
+            return self.modified
+
+        def setModificationDate(self, date):
+            self.modified = date
+
+        def setFormat(self, format):
+            pass
+
+        def _edit(self, text=None):
+            self.text = text
+
+        def indexObject(self):
+            pass
+
+        def __repr__(self):
+            return "<DicussionItem %s %s %s %s>" % (
+                self.Creator(),
+                self.ModificationDate(),
+                self.in_reply_to,
+                self.text
+                )
+
+    from Products.CMFDefault import DiscussionItem
+    DiscussionItem.DiscussionItem = MockDiscussionItem
+
+    class MockPortal(object):
+        _discussion = {
+            '1': MockDiscussionItem(None, 'comment to content'),
+            '2': MockDiscussionItem('1', 'reply to first comment'),
+            '3': MockDiscussionItem(None, 'other comment to content')
+        }
+        _container = {}
+
+        @property
+        def talkback(self):
+            return self
+
+        def objectItems(self):
+            l = self._discussion.items()
+            l.sort(key=lambda x: int(x[0]))
+            return l
+
+        def unrestrictedTraverse(self, path, default):
+            if path[0] == '/':
+                return default # path is absolute
+            if isinstance(path, unicode):
+                return default
+            if path == 'not/existing/bar':
+                return default
+            if path.endswith('/notdiscussable'):
+                return object()
+            return self
+
+        def getDiscussionFor(self, obj):
+            return self
+
+    portal = MockPortal()
+    test.globs['plone'] = portal
+    test.globs['transmogrifier'].context = test.globs['plone']
+
+    class CommentsSource(SampleSource):
+        classProvides(ISectionBlueprint)
+        implements(ISection)
+
+        def __init__(self, *args, **kw):
+            super(CommentsSource, self).__init__(*args, **kw)
+            self.sample = (
+                dict(),
+                dict(_path='not/existing/bar'),
+                dict(_path='spam/eggs/notdiscussable'),
+                dict(_path='spam/eggs/foo'),
+            )
+
+    provideUtility(CommentsSource,
+        name=u'quintagroup.transmogrifier.tests.commentssource')
+
+
+def dataCorrectorSetUp(test):
+    sectionsSetUp(test)
+
+    class MockPortal(object):
+        def unrestrictedTraverse(self, path, default):
+            if path[0] == '/':
+                return default # path is absolute
+            if isinstance(path, unicode):
+                return default
+            if path == 'not/existing/bar':
+                return default
+            if path.endswith('/notadaptable'):
+                return object()
+            return self
+
+    portal = MockPortal()
+    test.globs['plone'] = portal
+    test.globs['transmogrifier'].context = test.globs['plone']
+
+    from collective.transmogrifier.interfaces import ITransmogrifier
+    from quintagroup.transmogrifier.interfaces import IExportDataCorrector, \
+        IImportDataCorrector
+
+    class MockExportAdapter(object):
+        implements(IExportDataCorrector)
+        adapts(MockPortal, ITransmogrifier)
+        def __init__(self, context, transmogrifier):
+            self.context = context
+            self.transmogrifier = transmogrifier
+
+        def __call__(self, data):
+            return "modified export data"
+
+    provideAdapter(MockExportAdapter, name="marshall")
+
+    class MockImportAdapter(object):
+        implements(IImportDataCorrector)
+        adapts(MockPortal, ITransmogrifier)
+        def __init__(self, context, transmogrifier):
+            self.context = context
+            self.transmogrifier = transmogrifier
+
+        def __call__(self, data):
+            return "modified import data"
+
+    provideAdapter(MockImportAdapter, name="manifest")
+
+    class DataCorrectorSource(SampleSource):
+        classProvides(ISectionBlueprint)
+        implements(ISection)
+
+        def __init__(self, *args, **kw):
+            super(DataCorrectorSource, self).__init__(*args, **kw)
+            self.sample = (
+                dict(),
+                dict(_files=dict(marshall="item hasn't path")),
+                dict(_path='spam/eggs/foo'),
+                dict(_path='not/existing/bar'),
+                dict(_path='spam/eggs/notadaptable', _files=dict(marshall="object isn't adaptable")),
+                dict(_path='spam/eggs/foo',
+                     _files=dict(marshall='marshall data', unchanged='this must be unchanged')),
+                dict(_path='spam/eggs/foo',
+                     _files=dict(manifest='manifest data', unchanged='this must be unchanged')),
+            )
+
+    provideUtility(DataCorrectorSource,
+        name=u'quintagroup.transmogrifier.tests.datacorrectorsource')
+
+def writerSetUp(test):
+    sectionsSetUp(test)
+
+    class MockExportContext(object):
+        def __init__( self, *args, **kwargs):
+            self.args = args
+            for k, v in kwargs.items():
+                setattr(self, k, v)
+            self._wrote = []
+
+        def __getitem__(self, name):
+            return getattr(self, name, None)
+
+        def __contains__(self, name):
+            return hasattr(self, name)
+
+        def writeDataFile(self, filename, text, content_type, subdir=None):
+            filename = '%s/%s' % (subdir, filename)
+            self._wrote.append((filename, text, content_type))
+
+        def __repr__(self):
+            s = " ".join(["%s=%s" % (k,v) for k,v in self.__dict__.items()])
+            return "<%s %s>" % (self.__class__.__name__, s)
+
+
+    from Products.GenericSetup import context
+
+    context.DirectoryExportContext = type('Directory', (MockExportContext,), {})
+    context.TarballExportContext = type('Tarball', (MockExportContext,), {})
+    context.SnapshotExportContext = type('Snapshot', (MockExportContext,), {})
+
+    class WriterSource(SampleSource):
+        classProvides(ISectionBlueprint)
+        implements(ISection)
+
+        def __init__(self, *args, **kw):
+            super(WriterSource, self).__init__(*args, **kw)
+            self.sample = (
+                dict(_path='spam/eggs/foo'),
+                dict(_files=dict(mock=dict(name='.first.xml', data='some data'))),
+                dict(_path='spam/eggs/foo',
+                     _files=dict(mock=dict(name='.first.xml', data='some data'),
+                                 other=dict(name='.second.xml', data='other data'))),
+                dict(_path='other/path',
+                     _files=dict(mock=dict(name='.third.xml', data='some data')))
+            )
+
+    provideUtility(WriterSource,
+        name=u'quintagroup.transmogrifier.tests.writersource')
+
+    class SingleItemSource(SampleSource):
+        classProvides(ISectionBlueprint)
+        implements(ISection)
+
+        def __init__(self, *args, **kw):
+            super(SingleItemSource, self).__init__(*args, **kw)
+            self.sample = (
+                dict(_path='', _files={}),
+            )
+
+    provideUtility(SingleItemSource,
+        name=u"quintagroup.transmogrifier.tests.singleitemsource")
+
+def readerSetUp(test):
+    sectionsSetUp(test)
+
+    class MockImportContext(object):
+
+        _dirs = [
+            'structure',
+            'structure/news', 'structure/news/recent',
+            'structure/pages', 'structure/pages/front-page',
+        ]
+        _files = [
+            'structure/.properties.xml',
+            'structure/other.file',
+            'structure/news/.objects.xml',
+            'structure/pages/.objects.xml',
+            'structure/pages/front-page/.marshall.xml',
+            'structure/pages/front-page/.comments.xml',
+        ]
+
+        def __init__( self, *args, **kwargs):
+            self.args = args
+            for k, v in kwargs.items():
+                setattr(self, k, v)
+
+        def __repr__(self):
+            s = " ".join(["%s=%s" % (k,v) for k,v in self.__dict__.items()])
+            return "<%s %s>" % (self.__class__.__name__, s)
+
+        def readDataFile(self, filename, subdir=None):
+            return 'some data'
+
+        def isDirectory(self, path):
+            return path == '' or path in self._dirs
+
+        def listDirectory(self, path):
+            all_names = self._dirs + self._files
+            if path:
+                pfx_len = len(path)+1
+            else:
+                pfx_len = 0
+            names = []
+            for name in all_names:
+                if name == path:
+                    continue
+                if not name.startswith(path):
+                    continue
+                name = name[pfx_len:]
+                if '/' in name:
+                    continue
+                names.append(name)
+            return names
+
+    from Products.GenericSetup import context
+
+    context.DirectoryImportContext = type('Directory', (MockImportContext,),
+        {'listDirectory': lambda self, path: []})
+    context.TarballImportContext = type('Tarball', (MockImportContext,), {})
+    context.SnapshotImportContext = type('Snapshot', (MockImportContext,),
+        {'listDirectory': lambda self, path: []})
+
+def substitutionSetUp(test):
+    sectionsSetUp(test)
+
+    class SubstitutionSource(SampleSource):
+        classProvides(ISectionBlueprint)
+        implements(ISection)
+
+        def __init__(self, *args, **kw):
+            super(SubstitutionSource, self).__init__(*args, **kw)
+            self.sample = (
+                {},
+                {'_type': 'Blog'},
+                {'_type': 'PloneFormMailer'},
+                {'_type': 'Document'},
+            )
+
+    provideUtility(SubstitutionSource,
+        name=u'quintagroup.transmogrifier.tests.substitutionsource')
+
+class MetaDirectivesTests(unittest.TestCase):
+    def setUp(self):
+        zcml.load_config('meta.zcml', quintagroup.transmogrifier)
+
+    def tearDown(self):
+        stylesheet_registry.clear()
+        cleanup.cleanUp()
+
+    def testEmptyZCML(self):
+        zcml.load_string('''\
+<configure xmlns:transmogrifier="http://namespaces.plone.org/transmogrifier">
+</configure>''')
+        self.assertEqual(stylesheet_registry.listStylesheetNames(), ())
+
+    def testConfigZCML(self):
+        zcml.load_string('''\
+<configure
+    xmlns:transmogrifier="http://namespaces.plone.org/transmogrifier">
+<transmogrifier:stylesheet
+    source="marshall"
+    from="Blog"
+    to="Weblog"
+    file="blog.xsl"
+    />
+</configure>''')
+        self.assertEqual(stylesheet_registry.listStylesheetNames(),
+                         (u'marshall:Blog:Weblog',))
+        path = os.path.split(quintagroup.transmogrifier.__file__)[0]
+        self.assertEqual(
+            stylesheet_registry.getStylesheet('marshall', 'Blog', 'Weblog'),
+            dict(from_=u'Blog',
+                 to=u'Weblog',
+                 file=os.path.join(path, 'blog.xsl'))
+        )
+
+    def testMultipleZCML(self):
+        zcml.load_string('''\
+<configure
+    xmlns:transmogrifier="http://namespaces.plone.org/transmogrifier">
+<transmogrifier:stylesheet
+    source="marshall"
+    from="Blog"
+    to="Weblog"
+    file="blog.xsl"
+    />
+<transmogrifier:stylesheet
+    source="propertymanager"
+    from="BlogEntry"
+    to="WeblogEntry"
+    file="blogentry.xsl"
+    />
+</configure>''')
+        self.assertEqual(stylesheet_registry.listStylesheetNames(),
+                         (u'marshall:Blog:Weblog', u'propertymanager:BlogEntry:WeblogEntry'))
+
+def xsltSetUp(test):
+    sectionsSetUp(test)
+
+    class XSLTSource(SampleSource):
+        classProvides(ISectionBlueprint)
+        implements(ISection)
+
+        def __init__(self, *args, **kw):
+            super(XSLTSource, self).__init__(*args, **kw)
+            self.sample = (
+                {},
+                {'_type': 'Weblog'},
+                {'_old_type': 'Blog'},
+                {'_old_type': 'Blog',
+                 '_type': 'Weblog',
+                 '_files': {'manifest': {'data': 'xml', 'name': 'manifest.xml'}}},
+                {'_old_type': 'Blog',
+                 '_type': 'Weblog',
+                 '_files': {'marshall': {'data': 'xml', 'name': 'marshall.xml'}}},
+            )
+
+    provideUtility(XSLTSource,
+        name=u'quintagroup.transmogrifier.tests.xsltsource')
+
+    from quintagroup.transmogrifier.xslt import XSLTSection, stylesheet_registry
+
+    XSLTSection.applyTransformations = lambda self, xml, xslt: 'transformed xml'
+    test.globs['stylesheet_registry'] = stylesheet_registry
+
+def binarySetUp(test):
+    sectionsSetUp(test)
+
+    from Products.Archetypes.interfaces import IBaseObject
+
+    class MockPortal(object):
+        implements(IBaseObject)
+
+        _last_path = None
+        def unrestrictedTraverse(self, path, default):
+            if path[0] == '/':
+                return default # path is absolute
+            if isinstance(path, unicode):
+                return default
+            if path == 'not/existing/bar':
+                return default
+            if path.endswith('/notatcontent'):
+                return object()
+            self._last_path = path
+            return self
+
+        fields = ['id', 'title', 'file', 'image']
+
+        def Schema(self):
+            return dict.fromkeys(self.fields)
+
+        def isBinary(self, field):
+            return field in ('file', 'image')
+
+        _current_field = None
+        def getField(self, field):
+            self._current_field = field
+            return self
+
+        def getBaseUnit(self, obj):
+            return self
+
+        def getFilename(self):
+            if self._current_field == 'file':
+                return 'archive.tar.gz'
+            else:
+                return ''
+
+        def getContentType(self):
+            if self._current_field == 'file':
+                return 'application/x-tar'
+            else:
+                return 'image/png'
+
+        def getRaw(self):
+            if self._current_field == 'file':
+                return 'binary data'
+            else:
+                return 'image'
+
+        def getMutator(self, obj):
+            return self
+
+        updated = ()
+        def __call__(self, data, filename=None, mimetype=None):
+            self.updated += (filename, mimetype, data)
+
+    portal = MockPortal()
+    test.globs['plone'] = portal
+    test.globs['transmogrifier'].context = test.globs['plone']
+
+    class BinarySource(SampleSource):
+        classProvides(ISectionBlueprint)
+        implements(ISection)
+
+        def __init__(self, *args, **kw):
+            super(BinarySource, self).__init__(*args, **kw)
+            self.sample = (
+                dict(),
+                dict(_path='not/existing/bar'),
+                dict(_path='spam/eggs/notatcontent'),
+                dict(_path='spam/eggs/foo'),
+            )
+
+    provideUtility(BinarySource,
+        name=u'quintagroup.transmogrifier.tests.binarysource')
+
+from DateTime import DateTime
+
+def catalogSourceSetUp(test):
+    sectionsSetUp(test)
+
+    class MockContent(dict):
+        def __init__(self, **kw):
+            self.update(kw)
+            self['id'] = self.getId
+
+        def getPath(self):
+            return self['path']
+
+        @property
+        def getId(self):
+            path = self.getPath()
+            return path.rsplit('/', 1)[-1]
+
+        @property
+        def portal_type(self):
+            return self['portal_type']
+
+        @property
+        def is_folderish(self):
+            return self['portal_type'] == 'Folder' and True or False
+
+    class MockPortal(dict):
+
+        content = ()
+        def __call__(self, **kw):
+            res = []
+            for obj in self.content:
+                matched = True
+                for index, query in kw.items():
+                    if index not in obj:
+                        matched = False
+                        break
+                    if matched and index == 'modified':
+                        if isinstance(query, dict):
+                            value = query['query']
+                            range_ = query['range']
+                            if range_ == 'min' and DateTime(obj[index]) >= DateTime(value):
+                                matched = True
+                            elif range_ == 'max' and DateTime(obj[index]) <= DateTime(value):
+                                matched = True
+                            else:
+                                matched = False
+                        else:
+                            if DateTime(obj[index]) == DateTime(query):
+                                matched = True
+                            else:
+                                matched = False
+                    elif matched and index == 'path':
+                        if obj[index].startswith(query):
+                            matched = True
+                        else:
+                            matched = False
+                    elif matched:
+                        if obj[index] == query:
+                            matched = True
+                        else:
+                            matched = False
+                if matched:
+                    res.append(obj)
+
+            return res
+
+    portal = MockPortal()
+    doc1 = MockContent(path='/plone/document1', portal_type='Document',
+        modified='2008-11-01T12:00:00Z')
+    folder1 = MockContent(path='/plone/folder1', portal_type='Folder',
+        modified='2008-11-01T12:00:00Z')
+    doc2 = MockContent(path='/plone/folder1/document2', portal_type='Document',
+        modified='2008-11-02T12:00:00Z')
+    doc3 = MockContent(path='/plone/folder1/document3', portal_type='Document',
+        modified='2008-11-02T12:00:00Z')
+    folder2 = MockContent(path='/plone/folder2', portal_type='Folder',
+        modified='2008-11-02T12:00:00Z')
+    doc4 = MockContent(path='/plone/folder2/document4', portal_type='Document',
+        modified='2008-11-01T12:00:00Z')
+    comment = MockContent(path='/plone/folder2/document4/talkback/1234567890', portal_type='Discussion Item',
+        modified='2008-11-02T12:00:00Z')
+    # items are sorted on their modification date
+    portal.content = (doc1, folder1, folder2, doc2, doc3, doc4, comment)
+
+    test.globs['plone'] = portal
+    test.globs['transmogrifier'].context = test.globs['plone']
+
+def flushCacheSetUp(test):
+    sectionsSetUp(test)
+    
+    class DataBase(object):
+        def __init__(self, context):
+            self.context = context
+        
+        def cacheMinimize(self):
+            self.context.cacheMinimized += 1
+
+        def _getDB(self):
+            return self
+    
+    class DataBasePanel(object):
+        def __init__(self, context):
+            self.context = context
+        
+        def getDatabaseNames(self):
+            return ('main',)
+        
+        def __getitem__(self, key):
+            return DataBase(self.context)
+    
+    class ControlPanel(object):
+        def __init__(self, context):
+            self.Database = DataBasePanel(context)
+    
+    class MockPortal(object):
+        def __init__(self):
+            self.cacheMinimized = 0
+            self.Control_Panel = ControlPanel(self)
+        
+    test.globs['plone'] = MockPortal()
+    test.globs['transmogrifier'].context = test.globs['plone']
+
+
+def interfaceManagerSetUp(test):
+    sectionsSetUp(test)
+
+    from zope.interface import Interface
+    from zope.annotation.interfaces import IAttributeAnnotatable
+    from zope.interface import alsoProvides as orig_alsoProvides
+    from Products.Archetypes.interfaces import IBaseObject
+
+    class MockPortal(object):
+
+        implements(
+            IBaseObject,
+        )
+
+        _last_path = None
+        def unrestrictedTraverse(self, path, default):
+            if path[0] == '/':
+                return default # path is absolute
+            if isinstance(path, unicode):
+                return default
+            if path == 'not/existing/bar':
+                return default
+            if path.endswith('/notatcontent'):
+                return object()
+            self._last_path = path
+            return self
+
+        # implement portal_catalog reindex method
+        def reindexIndex(self, *args, **kwargs):
+            pass
+
+
+    updated = []
+    test.globs['updated'] = updated
+    def patch_alsoProvides(object, *interfaces):
+        updated.extend([i.__identifier__ for i in interfaces])
+        orig_alsoProvides(object, *interfaces)
+    quintagroup.transmogrifier.interfacemanager.alsoProvides = patch_alsoProvides
+
+    portal = MockPortal()
+    orig_alsoProvides(portal, IAttributeAnnotatable, Interface)
+    test.globs['plone'] = portal
+    test.globs['transmogrifier'].context = test.globs['plone']
+
+    class InterfaceManagerSource(SampleSource):
+        classProvides(ISectionBlueprint)
+        implements(ISection)
+
+        def __init__(self, *args, **kw):
+            super(InterfaceManagerSource, self).__init__(*args, **kw)
+            self.sample = (
+                dict(),
+                dict(_path='not/existing/bar'),
+                dict(_path='spam/eggs/notatcontent'),
+                dict(_path='spam/eggs/foo'),
+            )
+
+    provideUtility(InterfaceManagerSource,
+        name=u'quintagroup.transmogrifier.tests.interfacemanagersource')
+
+def portletsSetUp(test):
+    sectionsSetUp(test)
+
+    from zope.interface import Interface
+    from zope.annotation.interfaces import IAttributeAnnotatable
+    from plone.portlets.interfaces import ILocalPortletAssignable
+
+    # this bases class adds __of__ method
+    from Acquisition import Implicit
+
+    class MockPortal(Implicit):
+        implements(IAttributeAnnotatable, ILocalPortletAssignable)
+
+        _last_path = None
+        def unrestrictedTraverse(self, path, default):
+            if path[0] == '/':
+                return default # path is absolute
+            if isinstance(path, unicode):
+                return default
+            if path == 'not/existing/bar':
+                return default
+            if path.endswith('/notassignable'):
+                return object()
+            self._last_path = path
+            return self
+
+        def getPhysicalPath(self):
+            return [''] + self._last_path.split('/')
+
+    portal = MockPortal()
+    test.globs['plone'] = portal
+    test.globs['transmogrifier'].context = test.globs['plone']
+
+    class PortletsSource(SampleSource):
+        classProvides(ISectionBlueprint)
+        implements(ISection)
+
+        def __init__(self, *args, **kw):
+            super(PortletsSource, self).__init__(*args, **kw)
+            self.sample = (
+                dict(),
+                dict(_path='not/existing/bar'),
+                dict(_path='spam/eggs/notassignable'),
+                dict(_path='assignable'),
+                dict(_path='other-assignable', 
+                    files=dict(portlets=dict(
+                        name='.portlets.xml',
+                        data="""<?xml version="1.0" encoding="utf-8"?>
+<portlets>
+  <assignment category="context" key="/other-assignable" manager="plone.leftcolumn" name="habra-rss" type="portlets.rss">
+    <property name="count">
+      20
+    </property>
+    <property name="url">
+      http://habrahabr.ru/rss/
+    </property>
+    <property name="portlet_title">
+      Habrahabr RSS feed
+    </property>
+    <property name="timeout">
+      120
+    </property>
+  </assignment>
+  <blacklist category="user" manager="plone.leftcolumn" status="block"/>
+  <blacklist category="group" manager="plone.leftcolumn" status="acquire"/>
+  <blacklist category="content_type" manager="plone.leftcolumn" status="acquire"/>
+  <blacklist category="context" manager="plone.leftcolumn" status="acquire"/>
+</portlets>
+""")
+                    )
+                )
+            )
+
+    provideUtility(PortletsSource,
+        name=u'quintagroup.transmogrifier.tests.portletssource')
+
+    class PortletsSource2(SampleSource):
+        classProvides(ISectionBlueprint)
+        implements(ISection)
+
+        def __init__(self, *args, **kw):
+            super(PortletsSource2, self).__init__(*args, **kw)
+            self.sample = (
+                dict(),
+                dict(_path='other-assignable',
+                    files=dict(portlets=dict(
+                        name='.portlets.xml',
+                        data="""<?xml version="1.0" encoding="utf-8"?>
+<portlets>
+  <assignment category="context" key="/other-assignable" manager="plone.leftcolumn" name="sumno-rss-2" type="portlets.rss">
+    <property name="count">
+      30
+    </property>
+    <property name="url">
+      http://sumno.com/rss
+    </property>
+    <property name="portlet_title">
+      Sumno RSS feed
+    </property>
+    <property name="timeout">
+      360
+    </property>
+  </assignment>
+  <blacklist category="user" manager="plone.leftcolumn" status="block"/>
+  <blacklist category="group" manager="plone.leftcolumn" status="acquire"/>
+  <blacklist category="content_type" manager="plone.leftcolumn" status="acquire"/>
+  <blacklist category="context" manager="plone.leftcolumn" status="acquire"/>
+</portlets>
+""")
+                    )
+                )
+            )
+    provideUtility(PortletsSource2,
+       name=u'quintagroup.transmogrifier.tests.portletssource2')
+
+    # prepare the one portlet for testing
+    from zope.interface import alsoProvides
+    from zope.component import getUtility, getMultiAdapter
+    from zope.component.interfaces import IFactory
+    from zope.component.factory import Factory
+
+    from plone.portlets.manager import PortletManager
+    from plone.portlets.interfaces import IPortletManager, IPortletType, \
+        IPortletAssignmentMapping
+    from plone.portlets.registration import PortletType
+
+    from plone.app.portlets.assignable import localPortletAssignmentMappingAdapter
+    from plone.portlets.assignable import LocalPortletAssignmentManager
+    from plone.app.portlets.interfaces import IPortletTypeInterface
+    from plone.portlets.interfaces import  ILocalPortletAssignmentManager
+    from plone.portlets.constants import USER_CATEGORY
+    # from plone.app.portlets.browser.interfaces import IPortletAdding
+    from plone.app.portlets.portlets.rss import IRSSPortlet, Assignment #, Renderer, AddForm, EditForm
+
+    # register portlet manager and assignment mapping adapter
+    manager = PortletManager()
+    provideUtility(manager, IPortletManager, name='plone.leftcolumn')
+    provideAdapter(localPortletAssignmentMappingAdapter)
+    provideAdapter(LocalPortletAssignmentManager)
+    mapping = getMultiAdapter((portal, manager), IPortletAssignmentMapping)
+    assignable = getMultiAdapter((portal, manager), ILocalPortletAssignmentManager)
+    test.globs['mapping'] = mapping
+    test.globs['assignable'] = assignable
+
+    # register portlet (this is what plone:portlet zcml directive does)
+    PORTLET_NAME = 'portlets.rss'
+    alsoProvides(IRSSPortlet, IPortletTypeInterface)
+    provideUtility(provides=IPortletTypeInterface, name=PORTLET_NAME, component=IRSSPortlet)
+    provideUtility(provides=IFactory, name=PORTLET_NAME, component=Factory(Assignment))
+
+    # register a portlet type (this is what <portlet /> element in portlets.xml
+    # does)
+    portlet = PortletType()
+    portlet.title = 'RSS Feed'
+    portlet.description = 'A portlet which can receive and render an RSS feed.'
+    portlet.addview = PORTLET_NAME
+    portlet.for_ = [Interface]
+    provideUtility(component=portlet, provides=IPortletType, 
+        name=PORTLET_NAME)
+
+    # add a portlet and configure it (this is done on @@manage-portlets view)
+    assignment = getUtility(IFactory, name=PORTLET_NAME)()
+    mapping['rss'] = assignment
+    portlet_interface = getUtility(IPortletTypeInterface, name=PORTLET_NAME)
+    data = {
+        'portlet_title': u'RSS feed',
+        'count': 10,
+        'url': u'http://sumno.com/feeds/main-page/',
+        'timeout': 60
+    }
+    for k, v in data.items():
+        field = portlet_interface.get(k)
+        field = field.bind(assignment)
+        field.validate(v)
+        field.set(assignment, v)
+    # set blacklists for user category to 'block'
+    assignable.setBlacklistStatus(USER_CATEGORY, True)
+
+def test_suite():
+    import sys
+    suite = unittest.findTestCases(sys.modules[__name__])
+    suite.addTests((
+        doctest.DocFileSuite(
+            'sitewalker.txt',
+            setUp=siteWalkerSetUp, tearDown=tearDown),
+        doctest.DocFileSuite(
+            'manifest.txt',
+            setUp=manifestSetUp, tearDown=tearDown),
+        doctest.DocFileSuite(
+            'marshall.txt',
+            setUp=marshallSetUp, tearDown=tearDown),
+        doctest.DocFileSuite(
+            'propertymanager.txt',
+            setUp=propertyManagerSetUp, tearDown=tearDown),
+        doctest.DocFileSuite(
+            'comments.txt',
+            setUp=commentsSetUp, tearDown=tearDown),
+        doctest.DocFileSuite(
+            'datacorrector.txt',
+            setUp=dataCorrectorSetUp, tearDown=tearDown),
+        doctest.DocFileSuite(
+            'writer.txt',
+            setUp=writerSetUp, tearDown=tearDown),
+        doctest.DocFileSuite(
+            'reader.txt',
+            setUp=readerSetUp, tearDown=tearDown),
+        doctest.DocFileSuite(
+            'substitution.txt',
+            setUp=substitutionSetUp, tearDown=tearDown),
+        doctest.DocFileSuite(
+            'xslt.txt',
+            setUp=xsltSetUp, tearDown=tearDown),
+        doctest.DocFileSuite(
+            'binary.txt',
+            setUp=binarySetUp, tearDown=tearDown),
+        doctest.DocFileSuite(
+            'catalogsource.txt',
+            setUp=catalogSourceSetUp, tearDown=tearDown),
+        doctest.DocFileSuite(
+            'flushcache.txt',
+            setUp=flushCacheSetUp, tearDown=tearDown),
+        doctest.DocFileSuite(
+            'interfacemanager.txt',
+            setUp=interfaceManagerSetUp, tearDown=tearDown),
+        doctest.DocFileSuite(
+            'portlets.txt',
+            setUp=portletsSetUp, tearDown=tearDown),
+    ))
+    return suite
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/version.txt
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/version.txt	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/version.txt	(revision 1597)
@@ -0,0 +1,1 @@
+0.1
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/writer.py
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/writer.py	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/writer.py	(revision 1597)
@@ -0,0 +1,64 @@
+import os.path
+import time
+
+from zope.interface import classProvides, implements
+
+from collective.transmogrifier.interfaces import ISection, ISectionBlueprint
+from collective.transmogrifier.utils import defaultMatcher
+
+from Products.GenericSetup import context
+from Products.CMFCore import utils
+
+# import monkey pathes for GS TarballContext
+import quintagroup.transmogrifier.patches
+
+class WriterSection(object):
+    classProvides(ISectionBlueprint)
+    implements(ISection)
+
+    def __init__(self, transmogrifier, name, options, previous):
+        self.previous = previous
+        self.context = transmogrifier.context
+
+        self.pathkey = defaultMatcher(options, 'path-key', name, 'path')
+        self.fileskey = defaultMatcher(options, 'files-key', name, 'files')
+
+        if 'prefix' in options:
+            self.prefix = options['prefix'].strip()
+        else:
+            self.prefix = ''
+
+        context_type = options.get('context', 'tarball').strip()
+
+        setup_tool = utils.getToolByName(self.context, 'portal_setup')
+        if context_type == 'directory':
+            profile_path = options.get('path', '')
+            self.export_context = context.DirectoryExportContext(setup_tool, profile_path)
+        elif context_type == 'tarball':
+            self.export_context = context.TarballExportContext(setup_tool)
+        elif context_type == 'snapshot':
+            items = ('snapshot',) + time.gmtime()[:6]
+            snapshot_id = '%s-%4d%02d%02d%02d%02d%02d' % items
+            self.export_context = context.SnapshotExportContext(setup_tool, snapshot_id)
+        else:
+            self.export_context = context.TarballExportContext(setup_tool)
+
+    def __iter__(self):
+        for item in self.previous:
+            pathkey = self.pathkey(*item.keys())[0]
+            fileskey = self.fileskey(*item.keys())[0]
+
+            if not (pathkey and fileskey): # path doesn't exist or no data to write
+                yield item; continue
+
+            item['_export_context'] = self.export_context
+
+            path = item[pathkey]
+
+            item_path = os.path.join(self.prefix, path)
+            item_path = item_path.rstrip('/')
+
+            for k, v in item[fileskey].items():
+                self.export_context.writeDataFile(v['name'], v['data'], 'text/xml', subdir=item_path)
+
+            yield item
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/writer.txt
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/writer.txt	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/writer.txt	(revision 1597)
@@ -0,0 +1,96 @@
+Writer section
+==============
+
+A writer pipeline section is used to store files in different GenericSetup
+export contexts. The writer section blueprint name is 
+``quintagroup.transmogrifier.writer``.
+
+>>> writer = """
+... [transmogrifier]
+... pipeline =
+...     writersource
+...     writer
+...     dataprinter
+...     
+... [writersource]
+... blueprint = quintagroup.transmogrifier.tests.writersource
+... 
+... [writer]
+... blueprint = quintagroup.transmogrifier.writer
+... prefix = structure
+... 
+... [dataprinter]
+... blueprint = quintagroup.transmogrifier.tests.dataprinter
+... prettyprint = true
+... print = 
+...     _export_context
+...     _wrote
+... """
+>>> registerConfig(u'quintagroup.transmogrifier.tests.writer', writer)
+>>> transmogrifier(u'quintagroup.transmogrifier.tests.writer') # doctest: +ELLIPSIS, +REPORT_NDIFF
+[('structure/spam/eggs/foo/.second.xml', 'other data', 'text/xml'),
+ ('structure/spam/eggs/foo/.first.xml', 'some data', 'text/xml')]
+[('structure/spam/eggs/foo/.second.xml', 'other data', 'text/xml'),
+ ('structure/spam/eggs/foo/.first.xml', 'some data', 'text/xml'),
+ ('structure/other/path/.third.xml', 'some data', 'text/xml')]
+
+Now we test if writer context is properly controlled by ``context`` option.
+
+>>> writerdefault = """
+... [transmogrifier]
+... pipeline =
+...     source
+...     writer
+...     printer
+...     
+... [source]
+... blueprint = quintagroup.transmogrifier.tests.singleitemsource
+... 
+... [writer]
+... blueprint = quintagroup.transmogrifier.writer
+... 
+... [printer]
+... blueprint = collective.transmogrifier.sections.tests.pprinter
+... """
+>>> registerConfig(u'quintagroup.transmogrifier.tests.writerdefault', writerdefault)
+>>> transmogrifier(u'quintagroup.transmogrifier.tests.writerdefault') # doctest: +ELLIPSIS, +REPORT_NDIFF
+{'_export_context': <Tarball args=(...) _wrote=[]>,
+ '_files': {},
+ '_path': ''}
+>>> writerdir = """
+... [transmogrifier]
+... include = quintagroup.transmogrifier.tests.writerdefault
+... 
+... [writer]
+... context = directory
+... path = content
+... """
+>>> registerConfig(u'quintagroup.transmogrifier.tests.writerdir', writerdir)
+>>> transmogrifier(u'quintagroup.transmogrifier.tests.writerdir') # doctest: +ELLIPSIS, +REPORT_NDIFF
+{'_export_context': <Directory args=(..., 'content') _wrote=[]>,
+ '_files': {},
+ '_path': ''}
+>>> writertar = """
+... [transmogrifier]
+... include = quintagroup.transmogrifier.tests.writerdefault
+...
+... [writer]
+... context = tarball
+... """
+>>> registerConfig(u'quintagroup.transmogrifier.tests.writertar', writertar)
+>>> transmogrifier(u'quintagroup.transmogrifier.tests.writertar') # doctest: +ELLIPSIS, +REPORT_NDIFF
+{'_export_context': <Tarball args=(...) _wrote=[]>,
+ '_files': {},
+ '_path': ''}
+>>> writerdb = """
+... [transmogrifier]
+... include = quintagroup.transmogrifier.tests.writerdefault
+...
+... [writer]
+... context = snapshot
+... """
+>>> registerConfig(u'quintagroup.transmogrifier.tests.writerdb', writerdb)
+>>> transmogrifier(u'quintagroup.transmogrifier.tests.writerdb') # doctest: +ELLIPSIS, +REPORT_NDIFF
+{'_export_context': <Snapshot args=(..., 'snapshot-...') _wrote=[]>,
+ '_files': {},
+ '_path': ''}
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/xslt.py
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/xslt.py	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/xslt.py	(revision 1597)
@@ -0,0 +1,154 @@
+try:
+    import libxml2
+    import libxslt
+except ImportError:
+    HAS_LIBS = False
+else:
+    HAS_LIBS = True
+
+from zope.interface import classProvides, implements, Interface
+from zope.configuration.fields import Path
+from zope.schema import TextLine
+
+from collective.transmogrifier.interfaces import ISectionBlueprint
+from collective.transmogrifier.interfaces import ISection
+from collective.transmogrifier.utils import defaultMatcher
+
+class StylesheetRegistry(object):
+    def __init__(self):
+        self.clear()
+
+    def clear(self):
+        self._stylesheet_info = {}
+
+    def registerStylesheet(self, source, from_, to, file):
+        name = "%s:%s" % (from_, to)
+        if source in self._stylesheet_info:
+            if name in self._stylesheet_info[source]:
+                raise KeyError('Duplicate stylesheet registration: %s %s %s' % 
+                    (source, from_, to))
+        source = self._stylesheet_info.setdefault(source, {})
+        source[name] = {
+            'from_': from_,
+            'to': to,
+            'file': file
+        }
+
+    def getStylesheet(self, source, from_, to):
+        name = "%s:%s" % (from_, to)
+        try:
+            return self._stylesheet_info[source][name]
+        except KeyError:
+            return None
+
+    def listStylesheetNames(self):
+        names = []
+        for k, v in self._stylesheet_info.items():
+            for name in v.keys():
+                names.append("%s:%s" % (k, name))
+        return tuple(names)
+
+stylesheet_registry = StylesheetRegistry()
+
+# Test cleanup support
+from zope.testing.cleanup import addCleanUp
+addCleanUp(stylesheet_registry.clear)
+del addCleanUp
+
+class IStylesheetDirective(Interface):
+    """ Register XSLT file with the global registry.
+    """
+
+    source = TextLine(
+        title=u"Source",
+        description=u"The source of XML data.",
+        required=True
+        )
+
+    from_ = TextLine(
+        title=u"From",
+        description=u"Value which describes XML data before transformation.",
+        required=True
+        )
+
+    to = TextLine(
+        title=u"To",
+        description=u"Value which describes XML data after transformation.",
+        required=True
+        )
+
+    file = Path(
+        title=u"XSLT file",
+        description=u"The XSLT file to register.",
+        required=True
+        )
+
+
+def stylesheet(_context, source, from_, to, file):
+    """Add a new stylesheet to the registry"""
+
+    _context.action(
+        discriminator=('stylesheet', source, from_, to),
+        callable=stylesheet_registry.registerStylesheet,
+        args=(source, from_, to, file))
+
+
+class XSLTSection(object):
+    classProvides(ISectionBlueprint)
+    implements(ISection)
+
+    def __init__(self, transmogrifier, name, options, previous):
+        self.fileskey = defaultMatcher(options, 'files-key', name, 'files')
+        self.source = options.get('source', 'marshall').strip()
+        self.fromkey = defaultMatcher(options, 'from-key', name, 'from')
+        self.tokey = defaultMatcher(options, 'to-key', name, 'to')
+
+        self.previous = previous
+
+    def __iter__(self):
+        source = self.source
+        for item in self.previous:
+            fileskey = self.fileskey(*item.keys())[0]
+            fromkey = self.fromkey(*item.keys())[0]
+            tokey = self.tokey(*item.keys())[0]
+
+            if not (fileskey and fromkey and tokey):
+                yield item; continue
+
+            if not (source in item[fileskey] and item[fileskey][source]):
+                yield item; continue
+
+            from_ = item[fromkey]
+            to = item[tokey]
+            stylesheet_info = stylesheet_registry.getStylesheet(source, from_, to)
+
+            if stylesheet_info is None:
+                yield item; continue
+
+            fp = open(stylesheet_info['file'], 'r')
+            stylesheet = fp.read()
+            fp.close()
+
+            source_dict = item[fileskey][source]
+            source_dict['data'] = self.applyTransformations(source_dict['data'], stylesheet)
+
+            yield item
+
+    def applyTransformations(self, xml, xslt):
+        if not HAS_LIBS:
+            raise RuntimeError("Can't apply transformations, libxml2/libxslt packages are not available")
+        # parse document
+        doc = libxml2.parseDoc(xml)
+        # parse stylesheet
+        styledoc = libxml2.parseDoc(xslt)
+        # make style object
+        style = libxslt.parseStylesheetDoc(styledoc)
+        # apply style to document
+        result = style.applyStylesheet(doc, None)
+        # write result to a file
+        transformed = style.saveResultToString(result)
+        # free all documents
+        style.freeStylesheet()
+        doc.freeDoc()
+        result.freeDoc()
+        return transformed
Index: quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/xslt.txt
===================================================================
--- quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/xslt.txt	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/quintagroup/transmogrifier/xslt.txt	(revision 1597)
@@ -0,0 +1,65 @@
+XSLT section
+============
+
+A XSLT pipeline section lets you apply stylesheet to some XML data stored on
+item. The XSLT section blueprint name is ``quintagroup.transmogrifier.xslt``.
+
+That data need to be transformed is determined by ``source`` option. It gives
+the key in dictionay where data is stored. Dictionary is standard structure
+for storing files data in other our sections. Data is stored in item on ``_files``
+key, but this can be changed with ``files-key`` option.
+
+Usually stylesheets are used to transform data returned by ``marshall`` section.
+We register those stylesheets in ZCML with special directive, for example:
+
+::
+
+    <stylesheet xmlns="http://namespaces.zope.org/transmogrifier"
+        from="OldPortalType"
+        to="NewPortalType"
+        source="marshall"
+        file="path/to/stylesheet.xsl"
+        />
+
+``source`` attribute in this directive is similar to that in section option.
+The default value for this option is ``marshall``.
+
+Which stylesheet to use is determined for every pipeline item by inspecting
+it's values stored on keys specified in ``from-key`` and ``to-key`` section
+options. These options are similar to ``from`` and ``to`` attributes in 
+ZCML directive. If item hasn't those keys, it will be skipped.
+
+>>> import tempfile
+>>> tmp = tempfile.NamedTemporaryFile('w+', suffix='.xsl')
+>>> tmp.flush()
+>>> stylesheet_registry.registerStylesheet('marshall', 'Blog', 'Weblog', tmp.name)
+>>> xslt = """
+... [transmogrifier]
+... pipeline =
+...     xsltsource
+...     xslt
+...     printer
+... 
+... [xsltsource]
+... blueprint = quintagroup.transmogrifier.tests.xsltsource
+... 
+... [xslt]
+... blueprint = quintagroup.transmogrifier.xslt
+... source = marshall
+... from-key = _old_type
+... to-key = _type
+... 
+... [printer]
+... blueprint = collective.transmogrifier.sections.tests.pprinter
+... """
+>>> registerConfig(u'quintagroup.transmogrifier.tests.xslt', xslt)
+>>> transmogrifier(u'quintagroup.transmogrifier.tests.xslt') # doctest: +ELLIPSIS, +REPORT_NDIFF
+{}
+{'_type': 'Weblog'}
+{'_old_type': 'Blog'}
+{'_files': {'manifest': {'data': 'xml', 'name': 'manifest.xml'}},
+ '_old_type': 'Blog',
+ '_type': 'Weblog'}
+{'_files': {'marshall': {'data': 'transformed xml', 'name': 'marshall.xml'}},
+ '_old_type': 'Blog',
+ '_type': 'Weblog'}
Index: quintagroup.transmogrifier/branches/ofs/setup.cfg
===================================================================
--- quintagroup.transmogrifier/branches/ofs/setup.cfg	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/setup.cfg	(revision 1597)
@@ -0,0 +1,3 @@
+[egg_info]
+tag_build = dev
+tag_svn_revision = true
Index: quintagroup.transmogrifier/branches/ofs/setup.py
===================================================================
--- quintagroup.transmogrifier/branches/ofs/setup.py	(revision 1597)
+++ quintagroup.transmogrifier/branches/ofs/setup.py	(revision 1597)
@@ -0,0 +1,34 @@
+from setuptools import setup, find_packages
+import os
+
+version = '0.2'
+
+setup(name='quintagroup.transmogrifier',
+      version=version,
+      description="Plone blueprints for collective.transmogrifier pipelines.",
+      long_description=open("README.txt").read() + "\n" +
+                       open(os.path.join("docs", "HISTORY.txt")).read(),
+      # Get more strings from http://www.python.org/pypi?%3Aaction=list_classifiers
+      classifiers=[
+        "Programming Language :: Python",
+        "Topic :: Software Development :: Libraries :: Python Modules",
+        ],
+      keywords='',
+      author='Quintagroup',
+      author_email='info@quintagroup.com',
+      url='http://quintagroup.com',
+      license='GPL',
+      packages=find_packages(exclude=['ez_setup']),
+      namespace_packages=['quintagroup'],
+      include_package_data=True,
+      zip_safe=False,
+      install_requires=[
+          'setuptools',
+          'collective.transmogrifier',
+          'plone.app.transmogrifier'
+          # -*- Extra requirements: -*-
+      ],
+      entry_points="""
+      # -*- Entry points: -*-
+      """,
+      )
