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 functools import total_ordering
18
19from spdx import config
20
21
22@total_ordering
23class ExternalDocumentRef(object):
24    """
25    External Document References entity that contains the following fields :
26    - external_document_id: A unique string containing letters, numbers, '.',
27        '-' or '+'.
28    - spdx_document_uri: The unique ID of the SPDX document being referenced.
29    - check_sum: The checksum of the referenced SPDX document.
30    """
31
32    def __init__(self, external_document_id=None, spdx_document_uri=None,
33                 check_sum=None):
34        self.external_document_id = external_document_id
35        self.spdx_document_uri = spdx_document_uri
36        self.check_sum = check_sum
37
38    def __eq__(self, other):
39        return (
40            isinstance(other, ExternalDocumentRef)
41            and self.external_document_id == other.external_document_id
42            and self.spdx_document_uri == other.spdx_document_uri
43            and self.check_sum == other.check_sum
44        )
45
46    def __lt__(self, other):
47        return (
48            (self.external_document_id, self.spdx_document_uri,
49             self.check_sum) <
50            (other.external_document_id, other.spdx_document_uri,
51             other.check_sum,)
52        )
53
54    def validate(self, messages):
55        """
56        Validate all fields of the ExternalDocumentRef class and update the
57        messages list with user friendly error messages for display.
58        """
59        messages = self.validate_ext_doc_id(messages)
60        messages = self.validate_spdx_doc_uri(messages)
61        messages = self.validate_checksum(messages)
62
63        return messages
64
65    def validate_ext_doc_id(self, messages):
66        if not self.external_document_id:
67            messages = messages + [
68                'ExternalDocumentRef has no External Document ID.'
69            ]
70
71        return messages
72
73    def validate_spdx_doc_uri(self, messages):
74        if not self.spdx_document_uri:
75            messages = messages + [
76                'ExternalDocumentRef has no SPDX Document URI.'
77            ]
78
79        return messages
80
81    def validate_checksum(self, messages):
82        if not self.check_sum:
83            messages = messages + ['ExternalDocumentRef has no Checksum.']
84
85        return messages
86
87
88def _add_parens(required, text):
89    """
90    Add parens around a license expression if `required` is True, otherwise
91    return `text` unmodified.
92    """
93    return '({})'.format(text) if required else text
94
95
96@total_ordering
97class License(object):
98    def __init__(self, full_name, identifier):
99        self._full_name = full_name
100        self._identifier = identifier
101
102    @classmethod
103    def from_identifier(cls, identifier):
104        """If identifier exists in config.LICENSE_MAP
105        the full_name is retrieved from it. Otherwise
106        the full_name is the same as the identifier.
107        """
108        if identifier in config.LICENSE_MAP.keys():
109            return cls(config.LICENSE_MAP[identifier], identifier)
110        else:
111            return cls(identifier, identifier)
112
113    @classmethod
114    def from_full_name(cls, full_name):
115        """
116        Returna new License for a full_name. If the full_name exists in
117        config.LICENSE_MAP the identifier is retrieved from it.
118        Otherwise the identifier is the same as the full_name.
119        """
120        if full_name in config.LICENSE_MAP.keys():
121            return cls(full_name, config.LICENSE_MAP[full_name])
122        else:
123            return cls(full_name, full_name)
124
125    @property
126    def url(self):
127        return "http://spdx.org/licenses/{0}".format(self.identifier)
128
129    @property
130    def full_name(self):
131        return self._full_name
132
133    @full_name.setter
134    def full_name(self, value):
135        self._full_name = value
136
137    @property
138    def identifier(self):
139        return self._identifier
140
141    def __eq__(self, other):
142        return (
143            isinstance(other, License)
144            and self.identifier == other.identifier
145            and self.full_name == other.full_name)
146
147    def __lt__(self, other):
148        return isinstance(other, License) and self.identifier < other.identifier
149
150    def __str__(self):
151        return self.identifier
152
153    def __hash__(self):
154        return self.identifier.__hash__()
155
156
157class LicenseConjunction(License):
158    """
159    A conjunction of two licenses.
160    """
161
162    def __init__(self, license_1, license_2):
163        self.license_1 = license_1
164        self.license_2 = license_2
165        super(LicenseConjunction, self).__init__(self.full_name, self.identifier)
166
167    @property
168    def full_name(self):
169        license_1_complex = type(self.license_1) == LicenseDisjunction
170        license_2_complex = type(self.license_2) == LicenseDisjunction
171
172        return '{0} AND {1}'.format(
173            _add_parens(license_1_complex, self.license_1.full_name),
174            _add_parens(license_2_complex, self.license_2.full_name))
175
176    @property
177    def identifier(self):
178        license_1_complex = type(self.license_1) == LicenseDisjunction
179        license_2_complex = type(self.license_2) == LicenseDisjunction
180
181        return '{0} AND {1}'.format(
182            _add_parens(license_1_complex, self.license_1.identifier),
183            _add_parens(license_2_complex, self.license_2.identifier))
184
185
186class LicenseDisjunction(License):
187    """
188    A disjunction of two licenses.
189    """
190
191    def __init__(self, license_1, license_2):
192        self.license_1 = license_1
193        self.license_2 = license_2
194        super(LicenseDisjunction, self).__init__(self.full_name, self.identifier)
195
196    @property
197    def full_name(self):
198        license_1_complex = type(self.license_1) == LicenseConjunction
199        license_2_complex = type(self.license_2) == LicenseConjunction
200
201        return '{0} OR {1}'.format(
202            _add_parens(license_1_complex, self.license_1.full_name),
203            _add_parens(license_2_complex, self.license_2.full_name))
204
205    @property
206    def identifier(self):
207        license_1_complex = type(self.license_1) == LicenseConjunction
208        license_2_complex = type(self.license_2) == LicenseConjunction
209
210        return '{0} OR {1}'.format(
211            _add_parens(license_1_complex, self.license_1.identifier),
212            _add_parens(license_2_complex, self.license_2.identifier))
213
214
215@total_ordering
216class ExtractedLicense(License):
217    """
218    Represent an ExtractedLicense with its additional attributes:
219    - text: Extracted text, str. Mandatory.
220    - cross_ref: list of cross references.
221    - comment: license comment, str.
222    - full_name: license name. str or utils.NoAssert.
223    """
224    def __init__(self, identifier):
225        super(ExtractedLicense, self).__init__(None, identifier)
226        self.text = None
227        self.cross_ref = []
228        self.comment = None
229
230    def __eq__(self, other):
231        return (
232            isinstance(other, ExtractedLicense)
233            and self.identifier == other.identifier
234            and self.full_name == other.full_name)
235
236    def __lt__(self, other):
237        return isinstance(other, ExtractedLicense) and self.identifier < other.identifier
238
239    def add_xref(self, ref):
240        self.cross_ref.append(ref)
241
242    def validate(self, messages):
243        if self.text is None:
244            messages = messages + ['ExtractedLicense text can not be None']
245
246        return messages
247
248
249class Document(object):
250    """
251    Represent an SPDX document with these fields:
252    - version: Spec version. Mandatory, one - Type: Version.
253    - data_license: SPDX-Metadata license. Mandatory, one. Type: License.
254    - name: Name of the document. Mandatory, one. Type: str.
255    - spdx_id: SPDX Identifier for the document to refer to itself in
256      relationship to other elements. Mandatory, one. Type: str.
257    - ext_document_references: External SPDX documents referenced within the
258        given SPDX document. Optional, one or many. Type: ExternalDocumentRef
259    - comment: Comments on the SPDX file, optional one. Type: str
260    - namespace: SPDX document specific namespace. Mandatory, one. Type: str
261    - creation_info: SPDX file creation info. Mandatory, one. Type: CreationInfo
262    - package: Package described by this document. Mandatory, one. Type: Package
263    - extracted_licenses: List of licenses extracted that are not part of the
264      SPDX license list. Optional, many. Type: ExtractedLicense.
265    - reviews: SPDX document review information, Optional zero or more.
266      Type: Review.
267    - annotations: SPDX document annotation information, Optional zero or more.
268      Type: Annotation.
269    - snippet: Snippet information. Optional zero or more. Type: Snippet.
270    """
271
272    def __init__(self, version=None, data_license=None, name=None, spdx_id=None,
273                 namespace=None, comment=None, package=None):
274        # avoid recursive impor
275        from spdx.creationinfo import CreationInfo
276        self.version = version
277        self.data_license = data_license
278        self.name = name
279        self.spdx_id = spdx_id
280        self.ext_document_references = []
281        self.comment = comment
282        self.namespace = namespace
283        self.creation_info = CreationInfo()
284        self.package = package
285        self.extracted_licenses = []
286        self.reviews = []
287        self.annotations = []
288        self.snippet = []
289
290    def add_review(self, review):
291        self.reviews.append(review)
292
293    def add_annotation(self, annotation):
294        self.annotations.append(annotation)
295
296    def add_extr_lic(self, lic):
297        self.extracted_licenses.append(lic)
298
299    def add_ext_document_reference(self, ext_doc_ref):
300        self.ext_document_references.append(ext_doc_ref)
301
302    def add_snippet(self, snip):
303        self.snippet.append(snip)
304
305    @property
306    def files(self):
307        return self.package.files
308
309    @files.setter
310    def files(self, value):
311        self.package.files = value
312
313    @property
314    def has_comment(self):
315        return self.comment is not None
316
317    def validate(self, messages):
318        """
319        Validate all fields of the document and update the
320        messages list with user friendly error messages for display.
321        """
322        messages = self.validate_version(messages)
323        messages = self.validate_data_lics(messages)
324        messages = self.validate_name(messages)
325        messages = self.validate_spdx_id(messages)
326        messages = self.validate_namespace(messages)
327        messages = self.validate_ext_document_references(messages)
328        messages = self.validate_creation_info(messages)
329        messages = self.validate_package(messages)
330        messages = self.validate_extracted_licenses(messages)
331        messages = self.validate_reviews(messages)
332        messages = self.validate_snippet(messages)
333
334        return messages
335
336    def validate_version(self, messages):
337        if self.version is None:
338            messages = messages + ['Document has no version.']
339
340        return messages
341
342    def validate_data_lics(self, messages):
343        if self.data_license is None:
344            messages = messages + ['Document has no data license.']
345        else:
346        # FIXME: REALLY? what if someone wants to use something else?
347            if self.data_license.identifier != 'CC0-1.0':
348                messages = messages + ['Document data license must be CC0-1.0.']
349
350        return messages
351
352    def validate_name(self, messages):
353        if self.name is None:
354            messages = messages + ['Document has no name.']
355
356        return messages
357
358    def validate_namespace(self, messages):
359        if self.namespace is None:
360            messages = messages + ['Document has no namespace.']
361
362        return messages
363
364    def validate_spdx_id(self, messages):
365        if self.spdx_id is None:
366            messages = messages + ['Document has no SPDX Identifier.']
367        else:
368            if not self.spdx_id.endswith('SPDXRef-DOCUMENT'):
369                messages = messages + [
370                    'Invalid Document SPDX Identifier value.'
371                ]
372
373        return messages
374
375    def validate_ext_document_references(self, messages):
376        for doc in self.ext_document_references:
377            if isinstance(doc, ExternalDocumentRef):
378                messages = doc.validate(messages)
379            else:
380                messages = list(messages) + [
381                    'External document references must be of the type '
382                    'spdx.document.ExternalDocumentRef and not ' + str(type(doc))
383                ]
384        return messages
385
386    def validate_reviews(self, messages):
387        for review in self.reviews:
388            messages = review.validate(messages)
389
390        return messages
391
392    def validate_annotations(self, messages):
393        for annotation in self.annotations:
394            messages = annotation.validate(messages)
395
396        return messages
397
398    def validate_snippet(self, messages=None):
399        for snippet in self.snippet:
400            messages = snippet.validate(messages)
401
402        return messages
403
404    def validate_creation_info(self, messages):
405        if self.creation_info is not None:
406            messages = self.creation_info.validate(messages)
407        else:
408            messages = messages + ['Document has no creation information.']
409
410        return messages
411
412    def validate_package(self, messages):
413        if self.package is not None:
414            messages = self.package.validate(messages)
415        else:
416            messages = messages + ['Document has no package.']
417
418        return messages
419
420    def validate_extracted_licenses(self, messages):
421        for lic in self.extracted_licenses:
422            if isinstance(lic, ExtractedLicense):
423                messages = lic.validate(messages)
424            else:
425                messages = messages + [
426                    'Document extracted licenses must be of type '
427                    'spdx.document.ExtractedLicense and not ' + type(lic)
428                ]
429        return messages
430