Skip to content

Commit 9f1bfe4

Browse files
fviernausschuberth
authored andcommitted
refactor(fossid-webapp): Extract serializers to dedicated files
Improve the readability. Signed-off-by: Frank Viernau <[email protected]>
1 parent 897a353 commit 9f1bfe4

File tree

4 files changed

+220
-145
lines changed

4 files changed

+220
-145
lines changed

clients/fossid-webapp/src/main/kotlin/FossIdRestService.kt

Lines changed: 0 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,10 @@
2121

2222
package org.ossreviewtoolkit.clients.fossid
2323

24-
import com.fasterxml.jackson.core.JsonParser
25-
import com.fasterxml.jackson.core.JsonToken
26-
import com.fasterxml.jackson.databind.BeanProperty
27-
import com.fasterxml.jackson.databind.DeserializationContext
2824
import com.fasterxml.jackson.databind.DeserializationFeature
29-
import com.fasterxml.jackson.databind.JavaType
30-
import com.fasterxml.jackson.databind.JsonDeserializer
3125
import com.fasterxml.jackson.databind.MapperFeature
3226
import com.fasterxml.jackson.databind.ObjectMapper
3327
import com.fasterxml.jackson.databind.PropertyNamingStrategies
34-
import com.fasterxml.jackson.databind.deser.ContextualDeserializer
35-
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
3628
import com.fasterxml.jackson.module.kotlin.jsonMapper
3729
import com.fasterxml.jackson.module.kotlin.kotlinModule
3830

@@ -89,143 +81,6 @@ interface FossIdRestService {
8981
)
9082
}
9183

92-
/**
93-
* A class to modify the standard Jackson deserialization to deal with inconsistencies in responses
94-
* sent by the FossID server.
95-
* FossID usually returns data as a List or Map, but in case of no entries it returns a Boolean (which is set to
96-
* false). This custom deserializer streamlines the result:
97-
* - maps are converted to lists by ignoring the keys
98-
* - empty list is returned when the result is Boolean
99-
* - to address a FossID bug in get_all_scans operation, arrays are converted to list.
100-
*/
101-
private class PolymorphicListDeserializer(val boundType: JavaType? = null) :
102-
StdDeserializer<PolymorphicList<Any>>(PolymorphicList::class.java), ContextualDeserializer {
103-
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): PolymorphicList<Any> {
104-
requireNotNull(boundType) {
105-
"The PolymorphicListDeserializer needs a type to deserialize values!"
106-
}
107-
108-
return when (p.currentToken) {
109-
JsonToken.VALUE_FALSE -> PolymorphicList()
110-
JsonToken.START_ARRAY -> {
111-
val arrayType = ctxt.typeFactory.constructArrayType(boundType)
112-
val array = ctxt.readValue<Array<Any>>(p, arrayType)
113-
PolymorphicList(array.toList())
114-
}
115-
116-
JsonToken.START_OBJECT -> {
117-
val mapType = ctxt.typeFactory.constructMapType(
118-
LinkedHashMap::class.java,
119-
String::class.java,
120-
boundType.rawClass
121-
)
122-
val map = ctxt.readValue<Map<Any, Any>>(p, mapType)
123-
124-
// Only keep the map's values: If the FossID functions which return a PolymorphicList return a
125-
// map, it always is the list of elements grouped by id. Since the ids are also present in the
126-
// elements themselves, no information is lost by discarding the keys.
127-
PolymorphicList(map.values.toList())
128-
}
129-
130-
else -> error("FossID returned a type not handled by this deserializer!")
131-
}
132-
}
133-
134-
override fun createContextual(ctxt: DeserializationContext?, property: BeanProperty?): JsonDeserializer<*> {
135-
// Extract the type from the property, i.e. the T in PolymorphicList.data<T>
136-
val type = property?.member?.type?.bindings?.getBoundType(0)
137-
return PolymorphicListDeserializer(type)
138-
}
139-
}
140-
141-
/**
142-
* A custom JSON deserializer implementation to deal with inconsistencies in error responses sent by FossID
143-
* for requests returning a single value. If such a request fails, the response from FossID contains an
144-
* empty array for the value, which cannot be handled by the default deserialization.
145-
*/
146-
private class PolymorphicDataDeserializer(val boundType: JavaType? = null) :
147-
StdDeserializer<PolymorphicData<Any>>(PolymorphicData::class.java), ContextualDeserializer {
148-
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): PolymorphicData<Any> {
149-
requireNotNull(boundType) {
150-
"The PolymorphicDataDeserializer needs a type to deserialize values!"
151-
}
152-
153-
return when (p.currentToken) {
154-
JsonToken.START_ARRAY -> {
155-
val arrayType = ctxt.typeFactory.constructArrayType(boundType)
156-
val array = ctxt.readValue<Array<Any>>(p, arrayType)
157-
PolymorphicData(array.firstOrNull())
158-
}
159-
160-
JsonToken.START_OBJECT -> {
161-
val data = ctxt.readValue<Any>(p, boundType)
162-
PolymorphicData(data)
163-
}
164-
165-
else -> {
166-
val delegate = ctxt.findNonContextualValueDeserializer(boundType)
167-
PolymorphicData(delegate.deserialize(p, ctxt))
168-
}
169-
}
170-
}
171-
172-
override fun createContextual(ctxt: DeserializationContext?, property: BeanProperty?): JsonDeserializer<*> {
173-
val type = property?.member?.type?.bindings?.getBoundType(0)
174-
return PolymorphicDataDeserializer(type)
175-
}
176-
}
177-
178-
/**
179-
* A class to modify the standard Jackson deserialization to deal with inconsistencies in responses
180-
* sent by the FossID server.
181-
* When deleting a scan, FossID returns the scan id as a string in the `data` property of the response. If no
182-
* scan could be found, it returns an empty array. Starting with FossID version 2023.1, the return type of the
183-
* [deleteScan] function is now a map of strings to strings. Creating a special [FossIdServiceWithVersion]
184-
* implementation for this call is an overkill as ORT does not even use the return value. Therefore, this change
185-
* is also handled by the [PolymorphicIntDeserializer].
186-
* This custom deserializer streamlines the result: everything is converted to Int and empty array is converted
187-
* to `null`. This deserializer also accepts primitive integers and arrays containing integers and maps of
188-
* strings to strings containing a single entry with an integer value.
189-
*/
190-
private class PolymorphicIntDeserializer :
191-
StdDeserializer<PolymorphicInt>(PolymorphicInt::class.java) {
192-
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): PolymorphicInt {
193-
return when (p.currentToken) {
194-
JsonToken.VALUE_STRING -> {
195-
val value = ctxt.readValue(p, String::class.java)
196-
PolymorphicInt(value.toInt())
197-
}
198-
199-
JsonToken.VALUE_NUMBER_INT -> {
200-
val value = ctxt.readValue(p, Int::class.java)
201-
PolymorphicInt(value)
202-
}
203-
204-
JsonToken.START_ARRAY -> {
205-
val array = ctxt.readValue(p, IntArray::class.java)
206-
val value = if (array.isEmpty()) null else array.first()
207-
PolymorphicInt(value)
208-
}
209-
210-
JsonToken.START_OBJECT -> {
211-
val mapType = ctxt.typeFactory.constructMapType(
212-
LinkedHashMap::class.java,
213-
String::class.java,
214-
String::class.java
215-
)
216-
val map = ctxt.readValue<Map<Any, Any>>(p, mapType)
217-
if (map.size != 1) {
218-
error("A map representing a polymorphic integer should have one value!")
219-
}
220-
221-
PolymorphicInt(map.values.first().toString().toInt())
222-
}
223-
224-
else -> error("FossID returned a type not handled by this deserializer!")
225-
}
226-
}
227-
}
228-
22984
/**
23085
* Create the [FossIdServiceWithVersion] to interact with the FossID instance running at the given [url],
23186
* optionally using a pre-built OkHttp [client].
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright (C) 2020 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.clients.fossid
21+
22+
import com.fasterxml.jackson.core.JsonParser
23+
import com.fasterxml.jackson.core.JsonToken
24+
import com.fasterxml.jackson.databind.BeanProperty
25+
import com.fasterxml.jackson.databind.DeserializationContext
26+
import com.fasterxml.jackson.databind.JavaType
27+
import com.fasterxml.jackson.databind.JsonDeserializer
28+
import com.fasterxml.jackson.databind.deser.ContextualDeserializer
29+
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
30+
31+
/**
32+
* A custom JSON deserializer implementation to deal with inconsistencies in error responses sent by FossID
33+
* for requests returning a single value. If such a request fails, the response from FossID contains an
34+
* empty array for the value, which cannot be handled by the default deserialization.
35+
*/
36+
internal class PolymorphicDataDeserializer(val boundType: JavaType? = null) :
37+
StdDeserializer<PolymorphicData<Any>>(PolymorphicData::class.java), ContextualDeserializer {
38+
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): PolymorphicData<Any> {
39+
requireNotNull(boundType) {
40+
"The PolymorphicDataDeserializer needs a type to deserialize values!"
41+
}
42+
43+
return when (p.currentToken) {
44+
JsonToken.START_ARRAY -> {
45+
val arrayType = ctxt.typeFactory.constructArrayType(boundType)
46+
val array = ctxt.readValue<Array<Any>>(p, arrayType)
47+
PolymorphicData(array.firstOrNull())
48+
}
49+
50+
JsonToken.START_OBJECT -> {
51+
val data = ctxt.readValue<Any>(p, boundType)
52+
PolymorphicData(data)
53+
}
54+
55+
else -> {
56+
val delegate = ctxt.findNonContextualValueDeserializer(boundType)
57+
PolymorphicData(delegate.deserialize(p, ctxt))
58+
}
59+
}
60+
}
61+
62+
override fun createContextual(ctxt: DeserializationContext?, property: BeanProperty?): JsonDeserializer<*> {
63+
val type = property?.member?.type?.bindings?.getBoundType(0)
64+
return PolymorphicDataDeserializer(type)
65+
}
66+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright (C) 2020 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.clients.fossid
21+
22+
import com.fasterxml.jackson.core.JsonParser
23+
import com.fasterxml.jackson.core.JsonToken
24+
import com.fasterxml.jackson.databind.DeserializationContext
25+
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
26+
27+
/**
28+
* A class to modify the standard Jackson deserialization to deal with inconsistencies in responses
29+
* sent by the FossID server.
30+
* When deleting a scan, FossID returns the scan id as a string in the `data` property of the response. If no
31+
* scan could be found, it returns an empty array. Starting with FossID version 2023.1, the return type of the
32+
* [deleteScan] function is now a map of strings to strings. Creating a special [FossIdServiceWithVersion]
33+
* implementation for this call is an overkill as ORT does not even use the return value. Therefore, this change
34+
* is also handled by the [PolymorphicIntDeserializer].
35+
* This custom deserializer streamlines the result: everything is converted to Int and empty array is converted
36+
* to `null`. This deserializer also accepts primitive integers and arrays containing integers and maps of
37+
* strings to strings containing a single entry with an integer value.
38+
*/
39+
internal class PolymorphicIntDeserializer :
40+
StdDeserializer<PolymorphicInt>(PolymorphicInt::class.java) {
41+
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): PolymorphicInt {
42+
return when (p.currentToken) {
43+
JsonToken.VALUE_STRING -> {
44+
val value = ctxt.readValue(p, String::class.java)
45+
PolymorphicInt(value.toInt())
46+
}
47+
48+
JsonToken.VALUE_NUMBER_INT -> {
49+
val value = ctxt.readValue(p, Int::class.java)
50+
PolymorphicInt(value)
51+
}
52+
53+
JsonToken.START_ARRAY -> {
54+
val array = ctxt.readValue(p, IntArray::class.java)
55+
val value = if (array.isEmpty()) null else array.first()
56+
PolymorphicInt(value)
57+
}
58+
59+
JsonToken.START_OBJECT -> {
60+
val mapType = ctxt.typeFactory.constructMapType(
61+
LinkedHashMap::class.java,
62+
String::class.java,
63+
String::class.java
64+
)
65+
val map = ctxt.readValue<Map<Any, Any>>(p, mapType)
66+
if (map.size != 1) {
67+
error("A map representing a polymorphic integer should have one value!")
68+
}
69+
70+
PolymorphicInt(map.values.first().toString().toInt())
71+
}
72+
73+
else -> error("FossID returned a type not handled by this deserializer!")
74+
}
75+
}
76+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright (C) 2020 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.clients.fossid
21+
22+
import com.fasterxml.jackson.core.JsonParser
23+
import com.fasterxml.jackson.core.JsonToken
24+
import com.fasterxml.jackson.databind.BeanProperty
25+
import com.fasterxml.jackson.databind.DeserializationContext
26+
import com.fasterxml.jackson.databind.JavaType
27+
import com.fasterxml.jackson.databind.JsonDeserializer
28+
import com.fasterxml.jackson.databind.deser.ContextualDeserializer
29+
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
30+
31+
/**
32+
* A class to modify the standard Jackson deserialization to deal with inconsistencies in responses
33+
* sent by the FossID server.
34+
* FossID usually returns data as a List or Map, but in case of no entries it returns a Boolean (which is set to
35+
* false). This custom deserializer streamlines the result:
36+
* - maps are converted to lists by ignoring the keys
37+
* - empty list is returned when the result is Boolean
38+
* - to address a FossID bug in get_all_scans operation, arrays are converted to list.
39+
*/
40+
internal class PolymorphicListDeserializer(val boundType: JavaType? = null) :
41+
StdDeserializer<PolymorphicList<Any>>(PolymorphicList::class.java), ContextualDeserializer {
42+
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): PolymorphicList<Any> {
43+
requireNotNull(boundType) {
44+
"The PolymorphicListDeserializer needs a type to deserialize values!"
45+
}
46+
47+
return when (p.currentToken) {
48+
JsonToken.VALUE_FALSE -> PolymorphicList()
49+
JsonToken.START_ARRAY -> {
50+
val arrayType = ctxt.typeFactory.constructArrayType(boundType)
51+
val array = ctxt.readValue<Array<Any>>(p, arrayType)
52+
PolymorphicList(array.toList())
53+
}
54+
55+
JsonToken.START_OBJECT -> {
56+
val mapType = ctxt.typeFactory.constructMapType(
57+
LinkedHashMap::class.java,
58+
String::class.java,
59+
boundType.rawClass
60+
)
61+
val map = ctxt.readValue<Map<Any, Any>>(p, mapType)
62+
63+
// Only keep the map's values: If the FossID functions which return a PolymorphicList return a
64+
// map, it always is the list of elements grouped by id. Since the ids are also present in the
65+
// elements themselves, no information is lost by discarding the keys.
66+
PolymorphicList(map.values.toList())
67+
}
68+
69+
else -> error("FossID returned a type not handled by this deserializer!")
70+
}
71+
}
72+
73+
override fun createContextual(ctxt: DeserializationContext?, property: BeanProperty?): JsonDeserializer<*> {
74+
// Extract the type from the property, i.e. the T in PolymorphicList.data<T>
75+
val type = property?.member?.type?.bindings?.getBoundType(0)
76+
return PolymorphicListDeserializer(type)
77+
}
78+
}

0 commit comments

Comments
 (0)