/*
* Geotoolkit - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2009-2014, Geomatys
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotoolkit.data;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.sis.feature.FeatureTypeExt;
import org.apache.sis.feature.ReprojectFeatureType;
import org.apache.sis.feature.ViewFeatureType;
import org.apache.sis.storage.DataStoreException;
import org.geotoolkit.data.memory.GenericEmptyFeatureIterator;
import org.geotoolkit.data.memory.GenericFeatureWriter;
import org.geotoolkit.data.memory.GenericFilterFeatureIterator;
import org.geotoolkit.data.query.Query;
import org.geotoolkit.data.query.QueryBuilder;
import org.geotoolkit.data.query.Selector;
import org.geotoolkit.data.query.Source;
import org.geotoolkit.data.session.DefaultSession;
import org.geotoolkit.data.session.Session;
import org.geotoolkit.factory.Hints;
import org.geotoolkit.factory.HintsPending;
import org.geotoolkit.util.NamesExt;
import org.geotoolkit.parameter.Parameters;
import org.apache.sis.util.logging.Logging;
import org.geotoolkit.data.memory.GenericQueryFeatureIterator;
import org.geotoolkit.storage.StorageEvent;
import org.geotoolkit.storage.StorageListener;
import org.geotoolkit.version.Version;
import org.geotoolkit.version.VersionControl;
import org.geotoolkit.version.VersioningException;
import org.opengis.util.GenericName;
import org.geotoolkit.storage.DataStore;
import org.opengis.feature.Feature;
import org.opengis.feature.FeatureType;
import org.opengis.feature.MismatchedFeatureException;
import org.opengis.feature.PropertyType;
import org.opengis.filter.Filter;
import org.opengis.filter.Id;
import org.opengis.filter.identity.FeatureId;
import org.opengis.filter.sort.SortBy;
import org.opengis.geometry.Envelope;
import org.opengis.metadata.Metadata;
import org.opengis.parameter.ParameterNotFoundException;
import org.opengis.parameter.ParameterValueGroup;
import org.apache.sis.internal.feature.AttributeConvention;
import org.apache.sis.util.ArgumentChecks;
import org.opengis.util.ScopedName;
/**
* Uncomplete implementation of a feature store that handle most methods
* by fallbacking on others. It also offer some generic methods to
* handle query parameters and events.
*
* @author Johann Sorel (Geomatys)
* @module
*/
public abstract class AbstractFeatureStore extends DataStore implements FeatureStore {
/**
* Static variables refering to GML model.
*/
public static final String GML_311_NAMESPACE = "http://www.opengis.net/gml";
public static final String GML_32_NAMESPACE = "http://www.opengis.net/gml/3.2";
public static final String GML_NAME = "name";
public static final String GML_DESCRIPTION = "description";
protected static final String NO_NAMESPACE = "no namespace";
private final Logger Logger = Logging.getLogger("org.geotoolkit.data");
protected final ParameterValueGroup parameters;
protected String defaultNamespace;
protected final Set<StorageListener> listeners = new HashSet<>();
protected AbstractFeatureStore(final ParameterValueGroup params) {
this.parameters = params;
String namespace = null;
if(params != null){
try{
namespace = (String)Parameters.getOrCreate(AbstractFeatureStoreFactory.NAMESPACE, params).getValue();
}catch(ParameterNotFoundException ex){
//ignore this error, factory might not necessarily have a namespace parameter
//example : gpx
}
}
if (namespace == null) {
defaultNamespace = "http://geotoolkit.org";
} else if (namespace.equals(NO_NAMESPACE)) {
defaultNamespace = null;
} else {
defaultNamespace = namespace;
}
}
@Override
public ParameterValueGroup getConfiguration() {
return parameters;
}
protected String getDefaultNamespace() {
return defaultNamespace;
}
protected Logger getLogger(){
return Logger;
}
@Override
public Metadata getMetadata() throws DataStoreException {
return null;
}
/**
* Overwrite to enable versioning.
* @param version
*/
@Override
public VersionControl getVersioning(String typeName) throws VersioningException{
throw new VersioningException("Versioning not supported");
}
/**
* {@inheritDoc }
*/
@Override
public final Session createSession(final boolean async) {
return createSession(async, null);
}
/**
* {@inheritDoc }
*/
@Override
public Session createSession(final boolean async, Version version) {
return new DefaultSession(this, async,version);
}
@Override
public FeatureType getFeatureType(final Query query) throws DataStoreException, MismatchedFeatureException {
final Source source = query.getSource();
if(Query.GEOTK_QOM.equalsIgnoreCase(query.getLanguage()) && source instanceof Selector){
final Selector selector = (Selector) source;
FeatureType ft = selector.getSession().getFeatureStore().getFeatureType(query.getTypeName());
final String[] properties = query.getPropertyNames();
if (properties!=null && FeatureTypeExt.isAllProperties(ft, properties)) {
ft = new ViewFeatureType(ft, properties);
}
if(query.getCoordinateSystemReproject()!=null){
ft = new ReprojectFeatureType(ft, query.getCoordinateSystemReproject());
}
return ft;
}
throw new DataStoreException("Can not deduce feature type of query : " + query);
}
/**
* Default implementation, will return a list with the single feature tpe from method
* {@link #getFeatureType(org.geotoolkit.feature.type.Name) }
*
* @param typeName
* @return
* @throws DataStoreException
*/
@Override
public List<FeatureType> getFeatureTypeHierarchy(String typeName) throws DataStoreException {
return Collections.singletonList((FeatureType)getFeatureType(typeName));
}
/**
* {@inheritDoc }
*
* This implementation will try to aquire a writer and return true if it
* succeed.
*/
@Override
public boolean isWritable(final String typeName) throws DataStoreException {
//while raise an error if type doesnt exist
getFeatureType(typeName);
FeatureWriter writer = null;
try{
writer = getFeatureWriter(QueryBuilder.filtered(typeName, Filter.EXCLUDE));
return true;
}catch(Exception ex){
//catch anything, log it
getLogger().log(Level.WARNING, "Type not writable : {0}", ex.getMessage());
return false;
}finally{
if(writer != null){
writer.close();
}
}
}
/**
* {@inheritDoc }
*
* This implementation will aquiere a reader and iterate to count.
* Subclasses should override this method if they have a faster way to
* calculate count.
*/
@Override
public long getCount(Query query) throws DataStoreException {
query = addSeparateFeatureHint(query);
final FeatureReader reader = getFeatureReader(query);
return FeatureStoreUtilities.calculateCount(reader);
}
/**
* {@inheritDoc }
*
* This implementation will aquiere a reader and iterate to expend an envelope.
* Subclasses should override this method if they have a faster way to
* calculate envelope.
* @throws DataStoreException
* @throws FeatureStoreRuntimeException
*/
@Override
public Envelope getEnvelope(Query query) throws DataStoreException, FeatureStoreRuntimeException {
if(query.retrieveAllProperties()){
//we simplify it, get only geometry attributes + sort attribute
final FeatureType ft = getFeatureType(query.getTypeName());
final List<String> names = new ArrayList<>();
for(PropertyType desc : ft.getProperties(true)){
if(AttributeConvention.isGeometryAttribute(desc)){
names.add(desc.getName().toString());
} else if (query.getSortBy() != null) {
for (SortBy sortBy : query.getSortBy()) {
final String propName = sortBy.getPropertyName().getPropertyName();
if (desc.getName().toString().equals(propName) ||
desc.getName().tip().toString().equals(propName)) {
names.add(desc.getName().toString());
}
}
}
}
if(names.isEmpty()){
//no geometry field
return null;
}
final QueryBuilder qb = new QueryBuilder(query);
qb.setProperties(names.toArray(new String[names.size()]));
query = qb.buildQuery();
}
final String[] wantedProp = query.getPropertyNames();
if(wantedProp.length==0){
return null;
}
final FeatureReader reader = getFeatureReader(query);
return FeatureStoreUtilities.calculateEnvelope(reader);
}
private static Query addSeparateFeatureHint(final Query query){
//hints never null on a query
Hints hints = query.getHints();
hints.put(HintsPending.FEATURE_DETACHED, Boolean.FALSE);
return query;
}
@Override
public final List<FeatureId> addFeatures(String groupName, Collection<? extends Feature> newFeatures) throws DataStoreException {
return addFeatures(groupName,newFeatures,new Hints());
}
/**
* {@inheritDoc }
*/
@Override
public void close() throws DataStoreException {
synchronized (listeners) {
listeners.clear();
}
}
////////////////////////////////////////////////////////////////////////////
// listeners methods ///////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
/**
* Fires a schema add event to all listeners.
*
* @param name added schema name
* @param type added feature type
*/
protected void fireSchemaAdded(final GenericName name, final FeatureType type){
sendStructureEvent(FeatureStoreManagementEvent.createAddEvent(this, name, type));
}
/**
* Fires a schema update event to all listeners.
*
* @param name updated schema name
* @param oldType featuretype before change
* @param newType featuretype after change
*/
protected void fireSchemaUpdated(final GenericName name, final FeatureType oldType, final FeatureType newType){
sendStructureEvent(FeatureStoreManagementEvent.createUpdateEvent(this, name, oldType, newType));
}
/**
* Fires a schema delete event to all listeners.
*
* @param name deleted schema name
* @param type feature type of the deleted schema
*/
protected void fireSchemaDeleted(final GenericName name, final FeatureType type){
sendStructureEvent(FeatureStoreManagementEvent.createDeleteEvent(this, name, type));
}
/**
* Fires a features add event.
*
* @param name of the schema where features where added.
* @param ids modified feature ids
*/
protected void fireFeaturesAdded(final GenericName name, final Id ids){
sendContentEvent(FeatureStoreContentEvent.createAddEvent(this, name, ids));
}
/**
* Fires a features update event.
*
* @param name of the schema where features where updated.
* @param ids modified feature ids
*/
protected void fireFeaturesUpdated(final GenericName name, final Id ids){
sendContentEvent(FeatureStoreContentEvent.createUpdateEvent(this, name, ids));
}
/**
* Fires a features delete event.
*
* @param name of the schema where features where deleted
* @param ids modified feature ids
*/
protected void fireFeaturesDeleted(final GenericName name, final Id ids){
sendContentEvent(FeatureStoreContentEvent.createDeleteEvent(this, name, ids));
}
////////////////////////////////////////////////////////////////////////////
// useful methods for feature store that doesn't implement all query parameters/
////////////////////////////////////////////////////////////////////////////
/**
* Convenient method to check that the given type name exist.
* Will raise a datastore exception if the name do not exist in this FeatureStore.
*
* @param candidate Name to test.
* @throws DataStoreException if name do not exist.
*/
protected void typeCheck(final String candidate) throws DataStoreException{
ArgumentChecks.ensureNonNull("type name", candidate);
int count = 0;
final Collection<GenericName> names = getNames();
for (GenericName name : names) {
if (candidate.equals(name.toString())) {
count++;
if (count>1) break;
}
while (name instanceof ScopedName) {
name = ((ScopedName)name).tail();
if (candidate.equals(name.toString())) {
count++;
if (count>1) break;
}
}
}
if (count>1) {
final StringBuilder sb = new StringBuilder();
sb.append("Multiple match for name ");
sb.append(candidate);
sb.append(", available names are : ");
for(final GenericName n : names){
sb.append(n).append(", ");
}
throw new DataStoreException(sb.toString());
} else if (count==0) {
final StringBuilder sb = new StringBuilder("Type name : ");
sb.append(candidate);
sb.append(" do not exist in this feature store, available names are : ");
for(final GenericName n : names){
sb.append(n).append(", ");
}
throw new DataStoreException(sb.toString());
}
}
/**
* Wrap a feature reader with a query.
* This method can be use if the FeatureStore implementation can not support all
* filtering parameters. The returned reader will repect the remaining query
* parameters but keep in mind that this is done in a generic way, which might
* not be the most effective way.
*
* Becareful if you give a sortBy parameter in the query, this can cause
* OutOfMemory errors since the generic implementation must iterate over all
* feature and holds them in memory before ordering them.
* It may be a better solution to say in the query capabilities that sortBy
* are not handle by this FeatureStore implementation.
*
* @param reader FeatureReader to wrap
* @param remainingParameters , query holding the parameters that where not handle
* by the FeatureStore implementation
* @return FeatureReader Reader wrapping the given reader with all query parameters
* @throws org.apache.sis.storage.DataStoreException
*/
protected FeatureReader handleRemaining(FeatureReader reader, final Query remainingParameters) throws DataStoreException{
return GenericQueryFeatureIterator.wrap(reader, remainingParameters);
}
/**
* Wrap a feature writer with a Filter.
* This method can be used when the featurestore implementation is not
* intelligent enough to handle filtering.
*
* @param writer featureWriter to filter
* @param filter filter to use for hiding feature while iterating
* @return Filtered FeatureWriter
* @throws DataStoreException
*/
protected FeatureWriter handleRemaining(FeatureWriter writer, Filter filter) throws DataStoreException{
//wrap filter ----------------------------------------------------------
if(filter != null && filter != Filter.INCLUDE){
writer = GenericFilterFeatureIterator.wrap(writer, filter);
}
return writer;
}
/**
* Convinient method to handle adding features operation by using the
* FeatureWriter.
*
* @param groupName
* @param newFeatures
* @return list of ids of the features added.
* @throws DataStoreException
*/
protected List<FeatureId> handleAddWithFeatureWriter(final String groupName, final Collection<? extends Feature> newFeatures,
final Hints hints) throws DataStoreException{
try(FeatureWriter featureWriter = getFeatureWriter(QueryBuilder.filtered(groupName, Filter.EXCLUDE))) {
while (featureWriter.hasNext()) {
featureWriter.next();
}
return FeatureStoreUtilities.write(featureWriter, newFeatures);
}catch(FeatureStoreRuntimeException ex){
throw new DataStoreException(ex);
}
}
/**
* Convinient method to handle adding features operation by using the
* FeatureWriter.
*
* @param groupName
* @param filter
* @param values
* @throws DataStoreException
*/
protected void handleUpdateWithFeatureWriter(final String groupName, final Filter filter,
final Map<String, ?> values) throws DataStoreException {
try(FeatureWriter writer = getFeatureWriter(QueryBuilder.filtered(groupName, filter))) {
while(writer.hasNext()){
final Feature f = writer.next();
for(final Entry<String,?> entry : values.entrySet()){
f.setPropertyValue(entry.getKey(), entry.getValue());
}
writer.write();
}
} catch(FeatureStoreRuntimeException ex){
throw new DataStoreException(ex);
}
}
/**
* Convinient method to handle adding features operation by using the
* FeatureWriter.
*
* @param groupName
* @param filter
* @throws DataStoreException
*/
protected void handleRemoveWithFeatureWriter(final String groupName, final Filter filter) throws DataStoreException {
try(FeatureWriter writer = getFeatureWriter(QueryBuilder.filtered(groupName, filter))) {
while(writer.hasNext()){
writer.next();
writer.remove();
}
} catch(FeatureStoreRuntimeException ex){
throw new DataStoreException(ex);
}
}
/**
* Convenient method to handle modification operation by using the
* add, remove, update methods.
*
* @param query
* @return
* @throws DataStoreException
*/
protected FeatureWriter handleWriter(Query query) throws DataStoreException {
final Filter filter = query.getFilter();
final String groupName = query.getTypeName();
if (Filter.EXCLUDE.equals(filter) ) {
return GenericFeatureWriter.wrapAppend(this, groupName);
} else {
return GenericFeatureWriter.wrap(this, groupName, filter);
}
}
public static GenericName ensureGMLNS(final String namespace, final String local){
if(local.equals(GML_NAME)){
return NamesExt.create(GML_311_NAMESPACE, GML_NAME);
}else if(local.equals(GML_DESCRIPTION)){
return NamesExt.create(GML_311_NAMESPACE, GML_DESCRIPTION);
}else{
return NamesExt.create(namespace, local);
}
}
@Override
public void addStorageListener(final StorageListener listener) {
synchronized (listeners) {
listeners.add(listener);
}
}
@Override
public void removeStorageListener(final StorageListener listener) {
synchronized (listeners) {
listeners.remove(listener);
}
}
/**
* Forward a structure event to all listeners.
* @param event , event to send to listeners.
*/
protected void sendStructureEvent(final StorageEvent event){
final StorageListener[] lst;
synchronized (listeners) {
lst = listeners.toArray(new StorageListener[listeners.size()]);
}
for(final StorageListener listener : lst){
listener.structureChanged(event);
}
}
/**
* Forward a data event to all listeners.
* @param event , event to send to listeners.
*/
protected void sendContentEvent(final StorageEvent event){
final StorageListener[] lst;
synchronized (listeners) {
lst = listeners.toArray(new StorageListener[listeners.size()]);
}
for(final StorageListener listener : lst){
listener.contentChanged(event);
}
}
/**
* Forward given event, changing the source by this object.
* For implementation use only.
* @param event
*/
public void forwardStructureEvent(StorageEvent event){
sendStructureEvent(event.copy(this));
}
/**
* Forward given event, changing the source by this object.
* For implementation use only.
* @param event
*/
public void forwardContentEvent(StorageEvent event){
sendContentEvent(event.copy(this));
}
}