profile_generation.py 70.4 KB
Newer Older
1
# copyright 2016-2021 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# contact http://www.logilab.fr -- mailto:contact@logilab.fr
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
Sylvain Thénault's avatar
Sylvain Thénault committed
16
"""cubicweb-seda adapter classes for profile (schema) generation"""
17

18
from collections import defaultdict, namedtuple
19
from functools import partial
20
from itertools import chain
21

22
from lxml import etree
23
from pyxst.xml_struct import graph_nodes
24

25
26
from logilab.common import attrdict

27
28
from yams import BASE_TYPES

29
30
31
from cubicweb.predicates import is_instance
from cubicweb.view import EntityAdapter

32
33
34
from ..xsd import XSDM_MAPPING, JUMP_ELEMENTS
from ..xsd2yams import SKIP_ATTRS
from . import simplified_profile
35

36

37
JUMPED_OPTIONAL_ELEMENTS = set(('DataObjectPackage', 'FileInfo', 'PhysicalDimensions', 'Coverage'))
38
39


40
41
42
43
44
45
46
47
def substitute_xml_prefix(prefix_name, namespaces):
    """Given an XML prefixed name in the form `'ns:name'`, return the string `'{<ns_uri>}name'`
    where `<ns_uri>` is the URI for the namespace prefix found in `namespaces`.

    This new string is then suitable to build an LXML etree.Element object.

    Example::

48
49
      >>> substitude_xml_prefix('xlink:href', {'xlink': 'http://wwww.w3.org/1999/xlink'})
      '{http://www.w3.org/1999/xlink}href'
50
51
52
53
54
55
56
57
58
59

    """
    try:
        prefix, name = prefix_name.split(':', 1)
    except ValueError:
        return prefix_name
    assert prefix in namespaces, 'Unknown namespace prefix: {0}'.format(prefix)
    return '{{{0}}}'.format(namespaces[prefix]) + name


60
61
62
63
64
65
66
67
def content_types(content_type):
    """Return an ordered tuple of content types from pyxst `textual_content_type` that may be None, a
    set or a string value.
    """
    if content_type:
        if isinstance(content_type, set):
            content_types = sorted(content_type)
        else:
68
69
            if content_type == 'IDREF':
                content_type = 'NCName'
70
71
72
73
74
75
            content_types = (content_type,)
    else:
        content_types = ()
    return content_types


76
77
78
79
80
81
def _internal_reference(value):
    """Return True if the given value is a reference to an entity within the profile."""
    return getattr(value, 'cw_etype', None) in ('SEDAArchiveUnit',
                                                'SEDABinaryDataObject', 'SEDAPhysicalDataObject')


82
83
84
85
86
87
88
89
90
91
92
def _concept_value(concept, language):
    """Return string value to be inserted in a SEDA export for the given concept.

    * `concept` may be None, in which case None will be returned

    * `language` is the language matching the exported format (one of 'seda-2', 'seda-1' or
      'seda-02')
    """
    assert language in ('seda-2', 'seda-1', 'seda-02')
    if concept is None:
        return None
93
    for code in (language, 'seda', 'en', 'fr'):
94
95
96
97
        try:
            return concept.labels[code]
        except KeyError:
            continue
98
    return concept.label()
99
100


101
102
103
def xmlid(entity):
    """Return a value usable as ID/IDREF for the given entity."""
    return entity.cw_adapt_to('IXmlId').id()
104
105


106
def serialize(value, build_url):
107
108
109
110
111
    """Return typed `value` as an XSD string."""
    if value is None:
        return None
    if hasattr(value, 'eid'):
        if value.cw_etype == 'ConceptScheme':
112
            return build_url(value)
113
        if value.cw_etype == 'Concept':
114
            return _concept_value(value, 'seda-2')
115
        if _internal_reference(value):
116
            return xmlid(value)
117
118
119
        return None  # intermediary entity
    if isinstance(value, bool):
        return 'true' if value else 'false'
Noé Gaumont's avatar
Noé Gaumont committed
120
121
    assert isinstance(value, str), repr(value)
    return value
122
123


124
125
126
127
128
129
130
131
132
133
def integrity_cardinality(data_object):
    minvalue, maxvalue = minmax_cardinality(data_object.user_cardinality)
    itree = data_object.cw_adapt_to('ITreeBase')
    for parent in itree.iterancestors():
        try:
            parent_cardinality = parent.user_cardinality
        except AttributeError:
            continue
        minc, maxc = minmax_cardinality(parent_cardinality)
        minvalue = min(minc, minvalue)
134
        if maxc == graph_nodes.INFINITY or maxvalue == graph_nodes.INFINITY:
135
            maxvalue = graph_nodes.INFINITY
136
137
        else:
            maxvalue = max(maxc, maxvalue)
138
139
    if maxvalue == graph_nodes.INFINITY:
        maxvalue = 'n'
140
141
142
143
144
    if minvalue == maxvalue == 1:
        return '1'
    return '{}..{}'.format(minvalue, maxvalue)


145
146
147
148
def minmax_cardinality(string_cardinality, _allowed=('0..1', '0..n', '1', '1..n')):
    """Return (minimum, maximum) cardinality for the cardinality as string (one of '0..1', '0..n',
    '1' or '1..n').
    """
149
    assert string_cardinality in _allowed, '%s not allowed %s' % (string_cardinality, _allowed)
150
151
152
153
154
155
156
157
158
159
160
    if string_cardinality[0] == '0':
        minimum = 0
    else:
        minimum = 1
    if string_cardinality[-1] == 'n':
        maximum = graph_nodes.INFINITY
    else:
        maximum = 1
    return minimum, maximum


161
def element_minmax_cardinality(occ, card_entity):
162
163
164
    """Return (minimum, maximum) cardinality for the given pyxst Occurence and entity.

    Occurence 's cardinality may be overriden by the entity's user_cardinality value.
165
166
167
    """
    cardinality = getattr(card_entity, 'user_cardinality', None)
    if cardinality is None:
168
        return occ.minimum, occ.maximum
169
    else:
170
        return minmax_cardinality(cardinality)
171
172
173
174
175
176
177
178


def attribute_minimum_cardinality(occ, card_entity):
    """Return 0 or 1 for the given pyxst attribute's Occurence. Cardinality may be overriden by
    the data model's user_cardinality value.
    """
    cardinality = getattr(card_entity, 'user_cardinality', None)
    if cardinality is None:
179
        return occ.minimum
180
    else:
181
        return minmax_cardinality(cardinality, ('0..1', '1'))[0]
182
183


184
185
186
187
188
189
190
191
192
193
194
195
def iter_path_children(xselement, entity):
    """Return an iterator on `entity` children entities according to `xselement` definition.

    (`path`, `target`) is returned with `path` the path definition leading to the target, and
    `target` either a final value in case of attributes or a list of entities.
    """
    for rtype, role, _path in XSDM_MAPPING.iter_rtype_role(xselement.local_name):
        if _path[0][2] in BASE_TYPES:
            # entity attribute
            if getattr(entity, rtype) is not None:
                yield _path, getattr(entity, rtype)
        else:
196
            related = entity.related(rtype, role, entities=True)
197
198
199
200
            if related:
                yield _path, related


201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
class RNGMixin(object):
    """Mixin class providing some Relax NG schema generation helper methods."""

    def rng_element_parent(self, parent, minimum, maximum=1):
        """Given a etree node and minimum/maximum cardinalities of a desired child element,
        return suitable parent node for it.

        This will be one of rng:optional, rng:zeroOrMore or rng:oneOrMore that will be created by
        this method or the given parent itself if minimum == maximum == 1.
        """
        if minimum == 1 and maximum == 1:
            return parent
        elif minimum == 0 and maximum == 1:
            return self.element('rng:optional', parent)
        elif minimum == 0 and maximum == graph_nodes.INFINITY:
            return self.element('rng:zeroOrMore', parent)
        elif minimum == 1 and maximum == graph_nodes.INFINITY:
            return self.element('rng:oneOrMore', parent)
        else:
            assert False, ('unexpected min/max cardinality:', minimum, maximum)

    def rng_attribute_parent(self, parent, minimum):
        """Given a etree node and minimum cardinality of a desired attribute,
        return suitable parent node for it.

        This will be rng:optional that will be created by this method or the given parent itself if
        minimum == 1.
        """
        if minimum == 1:
            return parent
        else:
            return self.element('rng:optional', parent)

234
    def rng_value(self, element, qualified_datatype, fixed_value=None, default_value=None):
235
236
237
238
239
240
        """Given a (etree) schema element, a data type (e.g. 'xsd:token') and an optional fixed
        value, add RNG declaration to the element to declare the datatype and fix the value if
        necessary.
        """
        prefix, datatype = qualified_datatype.split(':')
        if prefix != 'xsd':
241
242
243
244
245
246
            # XXX RelaxNG compatible version of custom types? this would allow
            # `type_attrs['datatypeLibrary'] = self.namespaces[prefix]`. In the mean time, turn
            # every custom type to string, supposing transfer are also checked against the original
            # schema (as agape v1 was doing).
            datatype = 'string'
        type_attrs = {'type': datatype}
247
        if fixed_value is not None:
248
            if isinstance(fixed_value, (tuple, list)):
249
250
251
252
253
                choice = self.element('rng:choice', element)
                for value in fixed_value:
                    self.element('rng:value', choice, type_attrs, text=value)
            else:
                self.element('rng:value', element, type_attrs, text=fixed_value)
254
255
256
        elif default_value is not None:
            element.attrib[self.qname('a:defaultValue')] = default_value
            self.element('rng:data', element, type_attrs)
257
258
259
260
        else:
            self.element('rng:data', element, type_attrs)


261
class SEDA2ExportAdapter(EntityAdapter):
Sylvain Thénault's avatar
Sylvain Thénault committed
262
    """Abstract base class for export of SEDA profile."""
263
    __abstract__ = True
264
    __select__ = is_instance('SEDAArchiveTransfer')
265
266
    encoding = 'utf-8'
    content_type = 'application/xml'
267
268
    # to be defined in concret implementations
    namespaces = {}
269
270
271
272
273
274
275
276
277
278
279
    _root_attributes = {}

    @property
    def root_attributes(self):
        if self.entity.compat_list is None:
            # uncommited transfer may occurs during tests
            return self._root_attributes
        diag = '' if 'RNG' in self.entity.compat_list else 'rng-ambiguous'
        attributes = {'seda:warnings': diag}
        attributes.update(self._root_attributes)
        return attributes
280

281
282
283
284
285
286
    def dump(self, _encoding=None):
        """Return an schema string for the adapted SEDA profile

        _encoding will be used as "encoding" argument of lxml's tostring, in
        order to retrieve a unicode string. This is useful for tests.
        """
287
        root = self.dump_etree()
288
289
290
291
292
293
        kwargs = {}
        if _encoding is None:
            # We only want the XML declaration at all if _encoding is not specified.
            kwargs['standalone'] = False
        kwargs['encoding'] = _encoding or self.encoding
        return etree.tostring(root, pretty_print=True, **kwargs)
294
295
296
297
298
299
300
301
302
303
304
305

    def dump_etree(self):
        """Return an XSD etree for the adapted SEDA profile."""
        raise NotImplementedError()

    def qname(self, tag):
        return substitute_xml_prefix(tag, self.namespaces)

    def element(self, tag, parent=None, attributes=None, text=None):
        """Generic method to build a XSD element tag.

        Params:
306

Sylvain Thénault's avatar
Sylvain Thénault committed
307
        * `tag`, tag name of the element
308
309
310

        * `parent`, the parent etree node

Sylvain Thénault's avatar
Sylvain Thénault committed
311
312
313
314
        * `attributes`, dictionary of attributes - may contain a special 'documentation' attribute
          that will be added in a xsd:annotation node

        * `text`, textual content of the tag if any
315
316
317
318
        """
        attributes = attributes or {}
        tag = self.qname(tag)
        documentation = attributes.pop('documentation', None)
319
        for attr, value in list(attributes.items()):
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
            newattr = substitute_xml_prefix(attr, self.namespaces)
            attributes[newattr] = value
            if newattr != attr:
                attributes.pop(attr)
        if parent is None:
            elt = etree.Element(tag, attributes, nsmap=self.namespaces)
        else:
            elt = etree.SubElement(parent, tag, attributes)
        if text is not None:
            elt.text = text
        if documentation:
            annot = self.element('xsd:annotation', elt)
            self.element('xsd:documentation', annot).text = documentation
        return elt

    def dispatch_occ(self, profile_element, occ, target_value, to_process, card_entity):
        callback = getattr(self, 'element_' + occ.target.__class__.__name__.lower())
        callback(occ, profile_element, target_value, to_process, card_entity)

339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
    def _dump(self, root):
        entity = self.entity
        xselement = XSDM_MAPPING.root_xselement
        transfer_element = self.init_transfer_element(xselement, root, entity)
        to_process = defaultdict(list)
        to_process[xselement].append((entity, transfer_element))
        # first round to ensure we have necessary basic structure
        for xselement, etype, child_defs in XSDM_MAPPING:
            # print 'PROCESS', getattr(xselement, 'local_name', xselement.__class__.__name__), etype
            for entity, profile_element in to_process.pop(xselement, ()):
                assert etype == entity.cw_etype
                self._process(entity, profile_element, child_defs, to_process)
        # then process remaining elements
        # print 'STARTING ROUND 2'
        while to_process:
            xselement = next(iter(to_process))
            entities_profiles = to_process.pop(xselement, ())
            if entities_profiles:
                try:
                    etype, child_defs = XSDM_MAPPING[xselement]
                except KeyError:
360
361
                    # element has no children
                    continue
362
363
364
365
366
367
                for entity, profile_element in entities_profiles:
                    assert etype == entity.cw_etype
                    self._process(entity, profile_element, child_defs, to_process)

        assert not to_process, to_process

368
369
370
371
372
373
    def _process(self, entity, profile_element, child_defs, to_process):
        for occ, path in child_defs:
            # print '  child', getattr(occ.target, 'local_name', occ.target.__class__.__name__), \
            #    [x[:-1] for x in path]
            if not path:
                assert not isinstance(occ.target, graph_nodes.XMLAttribute)
374
                assert occ.target.local_name in JUMP_ELEMENTS, occ.target
375
                if occ.minimum == 0 and not any(iter_path_children(occ.target, entity)):
376
377
                    # element has no children, skip it
                    continue
378
379
380
381
382
                if occ.target.local_name in JUMPED_OPTIONAL_ELEMENTS:
                    # elements in JUMPED_OPTIONAL_ELEMENTS are jumped but have optional cardinality,
                    # so search in all it's child element, and mark it as mandatory if one of them
                    # is mandatory, else keep it optional
                    cardinality = '0..1'
383
                    for _path, target in iter_path_children(occ.target, entity):
384
385
386
                        if _path[0][2] in BASE_TYPES:
                            # special case of a mandatory attribute: parent element will be
                            # mandatory if some value is specified, else that's fine
387
                            if target is not None:
388
389
                                cardinality = '1'
                                break
390
                        elif any(te.user_cardinality == '1' for te in target):
391
392
393
394
395
396
397
398
                            cardinality = '1'
                            break
                else:
                    cardinality = None
                # jumped element: give None as target_value but register the generated element for
                # later processing
                self.dispatch_occ(profile_element, occ, None, to_process,
                                  card_entity=attrdict({'user_cardinality': cardinality}))
399
                to_process[occ.target].append((entity, self.jumped_element(profile_element)))
400
401
402
403
404
405
            else:
                # print '  values', _path_target_values(entity, path)
                for card_entity, target_value in _path_target_values(entity, path):
                    self.dispatch_occ(profile_element, occ, target_value, to_process,
                                      card_entity=card_entity)

406
407
408
409
410
411
412
413
    def init_transfer_element(self, xselement, root, entity):
        """Initialize and return the XML element holding the ArchiveTransfer definition, as well as
        any other necessary global definitions.
        """
        raise NotImplementedError()

    def jumped_element(self, profile_element):
        """Return the last generated element, for insertion of its content."""
414
415
        raise NotImplementedError()

416
417
418
419
420
421
422
423
    @staticmethod
    def cwuri_url(entity):
        """Return "public" URI for the given entity.

        In a staticmethod to ease overriding in subclasses (eg saem).
        """
        return entity.cwuri

424

425
class SEDA2RelaxNGExport(RNGMixin, SEDA2ExportAdapter):
Élodie Thiéblin's avatar
Élodie Thiéblin committed
426
    """Adapter to build a Relax NG representation of a SEDA profile, using SEDA 2.1 specification.
427
    """
Élodie Thiéblin's avatar
Élodie Thiéblin committed
428
    __regid__ = 'SEDA-2.1.rng'
429

430
    namespaces = {
Élodie Thiéblin's avatar
Élodie Thiéblin committed
431
432
        None: 'fr:gouv:culture:archivesdefrance:seda:v2.1',
        'seda': 'fr:gouv:culture:archivesdefrance:seda:v2.1',
433
        'xml': 'http://www.w3.org/XML/1998/namespace',
434
435
        'xsd': 'http://www.w3.org/2001/XMLSchema',
        'xlink': 'http://www.w3.org/1999/xlink',
436
437
        'rng': 'http://relaxng.org/ns/structure/1.0',
        'a': 'http://relaxng.org/ns/compatibility/annotations/1.0',
438
    }
439

440
    _root_attributes = {
Élodie Thiéblin's avatar
Élodie Thiéblin committed
441
        'ns': 'fr:gouv:culture:archivesdefrance:seda:v2.1',
442
443
444
445
        'datatypeLibrary': 'http://www.w3.org/2001/XMLSchema-datatypes',
    }

    def dump_etree(self):
Nicolas Chauvat's avatar
Nicolas Chauvat committed
446
        """Return a RelaxNG etree for the adapted SEDA profile."""
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
        root = self.element('rng:grammar', attributes=self.root_attributes)
        start = self.element('rng:start', root)
        # XXX http://lists.xml.org/archives/xml-dev/200206/msg01074.html ?
        # self.element('xsd:import', parent=root,
        #              attributes={'namespace': 'http://www.w3.org/1999/xlink',
        #                          'schemaLocation': 'http://www.w3.org/1999/xlink.xsd'})
        self._dump(start)

        open_type = self.element('rng:define', root, {'name': 'OpenType'})
        open_elt = self._create_hierarchy(open_type, ['rng:zeroOrMore', 'rng:element'])
        self.element('rng:anyName', open_elt)
        self._create_hierarchy(open_elt, ['rng:zeroOrMore', 'rng:attribute', 'rng:anyName'])

        # add a 'text' node to empty rng:element to satisfy the RNG grammar
        namespaces = self.namespaces.copy()
        del namespaces[None]  # xpath engine don't want None prefix
        for element in root.xpath('//rng:element[not(*)]', namespaces=namespaces):
            self.element('rng:text', element)
465
466
467

        self.postprocess_dataobjects(root, namespaces)

468
469
        return root

470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
    def postprocess_dataobjects(self, root, namespaces):
        """Insert rng:group node as parent of [Binary|Physical]DataObject node
        to avoid forcing an order among them
        """
        # start by looking for the rng:element node for DataObjectPackage or its parent rng:optional
        dops = root.xpath('/rng:grammar/rng:start/rng:element/'
                          'rng:element[@name="DataObjectPackage"]',
                          namespaces=namespaces)
        if not dops:
            dops = root.xpath('/rng:grammar/rng:start/rng:element/'
                              'rng:optional/rng:element[@name="DataObjectPackage"]',
                              namespaces=namespaces)
        if dops:
            assert len(dops) == 1
            dop = dops[0]
            nodes = dop.xpath(
                'rng:element[@name="BinaryDataObject" or @name="PhysicalDataObject"]',
                namespaces=namespaces)
            opt_nodes = dop.xpath(
                'rng:optional[rng:element[@name="BinaryDataObject" or @name="PhysicalDataObject"]]',
                namespaces=namespaces)
            if nodes or opt_nodes:
                group = self.element('rng:group')
                # insert after definition of dop's id attribute
                dop[0].addnext(group)
                for node in chain(nodes, opt_nodes):
                    group.append(node)

498
    def init_transfer_element(self, xselement, root, entity):
499
500
501
502
503
504
505
        transfer_element = self.element('rng:element', root,
                                        {'name': xselement.local_name,
                                         'documentation': entity.user_annotation})
        exc = self._create_hierarchy(
            transfer_element, ['rng:zeroOrMore', 'rng:attribute', 'rng:anyName', 'rng:except'])
        self.element('rng:nsName', exc)
        self.element('rng:nsName', exc, {'ns': ''})
506
        return transfer_element
507

508
509
510
511
512
513
514
    def jumped_element(self, profile_element):
        element = profile_element[-1]
        if element.tag != '{http://relaxng.org/ns/structure/1.0}element':
            # optional, zeroOrMore, etc.: should pick their child element
            element = element[-1]
            assert element.tag == '{http://relaxng.org/ns/structure/1.0}element', element
        return element
515
516
517
518
519
520
521
522

    def element_alternative(self, occ, profile_element, target_value, to_process, card_entity):
        parent_element = self._rng_element_parent(occ, card_entity, profile_element)
        target_element = self.element('rng:choice', parent_element)
        to_process[occ.target].append((target_value, target_element))

    def element_sequence(self, occ, profile_element, target_value, to_process, card_entity):
        parent_element = self._rng_element_parent(occ, card_entity, profile_element)
523
524
        target_element = self.element('rng:group', parent_element)  # XXX sequence
        to_process[occ.target].append((target_value, target_element))
525
526
527

    def element_xmlattribute(self, occ, profile_element, target_value, to_process, card_entity):
        parent_element = self._rng_attribute_parent(occ, card_entity, profile_element)
528
        self._rng_attribute(occ.target, parent_element, serialize(target_value, self.cwuri_url))
529
530
531
532
533
534
535
536
537
538
539
540
541
542

    def element_xmlelement(self, occ, profile_element, target_value, to_process, card_entity):  # noqa
        parent_element = self._rng_element_parent(occ, card_entity, profile_element)
        xselement = occ.target
        attrs = {'documentation': getattr(card_entity, 'user_annotation', None),
                 'name': xselement.local_name}
        if xselement.local_name == 'Signature':
            element = self.element('rng:element', parent_element, attrs)
            self.element('rng:ref', element, {'name': 'OpenType'})
        elif isinstance(occ, dict):  # fake occurence introduced for some elements'content
            # target element has already been introduced: it is now given as profile_element
            self.fill_element(xselement, profile_element, target_value, card_entity)
        else:
            target_element = self.element('rng:element', parent_element, attrs)
543
544
545
            xstypes = content_types(xselement.textual_content_type)
            if xstypes:
                if len(xstypes) == 1:
546
547
548
                    parent_element = target_element
                else:
                    parent_element = self.element('rng:choice', target_element)
549
                for xstype in xstypes:
550
551
552
553
554
                    self.fill_element(xselement, parent_element, target_value, card_entity,
                                      xstype=xstype, copy_attributes=True)
            else:
                # target is a complex element
                if getattr(target_value, 'eid', None):  # value is an entity
555
                    if target_value.cw_etype == 'AuthorityRecord':
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
                        self.fill_organization_element(target_element, target_value)
                elif xselement.local_name in ('ArchivalAgency', 'TransferringAgency'):
                    self.fill_organization_element(target_element, None)
                elif target_value is not None:
                    assert False, (xselement, target_value)
            if getattr(target_value, 'eid', None):  # value is an entity
                to_process[xselement].append((target_value, target_element))

    def fill_element(self, xselement, profile_element, value, card_entity,  # noqa
                     copy_attributes=False, xstype=None):
        if xselement.local_name == 'KeywordType':
            attr = self.element('rng:attribute', attributes={'name': 'listVersionID'},
                                parent=self.element('rng:optional', profile_element))
            if value:
                list_value = value.scheme.description or value.scheme.dc_title()
                attrs = {'type': xstype} if xstype else {}
                self.element('rng:value', attr, attrs, text=list_value)
            else:
                attr.attrib[self.qname('a:defaultValue')] = 'edition 2009'

        elif (xselement.local_name == 'KeywordReference' and card_entity.scheme):
            self.concept_scheme_attribute(xselement, profile_element, card_entity.scheme)

        elif getattr(value, 'cw_etype', None) == 'Concept':
            self.concept_scheme_attribute(xselement, profile_element, value.scheme)

        elif copy_attributes:
            for attrname, occ in xselement.attributes.items():
                if attrname in ('id', 'href') or attrname.startswith(('list', 'scheme')):
585
                    parent_element = self._rng_attribute_parent(occ, None, profile_element)
586
                    self._rng_attribute(occ.target, parent_element)
587
588
589
590
        # special case for KeywordReference content, the only known case where we want URL instead
        # of label of its concept value
        if value is not None and xselement.local_name == 'KeywordReference':
            fixed_value = self.cwuri_url(value)
591
        elif isinstance(value, (tuple, list)):
592
            fixed_value = [serialize(val, self.cwuri_url) for val in value]
593
594
        else:
            fixed_value = serialize(value, self.cwuri_url)
595
        if fixed_value is not None:
596
            if _internal_reference(value):
597
                profile_element.attrib[self.qname('a:defaultValue')] = fixed_value
598
                self.element('rng:data', profile_element, {'type': xstype})
599
            else:
600
601
602
603
604
605
606
607
                if len(profile_element):
                    # As there is a fixed value search for potential element
                    # tag data and extract its type
                    for elem in profile_element:
                        if elem.tag == '{http://relaxng.org/ns/structure/1.0}data':
                            xstype = elem.attrib.get('type')
                            profile_element.remove(elem)
                            break
608
                attrs = {'type': xstype} if xstype else {}
609
                if isinstance(fixed_value, (tuple, list)):
610
611
612
613
614
                    choice = self.element('rng:choice', profile_element)
                    for val in fixed_value:
                        self.element('rng:value', choice, attrs, text=val)
                else:
                    self.element('rng:value', profile_element, attrs, text=fixed_value)
615
616
617
618
619
        elif xstype is not None:
            self.element('rng:data', profile_element, {'type': xstype})

    def concept_scheme_attribute(self, xselement, type_element, scheme):
        try:
620
            scheme_attr = xselement_scheme_attribute(xselement)
621
        except KeyError:
622
623
624
            return
        scheme_attr = self.element('rng:attribute', type_element,
                                   attributes={'name': scheme_attr})
625
        self.element('rng:value', scheme_attr, text=self.cwuri_url(scheme))
626
627
628
629

    def fill_organization_element(self, parent_element, value):
        target_element = self.element('rng:element', parent_element, {'name': 'Identifier'})
        if value:
630
            self.element('rng:value', target_element, text=self.cwuri_url(value))
631

632
633
    def _rng_element_parent(self, occ, card_entity, profile_element):
        minimum, maximum = element_minmax_cardinality(occ, card_entity)
634
        return self.rng_element_parent(profile_element, minimum, maximum)
635
636

    def _rng_attribute_parent(self, occ, card_entity, profile_element):
637
638
        minimum = attribute_minimum_cardinality(occ, card_entity)
        return self.rng_element_parent(profile_element, minimum)
639
640
641
642
643
644
645
646
647
648

    def _rng_attribute(self, xselement, parent_element, value=None):
        xstypes = content_types(xselement.textual_content_type)
        if len(xstypes) > 1:
            parent_element = self.element('rng:choice', parent_element)
        for xstype in xstypes:
            attr_element = self.element('rng:attribute', parent_element,
                                        {'name': xselement.local_name})
            if value is not None:
                if xselement.local_name == 'id':
649
                    attr_element.attrib[self.qname('xml:id')] = value
650
                    self.element('rng:data', attr_element, {'type': 'ID'})
651
652
653
654
                else:
                    self.element('rng:value', attr_element, {'type': xstype}, text=value)
            else:
                self.element('rng:data', attr_element, {'type': xstype})
655
656
657
658
659
660
661

    def _create_hierarchy(self, parent, tags):
        for tag in tags:
            parent = self.element(tag, parent)
        return parent


662
663
664
665
666
667
668
669
670
671
672
673
674
675
def _safe_cardinality(entity):
    """Return entity's cardinality if some entity is given, else None."""
    if entity is None:
        return None
    return entity.user_cardinality


def _safe_concept_value(entity, concepts_language):
    """Return entity's targetted concept if some entity is given, else None."""
    if entity is None:
        return None
    return _concept_value(entity.concept, concepts_language)


676
677
class XAttr(namedtuple('_XAttr', ['name', 'qualified_type', 'cardinality', 'fixed_value'])):
    """Simple representation of an attribute element in a schema (RNG or XSD).
678

679
    Parameters:
680

681
    * `name`, the attribute's name,
682

683
    * `qualified_type`, its qualified type (e.g. 'xsd:string'),
684

685
686
687
688
    * `cardinality`, optional cardinality as string (None, '1' or '0..1') - default to '1' if some
      fixed value is provided, else to None (i.e. attribute is prohibited),

    * `fixed_value`, optional fixed value for the attribute.
689
690

    """
691
    def __new__(cls, name, qualified_type, cardinality='0..1', fixed_value=None):
692
        assert cardinality in (None, '1', '0..1'), cardinality
693
        if fixed_value:
694
            cardinality = '1'
695
            if isinstance(fixed_value, (tuple, list)) and len(fixed_value) == 1:
696
697
698
                fixed_value = fixed_value[0]
        else:
            fixed_value = None
699
        return super(XAttr, cls).__new__(cls, name, qualified_type, cardinality, fixed_value)
700
701


702
703
LIST_VERSION_ID_2009 = XAttr('listVersionID', 'xsd:token', '1', 'edition 2009')
LIST_VERSION_ID_2011 = XAttr('listVersionID', 'xsd:token', '1', 'edition 2011')
704
705


706
class SEDA1XSDExport(SEDA2ExportAdapter):
707
708
709
    """Adapter to build an XSD representation of a simplified SEDA profile, using SEDA 1.0
    specification.

Élodie Thiéblin's avatar
Élodie Thiéblin committed
710
    The SEDA2XSDExport implementation may be driven by the SEDA 2.1 XSD model because it's used as
711
712
713
714
    the basis for the Yams model generation. We can't do the same thing with lower version of SEDA,
    hence the limitation to simplified profile, and a direct implementation of the export.
    """
    __regid__ = 'SEDA-1.0.xsd'
715
    __select__ = SEDA2ExportAdapter.__select__ & simplified_profile()
716
717
718

    namespaces = {
        None: 'fr:gouv:culture:archivesdefrance:seda:v1.0',
Élodie Thiéblin's avatar
Élodie Thiéblin committed
719
        'seda': 'fr:gouv:culture:archivesdefrance:seda:v2.1',
720
        'xml': 'http://www.w3.org/XML/1998/namespace',
721
722
723
724
725
726
727
728
729
730
        'xsd': 'http://www.w3.org/2001/XMLSchema',
        'qdt': 'fr:gouv:culture:archivesdefrance:seda:v1.0:QualifiedDataType:1',
        'udt': 'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:10',
        'clmDAFFileTypeCode': 'urn:un:unece:uncefact:codelist:draft:DAF:fileTypeCode:2009-08-18',
        'clmIANACharacterSetCode':
        'urn:un:unece:uncefact:codelist:standard:IANA:CharacterSetCode:2007-05-14',
        'clmIANAMIMEMediaType':
        'urn:un:unece:uncefact:codelist:standard:IANA:MIMEMediaType:2008-11-12',
        'clm60133': 'urn:un:unece:uncefact:codelist:standard:6:0133:40106',
    }
731
    _root_attributes = {
732
733
734
735
736
737
738
739
        'targetNamespace': 'fr:gouv:culture:archivesdefrance:seda:v1.0',
        'attributeFormDefault': 'unqualified',
        'elementFormDefault': 'qualified',
        'version': '1.0',
    }

    concepts_language = 'seda-1'

740
741
742
743
744
745
    def element_schema(self, parent, name, xsd_type=None,
                       fixed_value=None, default_value=None,
                       cardinality='1', documentation=None,
                       xsd_attributes=(), extra_attributes=None):
        assert not (fixed_value and default_value), \
            'only one of fixed_value or default_value may be specified'
746
        attributes = {'name': name}
747
748
        if extra_attributes is not None:
            attributes.update(extra_attributes)
749
        if fixed_value is not None:
Noé Gaumont's avatar
Noé Gaumont committed
750
            attributes['fixed'] = str(fixed_value)
751
        elif default_value is not None:
Noé Gaumont's avatar
Noé Gaumont committed
752
            attributes['default'] = str(default_value)
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
        if xsd_type is not None and not xsd_attributes:
            attributes['type'] = xsd_type
        assert cardinality in ('0..1', '0..n', '1', '1..n')
        if cardinality != '1':
            if cardinality[0] == '0':
                attributes['minOccurs'] = '0'
            if cardinality[-1] == 'n':
                attributes['maxOccurs'] = 'unbounded'
        if documentation:
            attributes['documentation'] = documentation
        element = self.element('xsd:element', parent, attributes)
        children_parent = None
        if xsd_type is None:
            attributes_parent = self.element('xsd:complexType', element)
            children_parent = self.element('xsd:sequence', attributes_parent)
        elif xsd_attributes:
            ct = self.element('xsd:complexType', element)
            scontent = self.element('xsd:simpleContent', ct)
            attributes_parent = self.element('xsd:extension', scontent, {'base': xsd_type})
772
773
        for xattr in xsd_attributes:
            self.attribute_schema(attributes_parent, xattr)
774
775
        return children_parent

776
    def attribute_schema(self, parent, xattr):
777
        attrs = {'name': xattr.name}
778
779
780
781
782
783
        if xattr.cardinality is None:
            attrs['use'] = 'prohibited'
        elif xattr.cardinality == '1':
            attrs['use'] = 'required'
        else:
            attrs['use'] = 'optional'
784
        if not isinstance(xattr.fixed_value, (tuple, list)):
785
            attrs['type'] = xattr.qualified_type
Noé Gaumont's avatar
Noé Gaumont committed
786
787
            if isinstance(xattr.fixed_value, str):
                attrs['fixed'] = xattr.fixed_value
788
        attribute_element = self.element('xsd:attribute', parent, attrs)
789
        if isinstance(xattr.fixed_value, (tuple, list)):
790
791
792
793
794
            type_element = self.element('xsd:simpleType', attribute_element)
            restriction_element = self.element('xsd:restriction', type_element,
                                               {'base': 'xsd:token'})
            for value in xattr.fixed_value:
                self.element('xsd:enumeration', restriction_element, {'value': value})
795

796
797
798
799
800
801
802
803
804
805
806
807
808
    # business visit methods #######################################################################

    def dump_etree(self):
        """Return an XSD etree for the adapted SEDA profile."""
        root = self.element('xsd:schema', attributes=self.root_attributes)
        # self.element('xsd:import', parent=root,
        #              attributes={'namespace': 'http://www.w3.org/XML/1998/namespace',
        #                          'schemaLocation': 'http://www.w3.org/2001/xml.xsd'})
        self.xsd_transfer(root, self.entity)
        return root

    def xsd_transfer(self, parent, archive_transfer):
        """Append XSD elements for the archive transfer to the given parent node."""
809
        transfer_node = self.xsd_transfer_base(parent, archive_transfer)
810
        for archive_unit in archive_transfer.archive_units:
811
812
813
814
            self.xsd_archive(transfer_node, archive_unit)

    def xsd_archive(self, parent, archive_unit):
        """Append XSD elements for an archive to the given parent node."""
815
816
817
818
819
        archive_node = self.element_schema(
            parent, 'Archive',
            cardinality=archive_unit.user_cardinality,
            documentation=archive_unit.user_annotation,
            xsd_attributes=[XAttr('Id', 'xsd:ID')],
820
            extra_attributes={'xml:id': xmlid(archive_unit)},
821
        )
822
823
        transfer = archive_unit.cw_adapt_to('ITreeBase').parent()
        self.xsd_archival_agreement(archive_node, transfer)
824
        # hard-coded description's language XXX fine, content language may be specified
825
826
827
        self.element_schema(archive_node, 'DescriptionLanguage', 'qdt:CodeLanguageType',
                            fixed_value='fra',
                            xsd_attributes=[LIST_VERSION_ID_2011])
828
        name_entity = self.archive_unit_name(archive_unit)
829
830
831
832
        self.element_schema(archive_node, 'Name', 'udt:TextType',
                            fixed_value=name_entity.title,
                            documentation=name_entity.user_annotation,
                            xsd_attributes=[XAttr('languageID', 'xsd:language')])
833
        content_entity = self.archive_unit_content(archive_unit)
834
835
        self.xsd_transferring_agency_archive_identifier(archive_node, content_entity,
                                                        'TransferringAgencyArchiveIdentifier')
836
        self.xsd_content_description(archive_node, content_entity)
837
        self.xsd_rules(archive_node, archive_unit)
838
839
        self.xsd_children(archive_node, archive_unit)

840
    archive_object_tag_name = 'ArchiveObject'
841

842
843
    def xsd_archive_object(self, parent, archive_unit):
        """Append XSD elements for the archive object to the given parent node."""
844
845
846
847
848
        ao_node = self.element_schema(
            parent, self.archive_object_tag_name,
            cardinality=archive_unit.user_cardinality,
            documentation=archive_unit.user_annotation,
            xsd_attributes=[XAttr('Id', 'xsd:ID')],
849
            extra_attributes={'xml:id': xmlid(archive_unit)},
850
        )
851
852
853
854
855
856
857
858
859
860
861
862
863
        content_entity = self.archive_unit_content(archive_unit)
        self.element_schema(ao_node, 'Name', 'udt:TextType',
                            fixed_value=content_entity.title.title,
                            documentation=content_entity.title.user_annotation,
                            xsd_attributes=[XAttr('languageID', 'xsd:language')])
        self.xsd_transferring_agency_archive_identifier(ao_node, content_entity,
                                                        'TransferringAgencyObjectIdentifier')
        if (self.__regid__.startswith('SEDA-1.0')
                or content_entity.start_date
                or content_entity.end_date
                or content_entity.description
                or content_entity.keywords):
            self.xsd_content_description(ao_node, content_entity)
864
        self.xsd_rules(ao_node, archive_unit)
865
866
867
868
869
870
        self.xsd_children(ao_node, archive_unit)

        return ao_node

    def xsd_document(self, parent, data_object):
        """Append XSD elements for the document to the given parent node."""
871
872
873
874
875
        document_node = self.element_schema(
            parent, 'Document',
            cardinality=data_object.user_cardinality,
            documentation=data_object.user_annotation,
            xsd_attributes=[XAttr('Id', 'xsd:ID')],
876
            extra_attributes={'xml:id': xmlid(data_object)},
877
        )
878

879
        self.xsd_system_id(document_node, data_object)
880
881
        self.xsd_attachment(document_node, data_object)
        self.xsd_date_created(document_node, data_object)
882
        self.xsd_integrity(document_node, data_object)
883
        self.xsd_document_type(document_node, data_object)
884

885
886
887
    # in SEDA 1 sub-archive units are exposed before data objects
    last_children_type = 'SEDABinaryDataObject'

888
889
890
891
    def xsd_children(self, parent, entity):
        """Iter on archive/archive object children, which may be either
        archive objects or documents, and append XSD elements for them to the given parent node.
        """
892
        for au_or_bdo in sorted(entity.cw_adapt_to('ITreeBase').iterchildren(),
893
                                key=lambda x: x.cw_etype == self.last_children_type):
894
895
896
897
898
899
            if au_or_bdo.cw_etype == 'SEDABinaryDataObject':
                self.xsd_document(parent, au_or_bdo)
            else:
                assert au_or_bdo.cw_etype == 'SEDAArchiveUnit'
                self.xsd_archive_object(parent, au_or_bdo)

900
901
    agencies_in_order = ('ArchivalAgency', 'TransferringAgency')

902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
    def xsd_transfer_base(self, parent, archive_transfer):
        """Create ArchiveTransfer element and add child which are common in 0.2 and 1.0.
        """
        transfer_node = self.element_schema(parent, 'ArchiveTransfer',
                                            documentation=archive_transfer.title,
                                            xsd_attributes=[XAttr('Id', 'xsd:ID')])
        for comment in archive_transfer.comments:
            self.element_schema(transfer_node, 'Comment', 'udt:TextType',
                                fixed_value=comment.comment,
                                cardinality=comment.user_cardinality,
                                documentation=comment.user_annotation,
                                xsd_attributes=[XAttr('languageID', 'xsd:language')])
        self.element_schema(transfer_node, 'Date', 'udt:DateTimeType')
        self.element_schema(transfer_node, 'TransferIdentifier', 'qdt:ArchivesIDType',
                            xsd_attributes=self.xsd_attributes_scheme())
917
        for agency_type in self.agencies_in_order:
918
919
920
            self.xsd_agency(transfer_node, agency_type)
        return transfer_node

921
922
    def xsd_attachment(self, parent, data_object):
        _safe_concept = partial(_safe_concept_value, concepts_language=self.concepts_language)
923
924

        format_id = data_object.format_id
925
        if format_id is not None:
926
927
            format_ids = sorted(set(_concept_value(concept, self.concepts_language)
                                    for concept in format_id.concepts))
928
929
        else:
            format_ids = []
930
        mime_type = data_object.mime_type
931
        if mime_type is not None:
932
933
            mime_types = sorted(set(_concept_value(concept, self.concepts_language)
                                    for concept in mime_type.concepts))
934
935
        else:
            mime_types = []
936
        encoding = data_object.encoding
937
        self.element_schema(parent, 'Attachment', 'qdt:ArchivesBinaryObjectType',
938
939
                            xsd_attributes=[
                                XAttr('format', 'clmDAFFileTypeCode:FileTypeCodeType',
940
941
                                      cardinality=format_id.user_cardinality,
                                      fixed_value=format_ids),
942
943
                                XAttr('encodingCode',
                                      'clm60133:CharacterSetEncodingCodeContentType',
944
945
                                      cardinality=_safe_cardinality(encoding),
                                      fixed_value=_safe_concept(encoding)),
946
                                XAttr('mimeCode', 'clmIANAMIMEMediaType:MIMEMediaTypeContentType',
947
948
                                      cardinality=mime_type.user_cardinality,
                                      fixed_value=mime_types),
949
950
951
952
953
                                XAttr('filename', 'xsd:string',
                                      cardinality='0..1',
                                      fixed_value=data_object.filename),
                                # hard-coded attributes
                                XAttr('characterSetCode',
954
955
956
957
                                      'clmIANACharacterSetCode:CharacterSetCodeContentType',
                                      cardinality=None),
                                XAttr('uri', 'xsd:anyURI',
                                      cardinality=None),
958
                            ])
959
960

    def xsd_date_created(self, parent, data_object):
961
962
        date_created = data_object.date_created_by_application
        if date_created:
963
            self.element_schema(parent, 'Creation', 'udt:DateTimeType',
964
965
966
                                cardinality=date_created.user_cardinality,
                                documentation=date_created.user_annotation)

967
    def xsd_document_type(self, parent, data_object):
968
969
970
971
972
        references = list(data_object.referenced_by)
        assert len(references) == 1, (
            'Unexpected number of references in document {} of {}: {}'.format(
                data_object.eid, data_object.container[0].eid, references))
        seq = references[0]
973
        self.element_schema(parent, 'Type', 'qdt:CodeDocumentType',
974
                            fixed_value=_safe_concept_value(seq.type, self.concepts_language),
975
976
                            xsd_attributes=[LIST_VERSION_ID_2009])

977
978
979
980
981
982
983
984
985
986
    system_id_tag_name = 'ArchivalAgencyDocumentIdentifier'

    def xsd_system_id(self, parent, data_object):
        system_id = self.system_id(data_object)
        if system_id:
            self.element_schema(parent, self.system_id_tag_name, 'qdt:ArchivesIDType',
                                cardinality=system_id.user_cardinality,
                                documentation=system_id.user_annotation,
                                xsd_attributes=self.xsd_attributes_scheme())

987
988
989
990
991
992
993
994
995
    def xsd_agency(self, parent, agency_type, agency=None):
        agency_node = self.element_schema(parent, agency_type,
                                          cardinality=agency.user_cardinality if agency else '1')
        self.element_schema(agency_node, 'Identification', 'qdt:ArchivesIDType',
                            fixed_value=self.agency_id(agency) if agency else None,
                            xsd_attributes=self.xsd_attributes_scheme())
        self.element_schema(agency_node, 'Name', 'udt:TextType',
                            fixed_value=self.agency_name(agency) if agency else None,
                            cardinality='0..1')
996

997
998
999
1000
1001
1002
1003
1004
1005
    def xsd_archival_agreement(self, parent, transfer):
        agreement = transfer.archival_agreement
        if agreement:
            self.element_schema(parent, 'ArchivalAgreement', 'qdt:ArchivesIDType',
                                cardinality=agreement.user_cardinality,
                                documentation=agreement.user_annotation,
                                fixed_value=agreement.archival_agreement,
                                xsd_attributes=self.xsd_attributes_scheme())

1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
    def xsd_transferring_agency_archive_identifier(self, parent, content_entity, tag_name):
        if content_entity.transferring_agency_archive_unit_identifier:
            agency_entity = content_entity.transferring_agency_archive_unit_identifier
            self.element_schema(
                parent, tag_name, 'qdt:ArchiveIDType',
                fixed_value=agency_entity.transferring_agency_archive_unit_identifier,
                cardinality=agency_entity.user_cardinality,
                documentation=agency_entity.user_annotation,
                xsd_attributes=self.xsd_attributes_scheme())

1016
1017
1018
1019
1020
1021
1022
1023
    def xsd_rules(self, parent, archive_unit):
        access_rule_entity = archive_unit.inherited_rule('access')
        if access_rule_entity:
            self.xsd_access_rule(parent, access_rule_entity)
        appraisal_rule_entity = archive_unit.inherited_rule('appraisal')
        if appraisal_rule_entity:
            self.xsd_appraisal_rule(parent, appraisal_rule_entity)

1024
1025
    appraisal_tag_name = 'AppraisalRule'

1026
1027
1028
    def xsd_appraisal_rule(self, parent, appraisal_rule):
        # XXX cardinality 1 on rule, not multiple + element name : 'Appraisal' ou 'AppraisalRule'
        # (cf http://www.archivesdefrance.culture.gouv.fr/seda/api/index.html)
1029
        ar_node = self.element_schema(parent, self.appraisal_tag_name,
1030
1031
1032
                                      cardinality=appraisal_rule.user_cardinality,
                                      documentation=appraisal_rule.user_annotation,
                                      xsd_attributes=[XAttr('Id', 'xsd:ID')])
1033
1034
        ar_code = appraisal_rule.final_action_concept
        ar_code_value = _concept_value(ar_code, self.concepts_language)
1035
1036
1037
        self.element_schema(ar_node, 'Code', 'qdt:CodeAppraisalType',
                            fixed_value=ar_code_value,
                            xsd_attributes=[LIST_VERSION_ID_2009])
1038
1039
1040

        rule = appraisal_rule.rules[0] if appraisal_rule.rules else None
        value = _concept_value(rule.rule_concept, self.concepts_language) if rule else None
1041
1042
1043
1044
        self.element_schema(ar_node, 'Duration', 'qdt:ArchivesDurationType',
                            fixed_value=value,
                            documentation=rule.user_annotation if rule else None)
        self.element_schema(ar_node, 'StartDate', 'udt:DateType')
1045
1046
1047
1048
1049

    access_restriction_tag_name = 'AccessRestrictionRule'

    def xsd_access_rule(self, parent, access_rule):
        """Append XSD elements for an access restriction to the given parent node."""
1050
1051
1052
1053
        ar_node = self.element_schema(parent, self.access_restriction_tag_name,
                                      cardinality=access_rule.user_cardinality,
                                      documentation=access_rule.user_annotation,
                                      xsd_attributes=[XAttr('Id', 'xsd:ID')])
1054
1055
1056
        # XXX cardinality 1
        rule = access_rule.rules[0] if access_rule.rules else None
        value = _concept_value(rule.rule_concept, self.concepts_language) if rule else None
1057
1058
1059
1060
1061
        self.element_schema(ar_node, 'Code', 'qdt:CodeAccessRestrictionType',
                            fixed_value=value,
                            documentation=rule.user_annotation if rule else None,
                            xsd_attributes=[LIST_VERSION_ID_2009])
        self.element_schema(ar_node, 'StartDate', 'udt:DateType')
1062
1063
1064

    def xsd_content_description(self, parent, content):
        """Append XSD elements for a description content to the given parent node"""
1065
1066
        cd_node = self.element_schema(parent, 'ContentDescription',
                                      xsd_attributes=[XAttr('Id', 'xsd:ID')])
1067
        self.xsd_description(cd_node, content)
1068
1069
1070
        self.xsd_description_level(cd_node, content.description_level_concept)
        self.xsd_language(cd_node, content)
        self.xsd_content_dates(cd_node, content)
1071
        self.xsd_custodial_history(cd_node, content)
1072
1073
1074
1075
        self.xsd_keywords(cd_node, content)
        self.xsd_originating_agency(cd_node, content)

    def xsd_language(self, parent, content):
1076
1077
        # XXX language is 0..1 in SEDA 2, 1..n in earlier version
        language = content.language.concept if content.language else None
1078
        self.element_schema(parent, 'Language', 'qdt:CodeLanguageType',
1079
                            fixed_value=_concept_value(language, self.concepts_language),
1080
                            xsd_attributes=[LIST_VERSION_ID_2009])
1081
1082

    def xsd_content_dates(self, parent, content):
1083
        for seda2_name, seda1_name in (('end', 'latest'), ('start', 'oldest')):
1084
1085
            date_entity = getattr(content, '%s_date' % seda2_name)
            if date_entity:
1086
                self.element_schema(parent, '%sDate' % seda1_name.capitalize(), 'udt:DateType',
1087
1088
                                    cardinality=date_entity.user_cardinality,
                                    documentation=date_entity.user_annotation)
1089

1090
1091
1092
    def xsd_description_level(self, parent, concept):
        """Append XSD elements for a description level to the given parent node"""
        value = _concept_value(concept, self.concepts_language)
1093
1094
1095
        self.element_schema(parent, 'DescriptionLevel', 'qdt:CodeDescriptionLevelType',
                            fixed_value=value,
                            xsd_attributes=[LIST_VERSION_ID_2009])
1096

1097
    def xsd_description(self, parent, content):
1098
        """Append XSD elements for a description to the given parent node"""
1099
1100
1101
1102
1103
1104
1105
1106
        if content.description:
            self.element_schema(parent, 'Description', 'udt:TextType',
                                cardinality=content.description.user_cardinality,
                                documentation=content.description.user_annotation,
                                fixed_value=content.description.description,
                                xsd_attributes=[XAttr('languageID', 'xsd:language')])

    def xsd_keywords(self, parent, content):
1107
        for keyword in content.keywords:
1108
1109
            self.xsd_keyword(parent, keyword)

1110
1111
1112
1113
    def xsd_custodial_history(self, parent, content):
        if content.custodial_history_items:
            ch_node = self.element_schema(parent, 'CustodialHistory',
                                          cardinality='0..1')
1114
            for item in content.custodial_history_items:
1115
                when_card = item.when.user_cardinality if item.when else None
1116
1117
1118
1119
1120
1121
1122
                self.element_schema(
                    ch_node, 'CustodialHistoryItem', 'qdt:CustodialHistoryItemType',
                    cardinality=item.user_cardinality,
                    documentation=item.user_annotation,
                    xsd_attributes=[XAttr('when', 'udt:DateType',
                                          cardinality=when_card),
                                    XAttr('languageID', 'xsd:language')],
1123
                    extra_attributes={'xml:id': xmlid(item)},
1124
                )
1125

1126
1127
1128
    def xsd_originating_agency(self, parent, content):
        if content.originating_agency:
            self.xsd_agency(parent, 'OriginatingAgency', content.originating_agency)
1129

1130
    def xsd_integrity(self, parent, data_object):
1131
        cardinality = integrity_cardinality(data_object)
1132
1133
        algorithm = data_object.seda_algorithm[0] if data_object.seda_algorithm else None
        self.element_schema(parent, 'Integrity', 'qdt:ArchivesHashcodeBinaryObjectType',