├── .github
└── workflows
│ └── pr.yml
├── .gitignore
├── .mvn
└── wrapper
│ ├── maven-wrapper.jar
│ └── maven-wrapper.properties
├── LICENSE.txt
├── README.md
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
├── main
└── java
│ └── com
│ └── sipios
│ └── spring
│ └── data
│ └── event
│ ├── annotation
│ ├── DataEventEntity.java
│ └── EnableDataEvent.java
│ ├── broadcaster
│ └── DataEventBroadcaster.java
│ └── listener
│ └── DataEventListener.java
└── test
└── java
└── com
└── sipios
└── spring
└── data
└── event
├── broadcaster
├── DataEventBroadcasterAssociationTests.java
└── DataEventBroadcasterTest.java
└── listener
└── DataEventListenerTest.java
/.github/workflows/pr.yml:
--------------------------------------------------------------------------------
1 | name: Test & Coverage
2 | on:
3 | pull_request:
4 | branches:
5 | - '*'
6 |
7 | jobs:
8 | test-and-coverage:
9 | runs-on: ubuntu-latest
10 |
11 | steps:
12 | - uses: actions/checkout@v3
13 |
14 | - name: Set up JDK 21
15 | uses: actions/setup-java@v3
16 | with:
17 | java-version: '21'
18 | distribution: 'temurin'
19 |
20 | - name: Set up the Maven dependencies caching
21 | uses: actions/cache@v3
22 | with:
23 | path: ~/.m2
24 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
25 | restore-keys: ${{ runner.os }}-m2
26 |
27 | - name: Run tests
28 | run: mvn --batch-mode --update-snapshots verify -Dgpg.skip=true
29 |
30 | - name: Add coverage
31 | uses: madrapps/jacoco-report@v1.6.1
32 | with:
33 | paths: |
34 | ${{ github.workspace }}/**/target/site/jacoco/jacoco.xml
35 | token: ${{ secrets.GITHUB_TOKEN }}
36 | title: '### :zap: Coverage report'
37 | update-comment: true
38 | min-coverage-overall: 80
39 | min-coverage-changed-files: 60
40 | continue-on-error: false
41 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | HELP.md
2 | target/
3 | !.mvn/wrapper/maven-wrapper.jar
4 | !**/src/main/**/target/
5 | !**/src/test/**/target/
6 |
7 | ### STS ###
8 | .apt_generated
9 | .classpath
10 | .factorypath
11 | .project
12 | .settings
13 | .springBeans
14 | .sts4-cache
15 |
16 | ### IntelliJ IDEA ###
17 | .idea
18 | *.iws
19 | *.iml
20 | *.ipr
21 |
22 | ### NetBeans ###
23 | /nbproject/private/
24 | /nbbuild/
25 | /dist/
26 | /nbdist/
27 | /.nb-gradle/
28 | build/
29 | !**/src/main/**/build/
30 | !**/src/test/**/build/
31 |
32 | ### VS Code ###
33 | .vscode/
34 |
--------------------------------------------------------------------------------
/.mvn/wrapper/maven-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theodo-fintech/spring-data-event/269fc3eb5dc179aad7b6036dd0f370ffe8bbb1be/.mvn/wrapper/maven-wrapper.jar
--------------------------------------------------------------------------------
/.mvn/wrapper/maven-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.5/apache-maven-3.9.5-bin.zip
2 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar
3 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019-present Woody Rousseau and other contributors
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [![Stargazers][stars-shield]][stars-url]
2 | [![Issues][issues-shield]][issues-url]
3 |
4 |
5 |
Spring Data Event
6 |
7 |
8 | ### Features
9 |
10 | - **Data Event Entity**: Set up your JPA Entities to be automatically sent over Kafka topics when saved, updated or deleted simply using `@DataEventEntity`
11 |
12 | More to come later... Stay tuned !
13 |
14 |
15 | ## Getting Started
16 |
17 | ### Prerequisites
18 |
19 | This library has been currently tested on projects under SpringBoot on version 3.2.4 with Java 17 or later, using Hibernate as a JPA implementation.
20 |
21 |
22 | ### Installation
23 |
24 | You will have to add the dependency in your spring-boot-project
25 |
26 | ```xml
27 |
28 | com.sipios
29 | spring-data-event
30 | 0.1.0
31 |
32 | ```
33 |
34 | ### Configuration
35 |
36 | Set up your application properties file to make spring kafka work properly
37 |
38 | ```txt
39 | spring.kafka.bootstrap-servers=localhost:29092
40 | ```
41 |
42 | Then, you will have to enable the library so that it will be able to work properly.
43 | You just add the `@EnableDataEvent` on any of your `@Configuration` class already existing on your project, or directly on the `@SpringBootApplication` class.
44 |
45 | ## Usage
46 |
47 |
48 | To mark a JPA entity to be automatically sent over event platform, put the `@DataEventEntity` on your entity
49 |
50 | For instance
51 |
52 | ```java
53 |
54 | @Entity
55 | @DataEventEntity
56 | @Table(name= "user_account")
57 | public class UserEntity {
58 |
59 | @Id
60 | @Column(name = "id", nullable = false)
61 | @GeneratedValue(strategy = GenerationType.UUID)
62 | private UUID id;
63 |
64 |
65 | }
66 | ```
67 |
68 | By default, the topics on which the event will be sent are :
69 | - For the creation : `entity_name.created` (for example : `userentity.created`)
70 | - For the update : `entity_name.updated` (for example : `userentity.updated`)
71 | - For the deletion : `entity_name.deleted` (for example : `userentity.deleted`)
72 |
73 |
74 | ## FAQs
75 |
76 | No FAQ at the moment
77 |
78 | ## Roadmap
79 |
80 | - [x] Add simple case of sending creation, update and deletion event over kafka
81 | - [ ] Allow customizing which events should be sent or not
82 | - [ ] Allow customizing which attribute from the entity to be sent or not
83 | - [ ] Allow other event techno as RabbitMQ or Apache Pulsar
84 |
85 | ## Contributing
86 |
87 | We are just getting started on this project and would **highly appreciate** contributions
88 |
89 | ## License
90 |
91 | Distributed under the MIT License. See [LICENSE](/LICENSE.txt) for more information.
92 |
93 |
94 | [stars-shield]: https://img.shields.io/github/stars/sipios/spring-data-event?style=for-the-badge
95 | [stars-url]: https://github.com/sipios/spring-data-event/stargazers
96 | [issues-shield]: https://img.shields.io/github/issues/sipios/spring-data-event?style=for-the-badge
97 | [issues-url]: https://github.com/sipios/spring-data-event/issues
98 | [license-url]: https://github.com/sipios/spring-data-event/blob/main/LICENSE
99 |
--------------------------------------------------------------------------------
/mvnw:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # ----------------------------------------------------------------------------
3 | # Licensed to the Apache Software Foundation (ASF) under one
4 | # or more contributor license agreements. See the NOTICE file
5 | # distributed with this work for additional information
6 | # regarding copyright ownership. The ASF licenses this file
7 | # to you under the Apache License, Version 2.0 (the
8 | # "License"); you may not use this file except in compliance
9 | # with the License. You may obtain a copy of the License at
10 | #
11 | # https://www.apache.org/licenses/LICENSE-2.0
12 | #
13 | # Unless required by applicable law or agreed to in writing,
14 | # software distributed under the License is distributed on an
15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | # KIND, either express or implied. See the License for the
17 | # specific language governing permissions and limitations
18 | # under the License.
19 | # ----------------------------------------------------------------------------
20 |
21 | # ----------------------------------------------------------------------------
22 | # Apache Maven Wrapper startup batch script, version 3.2.0
23 | #
24 | # Required ENV vars:
25 | # ------------------
26 | # JAVA_HOME - location of a JDK home dir
27 | #
28 | # Optional ENV vars
29 | # -----------------
30 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven
31 | # e.g. to debug Maven itself, use
32 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
33 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files
34 | # ----------------------------------------------------------------------------
35 |
36 | if [ -z "$MAVEN_SKIP_RC" ] ; then
37 |
38 | if [ -f /usr/local/etc/mavenrc ] ; then
39 | . /usr/local/etc/mavenrc
40 | fi
41 |
42 | if [ -f /etc/mavenrc ] ; then
43 | . /etc/mavenrc
44 | fi
45 |
46 | if [ -f "$HOME/.mavenrc" ] ; then
47 | . "$HOME/.mavenrc"
48 | fi
49 |
50 | fi
51 |
52 | # OS specific support. $var _must_ be set to either true or false.
53 | cygwin=false;
54 | darwin=false;
55 | mingw=false
56 | case "$(uname)" in
57 | CYGWIN*) cygwin=true ;;
58 | MINGW*) mingw=true;;
59 | Darwin*) darwin=true
60 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
61 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
62 | if [ -z "$JAVA_HOME" ]; then
63 | if [ -x "/usr/libexec/java_home" ]; then
64 | JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME
65 | else
66 | JAVA_HOME="/Library/Java/Home"; export JAVA_HOME
67 | fi
68 | fi
69 | ;;
70 | esac
71 |
72 | if [ -z "$JAVA_HOME" ] ; then
73 | if [ -r /etc/gentoo-release ] ; then
74 | JAVA_HOME=$(java-config --jre-home)
75 | fi
76 | fi
77 |
78 | # For Cygwin, ensure paths are in UNIX format before anything is touched
79 | if $cygwin ; then
80 | [ -n "$JAVA_HOME" ] &&
81 | JAVA_HOME=$(cygpath --unix "$JAVA_HOME")
82 | [ -n "$CLASSPATH" ] &&
83 | CLASSPATH=$(cygpath --path --unix "$CLASSPATH")
84 | fi
85 |
86 | # For Mingw, ensure paths are in UNIX format before anything is touched
87 | if $mingw ; then
88 | [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] &&
89 | JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)"
90 | fi
91 |
92 | if [ -z "$JAVA_HOME" ]; then
93 | javaExecutable="$(which javac)"
94 | if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then
95 | # readlink(1) is not available as standard on Solaris 10.
96 | readLink=$(which readlink)
97 | if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then
98 | if $darwin ; then
99 | javaHome="$(dirname "\"$javaExecutable\"")"
100 | javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac"
101 | else
102 | javaExecutable="$(readlink -f "\"$javaExecutable\"")"
103 | fi
104 | javaHome="$(dirname "\"$javaExecutable\"")"
105 | javaHome=$(expr "$javaHome" : '\(.*\)/bin')
106 | JAVA_HOME="$javaHome"
107 | export JAVA_HOME
108 | fi
109 | fi
110 | fi
111 |
112 | if [ -z "$JAVACMD" ] ; then
113 | if [ -n "$JAVA_HOME" ] ; then
114 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
115 | # IBM's JDK on AIX uses strange locations for the executables
116 | JAVACMD="$JAVA_HOME/jre/sh/java"
117 | else
118 | JAVACMD="$JAVA_HOME/bin/java"
119 | fi
120 | else
121 | JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)"
122 | fi
123 | fi
124 |
125 | if [ ! -x "$JAVACMD" ] ; then
126 | echo "Error: JAVA_HOME is not defined correctly." >&2
127 | echo " We cannot execute $JAVACMD" >&2
128 | exit 1
129 | fi
130 |
131 | if [ -z "$JAVA_HOME" ] ; then
132 | echo "Warning: JAVA_HOME environment variable is not set."
133 | fi
134 |
135 | # traverses directory structure from process work directory to filesystem root
136 | # first directory with .mvn subdirectory is considered project base directory
137 | find_maven_basedir() {
138 | if [ -z "$1" ]
139 | then
140 | echo "Path not specified to find_maven_basedir"
141 | return 1
142 | fi
143 |
144 | basedir="$1"
145 | wdir="$1"
146 | while [ "$wdir" != '/' ] ; do
147 | if [ -d "$wdir"/.mvn ] ; then
148 | basedir=$wdir
149 | break
150 | fi
151 | # workaround for JBEAP-8937 (on Solaris 10/Sparc)
152 | if [ -d "${wdir}" ]; then
153 | wdir=$(cd "$wdir/.." || exit 1; pwd)
154 | fi
155 | # end of workaround
156 | done
157 | printf '%s' "$(cd "$basedir" || exit 1; pwd)"
158 | }
159 |
160 | # concatenates all lines of a file
161 | concat_lines() {
162 | if [ -f "$1" ]; then
163 | # Remove \r in case we run on Windows within Git Bash
164 | # and check out the repository with auto CRLF management
165 | # enabled. Otherwise, we may read lines that are delimited with
166 | # \r\n and produce $'-Xarg\r' rather than -Xarg due to word
167 | # splitting rules.
168 | tr -s '\r\n' ' ' < "$1"
169 | fi
170 | }
171 |
172 | log() {
173 | if [ "$MVNW_VERBOSE" = true ]; then
174 | printf '%s\n' "$1"
175 | fi
176 | }
177 |
178 | BASE_DIR=$(find_maven_basedir "$(dirname "$0")")
179 | if [ -z "$BASE_DIR" ]; then
180 | exit 1;
181 | fi
182 |
183 | MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR
184 | log "$MAVEN_PROJECTBASEDIR"
185 |
186 | ##########################################################################################
187 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
188 | # This allows using the maven wrapper in projects that prohibit checking in binary data.
189 | ##########################################################################################
190 | wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar"
191 | if [ -r "$wrapperJarPath" ]; then
192 | log "Found $wrapperJarPath"
193 | else
194 | log "Couldn't find $wrapperJarPath, downloading it ..."
195 |
196 | if [ -n "$MVNW_REPOURL" ]; then
197 | wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
198 | else
199 | wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
200 | fi
201 | while IFS="=" read -r key value; do
202 | # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' )
203 | safeValue=$(echo "$value" | tr -d '\r')
204 | case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;;
205 | esac
206 | done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
207 | log "Downloading from: $wrapperUrl"
208 |
209 | if $cygwin; then
210 | wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath")
211 | fi
212 |
213 | if command -v wget > /dev/null; then
214 | log "Found wget ... using wget"
215 | [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet"
216 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
217 | wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
218 | else
219 | wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
220 | fi
221 | elif command -v curl > /dev/null; then
222 | log "Found curl ... using curl"
223 | [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent"
224 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
225 | curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
226 | else
227 | curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
228 | fi
229 | else
230 | log "Falling back to using Java to download"
231 | javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java"
232 | javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class"
233 | # For Cygwin, switch paths to Windows format before running javac
234 | if $cygwin; then
235 | javaSource=$(cygpath --path --windows "$javaSource")
236 | javaClass=$(cygpath --path --windows "$javaClass")
237 | fi
238 | if [ -e "$javaSource" ]; then
239 | if [ ! -e "$javaClass" ]; then
240 | log " - Compiling MavenWrapperDownloader.java ..."
241 | ("$JAVA_HOME/bin/javac" "$javaSource")
242 | fi
243 | if [ -e "$javaClass" ]; then
244 | log " - Running MavenWrapperDownloader.java ..."
245 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath"
246 | fi
247 | fi
248 | fi
249 | fi
250 | ##########################################################################################
251 | # End of extension
252 | ##########################################################################################
253 |
254 | # If specified, validate the SHA-256 sum of the Maven wrapper jar file
255 | wrapperSha256Sum=""
256 | while IFS="=" read -r key value; do
257 | case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;;
258 | esac
259 | done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
260 | if [ -n "$wrapperSha256Sum" ]; then
261 | wrapperSha256Result=false
262 | if command -v sha256sum > /dev/null; then
263 | if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then
264 | wrapperSha256Result=true
265 | fi
266 | elif command -v shasum > /dev/null; then
267 | if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then
268 | wrapperSha256Result=true
269 | fi
270 | else
271 | echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available."
272 | echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties."
273 | exit 1
274 | fi
275 | if [ $wrapperSha256Result = false ]; then
276 | echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2
277 | echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2
278 | echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2
279 | exit 1
280 | fi
281 | fi
282 |
283 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
284 |
285 | # For Cygwin, switch paths to Windows format before running java
286 | if $cygwin; then
287 | [ -n "$JAVA_HOME" ] &&
288 | JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME")
289 | [ -n "$CLASSPATH" ] &&
290 | CLASSPATH=$(cygpath --path --windows "$CLASSPATH")
291 | [ -n "$MAVEN_PROJECTBASEDIR" ] &&
292 | MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR")
293 | fi
294 |
295 | # Provide a "standardized" way to retrieve the CLI args that will
296 | # work with both Windows and non-Windows executions.
297 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*"
298 | export MAVEN_CMD_LINE_ARGS
299 |
300 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
301 |
302 | # shellcheck disable=SC2086 # safe args
303 | exec "$JAVACMD" \
304 | $MAVEN_OPTS \
305 | $MAVEN_DEBUG_OPTS \
306 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
307 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
308 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
309 |
--------------------------------------------------------------------------------
/mvnw.cmd:
--------------------------------------------------------------------------------
1 | @REM ----------------------------------------------------------------------------
2 | @REM Licensed to the Apache Software Foundation (ASF) under one
3 | @REM or more contributor license agreements. See the NOTICE file
4 | @REM distributed with this work for additional information
5 | @REM regarding copyright ownership. The ASF licenses this file
6 | @REM to you under the Apache License, Version 2.0 (the
7 | @REM "License"); you may not use this file except in compliance
8 | @REM with the License. You may obtain a copy of the License at
9 | @REM
10 | @REM https://www.apache.org/licenses/LICENSE-2.0
11 | @REM
12 | @REM Unless required by applicable law or agreed to in writing,
13 | @REM software distributed under the License is distributed on an
14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | @REM KIND, either express or implied. See the License for the
16 | @REM specific language governing permissions and limitations
17 | @REM under the License.
18 | @REM ----------------------------------------------------------------------------
19 |
20 | @REM ----------------------------------------------------------------------------
21 | @REM Apache Maven Wrapper startup batch script, version 3.2.0
22 | @REM
23 | @REM Required ENV vars:
24 | @REM JAVA_HOME - location of a JDK home dir
25 | @REM
26 | @REM Optional ENV vars
27 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
28 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
29 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
30 | @REM e.g. to debug Maven itself, use
31 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
32 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
33 | @REM ----------------------------------------------------------------------------
34 |
35 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
36 | @echo off
37 | @REM set title of command window
38 | title %0
39 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
40 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
41 |
42 | @REM set %HOME% to equivalent of $HOME
43 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
44 |
45 | @REM Execute a user defined script before this one
46 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
47 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending
48 | if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
49 | if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
50 | :skipRcPre
51 |
52 | @setlocal
53 |
54 | set ERROR_CODE=0
55 |
56 | @REM To isolate internal variables from possible post scripts, we use another setlocal
57 | @setlocal
58 |
59 | @REM ==== START VALIDATION ====
60 | if not "%JAVA_HOME%" == "" goto OkJHome
61 |
62 | echo.
63 | echo Error: JAVA_HOME not found in your environment. >&2
64 | echo Please set the JAVA_HOME variable in your environment to match the >&2
65 | echo location of your Java installation. >&2
66 | echo.
67 | goto error
68 |
69 | :OkJHome
70 | if exist "%JAVA_HOME%\bin\java.exe" goto init
71 |
72 | echo.
73 | echo Error: JAVA_HOME is set to an invalid directory. >&2
74 | echo JAVA_HOME = "%JAVA_HOME%" >&2
75 | echo Please set the JAVA_HOME variable in your environment to match the >&2
76 | echo location of your Java installation. >&2
77 | echo.
78 | goto error
79 |
80 | @REM ==== END VALIDATION ====
81 |
82 | :init
83 |
84 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
85 | @REM Fallback to current working directory if not found.
86 |
87 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
88 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
89 |
90 | set EXEC_DIR=%CD%
91 | set WDIR=%EXEC_DIR%
92 | :findBaseDir
93 | IF EXIST "%WDIR%"\.mvn goto baseDirFound
94 | cd ..
95 | IF "%WDIR%"=="%CD%" goto baseDirNotFound
96 | set WDIR=%CD%
97 | goto findBaseDir
98 |
99 | :baseDirFound
100 | set MAVEN_PROJECTBASEDIR=%WDIR%
101 | cd "%EXEC_DIR%"
102 | goto endDetectBaseDir
103 |
104 | :baseDirNotFound
105 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
106 | cd "%EXEC_DIR%"
107 |
108 | :endDetectBaseDir
109 |
110 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
111 |
112 | @setlocal EnableExtensions EnableDelayedExpansion
113 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
114 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
115 |
116 | :endReadAdditionalConfig
117 |
118 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
119 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
120 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
121 |
122 | set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
123 |
124 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
125 | IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B
126 | )
127 |
128 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
129 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data.
130 | if exist %WRAPPER_JAR% (
131 | if "%MVNW_VERBOSE%" == "true" (
132 | echo Found %WRAPPER_JAR%
133 | )
134 | ) else (
135 | if not "%MVNW_REPOURL%" == "" (
136 | SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
137 | )
138 | if "%MVNW_VERBOSE%" == "true" (
139 | echo Couldn't find %WRAPPER_JAR%, downloading it ...
140 | echo Downloading from: %WRAPPER_URL%
141 | )
142 |
143 | powershell -Command "&{"^
144 | "$webclient = new-object System.Net.WebClient;"^
145 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
146 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
147 | "}"^
148 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^
149 | "}"
150 | if "%MVNW_VERBOSE%" == "true" (
151 | echo Finished downloading %WRAPPER_JAR%
152 | )
153 | )
154 | @REM End of extension
155 |
156 | @REM If specified, validate the SHA-256 sum of the Maven wrapper jar file
157 | SET WRAPPER_SHA_256_SUM=""
158 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
159 | IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B
160 | )
161 | IF NOT %WRAPPER_SHA_256_SUM%=="" (
162 | powershell -Command "&{"^
163 | "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^
164 | "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^
165 | " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^
166 | " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^
167 | " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^
168 | " exit 1;"^
169 | "}"^
170 | "}"
171 | if ERRORLEVEL 1 goto error
172 | )
173 |
174 | @REM Provide a "standardized" way to retrieve the CLI args that will
175 | @REM work with both Windows and non-Windows executions.
176 | set MAVEN_CMD_LINE_ARGS=%*
177 |
178 | %MAVEN_JAVA_EXE% ^
179 | %JVM_CONFIG_MAVEN_PROPS% ^
180 | %MAVEN_OPTS% ^
181 | %MAVEN_DEBUG_OPTS% ^
182 | -classpath %WRAPPER_JAR% ^
183 | "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
184 | %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
185 | if ERRORLEVEL 1 goto error
186 | goto end
187 |
188 | :error
189 | set ERROR_CODE=1
190 |
191 | :end
192 | @endlocal & set ERROR_CODE=%ERROR_CODE%
193 |
194 | if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
195 | @REM check for post script, once with legacy .bat ending and once with .cmd ending
196 | if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
197 | if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
198 | :skipRcPost
199 |
200 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
201 | if "%MAVEN_BATCH_PAUSE%"=="on" pause
202 |
203 | if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
204 |
205 | cmd /C exit /B %ERROR_CODE%
206 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 |
7 | org.springframework.boot
8 | spring-boot-starter-parent
9 | 3.2.5
10 |
11 |
12 | com.sipios
13 | spring-data-event
14 | 0.2.0
15 | com.sipios:spring-data-event
16 |
17 | A Java library offering the @DataEventEntity annotation for publishing database entity changes to a Kafka queue, enabling straightforward event-driven architectures by automating event publication upon entity persistence.
18 | https://github.com/sipios/spring-data-event
19 |
20 |
21 |
22 | MIT License
23 | http://www.opensource.org/licenses/mit-license.php
24 |
25 |
26 |
27 |
28 | 21
29 | 4.11.0
30 |
31 |
32 |
33 |
34 | org.springframework.kafka
35 | spring-kafka
36 |
37 |
38 | org.hibernate.orm
39 | hibernate-core
40 | provided
41 |
42 |
43 | com.fasterxml.jackson.core
44 | jackson-databind
45 |
46 |
47 | jakarta.annotation
48 | jakarta.annotation-api
49 |
50 |
51 |
52 | org.projectlombok
53 | lombok
54 | true
55 | compile
56 |
57 |
58 | org.springframework.kafka
59 | spring-kafka-test
60 | test
61 |
62 |
63 | org.springframework.boot
64 | spring-boot-starter-test
65 | test
66 |
67 |
68 | org.mockito
69 | mockito-core
70 | ${mockito.version}
71 | test
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 | maven-source-plugin
80 |
81 |
82 | attach-sources
83 | package
84 |
85 | jar-no-fork
86 |
87 |
88 |
89 |
90 |
91 |
92 | maven-deploy-plugin
93 |
94 |
95 | deploy
96 | deploy
97 |
98 | deploy
99 |
100 |
101 |
102 |
103 |
104 | org.apache.maven.plugins
105 | maven-gpg-plugin
106 | 3.1.0
107 |
108 |
109 | sign-artifacts
110 | verify
111 |
112 | sign
113 |
114 |
115 |
116 | --pinentry-mode
117 | loopback
118 |
119 |
120 |
121 |
122 |
123 |
124 | maven-javadoc-plugin
125 |
126 |
127 | attach-javadocs
128 | package
129 |
130 | jar
131 |
132 |
133 |
134 |
135 | false
136 |
137 |
138 |
139 | org.jacoco
140 | jacoco-maven-plugin
141 | 0.8.12
142 |
143 |
144 |
145 | prepare-agent
146 |
147 |
148 |
149 | report
150 | test
151 |
152 | report
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 | scm:git:https://github.com/sipios/spring-data-event.git
161 | scm:git:git@github.com:sipios/spring-data-event.git
162 | https://github.com/sipios/spring-data-event
163 | HEAD
164 |
165 |
166 |
167 | ossrh
168 | https://oss.sonatype.org/service/local/staging/deploy/maven2
169 |
170 |
171 |
172 |
--------------------------------------------------------------------------------
/src/main/java/com/sipios/spring/data/event/annotation/DataEventEntity.java:
--------------------------------------------------------------------------------
1 | package com.sipios.spring.data.event.annotation;
2 |
3 |
4 | import java.lang.annotation.ElementType;
5 | import java.lang.annotation.Retention;
6 | import java.lang.annotation.Target;
7 |
8 | import static java.lang.annotation.RetentionPolicy.RUNTIME;
9 |
10 | @Target(ElementType.TYPE)
11 | @Retention(RUNTIME)
12 | public @interface DataEventEntity {
13 |
14 | String creationTopic() default "";
15 | String deletionTopic() default "";
16 | String updateTopic() default "";
17 | }
--------------------------------------------------------------------------------
/src/main/java/com/sipios/spring/data/event/annotation/EnableDataEvent.java:
--------------------------------------------------------------------------------
1 | package com.sipios.spring.data.event.annotation;
2 |
3 | import com.sipios.spring.data.event.broadcaster.DataEventBroadcaster;
4 | import com.sipios.spring.data.event.listener.DataEventListener;
5 | import org.springframework.context.annotation.Import;
6 |
7 | import java.lang.annotation.Documented;
8 | import java.lang.annotation.ElementType;
9 | import java.lang.annotation.Retention;
10 | import java.lang.annotation.Target;
11 |
12 | import static java.lang.annotation.RetentionPolicy.RUNTIME;
13 |
14 |
15 | @Target(ElementType.TYPE)
16 | @Retention(RUNTIME)
17 | @Documented
18 | @Import({DataEventBroadcaster.class, DataEventListener.class})
19 | public @interface EnableDataEvent {
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/java/com/sipios/spring/data/event/broadcaster/DataEventBroadcaster.java:
--------------------------------------------------------------------------------
1 | package com.sipios.spring.data.event.broadcaster;
2 |
3 | import com.fasterxml.jackson.annotation.JsonIdentityInfo;
4 | import com.fasterxml.jackson.annotation.ObjectIdGenerators;
5 | import com.fasterxml.jackson.core.JsonProcessingException;
6 | import com.fasterxml.jackson.databind.ObjectMapper;
7 | import org.hibernate.CallbackException;
8 | import org.springframework.kafka.core.KafkaTemplate;
9 | import org.springframework.stereotype.Component;
10 |
11 | @Component
12 | public class DataEventBroadcaster {
13 |
14 | private final KafkaTemplate kafkaTemplate;
15 | private final ObjectMapper objectMapper;
16 |
17 | public DataEventBroadcaster(KafkaTemplate kafkaTemplate, ObjectMapper objectMapper) {
18 | this.kafkaTemplate = kafkaTemplate;
19 | this.objectMapper = configureObjectMapper(objectMapper);
20 | }
21 |
22 | private void broadcast(String topic, String message) {
23 | kafkaTemplate.send(topic, message);
24 | }
25 |
26 | private String getTopic(String eventType, Object entity) {
27 | return entity.getClass().getSimpleName().toLowerCase() + "." + eventType;
28 | }
29 |
30 | private String getMessage(Object entity) throws CallbackException {
31 | try {
32 | return objectMapper.writeValueAsString(entity);
33 | } catch (JsonProcessingException error) {
34 | throw new CallbackException(error);
35 | }
36 | }
37 |
38 | public void broadcastEntityCreated(Object entity, String topicLabel) throws CallbackException {
39 | String topic = "".equals(topicLabel)?getTopic("created", entity): topicLabel;
40 | String message = getMessage(entity);
41 | broadcast(topic, message);
42 | }
43 |
44 | public void broadcastEntityUpdated(Object entity, String topicLabel) throws CallbackException {
45 | String topic = "".equals(topicLabel)?getTopic("updated", entity): topicLabel;
46 | String message = getMessage(entity);
47 | broadcast(topic, message);
48 | }
49 |
50 | public void broadcastEntityDeleted(Object entity, String topicLabel) throws CallbackException {
51 | String topic = "".equals(topicLabel)?getTopic("deleted", entity): topicLabel;
52 | String message = getMessage(entity);
53 | broadcast(topic, message);
54 | }
55 |
56 | private ObjectMapper configureObjectMapper(ObjectMapper originalMapper) {
57 | ObjectMapper mapper = originalMapper.copy();
58 | mapper.addMixIn(Object.class, ObjectIdMixin.class);
59 | return mapper;
60 | }
61 |
62 | @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
63 | private static class ObjectIdMixin {
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/main/java/com/sipios/spring/data/event/listener/DataEventListener.java:
--------------------------------------------------------------------------------
1 | package com.sipios.spring.data.event.listener;
2 |
3 | import com.sipios.spring.data.event.annotation.DataEventEntity;
4 | import com.sipios.spring.data.event.broadcaster.DataEventBroadcaster;
5 | import jakarta.annotation.PostConstruct;
6 | import jakarta.persistence.EntityManagerFactory;
7 | import org.hibernate.event.service.spi.EventListenerRegistry;
8 | import org.hibernate.event.spi.*;
9 | import org.hibernate.internal.SessionFactoryImpl;
10 | import org.hibernate.persister.entity.EntityPersister;
11 | import org.springframework.stereotype.Component;
12 |
13 | @Component
14 | public class DataEventListener implements PostCommitInsertEventListener, PostCommitUpdateEventListener, PostCommitDeleteEventListener {
15 |
16 | private final DataEventBroadcaster dataEventBroadcaster;
17 | private final EntityManagerFactory entityManagerFactory;
18 |
19 | @PostConstruct
20 | private void init() {
21 | SessionFactoryImpl sessionFactory = entityManagerFactory.unwrap(SessionFactoryImpl.class);
22 | EventListenerRegistry registry = sessionFactory.getServiceRegistry().getService(EventListenerRegistry.class);
23 | registry.getEventListenerGroup(EventType.POST_COMMIT_INSERT).appendListener(this);
24 | registry.getEventListenerGroup(EventType.POST_COMMIT_UPDATE).appendListener(this);
25 | registry.getEventListenerGroup(EventType.POST_COMMIT_DELETE).appendListener(this);
26 | }
27 |
28 | public DataEventListener(DataEventBroadcaster dataEventBroadcaster, EntityManagerFactory entityManagerFactory) {
29 | this.dataEventBroadcaster = dataEventBroadcaster;
30 | this.entityManagerFactory = entityManagerFactory;
31 | }
32 |
33 | @Override
34 | public void onPostDelete(PostDeleteEvent event) {
35 | if(event.getEntity().getClass().isAnnotationPresent(DataEventEntity.class)) {
36 | DataEventEntity annotation = event.getEntity().getClass().getAnnotation(DataEventEntity.class);
37 | dataEventBroadcaster.broadcastEntityDeleted(event.getEntity(), annotation.deletionTopic());
38 | }
39 | }
40 |
41 | @Override
42 | public void onPostInsert(PostInsertEvent event) {
43 | if(event.getEntity().getClass().isAnnotationPresent(DataEventEntity.class)) {
44 | DataEventEntity annotation = event.getEntity().getClass().getAnnotation(DataEventEntity.class);
45 | dataEventBroadcaster.broadcastEntityCreated(event.getEntity(), annotation.creationTopic());
46 | }
47 | }
48 |
49 | @Override
50 | public void onPostUpdate(PostUpdateEvent event) {
51 | if(event.getEntity().getClass().isAnnotationPresent(DataEventEntity.class)) {
52 | DataEventEntity annotation = event.getEntity().getClass().getAnnotation(DataEventEntity.class);
53 | dataEventBroadcaster.broadcastEntityUpdated(event.getEntity(), annotation.updateTopic());
54 | }
55 | }
56 |
57 | @Override
58 | public boolean requiresPostCommitHandling(EntityPersister persister) {
59 | return true;
60 | }
61 |
62 | @Override
63 | public void onPostInsertCommitFailed(PostInsertEvent event) {
64 | }
65 |
66 | @Override
67 | public void onPostDeleteCommitFailed(PostDeleteEvent event) {
68 |
69 | }
70 |
71 | @Override
72 | public void onPostUpdateCommitFailed(PostUpdateEvent event) {
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/test/java/com/sipios/spring/data/event/broadcaster/DataEventBroadcasterAssociationTests.java:
--------------------------------------------------------------------------------
1 | package com.sipios.spring.data.event.broadcaster;
2 |
3 | import static org.junit.jupiter.api.Assertions.assertEquals;
4 | import static org.mockito.Mockito.*;
5 |
6 | import jakarta.persistence.*;
7 | import lombok.AllArgsConstructor;
8 | import lombok.Getter;
9 | import lombok.NoArgsConstructor;
10 | import lombok.Setter;
11 | import org.json.JSONObject;
12 | import org.junit.jupiter.api.BeforeEach;
13 | import org.junit.jupiter.api.Nested;
14 | import org.junit.jupiter.api.Test;
15 | import org.mockito.ArgumentCaptor;
16 | import org.mockito.Mock;
17 | import org.mockito.MockitoAnnotations;
18 | import org.springframework.kafka.core.KafkaTemplate;
19 | import com.fasterxml.jackson.databind.ObjectMapper;
20 |
21 | import java.util.HashSet;
22 | import java.util.Set;
23 |
24 | public class DataEventBroadcasterAssociationTests {
25 |
26 | private DataEventBroadcaster broadcaster;
27 |
28 | @Mock
29 | private KafkaTemplate kafkaTemplate;
30 |
31 | @BeforeEach
32 | void beforeEach() {
33 | MockitoAnnotations.openMocks(this);
34 | broadcaster = new DataEventBroadcaster(kafkaTemplate, new ObjectMapper());
35 | }
36 |
37 | @Nested
38 | class OneToOneAssociationTests {
39 | @Test
40 | void testBroadcastEntityWithOneToOneAssociation() throws Exception {
41 | Course course = new Course(1, "Test Course");
42 | Student student = new Student(2, "Test Name", course);
43 | course.setStudent(student);
44 |
45 | broadcaster.broadcastEntityCreated(course, "");
46 | String expectedJson = "{\"id\":1,\"title\":\"Test Course\",\"student\":{\"id\":2,\"name\":\"Test Name\",\"course\":1}}";
47 | JSONObject expectedJsonObject = new JSONObject(expectedJson);
48 |
49 | ArgumentCaptor jsonCaptor = ArgumentCaptor.forClass(String.class);
50 | verify(kafkaTemplate).send(eq("course.created"), jsonCaptor.capture());
51 |
52 | String actualJson = jsonCaptor.getValue();
53 | JSONObject actualJsonObject = new JSONObject(actualJson);
54 |
55 | assertEquals(expectedJsonObject.toString(), actualJsonObject.toString());
56 | }
57 |
58 | @Getter
59 | @Setter
60 | @AllArgsConstructor
61 | @NoArgsConstructor
62 | @Entity
63 | class Course {
64 | @Id
65 | private int id;
66 | private String title;
67 |
68 | @OneToOne(cascade = CascadeType.ALL, mappedBy = "course")
69 | private Student student;
70 |
71 | public Course(int id, String title) {
72 | this.id = id;
73 | this.title = title;
74 | }
75 |
76 | public void setStudent(Student student) {
77 | this.student = student;
78 | student.setCourse(this);
79 | }
80 | }
81 |
82 | @Getter
83 | @Setter
84 | @AllArgsConstructor
85 | @NoArgsConstructor
86 | @Entity
87 | class Student {
88 | @Id
89 | private int id;
90 | private String name;
91 |
92 | @OneToOne
93 | private Course course;
94 | }
95 | }
96 |
97 | @Nested
98 | class ManyToManyAssociationTests {
99 | @Test
100 | void testBroadcastEntityWithManyToManyAssociation() throws Exception {
101 | Student student = new Student(1, "Test Name");
102 | Course course = new Course(101, "Test Course");
103 | student.getCourses().add(course);
104 | course.getStudents().add(student);
105 |
106 | broadcaster.broadcastEntityCreated(student, "");
107 | String expectedJson = "{\"id\":1,\"name\":\"Test Name\",\"courses\":[{\"id\":101,\"name\":\"Test Course\",\"students\":[1]}]}";
108 | JSONObject expectedJsonObject = new JSONObject(expectedJson);
109 |
110 | ArgumentCaptor jsonCaptor = ArgumentCaptor.forClass(String.class);
111 | verify(kafkaTemplate).send(eq("student.created"), jsonCaptor.capture());
112 |
113 | String actualJson = jsonCaptor.getValue();
114 | JSONObject actualJsonObject = new JSONObject(actualJson);
115 |
116 | assertEquals(expectedJsonObject.toString(), actualJsonObject.toString());
117 | }
118 |
119 | @Getter
120 | @Setter
121 | @AllArgsConstructor
122 | @NoArgsConstructor
123 | @Entity
124 | class Student {
125 | @Id
126 | private int id;
127 | private String name;
128 |
129 | @ManyToMany(cascade = CascadeType.ALL)
130 | @JoinTable(
131 | name = "enrollment",
132 | joinColumns = @JoinColumn(name = "student_id"),
133 | inverseJoinColumns = @JoinColumn(name = "course_id")
134 | )
135 | private Set courses = new HashSet<>();
136 |
137 | public Student(int id, String name) {
138 | this.id = id;
139 | this.name = name;
140 | }
141 | }
142 |
143 | @Getter
144 | @Setter
145 | @AllArgsConstructor
146 | @NoArgsConstructor
147 | @Entity
148 | class Course {
149 | @Id
150 | private int id;
151 | private String name;
152 |
153 | @ManyToMany(mappedBy = "courses")
154 | private Set students = new HashSet<>();
155 |
156 | public Course(int id, String name) {
157 | this.id = id;
158 | this.name = name;
159 | }
160 | }
161 | }
162 |
163 | @Nested
164 | class OneToManyAssociationTests {
165 | @Test
166 | void testBroadcastEntityWithManyToOneAssociation() throws Exception {
167 | Course course = new Course(1, "Test Course");
168 | Student student = new Student(2, "Test Name", course);
169 | course.getStudents().add(student);
170 |
171 | broadcaster.broadcastEntityCreated(student, "");
172 | String expectedStudentJson = "{\"id\":2,\"name\":\"Test Name\",\"course\":{\"id\":1,\"title\":\"Test Course\", \"students\":[2]}}";
173 | JSONObject expectedStudentJsonObject = new JSONObject(expectedStudentJson);
174 |
175 | ArgumentCaptor studentJsonCaptor = ArgumentCaptor.forClass(String.class);
176 | verify(kafkaTemplate).send(eq("student.created"), studentJsonCaptor.capture());
177 |
178 | String actualStudentJson = studentJsonCaptor.getValue();
179 | JSONObject actualStudentJsonObject = new JSONObject(actualStudentJson);
180 |
181 | assertEquals(expectedStudentJsonObject.toString(), actualStudentJsonObject.toString());
182 | }
183 |
184 | @Test
185 | void testBroadcastEntityWithOneToManyAssociation() throws Exception {
186 | Course course = new Course(1, "Test Course");
187 | Student student = new Student(2, "Test Name", course);
188 | course.getStudents().add(student);
189 |
190 | broadcaster.broadcastEntityCreated(course, "");
191 | String expectedCourseJson = "{\"id\":1,\"title\":\"Test Course\",\"students\":[{\"id\":2,\"name\":\"Test Name\", \"course\":1}]}";
192 | JSONObject expectedCourseJsonObject = new JSONObject(expectedCourseJson);
193 |
194 | ArgumentCaptor courseJsonCaptor = ArgumentCaptor.forClass(String.class);
195 | verify(kafkaTemplate).send(eq("course.created"), courseJsonCaptor.capture());
196 |
197 | String actualCourseJson = courseJsonCaptor.getValue();
198 | JSONObject actualCourseJsonObject = new JSONObject(actualCourseJson);
199 |
200 | assertEquals(expectedCourseJsonObject.toString(), actualCourseJsonObject.toString());
201 | }
202 |
203 | @Getter
204 | @Setter
205 | @AllArgsConstructor
206 | @NoArgsConstructor
207 | @Entity
208 | class Course {
209 | @Id
210 | private int id;
211 | private String title;
212 |
213 | @OneToMany(mappedBy = "course", cascade = CascadeType.ALL)
214 | private Set students = new HashSet<>();
215 |
216 | public Course(int id, String title) {
217 | this.id = id;
218 | this.title = title;
219 | }
220 | }
221 |
222 | @Getter
223 | @Setter
224 | @AllArgsConstructor
225 | @NoArgsConstructor
226 | @Entity
227 | class Student {
228 | @Id
229 | private int id;
230 | private String name;
231 |
232 | @ManyToOne
233 | @JoinColumn(name = "course_id")
234 | private Course course;
235 | }
236 | }
237 | }
238 |
--------------------------------------------------------------------------------
/src/test/java/com/sipios/spring/data/event/broadcaster/DataEventBroadcasterTest.java:
--------------------------------------------------------------------------------
1 | package com.sipios.spring.data.event.broadcaster;
2 |
3 | import static org.junit.jupiter.api.Assertions.assertThrows;
4 | import static org.mockito.Mockito.*;
5 |
6 | import com.fasterxml.jackson.core.JsonProcessingException;
7 | import com.fasterxml.jackson.databind.ObjectMapper;
8 | import lombok.AllArgsConstructor;
9 | import lombok.Getter;
10 | import lombok.Setter;
11 | import org.hibernate.CallbackException;
12 | import org.junit.jupiter.api.BeforeEach;
13 | import org.junit.jupiter.api.Test;
14 | import org.junit.jupiter.params.ParameterizedTest;
15 | import org.junit.jupiter.params.provider.CsvSource;
16 | import org.springframework.kafka.core.KafkaTemplate;
17 |
18 | public class DataEventBroadcasterTest {
19 | private DataEventBroadcaster broadcaster;
20 | private KafkaTemplate kafkaTemplate;
21 | private ObjectMapper objectMapper;
22 |
23 | @BeforeEach
24 | void beforeEach() {
25 | kafkaTemplate = mock(KafkaTemplate.class);
26 | objectMapper = new ObjectMapper();
27 | broadcaster = new DataEventBroadcaster(kafkaTemplate, objectMapper);
28 | }
29 |
30 | @ParameterizedTest
31 | @CsvSource({
32 | "testEntity.created, testEntity.created",
33 | "'', testentity.created"
34 | })
35 | void testBroadcastEntityCreated(String topicLabel, String expectedTopic) throws Exception {
36 | TestEntity entity = new TestEntity(1, "Test Name", true);
37 | String expectedJson = objectMapper.writeValueAsString(entity);
38 |
39 | broadcaster.broadcastEntityCreated(entity, topicLabel);
40 |
41 | verify(kafkaTemplate).send(eq(expectedTopic), eq(expectedJson));
42 | }
43 |
44 | @ParameterizedTest
45 | @CsvSource({
46 | "testEntity.updated, testEntity.updated",
47 | "'', testentity.updated"
48 | })
49 | void testBroadcastEntityUpdated(String topicLabel, String expectedTopic) throws Exception {
50 | TestEntity entity = new TestEntity(1, "Test Name", false);
51 | String expectedJson = objectMapper.writeValueAsString(entity);
52 |
53 | broadcaster.broadcastEntityUpdated(entity, topicLabel);
54 |
55 | verify(kafkaTemplate).send(eq(expectedTopic), eq(expectedJson));
56 | }
57 |
58 | @ParameterizedTest
59 | @CsvSource({
60 | "testEntity.deleted, testEntity.deleted",
61 | "'', testentity.deleted"
62 | })
63 | void testBroadcastEntityDeleted(String topicLabel, String expectedTopic) throws Exception {
64 | TestEntity entity = new TestEntity(1, "Test Name", true);
65 | String expectedJson = objectMapper.writeValueAsString(entity);
66 |
67 | broadcaster.broadcastEntityDeleted(entity, topicLabel);
68 |
69 | verify(kafkaTemplate).send(eq(expectedTopic), eq(expectedJson));
70 | }
71 |
72 | @Test
73 | void testBroadcastEntityCreatedJsonProcessingException() throws Exception {
74 | objectMapper = mock(ObjectMapper.class);
75 | when(objectMapper.copy()).thenReturn(objectMapper);
76 | broadcaster = new DataEventBroadcaster(kafkaTemplate, objectMapper);
77 | TestEntity entity = new TestEntity(1, "Test Name", true);
78 |
79 | when(objectMapper.writeValueAsString(entity)).thenThrow(new JsonProcessingException("JSON processing error") {});
80 |
81 | assertThrows(CallbackException.class, () -> broadcaster.broadcastEntityCreated(entity, "testEntity.created"));
82 | }
83 |
84 | @Test
85 | void testBroadcastEntityUpdatedJsonProcessingException() throws Exception {
86 | objectMapper = mock(ObjectMapper.class);
87 | when(objectMapper.copy()).thenReturn(objectMapper);
88 | broadcaster = new DataEventBroadcaster(kafkaTemplate, objectMapper);
89 | TestEntity entity = new TestEntity(1, "Test Name", false);
90 |
91 | when(objectMapper.writeValueAsString(entity)).thenThrow(new JsonProcessingException("JSON processing error") {});
92 |
93 | assertThrows(CallbackException.class, () -> broadcaster.broadcastEntityUpdated(entity, "testEntity.updated"));
94 | }
95 |
96 | @Test
97 | void testBroadcastEntityDeletedJsonProcessingException() throws Exception {
98 | objectMapper = mock(ObjectMapper.class);
99 | when(objectMapper.copy()).thenReturn(objectMapper);
100 | broadcaster = new DataEventBroadcaster(kafkaTemplate, objectMapper);
101 | TestEntity entity = new TestEntity(1, "Test Name", true);
102 |
103 | when(objectMapper.writeValueAsString(entity)).thenThrow(new JsonProcessingException("JSON processing error") {});
104 |
105 | assertThrows(CallbackException.class, () -> broadcaster.broadcastEntityDeleted(entity, "testEntity.deleted"));
106 | }
107 |
108 | @Getter
109 | @Setter
110 | @AllArgsConstructor
111 | public static class TestEntity {
112 | private int id;
113 | private String name;
114 | private boolean active;
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/src/test/java/com/sipios/spring/data/event/listener/DataEventListenerTest.java:
--------------------------------------------------------------------------------
1 | package com.sipios.spring.data.event.listener;
2 |
3 | import com.sipios.spring.data.event.annotation.DataEventEntity;
4 | import com.sipios.spring.data.event.broadcaster.DataEventBroadcaster;
5 | import lombok.AllArgsConstructor;
6 | import lombok.Getter;
7 | import lombok.Setter;
8 | import org.hibernate.event.spi.PostDeleteEvent;
9 | import org.hibernate.event.spi.PostInsertEvent;
10 | import org.hibernate.event.spi.PostUpdateEvent;
11 | import org.junit.jupiter.api.Nested;
12 | import org.junit.jupiter.api.Test;
13 | import org.junit.jupiter.api.extension.ExtendWith;
14 | import org.mockito.InjectMocks;
15 | import org.mockito.Mock;
16 | import org.mockito.junit.jupiter.MockitoExtension;
17 |
18 | import static org.junit.jupiter.api.Assertions.assertThrows;
19 | import static org.mockito.Mockito.*;
20 |
21 | @ExtendWith(MockitoExtension.class)
22 | public class DataEventListenerTest {
23 | @Mock
24 | private DataEventBroadcaster dataEventBroadcaster;
25 |
26 | @InjectMocks
27 | private DataEventListener listener;
28 |
29 | @Nested
30 | class AnnotatedEntityTests {
31 |
32 | @Test
33 | public void testOnPostInsert() {
34 | PostInsertEvent event = mock(PostInsertEvent.class);
35 | TestEntity entity = new TestEntity(1, "Test Name", true);
36 | when(event.getEntity()).thenReturn(entity);
37 |
38 | listener.onPostInsert(event);
39 |
40 | verify(dataEventBroadcaster).broadcastEntityCreated(entity, "testEntity.created");
41 | }
42 |
43 | @Test
44 | public void testOnPostUpdate() {
45 | PostUpdateEvent event = mock(PostUpdateEvent.class);
46 | TestEntity entity = new TestEntity(1, "Test Name", true);
47 | when(event.getEntity()).thenReturn(entity);
48 |
49 | listener.onPostUpdate(event);
50 |
51 | verify(dataEventBroadcaster).broadcastEntityUpdated(entity, "testEntity.updated");
52 | }
53 |
54 | @Test
55 | public void testOnPostDelete() {
56 | PostDeleteEvent event = mock(PostDeleteEvent.class);
57 | TestEntity entity = new TestEntity(1, "Test Name", true);
58 | when(event.getEntity()).thenReturn(entity);
59 |
60 | listener.onPostDelete(event);
61 |
62 | verify(dataEventBroadcaster).broadcastEntityDeleted(entity, "testEntity.deleted");
63 | }
64 |
65 | @Test
66 | public void testOnPostInsertJsonProcessingException() throws Exception {
67 | PostInsertEvent event = mock(PostInsertEvent.class);
68 | TestEntity entity = new TestEntity(1, "Test Name", true);
69 | when(event.getEntity()).thenReturn(entity);
70 | doThrow(new RuntimeException("JSON processing error")).when(dataEventBroadcaster).broadcastEntityCreated(any(), any());
71 |
72 | assertThrows(RuntimeException.class, () -> listener.onPostInsert(event));
73 | }
74 |
75 | @Test
76 | public void testOnPostUpdateJsonProcessingException() throws Exception {
77 | PostUpdateEvent event = mock(PostUpdateEvent.class);
78 | TestEntity entity = new TestEntity(1, "Test Name", true);
79 | when(event.getEntity()).thenReturn(entity);
80 | doThrow(new RuntimeException("JSON processing error")).when(dataEventBroadcaster).broadcastEntityUpdated(any(), any());
81 |
82 | assertThrows(RuntimeException.class, () -> listener.onPostUpdate(event));
83 | }
84 |
85 | @Test
86 | public void testOnPostDeleteJsonProcessingException() throws Exception {
87 | PostDeleteEvent event = mock(PostDeleteEvent.class);
88 | TestEntity entity = new TestEntity(1, "Test Name", true);
89 | when(event.getEntity()).thenReturn(entity);
90 | doThrow(new RuntimeException("JSON processing error")).when(dataEventBroadcaster).broadcastEntityDeleted(any(), any());
91 |
92 | assertThrows(RuntimeException.class, () -> listener.onPostDelete(event));
93 | }
94 |
95 | @DataEventEntity(creationTopic = "testEntity.created", updateTopic = "testEntity.updated", deletionTopic = "testEntity.deleted")
96 | @Getter
97 | @Setter
98 | @AllArgsConstructor
99 | private static class TestEntity {
100 | private int id;
101 | private String name;
102 | private boolean active;
103 | }
104 | }
105 |
106 | @Nested
107 | class NonAnnotatedEntityTests {
108 |
109 | @Test
110 | public void testOnPostInsert() {
111 | PostInsertEvent event = mock(PostInsertEvent.class);
112 | NonAnnotatedEntity entity = new NonAnnotatedEntity(1, "Test Name", true);
113 | when(event.getEntity()).thenReturn(entity);
114 |
115 | listener.onPostInsert(event);
116 |
117 | verify(dataEventBroadcaster, never()).broadcastEntityCreated(any(), any());
118 | }
119 |
120 | @Test
121 | public void testOnPostUpdate() {
122 | PostUpdateEvent event = mock(PostUpdateEvent.class);
123 | NonAnnotatedEntity entity = new NonAnnotatedEntity(1, "Test Name", true);
124 | when(event.getEntity()).thenReturn(entity);
125 |
126 | listener.onPostUpdate(event);
127 |
128 | verify(dataEventBroadcaster, never()).broadcastEntityUpdated(any(), any());
129 | }
130 |
131 | @Test
132 | public void testOnPostDelete() {
133 | PostDeleteEvent event = mock(PostDeleteEvent.class);
134 | NonAnnotatedEntity entity = new NonAnnotatedEntity(1, "Test Name", true);
135 | when(event.getEntity()).thenReturn(entity);
136 |
137 | listener.onPostDelete(event);
138 |
139 | verify(dataEventBroadcaster, never()).broadcastEntityDeleted(any(), any());
140 | }
141 |
142 | @Getter
143 | @Setter
144 | @AllArgsConstructor
145 | private static class NonAnnotatedEntity {
146 | private int id;
147 | private String name;
148 | private boolean active;
149 | }
150 | }
151 | }
152 |
--------------------------------------------------------------------------------