2727import java .util .Optional ;
2828import java .util .Stack ;
2929
30+ import javax .annotation .Nullable ;
31+
32+ import org .spdx .core .DefaultModelStore ;
3033import org .spdx .core .IModelCopyManager ;
3134import org .spdx .core .InvalidSPDXAnalysisException ;
3235import org .spdx .library .LicenseInfoFactory ;
4548import org .spdx .library .model .v2 .license .SpdxNoAssertionLicense ;
4649import org .spdx .library .model .v2 .license .SpdxNoneLicense ;
4750import org .spdx .library .model .v2 .license .WithExceptionOperator ;
51+ import org .spdx .library .model .v3 .ExternalCustomLicense ;
52+ import org .spdx .library .model .v3 .ExternalCustomLicenseAddition ;
4853import org .spdx .library .model .v3 .SpdxConstantsV3 ;
54+ import org .spdx .library .model .v3 .core .NamespaceMap ;
55+ import org .spdx .library .model .v3 .core .SpdxDocument ;
4956import org .spdx .library .model .v3 .expandedlicensing .ExpandedLicensingConjunctiveLicenseSet ;
5057import org .spdx .library .model .v3 .expandedlicensing .ExpandedLicensingCustomLicense ;
58+ import org .spdx .library .model .v3 .expandedlicensing .ExpandedLicensingCustomLicenseAddition ;
5159import org .spdx .library .model .v3 .expandedlicensing .ExpandedLicensingDisjunctiveLicenseSet ;
5260import org .spdx .library .model .v3 .expandedlicensing .ExpandedLicensingExtendableLicense ;
5361import org .spdx .library .model .v3 .expandedlicensing .ExpandedLicensingLicense ;
62+ import org .spdx .library .model .v3 .expandedlicensing .ExpandedLicensingLicenseAddition ;
5463import org .spdx .library .model .v3 .expandedlicensing .ExpandedLicensingListedLicense ;
5564import org .spdx .library .model .v3 .expandedlicensing .ExpandedLicensingListedLicenseException ;
5665import org .spdx .library .model .v3 .expandedlicensing .ExpandedLicensingNoAssertionLicense ;
5766import org .spdx .library .model .v3 .expandedlicensing .ExpandedLicensingNoneLicense ;
5867import org .spdx .library .model .v3 .expandedlicensing .ExpandedLicensingOrLaterOperator ;
5968import org .spdx .library .model .v3 .expandedlicensing .ExpandedLicensingWithAdditionOperator ;
6069import org .spdx .library .model .v3 .simplelicensing .SimpleLicensingAnyLicenseInfo ;
61- import org .spdx .storage .CompatibleModelStoreWrapper ;
6270import org .spdx .storage .IModelStore ;
6371import org .spdx .storage .IModelStore .IdType ;
6472
@@ -98,24 +106,27 @@ enum Operator {
98106 * @param customLicenseUriPrefix Prefix for Object URI's created when appending custom license ID's or custom license additions. If any custom licenses or additions already exist, they will be used.
99107 * If none exist for an ID, they will be added. If null, the default model document URI will be used.
100108 * @param copyManager if non-null, allows for copying of any properties set which use other model stores or document URI's
109+ * @param spdxDocument enclosing SPDX document - required if there are any external custom licenses or additions
101110 * @return the parsed license expression
102111 * @throws InvalidSPDXAnalysisException
103112 */
104113 public static SimpleLicensingAnyLicenseInfo parseLicenseExpression (String expression , IModelStore store ,
105- String customLicenseUriPrefix , IModelCopyManager copyManager ) throws InvalidSPDXAnalysisException {
114+ String customLicenseUriPrefix , @ Nullable IModelCopyManager copyManager , @ Nullable SpdxDocument spdxDocument ) throws InvalidSPDXAnalysisException {
106115 if (expression == null || expression .trim ().isEmpty ()) {
107116 throw new LicenseParserException ("Empty license expression" );
108117 }
109118 Objects .requireNonNull (store , "Model store can not be null" );
110- Objects .requireNonNull (customLicenseUriPrefix , "Document URI can not be null" );
119+ if (Objects .isNull (customLicenseUriPrefix )) {
120+ customLicenseUriPrefix = DefaultModelStore .getDefaultDocumentUri () + "#" ;
121+ }
111122 String [] tokens = tokenizeExpression (expression );
112123 if (tokens .length == 1 && tokens [0 ].equals (SpdxConstantsCompatV2 .NOASSERTION_VALUE )) {
113124 return new ExpandedLicensingNoAssertionLicense ();
114125 } else if (tokens .length == 1 && tokens [0 ].equals (SpdxConstantsCompatV2 .NONE_VALUE )) {
115126 return new ExpandedLicensingNoneLicense ();
116127 } else {
117128 try {
118- return parseLicenseExpression (tokens , store , customLicenseUriPrefix , copyManager );
129+ return parseLicenseExpression (tokens , store , customLicenseUriPrefix , copyManager , spdxDocument );
119130 } catch (LicenseParserException ex ) {
120131 // Add the expression to the error message to provide additional information to the user
121132 throw new LicenseParserException (ex .getMessage ()+" License expression: '" +expression +"'" , ex );
@@ -203,11 +214,13 @@ private static void processPreToken(String preToken,
203214 * @param customLicenseUriPrefix Prefix for Object URI's created when appending custom license ID's or custom license additions. If any custom licenses or additions already exist, they will be used.
204215 * If none exist for an ID, they will be added. If null, the default model document URI will be used.
205216 * @param copyManager if non-null, allows for copying of any properties set which use other model stores or document URI's
206- * @return
217+ * @param spdxDocument enclosing SPDX document - required if there are any external custom licenses or additions
218+ * @return a license info representing the fully parsed list of tokens
207219 * @throws InvalidSPDXAnalysisException
208220 */
209221 private static SimpleLicensingAnyLicenseInfo parseLicenseExpression (String [] tokens , IModelStore store ,
210- String customLicenseUriPrefix , IModelCopyManager copyManager ) throws InvalidSPDXAnalysisException {
222+ String customLicenseUriPrefix , @ Nullable IModelCopyManager copyManager ,
223+ @ Nullable SpdxDocument spdxDocument ) throws InvalidSPDXAnalysisException {
211224 if (tokens == null || tokens .length == 0 ) {
212225 throw new LicenseParserException ("Expected license expression" );
213226 }
@@ -224,10 +237,11 @@ private static SimpleLicensingAnyLicenseInfo parseLicenseExpression(String[] tok
224237 throw new LicenseParserException ("Missing right parenthesis" );
225238 }
226239 String [] nestedTokens = Arrays .copyOfRange (tokens , tokenIndex , rightParenIndex );
227- operandStack .push (parseLicenseExpression (nestedTokens , store , customLicenseUriPrefix , copyManager ));
240+ operandStack .push (parseLicenseExpression (nestedTokens , store , customLicenseUriPrefix ,
241+ copyManager , spdxDocument ));
228242 tokenIndex = rightParenIndex + 1 ;
229243 } else if (OPERATOR_MAP .get (token ) == null ) { // assumed to be a simple licensing type
230- operandStack .push (parseSimpleLicenseToken (token , store , customLicenseUriPrefix , copyManager ));
244+ operandStack .push (parseSimpleLicenseToken (token , store , customLicenseUriPrefix , copyManager , spdxDocument ));
231245 } else {
232246 Operator operator = OPERATOR_MAP .get (token );
233247 if (operator == Operator .WITH ) {
@@ -240,21 +254,8 @@ private static SimpleLicensingAnyLicenseInfo parseLicenseExpression(String[] tok
240254 throw new LicenseParserException ("Missing exception clause" );
241255 }
242256 token = tokens [tokenIndex ++];
243- //TODO Update this to handle custom additions
244- ExpandedLicensingListedLicenseException licenseException = null ;
245- Optional <String > exceptionId = Optional .empty ();
246- if (LicenseInfoFactory .isSpdxListedExceptionId (token )) {
247- exceptionId = LicenseInfoFactory .listedExceptionIdCaseSensitive (token );
248- }
249- if (exceptionId .isPresent ()) {
250- licenseException = LicenseInfoFactory .getListedExceptionById (exceptionId .get ());
251- } else if (token .startsWith (SpdxConstantsCompatV2 .NON_STD_LICENSE_ID_PRENUM )) {
252- throw new LicenseParserException ("WITH must be followed by a license exception. " +token +" is a Listed License type." );
253- } else {
254- licenseException = new ExpandedLicensingListedLicenseException (store ,
255- CompatibleModelStoreWrapper .documentUriIdToUri (customLicenseUriPrefix , token , false ),
256- copyManager , true );
257- }
257+ ExpandedLicensingLicenseAddition licenseAddition = parseSimpleLicenseAdditionToken (token ,
258+ store , customLicenseUriPrefix , copyManager , spdxDocument );
258259 SimpleLicensingAnyLicenseInfo operand = operandStack .pop ();
259260 if (operand == null ) {
260261 throw new LicenseParserException ("Missing license for with clause" );
@@ -265,7 +266,7 @@ private static SimpleLicensingAnyLicenseInfo parseLicenseExpression(String[] tok
265266 ExpandedLicensingWithAdditionOperator weo = new ExpandedLicensingWithAdditionOperator (store ,
266267 store .getNextId (IdType .Anonymous ), copyManager , true );
267268 weo .setExpandedLicensingSubjectExtendableLicense ((ExpandedLicensingExtendableLicense )operand );
268- weo .setExpandedLicensingSubjectAddition (licenseException );
269+ weo .setExpandedLicensingSubjectAddition (licenseAddition );
269270 operandStack .push (weo );
270271 } else {
271272 // process in order of precedence using the shunting yard algorithm
@@ -403,25 +404,79 @@ private static int findMatchingParen(String[] tokens, int startToken) {
403404 }
404405 return -1 ;
405406 }
407+
408+ /**
409+ * Converts a string token into its equivalent license addition
410+ * checking for a listed license
411+ * @param token Token to translate to the equivalent license addition
412+ * @param store Store for the licenses
413+ * @param customLicenseUriPrefix Prefix to use for any created local licenses or additions
414+ * @param copyManager to use when copying from the listed license store
415+ * @param spdxDocument enclosing SPDX document - required if there are any external custom licenses or additions
416+ * @return a CustomLicenseAddition, ListedLicense, ListedLicenseException or CustomLicense depending on what is in the store
417+ * @throws InvalidSPDXAnalysisException
418+ */
419+ private static ExpandedLicensingLicenseAddition parseSimpleLicenseAdditionToken (String token , IModelStore store , String customLicenseUriPrefix ,
420+ @ Nullable IModelCopyManager copyManager , @ Nullable SpdxDocument spdxDocument ) throws InvalidSPDXAnalysisException {
421+ Objects .requireNonNull (token , "Token can not be null" );
422+ Objects .requireNonNull (store , "Model store can not be null" );
423+ Objects .requireNonNull (customLicenseUriPrefix , "URI Prefix can not be null" );
424+ if (token .contains (":" )) {
425+ // External License Ref
426+ return new ExternalCustomLicenseAddition (store ,
427+ convertToExternalObjectUri (token , spdxDocument ), copyManager );
428+ }
429+ Optional <String > exceptionId = Optional .empty ();
430+ if (LicenseInfoFactory .isSpdxListedExceptionId (token )) {
431+ // listed exception
432+ exceptionId = LicenseInfoFactory .listedExceptionIdCaseSensitive (token );
433+ }
434+ if (exceptionId .isPresent ()) {
435+ ExpandedLicensingListedLicenseException listedException = LicenseInfoFactory .getListedExceptionById (exceptionId .get ());
436+ if (!store .exists (SpdxConstantsCompatV2 .LISTED_LICENSE_NAMESPACE_PREFIX + exceptionId .get ())) {
437+ if (Objects .nonNull (copyManager )) {
438+ // copy to the local store
439+ copyManager .copy (store , listedException .getObjectUri (), listedException .getModelStore (),
440+ listedException .getObjectUri (), SpdxConstantsV3 .EXPANDED_LICENSING_EXPANDED_LICENSING_LISTED_LICENSE ,
441+ SpdxModelFactory .getLatestSpecVersion (), null );
442+ // copy to the local store
443+ }
444+ }
445+ return new ExpandedLicensingListedLicenseException (store , listedException .getObjectUri (), copyManager , true );
446+ } else {
447+ // custom addition
448+ String objectUri = customLicenseUriPrefix + token ;
449+ ExpandedLicensingCustomLicenseAddition localAddition = null ;
450+ if (store .exists (objectUri )) {
451+ localAddition = new ExpandedLicensingCustomLicenseAddition (store , objectUri , copyManager , false );
452+ } else {
453+ localAddition = new ExpandedLicensingCustomLicenseAddition (store , objectUri , copyManager , true );
454+ localAddition .setExpandedLicensingAdditionText (UNINITIALIZED_LICENSE_TEXT );
455+ }
456+ return localAddition ;
457+ }
458+ }
406459
407460 /**
408461 * Converts a string token into its equivalent license
409462 * checking for a listed license
410- * @param token
411- * @param baseStore
412- * @param customLicenseUriPrefix
413- * @param copyManager
414- * @return
463+ * @param token Token to translate to the equivalent license
464+ * @param store Store for the licenses
465+ * @param customLicenseUriPrefix Prefix to use for any created local licenses or additions
466+ * @param copyManager to use when copying from the listed license store
467+ * @param spdxDocument enclosing SPDX document - required if there are any external custom licenses or additions
468+ * @return a CustomLicenseAddition, ListedLicense, ListedLicenseException or CustomLicense depending on what is in the store
415469 * @throws InvalidSPDXAnalysisException
416470 */
417471 private static SimpleLicensingAnyLicenseInfo parseSimpleLicenseToken (String token , IModelStore store , String customLicenseUriPrefix ,
418- IModelCopyManager copyManager ) throws InvalidSPDXAnalysisException {
472+ @ Nullable IModelCopyManager copyManager , @ Nullable SpdxDocument spdxDocument ) throws InvalidSPDXAnalysisException {
419473 Objects .requireNonNull (token , "Token can not be null" );
420474 Objects .requireNonNull (store , "Model store can not be null" );
421- Objects .requireNonNull (customLicenseUriPrefix , "Document URI can not be null" );
475+ Objects .requireNonNull (customLicenseUriPrefix , "URI Prefix can not be null" );
422476 if (token .contains (":" )) {
423- // External License Ref
424- return SpdxModelFactory .getExternalAnyLicenseInfo (store , customLicenseUriPrefix + token , copyManager , SpdxModelFactory .getLatestSpecVersion ());
477+ // External Custom License
478+ return new ExternalCustomLicense (store ,
479+ convertToExternalObjectUri (token , spdxDocument ), copyManager );
425480 }
426481 Optional <String > licenseId = Optional .empty ();
427482 if (LicenseInfoFactory .isSpdxListedLicenseId (token )) {
@@ -442,19 +497,44 @@ private static SimpleLicensingAnyLicenseInfo parseSimpleLicenseToken(String toke
442497 return new ExpandedLicensingListedLicense (store , listedLicense .getObjectUri (), copyManager , true );
443498 } else {
444499 // LicenseRef
445- Optional < String > caseSensitiveId = store . getCaseSensisitiveId ( customLicenseUriPrefix , token ) ;
500+ String objectUri = customLicenseUriPrefix + token ;
446501 ExpandedLicensingCustomLicense localLicense = null ;
447- if (caseSensitiveId .isPresent ()) {
448- localLicense = new ExpandedLicensingCustomLicense (store , customLicenseUriPrefix + caseSensitiveId .get (), copyManager , false );
449-
502+ if (store .exists (objectUri )) {
503+ localLicense = new ExpandedLicensingCustomLicense (store , objectUri , copyManager , false );
450504 } else {
451- localLicense = new ExpandedLicensingCustomLicense (store , customLicenseUriPrefix + customLicenseUriPrefix , copyManager , true );
505+ localLicense = new ExpandedLicensingCustomLicense (store , objectUri , copyManager , true );
452506 localLicense .setSimpleLicensingLicenseText (UNINITIALIZED_LICENSE_TEXT );
453507 }
454508 return localLicense ;
455509 }
456510 }
457511
512+ /**
513+ * @param externalReference String of the form [prefix]:[id] where [prefix] is a prefix in the spdxDocument and ID is the suffix of the object URI
514+ * @param spdxDocument document containing the license expression string and (importantly) the namespace map for any external URIs
515+ * @return the full object URI with the [prefix] replaced by the associated namespace
516+ * @throws InvalidSPDXAnalysisException
517+ */
518+ private static String convertToExternalObjectUri (String externalReference , SpdxDocument spdxDocument ) throws InvalidSPDXAnalysisException {
519+ if (Objects .isNull (spdxDocument )) {
520+ throw new LicenseParserException ("References to external custom additions or external custom licenses must include the spdxDocument parameter" );
521+ }
522+ String [] refParts = externalReference .split (":" );
523+ if (refParts .length != 2 || refParts [0 ].isEmpty () || refParts [1 ].isEmpty ()) {
524+ throw new LicenseParserException ("Invalid external ID: " +externalReference );
525+ }
526+ String namespace = null ;
527+ for (NamespaceMap nm :spdxDocument .getNamespaceMaps ()) {
528+ if (refParts [0 ].equals (nm .getPrefix ())) {
529+ namespace = nm .getNamespace ();
530+ }
531+ }
532+ if (Objects .isNull (namespace )) {
533+ throw new InvalidSPDXAnalysisException ("Prefix " +refParts [0 ]+" not found in SPDX document " +spdxDocument .toString ());
534+ }
535+ return namespace + refParts [1 ];
536+ }
537+
458538 /**
459539 * Converts a string token into its equivalent license
460540 * checking for a listed license
@@ -469,7 +549,7 @@ private static AnyLicenseInfo parseSimpleLicenseTokenCompatV2(String token, IMod
469549 IModelCopyManager copyManager ) throws InvalidSPDXAnalysisException {
470550 Objects .requireNonNull (token , "Token can not be null" );
471551 Objects .requireNonNull (store , "Model store can not be null" );
472- Objects .requireNonNull (documentUri , "Document URI can not be null" );
552+ Objects .requireNonNull (documentUri , "URI prefix can not be null" );
473553 if (token .contains (":" )) {
474554 // External License Ref
475555 return new ExternalExtractedLicenseInfo (store , documentUri , token , copyManager , true );
0 commit comments