Skip to content

Commit df473e8

Browse files
committed
refactor(opossum): Migrate the reporter to KxS
Migrate the OpossumReporter plugin away from Jackson serialization to use kotlinx-serialization. To get properly typed data structures for serialization the previous untyped `Map<*, *>` are substituted by properly typed data classes. This leads to the creation of an `OpossumInputCreator` class that holds intermediate data structures needed for processing of the Reporter input. Signed-off-by: alexzurbonsen <[email protected]> Signed-off-by: Sebastian Schuberth <[email protected]>
1 parent 2762981 commit df473e8

File tree

4 files changed

+410
-278
lines changed

4 files changed

+410
-278
lines changed

plugins/reporters/opossum/build.gradle.kts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
plugins {
2121
// Apply precompiled plugins.
2222
id("ort-plugin-conventions")
23+
24+
// Apply third-party plugins.
25+
alias(libs.plugins.kotlinSerialization)
2326
}
2427

2528
dependencies {
@@ -33,8 +36,9 @@ dependencies {
3336
implementation(projects.utils.ortUtils)
3437
implementation(projects.utils.spdxUtils)
3538

36-
implementation(libs.jackson.annotations)
37-
implementation(libs.jackson.databind)
39+
implementation(libs.bundles.ks3)
40+
implementation(libs.kotlinx.serialization.core)
41+
implementation(libs.kotlinx.serialization.json)
3842

3943
funTestImplementation(libs.kotest.assertions.json)
4044
}
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
/*
2+
* Copyright (C) 2024 The ORT Project Authors (see <https://github.com/oss-review-toolkit/ort/blob/main/NOTICE>)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
* License-Filename: LICENSE
18+
*/
19+
20+
package org.ossreviewtoolkit.plugins.reporters.opossum
21+
22+
import io.ks3.java.typealiases.LocalDateTimeAsString
23+
import io.ks3.java.typealiases.UuidAsString
24+
25+
import java.io.File
26+
import java.time.LocalDateTime
27+
28+
import kotlinx.serialization.KSerializer
29+
import kotlinx.serialization.Serializable
30+
import kotlinx.serialization.builtins.MapSerializer
31+
import kotlinx.serialization.builtins.serializer
32+
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
33+
import kotlinx.serialization.encoding.Decoder
34+
import kotlinx.serialization.encoding.Encoder
35+
import kotlinx.serialization.json.Json
36+
import kotlinx.serialization.json.encodeToStream
37+
38+
import org.ossreviewtoolkit.model.Identifier
39+
import org.ossreviewtoolkit.model.utils.getPurlType
40+
import org.ossreviewtoolkit.utils.spdx.SpdxExpression
41+
42+
internal val JSON = Json {
43+
explicitNulls = false
44+
encodeDefaults = true
45+
}
46+
47+
internal fun File.writeReport(opossumInput: OpossumInput): File =
48+
apply { outputStream().use { JSON.encodeToStream(opossumInput, it) } }
49+
50+
@Serializable
51+
internal data class OpossumInput(
52+
val metadata: OpossumInputMetadata = OpossumInputMetadata(),
53+
val resources: OpossumResources,
54+
val externalAttributions: Map<UuidAsString, OpossumSignalFlat>,
55+
val resourcesToAttributions: Map<String, Set<UuidAsString>>,
56+
val attributionBreakpoints: Set<String>,
57+
val filesWithChildren: Set<String>,
58+
val frequentLicenses: Set<OpossumFrequentLicense>,
59+
val baseUrlsForSources: Map<String, String>,
60+
val externalAttributionSources: Map<String, OpossumExternalAttributionSource>
61+
) {
62+
fun getSignalsForFile(file: String): List<OpossumSignalFlat> =
63+
resourcesToAttributions[file].orEmpty().mapNotNull { uuid -> externalAttributions[uuid] }
64+
}
65+
66+
@Serializable
67+
internal data class OpossumInputMetadata(
68+
val projectId: String = "0",
69+
val fileCreationDate: LocalDateTimeAsString = LocalDateTime.now()
70+
)
71+
72+
@Serializable(OpossumResourcesSerializer::class)
73+
internal data class OpossumResources(
74+
val tree: MutableMap<String, OpossumResources> = mutableMapOf()
75+
) {
76+
fun addResource(pathPieces: List<String>) {
77+
if (pathPieces.isEmpty()) {
78+
return
79+
}
80+
81+
val head = pathPieces.first()
82+
val tail = pathPieces.drop(1)
83+
84+
if (head !in tree) {
85+
tree[head] = OpossumResources()
86+
}
87+
88+
tree.getValue(head).addResource(tail)
89+
}
90+
91+
fun addResource(path: String) {
92+
val pathPieces = path.split("/").filter { it.isNotEmpty() }
93+
94+
addResource(pathPieces)
95+
}
96+
97+
fun isFile() = tree.isEmpty()
98+
99+
fun isPathAFile(path: String): Boolean {
100+
val pathPieces = path.split("/").filter { it.isNotEmpty() }
101+
102+
return isPathAFile(pathPieces)
103+
}
104+
105+
fun isPathAFile(pathPieces: List<String>): Boolean {
106+
if (pathPieces.isEmpty()) {
107+
return isFile()
108+
}
109+
110+
val head = pathPieces.first()
111+
val tail = pathPieces.drop(1)
112+
113+
return head !in tree || tree.getValue(head).isPathAFile(tail)
114+
}
115+
116+
fun toFileList(): Set<String> =
117+
tree.flatMapTo(mutableSetOf()) { (key, value) ->
118+
value.toFileList().map { resolvePath(key, it, isDirectory = false) }
119+
}.plus("/")
120+
}
121+
122+
private object OpossumResourcesSerializer : KSerializer<OpossumResources> {
123+
override val descriptor = buildClassSerialDescriptor("Resource")
124+
125+
override fun serialize(encoder: Encoder, value: OpossumResources) {
126+
if (value.isFile()) {
127+
encoder.encodeInt(1)
128+
} else {
129+
encoder.encodeSerializableValue(MapSerializer(String.serializer(), this), value.tree)
130+
}
131+
}
132+
133+
override fun deserialize(decoder: Decoder): OpossumResources {
134+
throw NotImplementedError("Deserialization of OpossumResources is not supported.")
135+
}
136+
}
137+
138+
@Serializable
139+
internal data class OpossumSignalFlat(
140+
val source: OpossumSignalSource,
141+
val attributionConfidence: Int = 80,
142+
val packageType: String?,
143+
val packageNamespace: String?,
144+
val packageName: String?,
145+
val packageVersion: String?,
146+
val copyright: String?,
147+
val licenseName: String?,
148+
val url: String?,
149+
val preSelected: Boolean,
150+
val followUp: OpossumFollowUp?,
151+
val excludeFromNotice: Boolean,
152+
val comment: String?
153+
) {
154+
companion object {
155+
fun create(signal: OpossumSignal): OpossumSignalFlat =
156+
OpossumSignalFlat(
157+
source = signal.base.source,
158+
attributionConfidence = signal.attributionConfidence,
159+
packageType = signal.base.packageType,
160+
packageNamespace = signal.base.packageNamespace,
161+
packageName = signal.base.packageName,
162+
packageVersion = signal.base.packageVersion,
163+
copyright = signal.base.copyright,
164+
licenseName = signal.base.licenseName,
165+
url = signal.base.url,
166+
preSelected = signal.base.preSelected,
167+
followUp = signal.followUp,
168+
excludeFromNotice = signal.excludeFromNotice,
169+
comment = signal.base.comment
170+
)
171+
}
172+
173+
data class OpossumSignal(
174+
val base: OpossumSignalBase,
175+
val attributionConfidence: Int = 80,
176+
val followUp: OpossumFollowUp?,
177+
val excludeFromNotice: Boolean
178+
) {
179+
companion object {
180+
@Suppress("LongParameterList")
181+
fun create(
182+
source: String,
183+
id: Identifier? = null,
184+
url: String? = null,
185+
license: SpdxExpression? = null,
186+
copyright: String? = null,
187+
comment: String? = null,
188+
preSelected: Boolean = false,
189+
followUp: Boolean = false,
190+
excludeFromNotice: Boolean = false
191+
): OpossumSignal =
192+
OpossumSignal(
193+
base = OpossumSignalBase(
194+
source = OpossumSignalSource(name = source),
195+
packageType = id?.getPurlType().toString(),
196+
packageNamespace = id?.namespace,
197+
packageName = id?.name,
198+
packageVersion = id?.version,
199+
copyright = copyright,
200+
licenseName = license?.toString(),
201+
url = url,
202+
preSelected = preSelected,
203+
comment = comment
204+
),
205+
followUp = OpossumFollowUp.FOLLOW_UP.takeIf { followUp },
206+
excludeFromNotice = excludeFromNotice
207+
)
208+
}
209+
210+
data class OpossumSignalBase(
211+
val source: OpossumSignalSource,
212+
val packageType: String?,
213+
val packageNamespace: String?,
214+
val packageName: String?,
215+
val packageVersion: String?,
216+
val copyright: String?,
217+
val licenseName: String?,
218+
val url: String?,
219+
val preSelected: Boolean,
220+
val comment: String?
221+
)
222+
}
223+
}
224+
225+
@Serializable
226+
internal data class OpossumSignalSource(
227+
val name: String,
228+
val documentConfidence: Int = 80
229+
)
230+
231+
internal enum class OpossumFollowUp {
232+
FOLLOW_UP
233+
}
234+
235+
@Serializable
236+
internal data class OpossumFrequentLicense(
237+
val shortName: String,
238+
val fullName: String?,
239+
val defaultText: String?
240+
) : Comparable<OpossumFrequentLicense> {
241+
override fun compareTo(other: OpossumFrequentLicense) =
242+
compareValuesBy(
243+
this,
244+
other,
245+
{ it.shortName },
246+
{ it.fullName },
247+
{ it.defaultText }
248+
)
249+
}
250+
251+
@Serializable
252+
internal data class OpossumExternalAttributionSource(
253+
val name: String,
254+
val priority: Int
255+
)

0 commit comments

Comments
 (0)