profile_generation.py 69.9 KB
Newer Older
1
# copyright 2016 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
23
from six import text_type, string_types

24
from lxml import etree
25
from pyxst.xml_struct import graph_nodes
26

27
28
from logilab.common import attrdict

29
30
from yams import BASE_TYPES

31
32
33
from cubicweb.predicates import is_instance
from cubicweb.view import EntityAdapter

34
35
36
from ..xsd import XSDM_MAPPING, JUMP_ELEMENTS
from ..xsd2yams import SKIP_ATTRS
from . import simplified_profile
37

38

39
JUMPED_OPTIONAL_ELEMENTS = set(('DataObjectPackage', 'FileInfo', 'PhysicalDimensions', 'Coverage'))
40
41


42
43
44
45
46
47
48
49
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::

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

    """
    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


62
63
64
65
66
67
68
69
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:
70
71
            if content_type == 'IDREF':
                content_type = 'NCName'
72
73
74
75
76
77
            content_types = (content_type,)
    else:
        content_types = ()
    return content_types


78
79
80
81
82
83
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')


84
85
86
87
88
89
90
91
92
93
94
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
95
    for code in (language, 'seda', 'en', 'fr'):
96
97
98
99
        try:
            return concept.labels[code]
        except KeyError:
            continue
100
    return concept.label()
101
102


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


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


126
127
128
129
130
131
132
133
134
135
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)
136
137
138
139
        if maxc == graph_nodes.INFINITY:
            maxvalue = 'n'
        else:
            maxvalue = max(maxc, maxvalue)
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
287
288
289
290
291
292
293
294
295
296

    def dump(self):
        """Return an schema string for the adapted SEDA profile."""
        root = self.dump_etree()
        return etree.tostring(root, encoding=self.encoding, pretty_print=True, standalone=False)

    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:
297

Sylvain Thénault's avatar
Sylvain Thénault committed
298
        * `tag`, tag name of the element
299
300
301

        * `parent`, the parent etree node

Sylvain Thénault's avatar
Sylvain Thénault committed
302
303
304
305
        * `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
306
307
308
309
        """
        attributes = attributes or {}
        tag = self.qname(tag)
        documentation = attributes.pop('documentation', None)
310
        for attr, value in list(attributes.items()):
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
            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)

330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
    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:
351
352
                    # element has no children
                    continue
353
354
355
356
357
358
                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

359
360
361
362
363
364
    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)
365
                assert occ.target.local_name in JUMP_ELEMENTS, occ.target
366
                if occ.minimum == 0 and not any(iter_path_children(occ.target, entity)):
367
368
                    # element has no children, skip it
                    continue
369
370
371
372
373
                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'
374
                    for _path, target in iter_path_children(occ.target, entity):
375
376
377
                        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
378
                            if target is not None:
379
380
                                cardinality = '1'
                                break
381
                        elif any(te.user_cardinality == '1' for te in target):
382
383
384
385
386
387
388
389
                            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}))
390
                to_process[occ.target].append((entity, self.jumped_element(profile_element)))
391
392
393
394
395
396
            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)

397
398
399
400
401
402
403
404
    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."""
405
406
        raise NotImplementedError()

407
408
409
410
411
412
413
414
    @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

415

416
417
418
419
420
class SEDA2RelaxNGExport(RNGMixin, SEDA2ExportAdapter):
    """Adapter to build a Relax NG representation of a SEDA profile, using SEDA 2.0 specification.
    """
    __regid__ = 'SEDA-2.0.rng'

421
422
    namespaces = {
        None: 'fr:gouv:culture:archivesdefrance:seda:v2.0',
423
        'seda': 'fr:gouv:culture:archivesdefrance:seda:v2.0',
424
        'xml': 'http://www.w3.org/XML/1998/namespace',
425
426
        'xsd': 'http://www.w3.org/2001/XMLSchema',
        'xlink': 'http://www.w3.org/1999/xlink',
427
428
        'rng': 'http://relaxng.org/ns/structure/1.0',
        'a': 'http://relaxng.org/ns/compatibility/annotations/1.0',
429
    }
430

431
    _root_attributes = {
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
        'ns': 'fr:gouv:culture:archivesdefrance:seda:v2.0',
        'datatypeLibrary': 'http://www.w3.org/2001/XMLSchema-datatypes',
    }

    def dump_etree(self):
        """Return an XSD etree for the adapted SEDA profile."""
        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)
456
457
458

        self.postprocess_dataobjects(root, namespaces)

459
460
        return root

461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
    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)

489
    def init_transfer_element(self, xselement, root, entity):
490
491
492
493
494
495
496
        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': ''})
497
        return transfer_element
498

499
500
501
502
503
504
505
    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
506
507
508
509
510
511
512
513

    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)
514
515
        target_element = self.element('rng:group', parent_element)  # XXX sequence
        to_process[occ.target].append((target_value, target_element))
516
517
518

    def element_xmlattribute(self, occ, profile_element, target_value, to_process, card_entity):
        parent_element = self._rng_attribute_parent(occ, card_entity, profile_element)
519
        self._rng_attribute(occ.target, parent_element, serialize(target_value, self.cwuri_url))
520
521
522
523
524
525
526
527
528
529
530
531
532
533

    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)
534
535
536
            xstypes = content_types(xselement.textual_content_type)
            if xstypes:
                if len(xstypes) == 1:
537
538
539
                    parent_element = target_element
                else:
                    parent_element = self.element('rng:choice', target_element)
540
                for xstype in xstypes:
541
542
543
544
545
                    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
546
                    if target_value.cw_etype == 'AuthorityRecord':
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
                        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')):
576
                    parent_element = self._rng_attribute_parent(occ, None, profile_element)
577
                    self._rng_attribute(occ.target, parent_element)
578
579
580
581
        # 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)
582
        elif isinstance(value, (tuple, list)):
583
            fixed_value = [serialize(val, self.cwuri_url) for val in value]
584
585
        else:
            fixed_value = serialize(value, self.cwuri_url)
586
        if fixed_value is not None:
587
            if _internal_reference(value):
588
                profile_element.attrib[self.qname('a:defaultValue')] = fixed_value
589
                self.element('rng:data', profile_element, {'type': xstype})
590
            else:
591
                if (len(profile_element)
592
593
594
595
                        and profile_element[-1].tag == '{http://relaxng.org/ns/structure/1.0}data'):
                    xstype = profile_element[-1].attrib.get('type')
                    profile_element.remove(profile_element[-1])
                attrs = {'type': xstype} if xstype else {}
596
                if isinstance(fixed_value, (tuple, list)):
597
598
599
600
601
                    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)
602
603
604
605
606
        elif xstype is not None:
            self.element('rng:data', profile_element, {'type': xstype})

    def concept_scheme_attribute(self, xselement, type_element, scheme):
        try:
607
            scheme_attr = xselement_scheme_attribute(xselement)
608
        except KeyError:
609
610
611
            return
        scheme_attr = self.element('rng:attribute', type_element,
                                   attributes={'name': scheme_attr})
612
        self.element('rng:value', scheme_attr, text=self.cwuri_url(scheme))
613
614
615
616

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

619
620
    def _rng_element_parent(self, occ, card_entity, profile_element):
        minimum, maximum = element_minmax_cardinality(occ, card_entity)
621
        return self.rng_element_parent(profile_element, minimum, maximum)
622
623

    def _rng_attribute_parent(self, occ, card_entity, profile_element):
624
625
        minimum = attribute_minimum_cardinality(occ, card_entity)
        return self.rng_element_parent(profile_element, minimum)
626
627
628
629
630
631
632
633
634
635

    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':
636
                    attr_element.attrib[self.qname('xml:id')] = value
637
                    self.element('rng:data', attr_element, {'type': 'ID'})
638
639
640
641
                else:
                    self.element('rng:value', attr_element, {'type': xstype}, text=value)
            else:
                self.element('rng:data', attr_element, {'type': xstype})
642
643
644
645
646
647
648

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


649
650
651
652
653
654
655
656
657
658
659
660
661
662
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)


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

666
    Parameters:
667

668
    * `name`, the attribute's name,
669

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

672
673
674
675
    * `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.
676
677

    """
678
    def __new__(cls, name, qualified_type, cardinality='0..1', fixed_value=None):
679
        assert cardinality in (None, '1', '0..1'), cardinality
680
        if fixed_value:
681
            cardinality = '1'
682
            if isinstance(fixed_value, (tuple, list)) and len(fixed_value) == 1:
683
684
685
                fixed_value = fixed_value[0]
        else:
            fixed_value = None
686
        return super(XAttr, cls).__new__(cls, name, qualified_type, cardinality, fixed_value)
687
688


689
690
LIST_VERSION_ID_2009 = XAttr('listVersionID', 'xsd:token', '1', 'edition 2009')
LIST_VERSION_ID_2011 = XAttr('listVersionID', 'xsd:token', '1', 'edition 2011')
691
692


693
class SEDA1XSDExport(SEDA2ExportAdapter):
694
695
696
697
698
699
700
701
    """Adapter to build an XSD representation of a simplified SEDA profile, using SEDA 1.0
    specification.

    The SEDA2XSDExport implementation may be driven by the SEDA 2.0 XSD model because it's used as
    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'
702
    __select__ = SEDA2ExportAdapter.__select__ & simplified_profile()
703
704
705

    namespaces = {
        None: 'fr:gouv:culture:archivesdefrance:seda:v1.0',
706
        'seda': 'fr:gouv:culture:archivesdefrance:seda:v2.0',
707
        'xml': 'http://www.w3.org/XML/1998/namespace',
708
709
710
711
712
713
714
715
716
717
        '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',
    }
718
    _root_attributes = {
719
720
721
722
723
724
725
726
        'targetNamespace': 'fr:gouv:culture:archivesdefrance:seda:v1.0',
        'attributeFormDefault': 'unqualified',
        'elementFormDefault': 'qualified',
        'version': '1.0',
    }

    concepts_language = 'seda-1'

727
728
729
730
731
732
    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'
733
        attributes = {'name': name}
734
735
        if extra_attributes is not None:
            attributes.update(extra_attributes)
736
737
        if fixed_value is not None:
            attributes['fixed'] = text_type(fixed_value)
738
739
        elif default_value is not None:
            attributes['default'] = text_type(default_value)
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
        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})
759
760
        for xattr in xsd_attributes:
            self.attribute_schema(attributes_parent, xattr)
761
762
        return children_parent

763
    def attribute_schema(self, parent, xattr):
764
        attrs = {'name': xattr.name}
765
766
767
768
769
770
        if xattr.cardinality is None:
            attrs['use'] = 'prohibited'
        elif xattr.cardinality == '1':
            attrs['use'] = 'required'
        else:
            attrs['use'] = 'optional'
771
        if not isinstance(xattr.fixed_value, (tuple, list)):
772
773
774
775
            attrs['type'] = xattr.qualified_type
            if isinstance(xattr.fixed_value, string_types):
                attrs['fixed'] = text_type(xattr.fixed_value)
        attribute_element = self.element('xsd:attribute', parent, attrs)
776
        if isinstance(xattr.fixed_value, (tuple, list)):
777
778
779
780
781
            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})
782

783
784
785
786
787
788
789
790
791
792
793
794
795
    # 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."""
796
        transfer_node = self.xsd_transfer_base(parent, archive_transfer)
797
        for archive_unit in archive_transfer.archive_units:
798
799
800
801
            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."""
802
803
804
805
806
        archive_node = self.element_schema(
            parent, 'Archive',
            cardinality=archive_unit.user_cardinality,
            documentation=archive_unit.user_annotation,
            xsd_attributes=[XAttr('Id', 'xsd:ID')],
807
            extra_attributes={'xml:id': xmlid(archive_unit)},
808
        )
809
810
        transfer = archive_unit.cw_adapt_to('ITreeBase').parent()
        self.xsd_archival_agreement(archive_node, transfer)
811
        # hard-coded description's language XXX fine, content language may be specified
812
813
814
        self.element_schema(archive_node, 'DescriptionLanguage', 'qdt:CodeLanguageType',
                            fixed_value='fra',
                            xsd_attributes=[LIST_VERSION_ID_2011])
815
        name_entity = self.archive_unit_name(archive_unit)
816
817
818
819
        self.element_schema(archive_node, 'Name', 'udt:TextType',
                            fixed_value=name_entity.title,
                            documentation=name_entity.user_annotation,
                            xsd_attributes=[XAttr('languageID', 'xsd:language')])
820
        content_entity = self.archive_unit_content(archive_unit)
821
822
        self.xsd_transferring_agency_archive_identifier(archive_node, content_entity,
                                                        'TransferringAgencyArchiveIdentifier')
823
        self.xsd_content_description(archive_node, content_entity)
824
        self.xsd_rules(archive_node, archive_unit)
825
826
        self.xsd_children(archive_node, archive_unit)

827
    archive_object_tag_name = 'ArchiveObject'
828

829
830
    def xsd_archive_object(self, parent, archive_unit):
        """Append XSD elements for the archive object to the given parent node."""
831
832
833
834
835
        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')],
836
            extra_attributes={'xml:id': xmlid(archive_unit)},
837
        )
838
839
840
841
842
843
844
845
846
847
848
849
850
        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)
851
        self.xsd_rules(ao_node, archive_unit)
852
853
854
855
856
857
        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."""
858
859
860
861
862
        document_node = self.element_schema(
            parent, 'Document',
            cardinality=data_object.user_cardinality,
            documentation=data_object.user_annotation,
            xsd_attributes=[XAttr('Id', 'xsd:ID')],
863
            extra_attributes={'xml:id': xmlid(data_object)},
864
        )
865

866
        self.xsd_system_id(document_node, data_object)
867
868
        self.xsd_attachment(document_node, data_object)
        self.xsd_date_created(document_node, data_object)
869
        self.xsd_integrity(document_node, data_object)
870
        self.xsd_document_type(document_node, data_object)
871

872
873
874
    # in SEDA 1 sub-archive units are exposed before data objects
    last_children_type = 'SEDABinaryDataObject'

875
876
877
878
    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.
        """
879
        for au_or_bdo in sorted(entity.cw_adapt_to('ITreeBase').iterchildren(),
880
                                key=lambda x: x.cw_etype == self.last_children_type):
881
882
883
884
885
886
            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)

887
888
    agencies_in_order = ('ArchivalAgency', 'TransferringAgency')

889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
    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())
904
        for agency_type in self.agencies_in_order:
905
906
907
            self.xsd_agency(transfer_node, agency_type)
        return transfer_node

908
909
    def xsd_attachment(self, parent, data_object):
        _safe_concept = partial(_safe_concept_value, concepts_language=self.concepts_language)
910
911

        format_id = data_object.format_id
912
        if format_id is not None:
913
914
            format_ids = sorted(set(_concept_value(concept, self.concepts_language)
                                    for concept in format_id.concepts))
915
916
        else:
            format_ids = []
917
        mime_type = data_object.mime_type
918
        if mime_type is not None:
919
920
            mime_types = sorted(set(_concept_value(concept, self.concepts_language)
                                    for concept in mime_type.concepts))
921
922
        else:
            mime_types = []
923
        encoding = data_object.encoding
924
        self.element_schema(parent, 'Attachment', 'qdt:ArchivesBinaryObjectType',
925
926
                            xsd_attributes=[
                                XAttr('format', 'clmDAFFileTypeCode:FileTypeCodeType',
927
928
                                      cardinality=format_id.user_cardinality,
                                      fixed_value=format_ids),
929
930
                                XAttr('encodingCode',
                                      'clm60133:CharacterSetEncodingCodeContentType',
931
932
                                      cardinality=_safe_cardinality(encoding),
                                      fixed_value=_safe_concept(encoding)),
933
                                XAttr('mimeCode', 'clmIANAMIMEMediaType:MIMEMediaTypeContentType',
934
935
                                      cardinality=mime_type.user_cardinality,
                                      fixed_value=mime_types),
936
937
938
939
940
                                XAttr('filename', 'xsd:string',
                                      cardinality='0..1',
                                      fixed_value=data_object.filename),
                                # hard-coded attributes
                                XAttr('characterSetCode',
941
942
943
944
                                      'clmIANACharacterSetCode:CharacterSetCodeContentType',
                                      cardinality=None),
                                XAttr('uri', 'xsd:anyURI',
                                      cardinality=None),
945
                            ])
946
947

    def xsd_date_created(self, parent, data_object):
948
949
        date_created = data_object.date_created_by_application
        if date_created:
950
            self.element_schema(parent, 'Creation', 'udt:DateTimeType',
951
952
953
                                cardinality=date_created.user_cardinality,
                                documentation=date_created.user_annotation)

954
    def xsd_document_type(self, parent, data_object):
955
956
957
958
959
        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]
960
        self.element_schema(parent, 'Type', 'qdt:CodeDocumentType',
961
                            fixed_value=_safe_concept_value(seq.type, self.concepts_language),
962
963
                            xsd_attributes=[LIST_VERSION_ID_2009])

964
965
966
967
968
969
970
971
972
973
    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())

974
975
976
977
978
979
980
981
982
    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')
983

984
985
986
987
988
989
990
991
992
    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())

993
994
995
996
997
998
999
1000
1001
1002
    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())

1003
1004
1005
1006
1007
1008
1009
1010
    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)

1011
1012
    appraisal_tag_name = 'AppraisalRule'

1013
1014
1015
    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)
1016
        ar_node = self.element_schema(parent, self.appraisal_tag_name,
1017
1018
1019
                                      cardinality=appraisal_rule.user_cardinality,
                                      documentation=appraisal_rule.user_annotation,
                                      xsd_attributes=[XAttr('Id', 'xsd:ID')])
1020
1021
        ar_code = appraisal_rule.final_action_concept
        ar_code_value = _concept_value(ar_code, self.concepts_language)
1022
1023
1024
        self.element_schema(ar_node, 'Code', 'qdt:CodeAppraisalType',
                            fixed_value=ar_code_value,
                            xsd_attributes=[LIST_VERSION_ID_2009])
1025
1026
1027

        rule = appraisal_rule.rules[0] if appraisal_rule.rules else None
        value = _concept_value(rule.rule_concept, self.concepts_language) if rule else None
1028
1029
1030
1031
        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')
1032
1033
1034
1035
1036

    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."""
1037
1038
1039
1040
        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')])
1041
1042
1043
        # 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
1044
1045
1046
1047
1048
        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')
1049
1050
1051

    def xsd_content_description(self, parent, content):
        """Append XSD elements for a description content to the given parent node"""
1052
1053
        cd_node = self.element_schema(parent, 'ContentDescription',
                                      xsd_attributes=[XAttr('Id', 'xsd:ID')])
1054
        self.xsd_description(cd_node, content)
1055
1056
1057
        self.xsd_description_level(cd_node, content.description_level_concept)
        self.xsd_language(cd_node, content)
        self.xsd_content_dates(cd_node, content)
1058
        self.xsd_custodial_history(cd_node, content)
1059
1060
1061
1062
        self.xsd_keywords(cd_node, content)
        self.xsd_originating_agency(cd_node, content)

    def xsd_language(self, parent, content):
1063
1064
        # XXX language is 0..1 in SEDA 2, 1..n in earlier version
        language = content.language.concept if content.language else None
1065
        self.element_schema(parent, 'Language', 'qdt:CodeLanguageType',
1066
                            fixed_value=_concept_value(language, self.concepts_language),
1067
                            xsd_attributes=[LIST_VERSION_ID_2009])
1068
1069

    def xsd_content_dates(self, parent, content):
1070
        for seda2_name, seda1_name in (('end', 'latest'), ('start', 'oldest')):
1071
1072
            date_entity = getattr(content, '%s_date' % seda2_name)
            if date_entity:
1073
                self.element_schema(parent, '%sDate' % seda1_name.capitalize(), 'udt:DateType',
1074
1075
                                    cardinality=date_entity.user_cardinality,
                                    documentation=date_entity.user_annotation)
1076

1077
1078
1079
    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)
1080
1081
1082
        self.element_schema(parent, 'DescriptionLevel', 'qdt:CodeDescriptionLevelType',
                            fixed_value=value,
                            xsd_attributes=[LIST_VERSION_ID_2009])
1083

1084
    def xsd_description(self, parent, content):
1085
        """Append XSD elements for a description to the given parent node"""
1086
1087
1088
1089
1090
1091
1092
1093
        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):
1094
        for keyword in content.keywords:
1095
1096
            self.xsd_keyword(parent, keyword)

1097
1098
1099
1100
    def xsd_custodial_history(self, parent, content):
        if content.custodial_history_items:
            ch_node = self.element_schema(parent, 'CustodialHistory',
                                          cardinality='0..1')
1101
            for item in content.custodial_history_items:
1102
                when_card = item.when.user_cardinality if item.when else None
1103
1104
1105
1106
1107
1108
1109
                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')],
1110
                    extra_attributes={'xml:id': xmlid(item)},
1111
                )
1112

1113
1114
1115
    def xsd_originating_agency(self, parent, content):
        if content.originating_agency:
            self.xsd_agency(parent, 'OriginatingAgency', content.originating_agency)
1116

1117
    def xsd_integrity(self, parent, data_object):
1118
        cardinality = integrity_cardinality(data_object)
1119
1120
        algorithm = data_object.seda_algorithm[0] if data_object.seda_algorithm else None
        self.element_schema(parent, 'Integrity', 'qdt:ArchivesHashcodeBinaryObjectType',
1121
                            cardinality=cardinality,
1122
1123
1124
1125
1126
                            xsd_attributes=[XAttr('algorithme', 'xsd:string',
                                                  cardinality='1',
                                                  fixed_value=_concept_value(
                                                      algorithm, self.concepts_language))])

1127
1128
1129
    # extracted from xsd_keyword to allow parametrization for SEDA 1.0 vs 0.2 generation
    kw_tag_name = 'Keyword'
    kw_content_tag_type = 'qdt:KeywordContentType'
1130
1131
    kw_content_tag_attributes = [XAttr('role', 'xsd:token'),
                                 XAttr('languageID', 'xsd:language')]
1132
1133
1134

    def xsd_keyword(self, parent, keyword):
        """Append XSD elements for a keyword to the given parent node"""
Sylvain Thénault's avatar