55import io .debezium .engine .ChangeEvent ;
66import io .debezium .engine .DebeziumEngine ;
77import io .debezium .engine .format .Json ;
8+ import lombok .SneakyThrows ;
89import me .zort .sqllib .SQLDatabaseConnection ;
910import me .zort .sqllib .SQLDatabaseConnectionImpl ;
11+ import me .zort .sqllib .debezium .builder .EntityFilterBuilder ;
1012import org .jetbrains .annotations .NotNull ;
1113
14+ import java .lang .reflect .AnnotatedElement ;
15+ import java .net .URI ;
1216import java .sql .Connection ;
17+ import java .util .HashMap ;
1318import java .util .List ;
19+ import java .util .Map ;
20+ import java .util .concurrent .CompletableFuture ;
1421import java .util .concurrent .ExecutorService ;
1522import java .util .concurrent .Executors ;
23+ import java .util .function .Consumer ;
1624import java .util .function .Function ;
1725
26+ /**
27+ * Service that uses debezium engine to listen for changes of row changes in
28+ * a database.
29+ *
30+ * <p>Example usage:
31+ * <pre>{@code
32+ * ExecutorService executor = Executors.newSingleThreadExecutor();
33+ * ASQLDebeziumService service = ASQLDebeziumService.configure(connection)
34+ * .connector(ASQLDebeziumService.ConnectorType.MYSQL)
35+ * .executor(executor)
36+ * .build();
37+ *
38+ * </pre>
39+ *
40+ * @author ZorTik
41+ */
1842@ Beta
19- public final class ASQLDebeziumService implements DebeziumEngine .ChangeConsumer <ChangeEvent <String , String >> {
43+ public final class ASQLDebeziumService implements
44+ DebeziumEngine .ChangeConsumer <ChangeEvent <String , String >>, Runnable {
2045
21- public static @ NotNull Builder configure (SQLDatabaseConnection connection ) {
46+ @ SneakyThrows
47+ public static @ NotNull Builder configure (@ NotNull SQLDatabaseConnection connection ) {
2248 if (!(connection instanceof SQLDatabaseConnectionImpl )) {
2349 throw new IllegalArgumentException ("Connection does not contain options!" );
50+ } else if (!connection .isConnected ()) {
51+ throw new IllegalArgumentException ("Connection is not connected!" );
2452 }
2553 Connection rawConnection = connection .getConnection ();
54+ URI uri = new URI (rawConnection .getMetaData ().getURL ());
2655 Configuration .Builder configBuilder = Configuration .create ()
27- .with ("database.hostname" , "TODO" );
56+ .with ("database.hostname" , uri .getHost ())
57+ .with ("database.port" , uri .getPort ());
2858 // TODO: Build configuration builder from raw connection details
2959 return new Builder (configBuilder );
3060 }
3161
3262 private final DebeziumEngine <ChangeEvent <String , String >> engine ;
33- private final ExecutorService executor ;
34- private boolean running = false ;
63+ private final Map <RecordFilter , Consumer <ChangeEvent <String , String >>> handlers ;
3564
36- private ASQLDebeziumService (DebeziumEngine .Builder <ChangeEvent <String , String >> builder , ExecutorService executor ) {
65+ private ASQLDebeziumService (DebeziumEngine .Builder <ChangeEvent <String , String >> builder ) {
3766 this .engine = builder .notifying (this ).build ();
38- this .executor = executor ;
67+ this .handlers = null ;
3968 }
4069
4170 @ Override
@@ -44,31 +73,48 @@ public void handleBatch(
4473 DebeziumEngine .RecordCommitter <ChangeEvent <String , String >> committer
4574 ) throws InterruptedException {
4675 for (ChangeEvent <String , String > record : records ) {
47- // TODO
76+ new HashMap <>(handlers ).keySet ().forEach (filter -> {
77+ if (filter .expired ()) {
78+ handlers .remove (filter );
79+ } else if (filter .test (record )) {
80+ handlers .remove (filter ).accept (record );
81+ }
82+ });
4883 committer .markProcessed (record );
4984 }
5085 committer .markBatchFinished ();
5186 }
5287
53- public void start () {
54- if (executor .isShutdown () || executor .isTerminated ()) {
55- throw new IllegalStateException ("Executor is not running!" );
56- } else if (running ) {
57- throw new IllegalStateException ("Debezium service is already running!" );
58- }
59- executor .submit (engine );
60- running = true ;
88+ @ Override
89+ public void run () {
90+ engine .run ();
91+ }
92+
93+ /**
94+ * This method registers event handler to be executed when provided filter matches
95+ * the event. Note that the handler will be executed only once, then removed.
96+ *
97+ * @param filter Filter to match
98+ * @return Future accepting the event
99+ */
100+ public @ NotNull CompletableFuture <ChangeEvent <String , String >> awaitChange (RecordFilter filter ) {
101+ CompletableFuture <ChangeEvent <String , String >> future = new CompletableFuture <>();
102+ filter .markRegistered ();
103+ handlers .put (filter , future ::complete );
104+ return future ;
105+ }
106+
107+ public @ NotNull EntityFilterBuilder watch (AnnotatedElement entityElement ) {
108+ return new EntityFilterBuilder (this , entityElement );
61109 }
62110
63111 public static class Builder {
64112
65113 private static int serviceCount = 0 ;
66114 private Configuration .Builder config ;
67- private ExecutorService executor ;
68115
69116 private Builder (Configuration .Builder initialConfig ) {
70117 this .config = initialConfig ;
71- this .executor = null ;
72118 edit (builder -> builder
73119 .with ("name" , "Asql-Debezium-" + (++serviceCount )));
74120 }
@@ -84,18 +130,10 @@ private Builder(Configuration.Builder initialConfig) {
84130 return edit (builder -> builder .with ("connector.class" , type .getClassName ()));
85131 }
86132
87- public @ NotNull Builder executor (ExecutorService executor ) {
88- this .executor = executor ;
89- return this ;
90- }
91-
92133 public @ NotNull ASQLDebeziumService build () {
93134 DebeziumEngine .Builder <ChangeEvent <String , String >> builder = DebeziumEngine .create (Json .class )
94135 .using (config .build ().asProperties ());
95- return new ASQLDebeziumService (
96- builder ,
97- executor != null ? executor : Executors .newCachedThreadPool ()
98- );
136+ return new ASQLDebeziumService (builder );
99137 }
100138 }
101139
0 commit comments