blob: ab48a4d8e8517d498b0c2d16fead5bf68e962f93 [file] [log] [blame]
/*
* 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;
import java.util.Locale;
import java.util.Map;
import java.util.IdentityHashMap;
import java.util.Optional;
import org.opengis.util.ScopedName;
import org.opengis.util.GenericName;
import org.opengis.metadata.Metadata;
import org.opengis.metadata.Identifier;
import org.opengis.metadata.citation.Citation;
import org.opengis.metadata.identification.Identification;
import org.opengis.parameter.ParameterValueGroup;
import org.apache.sis.util.Localized;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.internal.storage.StoreUtilities;
import org.apache.sis.internal.storage.Resources;
import org.apache.sis.internal.util.Strings;
import org.apache.sis.referencing.NamedIdentifier;
import org.apache.sis.storage.event.StoreEvent;
import org.apache.sis.storage.event.StoreListener;
import org.apache.sis.storage.event.StoreListeners;
/**
* Manages a series of features, coverages or sensor data.
* Different {@code DataStore} subclasses exist for different formats (netCDF, GeoTIFF, <i>etc.</i>).
* The supported format can be identified by the {@linkplain #getProvider() provider}.
*
* <p>Each data store is itself a {@link Resource}. The data store subclasses should implement
* a more specialized {@code Resource} interface depending on the format characteristics.
* For example a {@code DataStore} for ShapeFiles will implement the {@link FeatureSet} interface,
* while a {@code DataStore} for netCDF files will implement the {@link Aggregate} interface.</p>
*
* <div class="section">Thread safety policy</div>
* Data stores should be thread-safe, but their synchronization lock is implementation-dependent.
* This base class uses only the {@code synchronized} keyword, applied on the following methods:
*
* <ul>
* <li>{@link #getLocale()}</li>
* <li>{@link #setLocale(Locale)}</li>
* </ul>
*
* Since above properties are used only for information purpose, concurrent modifications during a read or write
* operation should be harmless. Consequently subclasses are free use their own synchronization mechanism instead
* than {@code synchronized(this)} lock.
*
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
* @version 1.0
*
* @see DataStores#open(Object)
*
* @since 0.3
* @module
*/
public abstract class DataStore implements Resource, Localized, AutoCloseable {
/**
* The factory that created this {@code DataStore} instance, or {@code null} if unspecified.
* This information can be useful for fetching information common to all {@code DataStore}
* instances of the same class.
*
* @since 0.8
*
* @see #getProvider()
*/
protected final DataStoreProvider provider;
/**
* The store name (typically filename) for formatting error messages, or {@code null} if unknown.
* Shall <strong>not</strong> be used as an identifier.
*
* @see #getDisplayName()
*/
private final String name;
/**
* The locale to use for formatting warnings.
* This is not the locale for formatting data in the storage.
*
* @see #getLocale()
* @see #setLocale(Locale)
*/
private Locale locale;
/**
* The set of registered {@link StoreListener}s for this data store.
*/
protected final StoreListeners listeners;
/**
* Creates a new instance with no provider and initially no listener.
*/
protected DataStore() {
provider = null;
name = null;
locale = Locale.getDefault(Locale.Category.DISPLAY);
listeners = new StoreListeners(null, this);
}
/**
* Creates a new instance for the given storage (typically file or database).
* The {@code provider} argument is an optional information.
* The {@code connector} argument is mandatory.
*
* @param provider the factory that created this {@code DataStore} instance, or {@code null} if unspecified.
* @param connector information about the storage (URL, stream, reader instance, <i>etc</i>).
* @throws DataStoreException if an error occurred while creating the data store for the given storage.
*
* @since 0.8
*/
protected DataStore(final DataStoreProvider provider, final StorageConnector connector) throws DataStoreException {
ArgumentChecks.ensureNonNull("connector", connector);
this.provider = provider;
this.name = connector.getStorageName();
this.locale = Locale.getDefault(Locale.Category.DISPLAY);
this.listeners = new StoreListeners(null, this);
/*
* Above locale is NOT OptionKey.LOCALE because we are not talking about the same locale.
* The one in this DataStore is for warning and exception messages, not for parsing data.
*/
}
/**
* Creates a new instance as a child of another data store instance.
* The new instance inherits the parent {@linkplain #getProvider() provider}.
* Events created by this {@code DataStore} are forwarded to listeners registered
* into the parent data store too.
*
* @param parent the parent data store, or {@code null} if none.
* @param connector information about the storage (URL, stream, reader instance, <i>etc</i>).
* @throws DataStoreException if an error occurred while creating the data store for the given storage.
*
* @since 0.8
*/
protected DataStore(final DataStore parent, final StorageConnector connector) throws DataStoreException {
ArgumentChecks.ensureNonNull("connector", connector);
final StoreListeners forwardTo;
if (parent != null) {
forwardTo = parent.listeners;
provider = parent.provider;
locale = parent.locale;
} else {
forwardTo = null;
provider = null;
locale = Locale.getDefault(Locale.Category.DISPLAY);
}
listeners = new StoreListeners(forwardTo, this);
name = connector.getStorageName();
}
/**
* Returns the factory that created this {@code DataStore} instance.
* The provider gives additional information on this {@code DataStore} such as a format description
* and a list of parameters that can be used for opening data stores of the same class.
*
* @return the factory that created this {@code DataStore} instance, or {@code null} if unspecified.
*
* @see #provider
*
* @since 0.8
*/
public DataStoreProvider getProvider() {
return provider;
}
/**
* Returns the parameters used to open this data store.
* The collection of legal parameters is implementation-dependent
* ({@linkplain org.apache.sis.parameter.DefaultParameterValue#getDescriptor() their description}
* is given by {@link DataStoreProvider#getOpenParameters()}),
* but should contain at least a parameter named {@value org.apache.sis.storage.DataStoreProvider#LOCATION}
* with a {@link java.net.URI}, {@link java.nio.file.Path} or {@link javax.sql.DataSource} value.
*
* <p>In the event a data store must be closed and reopened later, those parameters can be stored in a file or
* database and used for {@linkplain DataStoreProvider#open(ParameterValueGroup) creating a new store} later.</p>
*
* <p>In some cases, for stores reading in-memory data or other inputs that can not fit with
* {@code ParameterDescriptorGroup} requirements (for example an {@link java.io.InputStream}
* connected to unknown or no {@link java.net.URL}), this method may return null.</p>
*
* @return parameters used for opening this {@code DataStore}, or {@code null} if not available.
*
* @see DataStoreProvider#getOpenParameters()
*
* @since 0.8
*/
public abstract ParameterValueGroup getOpenParameters();
/**
* Sets the locale to use for formatting warnings and other messages.
* In a client-server architecture, it should be the locale on the <em>client</em> side.
*
* <p>This locale is used on a <cite>best-effort</cite> basis; whether messages will honor this locale or not
* depends on the code that logged warnings or threw exceptions. In Apache SIS implementation, this locale has
* better chances to be honored by the {@link DataStoreException#getLocalizedMessage()} method rather than
* {@code getMessage()}. See {@code getLocalizedMessage()} javadoc for more information.</p>
*
* @param locale the new locale to use.
*
* @see DataStoreException#getLocalizedMessage()
*/
public synchronized void setLocale(final Locale locale) {
ArgumentChecks.ensureNonNull("locale", locale);
this.locale = locale;
}
// See class javadoc for a note on synchronization.
/**
* The locale to use for formatting warnings and other messages. This locale if for user interfaces
* 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() {
return locale;
}
/**
* Returns a short name or label for this data store.
* The returned name can be used in user interfaces or in error messages.
* It may be a title in natural language, but should be relatively short.
* The name may be localized in the language specified by the value of {@link #getLocale()}
* if this data store is capable to produce a name in various languages.
*
* <p>This name should not be used as an identifier since there is no guarantee that the name
* is unique among data stores, and no guarantee that the name is the same in all locales.
* The name may also contain any Unicode characters, including characters usually not allowed
* in identifiers like white spaces.</p>
*
* <p>This method should never throw an exception since it may be invoked for producing error
* messages, in which case throwing an exception here would mask the original exception.</p>
*
* <p>This method differs from {@link #getIdentifier()} in that it is typically a file name
* known at construction time instead than a property read from metadata.
* Default implementation returns the {@link StorageConnector#getStorageName()} value,
* or {@code null} if this data store has been created by the no-argument constructor.
* Subclasses should override this method if they can provide a better name.</p>
*
* @return a short name of label for this data store, or {@code null} if unknown.
*
* @see #getIdentifier()
* @see #getLocale()
*
* @since 0.8
*/
public String getDisplayName() {
return name;
}
/**
* Returns an identifier for the root resource of this data store, or {@code null} if none.
* If this data store contains many resources (as in an {@link Aggregate}),
* the returned identifier shall be different than the identifiers of those child resources.
* In other words, the following equality shall hold without ambiguity:
*
* {@preformat java
* findResource(getIdentifier().toString()) == this
* }
*
* Note that this identifier is not guaranteed to be unique between different {@code DataStore} instances;
* it only needs to be unique among the resources provided by this data store instance.
*
* <div class="section">Default implementation</div>
* <p>The default implementation searches for an identifier in the metadata,
* at the location shown below, provided that conditions are met:</p>
*
* <blockquote>
* <p><b>Path:</b> {@link Resource#getMetadata() metadata} /
* {@link org.apache.sis.metadata.iso.DefaultMetadata#getIdentificationInfo() identificationInfo} /
* {@link org.apache.sis.metadata.iso.identification.AbstractIdentification#getCitation() citation} /
* {@link org.apache.sis.metadata.iso.citation.DefaultCitation#getIdentifiers() identifier}</p>
*
* <p><b>Condition:</b> in default implementation, the identifier is presents only if exactly one
* {@code citation} is found at above path. If two or more {@code citation} instances are found,
* the identification is considered ambiguous and an empty value is returned.</p>
*
* <p><b>Selection:</b> the first identifier implementing the {@code GenericName} interface is returned.
* If there is no such identifier, then a {@link org.apache.sis.referencing.NamedIdentifier} is created
* from the first identifier. If there is no identifier at all, then an empty value is returned.</p>
* </blockquote>
*
* Subclasses are encouraged to override this method with more efficient implementations.
*
* @return an identifier for the root resource of this data store.
* @throws DataStoreException if an error occurred while fetching the identifier.
*
* @see #getMetadata()
* @see #getDisplayName()
*
* @since 1.0
*/
@Override
public Optional<GenericName> getIdentifier() throws DataStoreException {
final Metadata metadata = getMetadata();
if (metadata != null) {
Citation citation = null;
for (final Identification id : metadata.getIdentificationInfo()) {
final Citation c = id.getCitation();
if (c != null) {
if (citation != null && citation != c) {
return Optional.empty(); // Ambiguity.
}
citation = c;
}
}
if (citation != null) {
Identifier first = null;
for (final Identifier c : citation.getIdentifiers()) {
if (c instanceof GenericName) {
return Optional.of((GenericName) c);
} else if (first == null) {
first = c;
}
}
if (first != null) {
return Optional.of(new NamedIdentifier(first));
}
}
}
return Optional.empty();
}
/**
* Returns information about the data store as a whole. The returned metadata object can contain
* information such as the spatiotemporal extent of all contained {@linkplain Resource resources},
* contact information about the creator or distributor, data quality, update frequency, usage constraints,
* file format and more.
*
* @return information about resources in the data store, or {@code null} if none.
* @throws DataStoreException if an error occurred while reading the data.
*
* @see #getIdentifier()
*/
@Override
public abstract Metadata getMetadata() throws DataStoreException;
/**
* Searches for a resource identified by the given identifier. The given identifier should be the string
* representation of the return value of {@link Resource#getIdentifier()} on the desired resource.
* Implementation may also accept aliases for convenience. For example if the full name of a resource
* is {@code "foo:bar"}, then this method may accept {@code "bar"} as a synonymous of {@code "foo:bar"}
* provided that it does not introduce ambiguity.
*
* <p>The default implementation verifies if above criterion matches to this {@code DataStore}
* (which is itself a resource), then iterates recursively over {@link Aggregate} components
* if this data store is an aggregate.
* If a match is found without ambiguity, the associated resource is returned. Otherwise an exception is thrown.
* Subclasses are encouraged to override this method with a more efficient implementation.</p>
*
* @param identifier identifier of the resource to fetch. Must be non-null.
* @return resource associated to the given identifier (never {@code null}).
* @throws IllegalNameException if no resource is found for the given identifier, or if more than one resource is found.
* @throws DataStoreException if another kind of error occurred while searching resources.
*
* @see Resource#getIdentifier()
* @see FeatureNaming
*/
public Resource findResource(final String identifier) throws DataStoreException {
ArgumentChecks.ensureNonEmpty("identifier", identifier);
final Resource resource = findResource(identifier, this, new IdentityHashMap<>());
if (resource != null) {
return resource;
}
throw new IllegalNameException(StoreUtilities.resourceNotFound(this, identifier));
}
/**
* Recursively searches for a resource identified by the given identifier.
* This is the implementation of {@link #findResource(String)}.
*
* @param identifier identifier of the resource to fetch.
* @param candidate a resource to compare against the identifier.
* @param visited resources visited so-far, for avoiding never-ending loops if cycles exist.
* @return resource associated to the given identifier, or {@code null} if not found.
*/
private Resource findResource(final String identifier, final Resource candidate,
final Map<Resource,Boolean> visited) throws DataStoreException
{
if (candidate != null && visited.put(candidate, Boolean.TRUE) == null) {
GenericName name = candidate.getIdentifier().orElse(null);
if (name != null) {
do if (identifier.equals(name.toString())) {
return candidate;
}
while ((name instanceof ScopedName) && name != (name = ((ScopedName) name).tail()));
}
if (candidate instanceof Aggregate) {
Resource result = null;
for (final Resource child : ((Aggregate) candidate).components()) {
final Resource match = findResource(identifier, child, visited);
if (match != null) {
if (result == null) {
result = match;
} else {
throw new IllegalNameException(Resources.forLocale(getLocale())
.getString(Resources.Keys.ResourceIdentifierCollision_2, getDisplayName(), identifier));
}
}
}
return result;
}
}
return null;
}
/**
* Registers a listener to notify when the specified kind of event occurs in this data store or in a resource.
* The data store will call the {@link StoreListener#eventOccured(StoreEvent)} method when new events matching
* the {@code eventType} occur. An event may be a change in data store 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 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 individual resources of this data store.</p>
*
* <p>If this data store 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(Class, StoreListener) explicitly removed}
* or unless this {@code DataStore} is itself garbage collected. However if the given type of events can never
* happen with this data store, then this method is not required to keep a reference to the given listener.</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 data store.
* 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 eventType type of {@link StoreEvent} to listen (can not be {@code null}).
* @param listener listener to notify about events.
*
* @since 1.0
*/
@Override
public <T extends StoreEvent> void addListener(Class<T> eventType, StoreListener<? super T> listener) {
listeners.addListener(eventType, listener);
}
/**
* Unregisters a listener previously added to this data store 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
* children resources.
*
* <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(type, ls)} has been invoked twice, then
* {@code removeListener(type, ls)} 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 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 DataStore} will send future warnings to the loggers.
*
* @param <T> compile-time value of the {@code eventType} argument.
* @param eventType type of {@link StoreEvent} which were listened (can not be {@code null}).
* @param listener listener to stop notifying about events.
*
* @since 1.0
*/
@Override
public <T extends StoreEvent> void removeListener(Class<T> eventType, StoreListener<? super T> listener) {
listeners.removeListener(eventType, listener);
}
/**
* Closes this data store and releases any underlying resources.
*
* @throws DataStoreException if an error occurred while closing this data store.
*/
@Override
public abstract void close() throws DataStoreException;
/**
* Returns a string representation of this data store for debugging purpose.
* The content of the string returned by this method may change in any future SIS version.
*
* @return a string representation of this data store for debugging purpose.
*/
@Override
public String toString() {
return Strings.bracket(getClass(), getDisplayName());
}
}