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