Keeping Client Concerns Separate
The basic skills learned in using the Zope Component Architecture to keep client concerns separate are as follows:
- Identify generic reusable portion of business logic required to satisfy client needs
- Construct components implementing generic reusable business logic that use adapters or utilities to provide functionality
- Provide a default set of adapters or utilities for implementing the generic reusable business logic
- Provide client-specific adapters and utilities that provide the client-specific non-reusable business logic in an external client-specific product
What follows is a simple example of utilizing the Zope Component Architecture to keep the required client customizations separate from the reusable portion of your application.
Feedfeeder
I recently worked on a Zope 2 product for Zest Software called feedfeeder that would use the feedparser python library to create content items within a Plone site. The basic premise was that there was a folderish content type that could be assigned URL’s to ATOM feeds. Then there was an action (a simple Zope 3 view) made available on the folder that when called would pull down the ATOM feeds and hand the content over to a Zope 3 utility. That utility would then create the appropriate content representing the ATOM entries.
This was all find and dandy for handling generic ATOM feeds. The problem is that the client had specific entry metadata they wanted to make available for each ATOM entry. Of course we didn’t want to extend the feedfeeder product to take into account the client specific data so we had to figure out an acceptable manner of extending it.
Using Microformats
While searching for a way to enhance an ATOM feed with custom metadata, Reinout Van Rees stumbled across the concept of microformats. Basically in this case a microformat would be a way to read and write metadata that fits within the boundaries of an existing format.
Lets take a simple standard ATOM entry as an example:
<entry>
<title>Bicycle service closed 17 July-7 August</title>
<id>http://somecustomer.com/someuid</id>
<content type="xhtml">
<div xmlns="http://www.w3.org/1999/xhtml">
Here lies the body of the atom entry.
</div>
</content>
<updated>2006-07-18T12:00:00Z</updated>
<author><name>Joe Schmoe</name></author>
</entry>
Now the type of content can be any valid non-multipart mime type. In the case of XHTML and a few others you can even drop the first part of the mime type name (the text/ portion). One of the most common types is XHTML. So this means for us a microformat would have to live sensibly as any valid type for the content tag.
We decided to go ahead and describe a microformat that would be valid XHTML so that in the case of a feed parser that didn’t know what to do with our microformat, it would still be decent looking XHTML. So how would we describe metadata structurally using XHTML? Easy, with definition lists.
<dl> <dt>somename</dt> <dd>somevalue</dd> </dl>
In the case of the client’s specific metadata, we needed to send over a special date. So our new ATOM entry would look like this:
<entry>
<title>Bicycle service closed 17 July-7 August</title>
<id>http://somecustomer.com/someuid</id>
<content type="xhtml">
<div xmlns="http://www.w3.org/1999/xhtml" class="colloquia">
<dl>
<dt>date</dt>
<dd>2006-03-23</dd>
</dl>
<div>
Here lies the body of the atom entry.
</div>
</div>
</content>
<updated>2006-07-18T12:00:00Z</updated>
<author><name>Joe Schmoe</name></author>
</entry>
So we did three things:
- Introduced new DL tag for describing metadata
- Moved actual content body to a sub DIV tag
- Added a new class attribute to the toplevel DIV tag to indicate the class or type of metadata
The next step in this process is to give feedfeeder a generic way of understanding this microformat.
Extending Feedfeeder
- The question we’re left to answer is:
- How do we provide a replaceable mechanism in feedfeeder for handling the metadata?
- And the answer is:
- Abstract the content handling logic to lookup up an adapter to deal with the content.
So since we knew our microformat had to use the XHTML type, within the feedfeeder code for handling the content we would only do our thing if the type was XHTML.
- Check the content type, is it XHTML?
- If the answer is “yes”, proceed to step #4
- If the answer is “no”, simply go about your default business
- Since it’s XML (XHTML), we use the standard python library package, xml.dom.minidom, to handle the body as a DOM fragment
- Next we look up the class attribute on the toplevel DIV tag within the DOM fragment
- Do a named adapter lookup providing IFeedItemContentHandler using the class attribute as the name
- Did we find an adapter? If so, we call it’s handling function passing in the DOM fragment
- We didn’t find an adapter? Then lookup the default unnamed adapter providing IFeedItemContentHandler and pass it the DOM fragement
Extending Client Product
So now we’ve given our feedfeeder product way of extending it’s content handling without the feedfeeder ever needing to know the specific things that could be done by external specific code.
What next? We go inside the client product implementing specific client business logic and provide a named adapter that provides IFeedItemContentHandler. In this case the type of metadata we want to handle is referred to as Colloquia. Now we’ll go ahead and make the name of the adapter be “colloquia”. Here’s an example of the ZCML used to register the adapter:
<adapter
provides="Products.feedfeeder.interfaces.contenthandler.IFeedItemContentHandler"
for="Products.feedfeeder.interfaces.item.IFeedItem"
name="colloquia"
factory=".colloquia.ColloquiaContentHandler"
/>
With the actual ColloquiaContentHandler class we simply provide the appropriate handling function that will now take the DOM fragment and parse the toplevel DL tag to determine what metadata is available.
Annotations
Annotations in the Zope Component Architecture are a way of marking up arbitrary content with arbitrary metadata. In this case we will use annotations to mark up the created content items from the ATOM feed. In the case of our IFeedItemContentHandler adapter we will use annotations to apply our colloquia metadata to the content items.
Here is some code demonstrating annotation handling.
# make sure the content item implements IAttributeAnnotatable,
# otherwise we won't be able to use annotations on it
if not IAttributeAnnotatable.providedBy(self.context):
directly = zope.interface.directlyProvidedBy(self.context)
zope.interface.directlyProvides(self.context,
directly + IAttributeAnnotatable)
annotations = IAnnotations(self.context)
metadata = annotations.get('colloquia', None)
if metadata is None:
metadata = PersistentDict()
annotations['colloquia'] = metadata
# and what we're left with is a simple dict referenced by the
# variable, metadata
Now that we have a dict to store our metadata in, we simply parse the DL tag definitions and populate our metadata dict.
for dl_el in contentNode.childNodes:
if dl_el.nodeName != 'dl':
continue
term = None
for el in dl_el.childNodes:
if el.nodeName == 'dt':
term = self._extractText(el)
elif el.nodeName == 'dd':
definition = self._extractText(el)
metadata[term] = definition
And voila, we have our extra client-specific metadata being stored on the content item. Of course it’s then up to the client product to pull out that metadata in custom views and such. In fact we actually took this further by overriding the default view of the feedfeeder (in the client specific product) and displayed the metadata in a client-specific manner.
Sep 17, 2006 9:56:00 AM by rocky, 0 comments