Back to index

plone3  3.1.7
atns.py
Go to the documentation of this file.
00001 ##################################################################
00002 # Marshall: A framework for pluggable marshalling policies
00003 # Copyright (C) 2004 EnfoldSystems, LLC
00004 # Copyright (C) 2004 ObjectRealms, LLC
00005 #
00006 # This program is free software; you can redistribute it and/or modify
00007 # it under the terms of the GNU General Public License as published by
00008 # the Free Software Foundation; either version 2 of the License, or
00009 # (at your option) any later version.
00010 #
00011 # This program is distributed in the hope that it will be useful,
00012 # but WITHOUT ANY WARRANTY; without even the implied warranty of
00013 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00014 # GNU General Public License for more details.
00015 #
00016 # You should have received a copy of the GNU General Public License
00017 # along with this program; if not, write to the Free Software
00018 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
00019 ##################################################################
00020 
00021 """
00022 Serialize AT Schema Attributes 
00023 
00024 Created: 10/11/2004
00025 Authors: Kapil Thangavelu <k_vertigo@objectrealms.net>
00026          Sidnei De Silva <sidnei@awkly.org>
00027 
00028 $Id: $
00029 """
00030 
00031 from sets import Set
00032 
00033 from Products.CMFCore.utils import getToolByName
00034 from Products.Archetypes import config as atcfg
00035 from Products.Archetypes.debug import log
00036 from Products.Archetypes import public as atapi
00037 from Products.Marshall import config
00038 from Products.Marshall.handlers.atxml import XmlNamespace
00039 from Products.Marshall.handlers.atxml import SchemaAttribute
00040 from Products.Marshall.handlers.atxml import getRegisteredNamespaces
00041 from Products.Marshall.exceptions import MarshallingException
00042 from Products.Marshall import utils
00043 
00044 import transaction
00045 
00046 _marker = object()
00047 
00048 class BoundReference(object):
00049     
00050     __slots__ = ('ns_data', 'attribute', 'instance')
00051     
00052     def __init__(self, ns_data, attribute, instance ):
00053         self.ns_data = ns_data
00054         self.attribute = attribute
00055         self.instance = instance
00056     
00057     def resolve(self, context):
00058         self.attribute.deserialize( self.instance, self.ns_data )
00059 
00060 
00061 class ATAttribute(SchemaAttribute):
00062     
00063     def get(self, instance):
00064         values = atapi.BaseObject.__getitem__(instance, self.name)
00065         if not isinstance( values, ( list, tuple ) ):
00066             values = [values]
00067         return filter(None, values)
00068 
00069     def serialize(self, dom, parent_node, instance, options={}):
00070         
00071         values = self.get( instance )
00072         if not values:
00073             return
00074 
00075         is_ref = self.isReference( instance )
00076         
00077         for value in values:
00078             node = dom.createElementNS( self.namespace.xmlns, "field")
00079             name_attr = dom.createAttribute("name")
00080             name_attr.value = self.name
00081             node.setAttributeNode( name_attr )
00082             
00083             if is_ref:
00084                 if config.HANDLE_REFS:
00085                     ref_node = dom.createElementNS( self.namespace.xmlns,
00086                                                     'reference' )
00087                     uid_node = dom.createElementNS( self.namespace.xmlns,
00088                                                     'uid' )
00089                     value = response.createTextNode( str( value ) )
00090                     uid_node.append( value )
00091                     ref_node.append( uid_node )
00092                     node.append( ref_node )
00093             else:
00094                 value_node = dom.createTextNode( str( value ) )
00095                 node.appendChild( value_node )
00096         
00097             node.normalize()
00098             parent_node.appendChild( node )
00099 
00100         return True
00101 
00102     def processXmlValue(self, context, value ):
00103         value = value.strip()
00104         if not value:
00105             return
00106         data = context.getDataFor( self.namespace.xmlns )
00107         if data.has_key( self.name ):
00108             svalues = data[self.name]
00109             if not isinstance( svalues, list):
00110                 data[self.name] = svalues = [ svalues ]
00111             svalues.append( value )
00112             return
00113         else:
00114             data[self.name] = value
00115         
00116     def deserialize(self, instance, ns_data, options={}):
00117         values = ns_data.get( self.name )
00118         if not values:
00119             return
00120 
00121        # check if we are a schema attribute
00122         if self.isReference( instance ):
00123             values = self.resolveReferences( instance, values)
00124             if not config.HANDLE_REFS :
00125                 return
00126 
00127         mutator = instance.Schema()[self.name].getMutator(instance)
00128         if not mutator:
00129             # read only field no mutator, but try to set value still
00130             # since it might reflect object state (like ATCriteria)
00131             field = instance.getField( self.name ).set( instance, values )
00132             #raise AttributeError("No Mutator for %s"%self.name)
00133             return
00134         
00135         if self.name == "id":
00136             transaction.savepoint()
00137         mutator(values)
00138 
00139     def resolveReferences(self, instance, values):
00140         ref_values = []
00141         for value in values:
00142             if not isinstance( value, Reference):
00143                 ref_values.append( value )
00144                 continue
00145             ref = value.resolve( instance )
00146             if ref is None: # just for dup behavior
00147                 raise MarshallingException(
00148                     "Could not resolve reference %r"%value
00149                     )
00150             ref_values.append( ref )
00151         return ref_values
00152         
00153     def isReference(self, instance):
00154         return not not isinstance(instance.Schema()[self.name],
00155                                   atapi.ReferenceField)
00156 
00157 class ReferenceAttribute(SchemaAttribute):
00158 
00159     __slots__ = ('reference', 'name')
00160 
00161     def __init__(self, name, reference):
00162         super(ReferenceAttribute, self).__init__(name)
00163         self.reference = reference
00164         
00165     def processXml(self, context, node):
00166         return True
00167 
00168     def processXmlValue(self, context, value):
00169         self.reference[self.name]=value.strip()
00170 
00171 class ArchetypeUID(SchemaAttribute):
00172 
00173     def serialize(self, dom, parent_node, instance, options={}):
00174         value = getattr( instance, atcfg.UUID_ATTR, "")
00175         node = dom.createElementNS( Archetypes.xmlns, "uid")
00176         nvalue = dom.createTextNode( value )
00177         node.appendChild( nvalue )
00178         parent_node.appendChild( node )
00179 
00180     def deserialize(self, instance, ns_data):
00181         values = ns_data.get( self.name )
00182         if not values:
00183             return        
00184         self.resolveUID( instance, values )
00185 
00186     def resolveUID(self, instance, values):
00187         assert not isinstance(values, (list, tuple))
00188         at_uid = values
00189         existing = getattr(instance, atcfg.UUID_ATTR, _marker)
00190         if existing is _marker or existing != at_uid:
00191             ref = Reference(uid=at_uid)
00192             target = ref.resolve(instance)
00193             if target is not None:
00194                 raise MarshallingException, (
00195                         "Trying to set uid of "
00196                         "%s to an already existing uid "
00197                         "clashed with %s" % (
00198                         instance.absolute_url(), target.absolute_url()))
00199             instance._setUID(at_uid)
00200 
00201 
00202 RNGSchemaFragment = '''
00203   <define name="ArchetypesFields"
00204           ns="http://plone.org/ns/archetypes/"
00205           xmlns:note="atxml:annotations"
00206           datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes"
00207           xmlns="http://relaxng.org/ns/structure/1.0">
00208     <element name="field">
00209       <note:info>
00210         All non-standard Archetypes fields are represented by a 'field'
00211         element, their id specified by an attribute 'id'.
00212       </note:info>
00213       <attribute name="id" />
00214       <choice>
00215         <text />
00216         <zeroOrMore>
00217           <element name="reference">
00218             <note:info>
00219               References can be made by UID (Archetypes),
00220               relative path, or by specifying a group of values
00221               that can uniquely identify a piece of content.
00222             </note:info>
00223             <choice>
00224               <zeroOrMore>
00225                 <element name="uid"><text /></element>
00226               </zeroOrMore>
00227               <zeroOrMore>
00228                 <element name="path"><text /></element>
00229               </zeroOrMore>
00230               <zeroOrMore>
00231                 <ref name="Metadata" />
00232               </zeroOrMore>
00233             </choice>
00234           </element>
00235         </zeroOrMore>
00236       </choice>
00237     </element>
00238   </define>
00239   '''
00240 
00241 class Archetypes(XmlNamespace):
00242 
00243     xmlns = config.AT_NS
00244     prefix = None
00245     attributes = []
00246         
00247     def __init__(self):
00248         super(Archetypes, self).__init__()
00249         self.last_schema_id = None
00250         self.in_reference_mode = False
00251         self.new_reference_p = True
00252 
00253         uid_attribute  = ArchetypeUID('uid')
00254         uid_attribute.setNamespace( self )
00255         
00256         self.at_fields = {'uid' : uid_attribute}
00257 
00258     def getAttributeByName(self, schema_name, context=None):
00259         if context is not None and schema_name not in self.at_fields:
00260             if not context.instance.Schema().has_key( schema_name ):
00261                 return
00262                 raise AssertionError, \
00263                       "invalid attribute %s"%(schema_name)
00264         
00265         if schema_name in self.at_fields:
00266             return self.at_fields[ schema_name ]
00267 
00268         attribute = ATAttribute( schema_name )
00269         attribute.setNamespace( self )
00270         
00271         return attribute
00272 
00273     def getAttributes(self, instance, exclude_attrs=()):
00274 
00275         # remove fields delegated to other namespaces
00276         fields = []
00277         for ns in getRegisteredNamespaces():
00278             if ns.uses_at_fields:
00279                 fields.extend( ns.getATFields() )
00280         assert len(Set(fields)) == len(fields), (
00281             "Multiple NS multiplexing field")
00282 
00283         field_keys = [k for k in instance.Schema().keys()
00284                       if k not in exclude_attrs and k not in fields]
00285         #Set(instance.Schema().keys())-mset
00286 
00287         # remove primary field if still present
00288 ## XXX: we dont want to remove the PF, but want to be backward compatible (how to do that best?)        
00289 ##        p = instance.getPrimaryField()
00290 ##        pk = p and p.getName() or None
00291 ##        if pk and pk in field_keys:
00292 ##            field_keys.remove( pk )
00293             
00294         for fk in field_keys:
00295             yield self.getAttributeByName( fk )
00296 
00297         # yield additional intrinsic at framework attrs
00298         for attribute in self.at_fields.values():
00299             yield attribute
00300 
00301     def serialize(self, dom, parent_node, instance, options ):
00302         
00303         exclude_attrs = options.get('atns_exclude', () )
00304             
00305         for attribute in self.getAttributes( instance, exclude_attrs):
00306             if hasattr(attribute, 'isReference') and attribute.isReference( instance ):
00307                 continue
00308             attribute.serialize( dom, parent_node, instance, options )
00309 
00310     def deserialize(self, instance, ns_data, options):
00311         if not ns_data:
00312             return
00313             
00314         for attribute in self.getAttributes( instance ):
00315             if not config.HANDLE_REFS and hasattr(attribute, 'isReference') and attribute.isReference( instance ):
00316                 # simply skip it then... Gogo
00317                 continue
00318             attribute.deserialize( instance, ns_data )
00319 
00320     def processXml(self, context, data_node):
00321 
00322         tagname, namespace = utils.fixtag(data_node.tag, context.ns_map)
00323         
00324         if tagname == 'metadata':
00325             # ignore the container
00326             return False
00327 
00328         elif tagname == 'reference':
00329             # switch to reference mode, we tell the parser that we want
00330             # to explictly recieve all new node parse events, so we
00331             # can introspect the nested metadata that can be used
00332             # in reference specification.
00333             self.in_reference_mode = True
00334             self.new_reference_p = True
00335             assert self.last_schema_id
00336             context.setNamespaceDelegate( self )
00337             return False
00338 
00339         elif tagname == 'field':
00340             # basic at field specified, find the matching attribute
00341             # and annotate the data node with it
00342             schema_name = data_node.attrib.get('name', None)
00343             if schema_name is None:
00344                 log("'id' attribute for at:field is deprecated, use 'name' instead")
00345                 schema_name = data_node.attrib.get('id')
00346 ##            while context.reader.MoveToNextAttribute():
00347 ##                if context.reader.LocalName() == 'id':
00348 ##                    schema_name = context.reader.Value()
00349 ##                    break
00350             assert schema_name, "No field name specified in at:field element"
00351             #print "field", schema_name
00352             self.last_schema_id = schema_name
00353             attribute = self.getAttributeByName(schema_name, context)
00354             if attribute is None:
00355                 #print "na", schema_name
00356                 return False
00357             data_node.attribute = attribute
00358             return True
00359         
00360         elif self.in_reference_mode:
00361             # if we get new metadata elements while in references, they
00362             # are stored as additional data for resolving the reference
00363             # latter.
00364             data = context.getDataFor(self.xmlns)
00365             srefs = data.setdefault( self.last_schema_id, [])
00366             
00367             # if we've already added a reference to the node data,
00368             # put additional reference specification data onto the
00369             # existing reference.
00370             if self.new_reference_p:
00371                 ref = Reference()
00372                 srefs.append( ref )
00373                 self.new_reference_p = False
00374             else:
00375                 ref = srefs[-1]
00376                 
00377             attribute = ReferenceAttribute( data_node.name, ref )
00378             data_node.attribute = attribute
00379             return True
00380 
00381         elif tagname in self.at_fields:
00382             # pseudo fields like uid which are specified in a custom manner
00383             attribute = self.getAttributeByName( tagname )
00384             if attribute is None:
00385                 return False
00386             data_node.attribute = attribute
00387             return True
00388 
00389         return False
00390 
00391     def processXmlEnd(self, name, context):
00392         if name == 'reference':
00393             context.setNamespaceDelegate( None )
00394             self.in_reference_mode = False
00395             self.last_schema_id = None # guard against bad xml
00396 
00397     def getSchemaInfo( self ):
00398         return [ ("ArchetypesFields", "zeroOrMore", RNGSchemaFragment) ]
00399 
00400 class Reference(dict):
00401 
00402     index_map = dict([('title', 'Title'),
00403                       ('description', 'Description'),
00404                       ('creation_date', 'created'),
00405                       ('modification_data', 'modified'),
00406                       ('creators', 'Creator'),
00407                       ('subject', 'Subject'),
00408                       ('effectiveDate', 'effective'),
00409                       ('expirationDate', 'expires'),
00410                       ])
00411 
00412     def resolve(self, context):
00413         uid = self.get('uid')
00414         if uid is not None:
00415             rt = getToolByName(context, atcfg.REFERENCE_CATALOG)
00416             return rt.lookupObject(uid)
00417         path = self.get('path')
00418         if path is not None:
00419             return context.restrictedTraverse(path, None)
00420         catalog = getToolByName(context, 'portal_catalog')
00421         params = [(k, v) for k, v in self.items() \
00422                   if k not in ('uid', 'path')]
00423         kw = [(self.index_map.get(k), v) for k, v in params]
00424         kw = dict(filter(lambda x: x[0] is not None and x, kw))
00425         res = catalog(**kw)
00426         if not res:
00427             return None
00428 
00429         # First step: Try to filter by brain metadata
00430         # *Usually* a metadata item will exist with the same name
00431         # as the index.
00432         verify = lambda obj: filter(None, [obj[k] == v for k, v in kw.items()])
00433         for r in res:
00434             # Shortest path: If a match is found, return immediately
00435             # instead of checking all of the results.
00436             if verify(r):
00437                 return r.getObject()
00438 
00439         # Second step: Try to get the real objects and look
00440         # into them. Should be *very* slow, so use with care.
00441         # We use __getitem__ to access the field raw data.
00442         verify = lambda obj: filter(None, [obj[k] == v for k, v in params])
00443         valid = filter(verify, [r.getObject() for r in res])
00444         if not valid:
00445             return None
00446         if len(valid) > 1:
00447             raise MarshallingException, ('Metadata reference does not '
00448                                          'uniquely identifies the reference.')
00449         return valid[0]