Skip to content

Commit

Permalink
SpdxExpression: Add method to apply a choice on a SPDX expression
Browse files Browse the repository at this point in the history
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 <marcel.bochtler@bosch.io>
  • Loading branch information
MarcelBochtler committed Dec 10, 2020
1 parent a2a2b71 commit 7e24729
Show file tree
Hide file tree
Showing 2 changed files with 167 additions and 0 deletions.
58 changes: 58 additions & 0 deletions spdx-utils/src/main/kotlin/SpdxExpression.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/*
* Copyright (C) 2017-2019 HERE Europe B.V.
* Copyright (C) 2020 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.
Expand Down Expand Up @@ -153,6 +154,22 @@ sealed class SpdxExpression {
*/
open fun offersChoice(): Boolean = false

/**
* Applies a license [choice] to the given SPDX [subtreeMatcher].
* The [subtreeMatcher] must not contain more than two choices.
*/
open fun applyChoice(subtreeMatcher: SpdxExpression, choice: SpdxExpression): SpdxExpression {
if (!subtreeMatcher.isValidChoice(choice)) {
throw InvalidLicenseChoiceException("Invalid choice $choice for expression $subtreeMatcher.")
}

if (this == choice) {
return this
} else {
throw InvalidLicenseChoiceException("Cannot select $choice for expression $this.")
}
}

/**
* Concatenate [this][SpdxExpression] and [other] using [SpdxOperator.AND].
*/
Expand Down Expand Up @@ -236,6 +253,31 @@ class SpdxCompoundExpression(
SpdxOperator.AND -> left.offersChoice() || right.offersChoice()
}

override fun applyChoice(subtreeMatcher: SpdxExpression, choice: SpdxExpression): SpdxExpression {
if (!subtreeMatcher.isValidChoice(choice)) {
throw InvalidLicenseChoiceException(
"$choice is not a valid choice for the subtree expression. Valid choices are: ${validChoices()}."
)
}
if (subtreeMatcher.validChoices().size > 2) {
throw InvalidLicenseChoiceException(
"Subtree matcher $subtreeMatcher must not contain more than two choices."
)
}

return replaceMatcherWithChoice(subtreeMatcher, choice)
}

private fun replaceMatcherWithChoice(subtreeMatcher: SpdxExpression, choice: SpdxExpression) =
if (toString().contains(subtreeMatcher.toString())) {
toString().replace(subtreeMatcher.toString(), choice.toString()).toSpdx()
} else {
val dismissedLicense = subtreeMatcher.validChoices().first { it != choice }
val remains = this.validChoices().filter { it != dismissedLicense }

remains.choicesToSpdx()
}

override fun equals(other: Any?): Boolean {
if (other !is SpdxExpression) return false

Expand Down Expand Up @@ -491,6 +533,20 @@ data class SpdxLicenseReferenceExpression(
override fun getLicenseUrl(): String? = null
}

/**
* Aggregates multiple SpdxExpressions to a [SpdxCompoundExpression] concatenated using [SpdxOperator.OR].
*/
fun List<SpdxExpression>.choicesToSpdx(): SpdxExpression {
if (size == 1) return this[0]

var result: SpdxExpression = this[0]
this.subList(1, this.size).forEach {
result = result or it
}

return result
}

/**
* An SPDX operator for use in compound expressions as defined by version 2.1 of the
* [SPDX specification, appendix IV][1].
Expand All @@ -516,3 +572,5 @@ enum class SpdxOperator(
*/
OR(0)
}

class InvalidLicenseChoiceException(message: String) : Exception(message)
109 changes: 109 additions & 0 deletions spdx-utils/src/test/kotlin/SpdxExpressionTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import io.kotest.matchers.should
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe

import org.junit.jupiter.api.Assertions.assertThrows

import org.ossreviewtoolkit.spdx.SpdxExpression.Strictness
import org.ossreviewtoolkit.spdx.SpdxLicense.*
import org.ossreviewtoolkit.spdx.SpdxLicenseException.*
Expand Down Expand Up @@ -367,6 +369,113 @@ class SpdxExpressionTest : WordSpec() {
}
}

"applyChoice()" should {
"return the choice for a simple SPDX expression" {
val spdxExpression = "a".toSpdx()

val resolvedLicense = spdxExpression.applyChoice(spdxExpression, "a".toSpdx())

resolvedLicense shouldBe "a".toSpdx()
}

"throw an exception if the user chose a wrong license for a simple SPDX expression" {
val spdxExpression = "a".toSpdx()

assertThrows(InvalidLicenseChoiceException::class.java) {
spdxExpression.applyChoice(spdxExpression, "b".toSpdx())
}
}

"throw an exception if the matcher does not match the expression and the choice is invalid" {
val spdxExpression = "a".toSpdx()

assertThrows(InvalidLicenseChoiceException::class.java) {
spdxExpression.applyChoice("b".toSpdx(), "c".toSpdx())
}
}

"throw an exception if more than two choices are given for the matcher" {
val spdxExpression = "a OR b OR c".toSpdx()
val matcher = "a OR b OR c".toSpdx()

assertThrows(InvalidLicenseChoiceException::class.java) {
spdxExpression.applyChoice(matcher, "a".toSpdx())
}
}

"return the new subtree if the choice is valid and the expression contains multiple subtrees" {
val spdxExpression = "a OR b OR c".toSpdx()
val matcher = "a OR b".toSpdx()

val resolvedLicense = spdxExpression.applyChoice(matcher, "b".toSpdx())

resolvedLicense shouldBe "b OR c".toSpdx()
}

"throw an exception if the matcher contains more than one choice" {
val spdxExpression = "a OR b OR c".toSpdx()

assertThrows(InvalidLicenseChoiceException::class.java) {
spdxExpression.applyChoice(spdxExpression, "b".toSpdx())
}
}

"throw an exception if the chosen license is not an valid option" {
val spdxExpression = "a OR b".toSpdx()

assertThrows(InvalidLicenseChoiceException::class.java) {
spdxExpression.applyChoice(spdxExpression, "c".toSpdx())
}
}

"return the reduced subtree in dnf if the choice was valid" {
val spdxExpression = "(a OR b) AND c AND (d OR e)".toSpdx()
val matcher = "a AND c AND d OR a AND c AND e".toSpdx()

val resolvedLicense = spdxExpression.applyChoice(matcher, "a AND c AND d".toSpdx())

resolvedLicense shouldBe "a AND c AND d OR b AND c AND d OR b AND c AND e".toSpdx()
}

"return the expression if the matcher does not match any subexpression and needs to be converted to a dnf" {
val spdxExpression = "(a OR b) AND c AND (d OR e)".toSpdx()
val expressionMatcher = "(a AND c AND d) OR (x AND y AND z)".toSpdx()

val resolvedLicense = spdxExpression.applyChoice(expressionMatcher, "a AND c AND d".toSpdx())

resolvedLicense shouldBe "(a OR b) AND c AND (d OR e)".toSpdx()
}

"return the choice when the matcher matches only a subtree" {
val spdxExpression = "(a OR b) AND c".toSpdx()
val matcher = "a OR b".toSpdx()

val resolvedLicense = spdxExpression.applyChoice(matcher, "a".toSpdx())

resolvedLicense shouldBe "a AND c".toSpdx()
}
}

"choicesToSpdx()" should {
"return the expression if the list only contains a single expression" {
val choices = listOf("a".toSpdx())

choices.choicesToSpdx() shouldBe "a".toSpdx()
}

"return the SpdxCompoundExpression for multiple SpdxExpression" {
val choices = listOf("a".toSpdx(), "b".toSpdx())

choices.choicesToSpdx() shouldBe "a OR b".toSpdx()
}

"return the SpdxCompoundExpression for multiple SpdxExpressions compounded by AND" {
val choices = listOf("a AND b".toSpdx(), "a AND c".toSpdx())

choices.choicesToSpdx() shouldBe "a AND b OR a AND c".toSpdx()
}
}

"equals()" should {
"return true for semantically equal expressions" {
"a".toSpdx() shouldBe "a".toSpdx()
Expand Down

0 comments on commit 7e24729

Please sign in to comment.