vendor/CMF/1.6-r41367/GenericSetup

view utils.py @ 0:d62b03aaa782

CMF 1.6 r41367 snapshot.
author fguillaume
date Thu, 19 Jan 2006 16:49:24 +0000
parents
children
line source
1 ##############################################################################
2 #
3 # Copyright (c) 2004 Zope Corporation and Contributors. All Rights Reserved.
4 #
5 # This software is subject to the provisions of the Zope Public License,
6 # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
7 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
8 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
9 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
10 # FOR A PARTICULAR PURPOSE.
11 #
12 ##############################################################################
13 """ GenericSetup product utilities
15 $Id: utils.py 40878 2005-12-18 21:57:56Z yuppie $
16 """
18 import os
19 import sys
20 from inspect import getdoc
21 from xml.dom.minidom import _nssplit
22 from xml.dom.minidom import Document
23 from xml.dom.minidom import Element
24 from xml.dom.minidom import Node
25 from xml.dom.minidom import parseString
26 from xml.sax.handler import ContentHandler
27 from xml.parsers.expat import ExpatError
29 import Products
30 from AccessControl import ClassSecurityInfo
31 from Acquisition import Implicit
32 from Globals import InitializeClass
33 from Globals import package_home
34 try:
35 from OFS.interfaces import IOrderedContainer
36 except:
37 #BBB: for Zope 2.8
38 from Products.Five.bbb.OFS_interfaces import IOrderedContainer
39 from cgi import escape
40 from TAL.TALDefs import attrEscape
41 from zope.app import zapi
42 from zope.interface import implements
43 from zope.interface import providedBy
45 from exceptions import BadRequest
46 from interfaces import IBody
47 from interfaces import INode
48 from interfaces import ISetupContext
49 from permissions import ManagePortal
52 _pkgdir = package_home( globals() )
53 _wwwdir = os.path.join( _pkgdir, 'www' )
54 _xmldir = os.path.join( _pkgdir, 'xml' )
56 CONVERTER, DEFAULT, KEY = range(3)
57 I18NURI = 'http://xml.zope.org/namespaces/i18n'
60 def _getDottedName( named ):
62 if isinstance( named, basestring ):
63 return str( named )
65 try:
66 return '%s.%s' % ( named.__module__, named.__name__ )
67 except AttributeError:
68 raise ValueError, 'Cannot compute dotted name: %s' % named
70 def _resolveDottedName( dotted ):
72 __traceback_info__ = dotted
74 parts = dotted.split( '.' )
76 if not parts:
77 raise ValueError, "incomplete dotted name: %s" % dotted
79 parts_copy = parts[:]
81 while parts_copy:
82 try:
83 module = __import__( '.'.join( parts_copy ) )
84 break
86 except ImportError:
87 # Reraise if the import error was caused inside the imported file
88 if sys.exc_info()[2].tb_next is not None: raise
90 del parts_copy[ -1 ]
92 if not parts_copy:
93 raise
95 parts = parts[ 1: ] # Funky semantics of __import__'s return value
97 obj = module
99 for part in parts:
100 obj = getattr( obj, part )
102 return obj
104 def _extractDocstring( func, default_title, default_description ):
106 try:
107 doc = getdoc( func )
108 lines = doc.split( '\n' )
110 except AttributeError:
112 title = default_title
113 description = default_description
115 else:
116 title = lines[ 0 ]
118 if len( lines ) > 1 and lines[ 1 ].strip() == '':
119 del lines[ 1 ]
121 description = '\n'.join( lines[ 1: ] )
123 return title, description
126 class HandlerBase( ContentHandler ):
128 _encoding = None
129 _MARKER = object()
131 def _extract( self, attrs, key, default=None ):
133 result = attrs.get( key, self._MARKER )
135 if result is self._MARKER:
136 return default
138 return self._encode( result )
140 def _extractBoolean( self, attrs, key, default ):
142 result = attrs.get( key, self._MARKER )
144 if result is self._MARKER:
145 return default
147 result = result.lower()
148 return result in ( '1', 'yes', 'true' )
150 def _encode( self, content ):
152 if self._encoding is None:
153 return content
155 return content.encode( self._encoding )
158 class ImportConfiguratorBase(Implicit):
159 """ Synthesize data from XML description.
160 """
161 security = ClassSecurityInfo()
162 security.setDefaultAccess('allow')
164 def __init__(self, site, encoding=None):
166 self._site = site
167 self._encoding = encoding
169 security.declareProtected(ManagePortal, 'parseXML')
170 def parseXML(self, xml):
171 """ Pseudo API.
172 """
173 reader = getattr(xml, 'read', None)
175 if reader is not None:
176 xml = reader()
178 dom = parseString(xml)
179 root = dom.documentElement
181 return self._extractNode(root)
183 def _extractNode(self, node):
185 nodes_map = self._getImportMapping()
186 if node.nodeName not in nodes_map:
187 nodes_map = self._getSharedImportMapping()
188 if node.nodeName not in nodes_map:
189 raise ValueError('Unknown node: %s' % node.nodeName)
190 node_map = nodes_map[node.nodeName]
191 info = {}
193 for name, val in node.attributes.items():
194 key = node_map[name].get( KEY, str(name) )
195 val = self._encoding and val.encode(self._encoding) or val
196 info[key] = val
198 for child in node.childNodes:
199 name = child.nodeName
201 if name == '#comment':
202 continue
204 if not name == '#text':
205 key = node_map[name].get(KEY, str(name) )
206 info[key] = info.setdefault( key, () ) + (
207 self._extractNode(child),)
209 elif '#text' in node_map:
210 key = node_map['#text'].get(KEY, 'value')
211 val = child.nodeValue.lstrip()
212 val = self._encoding and val.encode(self._encoding) or val
213 info[key] = info.setdefault(key, '') + val
215 for k, v in node_map.items():
216 key = v.get(KEY, k)
218 if DEFAULT in v and not key in info:
219 if isinstance( v[DEFAULT], basestring ):
220 info[key] = v[DEFAULT] % info
221 else:
222 info[key] = v[DEFAULT]
224 elif CONVERTER in v and key in info:
225 info[key] = v[CONVERTER]( info[key] )
227 if key is None:
228 info = info[key]
230 return info
232 def _getSharedImportMapping(self):
234 return {
235 'object':
236 { 'i18n:domain': {},
237 'name': {KEY: 'id'},
238 'meta_type': {},
239 'insert-before': {},
240 'insert-after': {},
241 'property': {KEY: 'properties', DEFAULT: ()},
242 'object': {KEY: 'objects', DEFAULT: ()},
243 'xmlns:i18n': {} },
244 'property':
245 { 'name': {KEY: 'id'},
246 '#text': {KEY: 'value', DEFAULT: ''},
247 'element': {KEY: 'elements', DEFAULT: ()},
248 'type': {},
249 'select_variable': {},
250 'i18n:translate': {} },
251 'element':
252 { 'value': {KEY: None} },
253 'description':
254 { '#text': {KEY: None, DEFAULT: ''} } }
256 def _convertToBoolean(self, val):
258 return val.lower() in ('true', 'yes', '1')
260 def _convertToUnique(self, val):
262 assert len(val) == 1
263 return val[0]
265 InitializeClass(ImportConfiguratorBase)
268 class ExportConfiguratorBase(Implicit):
269 """ Synthesize XML description.
270 """
271 security = ClassSecurityInfo()
272 security.setDefaultAccess('allow')
274 def __init__(self, site, encoding=None):
276 self._site = site
277 self._encoding = encoding
278 self._template = self._getExportTemplate()
280 security.declareProtected(ManagePortal, 'generateXML')
281 def generateXML(self, **kw):
282 """ Pseudo API.
283 """
284 return self._template(**kw)
286 InitializeClass(ExportConfiguratorBase)
289 # BBB: old class mixing the two, will be removed in CMF 2.1
290 class ConfiguratorBase(ImportConfiguratorBase, ExportConfiguratorBase):
291 """ Synthesize XML description.
292 """
293 security = ClassSecurityInfo()
294 security.setDefaultAccess('allow')
296 def __init__(self, site, encoding=None):
297 ImportConfiguratorBase.__init__(self, site, encoding)
298 ExportConfiguratorBase.__init__(self, site, encoding)
300 InitializeClass(ConfiguratorBase)
303 class _LineWrapper:
305 def __init__(self, writer, indent, addindent, newl, max):
306 self._writer = writer
307 self._indent = indent
308 self._addindent = addindent
309 self._newl = newl
310 self._max = max
311 self._length = 0
312 self._queue = self._indent
314 def queue(self, text):
315 self._queue += text
317 def write(self, text='', enforce=False):
318 self._queue += text
320 if 0 < self._length > self._max - len(self._queue):
321 self._writer.write(self._newl)
322 self._length = 0
323 self._queue = '%s%s %s' % (self._indent, self._addindent,
324 self._queue)
326 if self._queue != self._indent:
327 self._writer.write(self._queue)
328 self._length += len(self._queue)
329 self._queue = ''
331 if 0 < self._length and enforce:
332 self._writer.write(self._newl)
333 self._length = 0
334 self._queue = self._indent
337 class _Element(Element):
339 """minidom element with 'pretty' XML output.
340 """
342 def writexml(self, writer, indent="", addindent="", newl=""):
343 # indent = current indentation
344 # addindent = indentation to add to higher levels
345 # newl = newline string
346 wrapper = _LineWrapper(writer, indent, addindent, newl, 78)
347 wrapper.write('<%s' % self.tagName)
349 # move 'name', 'meta_type' and 'title' to the top, sort the rest
350 attrs = self._get_attributes()
351 a_names = attrs.keys()
352 a_names.sort()
353 if 'title' in a_names:
354 a_names.remove('title')
355 a_names.insert(0, 'title')
356 if 'meta_type' in a_names:
357 a_names.remove('meta_type')
358 a_names.insert(0, 'meta_type')
359 if 'name' in a_names:
360 a_names.remove('name')
361 a_names.insert(0, 'name')
363 for a_name in a_names:
364 wrapper.write()
365 a_value = attrEscape(attrs[a_name].value)
366 wrapper.queue(' %s="%s"' % (a_name, a_value))
368 if self.childNodes:
369 wrapper.queue('>')
370 for node in self.childNodes:
371 if node.nodeType == Node.TEXT_NODE:
372 data = escape(node.data)
373 textlines = data.splitlines()
374 if textlines:
375 wrapper.queue(textlines.pop(0))
376 if textlines:
377 for textline in textlines:
378 wrapper.write('', True)
379 wrapper.queue('%s%s' % (addindent, textline))
380 else:
381 wrapper.write('', True)
382 node.writexml(writer, indent+addindent, addindent, newl)
383 wrapper.write('</%s>' % self.tagName, True)
384 else:
385 wrapper.write('/>', True)
388 class PrettyDocument(Document):
390 """minidom document with 'pretty' XML output.
391 """
393 def createElement(self, tagName):
394 e = _Element(tagName)
395 e.ownerDocument = self
396 return e
398 def createElementNS(self, namespaceURI, qualifiedName):
399 prefix, localName = _nssplit(qualifiedName)
400 e = _Element(qualifiedName, namespaceURI, prefix)
401 e.ownerDocument = self
402 return e
404 def writexml(self, writer, indent="", addindent="", newl="",
405 encoding = None):
406 if encoding is None:
407 writer.write('<?xml version="1.0"?>\n')
408 else:
409 writer.write('<?xml version="1.0" encoding="%s"?>\n' % encoding)
410 for node in self.childNodes:
411 node.writexml(writer, indent, addindent, newl)
414 class NodeAdapterBase(object):
416 """Node im- and exporter base.
417 """
419 implements(INode)
421 _LOGGER_ID = ''
423 def __init__(self, context, environ):
424 self.context = context
425 self.environ = environ
426 self._logger = environ.getLogger(self._LOGGER_ID)
427 self._doc = PrettyDocument()
429 def _getObjectNode(self, name, i18n=True):
430 node = self._doc.createElement(name)
431 node.setAttribute('name', self.context.getId())
432 node.setAttribute('meta_type', self.context.meta_type)
433 i18n_domain = getattr(self.context, 'i18n_domain', None)
434 if i18n and i18n_domain:
435 node.setAttributeNS(I18NURI, 'i18n:domain', i18n_domain)
436 self._i18n_props = ('title', 'description')
437 return node
439 def _getNodeText(self, node):
440 text = ''
441 for child in node.childNodes:
442 if child.nodeName != '#text':
443 continue
444 lines = [ line.lstrip() for line in child.nodeValue.splitlines() ]
445 text += '\n'.join(lines)
446 return text
448 def _convertToBoolean(self, val):
449 return val.lower() in ('true', 'yes', '1')
452 class BodyAdapterBase(NodeAdapterBase):
454 """Body im- and exporter base.
455 """
457 implements(IBody)
459 def _exportSimpleNode(self):
460 """Export the object as a DOM node.
461 """
462 return self._getObjectNode('object', False)
464 def _importSimpleNode(self, node):
465 """Import the object from the DOM node.
466 """
468 node = property(_exportSimpleNode, _importSimpleNode)
470 def _exportBody(self):
471 """Export the object as a file body.
472 """
473 return ''
475 def _importBody(self, body):
476 """Import the object from the file body.
477 """
479 body = property(_exportBody, _importBody)
481 mime_type = 'text/plain'
483 name = ''
485 suffix = ''
488 class XMLAdapterBase(BodyAdapterBase):
490 """XML im- and exporter base.
491 """
493 implements(IBody)
495 def _exportBody(self):
496 """Export the object as a file body.
497 """
498 self._doc.appendChild(self._exportNode())
499 return self._doc.toprettyxml(' ')
501 def _importBody(self, body):
502 """Import the object from the file body.
503 """
504 try:
505 dom = parseString(body)
506 except ExpatError, e:
507 filename = (self.filename or
508 '/'.join(self.context.getPhysicalPath()))
509 raise ExpatError('%s: %s' % (filename, e))
510 self._importNode(dom.documentElement)
512 body = property(_exportBody, _importBody)
514 mime_type = 'text/xml'
516 name = ''
518 suffix = '.xml'
520 filename = '' # for error reporting during import
522 class ObjectManagerHelpers(object):
524 """ObjectManager im- and export helpers.
525 """
527 def _extractObjects(self):
528 fragment = self._doc.createDocumentFragment()
529 objects = self.context.objectValues()
530 if not IOrderedContainer.providedBy(self.context):
531 objects = list(objects)
532 objects.sort(lambda x,y: cmp(x.getId(), y.getId()))
533 for obj in objects:
534 exporter = zapi.queryMultiAdapter((obj, self.environ), INode)
535 if exporter:
536 fragment.appendChild(exporter.node)
537 return fragment
539 def _purgeObjects(self):
540 for obj_id in self.context.objectIds():
541 self.context._delObject(obj_id)
543 def _initObjects(self, node):
544 for child in node.childNodes:
545 if child.nodeName != 'object':
546 continue
547 if child.hasAttribute('deprecated'):
548 continue
549 parent = self.context
551 obj_id = str(child.getAttribute('name'))
552 if obj_id not in parent.objectIds():
553 meta_type = str(child.getAttribute('meta_type'))
554 __traceback_info__ = obj_id, meta_type
555 for mt_info in Products.meta_types:
556 if mt_info['name'] == meta_type:
557 parent._setObject(obj_id, mt_info['instance'](obj_id))
558 break
559 else:
560 raise ValueError("unknown meta_type '%s'" % meta_type)
562 if child.hasAttribute('insert-before'):
563 insert_before = child.getAttribute('insert-before')
564 if insert_before == '*':
565 parent.moveObjectsToTop(obj_id)
566 else:
567 try:
568 position = parent.getObjectPosition(insert_before)
569 parent.moveObjectToPosition(obj_id, position)
570 except ValueError:
571 pass
572 elif child.hasAttribute('insert-after'):
573 insert_after = child.getAttribute('insert-after')
574 if insert_after == '*':
575 parent.moveObjectsToBottom(obj_id)
576 else:
577 try:
578 position = parent.getObjectPosition(insert_after)
579 parent.moveObjectToPosition(obj_id, position+1)
580 except ValueError:
581 pass
583 obj = getattr(self.context, obj_id)
584 importer = zapi.queryMultiAdapter((obj, self.environ), INode)
585 if importer:
586 importer.node = child
589 class PropertyManagerHelpers(object):
591 """PropertyManager im- and export helpers.
592 """
594 def _extractProperties(self):
595 fragment = self._doc.createDocumentFragment()
597 for prop_map in self.context._propertyMap():
598 prop_id = prop_map['id']
599 if prop_id == 'i18n_domain':
600 continue
602 # Don't export read-only nodes
603 if 'w' not in prop_map.get('mode', 'wd'):
604 continue
606 node = self._doc.createElement('property')
607 node.setAttribute('name', prop_id)
609 prop = self.context.getProperty(prop_id)
610 if isinstance(prop, (tuple, list)):
611 for value in prop:
612 child = self._doc.createElement('element')
613 child.setAttribute('value', value)
614 node.appendChild(child)
615 else:
616 if prop_map.get('type') == 'boolean':
617 prop = str(bool(prop))
618 elif not isinstance(prop, basestring):
619 prop = str(prop)
620 child = self._doc.createTextNode(prop)
621 node.appendChild(child)
623 if 'd' in prop_map.get('mode', 'wd') and not prop_id == 'title':
624 type = prop_map.get('type', 'string')
625 node.setAttribute('type', type)
626 select_variable = prop_map.get('select_variable', None)
627 if select_variable is not None:
628 node.setAttribute('select_variable', select_variable)
630 if hasattr(self, '_i18n_props') and prop_id in self._i18n_props:
631 node.setAttribute('i18n:translate', '')
633 fragment.appendChild(node)
635 return fragment
637 def _purgeProperties(self):
638 for prop_map in self.context._propertyMap():
639 mode = prop_map.get('mode', 'wd')
640 if 'w' not in mode:
641 continue
642 prop_id = prop_map['id']
643 if 'd' in mode and not prop_id == 'title':
644 self.context._delProperty(prop_id)
645 else:
646 prop_type = prop_map.get('type')
647 if prop_type == 'multiple selection':
648 prop_value = ()
649 elif prop_type in ('int', 'float'):
650 prop_value = 0
651 else:
652 prop_value = ''
653 self.context._updateProperty(prop_id, prop_value)
655 def _initProperties(self, node):
656 self.context.i18n_domain = node.getAttribute('i18n:domain')
657 for child in node.childNodes:
658 if child.nodeName != 'property':
659 continue
660 obj = self.context
661 prop_id = str(child.getAttribute('name'))
662 prop_map = obj.propdict().get(prop_id, None)
664 if prop_map is None:
665 if child.hasAttribute('type'):
666 val = child.getAttribute('select_variable')
667 obj._setProperty(prop_id, val, child.getAttribute('type'))
668 prop_map = obj.propdict().get(prop_id, None)
669 else:
670 raise ValueError("undefined property '%s'" % prop_id)
672 if not 'w' in prop_map.get('mode', 'wd'):
673 raise BadRequest('%s cannot be changed' % prop_id)
675 elements = []
676 for sub in child.childNodes:
677 if sub.nodeName == 'element':
678 elements.append(sub.getAttribute('value').encode('utf-8'))
680 if elements or prop_map.get('type') == 'multiple selection':
681 prop_value = tuple(elements) or ()
682 elif prop_map.get('type') == 'boolean':
683 prop_value = self._convertToBoolean(self._getNodeText(child))
684 else:
685 # if we pass a *string* to _updateProperty, all other values
686 # are converted to the right type
687 prop_value = self._getNodeText(child).encode('utf-8')
689 if not self._convertToBoolean(child.getAttribute('purge')
690 or 'True'):
691 # If the purge attribute is False, merge sequences
692 prop = obj.getProperty(prop_id)
693 if isinstance(prop, (tuple, list)):
694 prop_value = (tuple([p for p in prop
695 if p not in prop_value]) +
696 tuple(prop_value))
698 obj._updateProperty(prop_id, prop_value)
701 def exportObjects(obj, parent_path, context):
702 """ Export subobjects recursively.
703 """
704 exporter = zapi.queryMultiAdapter((obj, context), IBody)
705 path = '%s%s' % (parent_path, obj.getId().replace(' ', '_'))
706 if exporter:
707 if exporter.name:
708 path = '%s%s' % (parent_path, exporter.name)
709 filename = '%s%s' % (path, exporter.suffix)
710 body = exporter.body
711 if body is not None:
712 context.writeDataFile(filename, body, exporter.mime_type)
714 if getattr(obj, 'objectValues', False):
715 for sub in obj.objectValues():
716 exportObjects(sub, path+'/', context)
718 def importObjects(obj, parent_path, context):
719 """ Import subobjects recursively.
720 """
721 importer = zapi.queryMultiAdapter((obj, context), IBody)
722 path = '%s%s' % (parent_path, obj.getId().replace(' ', '_'))
723 __traceback_info__ = path
724 if importer:
725 if importer.name:
726 path = '%s%s' % (parent_path, importer.name)
727 filename = '%s%s' % (path, importer.suffix)
728 body = context.readDataFile(filename)
729 if body is None and filename == 'types.xml':
730 # BBB: for CMF 1.5 profiles
731 body = context.readDataFile('typestool.xml')
732 if body is not None:
733 importer.filename = filename # for error reporting
734 importer.body = body
736 if getattr(obj, 'objectValues', False):
737 for sub in obj.objectValues():
738 importObjects(sub, path+'/', context)