Skip to content

Commit 8338e39

Browse files
authored
Merge pull request #124 from ForteScarlet/support-jvm-reactive
support JvmReactive
2 parents c27022c + 70aa2f0 commit 8338e39

32 files changed

Lines changed: 1428 additions & 40 deletions

File tree

compiler/suspend-transform-plugin-cli/src/test/kotlin/love/forte/plugin/suspendtrans/cli/ConfigurationSerializeTests.kt

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import love.forte.plugin.suspendtrans.configuration.*
44
import love.forte.plugin.suspendtrans.configuration.SuspendTransformConfigurations.jsPromiseTransformer
55
import love.forte.plugin.suspendtrans.configuration.SuspendTransformConfigurations.jvmAsyncTransformer
66
import love.forte.plugin.suspendtrans.configuration.SuspendTransformConfigurations.jvmBlockingTransformer
7+
import love.forte.plugin.suspendtrans.configuration.SuspendTransformConfigurations.jvmReactiveMarkAnnotationClassInfo
8+
import love.forte.plugin.suspendtrans.configuration.SuspendTransformConfigurations.jvmReactiveTransformer
79
import kotlin.test.Test
810
import kotlin.test.assertEquals
911

@@ -75,12 +77,30 @@ class ConfigurationSerializeTests {
7577
)
7678
}
7779

80+
@OptIn(InternalSuspendTransformConfigurationApi::class)
81+
@Test
82+
fun testDecodeJvmReactiveTransformer() {
83+
val config = SuspendTransformConfiguration(
84+
transformers = mapOf(TargetPlatform.JVM to listOf(jvmReactiveTransformer))
85+
)
86+
87+
val decoded = decodeSuspendTransformConfigurationFromHex(config.encodeToHex())
88+
assertEquals(config, decoded)
89+
assertEquals(
90+
TransformReturnTypeGenericMode.NON_NULL,
91+
decoded.transformers.getValue(TargetPlatform.JVM).single().transformReturnTypeGenericMode
92+
)
93+
}
94+
7895
@OptIn(InternalSuspendTransformConfigurationApi::class)
7996
@Test
8097
fun testDecodeLegacyWithoutTransformReturnTypeGenericMode() {
8198
val config = SuspendTransformConfiguration(
8299
transformers = mapOf(
83-
TargetPlatform.JVM to listOf(jvmBlockingTransformer, jvmAsyncTransformer),
100+
TargetPlatform.JVM to listOf(
101+
jvmBlockingTransformer.withoutJvmReactiveCopyExclude(),
102+
jvmAsyncTransformer.withoutJvmReactiveCopyExclude(),
103+
),
84104
TargetPlatform.JS to listOf(jsPromiseTransformer),
85105
)
86106
)
@@ -113,7 +133,7 @@ class ConfigurationSerializeTests {
113133
val hex = config.encodeToHex()
114134

115135
assertEquals(
116-
"0abe0808011288040a95010a3c0a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e120b4a766d426c6f636b696e67180020001208626173654e616d651a06737566666978220a617350726f70657274792a08426c6f636b696e6730003a2b0a086d61726b4e616d6512190a0a6b6f746c696e2e6a766d12074a766d4e616d65180020001a046e616d6512390a266c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e72756e74696d65120f2472756e496e426c6f636b696e672420002a240a1e0a0a6b6f746c696e2e6a766d120c4a766d53796e7468657469631800200010001800323c0a360a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e1205417069344a18002000100018013801421e0a0a6b6f746c696e2e6a766d120c4a766d53796e74686574696318002000423c0a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e120b4a766d426c6f636b696e671800200042390a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e12084a766d4173796e631800200042130a066b6f746c696e12054f7074496e1800200042190a0a6b6f746c696e2e6a766d12074a766d4e616d65180020004800500012ae040a8f010a390a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e12084a766d4173796e63180020001208626173654e616d651a06737566666978220a617350726f70657274792a054173796e6330003a2b0a086d61726b4e616d6512190a0a6b6f746c696e2e6a766d12074a766d4e616d65180020001a046e616d6512360a266c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e72756e74696d65120c2472756e496e4173796e63241a2d0a146a6176612e7574696c2e636f6e63757272656e741211436f6d706c657461626c654675747572651800200020012a240a1e0a0a6b6f746c696e2e6a766d120c4a766d53796e7468657469631800200010001800323c0a360a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e1205417069344a18002000100018013801421e0a0a6b6f746c696e2e6a766d120c4a766d53796e74686574696318002000423c0a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e120b4a766d426c6f636b696e671800200042390a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e12084a766d4173796e631800200042130a066b6f746c696e12054f7074496e1800200042190a0a6b6f746c696e2e6a766d12074a766d4e616d6518002000480050000a990308021294030a8e010a3a0a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e12094a7350726f6d697365180020001208626173654e616d651a06737566666978220a617350726f70657274792a054173796e6330003a290a086d61726b4e616d6512170a096b6f746c696e2e6a7312064a734e616d65180020001a046e616d6512360a266c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e72756e74696d65120c2472756e496e4173796e63241a180a096b6f746c696e2e6a73120750726f6d697365180020002001323d0a370a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e1206417069344a7318002000100018013801423a0a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e12094a7350726f6d6973651800200042130a066b6f746c696e12054f7074496e1800200042170a096b6f746c696e2e6a7312064a734e616d651800200048005000",
136+
"0aba09080112c6040a95010a3c0a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e120b4a766d426c6f636b696e67180020001208626173654e616d651a06737566666978220a617350726f70657274792a08426c6f636b696e6730003a2b0a086d61726b4e616d6512190a0a6b6f746c696e2e6a766d12074a766d4e616d65180020001a046e616d6512390a266c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e72756e74696d65120f2472756e496e426c6f636b696e672420002a240a1e0a0a6b6f746c696e2e6a766d120c4a766d53796e7468657469631800200010001800323c0a360a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e1205417069344a18002000100018013801421e0a0a6b6f746c696e2e6a766d120c4a766d53796e74686574696318002000423c0a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e120b4a766d426c6f636b696e671800200042390a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e12084a766d4173796e6318002000423c0a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e120b4a766d52656163746976651800200042130a066b6f746c696e12054f7074496e1800200042190a0a6b6f746c696e2e6a766d12074a766d4e616d65180020004800500012ec040a8f010a390a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e12084a766d4173796e63180020001208626173654e616d651a06737566666978220a617350726f70657274792a054173796e6330003a2b0a086d61726b4e616d6512190a0a6b6f746c696e2e6a766d12074a766d4e616d65180020001a046e616d6512360a266c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e72756e74696d65120c2472756e496e4173796e63241a2d0a146a6176612e7574696c2e636f6e63757272656e741211436f6d706c657461626c654675747572651800200020012a240a1e0a0a6b6f746c696e2e6a766d120c4a766d53796e7468657469631800200010001800323c0a360a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e1205417069344a18002000100018013801421e0a0a6b6f746c696e2e6a766d120c4a766d53796e74686574696318002000423c0a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e120b4a766d426c6f636b696e671800200042390a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e12084a766d4173796e6318002000423c0a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e120b4a766d52656163746976651800200042130a066b6f746c696e12054f7074496e1800200042190a0a6b6f746c696e2e6a766d12074a766d4e616d6518002000480050000a990308021294030a8e010a3a0a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e12094a7350726f6d697365180020001208626173654e616d651a06737566666978220a617350726f70657274792a054173796e6330003a290a086d61726b4e616d6512170a096b6f746c696e2e6a7312064a734e616d65180020001a046e616d6512360a266c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e72756e74696d65120c2472756e496e4173796e63241a180a096b6f746c696e2e6a73120750726f6d697365180020002001323d0a370a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e1206417069344a7318002000100018013801423a0a296c6f76652e666f7274652e706c7567696e2e73757370656e647472616e732e616e6e6f746174696f6e12094a7350726f6d6973651800200042130a066b6f746c696e12054f7074496e1800200042170a096b6f746c696e2e6a7312064a734e616d651800200048005000",
117137
hex
118138
)
119139

@@ -124,3 +144,18 @@ class ConfigurationSerializeTests {
124144
}
125145

126146
}
147+
148+
@OptIn(InternalSuspendTransformConfigurationApi::class)
149+
private fun Transformer.withoutJvmReactiveCopyExclude(): Transformer =
150+
Transformer(
151+
markAnnotation = markAnnotation,
152+
transformFunctionInfo = transformFunctionInfo,
153+
transformReturnType = transformReturnType,
154+
transformReturnTypeGeneric = transformReturnTypeGeneric,
155+
originFunctionIncludeAnnotations = originFunctionIncludeAnnotations,
156+
syntheticFunctionIncludeAnnotations = syntheticFunctionIncludeAnnotations,
157+
copyAnnotationsToSyntheticFunction = copyAnnotationsToSyntheticFunction,
158+
copyAnnotationExcludes = copyAnnotationExcludes.filterNot { it == jvmReactiveMarkAnnotationClassInfo },
159+
copyAnnotationsToSyntheticProperty = copyAnnotationsToSyntheticProperty,
160+
transformReturnTypeGenericMode = transformReturnTypeGenericMode,
161+
)

compiler/suspend-transform-plugin-configuration/src/main/kotlin/love/forte/plugin/suspendtrans/configuration/SuspendTransformConfiguration.kt

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
package love.forte.plugin.suspendtrans.configuration
2424

2525
import kotlinx.serialization.Serializable
26+
import love.forte.plugin.suspendtrans.configuration.SuspendTransformConfigurations.jvmReactiveTransformer
2627

2728
// NOTE:
2829
// 配置信息均使用 `Protobuf` 进行序列化
@@ -505,6 +506,7 @@ object SuspendTransformConfigurations {
505506

506507
private const val JVM_RUN_IN_BLOCKING_FUNCTION_FUNCTION_NAME = "\$runInBlocking$"
507508
private const val JVM_RUN_IN_ASYNC_FUNCTION_FUNCTION_NAME = "\$runInAsync$"
509+
private const val JVM_RUN_IN_REACTIVE_FUNCTION_FUNCTION_NAME = "\$runInReactive$"
508510

509511
private const val JS_RUN_IN_ASYNC_FUNCTION_FUNCTION_NAME = "\$runInAsync$"
510512

@@ -586,6 +588,56 @@ object SuspendTransformConfigurations {
586588
functionName = JVM_RUN_IN_ASYNC_FUNCTION_FUNCTION_NAME,
587589
)
588590

591+
/**
592+
* The `love.forte.plugin.suspendtrans.annotation.JvmReactive`.
593+
*
594+
* @since 0.14.0
595+
*/
596+
@JvmStatic
597+
val jvmReactiveMarkAnnotationClassInfo = ClassInfo(
598+
packageName = SUSPENDTRANS_ANNOTATION_PACKAGE,
599+
className = "JvmReactive"
600+
)
601+
602+
/**
603+
* Default mark annotation mapping for [jvmReactiveTransformer].
604+
*
605+
* @since 0.14.0
606+
*/
607+
@JvmStatic
608+
val jvmReactiveAnnotationInfo = MarkAnnotation(
609+
classInfo = jvmReactiveMarkAnnotationClassInfo,
610+
defaultSuffix = "Reactive",
611+
markNameProperty = MarkNameProperty(
612+
propertyName = "markName",
613+
annotation = jvmNameAnnotationClassInfo,
614+
annotationMarkNamePropertyName = "name"
615+
)
616+
)
617+
618+
/**
619+
* Runtime transform function used by [jvmReactiveTransformer].
620+
*
621+
* @since 0.14.0
622+
*/
623+
@JvmStatic
624+
val jvmReactiveTransformFunction = FunctionInfo(
625+
packageName = SUSPENDTRANS_RUNTIME_PACKAGE,
626+
functionName = JVM_RUN_IN_REACTIVE_FUNCTION_FUNCTION_NAME,
627+
)
628+
629+
/**
630+
* The Reactive Streams `org.reactivestreams.Publisher` return type used by
631+
* [jvmReactiveTransformer].
632+
*
633+
* @since 0.14.0
634+
*/
635+
@JvmStatic
636+
val jvmReactivePublisherClassInfo = ClassInfo(
637+
packageName = "org.reactivestreams",
638+
className = "Publisher"
639+
)
640+
589641
@JvmStatic
590642
val jvmBlockingTransformer = Transformer(
591643
markAnnotation = jvmBlockingAnnotationInfo,
@@ -604,6 +656,7 @@ object SuspendTransformConfigurations {
604656
jvmSyntheticClassInfo,
605657
jvmBlockingMarkAnnotationClassInfo,
606658
jvmAsyncMarkAnnotationClassInfo,
659+
jvmReactiveMarkAnnotationClassInfo,
607660
kotlinOptInClassInfo,
608661
jvmNameAnnotationClassInfo,
609662
),
@@ -624,9 +677,47 @@ object SuspendTransformConfigurations {
624677
jvmSyntheticClassInfo,
625678
jvmBlockingMarkAnnotationClassInfo,
626679
jvmAsyncMarkAnnotationClassInfo,
680+
jvmReactiveMarkAnnotationClassInfo,
681+
kotlinOptInClassInfo,
682+
jvmNameAnnotationClassInfo,
683+
),
684+
)
685+
686+
/**
687+
* Default JVM transformer for `@JvmReactive`.
688+
*
689+
* It generates functions returning `org.reactivestreams.Publisher<T & Any>`.
690+
* The copied generic argument uses [TransformReturnTypeGenericMode.NON_NULL]
691+
* because Reactive Streams publishers must not emit `null`.
692+
*
693+
* Requires the target JVM classpath to contain:
694+
* - [`org.reactivestreams.Publisher`](https://www.reactive-streams.org/)
695+
* - [`org.jetbrains.kotlinx:kotlinx-coroutines-reactive`](https://github.com/Kotlin/kotlinx.coroutines/blob/master/reactive/README.md)
696+
*
697+
* These dependencies are not added automatically by this transformer.
698+
*
699+
* @since 0.14.0
700+
*/
701+
@JvmStatic
702+
val jvmReactiveTransformer = Transformer(
703+
markAnnotation = jvmReactiveAnnotationInfo,
704+
transformFunctionInfo = jvmReactiveTransformFunction,
705+
transformReturnType = jvmReactivePublisherClassInfo,
706+
transformReturnTypeGeneric = true,
707+
originFunctionIncludeAnnotations = listOf(IncludeAnnotation(jvmSyntheticClassInfo)),
708+
syntheticFunctionIncludeAnnotations = listOf(
709+
IncludeAnnotation(jvmApi4JAnnotationClassInfo, includeProperty = true)
710+
),
711+
copyAnnotationsToSyntheticFunction = true,
712+
copyAnnotationExcludes = listOf(
713+
jvmSyntheticClassInfo,
714+
jvmBlockingMarkAnnotationClassInfo,
715+
jvmAsyncMarkAnnotationClassInfo,
716+
jvmReactiveMarkAnnotationClassInfo,
627717
kotlinOptInClassInfo,
628718
jvmNameAnnotationClassInfo,
629719
),
720+
transformReturnTypeGenericMode = TransformReturnTypeGenericMode.NON_NULL,
630721
)
631722
//endregion
632723

compiler/suspend-transform-plugin-deprecated-configuration/src/main/kotlin/love/forte/plugin/suspendtrans/DeprecatedSuspendTransformConfiguration.kt

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ private const val JVM_RUN_IN_ASYNC_FUNCTION_PACKAGE_NAME: String = "love.forte.p
3535
private val JVM_RUN_IN_ASYNC_FUNCTION_CLASS_NAME: String? = null
3636
private const val JVM_RUN_IN_ASYNC_FUNCTION_FUNCTION_NAME: String = "\$runInAsync\$"
3737

38+
private const val JVM_RUN_IN_REACTIVE_FUNCTION_PACKAGE_NAME: String = "love.forte.plugin.suspendtrans.runtime"
39+
private val JVM_RUN_IN_REACTIVE_FUNCTION_CLASS_NAME: String? = null
40+
private const val JVM_RUN_IN_REACTIVE_FUNCTION_FUNCTION_NAME: String = "\$runInReactive\$"
41+
3842
private const val JS_RUN_IN_ASYNC_FUNCTION_PACKAGE_NAME: String = "love.forte.plugin.suspendtrans.runtime"
3943
private val JS_RUN_IN_ASYNC_FUNCTION_CLASS_NAME: String? = null
4044
private const val JS_RUN_IN_ASYNC_FUNCTION_FUNCTION_NAME: String = "\$runInAsync\$"
@@ -354,6 +358,46 @@ open class SuspendTransformConfiguration {
354358
JVM_RUN_IN_ASYNC_FUNCTION_CLASS_NAME,
355359
JVM_RUN_IN_ASYNC_FUNCTION_FUNCTION_NAME,
356360
)
361+
362+
/**
363+
* The deprecated `love.forte.plugin.suspendtrans.annotation.JvmReactive` class info.
364+
*
365+
* Deprecated configuration cannot represent `transformReturnTypeGenericMode`,
366+
* so Gradle maps this exact transformer to the new default reactive transformer.
367+
*
368+
* @since 0.14.0
369+
*/
370+
@JvmStatic
371+
val jvmReactiveMarkAnnotationClassInfo =
372+
ClassInfo("love.forte.plugin.suspendtrans.annotation", "JvmReactive")
373+
374+
/**
375+
* Deprecated mark annotation mapping for `@JvmReactive`.
376+
*
377+
* @since 0.14.0
378+
*/
379+
@JvmStatic
380+
val jvmReactiveAnnotationInfo = MarkAnnotation(jvmReactiveMarkAnnotationClassInfo, defaultSuffix = "Reactive")
381+
382+
/**
383+
* Deprecated runtime transform function info for `$runInReactive$`.
384+
*
385+
* @since 0.14.0
386+
*/
387+
@JvmStatic
388+
val jvmReactiveTransformFunction = FunctionInfo(
389+
JVM_RUN_IN_REACTIVE_FUNCTION_PACKAGE_NAME,
390+
JVM_RUN_IN_REACTIVE_FUNCTION_CLASS_NAME,
391+
JVM_RUN_IN_REACTIVE_FUNCTION_FUNCTION_NAME,
392+
)
393+
394+
/**
395+
* Deprecated Reactive Streams `Publisher` class info.
396+
*
397+
* @since 0.14.0
398+
*/
399+
@JvmStatic
400+
val jvmReactivePublisherClassInfo = ClassInfo("org.reactivestreams", "Publisher")
357401
//endregion
358402

359403
//region JVM blocking defaults
@@ -369,6 +413,7 @@ open class SuspendTransformConfiguration {
369413
jvmSyntheticClassInfo,
370414
jvmBlockingMarkAnnotationClassInfo,
371415
jvmAsyncMarkAnnotationClassInfo,
416+
jvmReactiveMarkAnnotationClassInfo,
372417
kotlinOptInClassInfo,
373418
),
374419
syntheticFunctionIncludeAnnotations = listOf(
@@ -392,6 +437,48 @@ open class SuspendTransformConfiguration {
392437
jvmSyntheticClassInfo,
393438
jvmBlockingMarkAnnotationClassInfo,
394439
jvmAsyncMarkAnnotationClassInfo,
440+
jvmReactiveMarkAnnotationClassInfo,
441+
kotlinOptInClassInfo,
442+
),
443+
syntheticFunctionIncludeAnnotations = listOf(
444+
IncludeAnnotation(jvmApi4JAnnotationClassInfo).apply {
445+
includeProperty = true
446+
}
447+
)
448+
)
449+
//endregion
450+
451+
//region JVM reactive defaults
452+
453+
/**
454+
* Deprecated default JVM transformer for `@JvmReactive`.
455+
*
456+
* This value is mainly a compatibility marker for the deprecated Gradle
457+
* extension. The new configuration model maps it to
458+
* `SuspendTransformConfigurations.jvmReactiveTransformer`, which applies
459+
* non-null generic arguments for Reactive Streams elements.
460+
*
461+
* Requires the target JVM classpath to contain:
462+
* - [`org.reactivestreams.Publisher`](https://www.reactive-streams.org/)
463+
* - [`org.jetbrains.kotlinx:kotlinx-coroutines-reactive`](https://github.com/Kotlin/kotlinx.coroutines/blob/master/reactive/README.md)
464+
*
465+
* These dependencies are not added automatically by this transformer.
466+
*
467+
* @since 0.14.0
468+
*/
469+
@JvmStatic
470+
val jvmReactiveTransformer = Transformer(
471+
markAnnotation = jvmReactiveAnnotationInfo,
472+
transformFunctionInfo = jvmReactiveTransformFunction,
473+
transformReturnType = jvmReactivePublisherClassInfo,
474+
transformReturnTypeGeneric = true,
475+
originFunctionIncludeAnnotations = listOf(IncludeAnnotation(jvmSyntheticClassInfo)),
476+
copyAnnotationsToSyntheticFunction = true,
477+
copyAnnotationExcludes = listOf(
478+
jvmSyntheticClassInfo,
479+
jvmBlockingMarkAnnotationClassInfo,
480+
jvmAsyncMarkAnnotationClassInfo,
481+
jvmReactiveMarkAnnotationClassInfo,
395482
kotlinOptInClassInfo,
396483
),
397484
syntheticFunctionIncludeAnnotations = listOf(
@@ -441,4 +528,3 @@ open class SuspendTransformConfiguration {
441528

442529
}
443530
}
444-

compiler/suspend-transform-plugin/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ dependencies {
3434
// testImplementation("org.bitbucket.mstrobel:procyon-compilertools:0.6.0")
3535

3636
testImplementation(libs.kotlinx.coroutines.core)
37+
testImplementation(libs.kotlinx.coroutines.reactive)
3738
}
3839

3940
//val compileKotlin: KotlinCompile by tasks

0 commit comments

Comments
 (0)