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 4f46eebc12787609d3e7354c02ff73a6a8f2af47 Author: Martin Desruisseaux <[email protected]> AuthorDate: Fri Sep 6 20:11:07 2019 +0200 Prepare replacement of WarningListeners by StoreListeners. https://issues.apache.org/jira/browse/SIS-421 --- .../apache/sis/util/logging/QuietLogRecord.java | 3 + .../apache/sis/util/logging/WarningListeners.java | 7 +- .../java/org/apache/sis/storage/DataStore.java | 8 + .../org/apache/sis/storage/DataStoreProvider.java | 22 + .../main/java/org/apache/sis/storage/Resource.java | 46 +- .../apache/sis/storage/event}/QuietLogRecord.java | 14 +- .../org/apache/sis/storage/event/StoreEvent.java | 50 +- .../apache/sis/storage/event/StoreListener.java | 28 +- .../apache/sis/storage/event/StoreListeners.java | 607 +++++++++++++++++++++ .../org/apache/sis/storage/event/WarningEvent.java | 90 +++ .../org/apache/sis/storage/event/package-info.java | 17 +- 11 files changed, 842 insertions(+), 50 deletions(-) diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/logging/QuietLogRecord.java b/core/sis-utility/src/main/java/org/apache/sis/util/logging/QuietLogRecord.java index 1b225fe..d10ff94 100644 --- a/core/sis-utility/src/main/java/org/apache/sis/util/logging/QuietLogRecord.java +++ b/core/sis-utility/src/main/java/org/apache/sis/util/logging/QuietLogRecord.java @@ -27,7 +27,10 @@ import java.util.logging.LogRecord; * @version 0.8 * @since 0.3 * @module + * + * @deprecated Moved to {@link org.apache.sis.storage.event.QuietLogRecord}. */ +@Deprecated final class QuietLogRecord extends LogRecord { /** * For cross-version compatibility. diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/logging/WarningListeners.java b/core/sis-utility/src/main/java/org/apache/sis/util/logging/WarningListeners.java index ac8709b..d7790cb 100644 --- a/core/sis-utility/src/main/java/org/apache/sis/util/logging/WarningListeners.java +++ b/core/sis-utility/src/main/java/org/apache/sis/util/logging/WarningListeners.java @@ -67,7 +67,10 @@ import org.apache.sis.internal.util.UnmodifiableArrayList; * * @since 0.3 * @module + * + * @deprecated Replaced by {@link org.apache.sis.storage.event.StoreListeners}. */ +@Deprecated public class WarningListeners<S> implements Localized { /** * The declared source of warnings. This is not necessarily the real source, @@ -176,12 +179,12 @@ public class WarningListeners<S> implements Localized { * @param record the warning as a log record. */ public void warning(final LogRecord record) { - final WarningListener<?>[] current; + final WarningListener<? super S>[] current; synchronized (this) { current = listeners; } if (current != null) { - for (final WarningListener<? super S> listener : listeners) { + for (final WarningListener<? super S> listener : current) { listener.warningOccured(source, record); } } else { diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/DataStore.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/DataStore.java index 07d371a..3b4577e 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/storage/DataStore.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/DataStore.java @@ -225,6 +225,8 @@ public abstract class DataStore implements Resource, Localized, AutoCloseable { * only – it has no effect on the data to be read or written from/to the data store. * * <p>The default value is the {@linkplain Locale#getDefault() system default locale}.</p> + * + * @see org.apache.sis.storage.event.StoreEvent#getLocale() */ @Override public synchronized Locale getLocale() { @@ -444,7 +446,10 @@ public abstract class DataStore implements Resource, Localized, AutoCloseable { * * @param listener the listener to add. * @throws IllegalArgumentException if the given listener is already registered in this data store. + * + * @deprecated Replaced by {@code addListener(listener, WarningEvent.class)}. */ + @Deprecated public void addWarningListener(final WarningListener<? super DataStore> listener) throws IllegalArgumentException { @@ -456,7 +461,10 @@ public abstract class DataStore implements Resource, Localized, AutoCloseable { * * @param listener the listener to remove. * @throws NoSuchElementException if the given listener is not registered in this data store. + * + * @deprecated Replaced by {@code removeListener(listener, WarningEvent.class)}. */ + @Deprecated public void removeWarningListener(final WarningListener<? super DataStore> listener) throws NoSuchElementException { diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/DataStoreProvider.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/DataStoreProvider.java index da71aba..09a7e4a 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/storage/DataStoreProvider.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/DataStoreProvider.java @@ -16,6 +16,7 @@ */ package org.apache.sis.storage; +import java.util.logging.Logger; import org.opengis.parameter.ParameterValueGroup; import org.opengis.parameter.ParameterDescriptorGroup; import org.opengis.metadata.distribution.Format; @@ -24,6 +25,7 @@ import org.apache.sis.internal.storage.URIDataStore; import org.apache.sis.metadata.iso.citation.DefaultCitation; import org.apache.sis.metadata.iso.distribution.DefaultFormat; import org.apache.sis.measure.Range; +import org.apache.sis.util.logging.Logging; import org.apache.sis.util.ArgumentChecks; import org.apache.sis.util.Version; @@ -322,4 +324,24 @@ public abstract class DataStoreProvider { ArgumentChecks.ensureNonNull("parameter", parameters); return open(URIDataStore.Provider.connector(this, parameters)); } + + /** + * Returns the logger where to report warnings. This logger is used only if no + * {@link org.apache.sis.storage.event.StoreListener} has been registered for + * {@link org.apache.sis.storage.event.WarningEvent}. + * + * <p>The default implementation returns a logger with the same name as the package name + * of the subclass of this {@code DataStoreProvider} instance. Subclasses should override + * this method if they can provide a more specific logger.</p> + * + * @return the logger to use as a fallback (when there is no listeners) for warning messages. + * + * @since 1.0 + */ + public Logger getLogger() { + String name = getClass().getName(); + final int separator = name.lastIndexOf('.'); + name = (separator >= 1) ? name.substring(0, separator) : ""; + return Logging.getLogger(name); + } } diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/Resource.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/Resource.java index 9996ed4..85a80f5 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/storage/Resource.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/Resource.java @@ -134,20 +134,26 @@ public interface Resource { Metadata getMetadata() throws DataStoreException; /** - * Registers a listener that is notified when some kind of events occur in the resource content or structure. - * The resource will call the {@link StoreListener#eventOccured(StoreEvent)} - * method when a new event matching the {@code eventType} is produced. + * Registers a listener to notify when the specified kind of event occurs in this resource or in children. + * The resource will call the {@link StoreListener#eventOccured(StoreEvent)} method when new events matching + * the {@code eventType} occur. An event may be a change in resource content or structure, or a warning that + * occurred during a read or write operation. * - * <p>Registering a listener for a given {@code eventType} also register the listener for all sub-types. - * The same listener can be added multiple times for different even types. - * Adding many times the same listener with the same even type has no effect: - * the listener will only be called once per event.</p> + * <p>Registering a listener for a given {@code eventType} also register the listener for all event sub-types. + * The same listener can be registered many times, but its {@link StoreListener#eventOccured(StoreEvent)} + * method will be invoked only once per event. This filtering applies even if the listener is registered + * on different resources in the same tree, for example a parent and its children.</p> * - * @todo When adding a listener to an aggregate, should the listener be added to all components? - * In other words, should listeners in a tree node also listen to events from all children? + * <p>If this resource may produce events of the given type, then the given listener is kept by strong reference; + * it will not be garbage collected unless {@linkplain #removeListener(StoreListener, Class) explicitly removed} + * or unless this {@code Resource} is itself garbage collected. However if the given type of events can never + * happen with this resource, then this method is not required to keep a reference to the given listener.</p> * - * <p>The resource is not required to keep a reference to the listener. - * For example the resource may discard a listener if no event of the given type happen on this resource.</p> + * <div class="section">Warning events</div> + * If {@code eventType} is assignable from <code>{@linkplain org.apache.sis.storage.event.WarningEvent}.class</code>, + * then registering that listener turns off logging of warning messages for this resource. + * 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 listener listener to notify about events. @@ -157,15 +163,19 @@ public interface Resource { /** * Unregisters a listener previously added to this resource for the given type of events. - * The {@code eventType} must be the exact same class than the one given to the {@code addListener(…)} method. + * The {@code eventType} must be the exact same class than the one given to the {@code addListener(…)} method; + * this method does not remove listeners registered for subclasses and does not remove listeners registered in + * parent resources. * - * <div class="note"><b>Example:</b> - * if the same listener has been added for {@code StoreEvent} and {@code StructuralChangeEvent}, that listener - * will be notified only once for all {@code StoreEvent}s. If that listener is removed for {@code StoreEvent}, - * then the listener will still receive {@code StructuralChangeEvent}s.</div> + * <p>If the same listener has been registered many times for the same even type, then this method removes only + * the most recent registration. In other words if {@code addListener(ls, type)} has been invoked twice, then + * {@code removeListener(ls, type)} needs to be invoked twice in order to remove all instances of that listener. + * If the given listener is not found, then this method does nothing (no exception is thrown).</p> * - * <p>Calling multiple times this method with the same listener and event type or a listener - * which is unknown to this resource will have no effect and will not raise an exception.</p> + * <div class="section">Warning events</div> + * If {@code eventType} is <code>{@linkplain org.apache.sis.storage.event.WarningEvent}.class</code> + * and if, after this method invocation, there is no remaining listener for warning events, + * then this {@code Resource} will send future warnings to the loggers. * * @param <T> compile-time value of the {@code eventType} argument. * @param listener listener to stop notifying about events. diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/logging/QuietLogRecord.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/event/QuietLogRecord.java similarity index 84% copy from core/sis-utility/src/main/java/org/apache/sis/util/logging/QuietLogRecord.java copy to storage/sis-storage/src/main/java/org/apache/sis/storage/event/QuietLogRecord.java index 1b225fe..06a3496 100644 --- a/core/sis-utility/src/main/java/org/apache/sis/util/logging/QuietLogRecord.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/event/QuietLogRecord.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.sis.util.logging; +package org.apache.sis.storage.event; import java.util.logging.Level; import java.util.logging.LogRecord; @@ -24,7 +24,7 @@ import java.util.logging.LogRecord; * A log record to be logged without stack trace, unless the user specified it explicitly. * * @author Martin Desruisseaux (Geomatys) - * @version 0.8 + * @version 1.0 * @since 0.3 * @module */ @@ -32,11 +32,11 @@ final class QuietLogRecord extends LogRecord { /** * For cross-version compatibility. */ - private static final long serialVersionUID = -8225936118310305206L; + private static final long serialVersionUID = 5652099235767670922L; /** - * {@code true} if the user invoked {@link #setThrown(Throwable)}. - * In such case, {@link #clearThrown()} will not reset the throwable to null. + * {@code true} if the user invoked {@link #setThrown(Throwable)} explicitly. + * In such case, {@link #clearImplicitThrown()} will not reset the throwable to null. */ private boolean explicitThrown; @@ -59,10 +59,10 @@ final class QuietLogRecord extends LogRecord { } /** - * Clears the throwable if it has not been explicit set by the user. + * Clears the throwable if it has not been explicitly set by the user. * Otherwise do nothing. */ - void clearThrown() { + void clearImplicitThrown() { if (!explicitThrown) { super.setThrown(null); } 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 8462e9d..3360ef9 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 @@ -16,8 +16,12 @@ */ package org.apache.sis.storage.event; +import java.util.Locale; import java.util.EventObject; +import org.apache.sis.util.Localized; import org.apache.sis.storage.Resource; +import org.apache.sis.storage.DataStore; +import org.apache.sis.internal.storage.StoreResource; /** @@ -33,7 +37,7 @@ import org.apache.sis.storage.Resource; * @since 1.0 * @module */ -public abstract class StoreEvent extends EventObject { +public abstract class StoreEvent extends EventObject implements Localized { /** * For cross-version compatibility. */ @@ -42,20 +46,52 @@ public abstract class StoreEvent extends EventObject { /** * Constructs an event that occurred in the given resource. * - * @param source the resource on which the event initially occurred. - * @throws IllegalArgumentException if the given source is null. + * @param source the resource where the event occurred. + * @throws IllegalArgumentException if the given source is null. */ - public StoreEvent(Resource source) { + protected StoreEvent(Resource source) { super(source); } /** - * Returns the resource on which the event initially occurred. + * Returns the resource where the event occurred. It is not necessarily the {@linkplain Resource#addListener + * resource in which listeners have been registered}; it may be one of the resource children. * - * @return the resource on which the Event initially occurred. + * @return the resource where the event occurred. */ @Override public Resource getSource() { - return (Resource) source; + return (Resource) super.getSource(); + } + + /** + * Returns the locale associated to this event, or {@code null} if unspecified. + * That locale may be used for formatting messages related to this event. + * The event locale is typically inherited from the {@link DataStore} locale. + * + * @return the locale associated to this event (typically specified by the data store), + * or {@code null} if unknown. + * + * @see DataStore#getLocale() + */ + @Override + public Locale getLocale() { + return getLocale(source); + } + + /** + * {@link #getLocale()} implementation shared with {@link StoreListeners#getLocale()}. + */ + static Locale getLocale(final Object source) { + if (source instanceof Localized) { + return ((Localized) source).getLocale(); + } + if (source instanceof StoreResource) { + final DataStore ds = ((StoreResource) source).getOriginator(); + if (ds != null) { + return ds.getLocale(); + } + } + return null; } } 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 4ba393a..7d49d22 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 @@ -16,23 +16,21 @@ */ package org.apache.sis.storage.event; +import java.util.EventListener; import org.apache.sis.storage.Resource; /** - * Defines an object which listens for events in resources (changes or warnings). - * The events in resources are described by {@link StoreEvent} instances. - * {@link Resource} implementations are responsible for instantiating the most specific {@code StoreEvent} subclass - * for the type of event, for example: + * An object which listens for events (typically changes or warnings) occurring in a resource + * or one of its children. The kind of event is defined by the subclass of the {@link StoreEvent} + * instance given to the {@link #eventOccured(StoreEvent)} method. For example if a warning occurred + * while reading data from a file, then the event will be an instance of {@link WarningEvent}. * - * <ul> - * <li>When a warning occurred.</li> - * <li>When the data store content changed (e.g. new feature instance added or removed).</li> - * <li>When the data store structure changed (e.g. a column is added in tabular data).</li> - * <li>Any other change at implementation choice.</li> - * </ul> - * - * Then, all {@code StoreListener}s that declared an interest in {@code StoreEvent}s of that kind are notified. + * <p>{@link Resource} implementations are responsible for instantiating the most specific + * {@code StoreEvent} subclass for the type of events. Then, all {@code StoreListener}s that + * {@linkplain Resource#addListener(StoreListener, Class) declared an interest} for + * {@code StoreEvent}s of that kind are notified, including listeners in parent resources. + * Each listener is notified only once per event even if the listener is registered twice.</p> * * @author Johann Sorel (Geomatys) * @version 1.0 @@ -40,13 +38,17 @@ import org.apache.sis.storage.Resource; * @param <T> the type of events of interest to this listener. * * @see StoreEvent + * @see Resource#addListener(StoreListener, Class) * * @since 1.0 * @module */ -public interface StoreListener<T extends StoreEvent> { +public interface StoreListener<T 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. + * It is not necessarily the {@linkplain Resource#addListener resource in which this + * listener has been registered}; it may be one of the resource children. * * @param event description of the change or warning that occurred in a resource. Shall not be {@code null}. */ 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 new file mode 100644 index 0000000..65e274b --- /dev/null +++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/event/StoreListeners.java @@ -0,0 +1,607 @@ +/* + * 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.util.Map; +import java.util.Locale; +import java.util.IdentityHashMap; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.logging.LogRecord; +import java.lang.reflect.Method; +import org.apache.sis.util.ArraysExt; +import org.apache.sis.util.Localized; +import org.apache.sis.util.Exceptions; +import org.apache.sis.util.ArgumentChecks; +import org.apache.sis.util.logging.Logging; +import org.apache.sis.util.logging.WarningListener; +import org.apache.sis.internal.system.Modules; +import org.apache.sis.internal.storage.StoreResource; +import org.apache.sis.storage.DataStoreProvider; +import org.apache.sis.storage.DataStore; +import org.apache.sis.storage.Resource; + + +/** + * Holds a list of {@link StoreListener} instances and provides convenience methods for sending events. + * This is a helper class for {@link DataStore} and {@link Resource} implementations. + * + * <p>Observers can {@linkplain #addListener add listeners} for being notified about events, + * and producers can invoke one of the {@code warning(…)} and other methods for emitting events. + * + * <div class="section">Warning events</div> + * All warnings are given to the listeners as {@link LogRecord} instances (this allows localizable messages + * and additional information like {@linkplain LogRecord#getThrown() stack trace}, timestamp, <i>etc.</i>). + * This {@code StoreListeners} class provides convenience methods like {@link #warning(String, Exception)}, + * which build {@code LogRecord} from an exception or from a string. But all those {@code warning(…)} methods + * ultimately delegate to {@link #warning(LogRecord)}, thus providing a single point that subclasses can override. + * When a warning is emitted, the default behavior is: + * + * <ul> + * <li>Notify all listeners registered for {@link WarningEvent} type + * in this {@code StoreListeners} and in the parent managers.</li> + * <li>If previous step found no listener registered for {@code WarningEvent}, + * then log the warning in the first logger found in following choices: + * <ol> + * <li>The logger specified by {@link LogRecord#getLoggerName()} if non-null.</li> + * <li>Otherwise the logger specified by {@link org.apache.sis.storage.DataStoreProvider#getLogger()} + * if the provider can be found.</li> + * <li>Otherwise a logger whose name is the source {@link DataStore} package name.</li> + * </ol> + * </li> + * </ul> + * + * <div class="section">Thread safety</div> + * The same {@code StoreListeners} instance can be safely used by many threads without synchronization + * on the part of the caller. Subclasses should make sure that any overridden methods remain safe to call + * from multiple threads. + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.0 + * @since 1.0 + * @module + */ +public class StoreListeners extends org.apache.sis.util.logging.WarningListeners<Resource> implements Localized { + /** + * Parent manager to notify in addition to this manager. + */ + private final StoreListeners parent; + + /** + * The declared source of events. This is not necessarily the real source, + * but this is the source that the implementer wants to declare as public API. + */ + private final Resource source; + + /** + * The head of a chained list of listeners, or {@code null} if none. + * Each element in this chain contains all listeners for a given even type. + */ + private volatile ForType<?> listeners; + + /** + * All listeners for a given even type. + * + * @param <T> the type of events of interest to the listeners. + */ + private static final class ForType<T extends StoreEvent> { + /** + * The types for which listeners have been registered. + */ + final Class<T> 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; + + /** + * Next element in the chain of listeners. Intentionally final; if we want to remove an element + * then we need to recreate all previous elements with new {@code next} values. We do that for + * avoiding the need to synchronize iterations over the elements. + */ + final ForType<?> next; + + /** + * Creates a new element in the chained list of listeners. + * + * @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) { + this.type = type; + this.next = next; + } + + /** + * Adds the given listener to the list of listeners for this type. + * This method does not check if the given listener was already registered; + * it a listener is registered twice, it will need to be removed twice. + * + * <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 int length = (list != null) ? list.length : 0; + @SuppressWarnings({"unchecked", "rawtypes"}) // Generic array creation. + final StoreListener<? super T>[] copy = new StoreListener[length + 1]; + if (list != null) { + System.arraycopy(list, 0, copy, 0, length); + } + copy[length] = listener; + listeners = copy; + } + + /** + * Removes a previously registered listener. + * It the listener has been registered twice, only the most recent registration is removed. + * + * <p>It is caller responsibility to perform synchronization.</p> + */ + final void remove(final StoreListener<? super T> listener) { + StoreListener<? super T>[] list = listeners; + if (list != null) { + for (int i=list.length; --i >= 0;) { + if (list[i] == listener) { + if (list.length == 1) { + list = null; + } else { + list = ArraysExt.remove(list, i, 1); + } + listeners = list; + break; + } + } + } + } + + /** + * Returns {@code true} if this element has at least one listener. + */ + final boolean hasListeners() { + return listeners != null; + } + + /** + * Sends the given event to all listeners registered in this element. + * + * @param event the event to send to listeners. + * @param done listeners who were already notified, for avoiding to notify them twice. + * @return the {@code done} map, created when first needed. + */ + final Map<StoreListener<?>,Boolean> eventOccured(final T event, Map<StoreListener<?>,Boolean> done) { + final StoreListener<? super T>[] list = listeners; + if (list != null) { + if (done == null) { + done = new IdentityHashMap<>(list.length); + } + for (final StoreListener<? super T> listener : list) { + if (done.put(listener, Boolean.TRUE) == null) { + listener.eventOccured(event); + } + } + } + return done; + } + } + + /** + * Creates a new instance with the given parent and initially no listener. + * The parent is typically the listeners of the {@link DataStore} that created a resource. + * + * @param parent the manager to notify in addition to this manager, or {@code null} if none. + * @param source the source of events. Can not be null. + */ + public StoreListeners(final StoreListeners parent, final Resource source) { + super(source); + ArgumentChecks.ensureNonNull("source", source); + this.source = source; + this.parent = parent; + } + + /** + * Returns the source of events. This value is specified at construction time. + * + * @return the source of events. + */ + @Override + public Resource getSource() { + return source; + } + + /** + * Returns the data store of the source, or {@code null} if unknown. + */ + private static DataStore getDataStore(StoreListeners m) { + do { + final Resource source = m.source; + if (source instanceof DataStore) { + return (DataStore) source; + } + if (source instanceof StoreResource) { + final DataStore ds = ((StoreResource) source).getOriginator(); + if (ds != null) return ds; + } + m = m.parent; + } while (m != null); + return null; + } + + /** + * Returns the locale used by this manager, or {@code null} if unspecified. + * That locale is typically inherited from the {@link DataStore} locale + * and can be used for formatting messages. + * + * @return the locale for messages (typically specified by the data store), or {@code null} if unknown. + * + * @see DataStore#getLocale() + * @see StoreEvent#getLocale() + */ + @Override + public Locale getLocale() { + return StoreEvent.getLocale(source); + } + + /** + * Returns the logger where to send warnings when no other destination is specified. + * This method tries to get the logger from {@link DataStoreProvider#getLogger()}. + * If that logger can not be found, then this method infers a logger name from the + * package name of the source data store. The returned logger is used when: + * + * <ul> + * <li>no listener has been {@linkplain #addListener registered} for the {@link WarningEvent} type, and</li> + * <li>the {@code LogRecord} does not {@linkplain LogRecord#getLoggerName() specify a logger}.</li> + * </ul> + * + * @return the logger where to send the warnings when there is no other destination. + */ + private Logger logger() { + Resource src = source; + final DataStore ds = getDataStore(this); + if (ds != null) { + final DataStoreProvider provider = ds.getProvider(); + if (provider != null) { + final Logger logger = provider.getLogger(); + if (logger != null) { + return logger; + } + } + src = ds; + } + return Logging.getLogger(src.getClass()); + } + + /** + * Reports a warning described by the given message. + * + * <p>This method is a shortcut for <code>{@linkplain #warning(Level, String, Exception) + * warning}({@linkplain Level#WARNING}, message, null)</code>. + * + * @param message the warning message to report. + */ + public void warning(final String message) { + ArgumentChecks.ensureNonNull("message", message); + warning(Level.WARNING, message, null); + } + + /** + * Reports a warning described by the given exception. + * The exception stack trace will be omitted at logging time for avoiding to pollute console output + * (keeping in mind that this method should be invoked only for non-fatal warnings). + * See {@linkplain #warning(Level, String, Exception) below} for more explanation. + * + * <p>This method is a shortcut for <code>{@linkplain #warning(Level, String, Exception) + * warning}({@linkplain Level#WARNING}, null, exception)</code>. + * + * @param exception the exception to report. + */ + public void warning(final Exception exception) { + ArgumentChecks.ensureNonNull("exception", exception); + warning(Level.WARNING, null, exception); + } + + /** + * Reports a warning described by the given message and exception. + * At least one of {@code message} and {@code exception} arguments shall be non-null. + * If both are non-null, then the exception message will be concatenated after the given message. + * If the exception is non-null, its stack trace will be omitted at logging time for avoiding to + * pollute console output (keeping in mind that this method should be invoked only for non-fatal + * warnings). See {@linkplain #warning(Level, String, Exception) below} for more explanation. + * + * <p>This method is a shortcut for <code>{@linkplain #warning(Level, String, Exception) + * warning}({@linkplain Level#WARNING}, message, exception)</code>. + * + * @param message the warning message to report, or {@code null} if none. + * @param exception the exception to report, or {@code null} if none. + */ + @Override + public void warning(String message, Exception exception) { + warning(Level.WARNING, message, exception); + } + + /** + * Reports a warning at the given level represented by the given message and exception. + * At least one of {@code message} and {@code exception} arguments shall be non-null. + * If both are non-null, then the exception message will be concatenated after the given message. + * + * <div class="section">Stack trace omission</div> + * If there is no registered listener for the {@link WarningEvent} type, then the {@link #warning(LogRecord)} + * method will send the record to a logger but <em>without</em> the stack trace. + * This is done that way because stack traces consume lot of space in the logging files, while being considered + * implementation details in the context of {@code StoreListeners} (on the assumption that the logging message + * provides sufficient information). If the stack trace is desired, then users can either: + * <ul> + * <li>invoke {@code warning(LogRecord)} directly, or</li> + * <li>override {@code warning(LogRecord)} and invoke {@link LogRecord#setThrown(Throwable)} explicitly, or</li> + * <li>register a listener which will log the record itself.</li> + * </ul> + * + * @param level the warning level. + * @param message the message to log, or {@code null} if none. + * @param exception the exception to log, or {@code null} if none. + */ + @Override + public void warning(final Level level, String message, final Exception exception) { + ArgumentChecks.ensureNonNull("level", level); + final LogRecord record; + final StackTraceElement[] trace; + if (exception != null) { + trace = exception.getStackTrace(); + message = Exceptions.formatChainedMessages(getLocale(), message, exception); + if (message == null) { + message = exception.toString(); + } + record = new QuietLogRecord(level, message, exception); + } else { + ArgumentChecks.ensureNonEmpty("message", message); + trace = Thread.currentThread().getStackTrace(); // TODO: on JDK9, use StackWalker instead. + record = new LogRecord(level, message); + } + try { + for (final StackTraceElement e : trace) { + if (setPublicSource(record, Class.forName(e.getClassName()), e.getMethodName())) { + break; + } + } + } catch (ClassNotFoundException | SecurityException e) { + Logging.ignorableException(Logging.getLogger(Modules.STORAGE), StoreListeners.class, "warning", e); + } + warning(record); + } + + /** + * Eventually sets the class name and method name in the given record, + * and returns {@code true} if the method is public resource method. + * + * @param record the record where to set the source class/method name. + * @param type the source class. This method does nothing if the class is not a {@link Resource}. + * @param methodName the source method. + * @return whether the source is a public method of a {@link Resource}. + * @throws SecurityException if this method is not allowed to get the list of public methods. + */ + private static boolean setPublicSource(final LogRecord record, final Class<?> type, final String methodName) { + if (Resource.class.isAssignableFrom(type)) { + record.setSourceClassName(type.getCanonicalName()); + record.setSourceMethodName(methodName); + for (final Method m : type.getMethods()) { // List of public methods, ignoring parameters. + if (methodName.equals(m.getName())) { + return true; + } + } + } + return false; + } + + /** + * Reports a warning described by the given log record. The default implementation forwards + * the given record to one of the following destinations, in preference order: + * + * <ul> + * <li><code>{@linkplain StoreListener#eventOccured StoreListener.eventOccured}(new + * {@linkplain WarningEvent}(source, record))</code> on all listeners registered for this kind of event.</li> + * <li>Only if above step found no listener, then <code>{@linkplain Logging#getLogger(String) + * Logging.getLogger}(record.loggerName).{@linkplain Logger#log(LogRecord) log}(record)</code> + * where {@code loggerName} is one of the following: + * <ul> + * <li><code>record.{@linkplain LogRecord#getLoggerName() getLoggerName()}</code> if that value is non-null.</li> + * <li>Otherwise the value of {@link DataStoreProvider#getLogger()} if the provider is found.</li> + * <li>Otherwise the source {@link DataStore} package name.</li> + * </ul> + * </li> + * </ul> + * + * @param description warning details provided as a log record. + */ + @Override + @SuppressWarnings("unchecked") + public void warning(final LogRecord description) { + if (!fire(new WarningEvent(source, description), WarningEvent.class)) { + final String name = description.getLoggerName(); + final Logger logger; + if (name != null) { + logger = Logging.getLogger(name); + } else { + logger = logger(); + description.setLoggerName(logger.getName()); + } + if (description instanceof QuietLogRecord) { + ((QuietLogRecord) description).clearImplicitThrown(); + } + logger.log(description); + } + } + + /** + * 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 + * notifies listeners registered in parent {@code StoreListeners}s. Each listener will be + * notified only once even if it has been registered many times. + * + * @param <T> compile-time value of the {@code eventType} argument. + * @param event the event to fire. + * @param eventType the type of events to be fired. + * @return {@code true} if the event has been sent to at least one listener. + */ + @SuppressWarnings("unchecked") + public <T extends StoreEvent> boolean fire(final T event, final Class<T> eventType) { + ArgumentChecks.ensureNonNull("event", event); + ArgumentChecks.ensureNonNull("eventType", eventType); + Map<StoreListener<?>,Boolean> done = null; + StoreListeners m = this; + do { + for (ForType<?> e = m.listeners; e != null; e = e.next) { + if (e.type.isAssignableFrom(eventType)) { + done = ((ForType<? super T>) e).eventOccured(event, done); + } + } + m = m.parent; + } while (m != null); + return (done != null) && !done.isEmpty(); + } + + /** + * Registers a listener to notify when the specified kind of event occurs. + * Registering a listener for a given {@code eventType} also register the listener for all event sub-types. + * The same listener can be registered many times, but its {@link StoreListener#eventOccured(StoreEvent)} + * method will be invoked only once per event. This filtering applies even if the listener is registered + * on different resources in the same tree, for example a parent and its children. + * + * <div class="section">Warning events</div> + * If {@code eventType} is assignable from <code>{@linkplain WarningEvent}.class</code>, + * then registering that listener turns off logging of warning messages for this manager. + * 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 listener listener to notify about events. + * @param eventType type of {@link StoreEvent} to listen (can not be {@code null}). + * + * @see Resource#addListener(StoreListener, Class) + */ + @SuppressWarnings("unchecked") + public synchronized <T extends StoreEvent> void addListener(final StoreListener<? super T> listener, final Class<T> eventType) { + ArgumentChecks.ensureNonNull("listener", listener); + ArgumentChecks.ensureNonNull("eventType", eventType); + ForType<T> ce = null; + for (ForType<?> e = listeners; e != null; e = e.next) { + if (e.type.equals(eventType)) { + ce = (ForType<T>) e; + break; + } + } + if (ce == null) { + ce = new ForType<>(eventType, listeners); + } + ce.add(listener); + } + + /** + * Unregisters a listener previously added for the given type of events. + * The {@code eventType} must be the exact same class than the one given to the {@code addListener(…)} method; + * this method does not remove listeners registered for subclasses and does not remove listeners registered in + * parent manager. + * + * <p>If the same listener has been registered many times for the same even type, then this method removes only + * the most recent registration. In other words if {@code addListener(ls, type)} has been invoked twice, then + * {@code removeListener(ls, type)} needs to be invoked twice in order to remove all instances of that listener. + * If the given listener is not found, then this method does nothing (no exception is thrown).</p> + * + * <div class="section">Warning events</div> + * If {@code eventType} is <code>{@linkplain WarningEvent}.class</code> and if, after this method invocation, + * there is no remaining listener 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 listener listener to stop notifying about events. + * @param eventType type of {@link StoreEvent} which were listened (can not be {@code null}). + * + * @see Resource#removeListener(StoreListener, Class) + */ + @SuppressWarnings("unchecked") + public synchronized <T extends StoreEvent> void removeListener(StoreListener<? super T> listener, Class<T> eventType) { + ArgumentChecks.ensureNonNull("listener", listener); + ArgumentChecks.ensureNonNull("eventType", eventType); + for (ForType<?> e = listeners; e != null; e = e.next) { + if (e.type.equals(eventType)) { + ((ForType<T>) e).remove(listener); + break; + } + } + } + + /** + * Returns {@code true} if this object contains at least one listener. + * + * @return {@code true} if this object contains at least one listener, {@code false} otherwise. + */ + @Override + public boolean hasListeners() { + StoreListeners m = this; + do { + if (listeners != null && listeners.hasListeners()) { + return true; + } + m = m.parent; + } while (m != null); + return false; + } + + /** + * @deprecated Replaced by {@code addListener(listener, WarningEvent.class)}. + */ + @Override + @Deprecated + public void addWarningListener(final WarningListener<? super Resource> listener) { + addListener(new Legacy(listener), WarningEvent.class); + } + + /** + * @deprecated Replaced by {@code removeListener(listener, WarningEvent.class)}. + */ + @Override + @Deprecated + public void removeWarningListener(final WarningListener<? super Resource> listener) { + for (ForType<?> e = listeners; e != null; e = e.next) { + if (e.type.equals(WarningEvent.class)) { + StoreListener<?>[] list = e.listeners; + if (list != null) { + for (final StoreListener<?> c : list) { + if (c instanceof Legacy && ((Legacy) c).delegate == listener) { + removeListener((StoreListener<WarningEvent>) c, WarningEvent.class); + break; + } + } + } + } + } + } + + @Deprecated + private static final class Legacy implements StoreListener<WarningEvent> { + final WarningListener<? super Resource> delegate; + + Legacy(final WarningListener<? super Resource> delegate) { + this.delegate = delegate; + } + + @Override public void eventOccured(WarningEvent event) { + delegate.warningOccured(event.getSource(), event.getDescription()); + } + } +} diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/event/WarningEvent.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/event/WarningEvent.java new file mode 100644 index 0000000..9d97135 --- /dev/null +++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/event/WarningEvent.java @@ -0,0 +1,90 @@ +/* + * 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.util.logging.Level; +import java.util.logging.LogRecord; +import org.apache.sis.storage.Resource; +import org.apache.sis.util.ArgumentChecks; + + +/** + * Describes non-fatal errors that occurred in a resource or a data store. + * The warning message is encapsulated in a {@link LogRecord} object, which allows the storage of various information + * ({@linkplain LogRecord#getThrown() stack trace}, {@linkplain LogRecord#getThreadID() thread identifier}, + * {@linkplain LogRecord#getInstant() log time}, <i>etc.</i>) in addition of warning message. + * + * @author Martin Desruisseaux (Geomatys) + * @since 1.0 + * @version 1.0 + * @module + */ +public class WarningEvent extends StoreEvent { + /** + * For cross-version compatibility. + */ + private static final long serialVersionUID = 3825327888379868663L; + + /** + * The warning message together with its severity level, source method/class name, + * stack trace, thread identifier, <i>etc</i>. + */ + private final LogRecord description; + + /** + * Constructs an event for a warning that occurred in the given resource. + * + * @param source the resource on which the warning initially occurred. + * @param description log record containing warning message, stack trace (if any) and other information. + * @throws IllegalArgumentException if the given source is null. + * @throws NullPointerException if the given description is null. + */ + public WarningEvent(final Resource source, final LogRecord description) { + super(source); + ArgumentChecks.ensureNonNull("description", description); + this.description = description; + } + + /** + * Returns the warning message together with stack trace (if any) and other information. + * + * @return the log record containing warning message, stack trace and other information. + */ + public LogRecord getDescription() { + return description; + } + + /** + * Returns a string representation of this warning for debugging purpose. + * + * @return a string representation of this warning. + */ + @Override + public String toString() { + final StringBuilder b = new StringBuilder(); + final Level level = description.getLevel(); + if (level != null) { + b.append(level.getLocalizedName()).append(": "); + } + b.append(description.getMessage()); + final Throwable cause = description.getThrown(); + if (cause != null) { + b.append(System.lineSeparator()).append("Caused by ").append(cause); + } + return b.toString(); + } +} diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/event/package-info.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/event/package-info.java index 4525209..5745fca 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/storage/event/package-info.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/event/package-info.java @@ -18,9 +18,20 @@ /** * Provides interfaces and classes for dealing with different types of events fired by resources. - * The different types of events are differentiated by the {@link StoreEvent} subclasses. - * There is different subclasses for warnings, structural changes or changes in resource content. - * It is possible to register a listener for only some specific types of events. + * The different types of events are specified by the {@link StoreEvent} subclasses. + * For example if a warning occurred while reading data from a file, + * then the {@link org.apache.sis.storage.DataStore} implementation should fire a {@link WarningEvent}. + * + * <p>Events may occur in the following situations:</p> + * <ul> + * <li>When a warning occurred.</li> + * <li>When the data store content changed (e.g. new feature instance added or removed).</li> + * <li>When the data store structure changed (e.g. a column is added in tabular data).</li> + * <li>Any other change at implementation choice.</li> + * </ul> + * + * Users can {@linkplain org.apache.sis.storage.Resource#addListener declare their interest + * to a specific kind of event}. * * @author Johann Sorel (Geomatys) * @since 1.0
