/* * Geotoolkit - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2008, Open Source Geospatial Foundation (OSGeo) * (C) 2009, 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.shapefile; import com.vividsolutions.jts.geom.Geometry; import org.geotoolkit.data.FeatureStoreFactory; import org.geotoolkit.data.shapefile.lock.ShpFileType; import org.geotoolkit.data.shapefile.lock.StorageFile; import org.geotoolkit.data.shapefile.lock.ShpFiles; import org.geotoolkit.data.shapefile.lock.AccessManager; import java.io.IOException; import java.net.MalformedURLException; import java.net.URI; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.channels.ReadableByteChannel; import java.nio.channels.WritableByteChannel; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.logging.Level; import org.apache.sis.feature.FeatureExt; import org.apache.sis.feature.FeatureTypeExt; import org.apache.sis.feature.builder.AttributeRole; import org.apache.sis.feature.builder.AttributeTypeBuilder; import org.apache.sis.feature.builder.FeatureTypeBuilder; import org.apache.sis.internal.feature.AttributeConvention; import org.apache.sis.io.wkt.Convention; import org.apache.sis.io.wkt.WKTFormat; import org.apache.sis.metadata.iso.citation.Citations; import org.geotoolkit.data.query.QueryBuilder; import org.geotoolkit.data.AbstractFeatureStore; import org.apache.sis.storage.DataStoreException; import org.geotoolkit.data.FeatureStoreRuntimeException; import org.geotoolkit.data.FeatureReader; import org.geotoolkit.data.FeatureWriter; import org.geotoolkit.data.memory.GenericEmptyFeatureIterator; import org.geotoolkit.data.query.DefaultQueryCapabilities; import org.geotoolkit.data.query.Query; import org.geotoolkit.data.query.QueryCapabilities; import org.geotoolkit.data.query.QueryUtilities; import org.geotoolkit.data.dbf.DbaseFileHeader; import org.geotoolkit.data.dbf.DbaseFileReader; import org.geotoolkit.data.shapefile.shp.ShapeType; import org.geotoolkit.data.shapefile.shp.ShapefileHeader; import org.geotoolkit.data.shapefile.shp.ShapefileReader; import org.geotoolkit.data.shapefile.shp.ShapefileWriter; import org.geotoolkit.factory.Hints; import org.geotoolkit.filter.visitor.FilterAttributeExtractor; import org.geotoolkit.geometry.jts.JTSEnvelope2D; import org.geotoolkit.io.wkt.PrjFiles; import org.geotoolkit.nio.IOUtilities; import org.geotoolkit.parameter.Parameters; import org.apache.sis.referencing.CRS; import org.apache.sis.referencing.CommonCRS; import org.geotoolkit.data.shapefile.cpg.CpgFiles; import org.geotoolkit.storage.DataFileStore; import org.opengis.util.GenericName; import org.opengis.filter.Filter; import org.opengis.filter.identity.FeatureId; import org.opengis.geometry.Envelope; import org.opengis.parameter.ParameterValueGroup; import org.opengis.referencing.crs.CoordinateReferenceSystem; import static org.geotoolkit.data.shapefile.lock.ShpFileType.*; import org.geotoolkit.storage.DataStores; import org.geotoolkit.util.NamesExt; import org.opengis.feature.AttributeType; import org.opengis.feature.Feature; import org.opengis.feature.FeatureType; import org.opengis.feature.MismatchedFeatureException; import org.opengis.feature.PropertyNotFoundException; import org.opengis.feature.PropertyType; /** * * @author Johann Sorel (Geomatys) * @module */ public class ShapefileFeatureStore extends AbstractFeatureStore implements DataFileStore { // This is the default character as specified by the DBF specification public static final Charset DEFAULT_STRING_CHARSET = DbaseFileReader.DEFAULT_STRING_CHARSET; private final QueryCapabilities capabilities = new DefaultQueryCapabilities(false); protected final ShpFiles shpFiles; protected final boolean useMemoryMappedBuffer; protected final Charset dbfCharset; private GenericName name; private FeatureType schema; /** * Creates a new instance of ShapefileDataStore. * * @param uri The URL of the shp file to use for this DataSource. * * @throws NullPointerException DOCUMENT ME! * @throws DataStoreException If computation of related URLs (dbf,shx) fails. * @throws java.net.MalformedURLException If we fail parsing input URI */ public ShapefileFeatureStore(final URI uri) throws DataStoreException,MalformedURLException { this(uri, null); } /** * this sets the datastore's namespace during construction (so the schema - * FeatureType - will have the correct value) You can call this with * namespace = null, but I suggest you give it an actual namespace. * * @param uri * @param namespace * @throws java.net.MalformedURLException If we fail parsing input URI * @throws org.apache.sis.storage.DataStoreException If input data analysis fails. */ public ShapefileFeatureStore(final URI uri, final String namespace) throws DataStoreException,MalformedURLException { this(uri, namespace, false, null); } /** * This sets the datastore's namespace during construction (so the schema - * FeatureType - will have the correct value) You can call this with * namespace = null, but I suggest you give it an actual namespace. * * @param uri * @param namespace * @param useMemoryMapped : default is true * @param dbfCharset : if null default will be ShapefileDataStore.DEFAULT_STRING_CHARSET * @throws java.net.MalformedURLException If we fail parsing input URI * @throws org.apache.sis.storage.DataStoreException If input data analysis fails. */ public ShapefileFeatureStore(final URI uri, final String namespace, final boolean useMemoryMapped, Charset dbfCharset) throws MalformedURLException, DataStoreException { this(toParameter(uri, namespace, useMemoryMapped, dbfCharset)); } public ShapefileFeatureStore(final ParameterValueGroup params) throws MalformedURLException, DataStoreException { super(params); final URI uri = (URI) params.parameter( ShapefileFeatureStoreFactory.PATH.getName().toString()).getValue(); final Boolean useMemoryMapped = (Boolean) params.parameter( ShapefileFeatureStoreFactory.MEMORY_MAPPED.getName().toString()).getValue(); Charset dbfCharset = (Charset) params.parameter( ShapefileFeatureStoreFactory.DBFCHARSET.getName().toString()).getValue(); shpFiles = new ShpFiles(uri); //search for a .cpg file which contains the character encoding if(dbfCharset == null && shpFiles.exists(CPG)){ try (ReadableByteChannel channel = shpFiles.getReadChannel(CPG)) { dbfCharset = CpgFiles.read(channel); } catch (IOException ex) { throw new DataStoreException(ex.getMessage(), ex); } } if(dbfCharset == null){ dbfCharset = DEFAULT_STRING_CHARSET; } if (!shpFiles.isWritable() || !shpFiles.exists(SHP)) { this.useMemoryMappedBuffer = false; } else { this.useMemoryMappedBuffer = useMemoryMapped; } this.dbfCharset = dbfCharset; } private static ParameterValueGroup toParameter(final URI uri, final String namespace, final boolean useMemoryMapped, Charset dbfCharset){ final ParameterValueGroup params = ShapefileFeatureStoreFactory.PARAMETERS_DESCRIPTOR.createValue(); Parameters.getOrCreate(ShapefileFeatureStoreFactory.PATH, params).setValue(uri); Parameters.getOrCreate(ShapefileFeatureStoreFactory.NAMESPACE, params).setValue(namespace); Parameters.getOrCreate(ShapefileFeatureStoreFactory.MEMORY_MAPPED, params).setValue(useMemoryMapped); if(dbfCharset!=null){ Parameters.getOrCreate(ShapefileFeatureStoreFactory.DBFCHARSET, params).setValue(dbfCharset); } return params; } @Override public FeatureStoreFactory getFactory() { return (FeatureStoreFactory) DataStores.getFactoryById(ShapefileFeatureStoreFactory.NAME); } @Override public boolean isWritable(final String typeName) throws DataStoreException { return shpFiles.isWritable(); } public GenericName getName() throws DataStoreException{ checkTypeExist(); return name; } public FeatureType getFeatureType() throws DataStoreException{ checkTypeExist(); return schema; } private void checkTypeExist() throws DataStoreException { if (name != null && schema != null) { return; } this.schema = buildSchema(getDefaultNamespace()); this.name = schema.getName(); } /** * {@inheritDoc } */ @Override public Set<GenericName> getNames() throws DataStoreException { return Collections.singleton(getName()); } /** * {@inheritDoc } */ @Override public FeatureType getFeatureType(final String typeName) throws DataStoreException { typeCheck(typeName); return schema; } /** * {@inheritDoc } */ @Override public QueryCapabilities getQueryCapabilities() { return capabilities; } /** * Gets the bounding box of the file represented by this data store as a * whole (that is, off all of the features in the shapefile) * * @param query A query to specify which data to use for envelope computing. * @return The bounding box of the datasource or null if unknown and too * expensive for the method to calculate. * @throws DataStoreException If reading of source features fails. */ @Override public Envelope getEnvelope(final Query query) throws DataStoreException, FeatureStoreRuntimeException { typeCheck(query.getTypeName()); if(QueryUtilities.queryAll(query)){ // This is way quick!!! ReadableByteChannel in = null; try { final ByteBuffer buffer = ByteBuffer.allocate(100); in = shpFiles.getReadChannel(SHP); try { in.read(buffer); buffer.flip(); final ShapefileHeader header = ShapefileHeader.read(buffer, true); final com.vividsolutions.jts.geom.Envelope env = new com.vividsolutions.jts.geom.Envelope( header.minX(), header.maxX(), header.minY(), header.maxY()); if (schema != null) { return new JTSEnvelope2D(env, FeatureExt.getCRS(schema)); }else{ return new JTSEnvelope2D(env, null); } } finally { in.close(); } } catch (IOException ioe) { // What now? This seems arbitrarily appropriate ! throw new DataStoreException("Problem getting Bbox", ioe); } finally { try { if (in != null) { in.close(); } } catch (IOException ioe) { // do nothing } } }else{ return super.getEnvelope(query); } } /** * {@inheritDoc } */ @Override public FeatureReader getFeatureReader(final Query query) throws DataStoreException { final FeatureType type = getFeatureType(query.getTypeName()); final Hints hints = query.getHints(); final String typeName = type.getName().tip().toString(); final String[] propertyNames = query.getPropertyNames(); final AttributeType geomAtt = FeatureExt.getDefaultGeometryAttribute(schema); final GenericName defaultGeomName = geomAtt.getName(); final double[] resample = query.getResolution(); //check if we must read the 3d values final CoordinateReferenceSystem reproject = query.getCoordinateSystemReproject(); final boolean read3D = (reproject == null || CRS.getVerticalComponent(reproject, true) != null); // gather attributes needed by the query tool, they will be used by the // query filter final FilterAttributeExtractor extractor = new FilterAttributeExtractor(); final Filter filter = query.getFilter(); filter.accept(extractor, null); final GenericName[] filterAttnames = extractor.getAttributeNames(); // check if the geometry is the one and only attribute needed // to return attribute _and_ to run the query filter if ( propertyNames != null && propertyNames.length == 1 && NamesExt.valueOf(propertyNames[0]).tip().toString().equals(defaultGeomName.tip().toString()) && (filterAttnames.length == 0 || (filterAttnames.length == 1 && filterAttnames[0].tip().toString() .equals(defaultGeomName.tip().toString())))) { try { final FeatureType newSchema = FeatureTypeExt.createSubType(schema, propertyNames); final ShapefileAttributeReader attReader = getAttributesReader(false,read3D,resample); final FeatureIDReader idReader = new DefaultFeatureIDReader(typeName); FeatureReader reader = ShapefileFeatureReader.create(attReader, idReader, newSchema, hints); final QueryBuilder remaining = new QueryBuilder(query.getTypeName()); remaining.setProperties(query.getPropertyNames()); remaining.setFilter(query.getFilter()); remaining.setHints(query.getHints()); remaining.setCRS(query.getCoordinateSystemReproject()); remaining.setSortBy(query.getSortBy()); remaining.setStartIndex(query.getStartIndex()); remaining.setMaxFeatures(query.getMaxFeatures()); reader = handleRemaining(reader, remaining.buildQuery()); return reader; } catch (MismatchedFeatureException se) { throw new DataStoreException("Error creating schema", se); } }else{ try { final FeatureType newSchema; if (propertyNames != null) { newSchema = FeatureTypeExt.createSubType(schema, propertyNames); } else { newSchema = schema; } final ShapefileAttributeReader attReader = getAttributesReader(true,read3D,resample); final FeatureIDReader idReader = new DefaultFeatureIDReader(typeName); FeatureReader reader = ShapefileFeatureReader.create(attReader,idReader, newSchema, hints); QueryBuilder query2 = new QueryBuilder(query.getTypeName()); query2.setProperties(query.getPropertyNames()); query2.setFilter(query.getFilter()); query2.setHints(query.getHints()); query2.setCRS(query.getCoordinateSystemReproject()); query2.setSortBy(query.getSortBy()); query2.setStartIndex(query.getStartIndex()); query2.setMaxFeatures(query.getMaxFeatures()); reader = handleRemaining(reader, query2.buildQuery()); return reader; } catch (MismatchedFeatureException se) { throw new DataStoreException("Error creating schema", se); } } } /** * {@inheritDoc } */ @Override public FeatureWriter getFeatureWriter(Query query) throws DataStoreException { FeatureType type = getFeatureType(query.getTypeName()); final ShapefileAttributeReader attReader = getAttributesReader(true,true,null); final FeatureIDReader idReader = new DefaultFeatureIDReader(type.getName().tip().toString()); FeatureReader featureReader; try { featureReader = ShapefileFeatureReader.create(attReader,idReader, schema, query.getHints()); } catch (Exception e) { featureReader = GenericEmptyFeatureIterator.createReader(schema); } try { return handleRemaining(new ShapefileFeatureWriter(this,type.getName().tip().toString(), shpFiles, attReader, featureReader, dbfCharset),query.getFilter()); } catch (IOException ex) { throw new DataStoreException(ex); } } //////////////////////////////////////////////////////////////////////////// // schema manipulation ///////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// /** * Set the FeatureType of this DataStore. This method will delete any * existing local resources or throw an IOException if the featurestore is * remote. * * @param typeName Name to use in this store for the given feature type. * @param featureType The desired FeatureType. * @throws DataStoreException If the featurestore is remote. * * @todo must synchronize this properly */ @Override public void createFeatureType(final FeatureType featureType) throws DataStoreException { final GenericName typeName = featureType.getName(); if (!isWritable(typeName.toString())) { throw new DataStoreException("Read-only acces prevent type creation."); } if(typeName == null){ throw new DataStoreException("Type name can not be null."); } if(!featureType.isSimple()){ throw new DataStoreException("Feature type must not be null and must be a simple feature type."); } if(!featureType.getName().equals(typeName)){ throw new DataStoreException("Shapefile featurestore can only hold typename same as feature type name."); } try { //delete the files shpFiles.delete(); } catch (IOException ex) { throw new DataStoreException("Cannot reset datastore content", ex); } final AccessManager locker = shpFiles.createLocker(); //update schema and name name = typeName; schema = featureType; AttributeType desc = FeatureExt.getDefaultGeometryAttribute(featureType); if (desc == null) { //search for the first geometry property for (PropertyType pt : featureType.getProperties(true)) { if (AttributeConvention.isGeometryAttribute(pt) && pt instanceof AttributeType) { desc = (AttributeType) pt; } } } CoordinateReferenceSystem crs = null; final Class<?> geomType; final ShapeType shapeType; if(desc != null){ crs = FeatureExt.getCRS(desc); geomType = desc.getValueClass(); shapeType = ShapeType.findBestGeometryType(geomType); }else{ geomType = null; shapeType = ShapeType.NULL; } if(shapeType == ShapeType.UNDEFINED){ throw new DataStoreException("Cannot create a shapefile whose geometry type is "+ geomType); } try{ final StorageFile shpStoragefile = locker.getStorageFile(SHP); final StorageFile shxStoragefile = locker.getStorageFile(SHX); final StorageFile dbfStoragefile = locker.getStorageFile(DBF); final StorageFile prjStoragefile = locker.getStorageFile(PRJ); final StorageFile cpgStoragefile = locker.getStorageFile(CPG); try (FileChannel shpChannel = shpStoragefile.getWriteChannel(); FileChannel shxChannel = shxStoragefile.getWriteChannel()) { try (ShapefileWriter writer = new ShapefileWriter(shpChannel, shxChannel)) { // try to get the domain first final Envelope domain = org.geotoolkit.referencing.CRS.getEnvelope(crs); if (domain != null) { writer.writeHeaders(new JTSEnvelope2D(domain), shapeType, 0, 100); } else { // try to reproject the single overall envelope keeping poles out of the way final JTSEnvelope2D env = new JTSEnvelope2D(-179, 179, -89, 89, CommonCRS.WGS84.normalizedGeographic()); JTSEnvelope2D transformedBounds; if (crs != null) { try { transformedBounds = env.transform(crs, true); } catch (Throwable t) { if (getLogger().isLoggable(Level.WARNING)) { getLogger().log(Level.WARNING, t.getLocalizedMessage(), t); } transformedBounds = env; crs = null; } } else { transformedBounds = env; } writer.writeHeaders(transformedBounds, shapeType, 0, 100); } } finally { assert !shpChannel.isOpen(); assert !shxChannel.isOpen(); } } final DbaseFileHeader dbfheader = DbaseFileHeader.createDbaseHeader(schema); dbfheader.setNumRecords(0); try (WritableByteChannel dbfChannel = dbfStoragefile.getWriteChannel()) { dbfheader.writeHeader(dbfChannel); } if (crs != null) { // .prj files should have no carriage returns in them, this messes up // ESRI's ArcXXX software, so we'll be compatible final WKTFormat format = new WKTFormat(Locale.ENGLISH, null); format.setConvention(Convention.WKT1_COMMON_UNITS); format.setNameAuthority(Citations.ESRI); format.setIndentation(WKTFormat.SINGLE_LINE); final String s = format.format(crs); IOUtilities.writeString(s, prjStoragefile.getFile(), Charset.forName("ISO-8859-1")); } else { getLogger().warning("PRJ file not generated for null CoordinateReferenceSystem"); Path prjFile = prjStoragefile.getFile(); Files.deleteIfExists(prjFile); } //write dbf encoding .cpg CpgFiles.write(dbfCharset, cpgStoragefile.getFile()); locker.disposeReaderAndWriters(); locker.replaceStorageFiles(); }catch(IOException ex){ throw new DataStoreException(ex); } //force reading it again since the file type may be a little different name = null; schema = null; //we still preserve the original type name and attribute classes which may be more restricted final FeatureTypeBuilder ftb = new FeatureTypeBuilder(getFeatureType()); ftb.setName(typeName); final AttributeTypeBuilder gtb = (AttributeTypeBuilder)ftb.getProperty("the_geom"); if (Geometry.class.equals(gtb.getValueClass())) { gtb.setValueClass(shapeType.bestJTSClass()); } gtb.setName(desc.getName()); for (PropertyType pt : featureType.getProperties(true)) { if (pt instanceof AttributeType) { final AttributeType at = (AttributeType) pt; if(!Geometry.class.isAssignableFrom(at.getValueClass())) { try { ((AttributeTypeBuilder)ftb.getProperty(at.getName().tip().toString())) .setValueClass(at.getValueClass()) .setName(at.getName()); }catch(PropertyNotFoundException ex){} } } } schema = ftb.build(); name = schema.getName(); } /** * {@inheritDoc } */ @Override public void updateFeatureType(final FeatureType featureType) throws DataStoreException { throw new DataStoreException("Can not update shapefile schema."); } /** * {@inheritDoc } */ @Override public void deleteFeatureType(final String typeName) throws DataStoreException { throw new DataStoreException("Can not delete shapefile schema."); } //////////////////////////////////////////////////////////////////////////// // utils /////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////// /** * Obtain the FeatureType of the given name. ShapefileFeatureStore contains * only one FeatureType. * * @return The FeatureType that this featurestore contains. * @throws IOException If a type by the requested name is not present. */ private synchronized FeatureType buildSchema(final String namespace) throws DataStoreException { //add an identifier field final FeatureTypeBuilder builder = new FeatureTypeBuilder(); builder.setName(namespace,shpFiles.getTypeName()); builder.addAttribute(String.class).setName(AttributeConvention.IDENTIFIER_PROPERTY); //read all attributes/////////////////////////////////////////////////// final AccessManager locker = shpFiles.createLocker(); final ShapefileReader shp; final DbaseFileReader dbf; try { shp = locker.getSHPReader(true, useMemoryMappedBuffer, true, null); dbf = locker.getDBFReader(useMemoryMappedBuffer, dbfCharset); } catch (IOException ex) { throw new DataStoreException(ex); } CoordinateReferenceSystem crs = null; //read the projection final boolean qpjExists = shpFiles.exists(QPJ); final boolean prjExists = shpFiles.exists(PRJ); if (qpjExists || prjExists) { try (final ReadableByteChannel channel = qpjExists ? shpFiles.getReadChannel(QPJ) : shpFiles.getReadChannel(PRJ)) { crs = PrjFiles.read(channel, true); } catch (IOException ex) { getLogger().log(Level.WARNING, ex.getMessage(), ex); crs = null; } } final AttributeType geomDescriptor; try { //get the descriptor from shp geomDescriptor = shp.getHeader().createDescriptor(namespace, crs); builder.addAttribute(geomDescriptor).addRole(AttributeRole.DEFAULT_GEOMETRY); //get dbf attributes if exist if (dbf != null) { final DbaseFileHeader header = dbf.getHeader(); for(AttributeType at : header.createDescriptors(namespace)){ builder.addAttribute(at); } } } finally { //we have finish readring what we want, dispose everything locker.disposeReaderAndWriters(); } return builder.build(); } /** * Returns the attribute reader, allowing for a pure shapefile reader, or a * combined dbf/shp reader. * * @param readDbf - if true, the dbf fill will be opened and read * @param read3D - for shp reader, read 3d coordinate or not. * @param resample - for shp reader, decimate coordinates while reading * @return A reader for reading of data attributes. * @throws DataStoreException If we fails reading underlyig data. */ protected ShapefileAttributeReader getAttributesReader(final boolean readDbf, final boolean read3D, final double[] resample) throws DataStoreException { final AccessManager locker = shpFiles.createLocker(); final FeatureType schema = getFeatureType(); final AttributeType[] descs; if(readDbf){ descs = getAttributes(schema,false).toArray(new AttributeType[0]); }else{ getLogger().fine("The DBF file won't be opened since no attributes will be read from it"); descs = new AttributeType[]{FeatureExt.getDefaultGeometryAttribute(schema)}; } try { return new ShapefileAttributeReader(locker, descs, read3D, useMemoryMappedBuffer,resample, readDbf, dbfCharset,null); } catch (IOException ex) { throw new DataStoreException(ex); } } @Override public Path[] getDataFiles() throws DataStoreException { final List<Path> files = new ArrayList<>(); for (final ShpFileType type : ShpFileType.values()) { final Path f = shpFiles.getPath(type); if (f != null && Files.exists(f)) { files.add(f); } } return files.toArray(new Path[files.size()]); } protected List<AttributeType> getAttributes(FeatureType type, boolean includeIdentifier){ final Collection<? extends PropertyType> properties = type.getProperties(true); final List<AttributeType> atts = new ArrayList<>(); for(PropertyType p : properties){ if(!includeIdentifier && p.getName().equals(AttributeConvention.IDENTIFIER_PROPERTY)) continue; if(p instanceof AttributeType){ atts.add((AttributeType) p); } } return atts; } protected static List<AttributeType> getAttributes(Collection<PropertyType> properties){ final List<AttributeType> atts = new ArrayList<>(); for(PropertyType p : properties){ if(p.getName().equals(AttributeConvention.IDENTIFIER_PROPERTY)) continue; if(p instanceof AttributeType){ atts.add((AttributeType) p); } } return atts; } //////////////////////////////////////////////////////////////////////////// //Fallback on iterative reader and writer ////////////////////////////////// //////////////////////////////////////////////////////////////////////////// /** * {@inheritDoc } */ @Override public List<FeatureId> addFeatures(final String groupName, final Collection<? extends Feature> newFeatures, final Hints hints) throws DataStoreException { final List<FeatureId> ids = handleAddWithFeatureWriter(groupName, newFeatures, hints); return ids; } /** * {@inheritDoc } */ @Override public void updateFeatures(final String groupName, final Filter filter, final Map<String, ? extends Object> values) throws DataStoreException { handleUpdateWithFeatureWriter(groupName, filter, values); } /** * {@inheritDoc } */ @Override public void removeFeatures(final String groupName, final Filter filter) throws DataStoreException { handleRemoveWithFeatureWriter(groupName, filter); } @Override public void refreshMetaModel() { name = null; schema = null; } }