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
4 changes: 2 additions & 2 deletions annotations/src/main/java/dev/morphia/EntityListener.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
/**
* Marks a lifecycle listener. A listener implements one or more of the per-event interfaces, {@code PrePersistListener},
* {@code PostPersistListener}, {@code PreLoadListener} and {@code PostLoadListener}, and Morphia calls it for those events
* alone. A class listed in {@link dev.morphia.annotations.EntityListeners @EntityListeners} may instead, or as well, declare
* methods annotated with a lifecycle event annotation.
* alone. A class listed in {@link dev.morphia.annotations.EntityListeners @EntityListeners} must implement at least one of them;
* only an entity's own methods may be annotated with a lifecycle event annotation.
*
* @param <T> the type of the entities the listener accepts
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
import dev.morphia.EntityListener;

/**
* Specifies other classes to participate in the @Entity's lifecycle
* Specifies other classes to participate in the @Entity's lifecycle. Each class implements one or more of the per-event listener
* interfaces, such as {@code PrePersistListener}, and has a no-argument constructor. Morphia creates an instance of each when it
* maps the entity. A listed class may not declare methods annotated with a lifecycle event annotation: only an entity's own
* methods can be.
*/
@Documented
@Inherited
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
* Called after the data has been loaded into the java object. This is a good place to perform
* initialization and sanity-checking of the object.
* <p>
* Only an entity's own methods may carry this annotation. An {@link EntityListeners @EntityListeners} class implements the
* event's listener interface instead.
* <p>
* Methods with this annotation may optionally take a parameter of type {@link org.bson.Document Document}
* and/or a parameter of type Datastore. If both parameters are used,
* the order is unimportant.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
* Called after the data has been persisted from the java object. Here you can alter the
* BSON Document before it is saved.
* <p>
* Only an entity's own methods may carry this annotation. An {@link EntityListeners @EntityListeners} class implements the
* event's listener interface instead.
* <p>
* Methods with this annotation may optionally take a parameter of type {@link org.bson.Document Document}
* and/or a parameter of type Datastore. If both parameters are used, the
* order is unimportant. For typical usage, the method should take a Document parameter.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
* Called before the data has been loaded into the object. Here you can alter the raw BSON Document from the
* datastore prior to object initialization.
* <p>
* Only an entity's own methods may carry this annotation. An {@link EntityListeners @EntityListeners} class implements the
* event's listener interface instead.
* <p>
* Methods with this annotation may optionally take a parameter of type {@link org.bson.Document Document}
* and/or a parameter of type Datastore. If both parameters are used, the
* order is unimportant. For typical usage, the method should take a Document parameter.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
* Called before the data has been persisted to the datastore (before mapping is done).
* Here you can alter your class members prior to being saved.
* <p>
* Only an entity's own methods may carry this annotation. An {@link EntityListeners @EntityListeners} class implements the
* event's listener interface instead.
* <p>
* Methods with this annotation may optionally take a parameter of type
* Datastore. This is the Datastore that
* data will be persisted to.
Expand Down
41 changes: 26 additions & 15 deletions core/src/main/java/dev/morphia/mapping/codec/pojo/EntityModel.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,13 @@
import dev.morphia.mapping.conventions.MorphiaConvention;
import dev.morphia.mapping.conventions.MorphiaDefaultsConvention;
import dev.morphia.mapping.lifecycle.EntityListenerAdapter;
import dev.morphia.mapping.lifecycle.OnEntityListenerAdapter;
import dev.morphia.mapping.lifecycle.TypedListeners;
import dev.morphia.sofia.Sofia;

import org.bson.Document;

import static dev.morphia.mapping.Mapper.LIFECYCLE_ANNOTATIONS;
import static dev.morphia.mapping.PropertyDiscovery.FIELDS;

Check warning on line 58 in core/src/main/java/dev/morphia/mapping/codec/pojo/EntityModel.java

View workflow job for this annotation

GitHub Actions / Critter (AOT)

dev.morphia.mapping.PropertyDiscovery in dev.morphia.mapping has been deprecated

Check warning on line 58 in core/src/main/java/dev/morphia/mapping/codec/pojo/EntityModel.java

View workflow job for this annotation

GitHub Actions / CodeAudits / Code Audits

dev.morphia.mapping.PropertyDiscovery in dev.morphia.mapping has been deprecated

Check warning on line 58 in core/src/main/java/dev/morphia/mapping/codec/pojo/EntityModel.java

View workflow job for this annotation

GitHub Actions / Build / Build

dev.morphia.mapping.PropertyDiscovery in dev.morphia.mapping has been deprecated
import static java.lang.String.format;
import static java.util.Arrays.stream;
import static java.util.Collections.emptyList;
Expand Down Expand Up @@ -98,7 +97,11 @@
private EntityModel superClass;
private PropertyModel idProperty;
private PropertyModel versionProperty;
private final List<EntityListenerAdapter> listeners = new ArrayList<>();
// the entity's own lifecycle methods, or null when it has none
@Nullable
private EntityListenerAdapter ownListener;
// instances of its @EntityListeners classes
private final List<EntityListener<?>> listeners = new ArrayList<>();
// bit i set when a listener handles Mapper.LIFECYCLE_ANNOTATIONS[i]; worked out when the listeners are set up
private int lifecycleEvents;
// bit i set when a listener's method for Mapper.LIFECYCLE_ANNOTATIONS[i] takes the Document
Expand Down Expand Up @@ -257,8 +260,11 @@
public void callLifecycleMethods(Class<? extends Annotation> event, Object entity, @Nullable Document document,
MorphiaDatastore datastore) {
if (hasLifecycle(event)) {
for (EntityListenerAdapter listener : listeners) {
listener.invoke(event, entity, document, datastore);
for (EntityListener<?> listener : listeners) {
TypedListeners.invoke(listener, event, entity, document, datastore);
}
if (ownListener != null) {
ownListener.invoke(event, entity, document, datastore);
}
}
for (EntityListener<?> listener : datastore.getMapper().getGlobalListeners()) {
Expand Down Expand Up @@ -603,35 +609,40 @@
}

/**
* Populates the {@code listeners} list from {@code @EntityListeners} and on-entity lifecycle methods.
* Creates the instances of the {@code @EntityListeners} classes and finds the entity's own lifecycle methods.
* Subclasses that bypass the normal {@link MappingUtil} pipeline (e.g. {@code CritterEntityModel}) must
* call this after their properties have been set up.
*
* @throws MappingException if an {@code @EntityListeners} class can't be created, implements none of the per-event
* listener interfaces, or declares annotated lifecycle methods
*/
protected void initializeListeners() {
final EntityListeners entityLisAnn = getAnnotation(EntityListeners.class);
if (entityLisAnn != null) {
for (Class<?> aClass : entityLisAnn.value()) {
listeners.add(new EntityListenerAdapter(aClass));
for (Class<? extends EntityListener<?>> type : entityLisAnn.value()) {
listeners.add(EntityListenerAdapter.create(type));
}
}

OnEntityListenerAdapter adapter = OnEntityListenerAdapter.listen(getType());
if (adapter != null) {
listeners.add(adapter);
}
ownListener = EntityListenerAdapter.listen(getType());

int events = 0;
int documents = 0;
for (int i = 0; i < LIFECYCLE_ANNOTATIONS.size(); i++) {
Class<? extends Annotation> event = LIFECYCLE_ANNOTATIONS.get(i);
for (EntityListenerAdapter listener : listeners) {
if (listener.handles(event)) {
for (EntityListener<?> listener : listeners) {
if (TypedListeners.handles(listener.getClass(), event, getType())) {
events |= 1 << i;
if (listener.usesDocument(event, getType())) {
if (TypedListeners.usesDocument(listener.getClass(), event, getType())) {
documents |= 1 << i;
}
}
}
if (ownListener != null && ownListener.handles(event)) {
events |= 1 << i;
if (ownListener.usesDocument(event, getType())) {
documents |= 1 << i;
}
}
}
lifecycleEvents = events;
documentEvents = documents;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
Expand All @@ -21,49 +22,87 @@
import org.bson.Document;

import static dev.morphia.mapping.Mapper.LIFECYCLE_ANNOTATIONS;
import static java.lang.String.format;

/**
* Calls the lifecycle methods of an {@code @EntityListeners} class, or of an entity: its methods annotated with a lifecycle
* event annotation, and those of any per-event listener interface it implements. It implements {@link EntityListener} only so
* it can be named in {@code @EntityListeners}.
* Calls an entity's own lifecycle methods: its methods annotated with a lifecycle event annotation, and those of any per-event
* listener interface it implements. Also creates the instances of {@code @EntityListeners} classes, which implement the
* interfaces only.
*
* @hidden
* @since 2.4
* @morphia.internal
*/
@MorphiaInternal
public class EntityListenerAdapter implements EntityListener<Object> {
public final class EntityListenerAdapter {
private final Map<Class<? extends Annotation>, List<Callback>> callbacks = new HashMap<>();
private final Class<?> listenerType;
// whether it implements a per-event listener interface
private final boolean typed;
private Object listener;
private final Class<?> entityType;

/**
* @param listenerType the {@code @EntityListeners} class or the entity
* @throws MappingException if it implements two listener interfaces for one event
*/
public EntityListenerAdapter(Class<?> listenerType) {
private EntityListenerAdapter(Class<?> entityType) {
for (Class<? extends Annotation> annotationClass : LIFECYCLE_ANNOTATIONS) {
callbacks.put(annotationClass, new ArrayList<>());
}
mapAnnotationsToMethods(listenerType);
this.listenerType = listenerType;
mapAnnotationsToMethods(entityType);
this.entityType = entityType;
}

/**
* @param entityType the entity
* @return the adapter for the entity's own lifecycle methods, or null when it has none
* @throws MappingException if it implements two listener interfaces for one event
*/
@Nullable
public static EntityListenerAdapter listen(Class<?> entityType) {
EntityListenerAdapter adapter = new EntityListenerAdapter(entityType);
// checked here, when the entity is mapped: annotated methods would otherwise short-circuit every check
typed = TypedListeners.handlesAny(listenerType);
boolean typed = TypedListeners.handlesAny(entityType);
return typed || adapter.hasCallbacks() ? adapter : null;
}

/**
* @param listenerType an {@code @EntityListeners} class
* @return a new instance of it
* @throws MappingException if it implements none of the per-event listener interfaces, or two for one event, declares
* annotated lifecycle methods, or has no no-argument constructor or can't be created with it
*/
public static EntityListener<?> create(Class<? extends EntityListener<?>> listenerType) {
for (Method method : getDeclaredAndInheritedMethods(listenerType)) {
for (Class<? extends Annotation> annotationClass : LIFECYCLE_ANNOTATIONS) {
if (method.isAnnotationPresent(annotationClass)) {
throw new MappingException(Sofia.annotatedListenerMethod(listenerType.getName(), method.getName(),
annotationClass.getSimpleName()));
}
}
}
if (!TypedListeners.handlesAny(listenerType)) {
throw new MappingException(Sofia.noLifecycleMethods(listenerType.getName()));
}
Constructor<? extends EntityListener<?>> constructor;
try {
constructor = listenerType.getDeclaredConstructor();
} catch (NoSuchMethodException e) {
throw new MappingException(Sofia.noargConstructorNotFound(listenerType), e);
}
try {
constructor.setAccessible(true);
return constructor.newInstance();
} catch (ReflectiveOperationException e) {
Throwable cause = e instanceof InvocationTargetException target && target.getCause() != null ? target.getCause() : e;
throw new MappingException(Sofia.cannotInstantiate(listenerType.getName(), cause.getMessage()), cause);
}
}

/**
* @param event the lifecycle event annotation
* @return true if the listener handles the event, with an annotated method or its per-event listener interface
* @return true if the entity handles the event, with an annotated method or its per-event listener interface
*/
public boolean handles(@NonNull Class<? extends Annotation> event) {
return !callbacks.get(event).isEmpty() || TypedListeners.handles(listenerType, event);
return !callbacks.get(event).isEmpty() || TypedListeners.handles(entityType, event);
}

/**
* @param event the lifecycle event annotation
* @param entityType the type of the entity the listener is for
* @param entityType the type of the entity the model is for: the declaring entity or a subtype
* @return true if a method for the event takes the {@code Document}: an annotated method with a {@code Document}
* parameter, or the event's listener interface's method if it accepts every entity of the type. One that
* accepts only a subtype doesn't count: a codec checks the subtype's own needs once it knows the entity is one
Expand All @@ -74,23 +113,20 @@ public boolean usesDocument(@NonNull Class<? extends Annotation> event, Class<?>
return true;
}
}
return TypedListeners.usesDocument(listenerType, event, entityType);
return TypedListeners.usesDocument(this.entityType, event, entityType);
}

/**
* @return true if any lifecycle method was found
*/
boolean hasCallbacks() {
private boolean hasCallbacks() {
for (List<Callback> list : callbacks.values()) {
if (!list.isEmpty()) {
return true;
}
}
return typed;
return false;
}

/**
* Calls the listener's annotated methods for the event, then its per-event listener interface's method.
* Calls the entity's annotated methods for the event, then its per-event listener interface's method.
*
* @param event the lifecycle event annotation
* @param entity the entity
Expand All @@ -100,48 +136,24 @@ boolean hasCallbacks() {
public void invoke(Class<? extends Annotation> event, Object entity, @Nullable Document document, Datastore datastore) {
for (Callback callback : callbacks.get(event)) {
try {
callback.method.invoke(target(entity), callback.arguments(entity, document, datastore));
callback.method.invoke(entity, callback.arguments(entity, document, datastore));
} catch (IllegalArgumentException | ReflectiveOperationException e) {
throw failure(callback.method, entity, e);
}
}
if (TypedListeners.handles(listenerType, event)) {
TypedListeners.invoke(target(entity), event, entity, document, datastore);
if (TypedListeners.handles(entityType, event)) {
TypedListeners.invoke(entity, event, entity, document, datastore);
}
}

/**
* @param entity the entity the event is for
* @return the object to call the lifecycle methods on: an instance of the listener type
*/
protected Object target(Object entity) {
return getListener();
}

/**
* @param method the lifecycle method that failed
* @param entity the entity the event is for
* @param e the failure
* @return the exception to throw
*/
protected RuntimeException failure(Method method, Object entity, Exception e) {
return e instanceof IllegalArgumentException illegal ? illegal : new RuntimeException(e);
}

Object getListener() {
if (listener == null) {
try {
Constructor<?> constructor = listenerType.getDeclaredConstructor();
constructor.setAccessible(true);
listener = constructor.newInstance();
} catch (ReflectiveOperationException e) {
throw new MappingException(Sofia.noargConstructorNotFound(listenerType), e);
}
private static RuntimeException failure(Method method, Object entity, Exception e) {
if (e instanceof IllegalArgumentException && "object is not an instance of declaring class".equals(e.getMessage())) {
return new RuntimeException(format("%s is not an instance of the method's type: %s",
entity.getClass().getName(), method.getDeclaringClass().getName()), e);
}
return listener;
return new RuntimeException("Failed to invoke method: " + method, e);
}

@NonNull
private void mapAnnotationsToMethods(Class<?> type) {
for (Method method : getDeclaredAndInheritedMethods(type)) {
for (Class<? extends Annotation> annotationClass : LIFECYCLE_ANNOTATIONS) {
Expand Down Expand Up @@ -198,7 +210,7 @@ Object[] arguments(Object entity, @Nullable Document document, Datastore datasto
}
}

private List<Method> getDeclaredAndInheritedMethods(Class<?> type) {
private static List<Method> getDeclaredAndInheritedMethods(Class<?> type) {
final List<Method> methods = new ArrayList<>();
if (type == Object.class) {
return methods;
Expand Down

This file was deleted.

Loading
Loading