Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.FileSystemResource;

import java.util.Properties;

/**
Expand Down
13 changes: 7 additions & 6 deletions api/src/main/java/org/openmrs/event/AggregatedEntityEvent.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
*/
package org.openmrs.event;

import java.util.HashSet;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

Expand All @@ -18,21 +18,23 @@
* <p>
* It needs to be used together with {@link TransactionalEventAggregator}.
*
* @since 2.9.x
* @since 2.9.0
*/
public abstract class AggregatedEntityEvent extends BaseEvent {
public abstract class AggregatedEntityEvent extends BaseSessionEvent {
private static final long serialVersionUID = 1L;

private List<EntityEvent<?>> events;

public AggregatedEntityEvent() {

Check warning on line 28 in api/src/main/java/org/openmrs/event/AggregatedEntityEvent.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Change the visibility of this constructor to "protected".

See more on https://sonarcloud.io/project/issues?id=openmrs_openmrs-core&issues=AZ6HwKJ0abZcyaJzhr7F&open=AZ6HwKJ0abZcyaJzhr7F&pullRequest=6151
this.events = new ArrayList<>();
}

public AggregatedEntityEvent(List<EntityEvent<?>> events) {

Check warning on line 32 in api/src/main/java/org/openmrs/event/AggregatedEntityEvent.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Change the visibility of this constructor to "protected".

See more on https://sonarcloud.io/project/issues?id=openmrs_openmrs-core&issues=AZ6HwKJ0abZcyaJzhr7G&open=AZ6HwKJ0abZcyaJzhr7G&pullRequest=6151
this(events, new HashSet<>());
this.events = events;
}

public AggregatedEntityEvent(List<EntityEvent<?>> events, Set<String> tags) {

Check warning on line 36 in api/src/main/java/org/openmrs/event/AggregatedEntityEvent.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Change the visibility of this constructor to "protected".

See more on https://sonarcloud.io/project/issues?id=openmrs_openmrs-core&issues=AZ6HwKJ0abZcyaJzhr7H&open=AZ6HwKJ0abZcyaJzhr7H&pullRequest=6151
super(events, tags);
super(tags);
this.events = events;
}

Expand All @@ -42,6 +44,5 @@

public void setEvents(List<EntityEvent<?>> events) {
this.events = events;
this.source = events;
}
}
29 changes: 7 additions & 22 deletions api/src/main/java/org/openmrs/event/BaseEvent.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
package org.openmrs.event;

import org.openmrs.event.outbox.OutboxableEvent;
import org.springframework.context.ApplicationEvent;

import java.util.HashSet;
import java.util.Set;
Expand All @@ -20,36 +19,22 @@
* <p>
* Extend {@link EntityEvent} or {@link AggregatedEntityEvent} instead.
*
* @since 2.9.x
* @since 2.9.0
*/
class BaseEvent extends ApplicationEvent implements OutboxableEvent {

class BaseEvent implements OutboxableEvent {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to Spring docs extending ApplicationEvent is no longer recommended or needed for events.

private static final long serialVersionUID = 1L;

protected final Set<String> tags;
private String sessionId;

public BaseEvent() {
this(""); //empty for deserialization to work
}

public BaseEvent(Object source) {
this(source, new HashSet<>());
this.tags = new HashSet<>();
}

public BaseEvent(Object source, Set<String> tags) {
super(source);
public BaseEvent(Set<String> tags) {
this.tags = tags;
}



public Set<String> getTags() {
return tags;
}

public String getSessionId() {
return sessionId;
}

public void setSessionId(String sessionId) {
this.sessionId = sessionId;
}
}
46 changes: 46 additions & 0 deletions api/src/main/java/org/openmrs/event/BaseSessionEvent.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
* the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
*
* Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
* graphic logo is a trademark of OpenMRS Inc.
*/
package org.openmrs.event;

import java.util.Set;

/**
* See {@link EntityEvent} or {@link AggregatedEntityEvent}.
*
* @since 2.9.0
*/
public abstract class BaseSessionEvent extends BaseEvent {

@rkorytkowski rkorytkowski Jun 2, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving sessionId to BaseSessionEvent since BaseEvent is now extended by CDCEvent, for which sessionId does not make any sense as it is published by an asynchronous task.

private static final long serialVersionUID = 1L;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consistency nit: BaseEvent is Serializable (via OutboxableEvent), but only this class declares serialVersionUIDBaseEvent, CDCEvent and the two CDCTransaction*Events don't. Add it everywhere or drop it here.

Also: double blank line before getSessionId(), and the (String sessionId, Set<String> tags) constructor has no callers (fine if intended as API).

protected String sessionId;

/**
* Default constructor for deserialization.
*/
public BaseSessionEvent() {

Check warning on line 27 in api/src/main/java/org/openmrs/event/BaseSessionEvent.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Change the visibility of this constructor to "protected".

See more on https://sonarcloud.io/project/issues?id=openmrs_openmrs-core&issues=AZ6HwKLeabZcyaJzhr7I&open=AZ6HwKLeabZcyaJzhr7I&pullRequest=6151
}

public BaseSessionEvent(Set<String> tags) {

Check warning on line 30 in api/src/main/java/org/openmrs/event/BaseSessionEvent.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Change the visibility of this constructor to "protected".

See more on https://sonarcloud.io/project/issues?id=openmrs_openmrs-core&issues=AZ6HwKLeabZcyaJzhr7J&open=AZ6HwKLeabZcyaJzhr7J&pullRequest=6151
super(tags);
}

public BaseSessionEvent(String sessionId, Set<String> tags) {

Check warning on line 34 in api/src/main/java/org/openmrs/event/BaseSessionEvent.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Change the visibility of this constructor to "protected".

See more on https://sonarcloud.io/project/issues?id=openmrs_openmrs-core&issues=AZ6HwKLeabZcyaJzhr7K&open=AZ6HwKLeabZcyaJzhr7K&pullRequest=6151
super(tags);
this.sessionId = sessionId;
}

public String getSessionId() {
return sessionId;
}

public void setSessionId(String sessionId) {
this.sessionId = sessionId;
}
}
164 changes: 164 additions & 0 deletions api/src/main/java/org/openmrs/event/CDCEvent.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
* the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
*
* Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
* graphic logo is a trademark of OpenMRS Inc.
*/
package org.openmrs.event;

import org.codehaus.jackson.annotate.JsonIgnore;
import org.jspecify.annotations.Nullable;
import org.springframework.core.ResolvableType;
import org.springframework.core.ResolvableTypeProvider;

import java.util.Map;
import java.util.Set;

/**
* Issued by the CDC engine (if available, see e.g. https://github.com/openmrs/openmrs-module-debezium)
* <p>
* Please note that CDC events are usually not happening inside an application transaction rather
* they are fetched from DB by a separate process e.g. Debezium. For this reason it does not make sense
* to use {@link org.springframework.transaction.event.TransactionalEventListener} to process them.
* <p>
* If you want to guarantee delivery, please use {@link org.springframework.context.event.EventListener}
* so that an event is processed in the same thread as the publisher and if the processing fails, it will be
* re-tried by the publisher. It is recommended only for fast enough listeners so that publisher is not
* blocked for too long, and it keeps up with the events queue.
* <p>
* If you combine {@link org.springframework.context.event.EventListener} with {@link org.springframework.scheduling.annotation.Async}
* you may run longer processing, but you lose the ability to re-try if anything fails, and in worst case you may lose the event.
* <p>
* For longer running processing with guaranteed delivery you may use {@link org.openmrs.event.outbox.OutboxEventListener}
* to persist events to the database and have them processed asynchronously or publish events with
* {@link org.springframework.context.event.EventListener} to a message broker for asynchronous processing.
* The latter is more performant, and it can also be used to exchange messages with external systems.
*
* @param <T> entityType associated with this event
*
* @since 2.9.0
*/
public class CDCEvent<T> extends BaseEvent implements ResolvableTypeProvider {
private static final long serialVersionUID = 1L;

private String snapshot;

private Class<T> entityType;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It makes it easier to listen to events for certain entity types without a need for conditions in code.


private String transactionId;

private Operation operation;

private String tableName;

private Map<String, Object> primaryKey;

Check failure on line 57 in api/src/main/java/org/openmrs/event/CDCEvent.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Make non-static "primaryKey" transient or serializable.

See more on https://sonarcloud.io/project/issues?id=openmrs_openmrs-core&issues=AZ6HwKPgabZcyaJzhr7O&open=AZ6HwKPgabZcyaJzhr7O&pullRequest=6151

private Map<String, Object> newState;

Check failure on line 59 in api/src/main/java/org/openmrs/event/CDCEvent.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Make non-static "newState" transient or serializable.

See more on https://sonarcloud.io/project/issues?id=openmrs_openmrs-core&issues=AZ6HwKPgabZcyaJzhr7P&open=AZ6HwKPgabZcyaJzhr7P&pullRequest=6151

private Map<String, Object> previousState;

Check failure on line 61 in api/src/main/java/org/openmrs/event/CDCEvent.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Make non-static "previousState" transient or serializable.

See more on https://sonarcloud.io/project/issues?id=openmrs_openmrs-core&issues=AZ6HwKPgabZcyaJzhr7Q&open=AZ6HwKPgabZcyaJzhr7Q&pullRequest=6151

/**
* Default constructor for deserialization.
*/
public CDCEvent() {
}

public CDCEvent(Class<T> entityType) {
this.entityType = entityType;
}

public CDCEvent(Class<T> entityType, Set<String> tags) {
super(tags);
this.entityType = entityType;
}

public void setPrimaryKey(Map<String, Object> primaryKey) {
this.primaryKey = primaryKey;
}

public Map<String, Object> getPrimaryKey() {
return primaryKey;
}

public boolean isCompositePrimaryKey() {
return primaryKey != null && primaryKey.size() > 1;
}

public void setEntityType(Class<T> entityType) {
this.entityType = entityType;
}

public Class<T> getEntityType() {
return entityType;
}

public void setTransactionId(String transactionId) {
this.transactionId = transactionId;
}

public String getTransactionId() {
return transactionId;
}

public void setTableName(String tableName) {
this.tableName = tableName;
}

public String getTableName() {
return tableName;
}

public void setOperation(Operation operation) {
this.operation = operation;
}

public Operation getOperation() {
return operation;
}

/**
* Indicates that the event is an initial data synchronization.
*
* @return true, first or last for snapshot event, false or null otherwise
*/
public String getSnapshot() {
return snapshot;
}

public void setSnapshot(String snapshot) {
this.snapshot = snapshot;
}

public Map<String, Object> getNewState() {
return newState;
}

public void setNewState(Map<String, Object> newState) {
this.newState = newState;
}

public Map<String, Object> getPreviousState() {
return previousState;
}

public void setPreviousState(Map<String, Object> previousState) {
this.previousState = previousState;
}

public enum Operation {
READ, CREATE, UPDATE, DELETE, TRUNCATE

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is TRUNCATE used for?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See https://debezium.io/documentation/reference/stable/connectors/mysql.html#mysql-truncate-events not useful to us at the moment, but added as suggested by AI for completeness.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok 👍

}

@JsonIgnore
@Override
public @Nullable ResolvableType getResolvableType() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest @JsonIgnore here (and on the pre-existing EntityEvent.getResolvableType()): serializing this class with a plain ObjectMapper fails with InvalidDefinitionException: Direct self-reference leading to cycle (...resolvableType->ResolvableType["componentType"]->...) — reproduced locally against this branch. It only works through the outbox today because JobRunrConfig's JacksonJsonMapper mutates the shared ObjectMapper bean to field-based visibility, so getters are never serialized. Since the class javadoc recommends forwarding events to message brokers, the first consumer to use a standard mapper will hit this.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added @JsonIgnore to getResolvableType

if (entityType != null && getClass().getTypeParameters().length == 1) {
return ResolvableType.forClassWithGenerics(getClass(), ResolvableType.forClass(entityType));
} else {
return ResolvableType.forClass(getClass());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
* the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
*
* Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
* graphic logo is a trademark of OpenMRS Inc.
*/
package org.openmrs.event;

/**
* Issued by the CDC engine upon completed transaction. It can be used to gather
* events belonging to the same transaction.
*
* @since 2.9.0
*/
public class CDCTransactionCompletedEvent extends BaseEvent {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we also need one for transaction rolled back?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No CDC for roll back. The DB log contains only committed data.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh i see! 😊


private static final long serialVersionUID = 1L;

private String transactionId;

public CDCTransactionCompletedEvent() {
}

public CDCTransactionCompletedEvent(String transactionId) {
this.transactionId = transactionId;
}

public String getTransactionId() {
return transactionId;
}

public void setTransactionId(String transactionId) {
this.transactionId = transactionId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
* the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
*
* Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
* graphic logo is a trademark of OpenMRS Inc.
*/
package org.openmrs.event;

/**
* Issued by the CDC engine upon started transaction.
*
* @since 2.9.0
*/
public class CDCTransactionStartedEvent extends BaseEvent {

private static final long serialVersionUID = 1L;

private String transactionId;

public CDCTransactionStartedEvent() {
}

public CDCTransactionStartedEvent(String transactionId) {
this.transactionId = transactionId;
}

public String getTransactionId() {
return transactionId;
}

public void setTransactionId(String transactionId) {
this.transactionId = transactionId;
}
}
Loading
Loading