3434from .._internal .compare import ComparableTuple as _ComparableTuple
3535from ..exception .model import MutuallyExclusivePropertiesException
3636from ..exception .serialization import CycloneDxDeserializationException
37+ from ..schema import SchemaVersion
3738from ..schema .schema import SchemaVersion1Dot5 , SchemaVersion1Dot6 , SchemaVersion1Dot7
3839from . import AttachedText , Property , XsUri
3940from .bom_ref import BomRef
@@ -278,6 +279,123 @@ def __repr__(self) -> str:
278279 return f'<License id={ self ._id !r} , name={ self ._name !r} >'
279280
280281
282+ @serializable .serializable_class (ignore_unknown_during_deserialization = True )
283+ class LicenseExpressionDetails :
284+ """
285+ This is our internal representation of the ``licenseExpressionDetailedType`` complex type that specifies the details
286+ and attributes related to a software license identifier within a CycloneDX BOM document.
287+
288+ .. note::
289+ Introduced in CycloneDX v1.7
290+
291+
292+ .. note::
293+ See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.7/xml/#type_licenseExpressionDetailedType
294+ """
295+
296+ def __init__ (
297+ self , license_identifier : str , * ,
298+ bom_ref : Optional [Union [str , BomRef ]] = None ,
299+ text : Optional [AttachedText ] = None ,
300+ url : Optional [XsUri ] = None ,
301+ ) -> None :
302+ self ._bom_ref = _bom_ref_from_str (bom_ref )
303+ self .license_identifier = license_identifier
304+ self .text = text
305+ self .url = url
306+
307+ @property
308+ @serializable .xml_name ('license-identifier' )
309+ @serializable .xml_string (serializable .XmlStringSerializationType .NORMALIZED_STRING )
310+ @serializable .xml_attribute ()
311+ def license_identifier (self ) -> str :
312+ """
313+ A valid SPDX license identifier. Refer to https://spdx.org/specifications for syntax requirements.
314+ This field serves as the primary key, which uniquely identifies each record.
315+
316+ Example values:
317+ - "Apache-2.0",
318+ - "GPL-3.0-only WITH Classpath-exception-2.0"
319+ - "LicenseRef-my-custom-license"
320+
321+ Returns:
322+ `str`
323+ """
324+ return self ._license_identifier
325+
326+ @license_identifier .setter
327+ def license_identifier (self , license_identifier : str ) -> None :
328+ self ._license_identifier = license_identifier
329+
330+ @property
331+ @serializable .json_name ('bom-ref' )
332+ @serializable .type_mapping (BomRef )
333+ @serializable .xml_attribute ()
334+ @serializable .xml_name ('bom-ref' )
335+ def bom_ref (self ) -> BomRef :
336+ """
337+ An identifier which can be used to reference the license elsewhere in the BOM. Every bom-ref MUST be
338+ unique within the BOM.
339+ Value SHOULD not start with the BOM-Link intro 'urn:cdx:' to avoid conflicts with BOM-Links.
340+
341+ Returns:
342+ `BomRef`
343+ """
344+ return self ._bom_ref
345+
346+ @property
347+ @serializable .xml_sequence (1 )
348+ def text (self ) -> Optional [AttachedText ]:
349+ """
350+ A way to include the textual content of the license.
351+
352+ Returns:
353+ `AttachedText` else `None`
354+ """
355+ return self ._text
356+
357+ @text .setter
358+ def text (self , text : Optional [AttachedText ]) -> None :
359+ self ._text = text
360+
361+ @property
362+ @serializable .xml_sequence (2 )
363+ def url (self ) -> Optional [XsUri ]:
364+ """
365+ The URL to the license file. If specified, a 'license' externalReference should also be specified for
366+ completeness.
367+
368+ Returns:
369+ `XsUri` or `None`
370+ """
371+ return self ._url
372+
373+ @url .setter
374+ def url (self , url : Optional [XsUri ]) -> None :
375+ self ._url = url
376+
377+ def __comparable_tuple (self ) -> _ComparableTuple :
378+ return _ComparableTuple ((
379+ self .bom_ref .value , self .license_identifier , self .url , self .text ,
380+ ))
381+
382+ def __eq__ (self , other : object ) -> bool :
383+ if isinstance (other , LicenseExpressionDetails ):
384+ return self .__comparable_tuple () == other .__comparable_tuple ()
385+ return False
386+
387+ def __lt__ (self , other : object ) -> bool :
388+ if isinstance (other , LicenseExpressionDetails ):
389+ return self .__comparable_tuple () < other .__comparable_tuple ()
390+ return NotImplemented
391+
392+ def __hash__ (self ) -> int :
393+ return hash (self .__comparable_tuple ())
394+
395+ def __repr__ (self ) -> str :
396+ return f'<LicenseExpressionDetails bom-ref={ self .bom_ref !r} , license_identifier={ self .license_identifier } >'
397+
398+
281399@serializable .serializable_class (
282400 name = 'expression' ,
283401 ignore_unknown_during_deserialization = True
@@ -296,10 +414,12 @@ def __init__(
296414 self , value : str , * ,
297415 bom_ref : Optional [Union [str , BomRef ]] = None ,
298416 acknowledgement : Optional [LicenseAcknowledgement ] = None ,
417+ details : Optional [Iterable [LicenseExpressionDetails ]] = None ,
299418 ) -> None :
300419 self ._bom_ref = _bom_ref_from_str (bom_ref )
301420 self ._value = value
302421 self ._acknowledgement = acknowledgement
422+ self .details = details or []
303423
304424 @property
305425 @serializable .view (SchemaVersion1Dot5 )
@@ -362,11 +482,30 @@ def acknowledgement(self) -> Optional[LicenseAcknowledgement]:
362482 def acknowledgement (self , acknowledgement : Optional [LicenseAcknowledgement ]) -> None :
363483 self ._acknowledgement = acknowledgement
364484
485+ @property
486+ @serializable .json_name ('expressionDetails' )
487+ @serializable .view (SchemaVersion1Dot7 )
488+ @serializable .xml_array (serializable .XmlArraySerializationType .FLAT , child_name = 'details' )
489+ @serializable .xml_sequence (1 )
490+ def details (self ) -> 'SortedSet[LicenseExpressionDetails]' :
491+ """
492+ Details for parts of the expression.
493+
494+ Returns:
495+ Set of `LicenseExpressionDetails`
496+ """
497+ return self ._details
498+
499+ @details .setter
500+ def details (self , details : Iterable [LicenseExpressionDetails ]) -> None :
501+ self ._details = SortedSet (details )
502+
365503 def __comparable_tuple (self ) -> _ComparableTuple :
366504 return _ComparableTuple ((
367505 self ._acknowledgement ,
368506 self ._value ,
369507 self ._bom_ref .value ,
508+ _ComparableTuple (self .details ),
370509 ))
371510
372511 def __hash__ (self ) -> int :
@@ -431,6 +570,38 @@ class LicenseRepository(SortedSet):
431570class _LicenseRepositorySerializationHelper (serializable .helpers .BaseHelper ):
432571 """ THIS CLASS IS NON-PUBLIC API """
433572
573+ @staticmethod
574+ def __supports_expression_details (view : Any ) -> bool :
575+ try :
576+ return view is not None and view ().schema_version_enum >= SchemaVersion .V1_7
577+ except Exception : # pragma: no cover
578+ return False
579+
580+ @staticmethod
581+ def __xml_normalize_license_expression_detailed (
582+ license_expression : LicenseExpression ,
583+ view : Optional [type [serializable .ViewType ]],
584+ xmlns : Optional [str ]
585+ ) -> Element :
586+ elem : Element = license_expression .as_xml ( # type:ignore[attr-defined]
587+ view_ = view , as_string = False , element_name = 'expression-detailed' , xmlns = xmlns )
588+ elem .set (f'{{{ xmlns } }}expression' if xmlns else 'expression' , license_expression .value )
589+ elem .text = None
590+ return elem
591+
592+ @staticmethod
593+ def __xml_denormalize_license_expression_detailed (
594+ li : Element ,
595+ default_ns : Optional [str ]
596+ ) -> LicenseExpression :
597+ expression_value = li .get ('expression' )
598+ if not expression_value :
599+ raise CycloneDxDeserializationException (f'unexpected content: { li !r} ' )
600+ license_expression : LicenseExpression = LicenseExpression .from_xml ( # type:ignore[attr-defined]
601+ li , default_ns )
602+ license_expression .value = expression_value
603+ return license_expression
604+
434605 @classmethod
435606 def json_normalize (cls , o : LicenseRepository , * ,
436607 view : Optional [type [serializable .ViewType ]],
@@ -482,8 +653,13 @@ def xml_normalize(cls, o: LicenseRepository, *,
482653 # mixed license expression and license? this is an invalid constellation according to schema!
483654 # see https://github.com/CycloneDX/specification/pull/205
484655 # but models need to allow it for backwards compatibility with JSON CDX < 1.5
485- elem .append (expression .as_xml ( # type:ignore[attr-defined]
486- view_ = view , as_string = False , element_name = 'expression' , xmlns = xmlns ))
656+ if expression .details and cls .__supports_expression_details (view ):
657+ elem .append (cls .__xml_normalize_license_expression_detailed (expression , view , xmlns ))
658+ else :
659+ if expression .details :
660+ warn ('LicenseExpression details are not supported in schema versions < 1.7; skipping serialization' )
661+ elem .append (expression .as_xml ( # type:ignore[attr-defined]
662+ view_ = view , as_string = False , element_name = 'expression' , xmlns = xmlns ))
487663 else :
488664 elem .extend (
489665 li .as_xml ( # type:ignore[attr-defined]
@@ -506,6 +682,8 @@ def xml_denormalize(cls, o: Element,
506682 elif tag == 'expression' :
507683 repo .add (LicenseExpression .from_xml ( # type:ignore[attr-defined]
508684 li , default_ns ))
685+ elif tag == 'expression-detailed' :
686+ repo .add (cls .__xml_denormalize_license_expression_detailed (li , default_ns ))
509687 else :
510688 raise CycloneDxDeserializationException (f'unexpected: { li !r} ' )
511689 return repo
0 commit comments