Skip to content
46 changes: 46 additions & 0 deletions model/src/main/kotlin/LicenseTextCuration.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright (C) 2026 The ORT Project Copyright Holders <https://github.com/oss-review-toolkit/ort/blob/main/NOTICE>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
* License-Filename: LICENSE
*/

package org.ossreviewtoolkit.model

import org.ossreviewtoolkit.utils.spdxexpression.SpdxSingleLicenseExpression

data class LicenseTextCuration(
/**
* The license identifier to which to add the license.
*/
val licenseId: SpdxSingleLicenseExpression,

/**
* A plain-text comment about this license text curation. Should contain information about how and why this
* license text curation was created.
*/
val comment: String? = null,

/**
* The license text (variant) to associate with the corresponding [licenseId] and [identifier][PackageCuration.id].
*/
val licenseText: String
) {
init {
require(licenseText.isNotBlank()) {
"The license text must not be blank."
}
}
}
38 changes: 38 additions & 0 deletions model/src/main/kotlin/LicenseTextCurations.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright (C) 2026 The ORT Project Copyright Holders <https://github.com/oss-review-toolkit/ort/blob/main/NOTICE>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
* License-Filename: LICENSE
*/

package org.ossreviewtoolkit.model

data class LicenseTextCurations(
/**
* The identifier of the package, analog to [PackageCuration.id].
*/
val id: Identifier,

/**
* The curations for [id].
*/
val curations: List<LicenseTextCuration>
) {
init {
require(curations.isNotEmpty()) {
"The curations must not be empty."
}
}
}
16 changes: 2 additions & 14 deletions model/src/main/kotlin/PackageCuration.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ package org.ossreviewtoolkit.model

import com.fasterxml.jackson.annotation.JsonProperty

import org.ossreviewtoolkit.model.utils.isApplicableIvyVersion
import org.ossreviewtoolkit.utils.common.equalsOrIsBlank
import org.ossreviewtoolkit.model.utils.idMatches

/**
* This class assigns a [PackageCurationData] object to a [Package] identified by the [id].
Expand All @@ -39,23 +38,12 @@ data class PackageCuration(
@JsonProperty("curations")
val data: PackageCurationData
) {
/**
* Return true if this [PackageCuration] is applicable to the package with the given [identifier][pkgId],
* disregarding the version.
*/
private fun isApplicableDisregardingVersion(pkgId: Identifier) =
id.type.equals(pkgId.type, ignoreCase = true)
&& id.namespace == pkgId.namespace
&& id.name.equalsOrIsBlank(pkgId.name)

/**
* Return true if this [PackageCuration] is applicable to the package with the given [identifier][pkgId]. The
* curation's version may be an
* [Ivy version matcher](http://ant.apache.org/ivy/history/2.4.0/settings/version-matchers.html).
*/
fun isApplicable(pkgId: Identifier): Boolean =
isApplicableDisregardingVersion(pkgId)
&& (id.version.equalsOrIsBlank(pkgId.version) || id.isApplicableIvyVersion(pkgId))
fun isApplicable(pkgId: Identifier): Boolean = idMatches(id, pkgId)

/**
* Apply the curation [data] to the provided [basePackage] by calling [PackageCurationData.apply], if applicable.
Expand Down
9 changes: 4 additions & 5 deletions model/src/main/kotlin/config/OrtConfiguration.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ import java.io.File
import org.apache.logging.log4j.kotlin.logger

import org.ossreviewtoolkit.model.Severity
import org.ossreviewtoolkit.plugins.api.PluginConfig
import org.ossreviewtoolkit.utils.common.EnvironmentVariableFilter
import org.ossreviewtoolkit.utils.ort.ORT_CUSTOM_LICENSE_TEXTS_DIRNAME
import org.ossreviewtoolkit.utils.ort.ORT_FAILURE_STATUS_CODE
Expand Down Expand Up @@ -88,10 +87,10 @@ data class OrtConfiguration(
* license facts, license facts from a local ScanCode installation, and license facts from the default
* [ORT_CUSTOM_LICENSE_TEXTS_DIRNAME].
*/
val licenseFactProviders: LinkedHashMap<String, PluginConfig> = linkedMapOf(
"DefaultDir" to PluginConfig.EMPTY,
"SPDX" to PluginConfig.EMPTY,
"ScanCode" to PluginConfig.EMPTY
val licenseFactProviders: List<ProviderPluginConfiguration> = listOf(
ProviderPluginConfiguration(type = "DefaultDir"),
ProviderPluginConfiguration(type = "SPDX"),
ProviderPluginConfiguration(type = "ScanCode")
),

/**
Expand Down
18 changes: 18 additions & 0 deletions model/src/main/kotlin/utils/Extensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@ package org.ossreviewtoolkit.model.utils
import java.net.URI

import org.ossreviewtoolkit.model.ArtifactProvenance
import org.ossreviewtoolkit.model.Identifier
import org.ossreviewtoolkit.model.KnownProvenance
import org.ossreviewtoolkit.model.Provenance
import org.ossreviewtoolkit.model.ProvenanceResolutionResult
import org.ossreviewtoolkit.model.RepositoryProvenance
import org.ossreviewtoolkit.model.SourceCodeOrigin
import org.ossreviewtoolkit.model.TextLocation
import org.ossreviewtoolkit.model.VcsInfo
import org.ossreviewtoolkit.utils.common.equalsOrIsBlank
import org.ossreviewtoolkit.utils.common.getDuplicates

fun String.prependPath(prefix: String): String = if (prefix.isBlank()) this else "${prefix.removeSuffix("/")}/$this"
Expand Down Expand Up @@ -95,3 +97,19 @@ internal fun List<SourceCodeOrigin>.requireNoDuplicates() {
"'sourceCodeOrigins' must not contain duplicates. Duplicates: $duplicates"
}
}

/**
* Return true if the [matcher] is matches the given [identifier][id], disregarding the version.
*/
internal fun idMatchesDisregardingVersion(matcher: Identifier, id: Identifier) =
matcher.type.equals(id.type, ignoreCase = true)
&& matcher.namespace == id.namespace
&& matcher.name.equalsOrIsBlank(id.name)

/**
* Return true if the [matcher] matches the given [identifier][id]. The [matcher]s version may be an
* [Ivy version matcher](http://ant.apache.org/ivy/history/2.4.0/settings/version-matchers.html).
*/
fun idMatches(matcher: Identifier, id: Identifier): Boolean =
idMatchesDisregardingVersion(matcher, id)
&& (matcher.version.equalsOrIsBlank(id.version) || matcher.isApplicableIvyVersion(id))
16 changes: 8 additions & 8 deletions model/src/main/resources/reference.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,14 @@ ort:

# License fact providers, ordered from highest to lowest priority.
licenseFactProviders:
SPDX: {}
ScanCode:
options:
scanCodeLicenseTextDir: '/path/to/scancode/license/text/dir'
DefaultDir: {}
Dir:
options:
licenseTextDir: '/path/to/license/text/dir'
- type: "SPDX"
- type: "ScanCode"
options:
scanCodeLicenseTextDir: '/path/to/scancode/license/text/dir'
- type: "DefaultDir"
- type: "Dir"
options:
licenseTextDir: '/path/to/license/text/dir'

licenseFilePatterns:
licenseFilenames: ['license*']
Expand Down
17 changes: 11 additions & 6 deletions model/src/test/kotlin/config/OrtConfigurationTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ import java.io.File

import org.ossreviewtoolkit.model.Severity
import org.ossreviewtoolkit.model.SourceCodeOrigin
import org.ossreviewtoolkit.plugins.api.PluginConfig
import org.ossreviewtoolkit.utils.common.EnvironmentVariableFilter
import org.ossreviewtoolkit.utils.ort.ORT_REFERENCE_CONFIG_FILENAME

Expand All @@ -60,11 +59,17 @@ class OrtConfigurationTest : WordSpec({

ortConfig.forceOverwrite shouldBe true

ortConfig.licenseFactProviders should containExactlyEntries(
"spdx" to PluginConfig.EMPTY,
"scancode" to PluginConfig(mapOf("scanCodeLicenseTextDir" to "/path/to/scancode/license/text/dir")),
"defaultdir" to PluginConfig.EMPTY,
"dir" to PluginConfig(mapOf("licenseTextDir" to "/path/to/license/text/dir"))
ortConfig.licenseFactProviders should containExactly(
ProviderPluginConfiguration(type = "SPDX"),
ProviderPluginConfiguration(
type = "ScanCode",
options = mapOf("scanCodeLicenseTextDir" to "/path/to/scancode/license/text/dir")
),
ProviderPluginConfiguration(type = "DefaultDir"),
ProviderPluginConfiguration(
type = "Dir",
options = mapOf("licenseTextDir" to "/path/to/license/text/dir")
)
)

with(ortConfig.licenseFilePatterns) {
Expand Down
14 changes: 5 additions & 9 deletions plugins/commands/reporter/src/main/kotlin/ReportCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -263,16 +263,12 @@
HowToFixTextProvider.fromKotlinScript(it.readText(), ortResult)
} ?: HowToFixTextProvider.NONE

val licenseFactProviders = mutableListOf<LicenseFactProvider>()

customLicenseTextsDir?.let {
licenseFactProviders += DirLicenseFactProviderFactory.create(it.absolutePath)
}
val licenseFactProviders = buildList<LicenseFactProvider> {

Check notice on line 266 in plugins/commands/reporter/src/main/kotlin/ReportCommand.kt

View workflow job for this annotation

GitHub Actions / qodana-scan

Unnecessary type argument

Explicit type arguments can be inferred
customLicenseTextsDir?.let {
this += DirLicenseFactProviderFactory.create(it.absolutePath)
}

ortConfig.licenseFactProviders.mapTo(licenseFactProviders) { (id, config) ->
requireNotNull(LicenseFactProviderFactory.ALL[id]) {
"License fact provider '$id' is not available in the classpath."
}.create(config)
this += LicenseFactProviderFactory.create2(ortConfig.licenseFactProviders).map { it.second }
}

val licenseFactProvider = CompositeLicenseFactProvider(licenseFactProviders)
Expand Down
2 changes: 2 additions & 0 deletions plugins/license-fact-providers/api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,6 @@ plugins {
dependencies {
api(projects.model)
api(projects.plugins.api)

implementation(projects.utils.commonUtils)
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,15 @@ class CompositeLicenseFactProvider(
summary = "A license fact provider that aggregates multiple license fact providers."
)

override fun hasLicenseText(licenseId: String) = providers.any { it.hasLicenseText(licenseId) }
override fun hasLicenseText(licenseOrExceptionId: String) =
providers.any { it.hasLicenseText(licenseOrExceptionId) }

override fun getLicenseText(licenseId: String) = providers.firstNotNullOfOrNull { it.getLicenseText(licenseId) }
override fun getLicenseText(licenseOrExceptionId: String) =
providers.firstNotNullOfOrNull { it.getLicenseText(licenseOrExceptionId) }

override fun hasLicenseTextForId(licenseId: String, id: Identifier): Boolean =
providers.any { it.hasLicenseTextForId(licenseId, id) }
override fun hasLicenseTextsForId(singleLicenseExpression: String, id: Identifier): Boolean =
providers.any { it.hasLicenseTextsForId(singleLicenseExpression, id) }

override fun getLicenseTextForId(licenseId: String, id: Identifier): LicenseText? =
providers.firstNotNullOfOrNull { it.getLicenseTextForId(licenseId, id) }
override fun getLicenseTextsForId(singleLicenseExpression: String, id: Identifier): Set<LicenseText> =
providers.flatMapTo(mutableSetOf()) { it.getLicenseTextsForId(singleLicenseExpression, id) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,58 +21,79 @@

import org.ossreviewtoolkit.model.Identifier
import org.ossreviewtoolkit.plugins.api.Plugin
import org.ossreviewtoolkit.utils.spdxexpression.SpdxSingleLicenseExpression

/** A provider for license facts. */
abstract class LicenseFactProvider : Plugin {
/** Return `true´ if this provider has a license text for the given [licenseId]. */
abstract fun hasLicenseText(licenseId: String): Boolean
/** Return `true´ if this provider has a license text for the given [licenseOrExceptionId]. */
abstract fun hasLicenseText(licenseOrExceptionId: String): Boolean

/** Return a [LicenseText] for the given [licenseId], or `null` if no valid text is available. */
abstract fun getLicenseText(licenseId: String): LicenseText?
/** Return a [LicenseText] for the given [licenseOrExceptionId], or `null` if no valid text is available. */
abstract fun getLicenseText(licenseOrExceptionId: String): LicenseText?

/** Return `true´ if this provider has an id-specific license text for the given [licenseId] and [id]. */
open fun hasLicenseTextForId(licenseId: String, id: Identifier): Boolean =
getLicenseTextForId(licenseId, id) != null
/**
* Return `true´ if this provider has an id-specific license text for the given [singleLicenseExpression] and [id].
*/
open fun hasLicenseTextsForId(singleLicenseExpression: String, id: Identifier): Boolean =
getLicenseTextsForId(singleLicenseExpression, id).isNotEmpty()

/**
* Return an id-specific [LicenseText] for the given [licenseId] and [id], or `null` if no such text is
* available.
* Return all id-specific [LicenseText]s for the given [singleLicenseExpression] and [id], or an empty set if no
* such text is available.
*/
open fun getLicenseTextForId(licenseId: String, id: Identifier): LicenseText? = null
open fun getLicenseTextsForId(singleLicenseExpression: String, id: Identifier): Set<LicenseText> = emptySet()

/**
* Return an id-specific [LicenseText] for the given [licenseId] and [id] if available, or the non-id-specific
* license text for [licenseId], or `null` if no such text is available.
* Return `true` if any id-specific [LicenseText]s for the given [singleLicenseExpression] and [id], or if a
* non-id-specific license text for [singleLicenseExpression] is available, or `false` otherwise.
*/
fun hasLicenseText(licenseId: String, id: Identifier): Boolean =
hasLicenseText(licenseId, id) || hasLicenseText(licenseId)
fun hasLicenseTexts(singleLicenseExpression: String, id: Identifier): Boolean {

Check warning on line 50 in plugins/license-fact-providers/api/src/main/kotlin/LicenseFactProvider.kt

View workflow job for this annotation

GitHub Actions / qodana-scan

Unused symbol

Function "hasLicenseTexts" is never used

Check warning

Code scanning / QDJVM

Unused symbol Warning

Function "hasLicenseTexts" is never used
if (hasLicenseTextsForId(singleLicenseExpression, id)) return true

val spdxExpression = SpdxSingleLicenseExpression.parse(singleLicenseExpression)
if (!hasLicenseText(spdxExpression.simpleLicense())) return false

return spdxExpression.exception()?.let { hasLicenseText(it) } == true
}

/**
* Return an id-specific [LicenseText] for the given [licenseId] and [id] if available, or the non-id-specific
* license text for [licenseId], or `null` if no such text is available.
* Return an id-specific [LicenseText] for the given [singleLicenseExpression] and [id] if available, or the
* non-id-specific license text for [singleLicenseExpression], or `null` if no such text is available.
*/
fun getLicenseText(licenseId: String, id: Identifier): LicenseText? =
getLicenseTextForId(licenseId, id) ?: getLicenseText(licenseId)
fun getLicenseTexts(singleLicenseExpression: String, id: Identifier): Set<LicenseText> =
getLicenseTextsForId(singleLicenseExpression, id).takeIf { it.isNotEmpty() }
?: buildString {
val spdxExpression = SpdxSingleLicenseExpression.parse(singleLicenseExpression)
val licenseText = getLicenseText(spdxExpression.simpleLicense()) ?: return emptySet()
val exceptionText = spdxExpression.exception()?.let {
getLicenseText(it) ?: return emptySet()
}

/** Return a non-blank license text for the given [licenseId], or `null` if no valid text is available. */
@Deprecated("Java-only API", level = DeprecationLevel.HIDDEN)
@JvmName("getLicenseText")
fun getNonBlankLicenseText(licenseId: String): String? = getLicenseText(licenseId)?.text
append(licenseText.text)
if (exceptionText != null) {
appendLine()
append(exceptionText.text)
}

Check failure

Code scanning / detekt

Detects blank lines before rbraces Error

Unexpected blank line(s) before "}"
}.trim().let { setOf(LicenseText(it)) }

/**
* Return a non-blank id-specific [LicenseText] for the given [licenseId] and [id], or `null` if no such text is
* available.
* Return a non-blank license text for the given [licenseOrExceptionId], or `null` if no valid text is available.
*/
@Deprecated("Java-only API", level = DeprecationLevel.HIDDEN)
@JvmName("getLicenseTextForId")
fun getNonBlankLicenseTextForId(licenseId: String, id: Identifier): String? =
getLicenseTextForId(licenseId, id)?.text
@JvmName("getLicenseText")
fun getNonBlankLicenseText(licenseOrExceptionId: String): String? = getLicenseText(licenseOrExceptionId)?.text

@JvmName("getLicenseTextsStringForId")
fun getNonBlankLicenseTextsForId(singleLicenseExpression: String, id: Identifier): Set<String> =

Check warning on line 88 in plugins/license-fact-providers/api/src/main/kotlin/LicenseFactProvider.kt

View workflow job for this annotation

GitHub Actions / qodana-scan

Unused symbol

Function "getNonBlankLicenseTextsForId" is never used

Check warning

Code scanning / QDJVM

Unused symbol Warning

Function "getNonBlankLicenseTextsForId" is never used
getLicenseTextsForId(singleLicenseExpression, id).mapTo(mutableSetOf()) { it.text }

/**
* Return a non-blank id-specific [LicenseText] for the given [licenseId] and [id] if available, or the
* non-id-specific license text for [licenseId], or `null` if no such text is available.
* Return all non-blank id-specific [LicenseText] for the given [singleLicenseExpression] and [id] if available, or
* the non-id-specific license text for [singleLicenseExpression], or an empty set if no such text is available.
*/
@Deprecated("Java-only API", level = DeprecationLevel.HIDDEN)
@JvmName("getLicenseText")
fun getNonBlankLicenseText(licenseId: String, id: Identifier): String? = getLicenseText(licenseId, id)?.text
@JvmName("getLicenseTextsString")
fun getNonBlankLicenseTexts(singleLicenseExpression: String, id: Identifier): Set<String> =

Check warning on line 97 in plugins/license-fact-providers/api/src/main/kotlin/LicenseFactProvider.kt

View workflow job for this annotation

GitHub Actions / qodana-scan

Unused symbol

Function "getNonBlankLicenseTexts" is never used

Check warning

Code scanning / QDJVM

Unused symbol Warning

Function "getNonBlankLicenseTexts" is never used
getLicenseTexts(singleLicenseExpression, id).mapTo(mutableSetOf()) { it.text }
}
Loading
Loading