1
2# Copyright (c) 2014 Ahmed H. Ismail
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#     http://www.apache.org/licenses/LICENSE-2.0
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS,
9# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10# See the License for the specific language governing permissions and
11# limitations under the License.
12
13from __future__ import absolute_import
14from __future__ import print_function
15from __future__ import unicode_literals
16
17from rdflib import BNode
18from rdflib import Graph
19from rdflib import Literal
20from rdflib import Namespace
21from rdflib import RDF
22from rdflib import RDFS
23from rdflib import URIRef
24from rdflib.compare import to_isomorphic
25
26from spdx import file
27from spdx import document
28from spdx import config
29from spdx import utils
30from spdx.writers.tagvalue import InvalidDocumentError
31
32
33class BaseWriter(object):
34    """
35    Base class for all Writer classes.
36    Provide utility functions and stores shared fields.
37    """
38
39    def __init__(self, document, out):
40        self.document = document
41        self.out = out
42        self.doap_namespace = Namespace('http://usefulinc.com/ns/doap#')
43        self.spdx_namespace = Namespace("http://spdx.org/rdf/terms#")
44        self.graph = Graph()
45
46    def create_checksum_node(self, chksum):
47        """
48        Return a node representing spdx.checksum.
49        """
50        chksum_node = BNode()
51        type_triple = (chksum_node, RDF.type, self.spdx_namespace.Checksum)
52        self.graph.add(type_triple)
53        algorithm_triple = (chksum_node, self.spdx_namespace.algorithm, Literal(chksum.identifier))
54        self.graph.add(algorithm_triple)
55        value_triple = (chksum_node, self.spdx_namespace.checksumValue, Literal(chksum.value))
56        self.graph.add(value_triple)
57        return chksum_node
58
59    def to_special_value(self, value):
60        """
61        Return proper spdx term or Literal
62        """
63        if isinstance(value, utils.NoAssert):
64            return self.spdx_namespace.noassertion
65        elif isinstance(value, utils.SPDXNone):
66            return self.spdx_namespace.none
67        else:
68            return Literal(value)
69
70
71class LicenseWriter(BaseWriter):
72    """
73    Handle all License classes from spdx.document module.
74    """
75
76    def __init__(self, document, out):
77        super(LicenseWriter, self).__init__(document, out)
78
79    def licenses_from_tree_helper(self, current, licenses):
80        if (isinstance(current, (document.LicenseConjunction,
81                                 document.LicenseDisjunction))):
82            self.licenses_from_tree_helper(current.license_1, licenses)
83            self.licenses_from_tree_helper(current.license_2, licenses)
84        else:
85            licenses.add(self.create_license_helper(current))
86
87    def licenses_from_tree(self, tree):
88        """
89        Traverse conjunctions and disjunctions like trees and return a
90        set of all licenses in it as nodes.
91        """
92        # FIXME: this is unordered!
93        licenses = set()
94        self.licenses_from_tree_helper(tree, licenses)
95        return licenses
96
97    def create_conjunction_node(self, conjunction):
98        """
99        Return a node representing a conjunction of licenses.
100        """
101        node = BNode()
102        type_triple = (node, RDF.type, self.spdx_namespace.ConjunctiveLicenseSet)
103        self.graph.add(type_triple)
104        licenses = self.licenses_from_tree(conjunction)
105        for lic in licenses:
106            member_triple = (node, self.spdx_namespace.member, lic)
107            self.graph.add(member_triple)
108        return node
109
110    def create_disjunction_node(self, disjunction):
111        """
112        Return a node representing a disjunction of licenses.
113        """
114        node = BNode()
115        type_triple = (node, RDF.type, self.spdx_namespace.DisjunctiveLicenseSet)
116        self.graph.add(type_triple)
117        licenses = self.licenses_from_tree(disjunction)
118        for lic in licenses:
119            member_triple = (node, self.spdx_namespace.member, lic)
120            self.graph.add(member_triple)
121        return node
122
123    def create_license_helper(self, lic):
124        """
125        Handle single(no conjunction/disjunction) licenses.
126        Return the created node.
127        """
128        if isinstance(lic, document.ExtractedLicense):
129            return self.create_extracted_license(lic)
130        if lic.identifier.rstrip('+') in config.LICENSE_MAP:
131            return URIRef(lic.url)
132        else:
133            matches = [l for l in self.document.extracted_licenses if l.identifier == lic.identifier]
134            if len(matches) != 0:
135                return self.create_extracted_license(matches[0])
136            else:
137                raise InvalidDocumentError('Missing extracted license: {0}'.format(lic.identifier))
138
139    def create_extracted_license(self, lic):
140        """
141        Handle extracted license.
142        Return the license node.
143        """
144        licenses = list(self.graph.triples((None, self.spdx_namespace.licenseId, lic.identifier)))
145        if len(licenses) != 0:
146            return licenses[0][0]  # return subject in first triple
147        else:
148            license_node = BNode()
149            type_triple = (license_node, RDF.type, self.spdx_namespace.ExtractedLicensingInfo)
150            self.graph.add(type_triple)
151            ident_triple = (license_node, self.spdx_namespace.licenseId, Literal(lic.identifier))
152            self.graph.add(ident_triple)
153            text_triple = (license_node, self.spdx_namespace.extractedText, Literal(lic.text))
154            self.graph.add(text_triple)
155            if lic.full_name is not None:
156                name_triple = (license_node, self.spdx_namespace.licenseName, self.to_special_value(lic.full_name))
157                self.graph.add(name_triple)
158            for ref in lic.cross_ref:
159                triple = (license_node, RDFS.seeAlso, URIRef(ref))
160                self.graph.add(triple)
161            if lic.comment is not None:
162                comment_triple = (license_node, RDFS.comment, Literal(lic.comment))
163                self.graph.add(comment_triple)
164            return license_node
165
166    def create_license_node(self, lic):
167        """
168        Return a node representing a license.
169        Could be a single license (extracted or part of license list.) or
170        a conjunction/disjunction of licenses.
171        """
172        if isinstance(lic, document.LicenseConjunction):
173            return self.create_conjunction_node(lic)
174        elif isinstance(lic, document.LicenseDisjunction):
175            return self.create_disjunction_node(lic)
176        else:
177            return self.create_license_helper(lic)
178
179    def license_or_special(self, lic):
180        """
181        Check for special values spdx:none and spdx:noassertion.
182        Return the term for the special value or the result of passing
183        license to create_license_node.
184        """
185        if isinstance(lic, utils.NoAssert):
186            return self.spdx_namespace.noassertion
187        elif isinstance(lic, utils.SPDXNone):
188            return self.spdx_namespace.none
189        else:
190            return self.create_license_node(lic)
191
192
193class FileWriter(LicenseWriter):
194    """
195    Write spdx.file.File
196    """
197    FILE_TYPES = {
198        file.FileType.SOURCE: 'fileType_source',
199        file.FileType.OTHER: 'fileType_other',
200        file.FileType.BINARY: 'fileType_binary',
201        file.FileType.ARCHIVE: 'fileType_archive'
202    }
203
204    def __init__(self, document, out):
205        super(FileWriter, self).__init__(document, out)
206
207    def create_file_node(self, doc_file):
208        """
209        Create a node for spdx.file.
210        """
211        file_node = URIRef('http://www.spdx.org/files#{id}'.format(
212            id=str(doc_file.spdx_id)))
213        type_triple = (file_node, RDF.type, self.spdx_namespace.File)
214        self.graph.add(type_triple)
215
216        name_triple = (file_node, self.spdx_namespace.fileName, Literal(doc_file.name))
217        self.graph.add(name_triple)
218
219        if doc_file.has_optional_field('comment'):
220            comment_triple = (file_node, RDFS.comment, Literal(doc_file.comment))
221            self.graph.add(comment_triple)
222
223        if doc_file.has_optional_field('type'):
224            ftype = self.spdx_namespace[self.FILE_TYPES[doc_file.type]]
225            ftype_triple = (file_node, self.spdx_namespace.fileType, ftype)
226            self.graph.add(ftype_triple)
227
228        self.graph.add((file_node, self.spdx_namespace.checksum, self.create_checksum_node(doc_file.chk_sum)))
229
230        conc_lic_node = self.license_or_special(doc_file.conc_lics)
231        conc_lic_triple = (file_node, self.spdx_namespace.licenseConcluded, conc_lic_node)
232        self.graph.add(conc_lic_triple)
233
234        license_info_nodes = map(self.license_or_special, doc_file.licenses_in_file)
235        for lic in license_info_nodes:
236            triple = (file_node, self.spdx_namespace.licenseInfoInFile, lic)
237            self.graph.add(triple)
238
239        if doc_file.has_optional_field('license_comment'):
240            comment_triple = (file_node, self.spdx_namespace.licenseComments, Literal(doc_file.license_comment))
241            self.graph.add(comment_triple)
242
243        cr_text_node = self.to_special_value(doc_file.copyright)
244        cr_text_triple = (file_node, self.spdx_namespace.copyrightText, cr_text_node)
245        self.graph.add(cr_text_triple)
246
247        if doc_file.has_optional_field('notice'):
248            notice_triple = (file_node, self.spdx_namespace.noticeText, doc_file.notice)
249            self.graph.add(notice_triple)
250
251        contrib_nodes = map(lambda c: Literal(c), doc_file.contributors)
252        contrib_triples = [(file_node, self.spdx_namespace.fileContributor, node) for node in contrib_nodes]
253        for triple in contrib_triples:
254            self.graph.add(triple)
255
256        return file_node
257
258    def files(self):
259        """
260        Return list of file nodes.
261        """
262        return map(self.create_file_node, self.document.files)
263
264    def add_file_dependencies_helper(self, doc_file):
265        """
266        Handle dependencies for a single file.
267        - doc_file - instance of spdx.file.File.
268        """
269        subj_triples = list(self.graph.triples((None, self.spdx_namespace.fileName, Literal(doc_file.name))))
270        if len(subj_triples) != 1:
271            raise InvalidDocumentError('Could not find dependency subject {0}'.format(doc_file.name))
272        subject_node = subj_triples[0][0]
273        for dependency in doc_file.dependencies:
274            dep_triples = list(self.graph.triples((None, self.spdx_namespace.fileName, Literal(dependency))))
275            if len(dep_triples) == 1:
276                dep_node = dep_triples[0][0]
277                dep_triple = (subject_node, self.spdx_namespace.fileDependency, dep_node)
278                self.graph.add(dep_triple)
279            else:
280                print('Warning could not resolve file dependency {0} -> {1}'.format(doc_file.name, dependency))
281
282    def add_file_dependencies(self):
283        """
284        Add file dependencies to the graph.
285        Called after all files have been added.
286        """
287        for doc_file in self.document.files:
288            self.add_file_dependencies_helper(doc_file)
289
290
291class SnippetWriter(LicenseWriter):
292
293    """
294    Write spdx.snippet.Snippet
295    """
296
297    def __init__(self, document, out):
298        super(SnippetWriter, self).__init__(document, out)
299
300    def create_snippet_node(self, snippet):
301        """
302        Return a snippet node.
303        """
304        snippet_node = URIRef('http://spdx.org/rdf/terms/Snippet#' + snippet.spdx_id)
305        type_triple = (snippet_node, RDF.type, self.spdx_namespace.Snippet)
306        self.graph.add(type_triple)
307
308        if snippet.has_optional_field('comment'):
309            comment_triple = (snippet_node, RDFS.comment, Literal(snippet.comment))
310            self.graph.add(comment_triple)
311
312        if snippet.has_optional_field('name'):
313            name_triple = (snippet_node, self.spdx_namespace.name, Literal(snippet.name))
314            self.graph.add(name_triple)
315
316        if snippet.has_optional_field('license_comment'):
317            lic_comment_triple = (snippet_node, self.spdx_namespace.licenseComments,
318                                  Literal(snippet.license_comment))
319            self.graph.add(lic_comment_triple)
320
321        cr_text_node = self.to_special_value(snippet.copyright)
322        cr_text_triple = (snippet_node, self.spdx_namespace.copyrightText, cr_text_node)
323        self.graph.add(cr_text_triple)
324
325        snip_from_file_triple = (snippet_node, self.spdx_namespace.snippetFromFile,
326                                 Literal(snippet.snip_from_file_spdxid))
327        self.graph.add(snip_from_file_triple)
328
329        conc_lic_node = self.license_or_special(snippet.conc_lics)
330        conc_lic_triple = (
331            snippet_node, self.spdx_namespace.licenseConcluded, conc_lic_node)
332        self.graph.add(conc_lic_triple)
333
334        license_info_nodes = map(self.license_or_special,
335                                 snippet.licenses_in_snippet)
336        for lic in license_info_nodes:
337            triple = (
338            snippet_node, self.spdx_namespace.licenseInfoInSnippet, lic)
339            self.graph.add(triple)
340
341        return snippet_node
342
343    def snippets(self):
344        """
345        Return list of snippet nodes.
346        """
347        return map(self.create_snippet_node, self.document.snippet)
348
349
350class ReviewInfoWriter(BaseWriter):
351
352    """
353    Write spdx.review.Review
354    """
355
356    def __init__(self, document, out):
357        super(ReviewInfoWriter, self).__init__(document, out)
358
359    def create_review_node(self, review):
360        """
361        Return a review node.
362        """
363        review_node = BNode()
364        type_triple = (review_node, RDF.type, self.spdx_namespace.Review)
365        self.graph.add(type_triple)
366
367        reviewer_node = Literal(review.reviewer.to_value())
368        self.graph.add((review_node, self.spdx_namespace.reviewer, reviewer_node))
369        reviewed_date_node = Literal(review.review_date_iso_format)
370        reviewed_triple = (review_node, self.spdx_namespace.reviewDate, reviewed_date_node)
371        self.graph.add(reviewed_triple)
372        if review.has_comment:
373            comment_node = Literal(review.comment)
374            comment_triple = (review_node, RDFS.comment, comment_node)
375            self.graph.add(comment_triple)
376
377        return review_node
378
379    def reviews(self):
380        "Returns a list of review nodes"
381        return map(self.create_review_node, self.document.reviews)
382
383
384class AnnotationInfoWriter(BaseWriter):
385    """
386    Write spdx.annotation.Annotation
387    """
388
389    def __init__(self, document, out):
390        super(AnnotationInfoWriter, self).__init__(document, out)
391
392    def create_annotation_node(self, annotation):
393        """
394        Return an annotation node.
395        """
396        annotation_node = URIRef(str(annotation.spdx_id))
397        type_triple = (annotation_node, RDF.type, self.spdx_namespace.Annotation)
398        self.graph.add(type_triple)
399
400        annotator_node = Literal(annotation.annotator.to_value())
401        self.graph.add((annotation_node, self.spdx_namespace.annotator, annotator_node))
402        annotation_date_node = Literal(annotation.annotation_date_iso_format)
403        annotation_triple = (annotation_node, self.spdx_namespace.annotationDate, annotation_date_node)
404        self.graph.add(annotation_triple)
405        if annotation.has_comment:
406            comment_node = Literal(annotation.comment)
407            comment_triple = (annotation_node, RDFS.comment, comment_node)
408            self.graph.add(comment_triple)
409        annotation_type_node = Literal(annotation.annotation_type)
410        annotation_type_triple = (annotation_node, self.spdx_namespace.annotationType, annotation_type_node)
411        self.graph.add(annotation_type_triple)
412
413        return annotation_node
414
415    def annotations(self):
416        """
417        Return a list of annotation nodes
418        """
419        return map(self.create_annotation_node, self.document.annotations)
420
421
422class CreationInfoWriter(BaseWriter):
423
424    """
425    Write class spdx.creationinfo.CreationInfo
426    """
427
428    def __init__(self, document, out):
429        super(CreationInfoWriter, self).__init__(document, out)
430
431    def creators(self):
432        """
433        Return a list of creator nodes.
434        Note: Does not add anything to the graph.
435        """
436        return map(lambda c: Literal(c.to_value()), self.document.creation_info.creators)
437
438    def create_creation_info(self):
439        """
440        Add and return a creation info node to graph
441        """
442        ci_node = BNode()
443        # Type property
444        type_triple = (ci_node, RDF.type, self.spdx_namespace.CreationInfo)
445        self.graph.add(type_triple)
446
447        created_date = Literal(self.document.creation_info.created_iso_format)
448        created_triple = (ci_node, self.spdx_namespace.created, created_date)
449        self.graph.add(created_triple)
450
451        creators = self.creators()
452        for creator in creators:
453            self.graph.add((ci_node, self.spdx_namespace.creator, creator))
454
455        if self.document.creation_info.has_comment:
456            comment_node = Literal(self.document.creation_info.comment)
457            comment_triple = (ci_node, RDFS.comment, comment_node)
458            self.graph.add(comment_triple)
459
460        return ci_node
461
462
463class ExternalDocumentRefWriter(BaseWriter):
464    """
465    Write class spdx.external_document_ref.ExternalDocumentRef
466    """
467
468    def __init__(self, document, out):
469        super(ExternalDocumentRefWriter, self).__init__(document, out)
470
471    def create_external_document_ref_node(self, ext_document_references):
472        """
473        Add and return a creation info node to graph
474        """
475        ext_doc_ref_node = BNode()
476        type_triple = (ext_doc_ref_node, RDF.type, self.spdx_namespace.ExternalDocumentRef)
477        self.graph.add(type_triple)
478
479        ext_doc_id = Literal(
480            ext_document_references.external_document_id)
481        ext_doc_id_triple = (
482            ext_doc_ref_node, self.spdx_namespace.externalDocumentId, ext_doc_id)
483        self.graph.add(ext_doc_id_triple)
484
485        doc_uri = Literal(
486            ext_document_references.spdx_document_uri)
487        doc_uri_triple = (
488            ext_doc_ref_node, self.spdx_namespace.spdxDocument, doc_uri)
489        self.graph.add(doc_uri_triple)
490
491        checksum_node = self.create_checksum_node(
492            ext_document_references.check_sum)
493        self.graph.add(
494            (ext_doc_ref_node, self.spdx_namespace.checksum, checksum_node))
495
496        return ext_doc_ref_node
497
498    def ext_doc_refs(self):
499        """
500        Return a list of review nodes
501        """
502        return map(self.create_external_document_ref_node,
503                   self.document.ext_document_references)
504
505
506class PackageWriter(LicenseWriter):
507
508    """
509    Write spdx.package.Package
510    """
511
512    def __init__(self, document, out):
513        super(PackageWriter, self).__init__(document, out)
514
515    def package_verif_node(self, package):
516        """
517        Return a node representing package verification code.
518        """
519        verif_node = BNode()
520        type_triple = (verif_node, RDF.type, self.spdx_namespace.PackageVerificationCode)
521        self.graph.add(type_triple)
522        value_triple = (verif_node, self.spdx_namespace.packageVerificationCodeValue, Literal(package.verif_code))
523        self.graph.add(value_triple)
524        excl_file_nodes = map(
525            lambda excl: Literal(excl), package.verif_exc_files)
526        excl_predicate = self.spdx_namespace.packageVerificationCodeExcludedFile
527        excl_file_triples = [(verif_node, excl_predicate, xcl_file) for xcl_file in excl_file_nodes]
528        for trp in excl_file_triples:
529            self.graph.add(trp)
530        return verif_node
531
532    def handle_package_literal_optional(self, package, package_node, predicate, field):
533        """
534        Check if optional field is set.
535        If so it adds the triple (package_node, predicate, $) to the graph.
536        Where $ is a literal or special value term of the value of the field.
537        """
538        if package.has_optional_field(field):
539            value = getattr(package, field, None)
540            value_node = self.to_special_value(value)
541            triple = (package_node, predicate, value_node)
542            self.graph.add(triple)
543
544    def handle_pkg_optional_fields(self, package, package_node):
545        """
546        Write package optional fields.
547        """
548        self.handle_package_literal_optional(package, package_node, self.spdx_namespace.versionInfo, 'version')
549        self.handle_package_literal_optional(package, package_node, self.spdx_namespace.packageFileName, 'file_name')
550        self.handle_package_literal_optional(package, package_node, self.spdx_namespace.supplier, 'supplier')
551        self.handle_package_literal_optional(package, package_node, self.spdx_namespace.originator, 'originator')
552        self.handle_package_literal_optional(package, package_node, self.spdx_namespace.sourceInfo, 'source_info')
553        self.handle_package_literal_optional(package, package_node, self.spdx_namespace.licenseComments, 'license_comment')
554        self.handle_package_literal_optional(package, package_node, self.spdx_namespace.summary, 'summary')
555        self.handle_package_literal_optional(package, package_node, self.spdx_namespace.description, 'description')
556        self.handle_package_literal_optional(package, package_node, self.spdx_namespace.comment, 'comment')
557        self.handle_package_literal_optional(package, package_node, self.spdx_namespace.filesAnalyzed, 'files_analyzed')
558
559
560        if package.has_optional_field('check_sum'):
561            checksum_node = self.create_checksum_node(package.check_sum)
562            self.graph.add((package_node, self.spdx_namespace.checksum, checksum_node))
563
564        if package.has_optional_field('homepage'):
565            homepage_node = URIRef(self.to_special_value(package.homepage))
566            homepage_triple = (package_node, self.doap_namespace.homepage, homepage_node)
567            self.graph.add(homepage_triple)
568
569    def create_package_node(self, package):
570        """
571        Return a Node representing the package.
572        Files must have been added to the graph before this method is called.
573        """
574        package_node = URIRef('http://www.spdx.org/tools#SPDXRef-Package')
575        type_triple = (package_node, RDF.type, self.spdx_namespace.Package)
576        self.graph.add(type_triple)
577        # Package SPDXID
578        if package.spdx_id:
579            pkg_spdx_id = URIRef(package.spdx_id)
580            pkg_spdx_id_triple = (package_node, self.spdx_namespace.Package,
581                                  pkg_spdx_id)
582            self.graph.add(pkg_spdx_id_triple)
583        # Handle optional fields:
584        self.handle_pkg_optional_fields(package, package_node)
585        # package name
586        name_triple = (package_node, self.spdx_namespace.name, Literal(package.name))
587        self.graph.add(name_triple)
588        # Package download location
589        down_loc_node = (package_node, self.spdx_namespace.downloadLocation, self.to_special_value(package.download_location))
590        self.graph.add(down_loc_node)
591        # Handle package verification
592        verif_node = self.package_verif_node(package)
593        verif_triple = (package_node, self.spdx_namespace.packageVerificationCode, verif_node)
594        self.graph.add(verif_triple)
595        # Handle concluded license
596        conc_lic_node = self.license_or_special(package.conc_lics)
597        conc_lic_triple = (package_node, self.spdx_namespace.licenseConcluded, conc_lic_node)
598        self.graph.add(conc_lic_triple)
599        # Handle declared license
600        decl_lic_node = self.license_or_special(package.license_declared)
601        decl_lic_triple = (package_node, self.spdx_namespace.licenseDeclared, decl_lic_node)
602        self.graph.add(decl_lic_triple)
603        # Package licenses from files
604        licenses_from_files_nodes = map(lambda el: self.license_or_special(el), package.licenses_from_files)
605        lic_from_files_predicate = self.spdx_namespace.licenseInfoFromFiles
606        lic_from_files_triples = [(package_node, lic_from_files_predicate, node) for node in licenses_from_files_nodes]
607        for triple in lic_from_files_triples:
608            self.graph.add(triple)
609        # Copyright Text
610        cr_text_node = self.to_special_value(package.cr_text)
611        cr_text_triple = (package_node, self.spdx_namespace.copyrightText, cr_text_node)
612        self.graph.add(cr_text_triple)
613        # Handle files
614        self.handle_package_has_file(package, package_node)
615        return package_node
616
617    def packages(self):
618        """
619        Return a node that represents the package in the graph.
620        Call this function to write package info.
621        """
622        # TODO: In the future this may be a list to support SPDX 2.0
623        return self.create_package_node(self.document.package)
624
625    def handle_package_has_file_helper(self, pkg_file):
626        """
627        Return node representing pkg_file
628        pkg_file should be instance of spdx.file.
629        """
630        nodes = list(self.graph.triples((None, self.spdx_namespace.fileName, Literal(pkg_file.name))))
631        if len(nodes) == 1:
632            return nodes[0][0]
633        else:
634            raise InvalidDocumentError('handle_package_has_file_helper could not' +
635                                       ' find file node for file: {0}'.format(pkg_file.name))
636
637    def handle_package_has_file(self, package, package_node):
638        """
639        Add hasFile triples to graph.
640        Must be called after files have been added.
641        """
642        file_nodes = map(self.handle_package_has_file_helper, package.files)
643        triples = [(package_node, self.spdx_namespace.hasFile, node) for node in file_nodes]
644        for triple in triples:
645            self.graph.add(triple)
646
647
648class PackageExternalRefWriter(BaseWriter):
649    """
650    Write class spdx.package.ExternalPackageRef
651    """
652
653    def __init__(self, document, out):
654        super(PackageExternalRefWriter, self).__init__(document, out)
655
656    def create_package_external_ref_node(self, pkg_ext_refs):
657        """
658        Add and return an external package reference node to graph.
659        """
660        pkg_ext_ref_node = BNode()
661        pkg_ext_ref_triple = (pkg_ext_ref_node, RDF.type, self.spdx_namespace.ExternalRef)
662        self.graph.add(pkg_ext_ref_triple)
663
664        pkg_ext_ref_category = Literal(pkg_ext_refs.category)
665        pkg_ext_ref_category_triple = (
666            pkg_ext_ref_node, self.spdx_namespace.referenceCategory, pkg_ext_ref_category)
667        self.graph.add(pkg_ext_ref_category_triple)
668
669        pkg_ext_ref_type = Literal(pkg_ext_refs.pkg_ext_ref_type)
670        pkg_ext_ref_type_triple = (
671            pkg_ext_ref_node, self.spdx_namespace.referenceType, pkg_ext_ref_type)
672        self.graph.add(pkg_ext_ref_type_triple)
673
674        pkg_ext_ref_locator = Literal(pkg_ext_refs.locator)
675        pkg_ext_ref_locator_triple = (
676            pkg_ext_ref_node, self.spdx_namespace.referenceLocator, pkg_ext_ref_locator)
677        self.graph.add(pkg_ext_ref_locator_triple)
678
679        pkg_ext_ref_comment = Literal(pkg_ext_refs.comment)
680        pkg_ext_ref_comment_triple = (
681            pkg_ext_ref_node, RDFS.comment, pkg_ext_ref_comment)
682        self.graph.add(pkg_ext_ref_comment_triple)
683
684        return pkg_ext_ref_node
685
686    def pkg_ext_refs(self):
687        """
688        Return a list of package external references.
689        """
690        return map(self.create_package_external_ref_node,
691                   self.document.package.pkg_ext_refs)
692
693
694class Writer(CreationInfoWriter, ReviewInfoWriter, FileWriter, PackageWriter,
695            PackageExternalRefWriter, ExternalDocumentRefWriter, AnnotationInfoWriter,
696            SnippetWriter):
697    """
698    Warpper for other writers to write all fields of spdx.document.Document
699    Call `write()` to start writing.
700    """
701
702    def __init__(self, document, out):
703        """
704        - document is spdx.document instance that will be written.
705        - out is a file-like object that will be written to.
706        """
707        super(Writer, self).__init__(document, out)
708
709    def create_doc(self):
710        """
711        Add and return the root document node to graph.
712        """
713        doc_node = URIRef('http://www.spdx.org/tools#SPDXRef-DOCUMENT')
714        # Doc type
715        self.graph.add((doc_node, RDF.type, self.spdx_namespace.SpdxDocument))
716        # Version
717        vers_literal = Literal(str(self.document.version))
718        self.graph.add((doc_node, self.spdx_namespace.specVersion, vers_literal))
719        # Data license
720        data_lics = URIRef(self.document.data_license.url)
721        self.graph.add((doc_node, self.spdx_namespace.dataLicense, data_lics))
722        if self.document.name:
723            doc_name = URIRef(self.document.name)
724            self.graph.add((doc_node, self.spdx_namespace.name, doc_name))
725        return doc_node
726
727    def write(self):
728        doc_node = self.create_doc()
729        # Add creation info
730        creation_info_node = self.create_creation_info()
731        ci_triple = (doc_node, self.spdx_namespace.creationInfo, creation_info_node)
732        self.graph.add(ci_triple)
733        # Add review info
734        review_nodes = self.reviews()
735        for review in review_nodes:
736            self.graph.add((doc_node, self.spdx_namespace.reviewed, review))
737        # Add external document references info
738        ext_doc_ref_nodes = self.ext_doc_refs()
739        for ext_doc_ref in ext_doc_ref_nodes:
740            ext_doc_ref_triple = (doc_node,
741                                  self.spdx_namespace.externalDocumentRef,
742                                  ext_doc_ref)
743            self.graph.add(ext_doc_ref_triple)
744        # Add extracted licenses
745        licenses = map(
746            self.create_extracted_license, self.document.extracted_licenses)
747        for lic in licenses:
748            self.graph.add((doc_node, self.spdx_namespace.hasExtractedLicensingInfo, lic))
749        # Add files
750        files = self.files()
751        for file_node in files:
752            self.graph.add((doc_node, self.spdx_namespace.referencesFile, file_node))
753        self.add_file_dependencies()
754        # Add package
755        package_node = self.packages()
756        package_triple = (doc_node, self.spdx_namespace.describesPackage, package_node)
757        self.graph.add(package_triple)
758        # Add snippet
759        snippet_nodes = self.snippets()
760        for snippet in snippet_nodes:
761            self.graph.add((doc_node, self.spdx_namespace.Snippet, snippet))
762
763        # normalize the graph to ensure that the sort order is stable
764        self.graph = to_isomorphic(self.graph)
765
766        # Write file
767        self.graph.serialize(self.out, 'pretty-xml', encoding='utf-8')
768
769
770def write_document(document, out, validate=True):
771    """
772    Write an SPDX RDF document.
773    - document - spdx.document instance.
774    - out - file like object that will be written to.
775    Optionally `validate` the document before writing and raise
776    InvalidDocumentError if document.validate returns False.
777    """
778
779    if validate:
780        messages = []
781        messages = document.validate(messages)
782        if messages:
783            raise InvalidDocumentError(messages)
784
785    writer = Writer(document, out)
786    writer.write()
787