Skip to content

Commit c9f0fe7

Browse files
authored
feat: Properly parse properties in Maven POM (#75)
1 parent 8a5690f commit c9f0fe7

File tree

13 files changed

+399
-18
lines changed

13 files changed

+399
-18
lines changed

cli/src/commonMain/kotlin/com/deezer/caupain/cli/resolver/CLISelfUpdateResolver.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import io.ktor.client.request.header
3939
import io.ktor.http.HttpHeaders
4040
import io.ktor.http.HttpMessageBuilder
4141
import io.ktor.http.isSuccess
42+
import io.ktor.serialization.ContentConvertException
4243
import kotlinx.coroutines.CoroutineDispatcher
4344
import kotlinx.coroutines.withContext
4445
import kotlinx.io.IOException
@@ -68,10 +69,13 @@ internal class CLISelfUpdateResolver(
6869
?.body<GithubRelease>()
6970
?.tagName
7071
} catch (ignored: IOException) {
71-
logger.error("Failed to fetch latest release from $UPDATE_URL", ignored)
72+
logger.error(ERROR_MESSAGE, ignored)
7273
null
7374
} catch (ignored: SendCountExceedException) {
74-
logger.error("Failed to fetch latest release from $UPDATE_URL", ignored)
75+
logger.error(ERROR_MESSAGE, ignored)
76+
null
77+
} catch (ignored: ContentConvertException) {
78+
logger.error(ERROR_MESSAGE, ignored)
7579
null
7680
}
7781
} ?: return null
@@ -98,6 +102,7 @@ internal class CLISelfUpdateResolver(
98102
companion object {
99103
private val TAG_REGEX = Regex("v(.*)")
100104
const val UPDATE_URL = "https://api.github.com/repos/deezer/caupain/releases/latest"
105+
val ERROR_MESSAGE = "Failed to fetch latest release from $UPDATE_URL"
101106
}
102107
}
103108

core/src/commonMain/kotlin/com/deezer/caupain/DependencyUpdateChecker.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ import com.deezer.caupain.resolver.SelfUpdateResolver
5959
import com.deezer.caupain.resolver.UpdatedVersionResolver
6060
import com.deezer.caupain.serialization.DefaultJson
6161
import com.deezer.caupain.serialization.DefaultToml
62-
import com.deezer.caupain.serialization.DefaultXml
62+
import com.deezer.caupain.serialization.xml.DefaultXml
6363
import dev.drewhamilton.poko.Poko
6464
import io.ktor.client.HttpClient
6565
import io.ktor.client.plugins.HttpRequestRetry

core/src/commonMain/kotlin/com/deezer/caupain/internal/http.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import io.ktor.client.engine.HttpClientEngineConfig
3030
import io.ktor.client.plugins.SendCountExceedException
3131
import io.ktor.client.statement.HttpResponse
3232
import io.ktor.http.isSuccess
33+
import io.ktor.serialization.ContentConvertException
3334
import kotlinx.io.IOException
3435

3536
internal expect fun HttpClientEngineConfig.configureKtorEngine()
@@ -54,6 +55,9 @@ internal suspend inline fun <reified T, R> HttpClient.processRequest(
5455
} catch (ignored: SendCountExceedException) {
5556
onRecoverableError(ignored)
5657
default
58+
} catch (ignored: ContentConvertException) {
59+
onRecoverableError(ignored)
60+
default
5761
}
5862
}
5963

@@ -70,4 +74,4 @@ internal suspend inline fun <reified R> HttpClient.processRequest(
7074
onRecoverableError = onRecoverableError,
7175
executeRequest = executeRequest,
7276
)
73-
}
77+
}

core/src/commonMain/kotlin/com/deezer/caupain/model/maven/MavenInfo.kt

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,16 @@
2424

2525
package com.deezer.caupain.model.maven
2626

27+
import com.deezer.caupain.serialization.xml.MavenInfoSerializer
2728
import kotlinx.serialization.SerialName
2829
import kotlinx.serialization.Serializable
29-
import nl.adaptivity.xmlutil.serialization.XmlChildrenName
3030
import nl.adaptivity.xmlutil.serialization.XmlElement
3131

32-
@Serializable
33-
@SerialName("project")
32+
@Serializable(MavenInfoSerializer::class)
3433
internal data class MavenInfo(
35-
@XmlElement(true) val name: String? = null,
36-
@XmlElement(true) val url: String? = null,
37-
@XmlChildrenName("dependency") val dependencies: List<Dependency> = emptyList(),
34+
val name: String? = null,
35+
val url: String? = null,
36+
val dependencies: List<Dependency> = emptyList(),
3837
val scm: SCMInfos? = null,
3938
)
4039

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
@file:Suppress("UnusedImport") // Detekt bug
2+
3+
package com.deezer.caupain.serialization.xml
4+
5+
import nl.adaptivity.xmlutil.XmlDelegatingReader
6+
import nl.adaptivity.xmlutil.XmlReader
7+
import nl.adaptivity.xmlutil.localPart
8+
import nl.adaptivity.xmlutil.namespaceURI
9+
import nl.adaptivity.xmlutil.prefix
10+
import nl.adaptivity.xmlutil.serialization.structure.XmlDescriptor
11+
12+
internal class DynamicTagReader(
13+
private val idPropertyName: String,
14+
reader: XmlReader,
15+
descriptor: XmlDescriptor
16+
) : XmlDelegatingReader(reader) {
17+
18+
private val filterDepth = delegate.depth - reader.depth
19+
20+
private val elementName = descriptor.tagName
21+
22+
private val idAttrName = (0 until descriptor.elementsCount)
23+
.first { descriptor.serialDescriptor.getElementName(it) == idPropertyName }
24+
.let { descriptor.getElementDescriptor(it) }
25+
.tagName
26+
27+
private val idValue = delegate.localName
28+
29+
override val attributeCount: Int
30+
get() = if (filterDepth == 0) super.attributeCount + 1 else super.attributeCount
31+
32+
override fun getAttributeNamespace(index: Int): String = if (filterDepth == 0) {
33+
if (index == 0) idAttrName.namespaceURI else super.getAttributeNamespace(index - 1)
34+
} else {
35+
super.getAttributeNamespace(index)
36+
}
37+
38+
override fun getAttributePrefix(index: Int): String = if (filterDepth == 0) {
39+
if (index == 0) idAttrName.prefix else super.getAttributePrefix(index - 1)
40+
} else {
41+
super.getAttributePrefix(index)
42+
}
43+
44+
override fun getAttributeLocalName(index: Int): String = if (filterDepth == 0) {
45+
if (index == 0) idAttrName.localPart else super.getAttributeLocalName(index - 1)
46+
} else {
47+
super.getAttributeLocalName(index)
48+
}
49+
50+
override fun getAttributeValue(index: Int): String = if (filterDepth == 0) {
51+
if (index == 0) idValue else super.getAttributeValue(index - 1)
52+
} else {
53+
super.getAttributeValue(index)
54+
}
55+
56+
@Suppress("UnnecessaryParentheses") // More understandable this way
57+
override fun getAttributeValue(nsUri: String?, localName: String): String? =
58+
if (
59+
filterDepth == 0 &&
60+
nsUri.orEmpty() == idAttrName.namespaceURI &&
61+
localName == idAttrName.localPart
62+
) {
63+
idValue
64+
} else {
65+
super.getAttributeValue(nsUri, localName)
66+
}
67+
68+
override val namespaceURI: String
69+
get() = if (filterDepth == 0) elementName.namespaceURI else super.namespaceURI
70+
71+
override val localName: String
72+
get() = if (filterDepth == 0) elementName.localPart else super.localName
73+
74+
override val prefix: String
75+
get() = if (filterDepth == 0) elementName.prefix else super.prefix
76+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package com.deezer.caupain.serialization.xml
2+
3+
import com.deezer.caupain.model.maven.Dependency
4+
import com.deezer.caupain.model.maven.MavenInfo
5+
import com.deezer.caupain.model.maven.SCMInfos
6+
import kotlinx.serialization.KSerializer
7+
import kotlinx.serialization.SerialName
8+
import kotlinx.serialization.Serializable
9+
import kotlinx.serialization.descriptors.SerialDescriptor
10+
import kotlinx.serialization.encoding.Decoder
11+
import kotlinx.serialization.encoding.Encoder
12+
import kotlinx.serialization.serializer
13+
import nl.adaptivity.xmlutil.serialization.XmlChildrenName
14+
import nl.adaptivity.xmlutil.serialization.XmlElement
15+
import nl.adaptivity.xmlutil.serialization.XmlSerialName
16+
17+
@Serializable
18+
@SerialName("project")
19+
private data class RawMavenInfo(
20+
@XmlElement val name: String? = null,
21+
@XmlElement val url: String? = null,
22+
@XmlChildrenName("dependency") val dependencies: List<Dependency> = emptyList(),
23+
@XmlElement @XmlSerialName("scm") val scm: SCMInfos? = null,
24+
@XmlElement
25+
@XmlSerialName("properties")
26+
@Serializable(PropertiesMapSerializer::class)
27+
val properties: Map<String, String> = emptyMap(),
28+
) {
29+
constructor(mavenInfo: MavenInfo) : this(
30+
name = mavenInfo.name,
31+
url = mavenInfo.url,
32+
dependencies = mavenInfo.dependencies,
33+
scm = mavenInfo.scm,
34+
)
35+
36+
fun resolved(): MavenInfo {
37+
return if (properties.isEmpty()) {
38+
return MavenInfo(
39+
name = name,
40+
url = url,
41+
dependencies = dependencies,
42+
scm = scm,
43+
)
44+
} else {
45+
MavenInfo(
46+
name = name?.resolve(properties),
47+
url = url?.resolve(properties),
48+
dependencies = dependencies.map { dependency ->
49+
Dependency(
50+
groupId = dependency.groupId.resolve(properties),
51+
artifactId = dependency.artifactId.resolve(properties),
52+
version = dependency.version?.resolve(properties),
53+
)
54+
},
55+
scm = scm?.let { scmInfos ->
56+
SCMInfos(
57+
url = scmInfos.url?.resolve(properties)
58+
)
59+
}
60+
)
61+
}
62+
}
63+
64+
companion object {
65+
private fun String.resolve(properties: Map<String, String>): String {
66+
return PLACEHOLDER_REGEX.replace(this) { matchResult ->
67+
properties[matchResult.groupValues[1]] ?: matchResult.value
68+
}
69+
}
70+
71+
private val PLACEHOLDER_REGEX = "\\$\\{(.+?)\\}".toRegex()
72+
}
73+
}
74+
75+
internal object MavenInfoSerializer : KSerializer<MavenInfo> {
76+
77+
private val rawSerializer = serializer<RawMavenInfo>()
78+
79+
override val descriptor: SerialDescriptor
80+
get() = rawSerializer.descriptor
81+
82+
override fun serialize(
83+
encoder: Encoder,
84+
value: MavenInfo
85+
) {
86+
encoder.encodeSerializableValue(rawSerializer, RawMavenInfo(value))
87+
}
88+
89+
override fun deserialize(decoder: Decoder): MavenInfo {
90+
return decoder.decodeSerializableValue(rawSerializer).resolved()
91+
}
92+
}

0 commit comments

Comments
 (0)