diff --git a/build.gradle.kts b/build.gradle.kts index 0b8f61b2..e324753a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,6 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.openapitools.generator.gradle.plugin.tasks.GenerateTask -import kotlin.math.min +import kotlin.math.max val ktor_version: String by project val kotlin_version: String by project @@ -29,7 +29,7 @@ version = "0.0.1" tasks.withType { useJUnitPlatform() val cpus = Runtime.getRuntime().availableProcessors() - maxParallelForks = min(1, cpus - 1) + maxParallelForks = max(1, cpus - 1) setForkEvery(4) } @@ -60,8 +60,14 @@ tasks.create("openApiGenerateMastodonCompatibleApi", GenerateTask: configOptions.put("interfaceOnly", "true") configOptions.put("useSpringBoot3", "true") additionalProperties.put("useTags", "true") + importMappings.put("org.springframework.core.io.Resource", "org.springframework.web.multipart.MultipartFile") typeMappings.put("org.springframework.core.io.Resource", "org.springframework.web.multipart.MultipartFile") + schemaMappings.put( + "StatusesRequest", + "dev.usbharu.hideout.domain.model.mastodon.StatusesRequest" + ) + templateDir.set("$rootDir/templates") } repositories { @@ -150,7 +156,7 @@ detekt { parallel = true config = files("detekt.yml") buildUponDefaultConfig = true - basePath = rootDir.absolutePath + basePath = "${rootDir.absolutePath}/src/" autoCorrect = true } diff --git a/detekt.yml b/detekt.yml index 658f1b3b..c1b54432 100644 --- a/detekt.yml +++ b/detekt.yml @@ -4,6 +4,7 @@ build: Indentation: 0 MagicNumber: 0 InjectDispatcher: 0 + EnumEntryNameCase: 0 style: ClassOrdering: @@ -161,3 +162,7 @@ potential-bugs: HasPlatformType: active: false + +coroutines: + RedundantSuspendModifier: + active: false diff --git a/src/main/kotlin/dev/usbharu/hideout/config/JsonOrFormBind.kt b/src/main/kotlin/dev/usbharu/hideout/config/JsonOrFormBind.kt new file mode 100644 index 00000000..02ff8520 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/config/JsonOrFormBind.kt @@ -0,0 +1,6 @@ +package dev.usbharu.hideout.config + +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.VALUE_PARAMETER) +annotation class JsonOrFormBind diff --git a/src/main/kotlin/dev/usbharu/hideout/config/JsonOrFormModelMethodProcessor.kt b/src/main/kotlin/dev/usbharu/hideout/config/JsonOrFormModelMethodProcessor.kt new file mode 100644 index 00000000..6d1e7382 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/config/JsonOrFormModelMethodProcessor.kt @@ -0,0 +1,54 @@ +package dev.usbharu.hideout.config + +import org.slf4j.LoggerFactory +import org.springframework.core.MethodParameter +import org.springframework.web.bind.support.WebDataBinderFactory +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.web.method.annotation.ModelAttributeMethodProcessor +import org.springframework.web.method.support.HandlerMethodArgumentResolver +import org.springframework.web.method.support.ModelAndViewContainer +import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor + +@Suppress("TooGenericExceptionCaught") +class JsonOrFormModelMethodProcessor( + private val modelAttributeMethodProcessor: ModelAttributeMethodProcessor, + private val requestResponseBodyMethodProcessor: RequestResponseBodyMethodProcessor +) : HandlerMethodArgumentResolver { + private val isJsonRegex = Regex("application/((\\w*)\\+)?json") + + override fun supportsParameter(parameter: MethodParameter): Boolean = + parameter.hasParameterAnnotation(JsonOrFormBind::class.java) + + override fun resolveArgument( + parameter: MethodParameter, + mavContainer: ModelAndViewContainer?, + webRequest: NativeWebRequest, + binderFactory: WebDataBinderFactory? + ): Any? { + val contentType = webRequest.getHeader("Content-Type").orEmpty() + logger.trace("ContentType is {}", contentType) + if (contentType.contains(isJsonRegex)) { + logger.trace("Determine content type as json.") + return requestResponseBodyMethodProcessor.resolveArgument( + parameter, + mavContainer, + webRequest, + binderFactory + ) + } + + return try { + modelAttributeMethodProcessor.resolveArgument(parameter, mavContainer, webRequest, binderFactory) + } catch (ignore: Exception) { + try { + requestResponseBodyMethodProcessor.resolveArgument(parameter, mavContainer, webRequest, binderFactory) + } catch (e: Exception) { + logger.warn("Failed to bind request", e) + } + } + } + + companion object { + val logger = LoggerFactory.getLogger(JsonOrFormModelMethodProcessor::class.java) + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/config/MvcConfigurer.kt b/src/main/kotlin/dev/usbharu/hideout/config/MvcConfigurer.kt new file mode 100644 index 00000000..d6afcfe6 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/config/MvcConfigurer.kt @@ -0,0 +1,29 @@ +package dev.usbharu.hideout.config + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.http.converter.HttpMessageConverter +import org.springframework.web.method.support.HandlerMethodArgumentResolver +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer +import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor +import org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor + +@Configuration +class MvcConfigurer(private val jsonOrFormModelMethodProcessor: JsonOrFormModelMethodProcessor) : WebMvcConfigurer { + override fun addArgumentResolvers(resolvers: MutableList) { + resolvers.add(jsonOrFormModelMethodProcessor) + } +} + +@Configuration +class JsonOrFormModelMethodProcessorConfig { + @Bean + fun jsonOrFormModelMethodProcessor(converter: List>): JsonOrFormModelMethodProcessor { + return JsonOrFormModelMethodProcessor( + ServletModelAttributeMethodProcessor(true), + RequestResponseBodyMethodProcessor( + converter + ) + ) + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/config/SecurityConfig.kt b/src/main/kotlin/dev/usbharu/hideout/config/SecurityConfig.kt index 38424b81..e9ff022b 100644 --- a/src/main/kotlin/dev/usbharu/hideout/config/SecurityConfig.kt +++ b/src/main/kotlin/dev/usbharu/hideout/config/SecurityConfig.kt @@ -35,7 +35,7 @@ import java.security.interfaces.RSAPrivateKey import java.security.interfaces.RSAPublicKey import java.util.* -@EnableWebSecurity(debug = true) +@EnableWebSecurity(debug = false) @Configuration class SecurityConfig { diff --git a/src/main/kotlin/dev/usbharu/hideout/config/SpringConfig.kt b/src/main/kotlin/dev/usbharu/hideout/config/SpringConfig.kt index 25ef84ed..18ee3845 100644 --- a/src/main/kotlin/dev/usbharu/hideout/config/SpringConfig.kt +++ b/src/main/kotlin/dev/usbharu/hideout/config/SpringConfig.kt @@ -2,7 +2,9 @@ package dev.usbharu.hideout.config import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.web.filter.CommonsRequestLoggingFilter import java.net.URL @Configuration @@ -13,6 +15,17 @@ class SpringConfig { @Autowired lateinit var storageConfig: StorageConfig + + @Bean + fun requestLoggingFilter(): CommonsRequestLoggingFilter { + val loggingFilter = CommonsRequestLoggingFilter() + loggingFilter.setIncludeHeaders(true) + loggingFilter.setIncludeClientInfo(true) + loggingFilter.setIncludeQueryString(true) + loggingFilter.setIncludePayload(true) + loggingFilter.setMaxPayloadLength(64000) + return loggingFilter + } } @ConfigurationProperties("hideout") diff --git a/src/main/kotlin/dev/usbharu/hideout/controller/mastodon/MastodonStatusesApiContoller.kt b/src/main/kotlin/dev/usbharu/hideout/controller/mastodon/MastodonStatusesApiContoller.kt index d5111577..7b537601 100644 --- a/src/main/kotlin/dev/usbharu/hideout/controller/mastodon/MastodonStatusesApiContoller.kt +++ b/src/main/kotlin/dev/usbharu/hideout/controller/mastodon/MastodonStatusesApiContoller.kt @@ -2,7 +2,7 @@ package dev.usbharu.hideout.controller.mastodon import dev.usbharu.hideout.controller.mastodon.generated.StatusApi import dev.usbharu.hideout.domain.mastodon.model.generated.Status -import dev.usbharu.hideout.domain.mastodon.model.generated.StatusesRequest +import dev.usbharu.hideout.domain.model.mastodon.StatusesRequest import dev.usbharu.hideout.service.api.mastodon.StatusesApiService import kotlinx.coroutines.runBlocking import org.springframework.http.HttpStatus @@ -10,17 +10,20 @@ import org.springframework.http.ResponseEntity import org.springframework.security.core.context.SecurityContextHolder import org.springframework.security.oauth2.jwt.Jwt import org.springframework.stereotype.Controller -import org.springframework.web.bind.annotation.ModelAttribute @Controller class MastodonStatusesApiContoller(private val statusesApiService: StatusesApiService) : StatusApi { - override fun apiV1StatusesPost(@ModelAttribute statusesRequest: StatusesRequest): ResponseEntity = - runBlocking { + override fun apiV1StatusesPost(devUsbharuHideoutDomainModelMastodonStatusesRequest: StatusesRequest): ResponseEntity { + return runBlocking { val jwt = SecurityContextHolder.getContext().authentication.principal as Jwt ResponseEntity( - statusesApiService.postStatus(statusesRequest, jwt.getClaim("uid").toLong()), + statusesApiService.postStatus( + devUsbharuHideoutDomainModelMastodonStatusesRequest, + jwt.getClaim("uid").toLong() + ), HttpStatus.OK ) } + } } diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Document.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Document.kt new file mode 100644 index 00000000..0dacaf00 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Document.kt @@ -0,0 +1,43 @@ +package dev.usbharu.hideout.domain.model.ap + +open class Document : Object { + + var mediaType: String? = null + var url: String? = null + + protected constructor() : super() + constructor( + type: List = emptyList(), + name: String? = null, + mediaType: String, + url: String + ) : super( + type = add(type, "Document"), + name = name, + actor = null, + id = null + ) { + this.mediaType = mediaType + this.url = url + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Document) return false + if (!super.equals(other)) return false + + if (mediaType != other.mediaType) return false + if (url != other.url) return false + + return true + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + (mediaType?.hashCode() ?: 0) + result = 31 * result + (url?.hashCode() ?: 0) + return result + } + + override fun toString(): String = "Document(mediaType=$mediaType, url=$url) ${super.toString()}" +} diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Note.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Note.kt index 21c2f25c..0375b673 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Note.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/Note.kt @@ -2,6 +2,7 @@ package dev.usbharu.hideout.domain.model.ap open class Note : Object { var attributedTo: String? = null + var attachment: List = emptyList() var content: String? = null var published: String? = null var to: List = emptyList() @@ -22,7 +23,8 @@ open class Note : Object { to: List = emptyList(), cc: List = emptyList(), sensitive: Boolean = false, - inReplyTo: String? = null + inReplyTo: String? = null, + attachment: List = emptyList() ) : super( type = add(type, "Note"), name = name, @@ -35,6 +37,7 @@ open class Note : Object { this.cc = cc this.sensitive = sensitive this.inReplyTo = inReplyTo + this.attachment = attachment } override fun equals(other: Any?): Boolean { @@ -42,23 +45,34 @@ open class Note : Object { if (other !is Note) return false if (!super.equals(other)) return false - if (id != other.id) return false if (attributedTo != other.attributedTo) return false + if (attachment != other.attachment) return false if (content != other.content) return false if (published != other.published) return false - return to == other.to + if (to != other.to) return false + if (cc != other.cc) return false + if (sensitive != other.sensitive) return false + if (inReplyTo != other.inReplyTo) return false + + return true } override fun hashCode(): Int { var result = super.hashCode() - result = 31 * result + (id?.hashCode() ?: 0) result = 31 * result + (attributedTo?.hashCode() ?: 0) + result = 31 * result + attachment.hashCode() result = 31 * result + (content?.hashCode() ?: 0) result = 31 * result + (published?.hashCode() ?: 0) result = 31 * result + to.hashCode() + result = 31 * result + cc.hashCode() + result = 31 * result + sensitive.hashCode() + result = 31 * result + (inReplyTo?.hashCode() ?: 0) return result } - override fun toString(): String = - "Note(id=$id, attributedTo=$attributedTo, content=$content, published=$published, to=$to) ${super.toString()}" + override fun toString(): String { + return "Note(attributedTo=$attributedTo, attachment=$attachment, " + + "content=$content, published=$published, to=$to, cc=$cc, sensitive=$sensitive," + + " inReplyTo=$inReplyTo) ${super.toString()}" + } } diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/ObjectDeserializer.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/ObjectDeserializer.kt index 66af888a..72fabd37 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/ObjectDeserializer.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/ap/ObjectDeserializer.kt @@ -84,7 +84,7 @@ class ObjectDeserializer : JsonDeserializer() { ExtendedActivityVocabulary.Service -> TODO() ExtendedActivityVocabulary.Article -> TODO() ExtendedActivityVocabulary.Audio -> TODO() - ExtendedActivityVocabulary.Document -> TODO() + ExtendedActivityVocabulary.Document -> p.codec.treeToValue(treeNode, Document::class.java) ExtendedActivityVocabulary.Event -> TODO() ExtendedActivityVocabulary.Image -> p.codec.treeToValue(treeNode, Image::class.java) ExtendedActivityVocabulary.Page -> TODO() diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/PostCreateDto.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/PostCreateDto.kt index 1869da21..c9cf69de 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/PostCreateDto.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/dto/PostCreateDto.kt @@ -8,5 +8,6 @@ data class PostCreateDto( val visibility: Visibility = Visibility.PUBLIC, val repostId: Long? = null, val repolyId: Long? = null, - val userId: Long + val userId: Long, + val mediaIds: List = emptyList() ) diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Post.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Post.kt index 3ed6ac53..afe45261 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Post.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/hideout/entity/Post.kt @@ -13,7 +13,8 @@ data class Post private constructor( val repostId: Long? = null, val replyId: Long? = null, val sensitive: Boolean = false, - val apId: String = url + val apId: String = url, + val mediaIds: List = emptyList() ) { companion object { @Suppress("FunctionMinLength", "LongParameterList") @@ -28,7 +29,8 @@ data class Post private constructor( repostId: Long? = null, replyId: Long? = null, sensitive: Boolean = false, - apId: String = url + apId: String = url, + mediaIds: List = emptyList() ): Post { val characterLimit = Config.configData.characterLimit @@ -67,7 +69,8 @@ data class Post private constructor( repostId = repostId, replyId = replyId, sensitive = sensitive, - apId = apId + apId = apId, + mediaIds = mediaIds ) } } diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/job/HideoutJob.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/job/HideoutJob.kt index 252dcb17..a4bc0510 100644 --- a/src/main/kotlin/dev/usbharu/hideout/domain/model/job/HideoutJob.kt +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/job/HideoutJob.kt @@ -18,6 +18,7 @@ object DeliverPostJob : HideoutJob("DeliverPostJob") { val post: Prop = string("post") val actor: Prop = string("actor") val inbox: Prop = string("inbox") + val media: Prop = string("media") } @Component diff --git a/src/main/kotlin/dev/usbharu/hideout/domain/model/mastodon/StatusesRequest.kt b/src/main/kotlin/dev/usbharu/hideout/domain/model/mastodon/StatusesRequest.kt new file mode 100644 index 00000000..a3a98ec4 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/domain/model/mastodon/StatusesRequest.kt @@ -0,0 +1,77 @@ +package dev.usbharu.hideout.domain.model.mastodon + +import com.fasterxml.jackson.annotation.JsonProperty +import dev.usbharu.hideout.domain.mastodon.model.generated.StatusesRequestPoll + +@Suppress("VariableNaming") +class StatusesRequest { + @JsonProperty("status") + var status: String? = null + + @JsonProperty("media_ids") + var media_ids: List = emptyList() + + @JsonProperty("poll") + var poll: StatusesRequestPoll? = null + + @JsonProperty("in_reply_to_id") + var in_reply_to_id: String? = null + + @JsonProperty("sensitive") + var sensitive: Boolean? = null + + @JsonProperty("spoiler_text") + var spoiler_text: String? = null + + @JsonProperty("visibility") + var visibility: Visibility? = null + + @JsonProperty("language") + var language: String? = null + + @JsonProperty("scheduled_at") + var scheduled_at: String? = null + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is StatusesRequest) return false + + if (status != other.status) return false + if (media_ids != other.media_ids) return false + if (poll != other.poll) return false + if (in_reply_to_id != other.in_reply_to_id) return false + if (sensitive != other.sensitive) return false + if (spoiler_text != other.spoiler_text) return false + if (visibility != other.visibility) return false + if (language != other.language) return false + if (scheduled_at != other.scheduled_at) return false + + return true + } + + override fun hashCode(): Int { + var result = status?.hashCode() ?: 0 + result = 31 * result + media_ids.hashCode() + result = 31 * result + (poll?.hashCode() ?: 0) + result = 31 * result + (in_reply_to_id?.hashCode() ?: 0) + result = 31 * result + (sensitive?.hashCode() ?: 0) + result = 31 * result + (spoiler_text?.hashCode() ?: 0) + result = 31 * result + (visibility?.hashCode() ?: 0) + result = 31 * result + (language?.hashCode() ?: 0) + result = 31 * result + (scheduled_at?.hashCode() ?: 0) + return result + } + + override fun toString(): String { + return "StatusesRequest(status=$status, mediaIds=$media_ids, poll=$poll, inReplyToId=$in_reply_to_id, " + + "sensitive=$sensitive, spoilerText=$spoiler_text, visibility=$visibility, language=$language," + + " scheduledAt=$scheduled_at)" + } + + @Suppress("EnumNaming") + enum class Visibility { + `public`, + unlisted, + private, + direct; + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/query/MediaQueryService.kt b/src/main/kotlin/dev/usbharu/hideout/query/MediaQueryService.kt new file mode 100644 index 00000000..183e808f --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/query/MediaQueryService.kt @@ -0,0 +1,7 @@ +package dev.usbharu.hideout.query + +import dev.usbharu.hideout.domain.model.hideout.entity.Media + +interface MediaQueryService { + suspend fun findByPostId(postId: Long): List +} diff --git a/src/main/kotlin/dev/usbharu/hideout/query/MediaQueryServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/query/MediaQueryServiceImpl.kt new file mode 100644 index 00000000..ecb687bb --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/query/MediaQueryServiceImpl.kt @@ -0,0 +1,17 @@ +package dev.usbharu.hideout.query + +import dev.usbharu.hideout.domain.model.hideout.entity.Media +import dev.usbharu.hideout.repository.PostsMedia +import dev.usbharu.hideout.repository.toMedia +import org.jetbrains.exposed.sql.innerJoin +import org.jetbrains.exposed.sql.select +import org.springframework.stereotype.Repository + +@Repository +class MediaQueryServiceImpl : MediaQueryService { + override suspend fun findByPostId(postId: Long): List { + return dev.usbharu.hideout.repository.Media.innerJoin(PostsMedia, onColumn = { id }, otherColumn = { mediaId }) + .select { PostsMedia.postId eq postId } + .map { it.toMedia() } + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/query/PostQueryServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/query/PostQueryServiceImpl.kt index 545c2bfd..8e8ead0b 100644 --- a/src/main/kotlin/dev/usbharu/hideout/query/PostQueryServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/query/PostQueryServiceImpl.kt @@ -3,20 +3,29 @@ package dev.usbharu.hideout.query import dev.usbharu.hideout.domain.model.hideout.entity.Post import dev.usbharu.hideout.exception.FailedToGetResourcesException import dev.usbharu.hideout.repository.Posts +import dev.usbharu.hideout.repository.PostsMedia import dev.usbharu.hideout.repository.toPost import dev.usbharu.hideout.util.singleOr +import org.jetbrains.exposed.sql.innerJoin import org.jetbrains.exposed.sql.select import org.springframework.stereotype.Repository @Repository class PostQueryServiceImpl : PostQueryService { override suspend fun findById(id: Long): Post = - Posts.select { Posts.id eq id } + Posts.innerJoin(PostsMedia, onColumn = { Posts.id }, otherColumn = { PostsMedia.postId }) + .select { Posts.id eq id } .singleOr { FailedToGetResourcesException("id: $id is duplicate or does not exist.", it) }.toPost() - override suspend fun findByUrl(url: String): Post = Posts.select { Posts.url eq url } - .singleOr { FailedToGetResourcesException("url: $url is duplicate or does not exist.", it) }.toPost() + override suspend fun findByUrl(url: String): Post = + Posts.innerJoin(PostsMedia, onColumn = { Posts.id }, otherColumn = { PostsMedia.postId }) + .select { Posts.url eq url } + .toPost() + .singleOr { FailedToGetResourcesException("url: $url is duplicate or does not exist.", it) } - override suspend fun findByApId(string: String): Post = Posts.select { Posts.apId eq string } - .singleOr { FailedToGetResourcesException("apId: $string is duplicate or does not exist.", it) }.toPost() + override suspend fun findByApId(string: String): Post = + Posts.innerJoin(PostsMedia, onColumn = { Posts.id }, otherColumn = { PostsMedia.postId }) + .select { Posts.apId eq string } + .toPost() + .singleOr { FailedToGetResourcesException("apId: $string is duplicate or does not exist.", it) } } diff --git a/src/main/kotlin/dev/usbharu/hideout/query/ReactionQueryServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/query/ReactionQueryServiceImpl.kt new file mode 100644 index 00000000..4d9897d9 --- /dev/null +++ b/src/main/kotlin/dev/usbharu/hideout/query/ReactionQueryServiceImpl.kt @@ -0,0 +1,61 @@ +package dev.usbharu.hideout.query + +import dev.usbharu.hideout.domain.model.hideout.dto.Account +import dev.usbharu.hideout.domain.model.hideout.dto.ReactionResponse +import dev.usbharu.hideout.domain.model.hideout.entity.Reaction +import dev.usbharu.hideout.exception.FailedToGetResourcesException +import dev.usbharu.hideout.repository.Reactions +import dev.usbharu.hideout.repository.Users +import dev.usbharu.hideout.repository.toReaction +import dev.usbharu.hideout.util.singleOr +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.springframework.stereotype.Repository + +@Repository +class ReactionQueryServiceImpl : ReactionQueryService { + override suspend fun findByPostId(postId: Long, userId: Long?): List { + return Reactions.select { + Reactions.postId.eq(postId) + }.map { it.toReaction() } + } + + @Suppress("FunctionMaxLength") + override suspend fun findByPostIdAndUserIdAndEmojiId(postId: Long, userId: Long, emojiId: Long): Reaction { + return Reactions + .select { + Reactions.postId.eq(postId).and(Reactions.userId.eq(userId)).and( + Reactions.emojiId.eq(emojiId) + ) + } + .singleOr { + FailedToGetResourcesException( + "postId: $postId,userId: $userId,emojiId: $emojiId is duplicate or does not exist.", + it + ) + } + .toReaction() + } + + override suspend fun reactionAlreadyExist(postId: Long, userId: Long, emojiId: Long): Boolean { + return Reactions.select { + Reactions.postId.eq(postId).and(Reactions.userId.eq(userId)).and( + Reactions.emojiId.eq(emojiId) + ) + }.empty().not() + } + + override suspend fun deleteByPostIdAndUserId(postId: Long, userId: Long) { + Reactions.deleteWhere { Reactions.postId.eq(postId).and(Reactions.userId.eq(userId)) } + } + + override suspend fun findByPostIdWithUsers(postId: Long, userId: Long?): List { + return Reactions + .leftJoin(Users, onColumn = { Reactions.userId }, otherColumn = { id }) + .select { Reactions.postId.eq(postId) } + .groupBy { _: ResultRow -> ReactionResponse("❤", true, "", emptyList()) } + .map { entry: Map.Entry> -> + entry.key.copy(accounts = entry.value.map { Account(it[Users.screenName], "", it[Users.url]) }) + } + } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/query/mastodon/StatusQueryServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/query/mastodon/StatusQueryServiceImpl.kt index ae9056f7..5fba667c 100644 --- a/src/main/kotlin/dev/usbharu/hideout/query/mastodon/StatusQueryServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/query/mastodon/StatusQueryServiceImpl.kt @@ -1,9 +1,11 @@ package dev.usbharu.hideout.query.mastodon import dev.usbharu.hideout.domain.mastodon.model.generated.Account +import dev.usbharu.hideout.domain.mastodon.model.generated.MediaAttachment import dev.usbharu.hideout.domain.mastodon.model.generated.Status -import dev.usbharu.hideout.repository.Posts -import dev.usbharu.hideout.repository.Users +import dev.usbharu.hideout.domain.model.hideout.dto.FileType +import dev.usbharu.hideout.repository.* +import org.jetbrains.exposed.sql.ResultRow import org.jetbrains.exposed.sql.innerJoin import org.jetbrains.exposed.sql.select import org.springframework.stereotype.Repository @@ -12,67 +14,21 @@ import java.time.Instant @Repository class StatusQueryServiceImpl : StatusQueryService { @Suppress("LongMethod") - override suspend fun findByPostIds(ids: List): List { - val pairs = Posts.innerJoin(Users, onColumn = { userId }, otherColumn = { id }) + override suspend fun findByPostIds(ids: List): List = findByPostIdsWithMediaAttachments(ids) + + @Suppress("unused") + private suspend fun internalFindByPostIds(ids: List): List { + val pairs = Posts + .innerJoin(Users, onColumn = { Posts.userId }, otherColumn = { id }) .select { Posts.id inList ids } .map { - Status( - id = it[Posts.id].toString(), - uri = it[Posts.apId], - createdAt = Instant.ofEpochMilli(it[Posts.createdAt]).toString(), - account = Account( - id = it[Users.id].toString(), - username = it[Users.name], - acct = "${it[Users.name]}@${it[Users.domain]}", - url = it[Users.url], - displayName = it[Users.screenName], - note = it[Users.description], - avatar = it[Users.url] + "/icon.jpg", - avatarStatic = it[Users.url] + "/icon.jpg", - header = it[Users.url] + "/header.jpg", - headerStatic = it[Users.url] + "/header.jpg", - locked = false, - fields = emptyList(), - emojis = emptyList(), - bot = false, - group = false, - discoverable = true, - createdAt = Instant.ofEpochMilli(it[Users.createdAt]).toString(), - lastStatusAt = Instant.ofEpochMilli(it[Users.createdAt]).toString(), - statusesCount = 0, - followersCount = 0, - followingCount = 0, - noindex = false, - moved = false, - suspendex = false, - limited = false - ), - content = it[Posts.text], - visibility = when (it[Posts.visibility]) { - 0 -> Status.Visibility.public - 1 -> Status.Visibility.unlisted - 2 -> Status.Visibility.private - 3 -> Status.Visibility.direct - else -> Status.Visibility.public - }, - sensitive = it[Posts.sensitive], - spoilerText = it[Posts.overview].orEmpty(), - mediaAttachments = emptyList(), - mentions = emptyList(), - tags = emptyList(), - emojis = emptyList(), - reblogsCount = 0, - favouritesCount = 0, - repliesCount = 0, - url = it[Posts.apId], - inReplyToId = it[Posts.replyId].toString(), - inReplyToAccountId = null, - language = null, - text = it[Posts.text], - editedAt = null - ) to it[Posts.repostId] + toStatus(it) to it[Posts.repostId] } + return resolveReplyAndRepost(pairs) + } + + private fun resolveReplyAndRepost(pairs: List>): List { val statuses = pairs.map { it.first } return pairs .map { @@ -90,4 +46,95 @@ class StatusQueryServiceImpl : StatusQueryService { } } } + + @Suppress("FunctionMaxLength") + private suspend fun findByPostIdsWithMediaAttachments(ids: List): List { + val pairs = Posts + .innerJoin(PostsMedia, onColumn = { Posts.id }, otherColumn = { PostsMedia.postId }) + .innerJoin(Users, onColumn = { Posts.userId }, otherColumn = { id }) + .innerJoin(Media, onColumn = { PostsMedia.mediaId }, otherColumn = { id }) + .select { Posts.id inList ids } + .groupBy { it[Posts.id] } + .map { it.value } + .map { + toStatus(it.first()).copy( + mediaAttachments = it.map { + it.toMedia().let { + MediaAttachment( + id = it.id.toString(), + type = when (it.type) { + FileType.Image -> MediaAttachment.Type.image + FileType.Video -> MediaAttachment.Type.video + FileType.Audio -> MediaAttachment.Type.audio + FileType.Unknown -> MediaAttachment.Type.unknown + }, + url = it.url, + previewUrl = it.thumbnailUrl, + remoteUrl = it.remoteUrl, + description = "", + blurhash = it.blurHash, + textUrl = it.url + ) + } + } + ) to it.first()[Posts.repostId] + } + return resolveReplyAndRepost(pairs) + } } + +private fun toStatus(it: ResultRow) = Status( + id = it[Posts.id].toString(), + uri = it[Posts.apId], + createdAt = Instant.ofEpochMilli(it[Posts.createdAt]).toString(), + account = Account( + id = it[Users.id].toString(), + username = it[Users.name], + acct = "${it[Users.name]}@${it[Users.domain]}", + url = it[Users.url], + displayName = it[Users.screenName], + note = it[Users.description], + avatar = it[Users.url] + "/icon.jpg", + avatarStatic = it[Users.url] + "/icon.jpg", + header = it[Users.url] + "/header.jpg", + headerStatic = it[Users.url] + "/header.jpg", + locked = false, + fields = emptyList(), + emojis = emptyList(), + bot = false, + group = false, + discoverable = true, + createdAt = Instant.ofEpochMilli(it[Users.createdAt]).toString(), + lastStatusAt = Instant.ofEpochMilli(it[Users.createdAt]).toString(), + statusesCount = 0, + followersCount = 0, + followingCount = 0, + noindex = false, + moved = false, + suspendex = false, + limited = false + ), + content = it[Posts.text], + visibility = when (it[Posts.visibility]) { + 0 -> Status.Visibility.public + 1 -> Status.Visibility.unlisted + 2 -> Status.Visibility.private + 3 -> Status.Visibility.direct + else -> Status.Visibility.public + }, + sensitive = it[Posts.sensitive], + spoilerText = it[Posts.overview].orEmpty(), + mediaAttachments = emptyList(), + mentions = emptyList(), + tags = emptyList(), + emojis = emptyList(), + reblogsCount = 0, + favouritesCount = 0, + repliesCount = 0, + url = it[Posts.apId], + inReplyToId = it[Posts.replyId].toString(), + inReplyToAccountId = null, + language = null, + text = it[Posts.text], + editedAt = null +) diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/MediaRepositoryImpl.kt b/src/main/kotlin/dev/usbharu/hideout/repository/MediaRepositoryImpl.kt index 610d3e5a..cfcae2c4 100644 --- a/src/main/kotlin/dev/usbharu/hideout/repository/MediaRepositoryImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/repository/MediaRepositoryImpl.kt @@ -55,18 +55,18 @@ class MediaRepositoryImpl(private val idGenerateService: IdGenerateService) : Me Media.id eq id } } +} - fun ResultRow.toMedia(): EntityMedia { - return EntityMedia( - id = this[Media.id], - name = this[Media.name], - url = this[Media.url], - remoteUrl = this[Media.remoteUrl], - thumbnailUrl = this[Media.thumbnailUrl], - type = FileType.values().first { it.ordinal == this[Media.type] }, - blurHash = this[Media.blurhash], - ) - } +fun ResultRow.toMedia(): EntityMedia { + return EntityMedia( + id = this[Media.id], + name = this[Media.name], + url = this[Media.url], + remoteUrl = this[Media.remoteUrl], + thumbnailUrl = this[Media.thumbnailUrl], + type = FileType.values().first { it.ordinal == this[Media.type] }, + blurHash = this[Media.blurhash], + ) } object Media : Table("media") { @@ -77,4 +77,5 @@ object Media : Table("media") { val thumbnailUrl = varchar("thumbnail_url", 255).nullable() val type = integer("type") val blurhash = varchar("blurhash", 255).nullable() + override val primaryKey = PrimaryKey(id) } diff --git a/src/main/kotlin/dev/usbharu/hideout/repository/PostRepositoryImpl.kt b/src/main/kotlin/dev/usbharu/hideout/repository/PostRepositoryImpl.kt index 3bafd654..400126ea 100644 --- a/src/main/kotlin/dev/usbharu/hideout/repository/PostRepositoryImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/repository/PostRepositoryImpl.kt @@ -29,7 +29,18 @@ class PostRepositoryImpl(private val idGenerateService: IdGenerateService) : Pos it[sensitive] = post.sensitive it[apId] = post.apId } + PostsMedia.batchInsert(post.mediaIds) { + this[PostsMedia.postId] = post.id + this[PostsMedia.mediaId] = it + } } else { + PostsMedia.deleteWhere { + PostsMedia.postId eq post.id + } + PostsMedia.batchInsert(post.mediaIds) { + this[PostsMedia.postId] = post.id + this[PostsMedia.mediaId] = it + } Posts.update({ Posts.id eq post.id }) { it[userId] = post.userId it[overview] = post.overview @@ -46,8 +57,12 @@ class PostRepositoryImpl(private val idGenerateService: IdGenerateService) : Pos return post } - override suspend fun findById(id: Long): Post = Posts.select { Posts.id eq id }.singleOrNull()?.toPost() - ?: throw FailedToGetResourcesException("id: $id was not found.") + override suspend fun findById(id: Long): Post = + Posts.innerJoin(PostsMedia, onColumn = { Posts.id }, otherColumn = { PostsMedia.postId }) + .select { Posts.id eq id } + .toPost() + .singleOrNull() + ?: throw FailedToGetResourcesException("id: $id was not found.") override suspend fun delete(id: Long) { Posts.deleteWhere { Posts.id eq id } @@ -69,6 +84,12 @@ object Posts : Table() { override val primaryKey: PrimaryKey = PrimaryKey(id) } +object PostsMedia : Table() { + val postId = long("post_id").references(Posts.id, ReferenceOption.CASCADE, ReferenceOption.CASCADE) + val mediaId = long("media_id").references(Media.id, ReferenceOption.CASCADE, ReferenceOption.CASCADE) + override val primaryKey = PrimaryKey(postId, mediaId) +} + fun ResultRow.toPost(): Post { return Post.of( id = this[Posts.id], @@ -81,6 +102,12 @@ fun ResultRow.toPost(): Post { repostId = this[Posts.repostId], replyId = this[Posts.replyId], sensitive = this[Posts.sensitive], - apId = this[Posts.apId] + apId = this[Posts.apId], ) } + +fun Query.toPost(): List { + return this.groupBy { it[Posts.id] } + .map { it.value } + .map { it.first().toPost().copy(mediaIds = it.map { it[PostsMedia.mediaId] }) } +} diff --git a/src/main/kotlin/dev/usbharu/hideout/service/ap/APNoteService.kt b/src/main/kotlin/dev/usbharu/hideout/service/ap/APNoteService.kt index bc1958ea..663b2d57 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/ap/APNoteService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/ap/APNoteService.kt @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.readValue import dev.usbharu.hideout.config.ApplicationConfig import dev.usbharu.hideout.domain.model.ap.Create +import dev.usbharu.hideout.domain.model.ap.Document import dev.usbharu.hideout.domain.model.ap.Note import dev.usbharu.hideout.domain.model.hideout.entity.Post import dev.usbharu.hideout.domain.model.hideout.entity.Visibility @@ -13,6 +14,7 @@ import dev.usbharu.hideout.exception.ap.IllegalActivityPubObjectException import dev.usbharu.hideout.plugins.getAp import dev.usbharu.hideout.plugins.postAp import dev.usbharu.hideout.query.FollowerQueryService +import dev.usbharu.hideout.query.MediaQueryService import dev.usbharu.hideout.query.PostQueryService import dev.usbharu.hideout.query.UserQueryService import dev.usbharu.hideout.repository.PostRepository @@ -46,6 +48,7 @@ class APNoteServiceImpl( private val userQueryService: UserQueryService, private val followerQueryService: FollowerQueryService, private val postQueryService: PostQueryService, + private val mediaQueryService: MediaQueryService, @Qualifier("activitypub") private val objectMapper: ObjectMapper, private val applicationConfig: ApplicationConfig, private val postService: PostService @@ -62,11 +65,13 @@ class APNoteServiceImpl( val followers = followerQueryService.findFollowersById(post.userId) val userEntity = userQueryService.findById(post.userId) val note = objectMapper.writeValueAsString(post) + val mediaList = objectMapper.writeValueAsString(mediaQueryService.findByPostId(post.id)) followers.forEach { followerEntity -> jobQueueParentService.schedule(DeliverPostJob) { props[DeliverPostJob.actor] = userEntity.url props[DeliverPostJob.post] = note props[DeliverPostJob.inbox] = followerEntity.inbox + props[DeliverPostJob.media] = mediaList } } } @@ -74,13 +79,19 @@ class APNoteServiceImpl( override suspend fun createNoteJob(props: JobProps) { val actor = props[DeliverPostJob.actor] val postEntity = objectMapper.readValue(props[DeliverPostJob.post]) + val mediaList = + objectMapper.readValue>( + props[DeliverPostJob.media] + ) val note = Note( name = "Note", id = postEntity.url, attributedTo = actor, content = postEntity.text, published = Instant.ofEpochMilli(postEntity.createdAt).toString(), - to = listOf(public, "$actor/follower") + to = listOf(public, "$actor/follower"), + attachment = mediaList.map { Document(mediaType = "image/jpeg", url = it.url) } + ) val inbox = props[DeliverPostJob.inbox] logger.debug("createNoteJob: actor={}, note={}, inbox={}", actor, postEntity, inbox) @@ -168,6 +179,7 @@ class APNoteServiceImpl( postQueryService.findByUrl(it) } + // TODO: リモートのメディア処理を追加 postService.createRemote( Post.of( id = postRepository.generateId(), diff --git a/src/main/kotlin/dev/usbharu/hideout/service/api/mastodon/StatusesApiService.kt b/src/main/kotlin/dev/usbharu/hideout/service/api/mastodon/StatusesApiService.kt index bebaa59a..039dfc2c 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/api/mastodon/StatusesApiService.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/api/mastodon/StatusesApiService.kt @@ -1,12 +1,15 @@ package dev.usbharu.hideout.service.api.mastodon +import dev.usbharu.hideout.domain.mastodon.model.generated.MediaAttachment import dev.usbharu.hideout.domain.mastodon.model.generated.Status -import dev.usbharu.hideout.domain.mastodon.model.generated.StatusesRequest +import dev.usbharu.hideout.domain.model.hideout.dto.FileType import dev.usbharu.hideout.domain.model.hideout.dto.PostCreateDto import dev.usbharu.hideout.domain.model.hideout.entity.Visibility +import dev.usbharu.hideout.domain.model.mastodon.StatusesRequest import dev.usbharu.hideout.exception.FailedToGetResourcesException import dev.usbharu.hideout.query.PostQueryService import dev.usbharu.hideout.query.UserQueryService +import dev.usbharu.hideout.repository.MediaRepository import dev.usbharu.hideout.service.core.Transaction import dev.usbharu.hideout.service.mastodon.AccountService import dev.usbharu.hideout.service.post.PostService @@ -15,7 +18,10 @@ import java.time.Instant @Service interface StatusesApiService { - suspend fun postStatus(statusesRequest: StatusesRequest, userId: Long): Status + suspend fun postStatus( + statusesRequest: dev.usbharu.hideout.domain.model.mastodon.StatusesRequest, + userId: Long + ): Status } @Service @@ -24,11 +30,16 @@ class StatsesApiServiceImpl( private val accountService: AccountService, private val postQueryService: PostQueryService, private val userQueryService: UserQueryService, + private val mediaRepository: MediaRepository, private val transaction: Transaction ) : StatusesApiService { - @Suppress("LongMethod") - override suspend fun postStatus(statusesRequest: StatusesRequest, userId: Long): Status = transaction.transaction { + @Suppress("LongMethod", "CyclomaticComplexMethod") + override suspend fun postStatus( + statusesRequest: dev.usbharu.hideout.domain.model.mastodon.StatusesRequest, + userId: Long + ): Status = transaction.transaction { + println("Post status media ids " + statusesRequest.media_ids) val visibility = when (statusesRequest.visibility) { StatusesRequest.Visibility.public -> Visibility.PUBLIC StatusesRequest.Visibility.unlisted -> Visibility.UNLISTED @@ -40,10 +51,11 @@ class StatsesApiServiceImpl( val post = postService.createLocal( PostCreateDto( text = statusesRequest.status.orEmpty(), - overview = statusesRequest.spoilerText, + overview = statusesRequest.spoiler_text, visibility = visibility, - repolyId = statusesRequest.inReplyToId?.toLongOrNull(), - userId = userId + repolyId = statusesRequest.in_reply_to_id?.toLongOrNull(), + userId = userId, + mediaIds = statusesRequest.media_ids.map { it.toLong() } ) ) val account = accountService.findById(userId) @@ -66,6 +78,27 @@ class StatsesApiServiceImpl( null } + // TODO: n+1解消 + val mediaAttachment = post.mediaIds.map { mediaId -> + mediaRepository.findById(mediaId) + }.map { + MediaAttachment( + id = it.id.toString(), + type = when (it.type) { + FileType.Image -> MediaAttachment.Type.image + FileType.Video -> MediaAttachment.Type.video + FileType.Audio -> MediaAttachment.Type.audio + FileType.Unknown -> MediaAttachment.Type.unknown + }, + url = it.url, + previewUrl = it.thumbnailUrl, + remoteUrl = it.remoteUrl, + description = "", + blurhash = it.blurHash, + textUrl = it.url + ) + } + Status( id = post.id.toString(), uri = post.apId, @@ -75,7 +108,7 @@ class StatsesApiServiceImpl( visibility = postVisibility, sensitive = post.sensitive, spoilerText = post.overview.orEmpty(), - mediaAttachments = emptyList(), + mediaAttachments = mediaAttachment, mentions = emptyList(), tags = emptyList(), emojis = emptyList(), @@ -87,7 +120,7 @@ class StatsesApiServiceImpl( inReplyToAccountId = replyUser?.toString(), language = null, text = post.text, - editedAt = null + editedAt = null, ) } } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/media/MediaServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/media/MediaServiceImpl.kt index 313bb8e1..b208f2f3 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/media/MediaServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/media/MediaServiceImpl.kt @@ -20,6 +20,7 @@ import javax.imageio.ImageIO import dev.usbharu.hideout.domain.model.hideout.entity.Media as EntityMedia @Service +@Suppress("TooGenericExceptionCaught") class MediaServiceImpl( private val mediaDataStore: MediaDataStore, private val fileTypeDeterminationService: FileTypeDeterminationService, diff --git a/src/main/kotlin/dev/usbharu/hideout/service/media/S3MediaDataStore.kt b/src/main/kotlin/dev/usbharu/hideout/service/media/S3MediaDataStore.kt index a10482f5..6dd357c5 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/media/S3MediaDataStore.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/media/S3MediaDataStore.kt @@ -29,7 +29,7 @@ class S3MediaDataStore(private val s3Client: S3Client, private val storageConfig .key(thumbnailKey) .build() - val pairList = withContext(Dispatchers.IO) { + withContext(Dispatchers.IO) { awaitAll( async { if (dataMediaSave.thumbnailInputStream != null) { @@ -37,23 +37,23 @@ class S3MediaDataStore(private val s3Client: S3Client, private val storageConfig thumbnailUploadRequest, RequestBody.fromBytes(dataMediaSave.thumbnailInputStream) ) - "thumbnail" to s3Client.utilities() + s3Client.utilities() .getUrl(GetUrlRequest.builder().bucket(storageConfig.bucket).key(thumbnailKey).build()) } else { - "thumbnail" to null + null } }, async { s3Client.putObject(fileUploadRequest, RequestBody.fromBytes(dataMediaSave.fileInputStream)) - "file" to s3Client.utilities() + s3Client.utilities() .getUrl(GetUrlRequest.builder().bucket(storageConfig.bucket).key(dataMediaSave.name).build()) } ) - }.toMap() + } return SuccessSavedMedia( - dataMediaSave.name, - pairList.getValue("file").toString(), - pairList.getValue("thumbnail").toString() + name = dataMediaSave.name, + url = "${storageConfig.publicUrl}/${storageConfig.bucket}/${dataMediaSave.name}", + thumbnailUrl = "${storageConfig.publicUrl}/${storageConfig.bucket}/$thumbnailKey" ) } diff --git a/src/main/kotlin/dev/usbharu/hideout/service/media/converter/MediaProcessServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/media/converter/MediaProcessServiceImpl.kt index c32688d5..7ad96ecd 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/media/converter/MediaProcessServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/media/converter/MediaProcessServiceImpl.kt @@ -8,6 +8,7 @@ import org.slf4j.LoggerFactory import org.springframework.stereotype.Service @Service +@Suppress("TooGenericExceptionCaught") class MediaProcessServiceImpl( private val mediaConverterRoot: MediaConverterRoot, private val thumbnailGenerateService: ThumbnailGenerateService diff --git a/src/main/kotlin/dev/usbharu/hideout/service/post/PostServiceImpl.kt b/src/main/kotlin/dev/usbharu/hideout/service/post/PostServiceImpl.kt index 755e5bb7..dbf30d9c 100644 --- a/src/main/kotlin/dev/usbharu/hideout/service/post/PostServiceImpl.kt +++ b/src/main/kotlin/dev/usbharu/hideout/service/post/PostServiceImpl.kt @@ -44,7 +44,8 @@ class PostServiceImpl( text = post.text, createdAt = Instant.now().toEpochMilli(), visibility = post.visibility, - url = "${user.url}/posts/$id" + url = "${user.url}/posts/$id", + mediaIds = post.mediaIds ) return internalCreate(createPost, isLocal) } diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 1736f642..1b8d5251 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -13,4 +13,5 @@ + diff --git a/src/test/kotlin/dev/usbharu/hideout/service/ap/APNoteServiceImplTest.kt b/src/test/kotlin/dev/usbharu/hideout/service/ap/APNoteServiceImplTest.kt index 45f567d3..4a1e06bc 100644 --- a/src/test/kotlin/dev/usbharu/hideout/service/ap/APNoteServiceImplTest.kt +++ b/src/test/kotlin/dev/usbharu/hideout/service/ap/APNoteServiceImplTest.kt @@ -10,6 +10,7 @@ import dev.usbharu.hideout.domain.model.hideout.entity.User import dev.usbharu.hideout.domain.model.hideout.entity.Visibility import dev.usbharu.hideout.domain.model.job.DeliverPostJob import dev.usbharu.hideout.query.FollowerQueryService +import dev.usbharu.hideout.query.MediaQueryService import dev.usbharu.hideout.query.UserQueryService import dev.usbharu.hideout.service.job.JobQueueParentService import io.ktor.client.* @@ -19,6 +20,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import kotlinx.serialization.json.Json import org.junit.jupiter.api.Test +import org.mockito.Mockito.anyLong import org.mockito.Mockito.eq import org.mockito.kotlin.* import utils.JsonObjectMapper @@ -29,118 +31,131 @@ import kotlin.test.assertEquals class APNoteServiceImplTest { @Test - fun `createPost 新しい投稿`() = runTest { - val followers = listOf( - User.of( - 2L, - "follower", - "follower.example.com", - "followerUser", - "test follower user", - "https://follower.example.com/inbox", - "https://follower.example.com/outbox", - "https://follower.example.com", - "https://follower.example.com", - publicKey = "", - createdAt = Instant.now() - ), - User.of( - 3L, - "follower2", - "follower2.example.com", - "follower2User", - "test follower2 user", - "https://follower2.example.com/inbox", - "https://follower2.example.com/outbox", - "https://follower2.example.com", - "https://follower2.example.com", - publicKey = "", - createdAt = Instant.now() + fun `createPost 新しい投稿`() { + val mediaQueryService = mock { + onBlocking { findByPostId(anyLong()) } doReturn emptyList() + } + runTest { + val followers = listOf( + User.of( + 2L, + "follower", + "follower.example.com", + "followerUser", + "test follower user", + "https://follower.example.com/inbox", + "https://follower.example.com/outbox", + "https://follower.example.com", + "https://follower.example.com", + publicKey = "", + createdAt = Instant.now() + ), + User.of( + 3L, + "follower2", + "follower2.example.com", + "follower2User", + "test follower2 user", + "https://follower2.example.com/inbox", + "https://follower2.example.com/outbox", + "https://follower2.example.com", + "https://follower2.example.com", + publicKey = "", + createdAt = Instant.now() + ) ) - ) - val userQueryService = mock { - onBlocking { findById(eq(1L)) } doReturn User.of( + val userQueryService = mock { + onBlocking { findById(eq(1L)) } doReturn User.of( + 1L, + "test", + "example.com", + "testUser", + "test user", + "a", + "https://example.com/inbox", + "https://example.com/outbox", + "https://example.com", + publicKey = "", + privateKey = "a", + createdAt = Instant.now() + ) + } + val followerQueryService = mock { + onBlocking { findFollowersById(eq(1L)) } doReturn followers + } + val jobQueueParentService = mock() + val activityPubNoteService = + APNoteServiceImpl( + httpClient = mock(), + jobQueueParentService = jobQueueParentService, + postRepository = mock(), + apUserService = mock(), + userQueryService = userQueryService, + followerQueryService = followerQueryService, + postQueryService = mock(), + objectMapper = objectMapper, + applicationConfig = testApplicationConfig, + postService = mock(), + mediaQueryService = mediaQueryService + ) + val postEntity = Post.of( 1L, - "test", - "example.com", - "testUser", - "test user", - "a", - "https://example.com/inbox", - "https://example.com/outbox", - "https://example.com", - publicKey = "", - privateKey = "a", - createdAt = Instant.now() + 1L, + null, + "test text", + 1L, + Visibility.PUBLIC, + "https://example.com" ) + activityPubNoteService.createNote(postEntity) + verify(jobQueueParentService, times(2)).schedule(eq(DeliverPostJob), any()) } - val followerQueryService = mock { - onBlocking { findFollowersById(eq(1L)) } doReturn followers - } - val jobQueueParentService = mock() - val activityPubNoteService = - APNoteServiceImpl( - httpClient = mock(), - jobQueueParentService = jobQueueParentService, + } + + @Test + fun `createPostJob 新しい投稿のJob`() { + runTest { + val mediaQueryService = mock { + onBlocking { findByPostId(anyLong()) } doReturn emptyList() + } + Config.configData = ConfigData(objectMapper = JsonObjectMapper.objectMapper) + val httpClient = HttpClient( + MockEngine { httpRequestData -> + assertEquals("https://follower.example.com/inbox", httpRequestData.url.toString()) + respondOk() + } + ) + val activityPubNoteService = APNoteServiceImpl( + httpClient = httpClient, + jobQueueParentService = mock(), postRepository = mock(), apUserService = mock(), - userQueryService = userQueryService, - followerQueryService = followerQueryService, + userQueryService = mock(), + followerQueryService = mock(), postQueryService = mock(), objectMapper = objectMapper, applicationConfig = testApplicationConfig, postService = mock(), + mediaQueryService = mediaQueryService ) - val postEntity = Post.of( - 1L, - 1L, - null, - "test text", - 1L, - Visibility.PUBLIC, - "https://example.com" - ) - activityPubNoteService.createNote(postEntity) - verify(jobQueueParentService, times(2)).schedule(eq(DeliverPostJob), any()) - } - - @Test - fun `createPostJob 新しい投稿のJob`() = runTest { - Config.configData = ConfigData(objectMapper = JsonObjectMapper.objectMapper) - val httpClient = HttpClient( - MockEngine { httpRequestData -> - assertEquals("https://follower.example.com/inbox", httpRequestData.url.toString()) - respondOk() - } - ) - val activityPubNoteService = APNoteServiceImpl( - httpClient = httpClient, - jobQueueParentService = mock(), - postRepository = mock(), - apUserService = mock(), - userQueryService = mock(), - followerQueryService = mock(), - postQueryService = mock(), - objectMapper = objectMapper, - applicationConfig = testApplicationConfig, - postService = mock(), - ) - activityPubNoteService.createNoteJob( - JobProps( - data = mapOf( - DeliverPostJob.actor.name to "https://follower.example.com", - DeliverPostJob.post.name to """{ - "id": 1, - "userId": 1, - "text": "test text", - "createdAt": 132525324, - "visibility": 0, - "url": "https://example.com" -}""", - DeliverPostJob.inbox.name to "https://follower.example.com/inbox" - ), - json = Json + activityPubNoteService.createNoteJob( + JobProps( + data = mapOf( + DeliverPostJob.actor.name to "https://follower.example.com", + DeliverPostJob.post.name to """{ + "id": 1, + "userId": 1, + "text": "test text", + "createdAt": 132525324, + "visibility": 0, + "url": "https://example.com" + }""", + DeliverPostJob.inbox.name to "https://follower.example.com/inbox", + DeliverPostJob.media.name to "[]" + ), + json = Json + ) ) - ) + } } } diff --git a/templates/api.mustache b/templates/api.mustache new file mode 100644 index 00000000..9d0dc1da --- /dev/null +++ b/templates/api.mustache @@ -0,0 +1,96 @@ +package {{package}} + +{{#imports}}import {{import}} +{{/imports}} +{{#swagger2AnnotationLibrary}} + import io.swagger.v3.oas.annotations.* + import io.swagger.v3.oas.annotations.enums.* + import io.swagger.v3.oas.annotations.media.* + import io.swagger.v3.oas.annotations.responses.* + import io.swagger.v3.oas.annotations.security.* +{{/swagger2AnnotationLibrary}} +{{#swagger1AnnotationLibrary}} + import io.swagger.annotations.Api + import io.swagger.annotations.ApiOperation + import io.swagger.annotations.ApiParam + import io.swagger.annotations.ApiResponse + import io.swagger.annotations.ApiResponses + import io.swagger.annotations.Authorization + import io.swagger.annotations.AuthorizationScope +{{/swagger1AnnotationLibrary}} +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity + +import org.springframework.web.bind.annotation.* +{{#useBeanValidation}} + import org.springframework.validation.annotation.Validated +{{/useBeanValidation}} +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.beans.factory.annotation.Autowired +import dev.usbharu.hideout.config.JsonOrFormBind + +{{#useBeanValidation}} + import {{javaxPackage}}.validation.Valid + import {{javaxPackage}}.validation.constraints.DecimalMax + import {{javaxPackage}}.validation.constraints.DecimalMin + import {{javaxPackage}}.validation.constraints.Email + import {{javaxPackage}}.validation.constraints.Max + import {{javaxPackage}}.validation.constraints.Min + import {{javaxPackage}}.validation.constraints.NotNull + import {{javaxPackage}}.validation.constraints.Pattern + import {{javaxPackage}}.validation.constraints.Size +{{/useBeanValidation}} + +{{#reactive}} + import kotlinx.coroutines.flow.Flow +{{/reactive}} +import kotlin.collections.List +import kotlin.collections.Map + +@RestController{{#beanQualifiers}}("{{package}}.{{classname}}Controller"){{/beanQualifiers}} +{{#useBeanValidation}} + @Validated +{{/useBeanValidation}} +{{#swagger1AnnotationLibrary}} + @Api(value = "{{{baseName}}}", description = "The {{{baseName}}} API") +{{/swagger1AnnotationLibrary}} +{{=<% %>=}} +@RequestMapping("\${api.base-path:<%contextPath%>}") +<%={{ }}=%> +{{#operations}} + class {{classname}}Controller({{#serviceInterface}}@Autowired(required = true) val service: {{classname}}Service{{/serviceInterface}}) { + {{#operation}} + + {{#swagger2AnnotationLibrary}} + @Operation( + summary = "{{{summary}}}", + operationId = "{{{operationId}}}", + description = """{{{unescapedNotes}}}""", + responses = [{{#responses}} + ApiResponse(responseCode = "{{{code}}}", description = "{{{message}}}"{{#baseType}}, content = [Content({{#isArray}}array = ArraySchema({{/isArray}}schema = Schema(implementation = {{{baseType}}}::class)){{#isArray}}){{/isArray}}]{{/baseType}}){{^-last}},{{/-last}}{{/responses}} ]{{#hasAuthMethods}}, + security = [ {{#authMethods}}SecurityRequirement(name = "{{name}}"{{#isOAuth}}, scopes = [ {{#scopes}}"{{scope}}"{{^-last}}, {{/-last}}{{/scopes}} ]{{/isOAuth}}){{^-last}},{{/-last}}{{/authMethods}} ]{{/hasAuthMethods}} + ){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}} + @ApiOperation( + value = "{{{summary}}}", + nickname = "{{{operationId}}}", + notes = "{{{notes}}}"{{#returnBaseType}}, + response = {{{.}}}::class{{/returnBaseType}}{{#returnContainer}}, + responseContainer = "{{{.}}}"{{/returnContainer}}{{#hasAuthMethods}}, + authorizations = [{{#authMethods}}Authorization(value = "{{name}}"{{#isOAuth}}, scopes = [{{#scopes}}AuthorizationScope(scope = "{{scope}}", description = "{{description}}"){{^-last}}, {{/-last}}{{/scopes}}]{{/isOAuth}}){{^-last}}, {{/-last}}{{/authMethods}}]{{/hasAuthMethods}}) + @ApiResponses( + value = [{{#responses}}ApiResponse(code = {{{code}}}, message = "{{{message}}}"{{#baseType}}, response = {{{.}}}::class{{/baseType}}{{#containerType}}, responseContainer = "{{{.}}}"{{/containerType}}){{^-last}},{{/-last}}{{/responses}}]){{/swagger1AnnotationLibrary}} + @RequestMapping( + method = [RequestMethod.{{httpMethod}}], + value = ["{{#lambda.escapeDoubleQuote}}{{path}}{{/lambda.escapeDoubleQuote}}"]{{#singleContentTypes}}{{#hasProduces}}, + produces = "{{{vendorExtensions.x-accepts}}}"{{/hasProduces}}{{#hasConsumes}}, + consumes = "{{{vendorExtensions.x-content-type}}}"{{/hasConsumes}}{{/singleContentTypes}}{{^singleContentTypes}}{{#hasProduces}}, + produces = [{{#produces}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/produces}}]{{/hasProduces}}{{#hasConsumes}}, + consumes = [{{#consumes}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/consumes}}]{{/hasConsumes}}{{/singleContentTypes}} + ) + {{#reactive}}{{^isArray}}suspend {{/isArray}}{{/reactive}}fun {{operationId}}({{#allParams}}{{>queryParams}}{{>pathParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{^-last}},{{/-last}}{{/allParams}}): ResponseEntity<{{>returnTypes}}> { + return {{>returnValue}} + } + {{/operation}} + } +{{/operations}} diff --git a/templates/apiController.mustache b/templates/apiController.mustache new file mode 100644 index 00000000..3c70a76f --- /dev/null +++ b/templates/apiController.mustache @@ -0,0 +1,25 @@ +package {{package}} + +import org.springframework.stereotype.Controller +import org.springframework.web.bind.annotation.RequestMapping +import java.util.Optional +import dev.usbharu.hideout.config.JsonOrFormBind + +{{>generatedAnnotation}} +@Controller{{#beanQualifiers}}("{{package}}.{{classname}}Controller"){{/beanQualifiers}} +{{=<% %>=}} +@RequestMapping("\${openapi.<%title%>.base-path:<%>defaultBasePath%>}") +<%={{ }}=%> +{{#operations}} + class {{classname}}Controller( + @org.springframework.beans.factory.annotation.Autowired(required = false) delegate: {{classname}}Delegate? + ) : {{classname}} { + private val delegate: {{classname}}Delegate + + init { + this.delegate = Optional.ofNullable(delegate).orElse(object : {{classname}}Delegate {}) + } + + override fun getDelegate(): {{classname}}Delegate = delegate + } +{{/operations}} diff --git a/templates/apiDelegate.mustache b/templates/apiDelegate.mustache new file mode 100644 index 00000000..e7d62ddb --- /dev/null +++ b/templates/apiDelegate.mustache @@ -0,0 +1,46 @@ +package {{package}} + +{{#imports}}import {{import}} +{{/imports}} +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.core.io.Resource +{{#reactive}} + import kotlinx.coroutines.flow.Flow +{{/reactive}} + +import java.util.Optional +{{#async}} + import java.util.concurrent.CompletableFuture +{{/async}} + +{{#operations}} + /** + * A delegate to be called by the {@link {{classname}}Controller}}. + * Implement this interface with a {@link org.springframework.stereotype.Service} annotated class. + */ + {{>generatedAnnotation}} + interface {{classname}}Delegate { + + fun getRequest(): Optional + = Optional.empty() + {{#operation}} + + /** + * @see {{classname}}#{{operationId}} + */ + {{#reactive}}{{^isArray}}suspend {{/isArray}}{{/reactive}}fun {{operationId}}({{#allParams}}{{paramName}} + : {{^isFile}}{{^reactive}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isArray}}{{>optionalDataType}}{{/isArray}}{{#isArray}}{{#isBodyParam}} + Flow<{{{baseType}}} + >{{/isBodyParam}}{{^isBodyParam}}{{>optionalDataType}}{{/isBodyParam}}{{/isArray}}{{/reactive}}{{/isFile}}{{#isFile}} + Resource?{{/isFile}}{{^-last}}, + {{/-last}}{{/allParams}}): {{#responseWrapper}}{{.}} + <{{/responseWrapper}}ResponseEntity<{{>returnTypes}}>{{#responseWrapper}}>{{/responseWrapper}} { + {{>methodBody}} + } + + {{/operation}} + } +{{/operations}} diff --git a/templates/apiInterface.mustache b/templates/apiInterface.mustache new file mode 100644 index 00000000..ca99383c --- /dev/null +++ b/templates/apiInterface.mustache @@ -0,0 +1,112 @@ +/** +* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) ({{{generatorVersion}}}). +* https://openapi-generator.tech +* Do not edit the class manually. +*/ +package {{package}} + +{{#imports}}import {{import}} +{{/imports}} +{{#swagger2AnnotationLibrary}} + import io.swagger.v3.oas.annotations.* + import io.swagger.v3.oas.annotations.enums.* + import io.swagger.v3.oas.annotations.media.* + import io.swagger.v3.oas.annotations.responses.* + import io.swagger.v3.oas.annotations.security.* +{{/swagger2AnnotationLibrary}} +{{#swagger1AnnotationLibrary}} + import io.swagger.annotations.Api + import io.swagger.annotations.ApiOperation + import io.swagger.annotations.ApiParam + import io.swagger.annotations.ApiResponse + import io.swagger.annotations.ApiResponses + import io.swagger.annotations.Authorization + import io.swagger.annotations.AuthorizationScope +{{/swagger1AnnotationLibrary}} +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity + +import org.springframework.web.bind.annotation.* +{{#useBeanValidation}} + import org.springframework.validation.annotation.Validated +{{/useBeanValidation}} +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.beans.factory.annotation.Autowired + +{{#useBeanValidation}} + import {{javaxPackage}}.validation.constraints.DecimalMax + import {{javaxPackage}}.validation.constraints.DecimalMin + import {{javaxPackage}}.validation.constraints.Email + import {{javaxPackage}}.validation.constraints.Max + import {{javaxPackage}}.validation.constraints.Min + import {{javaxPackage}}.validation.constraints.NotNull + import {{javaxPackage}}.validation.constraints.Pattern + import {{javaxPackage}}.validation.constraints.Size + import {{javaxPackage}}.validation.Valid +{{/useBeanValidation}} + +{{#reactive}} + import kotlinx.coroutines.flow.Flow +{{/reactive}} +import kotlin.collections.List +import kotlin.collections.Map +import dev.usbharu.hideout.config.JsonOrFormBind + +{{#useBeanValidation}} + @Validated +{{/useBeanValidation}} +{{#swagger1AnnotationLibrary}} + @Api(value = "{{{baseName}}}", description = "The {{{baseName}}} API") +{{/swagger1AnnotationLibrary}} +{{^useFeignClient}} + {{=<% %>=}} + @RequestMapping("\${api.base-path:<%contextPath%>}") + <%={{ }}=%> +{{/useFeignClient}} +{{#operations}} + interface {{classname}} { + {{#isDelegate}} + + fun getDelegate(): {{classname}}Delegate = object: {{classname}}Delegate {} + {{/isDelegate}} + {{#operation}} + + {{#swagger2AnnotationLibrary}} + @Operation( + summary = "{{{summary}}}", + operationId = "{{{operationId}}}", + description = """{{{unescapedNotes}}}""", + responses = [{{#responses}} + ApiResponse(responseCode = "{{{code}}}", description = "{{{message}}}"{{#baseType}}, content = [Content({{#isArray}}array = ArraySchema({{/isArray}}schema = Schema(implementation = {{{baseType}}}::class)){{#isArray}}){{/isArray}}]{{/baseType}}){{^-last}},{{/-last}}{{/responses}} + ]{{#hasAuthMethods}}, + security = [ {{#authMethods}}SecurityRequirement(name = "{{name}}"{{#isOAuth}}, scopes = [ {{#scopes}}"{{scope}}"{{^-last}}, {{/-last}}{{/scopes}} ]{{/isOAuth}}){{^-last}},{{/-last}}{{/authMethods}} ]{{/hasAuthMethods}} + ){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}} + @ApiOperation( + value = "{{{summary}}}", + nickname = "{{{operationId}}}", + notes = "{{{notes}}}"{{#returnBaseType}}, + response = {{{.}}}::class{{/returnBaseType}}{{#returnContainer}}, + responseContainer = "{{{.}}}"{{/returnContainer}}{{#hasAuthMethods}}, + authorizations = [{{#authMethods}}Authorization(value = "{{name}}"{{#isOAuth}}, scopes = [{{#scopes}}AuthorizationScope(scope = "{{scope}}", description = "{{description}}"){{^-last}}, {{/-last}}{{/scopes}}]{{/isOAuth}}){{^-last}}, {{/-last}}{{/authMethods}}]{{/hasAuthMethods}}) + @ApiResponses( + value = [{{#responses}}ApiResponse(code = {{{code}}}, message = "{{{message}}}"{{#baseType}}, response = {{{.}}}::class{{/baseType}}{{#containerType}}, responseContainer = "{{{.}}}"{{/containerType}}){{^-last}}, {{/-last}}{{/responses}}]){{/swagger1AnnotationLibrary}} + @RequestMapping( + method = [RequestMethod.{{httpMethod}}], + value = ["{{#lambda.escapeDoubleQuote}}{{path}}{{/lambda.escapeDoubleQuote}}"]{{#singleContentTypes}}{{#hasProduces}}, + produces = "{{{vendorExtensions.x-accepts}}}"{{/hasProduces}}{{#hasConsumes}}, + consumes = "{{{vendorExtensions.x-content-type}}}"{{/hasConsumes}}{{/singleContentTypes}}{{^singleContentTypes}}{{#hasProduces}}, + produces = [{{#produces}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/produces}}]{{/hasProduces}}{{#hasConsumes}}, + consumes = [{{#consumes}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/consumes}}]{{/hasConsumes}}{{/singleContentTypes}} + ) + {{#reactive}}{{^isArray}}suspend {{/isArray}}{{/reactive}}fun {{operationId}}({{#allParams}}{{>queryParams}}{{>pathParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{^-last}},{{/-last}}{{/allParams}}): ResponseEntity<{{>returnTypes}}>{{^skipDefaultInterface}} { + {{^isDelegate}} + return {{>returnValue}} + {{/isDelegate}} + {{#isDelegate}} + return getDelegate().{{operationId}}({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}) + {{/isDelegate}} + }{{/skipDefaultInterface}} + {{/operation}} + } +{{/operations}} diff --git a/templates/apiUtil.mustache b/templates/apiUtil.mustache new file mode 100644 index 00000000..101ed40e --- /dev/null +++ b/templates/apiUtil.mustache @@ -0,0 +1,23 @@ +package {{apiPackage}} + +{{^reactive}} + import org.springframework.web.context.request.NativeWebRequest + + import {{javaxPackage}}.servlet.http.HttpServletResponse + import java.io.IOException +{{/reactive}} + +object ApiUtil { +{{^reactive}} + fun setExampleResponse(req: NativeWebRequest, contentType: String, example: String) { + try { + val res = req.getNativeResponse(HttpServletResponse::class.java) + res?.characterEncoding = "UTF-8" + res?.addHeader("Content-Type", contentType) + res?.writer?.print(example) + } catch (e: IOException) { + throw RuntimeException(e) + } + } +{{/reactive}} +} diff --git a/templates/api_test.mustache b/templates/api_test.mustache new file mode 100644 index 00000000..d84af783 --- /dev/null +++ b/templates/api_test.mustache @@ -0,0 +1,38 @@ +package {{package}} + +{{#imports}}import {{import}} +{{/imports}} +import org.junit.jupiter.api.Test +{{#reactive}} + import kotlinx.coroutines.flow.Flow + import kotlinx.coroutines.test.runBlockingTest +{{/reactive}} +import org.springframework.http.ResponseEntity + +class {{classname}}Test { + +{{#serviceInterface}} + private val service: {{classname}}Service = {{classname}}ServiceImpl() +{{/serviceInterface}} +private val api: {{classname}}Controller = {{classname}}Controller({{#serviceInterface}}service{{/serviceInterface}}) +{{#operations}} + {{#operation}} + + /** + * To test {{classname}}Controller.{{operationId}} + * + * @throws ApiException + * if the Api call fails + */ + @Test + fun {{operationId}}Test() {{#reactive}}= runBlockingTest {{/reactive}}{ + {{#allParams}} + val {{{paramName}}}: {{>optionalDataType}} = TODO() + {{/allParams}} + val response: ResponseEntity<{{>returnTypes}}> = api.{{operationId}}({{#allParams}}{{{paramName}}}{{^-last}}, {{/-last}}{{/allParams}}) + + // TODO: test validations + } + {{/operation}} +{{/operations}} +} diff --git a/templates/beanValidation.mustache b/templates/beanValidation.mustache new file mode 100644 index 00000000..ee25df4a --- /dev/null +++ b/templates/beanValidation.mustache @@ -0,0 +1,4 @@ +{{#isContainer}}{{^isPrimitiveType}}{{^isEnum}} + @field:Valid{{/isEnum}}{{/isPrimitiveType}}{{/isContainer}}{{! +}}{{^isContainer}}{{^isPrimitiveType}}{{^isNumber}}{{^isUuid}}{{^isDateTime}} + @field:Valid{{/isDateTime}}{{/isUuid}}{{/isNumber}}{{/isPrimitiveType}}{{/isContainer}} \ No newline at end of file diff --git a/templates/beanValidationModel.mustache b/templates/beanValidationModel.mustache new file mode 100644 index 00000000..99635314 --- /dev/null +++ b/templates/beanValidationModel.mustache @@ -0,0 +1,38 @@ +{{! +format: email +}}{{#isEmail}} + @get:Email{{/isEmail}}{{! +pattern set +}}{{#pattern}} + @get:Pattern(regexp="{{{.}}}"){{/pattern}}{{! +minLength && maxLength set +}}{{#minLength}}{{#maxLength}} + @get:Size(min={{minLength}},max={{maxLength}}){{/maxLength}}{{/minLength}}{{! +minLength set, maxLength not +}}{{#minLength}}{{^maxLength}} + @get:Size(min={{minLength}}){{/maxLength}}{{/minLength}}{{! +minLength not set, maxLength set +}}{{^minLength}}{{#maxLength}} + @get:Size(max={{.}}){{/maxLength}}{{/minLength}}{{! +@Size: minItems && maxItems set +}}{{#minItems}}{{#maxItems}} + @get:Size(min={{minItems}},max={{maxItems}}) {{/maxItems}}{{/minItems}}{{! +@Size: minItems set, maxItems not +}}{{#minItems}}{{^maxItems}} + @get:Size(min={{minItems}}){{/maxItems}}{{/minItems}}{{! +@Size: minItems not set && maxItems set +}}{{^minItems}}{{#maxItems}} + @get:Size(max={{.}}){{/maxItems}}{{/minItems}}{{! +check for integer or long / all others=decimal type with @Decimal* +isInteger set +}}{{#isInteger}}{{#minimum}} + @get:Min({{.}}){{/minimum}}{{#maximum}} + @get:Max({{.}}){{/maximum}}{{/isInteger}}{{! +isLong set +}}{{#isLong}}{{#minimum}} + @get:Min({{.}}L){{/minimum}}{{#maximum}} + @get:Max({{.}}L){{/maximum}}{{/isLong}}{{! +Not Integer, not Long => we have a decimal value! +}}{{^isInteger}}{{^isLong}}{{#minimum}} + @get:DecimalMin("{{.}}"){{/minimum}}{{#maximum}} + @get:DecimalMax("{{.}}"){{/maximum}}{{/isLong}}{{/isInteger}} \ No newline at end of file diff --git a/templates/beanValidationPath.mustache b/templates/beanValidationPath.mustache new file mode 100644 index 00000000..8eb9029b --- /dev/null +++ b/templates/beanValidationPath.mustache @@ -0,0 +1,22 @@ +{{#isEmail}}@Email {{/isEmail}}{{! +pattern set +}}{{#pattern}}@Pattern(regexp="{{{.}}}") {{/pattern}}{{! +minLength && maxLength set +}}{{#minLength}}{{#maxLength}}@Size(min={{minLength}},max={{maxLength}}) {{/maxLength}}{{/minLength}}{{! +minLength set, maxLength not +}}{{#minLength}}{{^maxLength}}@Size(min={{minLength}}) {{/maxLength}}{{/minLength}}{{! +minLength not set, maxLength set +}}{{^minLength}}{{#maxLength}}@Size(max={{.}}) {{/maxLength}}{{/minLength}}{{! +@Size: minItems && maxItems set +}}{{#minItems}}{{#maxItems}}@Size(min={{minItems}},max={{maxItems}}) {{/maxItems}}{{/minItems}}{{! +@Size: minItems set, maxItems not +}}{{#minItems}}{{^maxItems}}@Size(min={{minItems}}) {{/maxItems}}{{/minItems}}{{! +@Size: minItems not set && maxItems set +}}{{^minItems}}{{#maxItems}}@Size(max={{.}}) {{/maxItems}}{{/minItems}}{{! +check for integer or long / all others=decimal type with @Decimal* +isInteger set +}}{{#isInteger}}{{#minimum}}@Min({{.}}){{/minimum}}{{#maximum}} @Max({{.}}) {{/maximum}}{{/isInteger}}{{! +isLong set +}}{{#isLong}}{{#minimum}}@Min({{.}}L){{/minimum}}{{#maximum}} @Max({{.}}L) {{/maximum}}{{/isLong}}{{! +Not Integer, not Long => we have a decimal value! +}}{{^isInteger}}{{^isLong}}{{#minimum}}@DecimalMin("{{.}}"){{/minimum}}{{#maximum}} @DecimalMax("{{.}}") {{/maximum}}{{/isLong}}{{/isInteger}} \ No newline at end of file diff --git a/templates/beanValidationPathParams.mustache b/templates/beanValidationPathParams.mustache new file mode 100644 index 00000000..3c57e76b --- /dev/null +++ b/templates/beanValidationPathParams.mustache @@ -0,0 +1 @@ +{{! PathParam is always required, no @NotNull necessary }}{{>beanValidationPath}} \ No newline at end of file diff --git a/templates/beanValidationQueryParams.mustache b/templates/beanValidationQueryParams.mustache new file mode 100644 index 00000000..cc53bc96 --- /dev/null +++ b/templates/beanValidationQueryParams.mustache @@ -0,0 +1 @@ +{{#required}}@NotNull {{/required}}{{>beanValidationPath}} \ No newline at end of file diff --git a/templates/bodyParams.mustache b/templates/bodyParams.mustache new file mode 100644 index 00000000..483d28d5 --- /dev/null +++ b/templates/bodyParams.mustache @@ -0,0 +1 @@ +{{#isBodyParam}}{{#swagger2AnnotationLibrary}}@Parameter(description = "{{{description}}}"{{#required}}, required = true{{/required}}{{^isContainer}}{{#allowableValues}}{{#defaultValue}}, schema = Schema(allowableValues = ["{{{allowableValues}}}"], defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}){{/defaultValue}}{{/allowableValues}}{{^allowableValues}}{{#defaultValue}}, schema = Schema(defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}){{/defaultValue}}{{/allowableValues}}{{#allowableValues}}{{^defaultValue}}, schema = Schema(allowableValues = ["{{{allowableValues}}}"]){{/defaultValue}}{{/allowableValues}}{{/isContainer}}){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{^isContainer}}{{#allowableValues}}, allowableValues = "{{{.}}}"{{/allowableValues}}{{/isContainer}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}){{/swagger1AnnotationLibrary}} {{#useBeanValidation}}@Valid{{/useBeanValidation}} @JsonOrFormBind {{{paramName}}}: {{^reactive}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isArray}}{{>optionalDataType}}{{/isArray}}{{#isArray}}Flow<{{{baseType}}}>{{/isArray}}{{/reactive}}{{/isBodyParam}} diff --git a/templates/dataClass.mustache b/templates/dataClass.mustache new file mode 100644 index 00000000..0fb78397 --- /dev/null +++ b/templates/dataClass.mustache @@ -0,0 +1,33 @@ +/** +* {{{description}}} +{{#vars}} + * @param {{name}} {{{description}}} +{{/vars}} +*/{{#discriminator}} + {{>typeInfoAnnotation}}{{/discriminator}} +{{#discriminator}}interface {{classname}}{{/discriminator}}{{^discriminator}}{{#hasVars}}data {{/hasVars}}class {{classname}}( +{{#requiredVars}} + {{>dataClassReqVar}}{{^-last}}, + {{/-last}}{{/requiredVars}}{{#hasRequired}}{{#hasOptional}}, +{{/hasOptional}}{{/hasRequired}}{{#optionalVars}}{{>dataClassOptVar}}{{^-last}}, +{{/-last}}{{/optionalVars}} +) {{/discriminator}}{{#parent}}: {{{.}}}{{/parent}}{ +{{#discriminator}} + {{#requiredVars}} + {{>interfaceReqVar}} + {{/requiredVars}} + {{#optionalVars}} + {{>interfaceOptVar}} + {{/optionalVars}} +{{/discriminator}} +{{#hasEnums}}{{#vars}}{{#isEnum}} + /** + * {{{description}}} + * Values: {{#allowableValues}}{{#enumVars}}{{&name}}{{^-last}},{{/-last}}{{/enumVars}}{{/allowableValues}} + */ + enum class {{{nameInCamelCase}}}(val value: {{#isContainer}}{{#items}}{{{dataType}}}{{/items}}{{/isContainer}}{{^isContainer}}{{{dataType}}}{{/isContainer}}) { + {{#allowableValues}}{{#enumVars}} + @JsonProperty({{{value}}}) {{{name}}}({{{value}}}){{^-last}},{{/-last}}{{/enumVars}}{{/allowableValues}} + } +{{/isEnum}}{{/vars}}{{/hasEnums}} +} diff --git a/templates/dataClassOptVar.mustache b/templates/dataClassOptVar.mustache new file mode 100644 index 00000000..3ba523bb --- /dev/null +++ b/templates/dataClassOptVar.mustache @@ -0,0 +1,5 @@ +{{#useBeanValidation}}{{>beanValidation}}{{>beanValidationModel}}{{/useBeanValidation}}{{#swagger2AnnotationLibrary}} + @Schema({{#example}}example = "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeDoubleQuote}}{{{.}}}{{/lambdaEscapeDoubleQuote}}{{/lambdaRemoveLineBreak}}", {{/example}}{{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}description = "{{{description}}}"){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}} + @ApiModelProperty({{#example}}example = "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeDoubleQuote}}{{{.}}}{{/lambdaEscapeDoubleQuote}}{{/lambdaRemoveLineBreak}}", {{/example}}{{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}value = "{{{description}}}"){{/swagger1AnnotationLibrary}}{{#deprecated}} + @Deprecated(message = ""){{/deprecated}} +@get:JsonProperty("{{{baseName}}}"){{#isInherited}} override{{/isInherited}} {{>modelMutable}} {{{name}}}: {{#isEnum}}{{#isArray}}{{baseType}}<{{/isArray}}{{classname}}.{{{nameInCamelCase}}}{{#isArray}}>{{/isArray}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}? = {{{defaultValue}}}{{^defaultValue}}null{{/defaultValue}} diff --git a/templates/dataClassReqVar.mustache b/templates/dataClassReqVar.mustache new file mode 100644 index 00000000..1812c1cf --- /dev/null +++ b/templates/dataClassReqVar.mustache @@ -0,0 +1,4 @@ +{{#useBeanValidation}}{{>beanValidation}}{{>beanValidationModel}}{{/useBeanValidation}}{{#swagger2AnnotationLibrary}} + @Schema({{#example}}example = "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeDoubleQuote}}{{{.}}}{{/lambdaEscapeDoubleQuote}}{{/lambdaRemoveLineBreak}}", {{/example}}required = true, {{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}description = "{{{description}}}"){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}} + @ApiModelProperty({{#example}}example = "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeDoubleQuote}}{{{.}}}{{/lambdaEscapeDoubleQuote}}{{/lambdaRemoveLineBreak}}", {{/example}}required = true, {{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}value = "{{{description}}}"){{/swagger1AnnotationLibrary}} +@get:JsonProperty("{{{baseName}}}", required = true){{#isInherited}} override{{/isInherited}} {{>modelMutable}} {{{name}}}: {{#isEnum}}{{#isArray}}{{baseType}}<{{/isArray}}{{classname}}.{{{nameInCamelCase}}}{{#isArray}}>{{/isArray}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{#isNullable}}?{{/isNullable}}{{#defaultValue}} = {{{.}}}{{/defaultValue}} diff --git a/templates/enumClass.mustache b/templates/enumClass.mustache new file mode 100644 index 00000000..57287535 --- /dev/null +++ b/templates/enumClass.mustache @@ -0,0 +1,8 @@ +/** +* {{{description}}} +* Values: {{#allowableValues}}{{#enumVars}}{{&name}}{{^-last}},{{/-last}}{{/enumVars}}{{/allowableValues}} +*/ +enum class {{classname}}(val value: {{dataType}}) { +{{#allowableValues}}{{#enumVars}} + @JsonProperty({{{value}}}) {{&name}}({{{value}}}){{^-last}},{{/-last}}{{/enumVars}}{{/allowableValues}} +} diff --git a/templates/exceptions.mustache b/templates/exceptions.mustache new file mode 100644 index 00000000..5a2a7aec --- /dev/null +++ b/templates/exceptions.mustache @@ -0,0 +1,29 @@ +package {{apiPackage}} + +import org.springframework.http.HttpStatus +import org.springframework.web.bind.annotation.ControllerAdvice +import org.springframework.web.bind.annotation.ExceptionHandler +import {{javaxPackage}}.servlet.http.HttpServletResponse +import {{javaxPackage}}.validation.ConstraintViolationException + +// TODO Extend ApiException for custom exception handling, e.g. the below NotFound exception +sealed class ApiException(msg: String, val code: Int) : Exception(msg) + +class NotFoundException(msg: String, code: Int = HttpStatus.NOT_FOUND.value()) : ApiException(msg, code) + + +@ControllerAdvice +class DefaultExceptionHandler { + +@ExceptionHandler(value = [ApiException::class]) +fun onApiException(ex: ApiException, response: HttpServletResponse): Unit = +response.sendError(ex.code, ex.message) + +@ExceptionHandler(value = [NotImplementedError::class]) +fun onNotImplemented(ex: NotImplementedError, response: HttpServletResponse): Unit = +response.sendError(HttpStatus.NOT_IMPLEMENTED.value()) + +@ExceptionHandler(value = [ConstraintViolationException::class]) +fun onConstraintViolation(ex: ConstraintViolationException, response: HttpServletResponse): Unit = +response.sendError(HttpStatus.BAD_REQUEST.value(), ex.constraintViolations.joinToString(", ") { it.message }) +} diff --git a/templates/formParams.mustache b/templates/formParams.mustache new file mode 100644 index 00000000..ec72b53b --- /dev/null +++ b/templates/formParams.mustache @@ -0,0 +1 @@ +{{#isFormParam}}{{^isFile}}{{#swagger2AnnotationLibrary}}@Parameter(description = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}{{#defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]{{^isContainer}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/isContainer}}){{/defaultValue}}{{/allowableValues}}{{#allowableValues}}{{^defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]){{/defaultValue}}{{/allowableValues}}{{^allowableValues}}{{#defaultValue}}{{^isContainer}}, schema = Schema(defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}){{/isContainer}}{{/defaultValue}}{{/allowableValues}}){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}, allowableValues = "{{#values}}{{{.}}}{{^-last}}, {{/-last}}{{/values}}"{{/allowableValues}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}){{/swagger1AnnotationLibrary}} @RequestParam(value = "{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}) {{paramName}}: {{>optionalDataType}} {{/isFile}}{{#isFile}}{{#swagger2AnnotationLibrary}}@Parameter(description = "file detail"){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "file detail"){{/swagger1AnnotationLibrary}} {{#useBeanValidation}}@Valid{{/useBeanValidation}} @RequestPart("file") {{baseName}}: {{>optionalDataType}}{{/isFile}}{{/isFormParam}} \ No newline at end of file diff --git a/templates/generatedAnnotation.mustache b/templates/generatedAnnotation.mustache new file mode 100644 index 00000000..1be8e755 --- /dev/null +++ b/templates/generatedAnnotation.mustache @@ -0,0 +1 @@ +@{{javaxPackage}}.annotation.Generated(value = ["{{generatorClass}}"]{{^hideGenerationTimestamp}}, date = "{{generatedDate}}"{{/hideGenerationTimestamp}}) \ No newline at end of file diff --git a/templates/headerParams.mustache b/templates/headerParams.mustache new file mode 100644 index 00000000..9bc3f600 --- /dev/null +++ b/templates/headerParams.mustache @@ -0,0 +1 @@ +{{#isHeaderParam}}{{#useBeanValidation}}{{>beanValidationPath}}{{/useBeanValidation}}{{#swagger2AnnotationLibrary}}@Parameter(description = "{{{description}}}", `in` = ParameterIn.HEADER{{#required}}, required = true{{/required}}{{#allowableValues}}{{#defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]{{^isContainer}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/isContainer}}){{/defaultValue}}{{/allowableValues}}{{#allowableValues}}{{^defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]){{/defaultValue}}{{/allowableValues}}{{^allowableValues}}{{#defaultValue}}{{^isContainer}}, schema = Schema(defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}){{/isContainer}}{{/defaultValue}}{{/allowableValues}}){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}, allowableValues = "{{#values}}{{{.}}}{{^-last}}, {{/-last}}{{/values}}"{{/allowableValues}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}){{/swagger1AnnotationLibrary}} @RequestHeader(value = "{{baseName}}", required = {{#required}}true{{/required}}{{^required}}false{{/required}}) {{paramName}}: {{>optionalDataType}}{{/isHeaderParam}} \ No newline at end of file diff --git a/templates/homeController.mustache b/templates/homeController.mustache new file mode 100644 index 00000000..b9ab27dd --- /dev/null +++ b/templates/homeController.mustache @@ -0,0 +1,91 @@ +package {{basePackage}} + +import org.springframework.context.annotation.Bean +import org.springframework.stereotype.Controller +import org.springframework.web.bind.annotation.RequestMapping +{{#sourceDocumentationProvider}} + import com.fasterxml.jackson.dataformat.yaml.YAMLMapper + import org.springframework.beans.factory.annotation.Value + import org.springframework.core.io.Resource + import org.springframework.util.StreamUtils + import org.springframework.web.bind.annotation.ResponseBody + import org.springframework.web.bind.annotation.GetMapping +{{/sourceDocumentationProvider}} +{{^sourceDocumentationProvider}} + {{#useSwaggerUI}} + import org.springframework.web.bind.annotation.ResponseBody + import org.springframework.web.bind.annotation.GetMapping + {{/useSwaggerUI}} +{{/sourceDocumentationProvider}} +{{#reactive}} + import org.springframework.web.reactive.function.server.HandlerFunction + import org.springframework.web.reactive.function.server.RequestPredicates.GET + import org.springframework.web.reactive.function.server.RouterFunction + import org.springframework.web.reactive.function.server.RouterFunctions.route + import org.springframework.web.reactive.function.server.ServerResponse + import java.net.URI +{{/reactive}} +{{#sourceDocumentationProvider}} + import java.nio.charset.Charset +{{/sourceDocumentationProvider}} + +/** +* Home redirection to OpenAPI api documentation +*/ +@Controller +class HomeController { +{{#useSwaggerUI}} + {{^springDocDocumentationProvider}} + {{#sourceDocumentationProvider}} + private val apiDocsPath = "/openapi.json" + {{/sourceDocumentationProvider}} + {{#springFoxDocumentationProvider}} + private val apiDocsPath = "/v2/api-docs" + {{/springFoxDocumentationProvider}} + {{/springDocDocumentationProvider}} +{{/useSwaggerUI}} +{{#sourceDocumentationProvider}} + private val yamlMapper = YAMLMapper() + + @Value("classpath:/openapi.yaml") + private lateinit var openapi: Resource + + @Bean + fun openapiContent(): String { + return openapi.inputStream.use { + StreamUtils.copyToString(it, Charset.defaultCharset()) + } + } + + @GetMapping(value = ["/openapi.yaml"], produces = ["application/vnd.oai.openapi"]) + @ResponseBody + fun openapiYaml(): String = openapiContent() + + @GetMapping(value = ["/openapi.json"], produces = ["application/json"]) + @ResponseBody + fun openapiJson(): Any = yamlMapper.readValue(openapiContent(), Any::class.java) +{{/sourceDocumentationProvider}} +{{#useSwaggerUI}} + {{^springDocDocumentationProvider}} + + @GetMapping(value = ["/swagger-config.yaml"], produces = ["text/plain"]) + @ResponseBody + fun swaggerConfig(): String = "url: $apiDocsPath\n" + {{/springDocDocumentationProvider}} + {{#reactive}} + + @Bean + fun index(): RouterFunction + = route( + GET("/"), HandlerFunction + { + ServerResponse.temporaryRedirect(URI.create("swagger-ui.html")).build() + }) + {{/reactive}} + {{^reactive}} + + @RequestMapping("/") + fun index(): String = "redirect:swagger-ui.html" + {{/reactive}} +{{/useSwaggerUI}} + } diff --git a/templates/interfaceOptVar.mustache b/templates/interfaceOptVar.mustache new file mode 100644 index 00000000..158ad759 --- /dev/null +++ b/templates/interfaceOptVar.mustache @@ -0,0 +1,4 @@ +{{#swagger2AnnotationLibrary}} + @get:Schema({{#example}}example = "{{{.}}}", {{/example}}{{#required}}requiredMode = Schema.RequiredMode.REQUIRED, {{/required}}{{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}description = "{{{description}}}"){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}} + @get:ApiModelProperty({{#example}}example = "{{{.}}}", {{/example}}{{#required}}required = {{required}}, {{/required}}{{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}value = "{{{description}}}"){{/swagger1AnnotationLibrary}} +{{>modelMutable}} {{{name}}}: {{#isEnum}}{{classname}}.{{nameInCamelCase}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}? {{^discriminator}}= {{{defaultValue}}}{{^defaultValue}}null{{/defaultValue}}{{/discriminator}} diff --git a/templates/interfaceReqVar.mustache b/templates/interfaceReqVar.mustache new file mode 100644 index 00000000..eeeda60d --- /dev/null +++ b/templates/interfaceReqVar.mustache @@ -0,0 +1,4 @@ +{{#swagger2AnnotationLibrary}} + @get:Schema({{#example}}example = "{{{.}}}", {{/example}}{{#required}}requiredMode = Schema.RequiredMode.REQUIRED, {{/required}}{{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}description = "{{{description}}}"){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}} + @get:ApiModelProperty({{#example}}example = "{{{.}}}", {{/example}}{{#required}}required = {{required}}, {{/required}}{{#isReadOnly}}readOnly = {{{isReadOnly}}}, {{/isReadOnly}}value = "{{{description}}}"){{/swagger1AnnotationLibrary}} +{{>modelMutable}} {{{name}}}: {{#isEnum}}{{classname}}.{{nameInCamelCase}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}} diff --git a/templates/libraries/spring-boot/README.mustache b/templates/libraries/spring-boot/README.mustache new file mode 100644 index 00000000..c75176be --- /dev/null +++ b/templates/libraries/spring-boot/README.mustache @@ -0,0 +1,21 @@ +# {{title}}{{^title}}Generated Kotlin Spring Boot App{{/title}} + +This Kotlin based [Spring Boot](https://spring.io/projects/spring-boot) application has been generated using the [OpenAPI Generator](https://github.com/OpenAPITools/openapi-generator). + +## Getting Started + +This document assumes you have either maven or gradle available, either via the wrapper or otherwise. This does not come with a gradle / maven wrapper checked in. + +By default a [`pom.xml`](pom.xml) file will be generated. If you specified `gradleBuildFile=true` when generating this project, a `build.gradle.kts` will also be generated. Note this uses [Gradle Kotlin DSL](https://github.com/gradle/kotlin-dsl). + +To build the project using maven, run: +```bash +mvn package && java -jar target/{{artifactId}}-{{artifactVersion}}.jar +``` + +To build the project using gradle, run: +```bash +gradle build && java -jar build/libs/{{artifactId}}-{{artifactVersion}}.jar +``` + +If all builds successfully, the server should run on [http://localhost:8080/](http://localhost:{{serverPort}}/) diff --git a/templates/libraries/spring-boot/application.mustache b/templates/libraries/spring-boot/application.mustache new file mode 100644 index 00000000..9f752949 --- /dev/null +++ b/templates/libraries/spring-boot/application.mustache @@ -0,0 +1,10 @@ +spring: +application: +name: {{title}} + +jackson: +serialization: +WRITE_DATES_AS_TIMESTAMPS: false + +server: +port: {{serverPort}} diff --git a/templates/libraries/spring-boot/buildGradle-sb3-Kts.mustache b/templates/libraries/spring-boot/buildGradle-sb3-Kts.mustache new file mode 100644 index 00000000..adc4d735 --- /dev/null +++ b/templates/libraries/spring-boot/buildGradle-sb3-Kts.mustache @@ -0,0 +1,61 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +group = "{{groupId}}" +version = "{{artifactVersion}}" +java.sourceCompatibility = JavaVersion.VERSION_17 + +repositories { +mavenCentral() +maven { url = uri("https://repo.spring.io/milestone") } +} + +tasks.withType + { + kotlinOptions.jvmTarget = "17" + } + + plugins { + val kotlinVersion = "1.7.10" + id("org.jetbrains.kotlin.jvm") version kotlinVersion + id("org.jetbrains.kotlin.plugin.jpa") version kotlinVersion + id("org.jetbrains.kotlin.plugin.spring") version kotlinVersion + id("org.springframework.boot") version "3.0.2" + id("io.spring.dependency-management") version "1.0.14.RELEASE" + } + + dependencies { + {{#reactive}} val kotlinxCoroutinesVersion = "1.6.1" + {{/reactive}} implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + implementation("org.jetbrains.kotlin:kotlin-reflect"){{^reactive}} + implementation("org.springframework.boot:spring-boot-starter-web"){{/reactive}}{{#reactive}} + implementation("org.springframework.boot:spring-boot-starter-webflux") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:$kotlinxCoroutinesVersion"){{/reactive}}{{#springDocDocumentationProvider}}{{#useSwaggerUI}} + implementation("org.springdoc:springdoc-openapi-starter-{{#reactive}} + webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}}-ui:2.0.0-M5"){{/useSwaggerUI}}{{^useSwaggerUI}} + implementation("org.springdoc:springdoc-openapi-{{#reactive}} + webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}} + -core:2.0.0-M5"){{/useSwaggerUI}}{{/springDocDocumentationProvider}}{{#springFoxDocumentationProvider}} + implementation("io.springfox:springfox-swagger2:2.9.2"){{/springFoxDocumentationProvider}}{{#useSwaggerUI}}{{^springDocDocumentationProvider}} + implementation("org.webjars:swagger-ui:4.10.3") + implementation("org.webjars:webjars-locator-core"){{/springDocDocumentationProvider}}{{/useSwaggerUI}}{{^springFoxDocumentationProvider}}{{^springDocDocumentationProvider}}{{#swagger1AnnotationLibrary}} + implementation("io.swagger:swagger-annotations:1.6.6"){{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}} + implementation("io.swagger.core.v3:swagger-annotations:2.2.0"){{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}{{/springFoxDocumentationProvider}} + + implementation("com.google.code.findbugs:jsr305:3.0.2") + implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml") + implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-xml") + implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + {{#useBeanValidation}} + implementation("jakarta.validation:jakarta.validation-api"){{/useBeanValidation}} + implementation("jakarta.annotation:jakarta.annotation-api:2.1.0") + + testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") + testImplementation("org.springframework.boot:spring-boot-starter-test") { + exclude(module = "junit") + } + {{#reactive}} + testImplementation`("org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinxCoroutinesVersion") + {{/reactive}} + } diff --git a/templates/libraries/spring-boot/buildGradleKts.mustache b/templates/libraries/spring-boot/buildGradleKts.mustache new file mode 100644 index 00000000..3459ef3d --- /dev/null +++ b/templates/libraries/spring-boot/buildGradleKts.mustache @@ -0,0 +1,68 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +buildscript { +repositories { +mavenCentral() +} +dependencies { +classpath("org.springframework.boot:spring-boot-gradle-plugin:2.6.7") +} +} + +group = "{{groupId}}" +version = "{{artifactVersion}}" + +repositories { +mavenCentral() +} + +tasks.withType + { + kotlinOptions.jvmTarget = "1.8" + } + + plugins { + val kotlinVersion = "1.6.21" + id("org.jetbrains.kotlin.jvm") version kotlinVersion + id("org.jetbrains.kotlin.plugin.jpa") version kotlinVersion + id("org.jetbrains.kotlin.plugin.spring") version kotlinVersion + id("org.springframework.boot") version "2.6.7" + id("io.spring.dependency-management") version "1.0.11.RELEASE" + } + + dependencies { + {{#reactive}} val kotlinxCoroutinesVersion = "1.6.1" + {{/reactive}} compile("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + compile("org.jetbrains.kotlin:kotlin-reflect"){{^reactive}} + compile("org.springframework.boot:spring-boot-starter-web"){{/reactive}}{{#reactive}} + compile("org.springframework.boot:spring-boot-starter-webflux") + compile("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion") + compile("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:$kotlinxCoroutinesVersion"){{/reactive}}{{#springDocDocumentationProvider}}{{#useSwaggerUI}} + compile("org.springdoc:springdoc-openapi-{{#reactive}} + webflux-{{/reactive}}ui:1.6.8"){{/useSwaggerUI}}{{^useSwaggerUI}} + compile("org.springdoc:springdoc-openapi-{{#reactive}} + webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}} + -core:1.6.8"){{/useSwaggerUI}}{{/springDocDocumentationProvider}}{{#springFoxDocumentationProvider}} + compile("io.springfox:springfox-swagger2:2.9.2"){{/springFoxDocumentationProvider}}{{#useSwaggerUI}}{{^springDocDocumentationProvider}} + compile("org.webjars:swagger-ui:4.10.3") + compile("org.webjars:webjars-locator-core"){{/springDocDocumentationProvider}}{{/useSwaggerUI}}{{^springFoxDocumentationProvider}}{{^springDocDocumentationProvider}}{{#swagger1AnnotationLibrary}} + compile("io.swagger:swagger-annotations:1.6.6"){{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}} + compile("io.swagger.core.v3:swagger-annotations:2.2.0"){{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}{{/springFoxDocumentationProvider}} + + compile("com.google.code.findbugs:jsr305:3.0.2") + compile("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml") + compile("com.fasterxml.jackson.dataformat:jackson-dataformat-xml") + compile("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") + compile("com.fasterxml.jackson.module:jackson-module-kotlin") + {{#useBeanValidation}} + compile("jakarta.validation:jakarta.validation-api"){{/useBeanValidation}} + compile("jakarta.annotation:jakarta.annotation-api:2.1.0") + + testCompile("org.jetbrains.kotlin:kotlin-test-junit5") + testCompile("org.springframework.boot:spring-boot-starter-test") { + exclude(module = "junit") + } + {{#reactive}} + testCompile("org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinxCoroutinesVersion") + {{/reactive}} + } diff --git a/templates/libraries/spring-boot/defaultBasePath.mustache b/templates/libraries/spring-boot/defaultBasePath.mustache new file mode 100644 index 00000000..3c7185bd --- /dev/null +++ b/templates/libraries/spring-boot/defaultBasePath.mustache @@ -0,0 +1 @@ +{{contextPath}} \ No newline at end of file diff --git a/templates/libraries/spring-boot/pom-sb3.mustache b/templates/libraries/spring-boot/pom-sb3.mustache new file mode 100644 index 00000000..5fa48b58 --- /dev/null +++ b/templates/libraries/spring-boot/pom-sb3.mustache @@ -0,0 +1,210 @@ + + 4.0.0 + {{groupId}} + {{artifactId}} + jar + {{artifactId}} + {{artifactVersion}} + {{#reactive}} + 1.6.1 + {{/reactive}}{{#springDocDocumentationProvider}}{{#useSwaggerUI}} + 2.0.2 + {{/useSwaggerUI}}{{/springDocDocumentationProvider}}{{#springFoxDocumentationProvider}} + 2.9.2 + {{/springFoxDocumentationProvider}}{{#useSwaggerUI}}{{^springDocDocumentationProvider}} + 4.15.5 + {{/springDocDocumentationProvider}}{{/useSwaggerUI}}{{^springFoxDocumentationProvider}}{{^springDocDocumentationProvider}}{{#swagger1AnnotationLibrary}} + 1.6.6 + {{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}} + 2.2.7 + {{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}{{/springFoxDocumentationProvider}} + 3.0.2 + 2.1.0 + 1.7.10 + + 1.7.10 + UTF-8 + + + org.springframework.boot + spring-boot-starter-parent + 3.0.2 + + + + repository.spring.milestone + Spring Milestone Repository + https://repo.spring.io/milestone + + + + + spring-milestones + https://repo.spring.io/milestone + + + + ${project.basedir}/src/main/kotlin + ${project.basedir}/src/test/kotlin + {{^interfaceOnly}} + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + {{/interfaceOnly}} + + kotlin-maven-plugin + org.jetbrains.kotlin + ${kotlin.version} + + + spring + + 17 + + + + compile + compile + + compile + + + + test-compile + test-compile + + test-compile + + + + + + org.jetbrains.kotlin + kotlin-maven-allopen + ${kotlin.version} + + + + + + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-reflect + ${kotlin.version} + {{^reactive}} + + org.springframework.boot + spring-boot-starter-web + {{/reactive}}{{#reactive}} + + org.springframework.boot + spring-boot-starter-webflux + + + org.jetbrains.kotlinx + kotlinx-coroutines-core + ${kotlinx-coroutines.version} + + + org.jetbrains.kotlinx + kotlinx-coroutines-reactor + ${kotlinx-coroutines.version} + {{/reactive}} + + {{#springDocDocumentationProvider}} + {{#useSwaggerUI}} + + org.springdoc + springdoc-openapi-starter-{{#reactive}} + webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}}-ui + + ${springdoc-openapi.version} + {{/useSwaggerUI}}{{^useSwaggerUI}} + + org.springdoc + springdoc-openapi-{{#reactive}}webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}}-core + + ${springdoc-openapi.version} + {{/useSwaggerUI}}{{/springDocDocumentationProvider}}{{#springFoxDocumentationProvider}} + + + io.springfox + springfox-swagger2 + ${springfox-swagger2.version} + {{/springFoxDocumentationProvider}}{{#useSwaggerUI}}{{^springDocDocumentationProvider}} + + org.webjars + swagger-ui + ${swagger-ui.version} + + + org.webjars + webjars-locator-core + {{/springDocDocumentationProvider}}{{/useSwaggerUI}}{{^springFoxDocumentationProvider}}{{^springDocDocumentationProvider}}{{#swagger1AnnotationLibrary}} + + io.swagger + swagger-annotations + ${swagger-annotations.version} + {{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}} + + io.swagger.core.v3 + swagger-annotations + ${swagger-annotations.version} + {{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}{{/springFoxDocumentationProvider}} + + + + com.google.code.findbugs + jsr305 + ${findbugs-jsr305.version} + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + com.fasterxml.jackson.module + jackson-module-kotlin + + {{#useBeanValidation}} + + + jakarta.validation + jakarta.validation-api + {{/useBeanValidation}} + + jakarta.annotation + jakarta.annotation-api + ${jakarta-annotation.version} + provided + + + org.jetbrains.kotlin + kotlin-test-junit5 + ${kotlin-test-junit5.version} + test + + + diff --git a/templates/libraries/spring-boot/pom.mustache b/templates/libraries/spring-boot/pom.mustache new file mode 100644 index 00000000..967ff19b --- /dev/null +++ b/templates/libraries/spring-boot/pom.mustache @@ -0,0 +1,195 @@ + + 4.0.0 + {{groupId}} + {{artifactId}} + jar + {{artifactId}} + {{artifactVersion}} + {{#reactive}} + 1.6.1 + {{/reactive}}{{#springDocDocumentationProvider}}{{#useSwaggerUI}} + 1.6.8 + {{/useSwaggerUI}}{{/springDocDocumentationProvider}}{{#springFoxDocumentationProvider}} + 2.9.2 + {{/springFoxDocumentationProvider}}{{#useSwaggerUI}}{{^springDocDocumentationProvider}} + 4.10.3 + {{/springDocDocumentationProvider}}{{/useSwaggerUI}}{{^springFoxDocumentationProvider}}{{^springDocDocumentationProvider}}{{#swagger1AnnotationLibrary}} + 1.6.6 + {{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}} + 2.2.0 + {{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}{{/springFoxDocumentationProvider}} + 3.0.2 + 2.1.0 + 1.6.21 + + 1.6.21 + UTF-8 + + + org.springframework.boot + spring-boot-starter-parent + 2.6.7 + + + ${project.basedir}/src/main/kotlin + ${project.basedir}/src/test/kotlin + {{^interfaceOnly}} + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + {{/interfaceOnly}} + + kotlin-maven-plugin + org.jetbrains.kotlin + ${kotlin.version} + + + spring + + 1.8 + + + + compile + compile + + compile + + + + test-compile + test-compile + + test-compile + + + + + + org.jetbrains.kotlin + kotlin-maven-allopen + ${kotlin.version} + + + + + + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-reflect + ${kotlin.version} + {{^reactive}} + + org.springframework.boot + spring-boot-starter-web + {{/reactive}}{{#reactive}} + + org.springframework.boot + spring-boot-starter-webflux + + + org.jetbrains.kotlinx + kotlinx-coroutines-core + ${kotlinx-coroutines.version} + + + org.jetbrains.kotlinx + kotlinx-coroutines-reactor + ${kotlinx-coroutines.version} + {{/reactive}} + + {{#springDocDocumentationProvider}} + {{#useSwaggerUI}} + + org.springdoc + springdoc-openapi-{{#reactive}}webflux-{{/reactive}}ui + ${springdoc-openapi.version} + {{/useSwaggerUI}}{{^useSwaggerUI}} + + org.springdoc + springdoc-openapi-{{#reactive}}webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}}-core + + ${springdoc-openapi.version} + {{/useSwaggerUI}}{{/springDocDocumentationProvider}}{{#springFoxDocumentationProvider}} + + + io.springfox + springfox-swagger2 + ${springfox-swagger2.version} + {{/springFoxDocumentationProvider}}{{#useSwaggerUI}}{{^springDocDocumentationProvider}} + + org.webjars + swagger-ui + ${swagger-ui.version} + + + org.webjars + webjars-locator-core + {{/springDocDocumentationProvider}}{{/useSwaggerUI}}{{^springFoxDocumentationProvider}}{{^springDocDocumentationProvider}}{{#swagger1AnnotationLibrary}} + + io.swagger + swagger-annotations + ${swagger-annotations.version} + {{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}} + + io.swagger.core.v3 + swagger-annotations + ${swagger-annotations.version} + {{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}{{/springFoxDocumentationProvider}} + + + + com.google.code.findbugs + jsr305 + ${findbugs-jsr305.version} + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + com.fasterxml.jackson.module + jackson-module-kotlin + + {{#useBeanValidation}} + + + jakarta.validation + jakarta.validation-api + {{/useBeanValidation}} + + jakarta.annotation + jakarta.annotation-api + ${jakarta-annotation.version} + provided + + + org.jetbrains.kotlin + kotlin-test-junit5 + ${kotlin-test-junit5.version} + test + + + diff --git a/templates/libraries/spring-boot/settingsGradle.mustache b/templates/libraries/spring-boot/settingsGradle.mustache new file mode 100644 index 00000000..3176ec97 --- /dev/null +++ b/templates/libraries/spring-boot/settingsGradle.mustache @@ -0,0 +1,15 @@ +pluginManagement { +repositories { +maven { url = uri("https://repo.spring.io/snapshot") } +maven { url = uri("https://repo.spring.io/milestone") } +gradlePluginPortal() +} +resolutionStrategy { +eachPlugin { +if (requested.id.id == "org.springframework.boot") { +useModule("org.springframework.boot:spring-boot-gradle-plugin:${requested.version}") +} +} +} +} +rootProject.name = "{{artifactId}}" diff --git a/templates/libraries/spring-boot/springBootApplication.mustache b/templates/libraries/spring-boot/springBootApplication.mustache new file mode 100644 index 00000000..e86b92e0 --- /dev/null +++ b/templates/libraries/spring-boot/springBootApplication.mustache @@ -0,0 +1,15 @@ +package {{basePackage}} + +import org.springframework.boot.runApplication +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.context.annotation.ComponentScan + +@SpringBootApplication +@ComponentScan(basePackages = ["{{basePackage}}", "{{apiPackage}}", "{{modelPackage}}"]) +class Application + +fun main(args: Array +) { + runApplication + (*args) + } diff --git a/templates/libraries/spring-boot/swagger-ui.mustache b/templates/libraries/spring-boot/swagger-ui.mustache new file mode 100644 index 00000000..ce2cfd92 --- /dev/null +++ b/templates/libraries/spring-boot/swagger-ui.mustache @@ -0,0 +1,57 @@ + + + + + + Swagger UI + + + + + + + +
+ + + + + + diff --git a/templates/libraries/spring-cloud/README.mustache b/templates/libraries/spring-cloud/README.mustache new file mode 100644 index 00000000..9949253f --- /dev/null +++ b/templates/libraries/spring-cloud/README.mustache @@ -0,0 +1,83 @@ +{{^interfaceOnly}} + # {{artifactId}} + + ## Requirements + + Building the API client library requires [Maven](https://maven.apache.org/) to be installed. + + ## Installation + + To install the API client library to your local Maven repository, simply execute: + + ```shell + mvn install + ``` + + To deploy it to a remote Maven repository instead, configure the settings of the repository and execute: + + ```shell + mvn deploy + ``` + + Refer to the [official documentation](https://maven.apache.org/plugins/maven-deploy-plugin/usage.html) for more information. + + ### Maven users + + Add this dependency to your project's POM: + + ```xml + + {{{groupId}}} + {{{artifactId}}} + {{{artifactVersion}}} + compile + + ``` + + ### Gradle users + + Add this dependency to your project's build file: + + ```groovy + compile "{{{groupId}}}:{{{artifactId}}}:{{{artifactVersion}}}" + ``` + + ### Others + + At first generate the JAR by executing: + + mvn package + + Then manually install the following JARs: + + * target/{{{artifactId}}}-{{{artifactVersion}}}.jar + * target/lib/*.jar +{{/interfaceOnly}} +{{#interfaceOnly}} + # OpenAPI generated API stub + + Spring Framework stub + + + ## Overview + This code was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. + By using the [OpenAPI-Spec](https://openapis.org), you can easily generate an API stub. + This is an example of building API stub interfaces in Java using the Spring framework. + + The stubs generated can be used in your existing Spring-MVC or Spring-Boot application to create controller endpoints + by adding ```@Controller``` classes that implement the interface. Eg: + ```java + @Controller + public class PetController implements PetApi { + // implement all PetApi methods + } + ``` + + You can also use the interface to create [Spring-Cloud Feign clients](http://projects.spring.io/spring-cloud/spring-cloud.html#spring-cloud-feign-inheritance).Eg: + ```java + @FeignClient(name="pet", url="http://petstore.swagger.io/v2") + public interface PetClient extends PetApi { + + } + ``` +{{/interfaceOnly}} diff --git a/templates/libraries/spring-cloud/apiClient.mustache b/templates/libraries/spring-cloud/apiClient.mustache new file mode 100644 index 00000000..877bfbbe --- /dev/null +++ b/templates/libraries/spring-cloud/apiClient.mustache @@ -0,0 +1,11 @@ +package {{package}} + +import org.springframework.cloud.openfeign.FeignClient +import {{configPackage}}.ClientConfiguration + +@FeignClient( +name="\${{openbrace}}{{classVarName}}.name:{{classVarName}}{{closebrace}}", +{{#useFeignClientUrl}}url="\${{openbrace}}{{classVarName}}.url:{{basePath}}{{closebrace}}", {{/useFeignClientUrl}} +configuration = [ClientConfiguration::class] +) +interface {{classname}}Client : {{classname}} diff --git a/templates/libraries/spring-cloud/apiKeyRequestInterceptor.mustache b/templates/libraries/spring-cloud/apiKeyRequestInterceptor.mustache new file mode 100644 index 00000000..e0fa18d2 --- /dev/null +++ b/templates/libraries/spring-cloud/apiKeyRequestInterceptor.mustache @@ -0,0 +1,19 @@ +package {{configPackage}} + +import feign.RequestInterceptor +import feign.RequestTemplate + +class ApiKeyRequestInterceptor( +private val location: String, +private val name: String, +private val value: String, +) : RequestInterceptor { + +override fun apply(requestTemplate: RequestTemplate) { +if (location == "header") { +requestTemplate.header(name, value) +} else if (location == "query") { +requestTemplate.query(name, value) +} +} +} diff --git a/templates/libraries/spring-cloud/buildGradle-sb3-Kts.mustache b/templates/libraries/spring-cloud/buildGradle-sb3-Kts.mustache new file mode 100644 index 00000000..d1cb45c9 --- /dev/null +++ b/templates/libraries/spring-cloud/buildGradle-sb3-Kts.mustache @@ -0,0 +1,72 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +group = "{{groupId}}" +version = "{{artifactVersion}}" +java.sourceCompatibility = JavaVersion.VERSION_17 + +repositories { +mavenCentral() +maven { url = uri("https://repo.spring.io/milestone") } +} + +tasks.withType + { + kotlinOptions.jvmTarget = "17" + } + + plugins { + val kotlinVersion = "1.7.10" + id("org.jetbrains.kotlin.jvm") version kotlinVersion + id("org.jetbrains.kotlin.plugin.jpa") version kotlinVersion + id("org.jetbrains.kotlin.plugin.spring") version kotlinVersion + id("org.springframework.boot") version "3.0.2" + id("io.spring.dependency-management") version "1.0.14.RELEASE" + } + + tasks.getByName("bootJar") { + enabled = false + } + + tasks.getByName("jar") { + enabled = true + } + + dependencyManagement { + imports { + mavenBom("org.springframework.cloud:spring-cloud-dependencies:2021.0.5") + } + } + + dependencies { + {{#reactive}} val kotlinxCoroutinesVersion = "1.6.1" + {{/reactive}} implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + implementation("org.jetbrains.kotlin:kotlin-reflect"){{^reactive}} + implementation("org.springframework.boot:spring-boot-starter-web"){{/reactive}}{{#reactive}} + implementation("org.springframework.boot:spring-boot-starter-webflux") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:$kotlinxCoroutinesVersion"){{/reactive}}{{#springDocDocumentationProvider}}{{#useSwaggerUI}} + implementation("org.springdoc:springdoc-openapi-starter-{{#reactive}} + webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}}-ui:2.0.0-M5"){{/useSwaggerUI}}{{^useSwaggerUI}} + implementation("org.springdoc:springdoc-openapi-{{#reactive}} + webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}} + -core:2.0.0-M5"){{/useSwaggerUI}}{{/springDocDocumentationProvider}}{{#springFoxDocumentationProvider}} + implementation("io.springfox:springfox-swagger2:2.9.2"){{/springFoxDocumentationProvider}}{{#useSwaggerUI}}{{^springDocDocumentationProvider}} + implementation("org.webjars:swagger-ui:4.10.3") + implementation("org.webjars:webjars-locator-core"){{/springDocDocumentationProvider}}{{/useSwaggerUI}}{{^springFoxDocumentationProvider}}{{^springDocDocumentationProvider}}{{#swagger1AnnotationLibrary}} + implementation("io.swagger:swagger-annotations:1.6.6"){{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}} + implementation("io.swagger.core.v3:swagger-annotations:2.2.0"){{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}{{/springFoxDocumentationProvider}} + + implementation("com.google.code.findbugs:jsr305:3.0.2") + implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml") + implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-xml") + implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + + implementation("org.springframework.cloud:spring-cloud-starter-openfeign"){{#hasAuthMethods}} + implementation("org.springframework.cloud:spring-cloud-starter-oauth2:2.2.5.RELEASE"){{/hasAuthMethods}} + + {{#useBeanValidation}} + implementation("jakarta.validation:jakarta.validation-api"){{/useBeanValidation}} + implementation("jakarta.annotation:jakarta.annotation-api:2.1.0") + + } diff --git a/templates/libraries/spring-cloud/buildGradleKts.mustache b/templates/libraries/spring-cloud/buildGradleKts.mustache new file mode 100644 index 00000000..b944c04c --- /dev/null +++ b/templates/libraries/spring-cloud/buildGradleKts.mustache @@ -0,0 +1,79 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +buildscript { +repositories { +mavenCentral() +} +dependencies { +classpath("org.springframework.boot:spring-boot-gradle-plugin:2.6.7") +} +} + +group = "{{groupId}}" +version = "{{artifactVersion}}" + +repositories { +mavenCentral() +} + +tasks.withType + { + kotlinOptions.jvmTarget = "1.8" + } + + plugins { + val kotlinVersion = "1.6.21" + id("org.jetbrains.kotlin.jvm") version kotlinVersion + id("org.jetbrains.kotlin.plugin.jpa") version kotlinVersion + id("org.jetbrains.kotlin.plugin.spring") version kotlinVersion + id("org.springframework.boot") version "2.6.7" + id("io.spring.dependency-management") version "1.0.11.RELEASE" + } + + tasks.getByName("bootJar") { + enabled = false + } + + tasks.getByName("jar") { + enabled = true + } + + dependencyManagement { + imports { + mavenBom("org.springframework.cloud:spring-cloud-dependencies:2021.0.5") + } + } + + dependencies { + {{#reactive}} val kotlinxCoroutinesVersion = "1.6.1" + {{/reactive}} implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + implementation("org.jetbrains.kotlin:kotlin-reflect"){{^reactive}} + implementation("org.springframework.boot:spring-boot-starter-web"){{/reactive}}{{#reactive}} + implementation("org.springframework.boot:spring-boot-starter-webflux") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:$kotlinxCoroutinesVersion"){{/reactive}}{{#springDocDocumentationProvider}}{{#useSwaggerUI}} + implementation("org.springdoc:springdoc-openapi-{{#reactive}} + webflux-{{/reactive}}ui:1.6.8"){{/useSwaggerUI}}{{^useSwaggerUI}} + implementation("org.springdoc:springdoc-openapi-{{#reactive}} + webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}} + -core:1.6.8"){{/useSwaggerUI}}{{/springDocDocumentationProvider}}{{#springFoxDocumentationProvider}} + implementation("io.springfox:springfox-swagger2:2.9.2"){{/springFoxDocumentationProvider}}{{#useSwaggerUI}}{{^springDocDocumentationProvider}} + implementation("org.webjars:swagger-ui:4.10.3") + implementation("org.webjars:webjars-locator-core"){{/springDocDocumentationProvider}}{{/useSwaggerUI}}{{^springFoxDocumentationProvider}}{{^springDocDocumentationProvider}}{{#swagger1AnnotationLibrary}} + implementation("io.swagger:swagger-annotations:1.6.6"){{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}} + implementation("io.swagger.core.v3:swagger-annotations:2.2.0"){{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}{{/springFoxDocumentationProvider}} + + implementation("com.google.code.findbugs:jsr305:3.0.2") + implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml") + implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-xml") + implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + + implementation("org.springframework.cloud:spring-cloud-starter-openfeign"){{#hasAuthMethods}} + implementation("org.springframework.cloud:spring-cloud-starter-oauth2:2.2.5.RELEASE"){{/hasAuthMethods}} + + {{#useBeanValidation}} + implementation("jakarta.validation:jakarta.validation-api"){{/useBeanValidation}} + implementation("jakarta.annotation:jakarta.annotation-api:2.1.0") + + } diff --git a/templates/libraries/spring-cloud/clientConfiguration.mustache b/templates/libraries/spring-cloud/clientConfiguration.mustache new file mode 100644 index 00000000..4b9615c4 --- /dev/null +++ b/templates/libraries/spring-cloud/clientConfiguration.mustache @@ -0,0 +1,132 @@ +package {{configPackage}} + +{{#authMethods}} + {{#isBasicBasic}} + import feign.auth.BasicAuthRequestInterceptor + {{/isBasicBasic}} + {{#-first}} + import org.springframework.beans.factory.annotation.Value + import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty + {{/-first}} + {{#isOAuth}} + import org.springframework.boot.context.properties.ConfigurationProperties + {{/isOAuth}} +{{/authMethods}} +import org.springframework.boot.context.properties.EnableConfigurationProperties +{{#authMethods}} + {{#-first}} + import org.springframework.context.annotation.Bean + {{/-first}} +{{/authMethods}} +import org.springframework.context.annotation.Configuration +{{#authMethods}} + {{#isOAuth}} + import org.springframework.cloud.openfeign.security.OAuth2FeignRequestInterceptor + import org.springframework.security.oauth2.client.DefaultOAuth2ClientContext + import org.springframework.security.oauth2.client.OAuth2ClientContext + {{#isApplication}} + import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails + {{/isApplication}} + {{#isCode}} + import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails + {{/isCode}} + {{#isImplicit}} + import org.springframework.security.oauth2.client.token.grant.implicit.ImplicitResourceDetails + {{/isImplicit}} + {{#isPassword}} + import org.springframework.security.oauth2.client.token.grant.password.ResourceOwnerPasswordResourceDetails + {{/isPassword}} + {{/isOAuth}} +{{/authMethods}} + +@Configuration +@EnableConfigurationProperties +class ClientConfiguration { + +{{#authMethods}} + {{#isBasicBasic}} + @Value("\${{openbrace}}{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}.username:{{closebrace}}") + private lateinit var {{#lambda.camelcase}}{{{name}}}{{/lambda.camelcase}}Username: String + + @Value("\${{openbrace}}{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}.password:{{closebrace}}") + private lateinit var {{#lambda.camelcase}}{{{name}}}{{/lambda.camelcase}}Password: String + + @Bean + @ConditionalOnProperty("{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}.username") + fun {{{name}}}RequestInterceptor(): BasicAuthRequestInterceptor { + return BasicAuthRequestInterceptor(this.{{#lambda.camelcase}}{{{name}}}{{/lambda.camelcase}}Username, this.{{#lambda.camelcase}}{{{name}}}{{/lambda.camelcase}}Password) + } + + {{/isBasicBasic}} + {{#isApiKey}} + @Value("\${{openbrace}}{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}.key:{{closebrace}}") + private lateinit var {{#lambda.camelcase}}{{{name}}}{{/lambda.camelcase}}Key: String + + @Bean + @ConditionalOnProperty("{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}.key") + fun {{#lambda.camelcase}}{{{name}}}{{/lambda.camelcase}}RequestInterceptor(): ApiKeyRequestInterceptor { + return ApiKeyRequestInterceptor({{#isKeyInHeader}}"header"{{/isKeyInHeader}}{{^isKeyInHeader}}"query"{{/isKeyInHeader}}, "{{{keyParamName}}}", this.{{#lambda.camelcase}}{{{name}}}{{/lambda.camelcase}}Key) + } + + {{/isApiKey}} + {{#isOAuth}} + @Bean + @ConditionalOnProperty("{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}.client-id") + fun {{#lambda.camelcase}}{{{name}}}{{/lambda.camelcase}}RequestInterceptor(oAuth2ClientContext: OAuth2ClientContext): OAuth2FeignRequestInterceptor { + return OAuth2FeignRequestInterceptor(oAuth2ClientContext, {{#lambda.camelcase}}{{{name}}}{{/lambda.camelcase}}ResourceDetails()) + } + + @Bean + @ConditionalOnProperty("{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}.client-id") + fun oAuth2ClientContext(): OAuth2ClientContext { + return DefaultOAuth2ClientContext() + } + + {{#isCode}} + @Bean + @ConditionalOnProperty("{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}.client-id") + @ConfigurationProperties("{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}") + fun {{#lambda.camelcase}}{{{name}}}{{/lambda.camelcase}}ResourceDetails(): AuthorizationCodeResourceDetails { + val details = AuthorizationCodeResourceDetails() + details.accessTokenUri = "{{{tokenUrl}}}" + details.userAuthorizationUri = "{{{authorizationUrl}}}" + return details + } + + {{/isCode}} + {{#isPassword}} + @Bean + @ConditionalOnProperty("{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}.client-id") + @ConfigurationProperties("{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}") + fun {{#lambda.camelcase}}{{{name}}}{{/lambda.camelcase}}ResourceDetails(): ResourceOwnerPasswordResourceDetails { + val details = ResourceOwnerPasswordResourceDetails() + details.accessTokenUri = "{{{tokenUrl}}}" + return details + } + + {{/isPassword}} + {{#isApplication}} + @Bean + @ConditionalOnProperty("{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}.client-id") + @ConfigurationProperties("{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}") + fun {{#lambda.camelcase}}{{{name}}}{{/lambda.camelcase}}ResourceDetails(): ClientCredentialsResourceDetails { + val details = ClientCredentialsResourceDetails() + details.accessTokenUri = "{{{tokenUrl}}}" + return details + } + + {{/isApplication}} + {{#isImplicit}} + @Bean + @ConditionalOnProperty("{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}.client-id") + @ConfigurationProperties("{{#lambda.lowercase}}{{{title}}}{{/lambda.lowercase}}.security.{{{name}}}") + fun {{#lambda.camelcase}}{{{name}}}{{/lambda.camelcase}}ResourceDetails(): ImplicitResourceDetails { + val details = ImplicitResourceDetails() + details.userAuthorizationUri= "{{{authorizationUrl}}}" + return details + } + + {{/isImplicit}} + {{/isOAuth}} +{{/authMethods}} +} diff --git a/templates/libraries/spring-cloud/pom-sb3.mustache b/templates/libraries/spring-cloud/pom-sb3.mustache new file mode 100644 index 00000000..4dfd1558 --- /dev/null +++ b/templates/libraries/spring-cloud/pom-sb3.mustache @@ -0,0 +1,233 @@ + + 4.0.0 + {{groupId}} + {{artifactId}} + jar + {{artifactId}} + {{artifactVersion}} + {{#reactive}} + 1.6.1 + {{/reactive}}{{#springDocDocumentationProvider}}{{#useSwaggerUI}} + 2.0.2 + {{/useSwaggerUI}}{{/springDocDocumentationProvider}}{{#springFoxDocumentationProvider}} + 2.9.2 + {{/springFoxDocumentationProvider}}{{#useSwaggerUI}}{{^springDocDocumentationProvider}} + 4.15.5 + {{/springDocDocumentationProvider}}{{/useSwaggerUI}}{{^springFoxDocumentationProvider}}{{^springDocDocumentationProvider}}{{#swagger1AnnotationLibrary}} + 1.6.6 + {{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}} + 2.2.7 + {{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}{{/springFoxDocumentationProvider}} + 3.0.2 + 2.1.0 + 1.7.10 + + 1.7.10 + UTF-8 + + + org.springframework.boot + spring-boot-starter-parent + 3.0.2 + + + + + org.springframework.cloud + spring-cloud-starter-parent + 2021.0.5 + pom + import + + + + + + repository.spring.milestone + Spring Milestone Repository + https://repo.spring.io/milestone + + + + + spring-milestones + https://repo.spring.io/milestone + + + + ${project.basedir}/src/main/kotlin + {{^interfaceOnly}} + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + {{/interfaceOnly}} + + kotlin-maven-plugin + org.jetbrains.kotlin + ${kotlin.version} + + + spring + + 17 + + + + compile + compile + + compile + + + + test-compile + test-compile + + test-compile + + + + + + org.jetbrains.kotlin + kotlin-maven-allopen + ${kotlin.version} + + + + + + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-reflect + ${kotlin.version} + {{^reactive}} + + org.springframework.boot + spring-boot-starter-web + {{/reactive}}{{#reactive}} + + org.springframework.boot + spring-boot-starter-webflux + + + org.jetbrains.kotlinx + kotlinx-coroutines-core + ${kotlinx-coroutines.version} + + + org.jetbrains.kotlinx + kotlinx-coroutines-reactor + ${kotlinx-coroutines.version} + {{/reactive}} + + {{#springDocDocumentationProvider}} + {{#useSwaggerUI}} + + org.springdoc + springdoc-openapi-starter-{{#reactive}} + webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}}-ui + + ${springdoc-openapi.version} + {{/useSwaggerUI}}{{^useSwaggerUI}} + + org.springdoc + springdoc-openapi-{{#reactive}}webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}}-core + + ${springdoc-openapi.version} + {{/useSwaggerUI}}{{/springDocDocumentationProvider}}{{#springFoxDocumentationProvider}} + + + io.springfox + springfox-swagger2 + ${springfox-swagger2.version} + {{/springFoxDocumentationProvider}}{{#useSwaggerUI}}{{^springDocDocumentationProvider}} + + org.webjars + swagger-ui + ${swagger-ui.version} + + + org.webjars + webjars-locator-core + {{/springDocDocumentationProvider}}{{/useSwaggerUI}}{{^springFoxDocumentationProvider}}{{^springDocDocumentationProvider}}{{#swagger1AnnotationLibrary}} + + io.swagger + swagger-annotations + ${swagger-annotations.version} + {{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}} + + io.swagger.core.v3 + swagger-annotations + ${swagger-annotations.version} + {{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}{{/springFoxDocumentationProvider}} + + + + com.google.code.findbugs + jsr305 + ${findbugs-jsr305.version} + + + org.springframework.cloud + spring-cloud-starter-openfeign + + {{#hasAuthMethods}} + + org.springframework.cloud + spring-cloud-starter-oauth2 + {{^parentOverridden}} + 2.2.5.RELEASE + {{/parentOverridden}} + + {{/hasAuthMethods}} + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + com.fasterxml.jackson.module + jackson-module-kotlin + + {{#useBeanValidation}} + + + jakarta.validation + jakarta.validation-api + {{/useBeanValidation}} + + jakarta.annotation + jakarta.annotation-api + ${jakarta-annotation.version} + provided + + + org.jetbrains.kotlin + kotlin-test-junit5 + ${kotlin-test-junit5.version} + test + + + diff --git a/templates/libraries/spring-cloud/pom.mustache b/templates/libraries/spring-cloud/pom.mustache new file mode 100644 index 00000000..867c9cf2 --- /dev/null +++ b/templates/libraries/spring-cloud/pom.mustache @@ -0,0 +1,218 @@ + + 4.0.0 + {{groupId}} + {{artifactId}} + jar + {{artifactId}} + {{artifactVersion}} + {{#reactive}} + 1.6.1 + {{/reactive}}{{#springDocDocumentationProvider}}{{#useSwaggerUI}} + 1.6.8 + {{/useSwaggerUI}}{{/springDocDocumentationProvider}}{{#springFoxDocumentationProvider}} + 2.9.2 + {{/springFoxDocumentationProvider}}{{#useSwaggerUI}}{{^springDocDocumentationProvider}} + 4.10.3 + {{/springDocDocumentationProvider}}{{/useSwaggerUI}}{{^springFoxDocumentationProvider}}{{^springDocDocumentationProvider}}{{#swagger1AnnotationLibrary}} + 1.6.6 + {{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}} + 2.2.0 + {{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}{{/springFoxDocumentationProvider}} + 3.0.2 + 2.1.0 + 1.6.21 + + 1.6.21 + UTF-8 + + + org.springframework.boot + spring-boot-starter-parent + 2.6.7 + + + + + org.springframework.cloud + spring-cloud-starter-parent + 2021.0.5 + pom + import + + + + + ${project.basedir}/src/main/kotlin + {{^interfaceOnly}} + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + {{/interfaceOnly}} + + kotlin-maven-plugin + org.jetbrains.kotlin + ${kotlin.version} + + + spring + + 1.8 + + + + compile + compile + + compile + + + + test-compile + test-compile + + test-compile + + + + + + org.jetbrains.kotlin + kotlin-maven-allopen + ${kotlin.version} + + + + + + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-reflect + ${kotlin.version} + {{^reactive}} + + org.springframework.boot + spring-boot-starter-web + {{/reactive}}{{#reactive}} + + org.springframework.boot + spring-boot-starter-webflux + + + org.jetbrains.kotlinx + kotlinx-coroutines-core + ${kotlinx-coroutines.version} + + + org.jetbrains.kotlinx + kotlinx-coroutines-reactor + ${kotlinx-coroutines.version} + {{/reactive}} + + {{#springDocDocumentationProvider}} + {{#useSwaggerUI}} + + org.springdoc + springdoc-openapi-{{#reactive}}webflux-{{/reactive}}ui + ${springdoc-openapi.version} + {{/useSwaggerUI}}{{^useSwaggerUI}} + + org.springdoc + springdoc-openapi-{{#reactive}}webflux{{/reactive}}{{^reactive}}webmvc{{/reactive}}-core + + ${springdoc-openapi.version} + {{/useSwaggerUI}}{{/springDocDocumentationProvider}}{{#springFoxDocumentationProvider}} + + + io.springfox + springfox-swagger2 + ${springfox-swagger2.version} + {{/springFoxDocumentationProvider}}{{#useSwaggerUI}}{{^springDocDocumentationProvider}} + + org.webjars + swagger-ui + ${swagger-ui.version} + + + org.webjars + webjars-locator-core + {{/springDocDocumentationProvider}}{{/useSwaggerUI}}{{^springFoxDocumentationProvider}}{{^springDocDocumentationProvider}}{{#swagger1AnnotationLibrary}} + + io.swagger + swagger-annotations + ${swagger-annotations.version} + {{/swagger1AnnotationLibrary}}{{#swagger2AnnotationLibrary}} + + io.swagger.core.v3 + swagger-annotations + ${swagger-annotations.version} + {{/swagger2AnnotationLibrary}}{{/springDocDocumentationProvider}}{{/springFoxDocumentationProvider}} + + + + com.google.code.findbugs + jsr305 + ${findbugs-jsr305.version} + + + org.springframework.cloud + spring-cloud-starter-openfeign + + {{#hasAuthMethods}} + + org.springframework.cloud + spring-cloud-starter-oauth2 + {{^parentOverridden}} + 2.2.5.RELEASE + {{/parentOverridden}} + + {{/hasAuthMethods}} + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + com.fasterxml.jackson.module + jackson-module-kotlin + + {{#useBeanValidation}} + + + jakarta.validation + jakarta.validation-api + {{/useBeanValidation}} + + jakarta.annotation + jakarta.annotation-api + ${jakarta-annotation.version} + provided + + + org.jetbrains.kotlin + kotlin-test-junit5 + ${kotlin-test-junit5.version} + test + + + diff --git a/templates/libraries/spring-cloud/settingsGradle.mustache b/templates/libraries/spring-cloud/settingsGradle.mustache new file mode 100644 index 00000000..3176ec97 --- /dev/null +++ b/templates/libraries/spring-cloud/settingsGradle.mustache @@ -0,0 +1,15 @@ +pluginManagement { +repositories { +maven { url = uri("https://repo.spring.io/snapshot") } +maven { url = uri("https://repo.spring.io/milestone") } +gradlePluginPortal() +} +resolutionStrategy { +eachPlugin { +if (requested.id.id == "org.springframework.boot") { +useModule("org.springframework.boot:spring-boot-gradle-plugin:${requested.version}") +} +} +} +} +rootProject.name = "{{artifactId}}" diff --git a/templates/methodBody.mustache b/templates/methodBody.mustache new file mode 100644 index 00000000..f7e69bcd --- /dev/null +++ b/templates/methodBody.mustache @@ -0,0 +1,28 @@ +{{^reactive}} + {{#examples}} + {{#-first}} + {{#async}} + return CompletableFuture.supplyAsync(()-> { + {{/async}}getRequest().ifPresent { request -> + {{#async}} {{/async}} for (mediaType in MediaType.parseMediaTypes(request.getHeader("Accept"))) { + {{/-first}} + {{#async}} {{/async}}{{^async}} {{/async}} if (mediaType.isCompatibleWith(MediaType.valueOf("{{{contentType}}}"))) { + {{#async}} {{/async}}{{^async}} {{/async}} ApiUtil.setExampleResponse(request, "{{{contentType}}}", "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeDoubleQuote}}{{{example}}}{{/lambdaEscapeDoubleQuote}}{{/lambdaRemoveLineBreak}}") + {{#async}} {{/async}}{{^async}} {{/async}} break + {{#async}} {{/async}}{{^async}} {{/async}} } + {{#-last}} + {{#async}} {{/async}}{{^async}} {{/async}} } + {{#async}} {{/async}} } + {{#async}} {{/async}} return ResponseEntity({{#returnSuccessCode}}HttpStatus.valueOf({{{statusCode}}}){{/returnSuccessCode}}{{^returnSuccessCode}}HttpStatus.NOT_IMPLEMENTED{{/returnSuccessCode}}) + {{#async}} + }, Runnable::run) + {{/async}} + {{/-last}} + {{/examples}} + {{^examples}} + return {{#async}}CompletableFuture.completedFuture({{/async}}ResponseEntity({{#returnSuccessCode}}HttpStatus.OK{{/returnSuccessCode}}{{^returnSuccessCode}}HttpStatus.NOT_IMPLEMENTED{{/returnSuccessCode}}) + {{/examples}} +{{/reactive}} +{{#reactive}} + return ResponseEntity({{#returnSuccessCode}}HttpStatus.OK{{/returnSuccessCode}}{{^returnSuccessCode}}HttpStatus.NOT_IMPLEMENTED{{/returnSuccessCode}}) +{{/reactive}} diff --git a/templates/model.mustache b/templates/model.mustache new file mode 100644 index 00000000..48ca8ae6 --- /dev/null +++ b/templates/model.mustache @@ -0,0 +1,28 @@ +package {{package}} + +import java.util.Objects +{{#imports}}import {{import}} +{{/imports}} +{{#useBeanValidation}} + import {{javaxPackage}}.validation.constraints.DecimalMax + import {{javaxPackage}}.validation.constraints.DecimalMin + import {{javaxPackage}}.validation.constraints.Email + import {{javaxPackage}}.validation.constraints.Max + import {{javaxPackage}}.validation.constraints.Min + import {{javaxPackage}}.validation.constraints.NotNull + import {{javaxPackage}}.validation.constraints.Pattern + import {{javaxPackage}}.validation.constraints.Size + import {{javaxPackage}}.validation.Valid +{{/useBeanValidation}} +{{#swagger2AnnotationLibrary}} + import io.swagger.v3.oas.annotations.media.Schema +{{/swagger2AnnotationLibrary}} +{{#swagger1AnnotationLibrary}} + import io.swagger.annotations.ApiModelProperty +{{/swagger1AnnotationLibrary}} + +{{#models}} + {{#model}} + {{#isEnum}}{{>enumClass}}{{/isEnum}}{{^isEnum}}{{>dataClass}}{{/isEnum}} + {{/model}} +{{/models}} diff --git a/templates/modelMutable.mustache b/templates/modelMutable.mustache new file mode 100644 index 00000000..4c7f3900 --- /dev/null +++ b/templates/modelMutable.mustache @@ -0,0 +1 @@ +{{#modelMutable}}var{{/modelMutable}}{{^modelMutable}}val{{/modelMutable}} \ No newline at end of file diff --git a/templates/openapi.mustache b/templates/openapi.mustache new file mode 100644 index 00000000..51ebafb0 --- /dev/null +++ b/templates/openapi.mustache @@ -0,0 +1 @@ +{{{openapi-yaml}}} \ No newline at end of file diff --git a/templates/optionalDataType.mustache b/templates/optionalDataType.mustache new file mode 100644 index 00000000..7c026fa8 --- /dev/null +++ b/templates/optionalDataType.mustache @@ -0,0 +1 @@ +{{#required}}{{{dataType}}}{{/required}}{{^required}}{{#defaultValue}}{{{dataType}}}{{/defaultValue}}{{^defaultValue}}{{{dataType}}}?{{/defaultValue}}{{/required}} \ No newline at end of file diff --git a/templates/pathParams.mustache b/templates/pathParams.mustache new file mode 100644 index 00000000..957ca220 --- /dev/null +++ b/templates/pathParams.mustache @@ -0,0 +1 @@ +{{#isPathParam}}{{#useBeanValidation}}{{>beanValidationPathParams}}{{/useBeanValidation}}{{#swagger2AnnotationLibrary}}@Parameter(description = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}{{#defaultValue}}, schema = Schema(allowableValues = [{{#enumVars}}"{{#lambdaEscapeDoubleQuote}}{{{value}}}{{/lambdaEscapeDoubleQuote}}"{{^-last}}, {{/-last}}{{/enumVars}}]{{^isContainer}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/isContainer}}{{/defaultValue}}{{/allowableValues}}{{#allowableValues}}{{^defaultValue}}, schema = Schema(allowableValues = [{{#enumVars}}"{{#lambdaEscapeDoubleQuote}}{{{value}}}{{/lambdaEscapeDoubleQuote}}"{{^-last}}, {{/-last}}{{/enumVars}}]){{/defaultValue}}{{/allowableValues}}{{^allowableValues}}{{#defaultValue}}{{^isContainer}}, schema = Schema(defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/isContainer}}{{/defaultValue}}{{/allowableValues}}){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}, allowableValues = "{{#enumVars}}{{#lambda.escapeDoubleQuote}}{{{value}}}{{/lambda.escapeDoubleQuote}}{{^-last}}, {{/-last}}{{/enumVars}}"{{/allowableValues}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}){{/swagger1AnnotationLibrary}} @PathVariable("{{baseName}}") {{paramName}}: {{>optionalDataType}}{{/isPathParam}} \ No newline at end of file diff --git a/templates/queryParams.mustache b/templates/queryParams.mustache new file mode 100644 index 00000000..3c254c53 --- /dev/null +++ b/templates/queryParams.mustache @@ -0,0 +1 @@ +{{#isQueryParam}}{{#useBeanValidation}}{{>beanValidationQueryParams}}{{/useBeanValidation}}{{#swagger2AnnotationLibrary}}@Parameter(description = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}{{#defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]{{^isContainer}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/isContainer}}){{/defaultValue}}{{/allowableValues}}{{#allowableValues}}{{^defaultValue}}, schema = Schema(allowableValues = [{{#values}}"{{{.}}}"{{^-last}}, {{/-last}}{{/values}}]){{/defaultValue}}{{/allowableValues}}{{^allowableValues}}{{#defaultValue}}{{^isContainer}}, schema = Schema(defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}){{/isContainer}}{{/defaultValue}}{{/allowableValues}}){{/swagger2AnnotationLibrary}}{{#swagger1AnnotationLibrary}}@ApiParam(value = "{{{description}}}"{{#required}}, required = true{{/required}}{{#allowableValues}}, allowableValues = "{{#values}}{{{.}}}{{^-last}}, {{/-last}}{{/values}}"{{/allowableValues}}{{^isContainer}}{{#defaultValue}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/defaultValue}}{{/isContainer}}){{/swagger1AnnotationLibrary}}{{#useBeanValidation}} @Valid{{/useBeanValidation}}{{^isModel}} @RequestParam(value = "{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}{{^isContainer}}{{#defaultValue}}, defaultValue = {{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{{defaultValue}}}{{^isString}}"{{/isString}}{{#isString}}{{#isEnum}}"{{/isEnum}}{{/isString}}{{/defaultValue}}{{/isContainer}}){{/isModel}}{{#isDate}} @org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE){{/isDate}}{{#isDateTime}} @org.springframework.format.annotation.DateTimeFormat(iso = org.springframework.format.annotation.DateTimeFormat.ISO.DATE_TIME){{/isDateTime}} {{paramName}}: {{>optionalDataType}}{{/isQueryParam}} \ No newline at end of file diff --git a/templates/returnTypes.mustache b/templates/returnTypes.mustache new file mode 100644 index 00000000..eddd93e3 --- /dev/null +++ b/templates/returnTypes.mustache @@ -0,0 +1,2 @@ +{{#isMap}}Map +{{/isMap}}{{#isArray}}{{#reactive}}Flow{{/reactive}}{{^reactive}}{{{returnContainer}}}{{/reactive}}<{{{returnType}}}>{{/isArray}}{{^returnContainer}}{{{returnType}}}{{/returnContainer}} diff --git a/templates/returnValue.mustache b/templates/returnValue.mustache new file mode 100644 index 00000000..8c7c1aa4 --- /dev/null +++ b/templates/returnValue.mustache @@ -0,0 +1 @@ +{{#serviceInterface}}ResponseEntity(service.{{operationId}}({{#allParams}}{{{paramName}}}{{^-last}}, {{/-last}}{{/allParams}}), {{#responses}}{{#-first}}HttpStatus.valueOf({{code}}){{/-first}}{{/responses}}){{/serviceInterface}}{{^serviceInterface}}ResponseEntity(HttpStatus.NOT_IMPLEMENTED){{/serviceInterface}} \ No newline at end of file diff --git a/templates/service.mustache b/templates/service.mustache new file mode 100644 index 00000000..f212e407 --- /dev/null +++ b/templates/service.mustache @@ -0,0 +1,36 @@ +package {{package}} + +{{#imports}}import {{import}} +{{/imports}} +{{#reactive}} + import kotlinx.coroutines.flow.Flow +{{/reactive}} + +{{#operations}} + interface {{classname}}Service { + {{#operation}} + + /** + * {{httpMethod}} {{{path}}}{{#summary}} : {{.}}{{/summary}} + {{#notes}} + * {{.}} + {{/notes}} + * + {{#allParams}} + * @param {{{paramName}}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}} + {{/allParams}} + * @return {{#responses}}{{message}} (status code {{code}}){{^-last}} + * or {{/-last}}{{/responses}} + {{#isDeprecated}} + * @deprecated + {{/isDeprecated}} + {{#externalDocs}} + * {{description}} + * @see {{summary}} Documentation + {{/externalDocs}} + * @see {{classname}}#{{operationId}} + */ + {{#reactive}}{{^isArray}}suspend {{/isArray}}{{/reactive}}fun {{operationId}}({{#allParams}}{{{paramName}}}: {{^isBodyParam}}{{>optionalDataType}}{{/isBodyParam}}{{#isBodyParam}}{{^reactive}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isArray}}{{>optionalDataType}}{{/isArray}}{{#isArray}}Flow<{{{baseType}}}>{{/isArray}}{{/reactive}}{{/isBodyParam}}{{^-last}}, {{/-last}}{{/allParams}}): {{>returnTypes}} + {{/operation}} + } +{{/operations}} diff --git a/templates/serviceImpl.mustache b/templates/serviceImpl.mustache new file mode 100644 index 00000000..7a707cb3 --- /dev/null +++ b/templates/serviceImpl.mustache @@ -0,0 +1,19 @@ +package {{package}} + +{{#imports}}import {{import}} +{{/imports}} +{{#reactive}} + import kotlinx.coroutines.flow.Flow +{{/reactive}} +import org.springframework.stereotype.Service +@Service +{{#operations}} + class {{classname}}ServiceImpl : {{classname}}Service { + {{#operation}} + + override {{#reactive}}{{^isArray}}suspend {{/isArray}}{{/reactive}}fun {{operationId}}({{#allParams}}{{paramName}}: {{^isBodyParam}}{{>optionalDataType}}{{/isBodyParam}}{{#isBodyParam}}{{^reactive}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isArray}}{{>optionalDataType}}{{/isArray}}{{#isArray}}Flow<{{{baseType}}}>{{/isArray}}{{/reactive}}{{/isBodyParam}}{{^-last}}, {{/-last}}{{/allParams}}): {{>returnTypes}} { + TODO("Implement me") + } + {{/operation}} + } +{{/operations}} diff --git a/templates/springdocDocumentationConfig.mustache b/templates/springdocDocumentationConfig.mustache new file mode 100644 index 00000000..95c4799c --- /dev/null +++ b/templates/springdocDocumentationConfig.mustache @@ -0,0 +1,53 @@ +package {{basePackage}} + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +import io.swagger.v3.oas.models.OpenAPI +import io.swagger.v3.oas.models.info.Info +import io.swagger.v3.oas.models.info.Contact +import io.swagger.v3.oas.models.info.License +import io.swagger.v3.oas.models.Components +import io.swagger.v3.oas.models.security.SecurityScheme + +@Configuration +class SpringDocConfiguration { + +@Bean +fun apiInfo(): OpenAPI { +return OpenAPI() +.info( +Info(){{#appName}} + .title("{{appName}}"){{/appName}} +.description("{{{appDescription}}}"){{#termsOfService}} + .termsOfService("{{termsOfService}}"){{/termsOfService}}{{#openAPI}}{{#info}}{{#contact}} + .contact( + Contact(){{#infoName}} + .name("{{infoName}}"){{/infoName}}{{#infoUrl}} + .url("{{infoUrl}}"){{/infoUrl}}{{#infoEmail}} + .email("{{infoEmail}}"){{/infoEmail}} + ){{/contact}}{{#license}} + .license( + License() + {{#licenseInfo}}.name("{{licenseInfo}}") + {{/licenseInfo}}{{#licenseUrl}}.url("{{licenseUrl}}") + {{/licenseUrl}} + ){{/license}}{{/info}}{{/openAPI}} +.version("{{appVersion}}") +){{#hasAuthMethods}} + .components( + Components(){{#authMethods}} + .addSecuritySchemes("{{name}}", SecurityScheme(){{#isBasic}} + .type(SecurityScheme.Type.HTTP) + .scheme("{{scheme}}"){{#bearerFormat}} + .bearerFormat("{{bearerFormat}}"){{/bearerFormat}}{{/isBasic}}{{#isApiKey}} + .type(SecurityScheme.Type.APIKEY){{#isKeyInHeader}} + .`in`(SecurityScheme.In.HEADER){{/isKeyInHeader}}{{#isKeyInQuery}} + .`in`(SecurityScheme.In.QUERY){{/isKeyInQuery}}{{#isKeyInCookie}} + .`in`(SecurityScheme.In.COOKIE){{/isKeyInCookie}} + .name("{{keyParamName}}"){{/isApiKey}}{{#isOAuth}} + .type(SecurityScheme.Type.OAUTH2){{/isOAuth}} + ){{/authMethods}} + ){{/hasAuthMethods}} +} +} diff --git a/templates/springfoxDocumentationConfig.mustache b/templates/springfoxDocumentationConfig.mustache new file mode 100644 index 00000000..ec08792e --- /dev/null +++ b/templates/springfoxDocumentationConfig.mustache @@ -0,0 +1,66 @@ +package {{basePackage}} + +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.web.util.UriComponentsBuilder +import springfox.documentation.builders.ApiInfoBuilder +import springfox.documentation.builders.RequestHandlerSelectors +import springfox.documentation.service.ApiInfo +import springfox.documentation.service.Contact +import springfox.documentation.spi.DocumentationType +import springfox.documentation.spring.web.paths.Paths +import springfox.documentation.spring.web.paths.RelativePathProvider +import springfox.documentation.spring.web.plugins.Docket +import springfox.documentation.swagger2.annotations.EnableSwagger2 +import {{javaxPackage}}.servlet.ServletContext + + +{{>generatedAnnotation}} +@Configuration +@EnableSwagger2 +class SpringFoxConfiguration { + +fun apiInfo(): ApiInfo { +return ApiInfoBuilder() +.title("{{appName}}") +.description("{{{appDescription}}}") +.license("{{licenseInfo}}") +.licenseUrl("{{licenseUrl}}") +.termsOfServiceUrl("{{infoUrl}}") +.version("{{appVersion}}") +.contact(Contact("", "", "{{infoEmail}}")) +.build() +} + +@Bean +{{=<% %>=}} +fun customImplementation(servletContext: ServletContext, @Value("\${openapi.<%title%>.base-path:<%>defaultBasePath%>}") basePath: String): Docket { +<%={{ }}=%> +return Docket(DocumentationType.SWAGGER_2) +.select() +.apis(RequestHandlerSelectors.basePackage("{{apiPackage}}")) +.build() +.pathProvider(BasePathAwareRelativePathProvider(servletContext, basePath)) +.directModelSubstitute(java.time.LocalDate::class.java, java.sql.Date::class.java) +.directModelSubstitute(java.time.OffsetDateTime::class.java, java.util.Date::class.java) +.apiInfo(apiInfo()) +} + +class BasePathAwareRelativePathProvider(servletContext: ServletContext, private val basePath: String) : +RelativePathProvider(servletContext) { + +override fun applicationPath(): String { +return Paths.removeAdjacentForwardSlashes( +UriComponentsBuilder.fromPath(super.applicationPath()).path(basePath).build().toString() +) +} + +override fun getOperationPath(operationPath: String): String { +val uriComponentsBuilder = UriComponentsBuilder.fromPath("/") +return Paths.removeAdjacentForwardSlashes( +uriComponentsBuilder.path(operationPath.replaceFirst("^$basePath", "")).build().toString() +) +} +} +} diff --git a/templates/typeInfoAnnotation.mustache b/templates/typeInfoAnnotation.mustache new file mode 100644 index 00000000..acc68dc7 --- /dev/null +++ b/templates/typeInfoAnnotation.mustache @@ -0,0 +1,8 @@ +{{#jackson}} + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "{{{discriminator.propertyBaseName}}}", visible = true) + @JsonSubTypes( + {{#discriminator.mappedModels}} + JsonSubTypes.Type(value = {{modelName}}::class, name = "{{^vendorExtensions.x-discriminator-value}}{{mappingName}}{{/vendorExtensions.x-discriminator-value}}{{#vendorExtensions.x-discriminator-value}}{{{vendorExtensions.x-discriminator-value}}}{{/vendorExtensions.x-discriminator-value}}"){{^-last}},{{/-last}} + {{/discriminator.mappedModels}} + ){{/jackson}}