start including of publishing subproject

This commit is contained in:
2020-07-26 23:03:23 +06:00
parent 1ef5cc5af0
commit 7cfa612a9c
24 changed files with 254 additions and 1 deletions

View File

@@ -0,0 +1,47 @@
buildscript {
repositories {
mavenLocal()
jcenter()
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
classpath "com.jfrog.bintray.gradle:gradle-bintray-plugin:$gradle_bintray_plugin_version"
}
}
plugins {
id "org.jetbrains.kotlin.plugin.serialization" version "$kotlin_version"
}
project.version = "$core_version"
project.group = "com.insanusmokrassar"
apply plugin: "java-library"
apply plugin: "kotlin"
apply from: "./publish.gradle"
repositories {
mavenLocal()
jcenter()
mavenCentral()
maven { url "https://kotlin.bintray.com/kotlinx" }
}
dependencies {
api "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
api "org.jetbrains.exposed:exposed-core:$exposed_version"
api "org.jetbrains.exposed:exposed-jdbc:$exposed_version"
if ((project.hasProperty('RELEASE_MODE') && project.property('RELEASE_MODE') == "true") || System.getenv('RELEASE_MODE') == "true") {
api "com.insanusmokrassar:postssystem.core:$core_version"
} else {
implementation project(":postssystem.core")
}
testImplementation "org.xerial:sqlite-jdbc:$test_sqlite_version"
testImplementation "org.jetbrains.kotlin:kotlin-test"
testImplementation "org.jetbrains.kotlin:kotlin-test-junit"
}

View File

@@ -0,0 +1,2 @@
exposed_version=0.23.1
test_sqlite_version=3.28.0

View File

@@ -0,0 +1,56 @@
apply plugin: 'maven-publish'
task sourcesJar(type: Jar) {
from sourceSets.main.allSource
classifier = 'sources'
}
task javadocJar(type: Jar) {
from javadoc
classifier = 'javadoc'
}
publishing {
publications {
maven(MavenPublication) {
from components.java
artifact sourcesJar
artifact javadocJar
pom.withXml {
asNode().children().last() + {
resolveStrategy = Closure.DELEGATE_FIRST
description "Exposed realisation for PostsSystem Core"
name "PostsSystem Core Exposed realization"
url "https://git.insanusmokrassar.com/PostsSystem/Core/"
scm {
developerConnection "scm:git:[fetch=]https://git.insanusmokrassar.com/PostsSystem/Core/.git[push=]https://git.insanusmokrassar.com/PostsSystem/Core/.git"
url "https://git.insanusmokrassar.com/PostsSystem/Core/.git"
}
developers {
developer {
id "InsanusMokrassar"
name "Ovsiannikov Aleksei"
email "ovsyannikov.alexey95@gmail.com"
}
}
licenses {
license {
name "Apache Software License 2.0"
url "https://git.insanusmokrassar.com/PostsSystem/Core/src/master/LICENSE"
}
}
}
}
}
}
}

View File

@@ -0,0 +1,39 @@
apply plugin: 'com.jfrog.bintray'
ext {
projectBintrayDir = "${project.group}/".replace(".", "/") + "${project.name}/${project.version}"
}
bintray {
user = project.hasProperty('BINTRAY_USER') ? project.property('BINTRAY_USER') : System.getenv('BINTRAY_USER')
key = project.hasProperty('BINTRAY_KEY') ? project.property('BINTRAY_KEY') : System.getenv('BINTRAY_KEY')
publications = ["maven"]
filesSpec {
into "$projectBintrayDir"
from("build/libs") {
include "**/*.asc"
}
from("build/publications/maven") {
rename 'pom-default.xml(.*)', "${project.name}-${project.version}.pom\$1"
}
}
pkg {
repo = "InsanusMokrassar"
name = "${project.name}"
vcsUrl = "https://github.com/PostsSystem/PostsSystemCore"
licenses = ["Apache-2.0"]
version {
name = "${project.version}"
released = new Date()
vcsTag = "${project.version}"
gpg {
sign = true
passphrase = project.hasProperty('signing.gnupg.passphrase') ? project.property('signing.gnupg.passphrase') : System.getenv('signing.gnupg.passphrase')
}
}
}
}
apply from: "maven.publish.gradle"
bintrayUpload.dependsOn publishToMavenLocal

View File

@@ -0,0 +1 @@
{"bintrayConfig":{"repo":"InsanusMokrassar","packageName":"${project.name}","packageVcs":"https://github.com/PostsSystem/PostsSystemCore"},"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://git.insanusmokrassar.com/PostsSystem/Core/src/master/LICENSE"}],"mavenConfig":{"name":"PostsSystem Core Exposed realization","description":"Exposed realisation for PostsSystem Core","url":"https://git.insanusmokrassar.com/PostsSystem/Core/","vcsUrl":"https://git.insanusmokrassar.com/PostsSystem/Core/.git","developers":[{"id":"InsanusMokrassar","name":"Ovsiannikov Aleksei","eMail":"ovsyannikov.alexey95@gmail.com"}]},"type":"JVM"}

View File

@@ -0,0 +1,3 @@
package com.insanusmokrassar.postssystem.core.exposed
internal const val ChannelDefaultSize = 64

View File

@@ -0,0 +1,134 @@
package com.insanusmokrassar.postssystem.core.exposed
import com.insanusmokrassar.postssystem.core.content.*
import com.insanusmokrassar.postssystem.core.content.api.ContentAPI
import com.insanusmokrassar.postssystem.core.exposed.content.*
import com.insanusmokrassar.postssystem.core.utils.generateContentId
import com.insanusmokrassar.postssystem.core.utils.pagination.*
import kotlinx.coroutines.channels.BroadcastChannel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
private val Content.type
get() = when (this) {
is TextContent -> "text"
is BinaryContent -> "binary"
is SpecialContent -> "special"
}
private class ContentAPIDatabaseTable(
private val database: Database,
private val textHolder: ContentHolderRepo<TextContent>,
private val binaryHolder: ContentHolderRepo<BinaryContent>,
private val specialHolder: ContentHolderRepo<SpecialContent>
) : Table("ContentAPI"), ContentAPI, ContentHolderRepo<Content> {
internal val idColumn = text("_id")
internal val typeColumn = text("type")
init {
transaction(database) {
SchemaUtils.createMissingTablesAndColumns(this@ContentAPIDatabaseTable)
}
}
private val contentCreatedBroadcastChannel = BroadcastChannel<RegisteredContent>(ChannelDefaultSize)
private val contentDeletedBroadcastChannel = BroadcastChannel<RegisteredContent>(ChannelDefaultSize)
override val contentCreatedFlow: Flow<RegisteredContent> = contentCreatedBroadcastChannel.asFlow()
override val contentDeletedFlow: Flow<RegisteredContent> = contentDeletedBroadcastChannel.asFlow()
private val String.holder
get() = when (this) {
"text" -> textHolder
"binary" -> binaryHolder
"special" -> specialHolder
else -> null
}
override suspend fun putContent(id: ContentId, content: Content) {
when (content) {
is TextContent -> textHolder.putContent(id, content)
is BinaryContent -> binaryHolder.putContent(id, content)
is SpecialContent -> specialHolder.putContent(id, content)
}
}
override suspend fun getContent(id: ContentId): Content? = transaction(database) {
select { idColumn.eq(id) }.limit(1).firstOrNull() ?.get(typeColumn)
} ?.holder ?.getContent(id)
override suspend fun removeContent(id: ContentId) {
transaction(database) {
select { idColumn.eq(id) }.limit(1).firstOrNull() ?.get(typeColumn)
} ?.holder ?.removeContent(id)
}
override suspend fun registerContent(content: Content): RegisteredContent? {
val id = generateContentId()
val type = content.type
return transaction(database) {
insert {
it[idColumn] = id
it[typeColumn] = type
}.getOrNull(idColumn)
} ?.let { id ->
putContent(id, content)
RegisteredContent(
id,
content
)
} ?.also {
contentCreatedBroadcastChannel.send(it)
} ?: null.also {
removeContent(id)
}
}
override suspend fun deleteContent(id: ContentId): Boolean {
val content = getContentById(id) ?: return false
return transaction(database) {
deleteWhere {
idColumn.eq(id)
} > 0
}.also {
if (it) {
removeContent(id)
contentDeletedBroadcastChannel.send(content)
}
}
}
private fun ResultRow.asRegisteredContent(content: Content): RegisteredContent? = get(idColumn).let {
RegisteredContent(
it,
content
)
}
override suspend fun getContentsIds(): Set<ContentId> {
return transaction(database) {
selectAll().map { it[idColumn] }
}.toSet()
}
override suspend fun getContentById(id: ContentId): RegisteredContent? {
val content = getContent(id) ?: return null
return transaction(database) {
select { idColumn.eq(id) }.limit(1).firstOrNull() ?.asRegisteredContent(content)
}
}
override suspend fun getContentByPagination(pagination: Pagination): PaginationResult<out RegisteredContent> {
return transaction(database) {
selectAll().count() to selectAll().paginate(pagination).map { it[idColumn] }
}.let { (count, results) ->
results.mapNotNull { RegisteredContent(it, getContent(it) ?: return@mapNotNull null) }.createPaginationResult(
pagination,
count
)
}
}
}
class ExposedContentAPI (
database: Database,
textHolder: ContentHolderRepo<TextContent> = TextContentHolderRepo(database),
binaryHolder: ContentHolderRepo<BinaryContent> = BinaryContentHolderRepo(database),
specialHolder: ContentHolderRepo<SpecialContent> = SpecialContentHolderRepo(database)
) : ContentAPI by ContentAPIDatabaseTable(database, textHolder, binaryHolder, specialHolder)

View File

@@ -0,0 +1,175 @@
package com.insanusmokrassar.postssystem.core.exposed
import com.insanusmokrassar.postssystem.core.content.ContentId
import com.insanusmokrassar.postssystem.core.post.*
import com.insanusmokrassar.postssystem.core.post.api.PostsAPI
import com.insanusmokrassar.postssystem.core.utils.generatePostId
import com.insanusmokrassar.postssystem.core.utils.pagination.*
import com.soywiz.klock.*
import kotlinx.coroutines.channels.BroadcastChannel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
private class PostsAPIContentRelations(
private val database: Database
) : Table() {
private val postIdColumn = text("postId")
private val contentIdColumn = text("contentId")
init {
transaction(database) {
SchemaUtils.createMissingTablesAndColumns(this@PostsAPIContentRelations)
}
}
fun getPostContents(postId: PostId): List<ContentId> {
return transaction(database) {
select { postIdColumn.eq(postId) }.map { it[contentIdColumn] }
}
}
fun getContentPosts(contentId: ContentId): List<PostId> {
return transaction(database) {
select { contentIdColumn.eq(contentId) }.map { it[postIdColumn] }
}
}
fun linkPostAndContents(postId: PostId, vararg contentIds: ContentId) {
transaction(database) {
val leftToPut = contentIds.toSet() - getPostContents(postId)
leftToPut.forEach { contentId ->
insert {
it[postIdColumn] = postId
it[contentIdColumn] = contentId
}
}
}
}
fun unlinkPostAndContents(postId: PostId, vararg contentIds: ContentId): Boolean {
return transaction(database) {
deleteWhere {
postIdColumn.eq(postId).and(contentIdColumn.inList(contentIds.toList()))
} > 0
}
}
}
private val dateTimeFormat = DateFormat("EEE, dd MMM yyyy HH:mm:ss z")
private class PostsAPIDatabaseTable(
private val database: Database
) : PostsAPI, Table() {
private val contentsTable = PostsAPIContentRelations(database)
private val idColumn = text("postId")
private val creationDateColumn = text("creationDate").default(
DateTime.now().toString(dateTimeFormat)
)
private val postCreatedBroadcastChannel = BroadcastChannel<RegisteredPost>(ChannelDefaultSize)
override val postCreatedFlow: Flow<RegisteredPost> = postCreatedBroadcastChannel.asFlow()
private val postDeletedBroadcastChannel = BroadcastChannel<RegisteredPost>(ChannelDefaultSize)
override val postDeletedFlow: Flow<RegisteredPost> = postDeletedBroadcastChannel.asFlow()
private val postUpdatedBroadcastChannel = BroadcastChannel<RegisteredPost>(ChannelDefaultSize)
override val postUpdatedFlow: Flow<RegisteredPost> = postUpdatedBroadcastChannel.asFlow()
init {
transaction(database) {
SchemaUtils.createMissingTablesAndColumns(this@PostsAPIDatabaseTable)
}
}
private fun ResultRow.toRegisteredPost(): RegisteredPost = get(idColumn).let { id ->
SimpleRegisteredPost(
id,
contentsTable.getPostContents(id),
dateTimeFormat.parse(get(creationDateColumn)).local
)
}
override suspend fun createPost(post: Post): RegisteredPost? {
val id = generatePostId()
return transaction(database) {
insert {
it[idColumn] = id
}
contentsTable.linkPostAndContents(id, *post.content.toTypedArray())
select { idColumn.eq(id) }.firstOrNull() ?.toRegisteredPost()
} ?.also {
postCreatedBroadcastChannel.send(it)
}
}
override suspend fun deletePost(id: PostId): Boolean {
val post = getPostById(id) ?: return false
return (transaction(database) {
deleteWhere { idColumn.eq(id) }
} > 0).also {
if (it) {
postDeletedBroadcastChannel.send(post)
contentsTable.unlinkPostAndContents(id, *post.content.toTypedArray())
}
}
}
override suspend fun updatePostContent(postId: PostId, post: Post): Boolean {
return transaction(database) {
val alreadyLinked = contentsTable.getPostContents(postId)
val toRemove = alreadyLinked - post.content
val toInsert = post.content - alreadyLinked
val updated = (toRemove.isNotEmpty() && contentsTable.unlinkPostAndContents(postId, *toRemove.toTypedArray())) || toInsert.isNotEmpty()
if (toInsert.isNotEmpty()) {
contentsTable.linkPostAndContents(postId, *toInsert.toTypedArray())
}
updated
}.also {
if (it) {
getPostById(postId) ?.also { updatedPost -> postUpdatedBroadcastChannel.send(updatedPost) }
}
}
}
override suspend fun getPostsIds(): Set<PostId> {
return transaction(database) {
selectAll().map { it[idColumn] }.toSet()
}
}
override suspend fun getPostById(id: PostId): RegisteredPost? {
return transaction(database) {
select { idColumn.eq(id) }.firstOrNull() ?.toRegisteredPost()
}
}
override suspend fun getPostsByContent(id: ContentId): List<RegisteredPost> {
return transaction(database) {
val postsIds = contentsTable.getContentPosts(id)
select { idColumn.inList(postsIds) }.map { it.toRegisteredPost() }
}
}
override suspend fun getPostsByCreatingDates(from: DateTime, to: DateTime): List<RegisteredPost> {
return transaction(database) {
select { creationDateColumn.between(from, to) }.map { it.toRegisteredPost() }
}
}
override suspend fun getPostsByPagination(pagination: Pagination): PaginationResult<RegisteredPost> {
return transaction(database) {
val posts = selectAll().paginate(pagination).orderBy(creationDateColumn).map {
it.toRegisteredPost()
}
val postsNumber = selectAll().count()
posts.createPaginationResult(pagination, postsNumber)
}
}
}
class ExposedPostsAPI (
database: Database
) : PostsAPI by PostsAPIDatabaseTable(database)

View File

@@ -0,0 +1,7 @@
package com.insanusmokrassar.postssystem.core.exposed
import com.insanusmokrassar.postssystem.core.utils.pagination.Pagination
import com.insanusmokrassar.postssystem.core.utils.pagination.firstIndex
import org.jetbrains.exposed.sql.Query
fun Query.paginate(pagination: Pagination) = limit(pagination.size, pagination.firstIndex.toLong())

View File

@@ -0,0 +1,58 @@
package com.insanusmokrassar.postssystem.core.exposed.content
import com.insanusmokrassar.postssystem.core.content.BinaryContent
import com.insanusmokrassar.postssystem.core.content.ContentId
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.statements.api.ExposedBlob
import org.jetbrains.exposed.sql.transactions.transaction
private class BinaryContentHolderRepoTable(
private val database: Database
) : ContentHolderRepo<BinaryContent>, Table() {
private val idColumn = text("id")
private val dataColumn = blob("data")
private val mimeColumn = text("mimeType")
private val originalFileNameColumn = text("filename")
override val primaryKey: PrimaryKey = PrimaryKey(idColumn)
init {
transaction(database) {
SchemaUtils.createMissingTablesAndColumns(this@BinaryContentHolderRepoTable)
}
}
override suspend fun getContent(id: ContentId): BinaryContent? = transaction(database) {
select {
idColumn.eq(id)
}.limit(1).firstOrNull() ?.let {
val bytes = it[dataColumn].bytes
BinaryContent(
it[mimeColumn],
it[originalFileNameColumn]
) {
bytes
}
}
}
override suspend fun removeContent(id: ContentId) {
transaction(database) {
deleteWhere { idColumn.eq(id) }
}
}
override suspend fun putContent(id: ContentId, content: BinaryContent) {
transaction(database) {
insert {
it[idColumn] = id
it[originalFileNameColumn] = content.originalFileName
it[mimeColumn] = content.mimeType
it[dataColumn] = ExposedBlob(content.dataAllocator())
}
}
}
}
class BinaryContentHolderRepo(
database: Database
) : ContentHolderRepo<BinaryContent> by BinaryContentHolderRepoTable(database)

View File

@@ -0,0 +1,10 @@
package com.insanusmokrassar.postssystem.core.exposed.content
import com.insanusmokrassar.postssystem.core.content.Content
import com.insanusmokrassar.postssystem.core.content.ContentId
interface ContentHolderRepo<T : Content> {
suspend fun getContent(id: ContentId) : T?
suspend fun removeContent(id: ContentId)
suspend fun putContent(id: ContentId, content: T)
}

View File

@@ -0,0 +1,47 @@
package com.insanusmokrassar.postssystem.core.exposed.content
import com.insanusmokrassar.postssystem.core.content.ContentId
import com.insanusmokrassar.postssystem.core.content.SpecialContent
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
private class SpecialContentHolderRepoTable(
private val database: Database
) : ContentHolderRepo<SpecialContent>, Table() {
private val idColumn = text("id")
private val internalIdColumn = text("internalId")
override val primaryKey: PrimaryKey = PrimaryKey(idColumn)
init {
transaction(database) {
SchemaUtils.createMissingTablesAndColumns(this@SpecialContentHolderRepoTable)
}
}
override suspend fun getContent(id: ContentId): SpecialContent? = transaction(database) {
select {
idColumn.eq(id)
}.limit(1).firstOrNull() ?.get(internalIdColumn) ?.let {
SpecialContent(it)
}
}
override suspend fun removeContent(id: ContentId) {
transaction(database) {
deleteWhere { idColumn.eq(id) }
}
}
override suspend fun putContent(id: ContentId, content: SpecialContent) {
transaction(database) {
insert {
it[idColumn] = id
it[internalIdColumn] = content.internalId
}
}
}
}
class SpecialContentHolderRepo(
database: Database
) : ContentHolderRepo<SpecialContent> by SpecialContentHolderRepoTable(database)

View File

@@ -0,0 +1,47 @@
package com.insanusmokrassar.postssystem.core.exposed.content
import com.insanusmokrassar.postssystem.core.content.ContentId
import com.insanusmokrassar.postssystem.core.content.TextContent
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
private class TextContentHolderRepoTable(
private val database: Database
) : ContentHolderRepo<TextContent>, Table() {
private val idColumn = text("id")
private val textColumn = text("text")
override val primaryKey: PrimaryKey = PrimaryKey(idColumn)
init {
transaction(database) {
SchemaUtils.createMissingTablesAndColumns(this@TextContentHolderRepoTable)
}
}
override suspend fun getContent(id: ContentId): TextContent? = transaction(database) {
select {
idColumn.eq(id)
}.limit(1).firstOrNull() ?.get(textColumn) ?.let {
TextContent(it)
}
}
override suspend fun removeContent(id: ContentId) {
transaction(database) {
deleteWhere { idColumn.eq(id) }
}
}
override suspend fun putContent(id: ContentId, content: TextContent) {
transaction(database) {
insert {
it[idColumn] = id
it[textColumn] = content.text
}
}
}
}
class TextContentHolderRepo(
database: Database
) : ContentHolderRepo<TextContent> by TextContentHolderRepoTable(database)

View File

@@ -0,0 +1,62 @@
package com.insanusmokrassar.postssystem.core.exposed
import com.insanusmokrassar.postssystem.core.content.TextContent
import kotlinx.coroutines.runBlocking
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.transactions.transactionManager
import java.io.File
import java.sql.Connection
import kotlin.test.Test
import kotlin.test.assertEquals
class ExposedContentAPICommonTests {
private val tempFolder = System.getProperty("java.io.tmpdir")!!
@Test
fun `Test that it is possible to use several different databases at one time`() {
val numberOfDatabases = 8
val databaseFiles = (0 until numberOfDatabases).map {
"$tempFolder/ExposedContentAPICommonTestsDB$it.db"
}
val apis = databaseFiles.map {
File(it).also {
it.delete()
it.deleteOnExit()
}
ExposedContentAPI(
Database.Companion.connect("jdbc:sqlite:$it", driver = "org.sqlite.JDBC").also {
it.transactionManager.defaultIsolationLevel = Connection.TRANSACTION_SERIALIZABLE
}
)
}
val results = apis.mapIndexed { i, api ->
val content = runBlocking { api.registerContent(TextContent(i.toString())) }
assert(content != null)
val ids = runBlocking { api.getContentsIds() }
assertEquals(ids.size, 1)
content!!
}
results.forEachIndexed { i, content ->
apis.forEachIndexed { j, api ->
assert(
runBlocking {
api.getContentById(content.id) == (if (i != j) null else content)
}
)
assert(
runBlocking {
api.deleteContent(content.id) == (i == j)
}
)
}
}
databaseFiles.forEach {
File(it).delete()
}
}
}

View File

@@ -0,0 +1,64 @@
package com.insanusmokrassar.postssystem.core.exposed
import com.insanusmokrassar.postssystem.core.post.SimplePost
import kotlinx.coroutines.runBlocking
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.transactions.transactionManager
import java.io.File
import java.sql.Connection
import kotlin.test.*
class ExposedPostsAPICommonTests {
private val tempFolder = System.getProperty("java.io.tmpdir")!!
private val numberOfDatabases = 8
private lateinit var databaseFiles: List<File>
private lateinit var apis: List<ExposedPostsAPI>
@BeforeTest
fun prepare() {
databaseFiles = (0 until numberOfDatabases).map {
File("$tempFolder/ExposedPostsAPICommonTestsDB$it.db")
}
apis = databaseFiles.map {
val database = Database.connect("jdbc:sqlite:${it.absolutePath}").also {
it.transactionManager.defaultIsolationLevel = Connection.TRANSACTION_SERIALIZABLE
}
ExposedPostsAPI(
database
)
}
}
@Test
fun `Test that it is possible to use several different databases at one time`() {
val posts = apis.mapIndexed { i, api ->
val content = runBlocking { api.createPost(SimplePost(listOf(i.toString()))) }
assert(content != null)
assert(runBlocking { api.getPostsIds().size == 1 })
content!!
}
posts.forEachIndexed { i, post ->
apis.forEachIndexed { j, api ->
assert(
runBlocking {
api.getPostById(post.id) == (if (i != j) null else post)
}
)
assert(
runBlocking {
api.deletePost(post.id) == (i == j)
}
)
}
}
}
@AfterTest
fun `Close and delete databases`() {
databaseFiles.forEach {
it.delete()
}
}
}