Contentmirror
=============

:Author:    $Author: seletz $
:Date:      $Date: 2008-01-17 11:18:45 +0100 (Do, 17 Jan 2008) $
:Revision:  $Revision: 57037 $

Abstract
--------

This package is provides a way to have a instance of a Plone_ content
mirrored transparently into one or more locations. To do so, we basically
need to:

- locate content to be mirrored on traversal

- insert the located oject into the traversed context's acquisition chain

To do so, we'll implement a adapter for the Zope 3 traversal mechanism, do
a lookup for content to be mirrored in this adapter, and insert the object
in the adapted contexts acquisition chain.

Gotchas, Limitations
--------------------

Because we do add the mirrored object like this, plone will eventually
reindex the object multiple times (for example if one edits the object when
appearing on a mirrored path). This has the following consequences:

searches -- on a search, the object is listed twice, because it's
    in the catalog twice. IMHO that's OK, because that's what we actually
    want, we want the object to appear in two places.

edit -- KSS edit and normal edit of mirrored content works as expected. The
    content will be reindexed on edit. Users which view the same object
    visible on another path will see the changes on page reload. Nothing
    unusual here.

removal, rename -- there it gets tricky. Because the object is in the
    twice, something like the **folder_listing** will show the object on
    mirrored paths despite it's no longer there. All we do is mirroring,
    not copying. I'll try to handle that with an event subscriber.

UID catalog -- because the object may be catalogued multiple times, the UID
    catalog will contain the UID multiple times. Furthermore, the catalog
    brains of the additional entries in the uid catalog will return
    **None** for a **getObject()**. The exact implications of this are
    unclear to me, that might well be an error in the UID catalog (why does
    it insert a UID twice in the first place?)

Basic Setup
-----------

some needed imports for this doctest::

    >>> from zope import interface
    >>> from zope import component
    >>> from zope.app.testing import ztapi
    >>> from zope.publisher.browser import TestRequest

Locating content
----------------

To locate content, we provide an interface::

    >>> from inquant.contentmirror.base.interfaces import IMirrorContentLocator

We now can define an adapter which is able to locate content from somewhere
else::

    >>> class TestLocator(object):
    ...     def __init__(self, context):
    ...         self.context = context
    ...     def locate( self, name):
    ...         return self.source.get(name)

So basically this adapter just has to return a object for a given name.
Let's try that. We need to setup some plone content for this::

    >>> _ = self.folder.invokeFactory("Folder", "src")
    >>> _ = self.folder.src.invokeFactory("Document", "doc", title="Muha")
    >>> _ = self.folder.invokeFactory("Folder", "target")

Now we can provide the adapter::

    >>> from Products.ATContentTypes.content.folder import ATFolder
    >>> ztapi.provideAdapter(ATFolder,
    ...    IMirrorContentLocator, TestLocator)

And look up the adapter::

    >>> locator = IMirrorContentLocator(self.folder.target)
    >>> locator.source = self.folder.src

now we can fetch the content by name::

    >>> locator.locate("doc")
    <ATDocument at /plone/Members/test_user_1_/src/doc>

Ok, that worked.

Inserting (mirroring) content
-----------------------------

Basically what we do is to strip the content to be mirrored from its
acquisition context and insert it into the target context's acquisition
chain. Lets try that::

    >>> from Acquisition import aq_inner, aq_base, aq_chain
    >>> obj = self.folder.src.doc
    >>> aq_chain(obj)
    [<ATDocument at /plone/Members/test_user_1_/src/doc>, <ATFolder at /plone/Members/test_user_1_/src>, ... 

We see, that **obj** has a normal acquisition chain, as one would expect.
Next, we'll fake the acquisition chain such that obj_mirrored will
**appear** to be below the **target** folder::

    >>> obj_mirrored = aq_base(obj).__of__(self.folder.target)
    >>> aq_chain(obj_mirrored)
    [<ATDocument at /plone/Members/test_user_1_/target/doc>, <ATFolder at /plone/Members/test_user_1_/target>, ... 

Yay.

Traversing
----------

Now all wa have to do is to provide a way to hook into Plone's object
traversal mechanism, and to alter it such that we can return the mirrored
object. The traverser we'll provide uses the **IPublishTraverse**
interface, which is the Zope 3 way of doing it::

    >>> from zope.publisher.interfaces import IPublishTraverse

Zope 2 used to use **__bobo_traverse__** to traverse objects. Nowadays,
traversal is done by providing a adapter to **IPublishTraverse**. The
default traverser is **DefaultPublishTraverse**, which is defined in the
Zope 2 publisher::

   >>> from ZPublisher.BaseRequest import DefaultPublishTraverse

This adapter does eventually call **__bobo_traverse__**. Thus, there's no
need to overwrite **__bobo_traverse__** anymore. Yay.

Our special adapter for our mirror content woll do the following:

- try to adapt the traversed context to **IMirrorContentLocator**, and
  locate a content for the currently traversed name

- strip the located content object of its acquisition chain and insert it
  into the travesed context's acquisition chain, and return it

Ok, let's try it.

First, we need to create a **IPublishTraverse** adapter. Note that this is
a multi adapter adapting a interface and a **IHTTPRequest** to
**IPublishTraverse**::

    >>> class MirrorTraverse(object):
    ...     def __init__(self,context,request):
    ...         self.context = context
    ...         self.request = request
    ...         self.locator = IMirrorContentLocator(context)
    ...     def publishTraverse(self, request, name):
    ...         obj = locator.locate(name)
    ...         return aq_base(aq_inner(obj)).__of__(self.context)

Now, we want to provide the adapter. We do NOT want to overwrite the
default behavior, though. That's why we define a marker interface to adapt
to **IMirrorContentProvider**. We provide the adapter::

    >>> from inquant.contentmirror.base.interfaces import IMirrorContentProvider
    >>> from zope.publisher.interfaces.http import IHTTPRequest
    >>> ztapi.provideAdapter(
    ...     (IMirrorContentProvider,IHTTPRequest),
    ...     IPublishTraverse,
    ...     MirrorTraverse)

Now we should be able to traverse. To call up the adapter we need a test
request, though::

    >>> request = TestRequest()
    >>> IHTTPRequest.providedBy(request)
    True

Query the ZCA for the adapter::

    >>> traverser = component.getMultiAdapter(
    ...     (self.folder.target, request), IPublishTraverse )
    Traceback (most recent call last):
    ...
    ComponentLookupError: ...

Ouch!
Ah, we need to provide the **IMirrorContentProvider** first::

    >>> interface.alsoProvides(self.folder.target, IMirrorContentProvider)
    >>> IMirrorContentProvider.providedBy(self.folder.target)
    True

Try again::

    >>> traverser = component.queryMultiAdapter(
    ...     (self.folder.target, request), IPublishTraverse )

A-haaa!

Unfortunately, for sake of this test, we need to patch in the source
manually. In reality, the locator adapter would of course determine the
source itself.::

    >>> traverser.locator.source = self.folder.src

Now try to traverse::

    >>> traverser.publishTraverse(request, "doc")
    <ATDocument at /plone/Members/test_user_1_/target/doc>

Yay! Note that the returned object seems to come from the **target**
folder, but it is located in the **src** folder in reality.

Cleanup
-------

Remove the adapter::

    >>> gsm = component.getGlobalSiteManager()
    >>> gsm.unregisterAdapter(
    ...     MirrorTraverse,
    ...     (IMirrorContentProvider,IHTTPRequest),
    ...     IPublishTraverse)
    True
    >>> gsm.unregisterAdapter(TestLocator, (ATFolder,),
    ...    IMirrorContentLocator)
    True

Links
-----

Plone_ is a CMS.

.. _Plone: http://plone.org


::

 vim: set ft=rst tw=75 nocin nosi ai sw=4 ts=4 expandtab:
