This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit 5d0170d465135d4cff56063d13a09816406006fa Author: Martin Desruisseaux <[email protected]> AuthorDate: Wed Jun 8 12:11:56 2022 +0200 Generalize the mechanism for propagating an event from parent store to children resources. For now only `CloseEvent` uses it. https://issues.apache.org/jira/browse/SIS-549 --- .../sis/storage/event/CascadedStoreEvent.java | 130 +++++++++++++++++++ .../org/apache/sis/storage/event/CloseEvent.java | 53 +++----- .../org/apache/sis/storage/event/StoreEvent.java | 6 +- .../apache/sis/storage/event/StoreListener.java | 6 +- .../apache/sis/storage/event/StoreListeners.java | 140 ++++++++++++--------- .../sis/storage/event/StoreListenersTest.java | 1 + 6 files changed, 232 insertions(+), 104 deletions(-) diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/event/CascadedStoreEvent.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/event/CascadedStoreEvent.java new file mode 100644 index 0000000000..6a2c82f09a --- /dev/null +++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/event/CascadedStoreEvent.java @@ -0,0 +1,130 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.sis.storage.event; + +import java.lang.ref.WeakReference; +import java.util.concurrent.ExecutionException; +import org.apache.sis.storage.Resource; + + +/** + * An event which, when occurring on a parent resource, is also fired by all children resources. + * For example when an {@link org.apache.sis.storage.Aggregate} (typically a data store) is closed, + * a {@link CloseEvent} is automatically fired by all resources that are components of the aggregate. + * This is similar to "cascade delete" in SQL databases. + * + * <h2>Difference between {@code StoreEvent} and {@code CascadedStoreEvent}</h2> + * By default {@link StoreEvent}s are propagated from children to parents. + * For example when a {@link WarningEvent} occurs in a child resource, + * all listeners registered on that resource are notified, + * then all listeners registered on the parent resource, and so forth until the root resource. + * All those listeners receive the same {@link WarningEvent} instance, + * i.e. the {@linkplain WarningEvent#getSource() event source} is always the resource where the warning occurred. + * + * <p>By contrast {@code CascadedStoreEvent} are fired in the opposite direction, from parent to children. + * Furthermore each child creates its own {@code CascadedStoreEvent}. For example if a {@link CloseEvent} is + * fired in a {@link org.apache.sis.storage.DataStore}, then it causes all resources of that data store to fire + * their own {@link CloseEvent} declaring themselves as the {@linkplain CloseEvent#getSource() event source}.</p> + * + * @author Martin Desruisseaux (Geomatys) + * @since 1.3 + * + * @param <E> the type of the event subclass. + * + * @version 1.3 + * @module + */ +public abstract class CascadedStoreEvent<E extends CascadedStoreEvent<E>> extends StoreEvent { + /** + * For cross-version compatibility. + */ + private static final long serialVersionUID = -1319167650150261418L; + + /** + * Constructs an event that occurred in the given resource. + * + * @param source the resource where the event occurred. + * @throws IllegalArgumentException if the given source is null. + */ + protected CascadedStoreEvent(Resource source) { + super(source); + super.consume(true); // Necessary for avoiding never-ending loop. + } + + /** + * Creates a new event of the same type than this event but with a different source. + * This method is invoked for creating the event to be fired by the children of the + * resource where the original event occurred. + * + * @param child the child resource for which to create the event to cascade. + * @return an event of the same type than this event but with the given resource. + */ + protected abstract E forSource(Resource child); + + + + + /** + * A listener to register on the parent of a resource for cascading an event to the children. + * + * @see StoreListeners#cascadedListeners + */ + static final class ParentListener<E extends CascadedStoreEvent<E>> implements StoreListener<E> { + /** + * The type of event to listen. + */ + private final Class<E> eventType; + + /** + * The parent resource to listen to. + */ + private final StoreListeners parent; + + /** + * The listeners to notify. + */ + private final WeakReference<StoreListeners> listeners; + + /** + * Creates a new listener to be registered on the parent of the given set of listeners. + * + * @param eventType the type of event to listen. + * @param parent the parent resource to listen to. + * @param listeners the child set of listeners. + */ + ParentListener(final Class<E> eventType, final StoreListeners parent, final StoreListeners listeners) { + this.eventType = eventType; + this.parent = parent; + this.listeners = new WeakReference<>(listeners); + } + + /** + * Invoked when an event is fired on a parent resource. + * This method causes similar event to be fired on children resources. + */ + @Override public void eventOccured(final E event) { + final StoreListeners r = listeners.get(); + if (r == null) { + parent.removeListener(eventType, this); + } else try { + StoreListeners.fire(r, eventType, event.forSource(r.getSource())); + } catch (ExecutionException e) { + StoreListeners.canNotNotify("fire (cascade)", e); + } + } + } +} diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/event/CloseEvent.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/event/CloseEvent.java index 4ca37e07a2..6e56d532d1 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/storage/event/CloseEvent.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/event/CloseEvent.java @@ -21,14 +21,19 @@ import org.apache.sis.storage.Resource; /** * Notifies listeners that a resource or a data store is being closed and should no longer be used. - * Resources are automatically considered closed when a parent resource or data store is closed. + * Firing a {@code CloseEvent} on a parent resource (typically a data store) + * automatically fires a {@code CloseEvent} in all children resources. + * See {@link CascadedStoreEvent} javadoc for more information. * * @author Martin Desruisseaux (Geomatys) - * @since 1.3 + * @since 1.3 + * + * @see StoreListeners#close() + * * @version 1.3 * @module */ -public class CloseEvent extends StoreEvent { +public class CloseEvent extends CascadedStoreEvent<CloseEvent> { /** * For cross-version compatibility. */ @@ -44,44 +49,14 @@ public class CloseEvent extends StoreEvent { super(source); } - - - /** - * A listener to register on the parent of a resource for closing the resource - * automatically if the parent is closed. + * Creates a new event of the same type than this event but with a different source. * - * @see StoreListeners#closeListener + * @param child the child resource for which to create the event to cascade. + * @return an event of the same type than this event but with the given resource. */ - static final class ParentListener implements StoreListener<CloseEvent> { - /** - * The parent resource to listen to. - */ - private final Resource parent; - - /** - * The listeners to notify. - */ - private final StoreListeners listeners; - - /** - * Creates a new listener to be registered on the parent of the given set of listeners. - * - * @param parent the parent resource to listen to. - * @param listeners the child set of listeners. - */ - ParentListener(final Resource parent, final StoreListeners listeners) { - this.parent = parent; - this.listeners = listeners; - } - - /** - * Invoked when a parent resource or data store is closed. - */ - @Override public void eventOccured(final CloseEvent event) { - if (event.getSource() == parent) { // Necessary check for avoiding never-ending loop. - listeners.close(); - } - } + @Override + protected CloseEvent forSource(final Resource child) { + return new CloseEvent(child); } } diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/event/StoreEvent.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/event/StoreEvent.java index a7f50abfd1..5b976f129d 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/storage/event/StoreEvent.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/event/StoreEvent.java @@ -123,14 +123,14 @@ public abstract class StoreEvent extends EventObject implements Localized { /** * Marks this event as consumed. This stops its further propagation to other listeners. * - * @param later {@code true} for consuming now, or {@code false} for consuming after all listeners + * @param later {@code false} for consuming now, or {@code true} for consuming after all listeners * registered on the {@linkplain #getSource() source} resource but before listeners registered * on the parent resource or data store. * * @since 1.3 */ public void consume(final boolean later) { - if (later) consumed = true; - else consumeLater = true; + if (later) consumeLater = true; + else consumed = true; } } diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/event/StoreListener.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/event/StoreListener.java index 9be960bffc..23c438bd3c 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/storage/event/StoreListener.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/event/StoreListener.java @@ -35,7 +35,7 @@ import org.apache.sis.storage.Resource; * @author Johann Sorel (Geomatys) * @version 1.0 * - * @param <T> the type of events of interest to this listener. + * @param <E> the type of events of interest to this listener. * * @see StoreEvent * @see Resource#addListener(Class, StoreListener) @@ -43,7 +43,7 @@ import org.apache.sis.storage.Resource; * @since 1.0 * @module */ -public interface StoreListener<T extends StoreEvent> extends EventListener { +public interface StoreListener<E extends StoreEvent> extends EventListener { /** * Invoked <em>after</em> a warning or a change occurred in a resource. * The {@link StoreEvent#getSource()} method gives the resource where the event occurred. @@ -52,5 +52,5 @@ public interface StoreListener<T extends StoreEvent> extends EventListener { * * @param event description of the change or warning that occurred in a resource. Shall not be {@code null}. */ - void eventOccured(T event); + void eventOccured(E event); } diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/event/StoreListeners.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/event/StoreListeners.java index 924d1f66cd..a580155f55 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/storage/event/StoreListeners.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/event/StoreListeners.java @@ -123,30 +123,30 @@ public class StoreListeners implements Localized { JDK9.setOf(WarningEvent.class, CloseEvent.class); /** - * The {@link CloseEvent.ParentListener} registered on {@link #parent}, or {@code null} if not yet created. - * This is created the first time that a {@link CloseEvent} listener is registered on a resource which is - * not the root resource. Those listeners are handled in a special way, because a close event on the root - * should propagate to all children. + * The {@link CascadedStoreEvent.ParentListener}s registered on {@link #parent}. + * This is created the first time that a {@link CascadedStoreEvent} listener is registered on a resource + * which is not the root resource. Those listeners are handled in a special way, because a closing event + * on the root resource should cause all children to also fire their own {@link CloseEvent}. */ - private StoreListener<CloseEvent> closeListener; + private Map<Class<?>, StoreListener<?>> cascadedListeners; /** * All listeners for a given even type. * - * @param <T> the type of events of interest to the listeners. + * @param <E> the type of events of interest to the listeners. */ - private static final class ForType<T extends StoreEvent> { + private static final class ForType<E extends StoreEvent> { /** * The types for which listeners have been registered. */ - final Class<T> type; + final Class<E> type; /** * The listeners for the {@linkplain #type event type}, or {@code null} if none. * This is a <cite>copy on write</cite> array: no elements are modified after an array has been created. */ @SuppressWarnings("VolatileArrayField") - private volatile StoreListener<? super T>[] listeners; + private volatile StoreListener<? super E>[] listeners; /** * Next element in the chain of listeners. Intentionally final; if we want to remove an element @@ -161,7 +161,7 @@ public class StoreListeners implements Localized { * @param type type of events of interest for listeners in this element. * @param next the next element in the chained list, or {@code null} if none. */ - ForType(final Class<T> type, final ForType<?> next) { + ForType(final Class<E> type, final ForType<?> next) { this.type = type; this.next = next; } @@ -173,11 +173,11 @@ public class StoreListeners implements Localized { * * <p>It is caller responsibility to perform synchronization and to verify that the listener is non-null.</p> */ - final void add(final StoreListener<? super T> listener) { - final StoreListener<? super T>[] list = listeners; + final void add(final StoreListener<? super E> listener) { + final StoreListener<? super E>[] list = listeners; final int length = (list != null) ? list.length : 0; @SuppressWarnings({"unchecked", "rawtypes"}) // Generic array creation. - final StoreListener<? super T>[] copy = new StoreListener[length + 1]; + final StoreListener<? super E>[] copy = new StoreListener[length + 1]; if (list != null) { System.arraycopy(list, 0, copy, 0, length); } @@ -194,8 +194,8 @@ public class StoreListeners implements Localized { * @param listener the listener to remove. * @return {@code true} if the list of listeners is empty after this method call. */ - final boolean remove(final StoreListener<? super T> listener) { - StoreListener<? super T>[] list = listeners; + final boolean remove(final StoreListener<? super E> listener) { + StoreListener<? super E>[] list = listeners; if (list != null) { for (int i=list.length; --i >= 0;) { if (list[i] == listener) { @@ -243,16 +243,16 @@ public class StoreListeners implements Localized { * @return the {@code done} map, created when first needed. * @throws ExecutionException if at least one listener failed to execute. */ - final Map<StoreListener<?>,Boolean> eventOccured(final T event, Map<StoreListener<?>,Boolean> done) + final Map<StoreListener<?>,Boolean> eventOccured(final E event, Map<StoreListener<?>,Boolean> done) throws ExecutionException { RuntimeException error = null; - final StoreListener<? super T>[] list = listeners; + final StoreListener<? super E>[] list = listeners; if (list != null) { if (done == null) { done = new IdentityHashMap<>(list.length); } - for (final StoreListener<? super T> listener : list) { + for (final StoreListener<? super E> listener : list) { if (event.isConsumed()) break; if (done.put(listener, Boolean.TRUE) == null) try { listener.eventOccured(event); @@ -576,7 +576,7 @@ public class StoreListeners implements Localized { */ @SuppressWarnings("unchecked") public void warning(final LogRecord description, final Filter onUnhandled) { - if (!fire(new WarningEvent(source, description), WarningEvent.class) && + if (!fire(WarningEvent.class, new WarningEvent(source, description)) && (onUnhandled == null || onUnhandled.isLoggable(description))) { final String name = description.getLoggerName(); @@ -604,10 +604,19 @@ public class StoreListeners implements Localized { * @param method name of the method invoking this method. * @param error the exception that occurred. */ - private static void canNotNotify(final String method, final ExecutionException error) { + static void canNotNotify(final String method, final ExecutionException error) { Logging.unexpectedException(Logger.getLogger(Modules.STORAGE), StoreListeners.class, method, error); } + /** + * @deprecated Replaced by {@link #fire(Class, StoreEvent)} for consistency with the argument order + * in all other methods of this class. + */ + @Deprecated + public <E extends StoreEvent> boolean fire(final E event, final Class<E> eventType) { + return fire(eventType, event); + } + /** * Sends the given event to all listeners registered for the given type or for a super-type. * This method first notifies the listeners registered in this {@code StoreListeners}, then @@ -619,16 +628,18 @@ public class StoreListeners implements Localized { * {@linkplain Logging#unexpectedException(Logger, Class, String, Throwable) log record}. * Runtime exceptions in listeners do not cause this method to fail.</p> * - * @param <T> compile-time value of the {@code eventType} argument. - * @param event the event to fire. + * @param <E> compile-time value of the {@code eventType} argument. * @param eventType the type of the event to be fired. + * @param event the event to fire. * @return {@code true} if the event has been sent to at least one listener. * @throws IllegalArgumentException if the given event type is not one of the types of events * that this {@code StoreListeners} can fire. * * @see #close() + * + * @since 1.3 */ - public <T extends StoreEvent> boolean fire(final T event, final Class<T> eventType) { + public <E extends StoreEvent> boolean fire(final Class<E> eventType, final E event) { ArgumentChecks.ensureNonNull("event", event); ArgumentChecks.ensureNonNull("eventType", eventType); final Set<Class<? extends StoreEvent>> permittedEventTypes = this.permittedEventTypes; @@ -636,7 +647,7 @@ public class StoreListeners implements Localized { throw illegalEventType(eventType); } try { - return fire(this, event, eventType); + return fire(this, eventType, event); } catch (ExecutionException ex) { canNotNotify("fire", ex); return true; @@ -649,15 +660,16 @@ public class StoreListeners implements Localized { * * <p>This method does not need (and should not) be synchronized.</p> * - * @param <T> compile-time value of the {@code eventType} argument. + * @param <E> compile-time value of the {@code eventType} argument. * @param m the set of listeners that may be interested in the event. - * @param event the event to fire. * @param eventType the type of the event to be fired. + * @param event the event to fire. * @return {@code true} if the event has been sent to at least one listener. - * @throws ExecutionException + * @throws ExecutionException if an exception is thrown inside {@link StoreListener#eventOccured(StoreEvent)}. + * All other listeners continue to receive the event before {@code ExecutionException} is thrown. */ @SuppressWarnings("unchecked") - private static <T extends StoreEvent> boolean fire(StoreListeners m, final T event, final Class<T> eventType) + static <E extends StoreEvent> boolean fire(StoreListeners m, final Class<E> eventType, final E event) throws ExecutionException { Map<StoreListener<?>,Boolean> done = null; @@ -665,13 +677,13 @@ public class StoreListeners implements Localized { do { for (ForType<?> e = m.listeners; e != null; e = e.next) { if (e.type.isAssignableFrom(eventType)) try { - done = ((ForType<? super T>) e).eventOccured(event, done); + done = ((ForType<? super E>) e).eventOccured(event, done); } catch (ExecutionException ex) { if (error == null) error = ex; else error.getCause().addSuppressed(ex.getCause()); } - if (event.isConsumedForParent()) break; } + if (event.isConsumedForParent()) break; m = m.parent; } while (m != null); if (error != null) { @@ -722,21 +734,21 @@ public class StoreListeners implements Localized { * This side-effect is applied on the assumption that the registered listener will handle * warnings in its own way, for example by showing warnings in a widget. * - * @param <T> compile-time value of the {@code eventType} argument. + * @param <E> compile-time value of the {@code eventType} argument. * @param eventType type of {@link StoreEvent} to listen (can not be {@code null}). * @param listener listener to notify about events. * * @see Resource#addListener(Class, StoreListener) */ - @SuppressWarnings("unchecked") - public synchronized <T extends StoreEvent> void addListener(final Class<T> eventType, final StoreListener<? super T> listener) { + @SuppressWarnings({"rawtypes","unchecked"}) + public synchronized <E extends StoreEvent> void addListener(final Class<E> eventType, final StoreListener<? super E> listener) { ArgumentChecks.ensureNonNull("listener", listener); ArgumentChecks.ensureNonNull("eventType", eventType); if (isPossibleEvent(permittedEventTypes, eventType)) { - ForType<T> ce = null; + ForType<E> ce = null; for (ForType<?> e = listeners; e != null; e = e.next) { if (e.type.equals(eventType)) { - ce = (ForType<T>) e; + ce = (ForType<E>) e; break; } } @@ -746,13 +758,18 @@ public class StoreListeners implements Localized { } ce.add(listener); /* - * If we are adding a listener for `CloseEvent`, we may need (as a special case) - * to register a listener to the parent for propagating the close events. + * If we are adding a listener for `CascadedStoreEvent`, we may need + * to register a listener in the parent for cascading the events. */ - if (parent != null) { - if (closeListener == null && CloseEvent.class.isAssignableFrom(eventType)) { - closeListener = new CloseEvent.ParentListener(parent.source, this); - parent.addListener(CloseEvent.class, closeListener); + if (parent != null && CascadedStoreEvent.class.isAssignableFrom(eventType)) { + if (cascadedListeners == null) { + cascadedListeners = new IdentityHashMap<>(4); + } + StoreListener cascade = cascadedListeners.get(eventType); + if (cascade == null) { + cascade = new CascadedStoreEvent.ParentListener(eventType, parent, this); + cascadedListeners.put(eventType, cascade); + parent.addListener(eventType, cascade); } } } @@ -774,27 +791,22 @@ public class StoreListeners implements Localized { * there are no remaining listeners for warning events, then this {@code StoreListeners} will send future * warnings to the loggers. * - * @param <T> compile-time value of the {@code eventType} argument. + * @param <E> compile-time value of the {@code eventType} argument. * @param eventType type of {@link StoreEvent} which were listened (can not be {@code null}). * @param listener listener to stop notifying about events. * * @see Resource#removeListener(Class, StoreListener) */ - @SuppressWarnings("unchecked") - public synchronized <T extends StoreEvent> void removeListener(Class<T> eventType, StoreListener<? super T> listener) { + @SuppressWarnings({"rawtypes","unchecked"}) + public synchronized <E extends StoreEvent> void removeListener(Class<E> eventType, StoreListener<? super E> listener) { ArgumentChecks.ensureNonNull("listener", listener); ArgumentChecks.ensureNonNull("eventType", eventType); for (ForType<?> e = listeners; e != null; e = e.next) { if (e.type.equals(eventType)) { - if (((ForType<T>) e).remove(listener) && parent != null) { - /* - * If the list of listeners become empty and if the event type was `CloseEvent`, - * cleanup the parent list of listeners too. We do a special case for close events - * because closing a parent data store implicitly closes the child resources. - */ - if (closeListener != null && CloseEvent.class.isAssignableFrom(eventType)) { - parent.removeListener(CloseEvent.class, closeListener); - closeListener = null; + if (((ForType<E>) e).remove(listener) && cascadedListeners != null) { + final StoreListener cascade = cascadedListeners.remove(eventType); + if (cascade != null) { + parent.removeListener(eventType, cascade); } } break; @@ -902,26 +914,36 @@ public class StoreListeners implements Localized { * Sends a {@link CloseEvent} to all listeners registered for that kind of event, * then discards listeners in this instance (but not in parents). * Because listeners are discarded, invoking this method many times - * on the same instance have no effect after the first invocation. + * on the same instance has no effect after the first invocation. * * <p>If one or many {@link StoreListener#eventOccured(StoreEvent)} implementations throw * a {@link RuntimeException}, those exceptions will be collected and reported in a single * {@linkplain Logging#unexpectedException(Logger, Class, String, Throwable) log record}. * Runtime exceptions in listeners do not cause this method to fail.</p> * - * @see #fire(StoreEvent, Class) + * @see #fire(Class, StoreEvent) * @see DataStore#close() + * @see CloseEvent * * @since 1.3 */ + @SuppressWarnings({"rawtypes","unchecked"}) public void close() { try { - fire(this, new CloseEvent(source), CloseEvent.class); + /* + * We use the private static method instead of `fire(Class, StoreEvent)` public method + * because calls to `close()` should never fail (except with `java.lang.Error` because + * we do not want to hide serious errors), so we bypass argument validation and method + * overriding as a safety. + */ + fire(this, CloseEvent.class, new CloseEvent(source)); } catch (ExecutionException ex) { canNotNotify("close", ex); } - closeListener = null; - listeners = null; // Volatile field should be last. - // Do not remove parent listeners; maybe parent will be closed next. + listeners = null; + /* + * No need to cleanup `cascadedListeners`. It does not hurt (those listeners practically + * become no-op) and the objects are probably going to be garbage collected soon anyway. + */ } } diff --git a/storage/sis-storage/src/test/java/org/apache/sis/storage/event/StoreListenersTest.java b/storage/sis-storage/src/test/java/org/apache/sis/storage/event/StoreListenersTest.java index f353231094..140b54a305 100644 --- a/storage/sis-storage/src/test/java/org/apache/sis/storage/event/StoreListenersTest.java +++ b/storage/sis-storage/src/test/java/org/apache/sis/storage/event/StoreListenersTest.java @@ -143,6 +143,7 @@ public final strictfp class StoreListenersTest extends TestCase implements Store @Override public void eventOccured(CloseEvent event) { assertSame(resource, event.getSource()); + assertFalse(isClosed); isClosed = true; } }
