From 6f9c47ce7bf34a936a3e95c2f2088d2955e27f6f Mon Sep 17 00:00:00 2001 From: "github-classroom[bot]" <66690702+github-classroom[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 14:51:26 +0000 Subject: [PATCH 01/15] Setting up GitHub Classroom Feedback From ad2e5b292a65f1ac2d774f88886fd479a1a563a0 Mon Sep 17 00:00:00 2001 From: "github-classroom[bot]" <66690702+github-classroom[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 14:51:29 +0000 Subject: [PATCH 02/15] add deadline --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 97f8c85..98999e5 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +[![Review Assignment Due Date](https://classroom.github.com/assets/deadline-readme-button-22041afd0340ce965d47ae6ef1cefeee28c7c493a6346c4f15d667ab976d596c.svg)](https://classroom.github.com/a/QODoQuhO) # Распределенная обработка текстовых данных с использованием брокера сообщений ## Цель задания: From 662a40be689ed64fdfeff62fec47a95a247f831d Mon Sep 17 00:00:00 2001 From: "ar.r.lysenko" Date: Mon, 24 Nov 2025 00:30:13 +0300 Subject: [PATCH 03/15] update gradle version, migrate to kotlin, update jvm version --- .gradle/8.4/checksums/checksums.lock | Bin 17 -> 17 bytes .gradle/8.4/checksums/md5-checksums.bin | Bin 18597 -> 21847 bytes .gradle/8.4/checksums/sha1-checksums.bin | Bin 21305 -> 31079 bytes .../executionHistory/executionHistory.lock | Bin 17 -> 17 bytes .gradle/8.4/fileHashes/fileHashes.bin | Bin 18747 -> 18797 bytes .gradle/8.4/fileHashes/fileHashes.lock | Bin 17 -> 17 bytes .../8.4/fileHashes/resourceHashesCache.bin | Bin 18531 -> 18565 bytes .../buildOutputCleanup.lock | Bin 17 -> 17 bytes .gradle/buildOutputCleanup/cache.properties | 4 +- .gradle/buildOutputCleanup/outputFiles.bin | Bin 19073 -> 18857 bytes .gradle/file-system.probe | Bin 8 -> 8 bytes .idea/amplicode-jpa.xml | 6 ++ .idea/codeStyles/codeStyleConfig.xml | 5 ++ .idea/compiler.xml | 2 +- .idea/gradle.xml | 2 + .idea/kotlinc.xml | 3 + .idea/libraries/KotlinJavaRuntime.xml | 23 +++++++ .idea/misc.xml | 5 +- .idea/modules.xml | 9 --- README.md | 56 +++++----------- build.gradle.kts | 23 ++++--- docs/TASK.md | 61 ++++++++++++++++++ gradle/wrapper/gradle-wrapper.jar | Bin 63721 -> 43764 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 12 ++-- gradlew.bat | 26 ++++---- src/main/java/Main.java | 5 -- src/main/kotlin/org/quicklybly/dumbmq/Main.kt | 5 ++ 28 files changed, 164 insertions(+), 85 deletions(-) create mode 100644 .idea/amplicode-jpa.xml create mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 .idea/libraries/KotlinJavaRuntime.xml delete mode 100644 .idea/modules.xml create mode 100644 docs/TASK.md delete mode 100644 src/main/java/Main.java create mode 100644 src/main/kotlin/org/quicklybly/dumbmq/Main.kt diff --git a/.gradle/8.4/checksums/checksums.lock b/.gradle/8.4/checksums/checksums.lock index b01eae26ceeb6ce3aecfed8895d5499faa4d159d..c199a090801026da56bfc7358c445081ac83cf48 100644 GIT binary patch literal 17 VcmZSH*cj4lb6?~<0~j!x001*i1WEt^ literal 17 UcmZSH*cj4lb6?~<0|ZzB05ehqPXGV_ diff --git a/.gradle/8.4/checksums/md5-checksums.bin b/.gradle/8.4/checksums/md5-checksums.bin index 1cf5320b892fe9573559db0ea50b42eb56e9fe86..0c957de6eba111af3d77b1eba34ba508652d148c 100644 GIT binary patch literal 21847 zcmeI3dpK2DAICSH<1Q*G(I6@J<60pTQE}WkC{b<^LuAUWi!vFdq)FtG+(y%&lu$%S zZ!&5W8o4W(Qd6()WYW8~wU>F`z5Vu>6Tk`J1aJa40h|C%04IPGzzN_4Z~{01oB&P$Cx8?9e@Vav z9Eb=!n1verGC4f6x7XLY_iU#15?w%LsS|KM<5-E)ySHqwyWx=iziiAl}A zXB2l)M*LWQsmzC()<58Qo;uZ5v3)JHvcN9fDEk34EPJ-VJa)&O&Gdi=2P0d6q zA?F4lo_jTXU3y`cHssFZi03KC9gAY=?}psYewuIAG7Rk0HH6%nGtIr-H`TIgiXeBG zgLtXY<=EkO$2A~#Y)8CY)+5Ms1LqRtRu86mcypJa%eHyc{=KJp=Ka1$7TG+=ZITeL z_-S~|tJY~Pwf{WCuj)UFHGM)%K<>mwynca<=w@>ZYsf9l5Wg8`Hj)0}^D}DvJ;ZN| zbdG8IoBs&8^(VyJ_{{4@$I9+P&U-h_gF3!8bp;GT~+OrZlif16+dEe0W zcC^Jd$enc%@8V75N*LW%p~lZby!-VT-J3@TzlGe<1o1(crTv=1JwA}z4Nr6K6`o&H z-EoRvL3||engE}UaysNTZxH`CE7J8{%SDI@=TyW$)+v{Z(K>F(0a(Nau64h8YxK8Yhpvr^F(|$SK@x+CvR$f z@){7IcYzb4ll+#tzN}LamkwKcsp4SfO=|zb)4bN}ux+^LI>@JGQ z5nnA_HsV`h(5x1$zGtiy;8kpui0DP>@~zZ(5xo?X3{^ z(aIlFIN)1G?Y|T805kK@VQ&svKgNh}AAh#DYE##74Fc=KuAmtN+%$kPCFCxpL7^|tf$&vj7e#r7roVBcg! zk3x%29UO0?iunEtFQ51a!ikVOhEDUbolT$kc+`2cA3z-aCJ2E|(7`=U04IPGzzN_4 zZ~{01oB&P$Cx8>c3E%{90yqJj08RiWfD^z8-~@02I02jhP5>u>6Tk`J1aJa40h|C% z04IPGzzN_4Z~{01oWTD&0rDYbRq&%o{z&Tovbi$Ea%ZgQm*^cko8K)X2)^SttM#cT zy7}|KAg%v4=18oH)wr|PWbVo?jwx3dJQL1$e0H&~2N($&+Y59wV}=@;WhXtUr)FoW z;Zf`&A~!!;!nc~Q@4~AcvY-Yqd>Y6K;JpUYq}7=1!LMmIU-bNrC(HYCD%Yt&!?$Zn zAf(7ATxmwqY&WiNZ)1J-1j}OqTTmPtUeq(b##fkz%7}K@w+8ybC9(W`viF?9NOD&G z@JU$yKy{|kR35du*FqfBG{gBPl!;_8b_cHa#V>W=#wI~{jhBg%+? zHH6%m22ZwkgO=W;m=mjDiz(X>ox#)4kQ-ncAx|UUj;^YHuhV$L%}~n7ldACv8l50J z`jHo1sI?q5B16xsucHS+@K8d z9s(LW0Ccphl0L-K#&VxRA} z7);$06|HRjJ zURFFdd}sJoe6InwAT)kv_G7EIH8eDK~w>)im{p+r`CQ=ujWhjGurw=q` z7tqm+y&sa3mGmZaM1K_2F>b;BH^{Ro9?rtn2FeMNxw=s=?%HF+M^Y^b=mz}G6>*;5H zvc}vOO4R!|CSVWr8l#+WlMU-fjH`y&gL@8FfO|S$#?Rlh0h4SV%*_IHE~Wq~ToC z-U|)q8NOxcl&C*;VRLF*R;)3XdNw=&jSf#}h-ETs?2Fr!*ZH_WA@LIb*yWcW;axP1 zvmYAD-AqHx?E5o~*P}lNg#9LId~0DnsFCrcr;5DmfF}1d=xABxhkj(WozhV)oH$Y2 z!L8_m#x?41l>Q2)QQuxa99O z->*^!YlfeT=8(Y>#NRnBVrEx%JWYZvDPNdZkTy)&| z-ur%`Lg#tkMCj7*cyN=i%?m6hQ64+RVi42~NWe~521xFJ5-(IW)_uhSEq diff --git a/.gradle/8.4/checksums/sha1-checksums.bin b/.gradle/8.4/checksums/sha1-checksums.bin index d8e573f6fc3bfde6debcc96f35e3a8b57dd7b897..ff11886d884ab9b3a50f0be4feccf0742c47448e 100644 GIT binary patch literal 31079 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 aun>fe(97TdC`#d#Ik}!mw{jdudFcm7_e9qK diff --git a/.gradle/8.4/executionHistory/executionHistory.lock b/.gradle/8.4/executionHistory/executionHistory.lock index b20bb36b2aa61817e0c0b747e7609e15bd8711a6..8851d5e7cf0df33bf2d5a2db307a27d5fb59345a 100644 GIT binary patch literal 17 UcmZP$e%WuKuw>Um1_Um1_)pS05yaJYybcN diff --git a/.gradle/8.4/fileHashes/fileHashes.bin b/.gradle/8.4/fileHashes/fileHashes.bin index 7d9de1c3a812b6b1369327daf440d51bf896f55a..d05973ad1d6b0f17d31e1434547cc9bf9fa3732e 100644 GIT binary patch delta 101 zcmdlziSg|u#tkMCjBJxlB@zTIzr++V+D@3q00y4clY1q!85eF;{2{*4phA4IqsK2J tW1y5K5G#VP!0S&}mCCZ7p7Yl5|6JE-d6$8Kan`2EuM>chAm9bWdH_Y*9xVU> delta 33 pcmaDmiE;NN#tkMCjI5JQB@!mDl~88v+^G0Ne51hx@yU)JzW~rM43z)? diff --git a/.gradle/8.4/fileHashes/fileHashes.lock b/.gradle/8.4/fileHashes/fileHashes.lock index 1c30060c68f25f0d7932ded2a89920790fab263b..15904d6dd9fe3772c3e81054db8c9e94cd297e09 100644 GIT binary patch literal 17 UcmZR!Ha(!f_C&^91_%%W04_}f?*IS* literal 17 UcmZR!Ha(!f_C&^91_Px# diff --git a/.gradle/8.4/fileHashes/resourceHashesCache.bin b/.gradle/8.4/fileHashes/resourceHashesCache.bin index ea5df033274bc18e5071c03b8093eb650e9c064a..a47e42febbf6837b9beeaaec89b3e35b7157d865 100644 GIT binary patch delta 118 zcmaDnfw6TW;|3E6Tc#I@b%!U&l@&37frsOND6o7Oa5K4{M-M8V3=@aZKyk;7ia*3R q8r%?{?C7DSWDFFM1Y$uD7MS^X!9zj&7{29w{anZH7hVSn$^ih988X5E delta 62 zcmZpj$oO~y;|3E6RmK;Ib%!U&l@&37frsONGyoLV+^G0Ne51ij@refPlN~*j0G`Vp AH~;_u diff --git a/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/.gradle/buildOutputCleanup/buildOutputCleanup.lock index c05759e532fef45e5edb5549336c92ad88247d51..0394d6c40afe1b17d4619e41398767cb22ef2981 100644 GIT binary patch literal 17 UcmZQ(iAnn(@h$2(0|ev%05dQI!~g&Q literal 17 UcmZQ(iAnn(@h$2(0|aOS05a_afdBvi diff --git a/.gradle/buildOutputCleanup/cache.properties b/.gradle/buildOutputCleanup/cache.properties index 674ef95..bb2921d 100644 --- a/.gradle/buildOutputCleanup/cache.properties +++ b/.gradle/buildOutputCleanup/cache.properties @@ -1,2 +1,2 @@ -#Tue Apr 09 14:58:45 MSK 2024 -gradle.version=8.4 +#Mon Nov 24 00:28:32 MSK 2025 +gradle.version=8.14 diff --git a/.gradle/buildOutputCleanup/outputFiles.bin b/.gradle/buildOutputCleanup/outputFiles.bin index 17f893cdb516becb510d0447cadcc568e6cf9056..1ec9c4f044397e3f46e3802759919f834848e8ef 100644 GIT binary patch literal 18857 zcmeI&O-NKx7=YoU8>`W=!c0XPG!wOmL<$*H@MkfDkP8{1owE>s3RI+~McWpF1wqj; zS>d9cE(*ug3UPL#iRKS3%!MX0(8fgsBcyk{-=~NhaUmG*g**4~&N=6Dp6yJHF&XE# zGYwfSSN&5T2=hVpu->3h^ms*#%T>FnFpT=5V6gs6}*FG2ayuhiZd{W3fJ>zzaTfc8h-iIwA3KS%8DeXU)e#w3W5J%djK5I_I{1Q0*~ z0R#|0009ILKmY**5I_I{1Q0*~0R#|0009ILKmY**5I_I{1Q0*~0R#|00D;ph5dZW0 jS?f-a+c*!Dcs{9Oz3Le6I=t#A<#^{hyz^1^*+D$)3R01p1KQppP+uj5ZWU~CNlZ_YU&=xC4c!j#~|xw1@%siyD|L3 zY2|ZnQuh{bX;bM(&hkE+`h(L&C2tFQ!hB8!b>HzbRd&VQRMtfs^_~wtOEi0DKkotR z{Z6koI99oh_h_OAJUcopQ_op9S*gEvWEl}}Z>Bz8X4rS%GnLExI_gnndDr*R@h5yvJN2B$r?OmX^+(pthp6iX)4?YF@Fd?~ zNqzaoPqp^2`W5eMsIORJDD4h5-{x})6a83SuB&U|DC^>7>c-aVN>|vL#`l*|U;8e{ zsVd4p%;#^WE=JNr^ + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..a55e7a1 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 61a9130..b86273d 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index f9163b4..1cbcde6 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -1,9 +1,11 @@ + \ No newline at end of file diff --git a/.idea/git_toolbox_prj.xml b/.idea/git_toolbox_prj.xml new file mode 100644 index 0000000..02b915b --- /dev/null +++ b/.idea/git_toolbox_prj.xml @@ -0,0 +1,15 @@ + + + + + + + \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index cc54ee4..414cf32 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,9 @@ plugins { - kotlin("jvm") version "1.9.20" + val kotlinVersion = "1.9.20" + kotlin("jvm") version kotlinVersion + kotlin("plugin.spring") version kotlinVersion + + id("org.springframework.boot") version "3.4.5" application } @@ -14,6 +18,8 @@ dependencies { // implementation("javax.jms:jms-api:2.0.1") // implementation("org.apache.activemq:activemq-broker:6.1.1") testImplementation(kotlin("test")) + testImplementation("org.junit.jupiter:junit-jupiter:5.11.0") + testImplementation("org.assertj:assertj-core:3.24.2") } tasks { diff --git a/src/main/kotlin/org/quicklybly/dumbmq/producer/splitter/TextSplitter.kt b/src/main/kotlin/org/quicklybly/dumbmq/producer/splitter/TextSplitter.kt new file mode 100644 index 0000000..e4c2cee --- /dev/null +++ b/src/main/kotlin/org/quicklybly/dumbmq/producer/splitter/TextSplitter.kt @@ -0,0 +1,7 @@ +package org.quicklybly.dumbmq.producer.splitter + +import java.io.BufferedReader + +abstract class TextSplitter( + protected val reader: BufferedReader, +) : Iterator diff --git a/src/main/kotlin/org/quicklybly/dumbmq/producer/splitter/impl/SentenceTextSplitter.kt b/src/main/kotlin/org/quicklybly/dumbmq/producer/splitter/impl/SentenceTextSplitter.kt new file mode 100644 index 0000000..155d62d --- /dev/null +++ b/src/main/kotlin/org/quicklybly/dumbmq/producer/splitter/impl/SentenceTextSplitter.kt @@ -0,0 +1,176 @@ +package org.quicklybly.dumbmq.producer.splitter.impl + +import org.quicklybly.dumbmq.producer.splitter.TextSplitter +import java.io.BufferedReader + +class SentenceTextSplitter( + reader: BufferedReader, + private val configuration: Configuration, +) : TextSplitter(reader) { + + /** + * Configuration for sentence splitting + * @param maxChunkSize maximum size of chunk in characters (must be > 0) + * @param sentenceTerminators set of sentence ending characters + * @param bufferSize size of read buffer for performance optimization + */ + data class Configuration( + val maxChunkSize: Int, + val sentenceTerminators: Set = setOf('.', '!', '?'), + val bufferSize: Int = 8192 + ) { + init { + require(maxChunkSize > 0) { + "maxChunkSize must be positive, got: $maxChunkSize" + } + require(bufferSize > 0) { + "bufferSize must be positive, got: $bufferSize" + } + require(sentenceTerminators.isNotEmpty()) { + "sentenceTerminators must not be empty" + } + } + } + + private val textBuffer = StringBuilder() + private var nextSentence: String? = null + private var readerExhausted = false + private val readBuffer = CharArray(configuration.bufferSize) + + override fun hasNext(): Boolean { + if (nextSentence != null) { + return true + } + + if (!readerExhausted || textBuffer.isNotEmpty()) { + nextSentence = extractNextSentence() + } + + return nextSentence != null + } + + override fun next(): String { + if (nextSentence == null && !hasNext()) { + throw NoSuchElementException() + } + + return nextSentence!!.also { + nextSentence = null + } + } + + private fun extractNextSentence(): String? { + while (!readerExhausted || textBuffer.isNotEmpty()) { + val sentenceEndIndex = findSentenceEnd() + + if (sentenceEndIndex != -1) { + val sentence = textBuffer.substring(0, sentenceEndIndex + 1) + textBuffer.delete(0, sentenceEndIndex + 1) + + trimLeadingWhitespace() + + return sentence.trim().takeIf { it.isNotEmpty() } + } + + if (textBuffer.length >= configuration.maxChunkSize) { + return extractMaxChunk() + } + + if (!readerExhausted) { + readMoreData() + } else { + return if (textBuffer.isNotEmpty()) { + val trimmed = textBuffer.toString().trim() + textBuffer.clear() + trimmed.takeIf { it.isNotEmpty() } + } else { + null + } + } + } + + return null + } + + /** + * Находит индекс конца предложения в буфере + */ + private fun findSentenceEnd(): Int { + for (i in textBuffer.indices) { + val char = textBuffer[i] + + if (char in configuration.sentenceTerminators) { + if (!isLikelyAbbreviation(i)) { + return i + } + } + } + + return -1 + } + + private fun isLikelyAbbreviation(terminatorIndex: Int): Boolean { + if (textBuffer[terminatorIndex] != '.') { + return false + } + + if (terminatorIndex > 0 && terminatorIndex < textBuffer.length - 1) { + val prevChar = textBuffer[terminatorIndex - 1] + val nextChar = textBuffer[terminatorIndex + 1] + + if (prevChar.isDigit() && nextChar.isDigit()) { + return true + } + + if (terminatorIndex >= 2) { + val prevPrevChar = textBuffer[terminatorIndex - 2] + if (prevChar.isLetter() && + prevChar.isUpperCase() && + (prevPrevChar.isWhitespace() || terminatorIndex == 2) + ) { + return true + } + } + } + + return false + } + + private fun extractMaxChunk(): String { + var cutIndex = configuration.maxChunkSize + + if (cutIndex < textBuffer.length) { + for (i in cutIndex - 1 downTo maxOf(0, cutIndex - 100)) { + if (textBuffer[i].isWhitespace()) { + cutIndex = i + 1 + break + } + } + } else { + cutIndex = textBuffer.length + } + + val chunk = textBuffer.substring(0, cutIndex) + textBuffer.delete(0, cutIndex) + trimLeadingWhitespace() + + return chunk.trim() + } + + + private fun readMoreData() { + val charsRead = reader.read(readBuffer) + + if (charsRead > 0) { + textBuffer.appendRange(readBuffer, 0, charsRead) + } else { + readerExhausted = true + } + } + + private fun trimLeadingWhitespace() { + while (textBuffer.isNotEmpty() && textBuffer[0].isWhitespace()) { + textBuffer.deleteCharAt(0) + } + } +} diff --git a/src/test/kotlin/org/quicklybly/dumbmq/producer/splitter/impl/SentenceTextSplitterTest.kt b/src/test/kotlin/org/quicklybly/dumbmq/producer/splitter/impl/SentenceTextSplitterTest.kt new file mode 100644 index 0000000..c608604 --- /dev/null +++ b/src/test/kotlin/org/quicklybly/dumbmq/producer/splitter/impl/SentenceTextSplitterTest.kt @@ -0,0 +1,51 @@ +package org.quicklybly.dumbmq.producer.splitter.impl + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.io.BufferedReader +import java.io.StringReader + +class SentenceTextSplitterTest { + + @Test + fun `should split simple sentences correctly`() { + val text = "First sentence. Second one! Third?" + val reader = BufferedReader(StringReader(text)) + val config = SentenceTextSplitter.Configuration( + maxChunkSize = 100, + sentenceTerminators = setOf('.', '!', '?') + ) + val splitter = SentenceTextSplitter(reader, config) + + assertThat("First sentence.").isEqualTo(splitter.next()) + assertThat("Second one!").isEqualTo(splitter.next()) + assertThat("Third?").isEqualTo(splitter.next()) + assertThat(splitter.hasNext()).isFalse() + } + + @Test + fun `should handle maxChunkSize correctly`() { + val text = "This is a very long sentence without terminators" + val reader = BufferedReader(StringReader(text)) + val config = SentenceTextSplitter.Configuration( + maxChunkSize = 20, + sentenceTerminators = setOf('.') + ) + val splitter = SentenceTextSplitter(reader, config) + + val chunk = splitter.next() + assertThat(chunk.length).isLessThanOrEqualTo(20) + assertThat(splitter.hasNext()).isTrue() + } + + @Test + fun `should throw on invalid configuration`() { + assertThrows { + SentenceTextSplitter.Configuration( + maxChunkSize = 0, + sentenceTerminators = setOf('.') + ) + } + } +} \ No newline at end of file From aaf9ea4f3b35ac2a8b84b6616d2b18d0bab94157 Mon Sep 17 00:00:00 2001 From: "ar.r.lysenko" Date: Tue, 25 Nov 2025 01:26:38 +0300 Subject: [PATCH 05/15] add rabbit docker file --- docker/compose.yaml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 docker/compose.yaml diff --git a/docker/compose.yaml b/docker/compose.yaml new file mode 100644 index 0000000..5e9df62 --- /dev/null +++ b/docker/compose.yaml @@ -0,0 +1,16 @@ +version: '3.8' + +services: + rabbitmq: + image: rabbitmq:3.12-management + environment: + RABBITMQ_DEFAULT_USER: admin + RABBITMQ_DEFAULT_PASS: admin + ports: + - "5672:5672" + - "15672:15672" + volumes: + - rabbitmq_data:/var/lib/rabbitmq + +volumes: + rabbitmq_data: From af8cd2b1f99869f5e057661ac8377987b357f309 Mon Sep 17 00:00:00 2001 From: "ar.r.lysenko" Date: Tue, 25 Nov 2025 02:52:17 +0300 Subject: [PATCH 06/15] add gitignore --- .gitignore | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..269be06 --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Kotlin ### +.kotlin +/src/main/resources/config/application-local.yaml From 92e08442304ce2914be121123cb291291a3f97a1 Mon Sep 17 00:00:00 2001 From: "ar.r.lysenko" Date: Tue, 25 Nov 2025 02:52:34 +0300 Subject: [PATCH 07/15] add spring --- build.gradle.kts | 14 +- .../quicklybly/dumbmq/DumbMqApplication.kt | 11 ++ src/main/kotlin/org/quicklybly/dumbmq/Main.kt | 5 - .../ObjectMapperConfiguration.kt | 27 ++++ .../configuration/RabbitConfiguration.kt | 142 ++++++++++++++++++ src/main/resources/application.yaml | 16 ++ 6 files changed, 208 insertions(+), 7 deletions(-) create mode 100644 src/main/kotlin/org/quicklybly/dumbmq/DumbMqApplication.kt delete mode 100644 src/main/kotlin/org/quicklybly/dumbmq/Main.kt create mode 100644 src/main/kotlin/org/quicklybly/dumbmq/configuration/ObjectMapperConfiguration.kt create mode 100644 src/main/kotlin/org/quicklybly/dumbmq/configuration/RabbitConfiguration.kt create mode 100644 src/main/resources/application.yaml diff --git a/build.gradle.kts b/build.gradle.kts index 414cf32..5a5150d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,6 +4,7 @@ plugins { kotlin("plugin.spring") version kotlinVersion id("org.springframework.boot") version "3.4.5" + id("io.spring.dependency-management") version "1.1.0" application } @@ -15,8 +16,17 @@ repositories { } dependencies { -// implementation("javax.jms:jms-api:2.0.1") -// implementation("org.apache.activemq:activemq-broker:6.1.1") + implementation("org.springframework.boot:spring-boot-starter") + implementation("org.springframework.boot:spring-boot-starter-web") + + implementation("org.springframework.boot:spring-boot-starter-amqp") + + implementation("io.github.microutils:kotlin-logging-jvm:3.0.5") + + implementation("org.jetbrains.kotlin:kotlin-reflect") + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + testImplementation(kotlin("test")) testImplementation("org.junit.jupiter:junit-jupiter:5.11.0") testImplementation("org.assertj:assertj-core:3.24.2") diff --git a/src/main/kotlin/org/quicklybly/dumbmq/DumbMqApplication.kt b/src/main/kotlin/org/quicklybly/dumbmq/DumbMqApplication.kt new file mode 100644 index 0000000..247520b --- /dev/null +++ b/src/main/kotlin/org/quicklybly/dumbmq/DumbMqApplication.kt @@ -0,0 +1,11 @@ +package org.quicklybly.dumbmq + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication + +@SpringBootApplication +class DumbMqApplication + +fun main(args: Array) { + runApplication(*args) +} diff --git a/src/main/kotlin/org/quicklybly/dumbmq/Main.kt b/src/main/kotlin/org/quicklybly/dumbmq/Main.kt deleted file mode 100644 index 7673e82..0000000 --- a/src/main/kotlin/org/quicklybly/dumbmq/Main.kt +++ /dev/null @@ -1,5 +0,0 @@ -package org.quicklybly.dumbmq - -fun main() { - println("Hello world!") -} diff --git a/src/main/kotlin/org/quicklybly/dumbmq/configuration/ObjectMapperConfiguration.kt b/src/main/kotlin/org/quicklybly/dumbmq/configuration/ObjectMapperConfiguration.kt new file mode 100644 index 0000000..a4df85a --- /dev/null +++ b/src/main/kotlin/org/quicklybly/dumbmq/configuration/ObjectMapperConfiguration.kt @@ -0,0 +1,27 @@ +package org.quicklybly.dumbmq.configuration + +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.paramnames.ParameterNamesModule +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class ObjectMapperConfiguration { + + @Bean + fun objectMapper(): ObjectMapper = jacksonObjectMapper() + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .setSerializationInclusion(JsonInclude.Include.NON_NULL) + .registerModules( + Jdk8Module(), + JavaTimeModule(), + ParameterNamesModule(), + ) +} diff --git a/src/main/kotlin/org/quicklybly/dumbmq/configuration/RabbitConfiguration.kt b/src/main/kotlin/org/quicklybly/dumbmq/configuration/RabbitConfiguration.kt new file mode 100644 index 0000000..426900d --- /dev/null +++ b/src/main/kotlin/org/quicklybly/dumbmq/configuration/RabbitConfiguration.kt @@ -0,0 +1,142 @@ +package org.quicklybly.dumbmq.configuration + +import com.fasterxml.jackson.databind.ObjectMapper +import mu.KotlinLogging +import org.springframework.amqp.core.AcknowledgeMode +import org.springframework.amqp.core.Binding +import org.springframework.amqp.core.BindingBuilder +import org.springframework.amqp.core.DirectExchange +import org.springframework.amqp.core.FanoutExchange +import org.springframework.amqp.core.Queue +import org.springframework.amqp.rabbit.annotation.EnableRabbit +import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory +import org.springframework.amqp.rabbit.connection.ConnectionFactory +import org.springframework.amqp.rabbit.core.RabbitTemplate +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter +import org.springframework.amqp.support.converter.MessageConverter +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +private val logger = KotlinLogging.logger {} + +object RabbitMqConstants { + const val TASK_EXCHANGE = "task.exchange" + const val WORKER_CONTROL_EXCHANGE = "worker.control.exchange" + const val AGGREGATOR_CONTROL_EXCHANGE = "aggregator.control.exchange" + + const val TASKS_QUEUE = "tasks.queue" + const val WORKER_CONTROL_QUEUE = "worker.control.queue" + const val AGGREGATOR_CONTROL_QUEUE = "aggregator.control.queue" + + const val TASK_ROUTING_KEY = "task" +} + +@Configuration +@EnableRabbit +class RabbitConfiguration { + + // Exchanges + @Bean(RabbitMqConstants.TASK_EXCHANGE) + fun taskExchange(): DirectExchange { + return DirectExchange(RabbitMqConstants.TASK_EXCHANGE) + } + + @Bean(RabbitMqConstants.WORKER_CONTROL_EXCHANGE) + fun workerControlExchange(): FanoutExchange { + return FanoutExchange(RabbitMqConstants.WORKER_CONTROL_EXCHANGE) + } + + @Bean(RabbitMqConstants.AGGREGATOR_CONTROL_EXCHANGE) + fun aggregatorControlExchange(): FanoutExchange { + return FanoutExchange(RabbitMqConstants.AGGREGATOR_CONTROL_EXCHANGE) + } + + // Queues + @Bean(RabbitMqConstants.TASKS_QUEUE) + fun tasksQueue(): Queue { + return Queue(RabbitMqConstants.TASKS_QUEUE) + } + + @Bean(RabbitMqConstants.WORKER_CONTROL_QUEUE) + fun workerControlQueue(): Queue { + return Queue(RabbitMqConstants.WORKER_CONTROL_QUEUE) + } + + @Bean(RabbitMqConstants.AGGREGATOR_CONTROL_QUEUE) + fun aggregatorControlQueue(): Queue { + return Queue(RabbitMqConstants.AGGREGATOR_CONTROL_QUEUE) + } + + // Bindings + @Bean + fun taskBinding( + @Qualifier(RabbitMqConstants.TASK_EXCHANGE) taskExchange: DirectExchange, + @Qualifier(RabbitMqConstants.TASKS_QUEUE) tasksQueue: Queue, + ): Binding { + return BindingBuilder + .bind(tasksQueue) + .to(taskExchange) + .with(RabbitMqConstants.TASK_ROUTING_KEY) + } + + @Bean + fun workerControlBinding( + @Qualifier(RabbitMqConstants.WORKER_CONTROL_EXCHANGE) workerControlExchange: FanoutExchange, + @Qualifier(RabbitMqConstants.WORKER_CONTROL_QUEUE) workerControlQueue: Queue, + ): Binding { + return BindingBuilder + .bind(workerControlQueue) + .to(workerControlExchange) + } + + @Bean + fun aggregatorControlBinding( + @Qualifier(RabbitMqConstants.AGGREGATOR_CONTROL_EXCHANGE) aggregatorControlExchange: FanoutExchange, + @Qualifier(RabbitMqConstants.AGGREGATOR_CONTROL_QUEUE) aggregatorControlQueue: Queue, + ): Binding { + return BindingBuilder + .bind(aggregatorControlQueue) + .to(aggregatorControlExchange) + } + + @Bean + fun messageConverter(objectMapper: ObjectMapper): MessageConverter { + return Jackson2JsonMessageConverter(objectMapper) + } + + @Bean + fun rabbitListenerContainerFactory( + connectionFactory: ConnectionFactory, + messageConverter: MessageConverter + ): SimpleRabbitListenerContainerFactory { + return SimpleRabbitListenerContainerFactory().apply { + setConnectionFactory(connectionFactory) + setMessageConverter(messageConverter) + setAcknowledgeMode(AcknowledgeMode.MANUAL) + setPrefetchCount(1) + setDefaultRequeueRejected(false) + } + } + + @Bean + fun rabbitTemplate( + connectionFactory: ConnectionFactory, + messageConverter: MessageConverter + ): RabbitTemplate { + return RabbitTemplate(connectionFactory).apply { + this.messageConverter = messageConverter + setMandatory(true) + + setConfirmCallback { _, ack, cause -> + if (!ack) { + logger.error { "Message not delivered: $cause" } + } + } + + setReturnsCallback { returned -> + logger.error { "Message returned: ${returned.message}" } + } + } + } +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml new file mode 100644 index 0000000..a41b8e9 --- /dev/null +++ b/src/main/resources/application.yaml @@ -0,0 +1,16 @@ +spring: + application: + name: dumbmq + output: + ansi: + enabled: always + rabbitmq: + host: localhost + port: 5672 + username: admin + password: admin + +server: + port: 8080 + servlet: + context-path: /${spring.application.name} From 2fe2d290590312dae2c7e7c31dd33a75e9c42ba9 Mon Sep 17 00:00:00 2001 From: "ar.r.lysenko" Date: Tue, 25 Nov 2025 03:17:08 +0300 Subject: [PATCH 08/15] add jobs api --- build.gradle.kts | 1 + .../quicklybly/dumbmq/api/ControllerAdvice.kt | 27 ++++++++ .../quicklybly/dumbmq/api/JobController.kt | 20 ++++++ .../quicklybly/dumbmq/api/dto/JobRequest.kt | 8 +++ .../quicklybly/dumbmq/api/dto/JobResponse.kt | 7 ++ .../api/dto/validation/ValidFileUrls.kt | 13 ++++ .../dto/validation/ValidFileUrlsValidator.kt | 64 +++++++++++++++++++ 7 files changed, 140 insertions(+) create mode 100644 src/main/kotlin/org/quicklybly/dumbmq/api/ControllerAdvice.kt create mode 100644 src/main/kotlin/org/quicklybly/dumbmq/api/JobController.kt create mode 100644 src/main/kotlin/org/quicklybly/dumbmq/api/dto/JobRequest.kt create mode 100644 src/main/kotlin/org/quicklybly/dumbmq/api/dto/JobResponse.kt create mode 100644 src/main/kotlin/org/quicklybly/dumbmq/api/dto/validation/ValidFileUrls.kt create mode 100644 src/main/kotlin/org/quicklybly/dumbmq/api/dto/validation/ValidFileUrlsValidator.kt diff --git a/build.gradle.kts b/build.gradle.kts index 5a5150d..0ea90dc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -18,6 +18,7 @@ repositories { dependencies { implementation("org.springframework.boot:spring-boot-starter") implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-validation") implementation("org.springframework.boot:spring-boot-starter-amqp") diff --git a/src/main/kotlin/org/quicklybly/dumbmq/api/ControllerAdvice.kt b/src/main/kotlin/org/quicklybly/dumbmq/api/ControllerAdvice.kt new file mode 100644 index 0000000..d4be03f --- /dev/null +++ b/src/main/kotlin/org/quicklybly/dumbmq/api/ControllerAdvice.kt @@ -0,0 +1,27 @@ +package org.quicklybly.dumbmq.api + +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpStatusCode +import org.springframework.http.ProblemDetail +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.MethodArgumentNotValidException +import org.springframework.web.bind.annotation.RestControllerAdvice +import org.springframework.web.context.request.WebRequest +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler + +@RestControllerAdvice +class ControllerAdvice : ResponseEntityExceptionHandler() { + + override fun handleMethodArgumentNotValid( + ex: MethodArgumentNotValidException, + headers: HttpHeaders, + status: HttpStatusCode, + request: WebRequest + ): ResponseEntity? { + val problemDetail = ProblemDetail.forStatus(status) + problemDetail.detail = "Validation failed for object='${ex.objectName}'" + problemDetail.setProperty("errors", ex.bindingResult.allErrors.map { it.defaultMessage }) + + return ResponseEntity(problemDetail, headers, status) + } +} diff --git a/src/main/kotlin/org/quicklybly/dumbmq/api/JobController.kt b/src/main/kotlin/org/quicklybly/dumbmq/api/JobController.kt new file mode 100644 index 0000000..2ae659f --- /dev/null +++ b/src/main/kotlin/org/quicklybly/dumbmq/api/JobController.kt @@ -0,0 +1,20 @@ +package org.quicklybly.dumbmq.api + +import jakarta.validation.Valid +import org.quicklybly.dumbmq.api.dto.JobRequest +import org.quicklybly.dumbmq.api.dto.JobResponse +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController +import java.util.UUID + +@RestController +@RequestMapping("/jobs") +class JobController { + + @PostMapping + fun createJob(@Valid @RequestBody request: JobRequest): JobResponse { + return JobResponse(UUID.randomUUID()) + } +} diff --git a/src/main/kotlin/org/quicklybly/dumbmq/api/dto/JobRequest.kt b/src/main/kotlin/org/quicklybly/dumbmq/api/dto/JobRequest.kt new file mode 100644 index 0000000..a6d11ba --- /dev/null +++ b/src/main/kotlin/org/quicklybly/dumbmq/api/dto/JobRequest.kt @@ -0,0 +1,8 @@ +package org.quicklybly.dumbmq.api.dto + +import org.quicklybly.dumbmq.api.dto.validation.ValidFileUrls + +data class JobRequest( + @field:ValidFileUrls + val fileUrls: List, +) diff --git a/src/main/kotlin/org/quicklybly/dumbmq/api/dto/JobResponse.kt b/src/main/kotlin/org/quicklybly/dumbmq/api/dto/JobResponse.kt new file mode 100644 index 0000000..db78fa0 --- /dev/null +++ b/src/main/kotlin/org/quicklybly/dumbmq/api/dto/JobResponse.kt @@ -0,0 +1,7 @@ +package org.quicklybly.dumbmq.api.dto + +import java.util.UUID + +data class JobResponse( + val jobId: UUID, +) diff --git a/src/main/kotlin/org/quicklybly/dumbmq/api/dto/validation/ValidFileUrls.kt b/src/main/kotlin/org/quicklybly/dumbmq/api/dto/validation/ValidFileUrls.kt new file mode 100644 index 0000000..86a1a3c --- /dev/null +++ b/src/main/kotlin/org/quicklybly/dumbmq/api/dto/validation/ValidFileUrls.kt @@ -0,0 +1,13 @@ +package org.quicklybly.dumbmq.api.dto.validation + +import jakarta.validation.Constraint +import kotlin.reflect.KClass + +@Target(AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +@Constraint(validatedBy = [ValidFileUrlsValidator::class]) +annotation class ValidFileUrls( + val message: String = "Each URL must be a valid and readable URL", + val groups: Array> = [], + val payload: Array> = [], +) diff --git a/src/main/kotlin/org/quicklybly/dumbmq/api/dto/validation/ValidFileUrlsValidator.kt b/src/main/kotlin/org/quicklybly/dumbmq/api/dto/validation/ValidFileUrlsValidator.kt new file mode 100644 index 0000000..68f412a --- /dev/null +++ b/src/main/kotlin/org/quicklybly/dumbmq/api/dto/validation/ValidFileUrlsValidator.kt @@ -0,0 +1,64 @@ +package org.quicklybly.dumbmq.api.dto.validation + +import jakarta.validation.ConstraintValidator +import jakarta.validation.ConstraintValidatorContext +import java.io.File +import java.net.URI + +class ValidFileUrlsValidator : ConstraintValidator> { + + companion object { + private val supportedSchemes = listOf("file") + } + + override fun initialize(constraintAnnotation: ValidFileUrls) {} + + override fun isValid( + value: List?, + context: ConstraintValidatorContext + ): Boolean { + if (value == null || value.isEmpty()) return true + + for ((index, url) in value.withIndex()) { + val result = validateFileUrl(url, context, index) + if (!result) return false + } + + return true + } + + private fun validateFileUrl( + url: String, + context: ConstraintValidatorContext, + index: Int + ): Boolean { + return try { + val uri = URI(url) + if (uri.scheme !in supportedSchemes) { + context.addMessage("[$index] Unsupported URL scheme: ${uri.scheme}, use $supportedSchemes") + return false + } + + val file = File(uri) + if (!file.exists()) { + context.addMessage("[$index] File does not exist: $url") + return false + } + + if (!file.canRead()) { + context.addMessage("[$index] File is not readable: $url") + return false + } + + true + } catch (e: Exception) { + context.addMessage("[$index] Invalid URL: $url, error: ${e.message}") + false + } + } + + private fun ConstraintValidatorContext.addMessage(message: String) { + this.disableDefaultConstraintViolation() + this.buildConstraintViolationWithTemplate(message).addConstraintViolation() + } +} From 5eb28e2bdee90c2e5f7fcd3fd73bb64d5bdfd049 Mon Sep 17 00:00:00 2001 From: "ar.r.lysenko" Date: Tue, 25 Nov 2025 03:46:21 +0300 Subject: [PATCH 09/15] add JobService --- inputs/file.txt | 11 ++++++++ .../quicklybly/dumbmq/api/JobController.kt | 12 ++++++-- .../quicklybly/dumbmq/service/JobService.kt | 28 +++++++++++++++++++ 3 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 inputs/file.txt create mode 100644 src/main/kotlin/org/quicklybly/dumbmq/service/JobService.kt diff --git a/inputs/file.txt b/inputs/file.txt new file mode 100644 index 0000000..cea2388 --- /dev/null +++ b/inputs/file.txt @@ -0,0 +1,11 @@ +Michael and Sarah decided to visit their friends, David and Emily, for the weekend. The plan was to meet at the old train station. David asked his sister, Jennifer, to join them. Emily invited her colleague, Robert, as well. Robert brought his dog, a cheerful corgi named Max. + +On the train, they ran into an old teacher of theirs, Mr. Thompson. Mr. Thompson was traveling to see his granddaughter, Lily. He spoke fondly of his neighbor, Mrs. Higgins. Mrs. Higgins was a retired librarian who loved gardening. Her cat, Oliver, was known throughout the neighborhood. + +The group arrived in the town and was greeted by David's cousin, Frank. Frank drove them to a local cafe owned by a woman named Maria. Maria introduced them to her business partner, James. James recommended the special dessert, a recipe from his grandmother, Agnes. Everyone agreed it was delicious. + +After lunch, they walked through the park. A man named George was playing chess with his friend, Henry. Sarah recognized George from her university days. She had studied with his brother, Thomas. Thomas now worked for a company led by a CEO named Arthur Finch. + +Later, they attended a small concert in the town square. The performer was a talented singer named Chloe. Her guitarist was a man called Leo. Chloe dedicated a song to her parents, Richard and Elizabeth. The crowd, including a young girl named Sophia, cheered loudly. + +That evening, they shared stories about their families. Michael talked about his aunt, Patricia. Sarah mentioned her nephew, Noah. David showed a picture of his new baby cousin, Evelyn. It was a wonderful day full of friendly conversations. diff --git a/src/main/kotlin/org/quicklybly/dumbmq/api/JobController.kt b/src/main/kotlin/org/quicklybly/dumbmq/api/JobController.kt index 2ae659f..ffdc37c 100644 --- a/src/main/kotlin/org/quicklybly/dumbmq/api/JobController.kt +++ b/src/main/kotlin/org/quicklybly/dumbmq/api/JobController.kt @@ -3,6 +3,9 @@ package org.quicklybly.dumbmq.api import jakarta.validation.Valid import org.quicklybly.dumbmq.api.dto.JobRequest import org.quicklybly.dumbmq.api.dto.JobResponse +import org.quicklybly.dumbmq.service.JobService +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping @@ -11,10 +14,15 @@ import java.util.UUID @RestController @RequestMapping("/jobs") -class JobController { +class JobController(private val jobService: JobService) { @PostMapping fun createJob(@Valid @RequestBody request: JobRequest): JobResponse { - return JobResponse(UUID.randomUUID()) + return jobService.createJob(request) + } + + @GetMapping("/{jobId}") + fun getReport(@PathVariable jobId: UUID) { + } } diff --git a/src/main/kotlin/org/quicklybly/dumbmq/service/JobService.kt b/src/main/kotlin/org/quicklybly/dumbmq/service/JobService.kt new file mode 100644 index 0000000..e3adba8 --- /dev/null +++ b/src/main/kotlin/org/quicklybly/dumbmq/service/JobService.kt @@ -0,0 +1,28 @@ +package org.quicklybly.dumbmq.service + +import org.quicklybly.dumbmq.api.dto.JobRequest +import org.quicklybly.dumbmq.api.dto.JobResponse +import org.quicklybly.dumbmq.producer.splitter.ProducerService +import org.springframework.stereotype.Service +import java.util.UUID +import java.util.concurrent.ConcurrentHashMap + +@Service +class JobService(private val producerService: ProducerService) { + + private val jobsInProgress = ConcurrentHashMap.newKeySet() + + fun createJob(request: JobRequest): JobResponse { + val jobId = UUID.randomUUID() + jobsInProgress.add(jobId) + return JobResponse(jobId) + } + + fun completeJob(jobId: UUID) { + jobsInProgress.remove(jobId) + } + + fun getReport(jobId: UUID) { + TODO() + } +} From f52f6523a7e869f5e8c934eafd3cfe3032f65818 Mon Sep 17 00:00:00 2001 From: "ar.r.lysenko" Date: Tue, 25 Nov 2025 10:14:59 +0300 Subject: [PATCH 10/15] add job initialization --- .../buildOutputCleanup.lock | Bin 17 -> 17 bytes .gradle/buildOutputCleanup/outputFiles.bin | Bin 19037 -> 19073 bytes .../dumbmq/common/dto/JobInitDto.kt | 6 +++ .../configuration/RabbitConfiguration.kt | 36 +++++++++++++++++- .../producer/splitter/ProducerService.kt | 31 +++++++++++++++ .../quicklybly/dumbmq/service/JobService.kt | 34 +++++++++++++---- .../quicklybly/dumbmq/service/JobStorage.kt | 22 +++++++++++ 7 files changed, 120 insertions(+), 9 deletions(-) create mode 100644 src/main/kotlin/org/quicklybly/dumbmq/common/dto/JobInitDto.kt create mode 100644 src/main/kotlin/org/quicklybly/dumbmq/producer/splitter/ProducerService.kt create mode 100644 src/main/kotlin/org/quicklybly/dumbmq/service/JobStorage.kt diff --git a/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/.gradle/buildOutputCleanup/buildOutputCleanup.lock index c0f401d5bda89dcdbd648f6e0ba06a5b2a1d1a91..2dd4749f2728948d6b5c06929bd9e90cf218491a 100644 GIT binary patch literal 17 UcmZQ(iAnn(@h$2(0|fj605iS?QUCw| literal 17 UcmZQ(iAnn(@h$2(0|e9n05d=Y)Bpeg diff --git a/.gradle/buildOutputCleanup/outputFiles.bin b/.gradle/buildOutputCleanup/outputFiles.bin index 3c0f738d98afc6d515eff81f0ab0aca362050b3d..0283b4c5e40253fdb3421fdde554aa805e07b7ab 100644 GIT binary patch delta 79 zcmcaRg|Tre;|3E6Muo|y5(xt5v?5g%RbB2ffPq*3=3a>cMge1%Emf@5 blwxFJ7Tl=#LwutFhv;TU4;iM(`#n?uk3|-s delta 41 xcmZpi%6NAQ;|3E6M!CtR5(%5vN@Or@)|C=sWMX=_QSpcPMgt4c&5j;2OaLOG4Iuyk diff --git a/src/main/kotlin/org/quicklybly/dumbmq/common/dto/JobInitDto.kt b/src/main/kotlin/org/quicklybly/dumbmq/common/dto/JobInitDto.kt new file mode 100644 index 0000000..a6235a5 --- /dev/null +++ b/src/main/kotlin/org/quicklybly/dumbmq/common/dto/JobInitDto.kt @@ -0,0 +1,6 @@ +package org.quicklybly.dumbmq.common.dto + +data class JobInitDto( + val jobId: String, + val fileUrls: List, +) diff --git a/src/main/kotlin/org/quicklybly/dumbmq/configuration/RabbitConfiguration.kt b/src/main/kotlin/org/quicklybly/dumbmq/configuration/RabbitConfiguration.kt index 426900d..5cbfe46 100644 --- a/src/main/kotlin/org/quicklybly/dumbmq/configuration/RabbitConfiguration.kt +++ b/src/main/kotlin/org/quicklybly/dumbmq/configuration/RabbitConfiguration.kt @@ -2,6 +2,7 @@ package org.quicklybly.dumbmq.configuration import com.fasterxml.jackson.databind.ObjectMapper import mu.KotlinLogging +import org.quicklybly.dumbmq.service.JobStorage import org.springframework.amqp.core.AcknowledgeMode import org.springframework.amqp.core.Binding import org.springframework.amqp.core.BindingBuilder @@ -17,18 +18,22 @@ import org.springframework.amqp.support.converter.MessageConverter import org.springframework.beans.factory.annotation.Qualifier import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import java.util.UUID private val logger = KotlinLogging.logger {} object RabbitMqConstants { + const val INIT_EXCHANGE = "init.exchange" const val TASK_EXCHANGE = "task.exchange" const val WORKER_CONTROL_EXCHANGE = "worker.control.exchange" const val AGGREGATOR_CONTROL_EXCHANGE = "aggregator.control.exchange" + const val INIT_QUEUE = "init.queue" const val TASKS_QUEUE = "tasks.queue" const val WORKER_CONTROL_QUEUE = "worker.control.queue" const val AGGREGATOR_CONTROL_QUEUE = "aggregator.control.queue" + const val INIT_ROUTING_KEY = "init" const val TASK_ROUTING_KEY = "task" } @@ -37,6 +42,11 @@ object RabbitMqConstants { class RabbitConfiguration { // Exchanges + @Bean(RabbitMqConstants.INIT_EXCHANGE) + fun initExchange(): DirectExchange { + return DirectExchange(RabbitMqConstants.INIT_EXCHANGE) + } + @Bean(RabbitMqConstants.TASK_EXCHANGE) fun taskExchange(): DirectExchange { return DirectExchange(RabbitMqConstants.TASK_EXCHANGE) @@ -53,6 +63,11 @@ class RabbitConfiguration { } // Queues + @Bean(RabbitMqConstants.INIT_QUEUE) + fun initQueue(): Queue { + return Queue(RabbitMqConstants.INIT_QUEUE) + } + @Bean(RabbitMqConstants.TASKS_QUEUE) fun tasksQueue(): Queue { return Queue(RabbitMqConstants.TASKS_QUEUE) @@ -69,6 +84,17 @@ class RabbitConfiguration { } // Bindings + @Bean + fun initBinding( + @Qualifier(RabbitMqConstants.INIT_EXCHANGE) initExchange: DirectExchange, + @Qualifier(RabbitMqConstants.INIT_QUEUE) initQueue: Queue, + ): Binding { + return BindingBuilder + .bind(initQueue) + .to(initExchange) + .with(RabbitMqConstants.INIT_ROUTING_KEY) + } + @Bean fun taskBinding( @Qualifier(RabbitMqConstants.TASK_EXCHANGE) taskExchange: DirectExchange, @@ -122,15 +148,21 @@ class RabbitConfiguration { @Bean fun rabbitTemplate( connectionFactory: ConnectionFactory, - messageConverter: MessageConverter + messageConverter: MessageConverter, + jobStorage: JobStorage, ): RabbitTemplate { return RabbitTemplate(connectionFactory).apply { this.messageConverter = messageConverter setMandatory(true) - setConfirmCallback { _, ack, cause -> + setConfirmCallback { correlationData, ack, cause -> if (!ack) { logger.error { "Message not delivered: $cause" } + + if (correlationData != null) { + val jobId = correlationData.id + jobStorage.removeJob(UUID.fromString(jobId)) + } } } diff --git a/src/main/kotlin/org/quicklybly/dumbmq/producer/splitter/ProducerService.kt b/src/main/kotlin/org/quicklybly/dumbmq/producer/splitter/ProducerService.kt new file mode 100644 index 0000000..5b6a1ba --- /dev/null +++ b/src/main/kotlin/org/quicklybly/dumbmq/producer/splitter/ProducerService.kt @@ -0,0 +1,31 @@ +package org.quicklybly.dumbmq.producer.splitter + +import com.rabbitmq.client.Channel +import mu.KotlinLogging +import org.quicklybly.dumbmq.common.dto.JobInitDto +import org.quicklybly.dumbmq.configuration.RabbitMqConstants +import org.springframework.amqp.rabbit.annotation.RabbitListener +import org.springframework.amqp.rabbit.core.RabbitTemplate +import org.springframework.amqp.support.AmqpHeaders +import org.springframework.messaging.handler.annotation.Header +import org.springframework.stereotype.Component + +private val logger = KotlinLogging.logger { } + +@Component +class ProducerService(val rabbitTemplate: RabbitTemplate) { + + // todo jobStorage + // todo don't forget to update JobService + // todo create async API + + @RabbitListener(queues = [RabbitMqConstants.INIT_QUEUE]) + fun jobInitListener( + jobInitDto: JobInitDto, + @Header(AmqpHeaders.CHANNEL) channel: Channel, + @Header(AmqpHeaders.DELIVERY_TAG) deliveryTag: Long, + ) { + logger.info { jobInitDto } + channel.basicAck(deliveryTag, false) + } +} diff --git a/src/main/kotlin/org/quicklybly/dumbmq/service/JobService.kt b/src/main/kotlin/org/quicklybly/dumbmq/service/JobService.kt index e3adba8..7b37adb 100644 --- a/src/main/kotlin/org/quicklybly/dumbmq/service/JobService.kt +++ b/src/main/kotlin/org/quicklybly/dumbmq/service/JobService.kt @@ -2,24 +2,44 @@ package org.quicklybly.dumbmq.service import org.quicklybly.dumbmq.api.dto.JobRequest import org.quicklybly.dumbmq.api.dto.JobResponse -import org.quicklybly.dumbmq.producer.splitter.ProducerService +import org.quicklybly.dumbmq.common.dto.JobInitDto +import org.quicklybly.dumbmq.configuration.RabbitMqConstants +import org.springframework.amqp.rabbit.connection.CorrelationData +import org.springframework.amqp.rabbit.core.RabbitTemplate import org.springframework.stereotype.Service import java.util.UUID -import java.util.concurrent.ConcurrentHashMap @Service -class JobService(private val producerService: ProducerService) { - - private val jobsInProgress = ConcurrentHashMap.newKeySet() +class JobService( + private val storage: JobStorage, + private val rabbitTemplate: RabbitTemplate, +) { fun createJob(request: JobRequest): JobResponse { val jobId = UUID.randomUUID() - jobsInProgress.add(jobId) + storage.addJob(jobId) + + try { + val correlation = CorrelationData(jobId.toString()) + rabbitTemplate.convertAndSend( + RabbitMqConstants.INIT_EXCHANGE, + RabbitMqConstants.INIT_ROUTING_KEY, + JobInitDto( + jobId = jobId.toString(), + fileUrls = request.fileUrls, + ), + correlation, + ) + } catch (e: Exception) { + storage.removeJob(jobId) + throw e + } + return JobResponse(jobId) } fun completeJob(jobId: UUID) { - jobsInProgress.remove(jobId) + storage.removeJob(jobId) } fun getReport(jobId: UUID) { diff --git a/src/main/kotlin/org/quicklybly/dumbmq/service/JobStorage.kt b/src/main/kotlin/org/quicklybly/dumbmq/service/JobStorage.kt new file mode 100644 index 0000000..5210b09 --- /dev/null +++ b/src/main/kotlin/org/quicklybly/dumbmq/service/JobStorage.kt @@ -0,0 +1,22 @@ +package org.quicklybly.dumbmq.service + +import org.springframework.stereotype.Component +import java.util.UUID +import java.util.concurrent.ConcurrentHashMap + +@Component +class JobStorage { + private val jobsInProgress = ConcurrentHashMap.newKeySet() + + fun addJob(jobId: UUID) { + jobsInProgress.add(jobId) + } + + fun removeJob(jobId: UUID) { + jobsInProgress.remove(jobId) + } + + fun containsJob(jobId: UUID): Boolean { + return jobsInProgress.contains(jobId) + } +} From 2105af1ce04767a5db0886a60f70ebad12be5234 Mon Sep 17 00:00:00 2001 From: "ar.r.lysenko" Date: Tue, 25 Nov 2025 12:35:32 +0300 Subject: [PATCH 11/15] add splitter configuration --- .../configuration/SplitterConfiguration.kt | 21 +++++++++++++++++++ .../properties/SplitterProperties.kt | 10 +++++++++ src/main/resources/application.yaml | 12 +++++++++++ 3 files changed, 43 insertions(+) create mode 100644 src/main/kotlin/org/quicklybly/dumbmq/configuration/SplitterConfiguration.kt create mode 100644 src/main/kotlin/org/quicklybly/dumbmq/configuration/properties/SplitterProperties.kt diff --git a/src/main/kotlin/org/quicklybly/dumbmq/configuration/SplitterConfiguration.kt b/src/main/kotlin/org/quicklybly/dumbmq/configuration/SplitterConfiguration.kt new file mode 100644 index 0000000..c5360f9 --- /dev/null +++ b/src/main/kotlin/org/quicklybly/dumbmq/configuration/SplitterConfiguration.kt @@ -0,0 +1,21 @@ +package org.quicklybly.dumbmq.configuration + +import org.quicklybly.dumbmq.configuration.properties.SplitterProperties +import org.quicklybly.dumbmq.producer.splitter.impl.SentenceTextSplitter +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +@EnableConfigurationProperties(SplitterProperties::class) +class SplitterConfiguration { + + @Bean + fun splitterConfig(properties: SplitterProperties): SentenceTextSplitter.Configuration { + return SentenceTextSplitter.Configuration( + maxChunkSize = properties.maxChunkSize, + sentenceTerminators = properties.sentenceTerminators, + bufferSize = properties.bufferSize, + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/quicklybly/dumbmq/configuration/properties/SplitterProperties.kt b/src/main/kotlin/org/quicklybly/dumbmq/configuration/properties/SplitterProperties.kt new file mode 100644 index 0000000..191977c --- /dev/null +++ b/src/main/kotlin/org/quicklybly/dumbmq/configuration/properties/SplitterProperties.kt @@ -0,0 +1,10 @@ +package org.quicklybly.dumbmq.configuration.properties + +import org.springframework.boot.context.properties.ConfigurationProperties + +@ConfigurationProperties("dumbmq.splitter") +data class SplitterProperties( + val maxChunkSize: Int, + val sentenceTerminators: Set = setOf('.', '!', '?'), + val bufferSize: Int = 8192, +) \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index a41b8e9..c106d6a 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -14,3 +14,15 @@ server: port: 8080 servlet: context-path: /${spring.application.name} + +dumbmq: + splitter: + max-chunk-size: 10000 + sentence-terminators: + - '.' + - '!' + - '?' + buffer-size: 8192 + worker: + count: 2 + From 1e51abe911cdc2b3196280019c1fa179e9525b40 Mon Sep 17 00:00:00 2001 From: "ar.r.lysenko" Date: Tue, 25 Nov 2025 12:35:57 +0300 Subject: [PATCH 12/15] add worker registration --- .../dumbmq/common/dto/SentenceTaskDto.kt | 8 +++ .../configuration/WorkerConfiguration.kt | 49 +++++++++++++++++++ .../properties/WorkerProperties.kt | 8 +++ .../org/quicklybly/dumbmq/worker/Worker.kt | 26 ++++++++++ 4 files changed, 91 insertions(+) create mode 100644 src/main/kotlin/org/quicklybly/dumbmq/common/dto/SentenceTaskDto.kt create mode 100644 src/main/kotlin/org/quicklybly/dumbmq/configuration/WorkerConfiguration.kt create mode 100644 src/main/kotlin/org/quicklybly/dumbmq/configuration/properties/WorkerProperties.kt create mode 100644 src/main/kotlin/org/quicklybly/dumbmq/worker/Worker.kt diff --git a/src/main/kotlin/org/quicklybly/dumbmq/common/dto/SentenceTaskDto.kt b/src/main/kotlin/org/quicklybly/dumbmq/common/dto/SentenceTaskDto.kt new file mode 100644 index 0000000..a5836b7 --- /dev/null +++ b/src/main/kotlin/org/quicklybly/dumbmq/common/dto/SentenceTaskDto.kt @@ -0,0 +1,8 @@ +package org.quicklybly.dumbmq.common.dto + +import java.util.UUID + +data class SentenceTaskDto( + val jobId: UUID, + val sentence: String, +) diff --git a/src/main/kotlin/org/quicklybly/dumbmq/configuration/WorkerConfiguration.kt b/src/main/kotlin/org/quicklybly/dumbmq/configuration/WorkerConfiguration.kt new file mode 100644 index 0000000..a2a07a6 --- /dev/null +++ b/src/main/kotlin/org/quicklybly/dumbmq/configuration/WorkerConfiguration.kt @@ -0,0 +1,49 @@ +package org.quicklybly.dumbmq.configuration + +import mu.KotlinLogging +import org.quicklybly.dumbmq.configuration.properties.WorkerProperties +import org.quicklybly.dumbmq.worker.Worker +import org.springframework.beans.factory.config.BeanDefinition +import org.springframework.beans.factory.support.BeanDefinitionBuilder +import org.springframework.beans.factory.support.BeanDefinitionRegistry +import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.context.EnvironmentAware +import org.springframework.context.annotation.Configuration +import org.springframework.core.env.Environment +import java.util.UUID + +private val logger = KotlinLogging.logger { } + +@Configuration +@EnableConfigurationProperties(WorkerProperties::class) +class WorkerConfiguration : BeanDefinitionRegistryPostProcessor, EnvironmentAware { + + private var workerCount: Int = 0 + + override fun setEnvironment(environment: Environment) { + workerCount = environment.getProperty("dumbmq.worker.count")?.toInt() + ?: throw IllegalArgumentException("Worker count is not set") + } + + override fun postProcessBeanDefinitionRegistry( + registry: BeanDefinitionRegistry, + ) { + logger.info { "Registering $workerCount workers" } + + repeat(workerCount) { index -> + val workerId = UUID.randomUUID() + val beanName = "worker-$index-$workerId" + + val beanDefinition = BeanDefinitionBuilder + .genericBeanDefinition(Worker::class.java) + .addConstructorArgValue(workerId) + .addConstructorArgReference("rabbitTemplate") + .setScope(BeanDefinition.SCOPE_SINGLETON) + .beanDefinition + + registry.registerBeanDefinition(beanName, beanDefinition) + logger.info { "Registered worker bean: $beanName with ID: $workerId" } + } + } +} diff --git a/src/main/kotlin/org/quicklybly/dumbmq/configuration/properties/WorkerProperties.kt b/src/main/kotlin/org/quicklybly/dumbmq/configuration/properties/WorkerProperties.kt new file mode 100644 index 0000000..55376e9 --- /dev/null +++ b/src/main/kotlin/org/quicklybly/dumbmq/configuration/properties/WorkerProperties.kt @@ -0,0 +1,8 @@ +package org.quicklybly.dumbmq.configuration.properties + +import org.springframework.boot.context.properties.ConfigurationProperties + +@ConfigurationProperties("dumbmq.worker") +data class WorkerProperties( + val count: Int, +) diff --git a/src/main/kotlin/org/quicklybly/dumbmq/worker/Worker.kt b/src/main/kotlin/org/quicklybly/dumbmq/worker/Worker.kt new file mode 100644 index 0000000..407d2fd --- /dev/null +++ b/src/main/kotlin/org/quicklybly/dumbmq/worker/Worker.kt @@ -0,0 +1,26 @@ +package org.quicklybly.dumbmq.worker + +import com.rabbitmq.client.Channel +import mu.KotlinLogging +import org.quicklybly.dumbmq.common.dto.SentenceTaskDto +import org.quicklybly.dumbmq.configuration.RabbitMqConstants +import org.springframework.amqp.rabbit.annotation.RabbitListener +import org.springframework.amqp.rabbit.core.RabbitTemplate +import org.springframework.amqp.support.AmqpHeaders +import org.springframework.messaging.handler.annotation.Header +import java.util.UUID + +private val logger = KotlinLogging.logger { } + +class Worker(private val id: UUID, private val rabbitTemplate: RabbitTemplate) { + + @RabbitListener(queues = [RabbitMqConstants.TASKS_QUEUE]) + fun taskListener( + taskDto: SentenceTaskDto, + @Header(AmqpHeaders.CHANNEL) channel: Channel, + @Header(AmqpHeaders.DELIVERY_TAG) deliveryTag: Long, + ) { + logger.info { "worker $id received sentence ${taskDto.sentence}" } + channel.basicAck(deliveryTag, false) + } +} From a7e7744c96466c757f857154679892b7f6c6019a Mon Sep 17 00:00:00 2001 From: "ar.r.lysenko" Date: Tue, 25 Nov 2025 12:42:24 +0300 Subject: [PATCH 13/15] update job init dto --- .../kotlin/org/quicklybly/dumbmq/common/dto/JobInitDto.kt | 4 +++- src/main/kotlin/org/quicklybly/dumbmq/service/JobService.kt | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/org/quicklybly/dumbmq/common/dto/JobInitDto.kt b/src/main/kotlin/org/quicklybly/dumbmq/common/dto/JobInitDto.kt index a6235a5..ecb4e40 100644 --- a/src/main/kotlin/org/quicklybly/dumbmq/common/dto/JobInitDto.kt +++ b/src/main/kotlin/org/quicklybly/dumbmq/common/dto/JobInitDto.kt @@ -1,6 +1,8 @@ package org.quicklybly.dumbmq.common.dto +import java.util.UUID + data class JobInitDto( - val jobId: String, + val jobId: UUID, val fileUrls: List, ) diff --git a/src/main/kotlin/org/quicklybly/dumbmq/service/JobService.kt b/src/main/kotlin/org/quicklybly/dumbmq/service/JobService.kt index 7b37adb..23cd30a 100644 --- a/src/main/kotlin/org/quicklybly/dumbmq/service/JobService.kt +++ b/src/main/kotlin/org/quicklybly/dumbmq/service/JobService.kt @@ -25,7 +25,7 @@ class JobService( RabbitMqConstants.INIT_EXCHANGE, RabbitMqConstants.INIT_ROUTING_KEY, JobInitDto( - jobId = jobId.toString(), + jobId = jobId, fileUrls = request.fileUrls, ), correlation, From 9277ea72881efb6ef398660c97da20f97871e57d Mon Sep 17 00:00:00 2001 From: "ar.r.lysenko" Date: Tue, 25 Nov 2025 14:46:22 +0300 Subject: [PATCH 14/15] add nlp service for name detection --- build.gradle.kts | 5 ++++ gradle.properties | 1 + .../dumbmq/configuration/NlpConfiguration.kt | 24 ++++++++++++++++ .../quicklybly/dumbmq/service/NlpService.kt | 28 +++++++++++++++++++ .../dumbmq/service/NlpServiceTest.kt | 18 ++++++++++++ 5 files changed, 76 insertions(+) create mode 100644 gradle.properties create mode 100644 src/main/kotlin/org/quicklybly/dumbmq/configuration/NlpConfiguration.kt create mode 100644 src/main/kotlin/org/quicklybly/dumbmq/service/NlpService.kt create mode 100644 src/test/kotlin/org/quicklybly/dumbmq/service/NlpServiceTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index 0ea90dc..60fcee9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -28,6 +28,10 @@ dependencies { implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + val stanfordNlpVersion = "4.5.4" + implementation("edu.stanford.nlp:stanford-corenlp:$stanfordNlpVersion") + implementation("edu.stanford.nlp:stanford-corenlp:$stanfordNlpVersion:models") + testImplementation(kotlin("test")) testImplementation("org.junit.jupiter:junit-jupiter:5.11.0") testImplementation("org.assertj:assertj-core:3.24.2") @@ -35,6 +39,7 @@ dependencies { tasks { test { + jvmArgs("-Xmx8g") useJUnitPlatform() } diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..69df0b9 --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +org.gradle.jvmargs=-Xmx8g -XX:+UseG1GC -Dfile.encoding=UTF-8 diff --git a/src/main/kotlin/org/quicklybly/dumbmq/configuration/NlpConfiguration.kt b/src/main/kotlin/org/quicklybly/dumbmq/configuration/NlpConfiguration.kt new file mode 100644 index 0000000..844467e --- /dev/null +++ b/src/main/kotlin/org/quicklybly/dumbmq/configuration/NlpConfiguration.kt @@ -0,0 +1,24 @@ +package org.quicklybly.dumbmq.configuration + +import edu.stanford.nlp.pipeline.StanfordCoreNLP +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import java.util.Properties + +@Configuration +class NlpConfiguration { + + @Bean + fun pipeline(): StanfordCoreNLP { + val props = Properties().apply { + setProperty("annotators", "tokenize,ssplit,pos,lemma,ner") + setProperty("ner.useSUTime", "0") + + setProperty( + "ner.model", + "edu/stanford/nlp/models/ner/english.all.3class.distsim.crf.ser.gz" + ) + } + return StanfordCoreNLP(props) + } +} diff --git a/src/main/kotlin/org/quicklybly/dumbmq/service/NlpService.kt b/src/main/kotlin/org/quicklybly/dumbmq/service/NlpService.kt new file mode 100644 index 0000000..2169fe5 --- /dev/null +++ b/src/main/kotlin/org/quicklybly/dumbmq/service/NlpService.kt @@ -0,0 +1,28 @@ +package org.quicklybly.dumbmq.service + +import edu.stanford.nlp.pipeline.StanfordCoreNLP +import org.springframework.stereotype.Service + +@Service +class NlpService(private val pipeline: StanfordCoreNLP) { + + companion object { + private const val NAME_PLACEHOLDER = "[NAME]" + } + + fun anonymizeNames(text: String): String { + val document = pipeline.processToCoreDocument(text) + val entities = document.entityMentions() + .filter { it.entityType() == "PERSON" } + + var result = text + + for (entity in entities) { + val name = entity.text() + val regex = Regex("\\b$name\\b") + result = regex.replace(result, NAME_PLACEHOLDER) + } + + return result + } +} diff --git a/src/test/kotlin/org/quicklybly/dumbmq/service/NlpServiceTest.kt b/src/test/kotlin/org/quicklybly/dumbmq/service/NlpServiceTest.kt new file mode 100644 index 0000000..4e083fc --- /dev/null +++ b/src/test/kotlin/org/quicklybly/dumbmq/service/NlpServiceTest.kt @@ -0,0 +1,18 @@ +package org.quicklybly.dumbmq.service + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.quicklybly.dumbmq.configuration.NlpConfiguration + +class NlpServiceTest { + private val config = NlpConfiguration() + private val service = NlpService(config.pipeline()) + + @Test + fun test() { + val text = "John met Anna in New York." + val result = service.anonymizeNames(text) + + assertThat(result).isEqualTo("[NAME] met [NAME] in New York.") + } +} From ebd562d2ced7ab5719b7c4fafee38fe39aa6117e Mon Sep 17 00:00:00 2001 From: "ar.r.lysenko" Date: Fri, 12 Dec 2025 16:58:47 +0300 Subject: [PATCH 15/15] commit core logic (after a month..) --- .gradle/8.4/checksums/checksums.lock | Bin 17 -> 0 bytes .gradle/8.4/checksums/md5-checksums.bin | Bin 21847 -> 0 bytes .gradle/8.4/checksums/sha1-checksums.bin | Bin 31079 -> 0 bytes .../dependencies-accessors.lock | Bin 17 -> 0 bytes .../8.4/dependencies-accessors/gc.properties | 0 .../8.4/executionHistory/executionHistory.bin | Bin 19607 -> 0 bytes .../executionHistory/executionHistory.lock | Bin 17 -> 0 bytes .gradle/8.4/fileChanges/last-build.bin | Bin 1 -> 0 bytes .gradle/8.4/fileHashes/fileHashes.bin | Bin 18797 -> 0 bytes .gradle/8.4/fileHashes/fileHashes.lock | Bin 17 -> 0 bytes .../8.4/fileHashes/resourceHashesCache.bin | Bin 18565 -> 0 bytes .gradle/8.4/gc.properties | 0 .../buildOutputCleanup.lock | Bin 17 -> 17 bytes .gradle/file-system.probe | Bin 8 -> 8 bytes inputs/war_and_peace_eng.txt | 64851 ++++++++++++++++ .../dumbmq/aggregator/Aggregator.kt | 136 + .../quicklybly/dumbmq/api/JobController.kt | 11 +- .../dumbmq/common/dto/ControlDto.kt | 9 + .../dumbmq/common/dto/SentenceTaskDto.kt | 1 + .../dumbmq/common/dto/WorkerMetricsDto.kt | 56 + .../configuration/RabbitConfiguration.kt | 26 +- .../configuration/WorkerConfiguration.kt | 2 + .../properties/MetricsProperties.kt | 14 + .../producer/splitter/ProducerService.kt | 93 +- .../quicklybly/dumbmq/service/JobService.kt | 22 +- .../quicklybly/dumbmq/service/NlpService.kt | 3 +- .../quicklybly/dumbmq/service/TextAnalyzer.kt | 84 + .../kotlin/org/quicklybly/dumbmq/sink/Sink.kt | 33 + .../org/quicklybly/dumbmq/worker/Worker.kt | 33 +- src/main/resources/application.yaml | 5 +- 30 files changed, 65349 insertions(+), 30 deletions(-) delete mode 100644 .gradle/8.4/checksums/checksums.lock delete mode 100644 .gradle/8.4/checksums/md5-checksums.bin delete mode 100644 .gradle/8.4/checksums/sha1-checksums.bin delete mode 100644 .gradle/8.4/dependencies-accessors/dependencies-accessors.lock delete mode 100644 .gradle/8.4/dependencies-accessors/gc.properties delete mode 100644 .gradle/8.4/executionHistory/executionHistory.bin delete mode 100644 .gradle/8.4/executionHistory/executionHistory.lock delete mode 100644 .gradle/8.4/fileChanges/last-build.bin delete mode 100644 .gradle/8.4/fileHashes/fileHashes.bin delete mode 100644 .gradle/8.4/fileHashes/fileHashes.lock delete mode 100644 .gradle/8.4/fileHashes/resourceHashesCache.bin delete mode 100644 .gradle/8.4/gc.properties create mode 100644 inputs/war_and_peace_eng.txt create mode 100644 src/main/kotlin/org/quicklybly/dumbmq/aggregator/Aggregator.kt create mode 100644 src/main/kotlin/org/quicklybly/dumbmq/common/dto/ControlDto.kt create mode 100644 src/main/kotlin/org/quicklybly/dumbmq/common/dto/WorkerMetricsDto.kt create mode 100644 src/main/kotlin/org/quicklybly/dumbmq/configuration/properties/MetricsProperties.kt create mode 100644 src/main/kotlin/org/quicklybly/dumbmq/service/TextAnalyzer.kt create mode 100644 src/main/kotlin/org/quicklybly/dumbmq/sink/Sink.kt diff --git a/.gradle/8.4/checksums/checksums.lock b/.gradle/8.4/checksums/checksums.lock deleted file mode 100644 index c199a090801026da56bfc7358c445081ac83cf48..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17 VcmZSH*cj4lb6?~<0~j!x001*i1WEt^ diff --git a/.gradle/8.4/checksums/md5-checksums.bin b/.gradle/8.4/checksums/md5-checksums.bin deleted file mode 100644 index 0c957de6eba111af3d77b1eba34ba508652d148c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21847 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 diff --git a/.gradle/8.4/fileHashes/fileHashes.lock b/.gradle/8.4/fileHashes/fileHashes.lock deleted file mode 100644 index 15904d6dd9fe3772c3e81054db8c9e94cd297e09..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17 UcmZR!Ha(!f_C&^91_%%W04_}f?*IS* diff --git a/.gradle/8.4/fileHashes/resourceHashesCache.bin b/.gradle/8.4/fileHashes/resourceHashesCache.bin deleted file mode 100644 index a47e42febbf6837b9beeaaec89b3e35b7157d865..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18565 zcmeI%u}Xqb6ae6hZX&5eV2e)>5r~762oBQJ5{R^fxLc5JYHLHbwmH><1l1czlSoT% zVfW|vM|y#V_%57#59c1ZoNv9I5US