From fe23cd38befaaa1758190a32541c785c4cbc60f3 Mon Sep 17 00:00:00 2001 From: Marcel Bochtler Date: Mon, 7 Dec 2020 12:37:40 +0100 Subject: [PATCH] SpdxExpression: Add method to apply a choice on a SPDX expression To be able to apply license choices to SpdxCompoundExpressions, a logic is added to apply the user's choices to a specified matcher. Signed-off-by: Marcel Bochtler --- spdx-utils/src/main/kotlin/SpdxException.kt | 2 +- spdx-utils/src/main/kotlin/SpdxExpression.kt | 68 +++++++++++ .../src/test/kotlin/SpdxExpressionTest.kt | 108 ++++++++++++++++++ 3 files changed, 177 insertions(+), 1 deletion(-) diff --git a/spdx-utils/src/main/kotlin/SpdxException.kt b/spdx-utils/src/main/kotlin/SpdxException.kt index 275bafa103bb0..11d5ac99b1c0f 100644 --- a/spdx-utils/src/main/kotlin/SpdxException.kt +++ b/spdx-utils/src/main/kotlin/SpdxException.kt @@ -19,7 +19,7 @@ package org.ossreviewtoolkit.spdx -class SpdxException : RuntimeException { +open class SpdxException : RuntimeException { constructor(message: String?, cause: Throwable?) : super(message, cause) constructor(message: String?) : super(message) constructor(cause: Throwable?) : super(cause) diff --git a/spdx-utils/src/main/kotlin/SpdxExpression.kt b/spdx-utils/src/main/kotlin/SpdxExpression.kt index 50d1b1d6ee38c..94cbb6a2a1bdf 100644 --- a/spdx-utils/src/main/kotlin/SpdxExpression.kt +++ b/spdx-utils/src/main/kotlin/SpdxExpression.kt @@ -1,5 +1,6 @@ /* * Copyright (C) 2017-2019 HERE Europe B.V. + * Copyright (C) 2020-2021 Bosch.IO GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -153,6 +154,22 @@ sealed class SpdxExpression { */ open fun offersChoice(): Boolean = false + /** + * Apply a license [choice], optionally limited to the given [subExpression], and return a [SpdxExpression] where + * the choice is resolved. + */ + open fun applyChoice(choice: SpdxExpression, subExpression: SpdxExpression = this): SpdxExpression { + if (this != subExpression) { + throw InvalidSubExpressionException("$subExpression is not a valid subExpression for $this") + } + + if (this != choice) { + throw InvalidLicenseChoiceException("Cannot select $choice for expression $this.") + } + + return this + } + /** * Concatenate [this][SpdxExpression] and [other] using [SpdxOperator.AND]. */ @@ -236,6 +253,47 @@ class SpdxCompoundExpression( SpdxOperator.AND -> left.offersChoice() || right.offersChoice() } + override fun applyChoice(choice: SpdxExpression, subExpression: SpdxExpression): SpdxExpression { + if (!subExpression.validChoices().containsAll(choice.validChoices())) { + throw InvalidLicenseChoiceException( + "$choice is not a valid choice for $subExpression. Valid choices are: ${validChoices()}." + ) + } + + if (!isValidSubExpression(subExpression)) { + throw InvalidSubExpressionException("$subExpression is not not a valid subExpression of $this") + } + + return replaceSubexpressionWithChoice(subExpression, choice) + } + + private fun replaceSubexpressionWithChoice(subExpression: SpdxExpression, choice: SpdxExpression): SpdxExpression { + val expressionString = toString() + val subExpressionString = subExpression.toString() + val choiceString = choice.toString() + + return if (expressionString.contains(subExpressionString)) { + expressionString.replace(subExpressionString, choiceString).toSpdx() + } else { + val dismissedLicense = subExpression.validChoices().first { it != choice } + val unchosenLicenses = validChoices().filter { it != dismissedLicense } + + if (unchosenLicenses.isEmpty()) { + throw IllegalArgumentException("No licenses left after applying choice $choice to $subExpression") + } else { + unchosenLicenses.reduce(SpdxExpression::or) + } + } + } + + private fun isValidSubExpression(subExpression: SpdxExpression): Boolean { + val expressionString = toString() + val subExpressionString = subExpression.toString() + + return validChoices().containsAll(subExpression.validChoices()) || + expressionString.contains(subExpressionString) + } + override fun equals(other: Any?): Boolean { if (other !is SpdxExpression) return false @@ -360,12 +418,14 @@ class SpdxLicenseWithExceptionExpression( override fun equals(other: Any?) = when (other) { is SpdxLicenseWithExceptionExpression -> license == other.license && exception == other.exception + is SpdxExpression -> { val decomposed = other.decompose() decomposed.size == 1 && decomposed.first().let { it is SpdxLicenseWithExceptionExpression && it.license == license && it.exception == exception } } + else -> false } @@ -425,12 +485,14 @@ class SpdxLicenseIdExpression( override fun equals(other: Any?) = when (other) { is SpdxLicenseIdExpression -> id == other.id && orLaterVersion == other.orLaterVersion + is SpdxExpression -> { val decomposed = other.decompose() decomposed.size == 1 && decomposed.first().let { it is SpdxLicenseIdExpression && it.id == id && it.orLaterVersion == orLaterVersion } } + else -> false } @@ -477,10 +539,12 @@ data class SpdxLicenseReferenceExpression( override fun equals(other: Any?) = when (other) { is SpdxLicenseReferenceExpression -> id == other.id + is SpdxExpression -> { val decomposed = other.decompose() decomposed.size == 1 && decomposed.first().let { it is SpdxLicenseReferenceExpression && it.id == id } } + else -> false } @@ -516,3 +580,7 @@ enum class SpdxOperator( */ OR(0) } + +class InvalidLicenseChoiceException(message: String) : SpdxException(message) + +class InvalidSubExpressionException(message: String) : SpdxException(message) diff --git a/spdx-utils/src/test/kotlin/SpdxExpressionTest.kt b/spdx-utils/src/test/kotlin/SpdxExpressionTest.kt index 29c223e7c6113..5435bb6640256 100644 --- a/spdx-utils/src/test/kotlin/SpdxExpressionTest.kt +++ b/spdx-utils/src/test/kotlin/SpdxExpressionTest.kt @@ -1,5 +1,6 @@ /* * Copyright (C) 2017-2019 HERE Europe B.V. + * Copyright (C) 2020-2021 Bosch.IO GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -367,6 +368,113 @@ class SpdxExpressionTest : WordSpec() { } } + "applyChoice()" should { + "return the choice for a simple expression" { + val expression = "a".toSpdx() + val choice = "a".toSpdx() + + val result = expression.applyChoice(choice) + + result shouldBe "a".toSpdx() + } + + "throw an exception if the user chose a wrong license for a simple expression" { + val expression = "a".toSpdx() + val choice = "b".toSpdx() + + shouldThrow { expression.applyChoice(choice) } + } + + "return the new expression if only a part of the expression is matched by the subExpression" { + val expression = "a OR b OR c".toSpdx() + val choice = "b".toSpdx() + val subExpression = "a OR b".toSpdx() + + val result = expression.applyChoice(choice, subExpression) + + result shouldBe "b OR c".toSpdx() + } + + "work with choices that itself are a choice" { + val expression = "a OR b OR c OR d".toSpdx() + val choice = "a OR b".toSpdx() + val subExpression = "a OR b OR c".toSpdx() + + val result = expression.applyChoice(choice, subExpression) + + result shouldBe "a OR b OR d".toSpdx() + } + + "apply the choice if the expression contains multiple choices" { + val expression = "a OR b OR c".toSpdx() + val choice = "b".toSpdx() + + val result = expression.applyChoice(choice) + + result shouldBe "b".toSpdx() + } + + "throw an exception if the chosen license is not a valid option" { + val expression = "a OR b".toSpdx() + val choice = "c".toSpdx() + + shouldThrow { expression.applyChoice(choice) } + } + + "apply the choice if the expression is not in DNF" { + val expression = "(a OR b) AND c".toSpdx() + val choice = "a AND c".toSpdx() + + val result = expression.applyChoice(choice) + + result shouldBe "a AND c".toSpdx() + } + + "return the reduced subExpression in DNF if the choice was valid" { + val expression = "(a OR b) AND c AND (d OR e)".toSpdx() + val choice = "a AND c AND d".toSpdx() + val subExpression = "a AND c AND d OR a AND c AND e".toSpdx() + + val result = expression.applyChoice(choice, subExpression) + + result shouldBe "a AND c AND d OR b AND c AND d OR b AND c AND e".toSpdx() + } + + "throw an exception if the subExpression does not match the simple expression" { + val expression = "a".toSpdx() + val choice = "x".toSpdx() + val subExpression = "x OR y".toSpdx() + + shouldThrow { expression.applyChoice(choice, subExpression) } + } + + "throw an exception if the subExpression does not match the expression" { + val expression = "a OR b OR c".toSpdx() + val choice = "x".toSpdx() + val subExpression = "x OR y OR z".toSpdx() + + shouldThrow { expression.applyChoice(choice, subExpression) } + } + + "throw an exception if the subExpression does not match and needs to be converted to a DNF" { + val expression = "(a OR b) AND c AND (d OR e)".toSpdx() + val choice = "a AND c AND d".toSpdx() + val subExpression = "(a AND c AND d) OR (x AND y AND z)".toSpdx() + + shouldThrow { expression.applyChoice(choice, subExpression) } + } + + "apply the choice when the subExpression matches only a part of the expression" { + val expression = "(a OR b) AND c".toSpdx() + val choice = "a".toSpdx() + val subExpression = "a OR b".toSpdx() + + val result = expression.applyChoice(choice, subExpression) + + result shouldBe "a AND c".toSpdx() + } + } + "equals()" should { "return true for semantically equal expressions" { "a".toSpdx() shouldBe "a".toSpdx()