sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] 05/05: Prepare replacement of WarningListeners by StoreListeners. https://issues.apache.org/jira/browse/SIS-421
Date Fri, 06 Sep 2019 18:11:53 GMT
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 <martin.desruisseaux@geomatys.com>
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


Mime
View raw message