|
|
|
06/25/2009
By now, most of you should have heard about CMIS, the upcoming specification that promises interoperability between many systems for common content management tasks. The CMIS specification is being driven by an OASIS Technical Committee and is currently still a draft; it is expected to be finalized late 2009 or early 2010. I won't detail here all that CMIS will bring, this has been covered extensively already and will be even more in the future... No, the purpose of this article is to present Chemistry. ChemistryChemistry is a new Apache project for CMIS that started incubating recently ("incubation" is the term used in the Apache Software Foundation for young projects that still have to prove themselves). Chemistry's goal is to provide general purposes libraries for interaction using CMIS between a server and a client. These libraries are mainly written in Java, but some JavaScript code has been added as well, and we're open to more. Chemistry provides a high level API so that a developer can manipulate objects like documents or folders and can call simple methods on them without having to deal with details of a specific low-level communication transport. In addition to that, Chemistry also provides a SPI (Service Provider Interface) for backend developers, making it quite easy to use Chemistry to store documents in a project-specific manner. Underlying this, Chemistry has implementations for the CMIS transports. CMIS specifies two mandatory transport protocol bindings (one extending AtomPub, for a lightweight RESTful HTTP interface, and another using SOAP for a WebService-based interface), and Chemistry will support both — and probably more in the future. The current Chemistry code base has an initial version of the API/SPI together with some actual implementations around the AtomPub protocol. Already Chemistry can talk to itself (AtomPub client talking to AtomPub server) and store data in-memory (which is very handy for unit tests). Outside of the Apache code base, Nuxeo has also coded a backend to provide access to Nuxeo 5.2 repositories using Chemistry. Generic CMIS AtomPub clients like CMIS Explorer are able to see a Nuxeo repository through Chemistry for instance. Chemistry ModulesThe following modules will be available in Chemistry:
In the future, it is expected that more implementations of the APIs will be available, for example we envision new transports:
And new backends:
The Pieces of the PuzzleAs you can see, these modules will allow for wide interoperability between systems. Here's a graphical representation of the building blocks: The User Application speaks the API:
The API can be implemented in many ways. First, it could be a direct backend:
Or, more commonly, the API will be implemented as a client binding for a specific protocol, SOAP of AtomPub:
Each protocol speaks in its own way on the wire:
And this is connected to a server that speaks the protocol as well:
Finally, behind the server, a backend has to store the actual information somewhere:
Anyone is welcome to create new pieces, for instance new protocol bindings:
Or new storage backends:
Now let's see how the main pieces can be plugged together. The simplest connection is between an application and a direct backend:
If the backend only wants to deal with the SPI, its implementation can reuse the API-to-SPI to provide a full API experience:
When talking through a wire protocol, we plug together a client and a server:
The end result is an application talking to a backend through a wire protocol:
Of course we can get creative and plug many more together:
DevelopmentAll of this is still a work in progress (even the spec!), but you should expect rapid changes in the available features in the coming months as the spec settles down, more code is written, more test cases are written, and more testing against third-party implementations is done. If you're interested in helping, please join the list chemistry-dev@incubator.apache.org by sending an empty email to chemistry-dev-subscribe@incubator.apache.org.
Posted by Florent Guillaume @ 06/25/2009 03:26 PM.
-
Categories:
ecm,
java,
nuxeo,
nuxeo5
-
0 comments
06/23/2009
A few weeks ago I gave an interview to Irina Guseva of CMSWire. We touched the subjects of strategic value of CMIS, Apache Chemistry project history, partnerships, open source, future plans around CMIS, and more. Chemistry has extremely ambitious plans. We believe that it can become the de facto bridge between most of the Java-based content-oriented products, allowing a very wide variety of back-ends and applications to be connected together. And actually Java is not the sole language that this project is targeting, as David Nuescheler is also working on a JavaScript library for CMIS. In the coming month you should see an exponential increase in the functionality that Chemistry provides... You can read the full article at CMSWire.
Posted by Florent Guillaume @ 06/23/2009 03:30 PM.
-
Categories:
ecm,
java,
nuxeo,
nuxeo5
-
0 comments
02/03/2009
Last week took place the first face-to-face meeting of the OASIS CMIS Technical Committee (TC).
This first meeting was very productive, and allowed very constructive discussions. I'll try to retrace below the gist of the conversations around the topics I found most interesting, sometimes these were conversation I had with just one or two people, or topics related to that. The outlook from these discussions, and from the scope of the spec itself, is very positive. I believe that within a year CMIS will start to actively redefine the world of content management systems, which will be an opportunity both for big vendors who will see easier adoption of their solutions by customers concerned by lock-in or interoperability, and for smaller vendors whose products will be able to take advantage of a much broader spectrum of connectors to third-party systems. ScheduleThe most important news at the end of these three days is that there is enormous support in the TC for CMIS 1.0 to be released as soon as reasonably possible, as it is felt by all that a simple and solid spec that can be implemented and used ASAP by everyone is paramount. Due to time constraints inherent in the OASIS standardization process, this is likely to be in late 2009 or early 2010 -- and that's if we can polish the current draft and fix the problems within something like two months!Existing capabilitiesDuring this meeting it was stressed many times that the goal of CMIS is not to define the semantics for new features that a repository could implement, but to provide access to existing features of existing repositories, so that they can interoperate.This implies that complex features, non-standard features, or features that are common but implemented with a wide variety of semantics, have to be out of scope for CMIS 1.0. When features are exposed through CMIS, there is a duty to make sure that this can be done by almost everyone without rethinking the repository's architecture. Retention & HoldFor those not familiar with the terms, a retention policy describes the rules along which documents will be kept for a certain time then archived or destroyed, and holds are typically put on documents for legal purposes to prevent their destruction when companies are being sued or subpoenaed. A way to specify and discover documents that can have various holds put on them, or various retention policies, is critical to all the Record Management folks. It's not clear what can be standardized though, as there is a huge amount of possible semantics for such policies. Note also that record management features are explicitly out of scope for CMIS 1.0 (for the very reason that variations between repositories are enormous).TaggingWhile initially it can be seen as very simple concept, tagging is more than just the setting of a multi-valued property (MVP) on a document (à la Dublin Core "subjects"). A complete tagging solution can involve the following features:
For CMIS 1.0 this will be hard to standardize, but there's still some time left for something simple to be proposed by interested parties. TransactionsThe fact that transaction capabilities are not mentioned in the spec was surprising to some. This is due to the fact that too many vendors don't support them. In addition, WS-Transaction can be used to get transactions spanning several requests when using the SOAP bindings, so repositories having them can still expose them in this manner.Events and notificationsHaving a CMIS repository notify the outside world would be very powerful, and has been mentioned as quite useful in the context of user email notification as well as unified search. However CMIS is a protocol-based spec, where a client sends commands to a server and receives answers, so there is no simple way in CMIS 1.0 to expose direct notification capabilities. Registering code that can be executed by the repository on certain events would be useful as well, but again CMIS is a language-neutral spec and cannot standardize this.RESTWhile what we have today with the AtomPub bindings may not be by-the-book REST, we need a simple protocol that can be used by simple tools (and many scripting languages) to do simple access to the repository in a few lines, or even that can be used directly by JavaScript in a browser. The AtomPub bindings are here for this, and many clients can take advantage of them today, although the way some things are exposed may not be perfect (there was consensus on making sure that the bindings are as close as possible to the best practices of AtomPub). It was also noted that, for what it's worth, as a marketing term "REST" now carries a lot of weight and its presence in the spec has already been a significant factor in the adoption or interest (internal or not) in CMIS by various vendors.There was discussion of using WebDAV, which would fit very well with the concept of navigating folders and finding documents, and already has many clients. The reason why this is not in the spec today instead of AtomPub is basically historical, there was no-one in the group to push for WebDAV when the spec was initially created, and it seems that IBM is very pro-AtomPub :) As we all want a CMIS 1.0 soon, AtomPub won't be replaced, but there may be side work going on so that post-1.0 we can find ways for different repositories to expose their CMIS features through WebDAV in a compatible manner. RI/TCKIt was not felt that a Reference Implementation (RI) would bring much, as it is expected that many vendors will have implementations of CMIS very soon, including several open source ones. In any case, it's not the job of an OASIS TC to write software. Regarding a Technology Compatibility Kit (TCK), most people agree that it would be nice to have something, either in an abstract format or as executable test cases. Here I feel that the ball is in the camp of open source vendors; we can easily get together and pool our unit testing resources to turn them into a nice TCK. It won't be a deliverable of the OASIS CMIS TC though, and won't be normative, although conceivably the TC can formally approve a given version.ACLsOf all the points that really merit further work before a 1.0 version can be considered, ACLs ranked highest -- practically everyone agrees that ACLs should be in the spec in some form. However, ACLs are also one of the features that vary most between repositories, so common ground will be hard to find. Nevertheless, ACLs are crucial to some use cases. Unified search was the most frequently mentioned, but many people also have the simple use case of being able to inform a repository that a given document is now readable by Bob.A simple way of being able to express positive ACLs (but not blocking), and to give a hint that a given ACL is inherited or not (whatever the meaning of "inherited"), would be a good step toward interoperability. If ACLs find their way into the spec, it is likely that the separate notion of a Policy will disappear. SearchThe use cases of Federated Search (an engine that, when queried, delegates the search to many repositories and then aggregates the results) and Unified Search (an engine that somehow crawls many repositories to build a database of what's in them, and can then be directly queried) have been discussed a lot, especially unified search as it impacts a number of other features.One feature needed is something allowing the discovery of permissions, to be able to serve search results without having to check with the repository for each document if access can be granted; this will presumably involve some kind of ACLs. Even if such permission discovery does not reflect the full security policy applicable to a document, it can still be useful to weed out some of the documents and improve the efficiency of the search. Another feature needed is something allowing the discovery of what has changed in the repository since a previous crawl; this can be done either through push/events (but as mentioned above this would be out of scope for CMIS 1.0), or through pull/polling/querying to retrieve some kind of journal of the last changes, including deleted documents; this feature is sometimes called an Event Journal or a Transaction Log, and the problem is to make it available efficiently outside the repository for the benefit of search engines. Next stepsFor the TC the coming weeks will be busy, but we hope that very soon a new draft closer to 1.0 will be available to try to resolve some of the issues listed above (and a few others I skipped over). Expect news very soon! And, of course, any feedback to the TC will be reviewed carefully, please submit yours (the cmis-comment mailing-list is listed at the bottom of the CMIS committee page).
05/12/2007
JavaOne is now finished. We
had a very interesting few days there, meeting lots of people interested in
Nuxeo and wanting to know
more about the technology and the business around it. My talk itself was
well received, and I hope it gave people a good sense of what is possible
and desirable in the Open Source
ECM problem space.
You can view the slides of the talk here. I also attended some very interesting sessions regarding subjects that are important when writing an ECM framework: persistence, distribution, scalability, validation, object-relational modeling, etc. A number of efforts are ongoing to standardize as JSRs some important aspects related to this. In the coming days I'll be blogging in more detail about these JSRs that we'll be tracking closely. For our first participation, this was a very fruitful experience, and we'll certainly be going again next year!
05/03/2007
JavaOne 2007 is in a few days
now, and we'll be giving a talk about Nuxeo 5.
The talk has ID TS-4532 (in the Java EE track), and takes place on Thursday the 10th at 5:30 pm in Hall E - 134. The title of the session is Building an Embeddable Enterprise Content Management Core with the Latest Java Technologies. This is a great opportunity if you want to discover more about how Nuxeo 5 is architected, see demos of the product, or talk to us about technical or business-related questions. As a teaser, here's the overview of the talk: Learn about the design and use of Nuxeo 5, an embeddable, extensible Enterprise Content Management framework for Java EE and other platforms Agenda:
See you in San Francisco!
10/18/2006
Nuxeo is switching its ECM to Java, and we're using JCR for our document storage. JCR (Java Content Repository, standardized by JSR-170 and the upcoming JSR-283) is a young specification with a promising future — but what's its point, you may ask, as all existing content management systems are already storing content very well without it? Its goal is interoperability between vendors, which will make it possible for people who write applications needing to store content to have a unified API for such manipulations. All major content repository vendors are active in the JSR-283 expert group, and all are working on JCR bindings for their various proprietary repositories. Of course a standardized and wildly successful way of manipulating content already existed before JCR: SQL. But SQL and JCR have a different focus:
SQL and JCR have quite different underlying data models:
JCR also offers higher level features than SQL, notably workspace and version management. For many kinds of applications, there is a focus on being able to arrange documents in folder hierarchies, and to have a wild variety of structure for these documents. In this case, a storage model based on JCR is much more suited than something based on SQL. It should be noted that many things in the computing world already are based on the notion of folder hierarchies storing arbitrary documents:
This is why JCR has emerged as a common and useful storage API for all these use cases. For an ECM framework like Nuxeo 5, JCR interoperability can be seen from two different directions:
For its initial release, Nuxeo 5 is focusing on the use of JCR as its main storage implementation and uses Jackrabbit to store most of our content. In the future, we'll also provide JCR bindings so that the high-level content we provide can be directly accessed from external applications using JCR too.
Posted by Florent Guillaume @ 10/18/2006 05:40 PM.
-
Categories:
cps,
ecm,
java,
nuxeo5
-
0 comments
In a content management system, the actual data that the system or the users manipulate comes from many kinds of sources. Content can come from a JCR repository, or from a relational database, or from an LDAP directory, or from a semantic storage engine like Jena, or from any other kind of open or proprietary storage engine. But fundamentally all these kinds of content, which I'll call "records", aren't very different:
One of the strengths of CPS is to use a common abstraction for many of these concepts, embodied in the CPSSchemas component. In Nuxeo 5 we want to go further and provide even more integration for all these, the base components for these abstractions are NXCore and NXTypeManager. The reasons to strive for convergence are numerous:
It should be noted that this means that JCR is in no way the primary storage model for Nuxeo 5, it's only the first one to be implemented. In the future, it will be possible to store documents in LDAP or an RDB. When a suitable storage model is devised and implemented, you'll be able to apply workflow or versioning to RDB-based documents for instance. This convergence is quite exciting to us, and our goal is to allow people to build complex applications with Nuxeo 5 in a more straightforward manner.
Posted by Florent Guillaume @ 10/18/2006 12:58 PM.
-
Categories:
cps,
ecm,
java,
nuxeo5
-
0 comments
01/24/2006
GenericSetup is a framework to describe the configuration of a Zope site as a set of XML files (and sometimes other associated files). It can import profiles, which may create objects or change their configuration, and export profiles, which makes a snapshot of the configuration and writes it to a set of XML files. GenericSetup provides a tool that can store snapshots of a configuration in the ZODB itself, where it can be examined and even modified. It can also do diffs between two snapshots, which is very useful to find out what changed in a configuration (it's a good idea to take a full snapshot anytime some significant changes are made to the configuration). GenericSetup differentiates between Base and Extension profiles. A Base profile is a profile that describes "everything". When it is imported, it removes and overwrites any previous configuration. An Extension profile is a profile designed to be incrementally added on top of a previously existing configuration. It may of course overwrite some settings, or even in some case remove objects, but its goal is generally to add optional configuration on top of a main one GenericSetup is based on a small number of concepts: the toolset, and some import and export steps. GenericSetup provides a framework where import and export steps can be written simply using Zope 3 adapters. GenericSetup was born as "CMFSetup" but was later made generic and can be used by any Zope 2 application. It is now available at http://svn.zope.org/GenericSetup/trunk/. A subclass with additional features is used in CPS. Setup toolThe setup tool is called setup_tool by default, and portal_setup in CPS (if missing, it can be instantiated by selecting "CPS Tools" from the ZMI Add menu). At a given time, the setup tool knows about a few things:
When a new profile it selected, its toolset, import steps and export steps XML files are loaded and merged with the tool's current ones. Any import or export is based on the full toolset or import/export steps (even if the currently selected profile is an extension profile), but the source of XML configuration files depends solely on the currently selected profile. ToolsetThe file toolset.xml describes the Toolset, which is the set of tools needed in a given configuration. (While the name "tool" would suggest CMF, it's just a set of objects that can be instantiated at the root of the configured site.) Beyond being based on data that is updated when an extension profile is selected, the toolset is a normal import/export step. When the toolset is imported, all the objects in the toolset are instantiated if they're missing or if their class doesn't match with what it's supposed to be. An excerpt of toolset.xml for the CPS Tree Tool would be:
<?xml version="1.0"?>
<tool-setup>
...
<required tool_id="portal_trees"
class="Products.CPSCore.TreeTool.TreeTool"/>
...
</tool-setup>
Import stepsImport steps (import_steps.xml) describe a set of configuration steps available for import, and their dependencies. A step is just the dotted name of a function, and the dependencies is simply the steps that have to be run before this one can be done (most steps depend on the toolset, because they need a base tool to be instantiated before it can be configured). While an import step can do anything it likes, GenericSetup provides a framework based on Zope 3 adapters to simply describe how a single object is imported from XML, and to recurse among objects to create or configure them one by one. PurgeDuring import, there are two possible behaviors, corresponding to the two kinds of profiles. For a Base profile, the import happens in "purge" mode, while for an extension profile the import doesn't purge. The functions and adapters doing the import have to take that into account when they read a profile. In purge mode, every previous configuration has to be removed, so that the result is indistinguishable from an install from scratch. In non-purge mode, care must be taken to not overwrite or remove settings or objects which are not explicitely specified in the imported XML file. An excerpt of import_steps.xml for the CPS Tree Tool would be:
<?xml version="1.0"?>
<import-steps>
...
<import-step id="trees" version="20051230-01"
handler="Products.CPSCore.exportimport.importTreeTool"
title="Tree Tool">
<dependency step="toolset"/>
Import tree tool and tree caches.
</import-step>
...
</import-steps>
Export stepsExport steps (export_steps.xml) describe the list of steps available for export. An export step is used in exactly the same way than an import step, except for the fact that there are no dependencies between export steps, and that instead of being read, XML files are written. One important thing to note is that export steps cannot do the "incremental exports" that many people expect. When an extension profile is read by the import steps, only the available XML files for that profile are read. However when writing, there's no way to choose which XML files are relevant, so the whole profile for that step is written (and recursion is done in all subobjects). It's possible that future versions of GenericSetup will have some capabilities to do incremental exports, but this is not possible for now. An excerpt of export_steps.xml for the CPS Tree Tool would be:
<?xml version="1.0"?>
<export-steps>
...
<export-step id="trees"
handler="Products.CPSCore.exportimport.exportTreeTool"
title="Tree Tool">
Export tree tool and tree caches.
</export-step>
...
</export-steps>
AdaptersThe standard work done in an import or export step is to find the base tool, and call importObjects or exportObjects on it; these are recursive functions that take each object, find an adapter for them describing how the XML import or export is done, and call it. For the CPS Tree Tool, the import steps and export steps call the following functions:
def exportTreeTool(context):
"""Export Tree tool and tree caches as a set of XML files.
"""
site = context.getSite()
tool = getToolByName(site, 'portal_trees', None)
if tool is None:
logger = context.getLogger('trees')
logger.info("Nothing to export.")
return
exportObjects(tool, '', context)
def importTreeTool(context):
"""Import Tree tool and tree caches as a set of XML files.
"""
site = context.getSite()
tool = getToolByName(site, 'portal_trees')
importObjects(tool, '', context)
This is pretty boileplate and could even be simplified in the future through ZCML declarations. Above, '' simply refers to the root of the profile directory. The adapters are multi-adapters, adapting both an object and an import context (called "environ" in GenericSetup), to the IBody interface that basically describes a file body. They can be registered through ZCML using the standard statement:
<adapter
factory=".exportimport.TreeToolXMLAdapter"
provides="Products.GenericSetup.interfaces.IBody"
for=".interfaces.ITreeTool
Products.GenericSetup.interfaces.ISetupEnviron"
/>
This assumes of course that the exported object is described through an interface, for instance here declared as:
from zope.interface import Interface
class ITreeTool(Interface):
"""Tree Tool.
"""
The interface is implemented by the object's class with:
from zope.interface import implements
class TreeTool(UniqueObject, Folder):
"""Tree Tool that caches information about the site's hierarchies.
"""
implements(ITreeTool)
id = 'portal_trees'
meta_type = 'CPS Tree Tool'
...
When doing an export, the adapters build a DOM tree for the configuration. When doing an import, they read the DOM tree and create or modify properties as needed. The standard adapters don't create subobjects or recurse in them, this is left to the importObjects function. These adapters can be written easily because GenericSetup provides helpers for the common cases of objects configured only through standard Zope 2 properties (PropertyManager), or having subobjects (ObjectManager). Of course all this can be changed for specific cases. Many older CMF tools are configured through things that are not standard properties, and for instance CPS needs to do recursion into more than simple ObjectManager subobjects. It is also possible to read and write files that are not XML files, CPS does this for the images included in portlet objects, where it writes a real image file. Additional CPS feature: upgradesCPS has extended the setup tool to provide a basic Upgrade feature, that is related to the configuration of the site but cannot be expressed by the standard GenericSetup profiles. An upgrade step is registered through ZCML with something like:
<cps:upgradeStep
title="Replace CMF URL tool with a CPS-specific one"
source="3.3.4" destination="3.3.5"
handler=".upgrade.upgradeURLTool"
checker=".upgrade.check_upgradeURLTool"
/>
This describes between what CPS versions this upgrade is needed, how to do it, and how to check if it's already been done. The setup tool lists which steps have not yet been done, and provides a way to run them one by one or all at once. At a given time, a CPS site "knows" (through a site-global property last_upgraded_version) up to which version it's been upgraded. The checker is optional; if it would be too costly to check for the conversion, it can be omitted. This is the case typically for an upgrade step that would recurse in the content object to do some fixups; to check if they've already been done it would have to recurse in the content objects too, and that's too much work for a simple check. The setup tool won't list a step without a checker if the portal has already been upgraded to a later version than the step's destination version.
Posted by Florent Guillaume @ 01/24/2006 08:04 PM.
-
Categories:
cps,
five,
zope,
zope3
-
3 comments
11/10/2005
Here are some explanations about what happens in Zope 3.2 (and Zope 2.9 when using Five) when an event notification is sent by some code, up to a specific subscriber. It focuses more specifically on object events, which go through some additional hoops. All this is complex because there are many simple components that are linked together. Let's start with some framework code that sends an event after an object has been added (similar to what zope.app.container.contained actually does): event = ObjectAddedEvent(ob, container, name) zope.event.notify(event) In zope.event we have the definition for this function:
subscribers = [] # registered subscribers
def notify(event):
for subscriber in subscribers:
subscriber(event)
During initialization, zope.app.event.dispatching has registered a subscriber:
def dispatch(*event):
# Iterating over subscribers assures they get executed.
for ignored in zope.component.subscribers(event, None):
pass
zope.event.subscribers.append(dispatch)
The function zope.component.subscribers will then call all matching subscribers. During initialization, zope/app/event/configure.zcml has registered a subscriber for zope.app.event.interfaces.IObjectEvent with the handler zope.app.event.objectevent.objectEventNotify:
<subscriber
for="zope.app.event.interfaces.IObjectEvent"
handler="zope.app.event.objectevent.objectEventNotify"
/>
This handler does:
def objectEventNotify(event):
adapters = zope.component.subscribers((event.object, event), None)
for adapter in adapters:
pass # Getting them does the work.
This means that the event will be redispatched, but this time a subscriber can match using multi-adaptation on both the object and the event interfaces, which gives much more flexibility and filtering possibilities. During initialization, zope/app/container/configure.zcml has registered a multi-subscriber:
<subscriber
for="zope.app.location.interfaces.ILocation
zope.app.container.interfaces.IObjectMovedEvent"
handler="zope.app.container.contained.dispatchToSublocations"
/>
Note that IContained, a base interface for most content objects, derives from ILocation, so this subscriber will match most content objects. Also, the IObjectAddedEvent sent initially derives from IObjectMovedEvent so it will be matched. When using Five, in Five/event.zcml some similar subscribers are registered to react on IObjectManager instead of ILocation, and to dispatch using the same handler. The dispatchToSublocations handler is:
def dispatchToSublocations(object, event):
subs = ISublocations(object, None)
if subs is not None:
for sub in subs.sublocations():
for ignored in zapi.subscribers((sub, event), None):
pass # They do work in the adapter fetch
This redispatches the same event to all subobjects, where "subobjects" is defined using the ISublocations adapter. Now, zope/app/container/configure.zcml has an adapter:
<adapter
for="zope.app.container.interfaces.IReadContainer"
provides="zope.app.location.interfaces.ISublocations"
factory="zope.app.container.contained.ContainerSublocations"
/>
Most Zope 3 container objects are also IReadContainer. The ContainerSublocations handler does simply get the sublocations using:
class ContainerSublocations(object):
def __init__(self, container):
self.container = container
def sublocations(self):
container = self.container
for key in container:
yield container[key]
When using Five, a similar adapter is registered:
<adapter
for="OFS.interfaces.IObjectManager"
provides="zope.app.location.interfaces.ISublocations"
factory="OFS.subscribers.ObjectManagerSublocations"
/>
And the Five adapter does something comparable to the Zope 3 one:
class ObjectManagerSublocations(object):
def __init__(self, container):
self.container = container
def sublocations(self):
for ob in self.container.objectValues():
yield ob
All in all, our initial event will be redispatched to the sublocations of the original object, and the process will be done recursively to all the sublocations. Now some user code's configure.zcml can registered a multi-subscriber:
<subscriber
for=".interfaces.IFoo
zope.app.container.interfaces.IObjectAddedEvent"
handler=".foo.reactOnAdd"
/>
Which ties to the handler:
def reactOnAdd(ob, event):
"""Does something."""
In the handler, which will be called for all sublocations, ob is any sublocation of the original object (including the object itself), and event is the original event (which means that event.object is the original object). This concludes our dive into through Zope 3 events. You can read in http://blogs.nuxeo.com/sections/blogs/florent_guillaume/2005_11_08_events-in-zope-2-9 how object events can be used in Zope 2.9 to do what used to be done using manage_afterAdd.
11/08/2005
Zope 2.9 (and Zope 2.8 when using Five 1.2) introduces a big change: Zope 3 style container events. With container events, you finally have the ability to react to things happening to objects without have to subclass manage_afterAdd, manage_beforeDelete or manage_afterClone. Instead, you just have to register a subscriber for the appropriate event, for instance IObjectAddedEvent, and make it do the work. Indeed, the old methods like manage_afterAdd are now deprecated, you shouldn't use them anymore. Let's see how to migrate your products. Old productSuppose that in an old product you have code that needs to register through a central tool whenever a document is created. Or it could be indexing itself. Or it could initialize an attribute according to its current path. Code like:
class CoolDocument(...):
...
def manage_afterAdd(self, item, container):
self.mangled_path = mangle('/'.join(self.getPhysicalPath()))
getToolByName(self, 'portal_cool').registerCool(self)
super(CoolDocument, self).manage_afterAdd(item, container)
def manage_afterClone(self, item):
self.mangled_path = mangle('/'.join(self.getPhysicalPath()))
getToolByName(self, 'portal_cool').registerCool(self)
super(CoolDocument, self).manage_afterClone(item)
def manage_beforeDelete(self, item, container):
super(CoolDocument, self).manage_beforeDelete(item, container)
getToolByName(self, 'portal_cool').unregisterCool(self)
This would be the best practice in Zope 2.8. Note the use of super() to call the base class, which is often omitted because people "know" that SimpleItem for instance doesn't do anything in these methods. If you run this code in Zope 2.9, you will get deprecation warnings, telling you that: Calling Products.CoolProduct.CoolDocument.CoolDocument.manage_afterAdd is deprecated when using Five, instead use event subscribers or mark the class with <five:deprecatedManageAddDelete/> Using five:deprecatedManageAddDeleteThe simplest thing you can do to deal with the deprecation warnings, and have correct behavior, is to add in your products a configure.zcml file containing:
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:five="http://namespaces.zope.org/five">
<five:deprecatedManageAddDelete
class="Products.CoolProduct.CoolDocument.CoolDocument"/>
</configure>
This tells Zope that you acknowledge that your class contains deprecated methods, and ask it to still call them in the proper manner. So Zope will be sending events when an object is added, for instance, and in addition call your old manage_afterAdd method. One subtlety here is that you may have to modify you methods to just do their work, and not call their super class. This is necessary because proper events are already dispatched to all relevant classes, and the work of the super class will be done trough events, you must not redo it by hand. If you call the super class, you will get a warning, saying for instance: CoolDocument.manage_afterAdd is deprecated and will be removed in Zope 2.11, you should use an IObjectAddedEvent subscriber instead. The fact that you must "just do your work" is especially important for the rare cases where people subclass the manage_afterAdd of object managers like folders, and decided to reimplement recursion into the children themselves. If you do that, then there will be two recursions going on in parallel, the one done by events, and the one done by your code. This would be bad. Using subscribersIn the long run, and before Zope 2.11 where manage_afterAdd and friends will be removed, you will want to use proper subscribers. First, you'll have to write a subscriber that "does the work", for instance:
def addedCoolDocument(ob, event):
"""A Cool Document was added to a container."""
self.mangled_path = mangle('/'.join(self.getPhysicalPath()))
Note that we're not calling the portal_cool tool anymore, because presumably this tool will also be modified to do its work through events, and will have a similar subscriber doing the necessary registerCool. Note also that here we don't care about the event, but in more complex cases we would. Now we have to register our subscriber for our object. To do that, we need to "mark" our object through an interface. We can define in our product's interfaces.py:
from zope.interface import Interface, Attribute
class ICoolDocument(Interface):
"""Cool Document."""
mangled_path = Attribute("Our mangled path.")
...
Then the class CoolDocument is marked with this interface:
from zope.interface import implements
from Products.CoolProduct.interfaces import ICoolDocument
class CoolDocument(...):
implements(ICoolDocument)
...
Finally we must link the event and the interface to the subscriber using zcml, so in configure.zcml we'll add:
...
<subscriber
for="Products.CoolProduct.interfaces.ICoolDocument
zope.app.container.interfaces.IObjectAddedEvent"
handler="Products.CoolProduct.CoolDocument.addedCoolDocument"
/>
...
And that's it, everything is plugged. Note that IObjectAddedEvent takes care of both manage_afterAdd and manage_afterClone, as it's sent whenever a new object is placed into a container. However this won't take care of moves and renamings, we'll see below how to do that. Event dispatchingWhen an IObjectEvent (from which all the events we're talking here derive) is initially sent, it concerns one object. For instance, a specific object is removed. The event.object attribute is this object. To be able to know about removals, we could just subscribe to the appropriate event using a standard event subscriber. In that case, we'd have to filter "by hand" to check if the object removed is of the type we're interested in, which would be a chore. In addition, any subobjects of the removed object wouldn't know what happens to them, and for instance they wouldn't have any way of doing some cleanup before they disappear. To solve these two problems, Zope 3 has an additional mechanism by which any IObjectEvent is redispatched using multi-adapters of the form (ob, event), so that a subscriber can be specific about the type of object it's interested in. Furthermore, this is done recursively for all sublocations ob of the initial object. The event won't change though, and event.object will still be the original object for which the event was initially sent (this corresponds to self and item in the manage_afterAdd method -- self is ob, and item is event.object). Understanding the hierarchy of events is important to see how to subscribe to them.
There are only a few basic use cases about what one wants to do with respect to events (but you might want to read the full story in Five/tests/event.txt). The first use case is the one where the object has to be aware of its path, like in the CoolDocument example above. That's strictly a Zope 2 concern, as Zope 3 has others ways to deal with this. In Zope 2 an object has a new path through creation, copy or move (rename is a kind of move). The events sent during these three operations are varied: creation sends IObjectAddedEvent, copy sends IObjectCopiedEvent then IObjectAddedEvent, and move sends IObjectMovedEvent. So to react to new paths, we have to subscribe to IObjectMovedEvent, but this will also get us any IObjectRemovedEvent, which we'll have to filter out by hand (this is unfortunate, and due to the way the Zope 3 interface hierarchy is organized). So to fix the CoolDocument configuration we have to add:
def movedCoolDocument(ob, event):
"""A Cool Document was moved."""
if not IObjectRemovedEvent.providedBy(event):
addedCoolDocument(ob, event)
And replace the subscriber with:
...
<subscriber
for="Products.CoolProduct.interfaces.ICoolDocument
zope.app.container.interfaces.IObjectMovedEvent"
handler="Products.CoolProduct.CoolDocument.movedCoolDocument"
/>
...
The second use case is when the object has to do some cleanup when it is removed from its parent. This used to be in manage_beforeDelete, now we can do the work in a removedCoolDocument method and just subscribe to IObjectRemovedEvent. But wait, this won't take into account moves... So in the same vein as above, we would have to write:
def movedCoolDocument(ob, event):
"""A Cool Document was moved."""
if not IObjectRemovedEvent.providedBy(event):
addedCoolDocument(ob, event)
if not IObjectAddedEvent.providedBy(event):
removedCoolDocument(ob, event)
The third use case is when your object has to stay registered with some tool, for instance indexed in a catalog, or as above registered with portal_cool. Here we have to know the old object's path to unregister it, so we have to be called before it is removed. We'll use IObjectWillBe... events, that are sent before the actual operations take place:
from OFS.interfaces import IObjectWillBeAddedEvent
def beforeMoveCoolDocument(ob, event):
"""A Cool Document will be moved."""
if not IObjectWillBeAddedEvent.providedBy(event):
getToolByName(ob, 'portal_cool').unregisterCool(ob)
def movedCoolDocument(ob, event):
"""A Cool Document was moved."""
if not IObjectRemovedEvent.providedBy(event):
getToolByName(ob, 'portal_cool').registerCool(ob)
...
And use an additional subscriber:
...
<subscriber
for="Products.CoolProduct.interfaces.ICoolDocument
OFS.interfaces.IObjectWillBeMovedEvent"
handler="Products.CoolProduct.CoolDocument.beforeMoveCoolDocument"
/>
...
This has to be done if the tool cannot react by itself to objects being added and removed, which obviously would be better as it's ultimately the tool's responsibility and not the object's. Note that if having tests like: if not IObjectWillBeAddedEvent.providedBy(event): if not IObjectRemovedEvent.providedBy(event): seems cumbersome (and backwards), it is also possible to check what kind of event you're dealing with using: if event.oldParent is not None: if event.newParent is not None: (However be careful, the oldParent and newParent are the old and new parents of the original object for which the event was sent, not of the one to which the event was redispatched using the multi-subscribers we have registered.) The IObjectWillBe... events are specific to Zope 2 (and imported from OFS.interfaces). Zope 3 doesn't really need them, as object identity is often enough.
Last modified:
01/25/2005 03:18 PM
|
Nuxeo Bloggers: Log in! Search Nuxeo Blogs
About this blog
Nuxeo Bloggers
Photos and Pictures
|
|
Nuxeo -
Indesko -
Nuxeo 5 Project
All content is copyrighted by their author. CPSSkins is Copyright © 2003-2006 by Jean-Marc Orliaguet. | CPS is Copyright © 2002-2006 by Nuxeo SAS. |