This is an automated email from the ASF dual-hosted git repository.

jsorel pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new 94bffce331 feat(Shapefile): support updateType, add a first writing 
support
94bffce331 is described below

commit 94bffce33101a36a15f7d5a6a3d8d618455a47a9
Author: jsorel <johann.so...@geomatys.com>
AuthorDate: Wed Nov 29 17:18:45 2023 +0100

    feat(Shapefile): support updateType, add a first writing support
---
 .../sis/storage/shapefile/ShapefileStore.java      | 433 +++++++++++++++++----
 .../shapefile/shp/ShapeGeometryEncoder.java        | 231 +++++++++--
 .../sis/storage/shapefile/shp/ShapeRecord.java     |   4 +
 .../sis/storage/shapefile/shp/ShapeWriter.java     |  14 +-
 .../sis/storage/shapefile/ShapefileStoreTest.java  | 122 +++++-
 .../test/module-info.java                          |   3 +
 6 files changed, 678 insertions(+), 129 deletions(-)

diff --git 
a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/ShapefileStore.java
 
b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/ShapefileStore.java
index 303b6d6f20..2d6fe5e827 100644
--- 
a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/ShapefileStore.java
+++ 
b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/ShapefileStore.java
@@ -24,9 +24,7 @@ import java.nio.channels.SeekableByteChannel;
 import java.nio.channels.WritableByteChannel;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.StandardOpenOption;
+import java.nio.file.*;
 import java.time.LocalDate;
 import java.util.AbstractMap;
 import java.util.ArrayList;
@@ -34,12 +32,14 @@ import java.util.Arrays;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map.Entry;
 import java.util.Optional;
 import java.util.OptionalLong;
 import java.util.Set;
 import java.util.Spliterator;
 import java.util.Spliterators;
+import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReadWriteLock;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
 import java.util.function.Consumer;
@@ -49,6 +49,8 @@ import java.util.logging.Level;
 import java.util.logging.Logger;
 import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
+
+import org.apache.sis.geometry.ImmutableEnvelope;
 import org.locationtech.jts.geom.Geometry;
 import org.locationtech.jts.geom.LineString;
 import org.locationtech.jts.geom.MultiLineString;
@@ -76,6 +78,9 @@ import org.apache.sis.geometry.wrapper.Geometries;
 import org.apache.sis.io.stream.ChannelDataInput;
 import org.apache.sis.io.stream.ChannelDataOutput;
 import org.apache.sis.io.stream.IOUtilities;
+import org.apache.sis.io.wkt.Convention;
+import org.apache.sis.io.wkt.WKTFormat;
+import org.apache.sis.metadata.iso.citation.Citations;
 import org.apache.sis.parameter.Parameters;
 import org.apache.sis.referencing.CRS;
 import org.apache.sis.referencing.CommonCRS;
@@ -587,116 +592,205 @@ public final class ShapefileStore extends DataStore 
implements WritableFeatureSe
         }
 
         @Override
-        public void updateType(FeatureType newType) throws DataStoreException {
-            if (true) throw new UnsupportedOperationException("Not supported 
yet.");
+        public synchronized void updateType(FeatureType newType) throws 
DataStoreException {
 
             if (!isDefaultView()) throw new DataStoreException("Resource not 
writable in current filter state");
             if (Files.exists(shpPath)) {
                 throw new DataStoreException("Update type is possible only 
when files do not exist. It can be used to create a new shapefile but not to 
update one.");
             }
 
-            final ShapeHeader shpHeader = new ShapeHeader();
-            final DBFHeader dbfHeader = new DBFHeader();
-            final Charset charset = userDefinedCharSet == null ? 
StandardCharsets.UTF_8 : userDefinedCharSet;
-            CoordinateReferenceSystem crs = 
CommonCRS.WGS84.normalizedGeographic();
-
-            for (PropertyType pt : newType.getProperties(true)) {
-                if (pt instanceof AttributeType) {
-                    final AttributeType at = (AttributeType) pt;
-                    final Class valueClass = at.getValueClass();
-
-                    Integer length = 
AttributeConvention.getMaximalLengthCharacteristic(newType, pt);
-                    if (length == 0) length = 255;
+            lock.writeLock().lock();
+            try {
+                final ShapeHeader shpHeader = new ShapeHeader();
+                shpHeader.bbox = new ImmutableEnvelope(new GeneralEnvelope(4));
+                final DBFHeader dbfHeader = new DBFHeader();
+                dbfHeader.fields = new DBFField[0];
+                final Charset charset = userDefinedCharSet == null ? 
StandardCharsets.UTF_8 : userDefinedCharSet;
+                CoordinateReferenceSystem crs = 
CommonCRS.WGS84.normalizedGeographic();
+
+                for (PropertyType pt : newType.getProperties(true)) {
+                    if (pt instanceof AttributeType) {
+                        final AttributeType at = (AttributeType) pt;
+                        final Class valueClass = at.getValueClass();
+                        final String attName = at.getName().tip().toString();
+
+                        Integer length = 
AttributeConvention.getMaximalLengthCharacteristic(newType, pt);
+                        if (length == null || length == 0) length = 255;
+
+                        if (Geometry.class.isAssignableFrom(valueClass)) {
+                            if (shpHeader.shapeType != 0) {
+                                throw new DataStoreException("Shapefile format 
can only contain one geometry");
+                            }
+                            if (Point.class.isAssignableFrom(valueClass)) 
shpHeader.shapeType = ShapeType.VALUE_POINT;
+                            else if 
(MultiPoint.class.isAssignableFrom(valueClass))
+                                shpHeader.shapeType = 
ShapeType.VALUE_MULTIPOINT;
+                            else if 
(LineString.class.isAssignableFrom(valueClass) || 
MultiLineString.class.isAssignableFrom(valueClass))
+                                shpHeader.shapeType = ShapeType.VALUE_POLYLINE;
+                            else if 
(Polygon.class.isAssignableFrom(valueClass) || 
MultiPolygon.class.isAssignableFrom(valueClass))
+                                shpHeader.shapeType = ShapeType.VALUE_POLYGON;
+                            else throw new DataStoreException("Unsupported 
geometry type " + valueClass);
+
+                            Object cdt = 
at.characteristics().get(AttributeConvention.CRS);
+                            if (cdt instanceof AttributeType) {
+                                Object defaultValue = ((AttributeType) 
cdt).getDefaultValue();
+                                if (defaultValue instanceof 
CoordinateReferenceSystem) {
+                                    crs = (CoordinateReferenceSystem) 
defaultValue;
+                                }
+                            }
 
-                    if (Geometry.class.isAssignableFrom(valueClass)) {
-                        if (shpHeader.shapeType != 0) {
-                            throw new DataStoreException("Shapefile format can 
only contain one geometry");
-                        }
-                        if (Point.class.isAssignableFrom(valueClass)) 
shpHeader.shapeType = ShapeType.VALUE_POINT;
-                        else if 
(MultiPoint.class.isAssignableFrom(valueClass)) shpHeader.shapeType = 
ShapeType.VALUE_MULTIPOINT;
-                        else if (LineString.class.isAssignableFrom(valueClass) 
|| MultiLineString.class.isAssignableFrom(valueClass)) shpHeader.shapeType = 
ShapeType.VALUE_POLYLINE;
-                        else if (Polygon.class.isAssignableFrom(valueClass) || 
MultiPolygon.class.isAssignableFrom(valueClass)) shpHeader.shapeType = 
ShapeType.VALUE_POLYGON;
-                        else throw new DataStoreException("Unsupported 
geometry type " + valueClass);
-
-                        Object cdt = 
at.characteristics().get(AttributeConvention.CRS_CHARACTERISTIC);
-                        if (cdt instanceof CoordinateReferenceSystem) {
-                            crs = (CoordinateReferenceSystem) cdt;
+                        } else if (String.class.isAssignableFrom(valueClass)) {
+                            dbfHeader.fields = 
ArraysExt.append(dbfHeader.fields, new DBFField(attName, (char) 
DBFField.TYPE_CHAR, 0, length, 0, charset));
+                        } else if (Byte.class.isAssignableFrom(valueClass)) {
+                            dbfHeader.fields = 
ArraysExt.append(dbfHeader.fields, new DBFField(attName, (char) 
DBFField.TYPE_NUMBER, 0, 4, 0, null));
+                        } else if (Short.class.isAssignableFrom(valueClass)) {
+                            dbfHeader.fields = 
ArraysExt.append(dbfHeader.fields, new DBFField(attName, (char) 
DBFField.TYPE_NUMBER, 0, 6, 0, null));
+                        } else if (Integer.class.isAssignableFrom(valueClass)) 
{
+                            dbfHeader.fields = 
ArraysExt.append(dbfHeader.fields, new DBFField(attName, (char) 
DBFField.TYPE_NUMBER, 0, 9, 0, null));
+                        } else if (Long.class.isAssignableFrom(valueClass)) {
+                            dbfHeader.fields = 
ArraysExt.append(dbfHeader.fields, new DBFField(attName, (char) 
DBFField.TYPE_NUMBER, 0, 19, 0, null));
+                        } else if (Float.class.isAssignableFrom(valueClass)) {
+                            dbfHeader.fields = 
ArraysExt.append(dbfHeader.fields, new DBFField(attName, (char) 
DBFField.TYPE_NUMBER, 0, 11, 8, null));
+                        } else if (Double.class.isAssignableFrom(valueClass)) {
+                            dbfHeader.fields = 
ArraysExt.append(dbfHeader.fields, new DBFField(attName, (char) 
DBFField.TYPE_NUMBER, 0, 33, 30, null));
+                        } else if 
(LocalDate.class.isAssignableFrom(valueClass)) {
+                            dbfHeader.fields = 
ArraysExt.append(dbfHeader.fields, new DBFField(attName, (char) 
DBFField.TYPE_DATE, 0, 20, 0, null));
+                        } else {
+                            LOGGER.log(Level.WARNING, "Shapefile writing, 
field {0} is not supported", pt.getName());
                         }
-
-                    } else if (String.class.isAssignableFrom(valueClass)) {
-                        dbfHeader.fields = ArraysExt.append(dbfHeader.fields,  
new DBFField(idField, (char)DBFField.TYPE_CHAR, 0, length, 0, charset));
-                    } else if (Byte.class.isAssignableFrom(valueClass)) {
-                        dbfHeader.fields = ArraysExt.append(dbfHeader.fields, 
new DBFField(idField, (char)DBFField.TYPE_NUMBER, 0, 4, 0, null));
-                    } else if (Short.class.isAssignableFrom(valueClass)) {
-                        dbfHeader.fields = ArraysExt.append(dbfHeader.fields, 
new DBFField(idField, (char)DBFField.TYPE_NUMBER, 0, 6, 0, null));
-                    } else if (Integer.class.isAssignableFrom(valueClass)) {
-                        dbfHeader.fields = ArraysExt.append(dbfHeader.fields, 
new DBFField(idField, (char)DBFField.TYPE_NUMBER, 0, 9, 0, null));
-                    } else if (Long.class.isAssignableFrom(valueClass)) {
-                        dbfHeader.fields = ArraysExt.append(dbfHeader.fields, 
new DBFField(idField, (char)DBFField.TYPE_NUMBER, 0, 19, 0, null));
-                    } else if (Float.class.isAssignableFrom(valueClass)) {
-                        dbfHeader.fields = ArraysExt.append(dbfHeader.fields, 
new DBFField(idField, (char)DBFField.TYPE_NUMBER, 0, 33, 0, null));
-                    } else if (Double.class.isAssignableFrom(valueClass)) {
-
-                    } else if (LocalDate.class.isAssignableFrom(valueClass)) {
-
                     } else {
                         LOGGER.log(Level.WARNING, "Shapefile writing, field 
{0} is not supported", pt.getName());
                     }
-                } else {
-                    LOGGER.log(Level.WARNING, "Shapefile writing, field {0} is 
not supported", pt.getName());
                 }
-            }
 
-            //write shapefile
-            try (ShapeWriter writer = new 
ShapeWriter(ShpFiles.openWriteChannel(files.shpFile))) {
-                writer.write(shpHeader);
-            } catch (IOException ex){
-                throw new DataStoreException("Failed to create shapefile 
(shp).", ex);
-            }
-
-            //write shx
-            try (IndexWriter writer = new 
IndexWriter(ShpFiles.openWriteChannel(files.shxFile))) {
-                writer.write(shpHeader);
-            } catch (IOException ex){
-                throw new DataStoreException("Failed to create shapefile 
(shx).", ex);
-            }
+                //write shapefile
+                try (ShapeWriter writer = new 
ShapeWriter(ShpFiles.openWriteChannel(files.shpFile, StandardOpenOption.CREATE, 
StandardOpenOption.TRUNCATE_EXISTING))) {
+                    writer.write(shpHeader);
+                } catch (IOException ex) {
+                    throw new DataStoreException("Failed to create shapefile 
(shp).", ex);
+                }
 
-            //write dbf
-            try (DBFWriter writer = new 
DBFWriter(ShpFiles.openWriteChannel(files.dbfFile))) {
-                writer.write(dbfHeader);
-            } catch (IOException ex){
-                throw new DataStoreException("Failed to create shapefile 
(dbf).", ex);
-            }
+                //write shx
+                try (IndexWriter writer = new 
IndexWriter(ShpFiles.openWriteChannel(files.getShx(true), 
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING))) {
+                    writer.write(shpHeader);
+                } catch (IOException ex) {
+                    throw new DataStoreException("Failed to create shapefile 
(shx).", ex);
+                }
 
-            //write cpg
-            try {
-                CpgFiles.write(charset, files.cpgFile);
-            } catch (IOException ex) {
-                throw new DataStoreException("Failed to create shapefile 
(cpg).", ex);
-            }
+                //write dbf
+                try (DBFWriter writer = new 
DBFWriter(ShpFiles.openWriteChannel(files.getDbf(true), 
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING))) {
+                    writer.write(dbfHeader);
+                } catch (IOException ex) {
+                    throw new DataStoreException("Failed to create shapefile 
(dbf).", ex);
+                }
 
-            //write prj
-            //todo
+                //write cpg
+                try {
+                    CpgFiles.write(charset, files.getCpg(true));
+                } catch (IOException ex) {
+                    throw new DataStoreException("Failed to create shapefile 
(cpg).", ex);
+                }
 
+                //write prj
+                try {
+                    final WKTFormat format = new WKTFormat(Locale.ENGLISH, 
null);
+                    format.setConvention(Convention.WKT1_COMMON_UNITS);
+                    format.setNameAuthority(Citations.ESRI);
+                    format.setIndentation(WKTFormat.SINGLE_LINE);
+                    Files.writeString(files.getPrj(true), format.format(crs), 
StandardCharsets.ISO_8859_1, StandardOpenOption.CREATE, 
StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE);
+                } catch (IOException ex) {
+                    throw new DataStoreException("Failed to create shapefile 
(prj).", ex);
+                }
 
+                //update file list
+                files.scan();
+            } finally {
+                lock.writeLock().unlock();
+            }
         }
 
         @Override
         public void add(Iterator<? extends Feature> features) throws 
DataStoreException {
             if (!isDefaultView()) throw new DataStoreException("Resource not 
writable in current filter state");
-            throw new UnsupportedOperationException("Not supported yet.");
+            if (!Files.exists(shpPath)) throw new 
DataStoreException("FeatureType do not exist, use updateType before modifying 
features.");
+
+            final Writer writer = new Writer(charset);
+            try {
+                //write existing features
+                try (Stream<Feature> stream = features(false)) {
+                    Iterator<Feature> iterator = stream.iterator();
+                    while (iterator.hasNext()) {
+                        writer.write(iterator.next());
+                    }
+                }
+
+                //write new features
+                while (features.hasNext()) {
+                    writer.write(features.next());
+                }
+
+                writer.finish(true);
+            } catch (IOException ex) {
+                try {
+                    writer.finish(false);
+                } catch (IOException e) {
+                    ex.addSuppressed(e);
+                }
+                throw  new DataStoreException("Writing failed", ex);
+            }
         }
 
         @Override
         public void removeIf(Predicate<? super Feature> filter) throws 
DataStoreException {
             if (!isDefaultView()) throw new DataStoreException("Resource not 
writable in current filter state");
-            throw new UnsupportedOperationException("Not supported yet.");
+            if (!Files.exists(shpPath)) throw new 
DataStoreException("FeatureType do not exist, use updateType before modifying 
features.");
+
+            final Writer writer = new Writer(charset);
+            try {
+                //write existing features not matching filter
+                try (Stream<Feature> stream = features(false)) {
+                    Iterator<Feature> iterator = 
stream.filter(filter.negate()).iterator();
+                    while (iterator.hasNext()) {
+                        writer.write(iterator.next());
+                    }
+                }
+                writer.finish(true);
+            } catch (IOException ex) {
+                try {
+                    writer.finish(false);
+                } catch (IOException e) {
+                    ex.addSuppressed(e);
+                }
+                throw  new DataStoreException("Writing failed", ex);
+            }
         }
 
         @Override
         public void replaceIf(Predicate<? super Feature> filter, 
UnaryOperator<Feature> updater) throws DataStoreException {
             if (!isDefaultView()) throw new DataStoreException("Resource not 
writable in current filter state");
-            throw new UnsupportedOperationException("Not supported yet.");
+            if (!Files.exists(shpPath)) throw new 
DataStoreException("FeatureType do not exist, use updateType before modifying 
features.");
+
+            final Writer writer = new Writer(charset);
+            try {
+                //write existing features applying modifications
+                try (Stream<Feature> stream = features(false)) {
+                    Iterator<Feature> iterator = stream.iterator();
+                    while (iterator.hasNext()) {
+                        Feature feature = iterator.next();
+                        if (filter.test(feature)) {
+                            feature = updater.apply(feature);
+                        }
+                        if (feature != null) writer.write(feature);
+                    }
+                }
+                writer.finish(true);
+            } catch (IOException ex) {
+                try {
+                    writer.finish(false);
+                } catch (IOException e) {
+                    ex.addSuppressed(e);
+                }
+                throw  new DataStoreException("Writing failed", ex);
+            }
         }
 
         @Override
@@ -707,7 +801,7 @@ public final class ShapefileStore extends DataStore 
implements WritableFeatureSe
             final Path dbf = files.getDbf(false);
             final Path prj = files.getPrj(false);
             final Path cpg = files.getCpg(false);
-            if (shp != null && Files.exists(shp)) paths.add(shp);
+            if (               Files.exists(shp)) paths.add(shp);
             if (shx != null && Files.exists(shx)) paths.add(shx);
             if (dbf != null && Files.exists(dbf)) paths.add(dbf);
             if (prj != null && Files.exists(prj)) paths.add(prj);
@@ -734,6 +828,14 @@ public final class ShapefileStore extends DataStore 
implements WritableFeatureSe
             final String fileName = shpFile.getFileName().toString();
             baseUpper = 
Character.isUpperCase(fileName.codePointAt(fileName.length()-1));
             this.baseName = IOUtilities.filenameWithoutExtension(fileName);
+            scan();
+        }
+
+        /**
+         * Search related files.
+         * Should be called after data have been modified.
+         */
+        private void scan() {
             shxFile = findSibling("shx");
             dbfFile = findSibling("dbf");
             prjFile = findSibling("prj");
@@ -784,6 +886,45 @@ public final class ShapefileStore extends DataStore 
implements WritableFeatureSe
             return cpgFile;
         }
 
+        /**
+         * Create a set of temporary files for edition.
+         */
+        private ShpFiles createTempFiles() throws IOException{
+            final Path tmp = Files.createTempFile("tmp", ".shp");
+            Files.delete(tmp);
+            return new ShpFiles(tmp);
+        }
+
+        /**
+         * Delete files permanently.
+         */
+        private void deleteFiles() throws IOException{
+            Files.deleteIfExists(shpFile);
+            if (shxFile != null) Files.deleteIfExists(shxFile);
+            if (dbfFile != null) Files.deleteIfExists(dbfFile);
+            if (cpgFile != null) Files.deleteIfExists(cpgFile);
+            if (prjFile != null) Files.deleteIfExists(prjFile);
+        }
+
+        /**
+         * Override target files by current ones.
+         */
+        private void replace(ShpFiles toReplace) throws IOException{
+            replace(shpFile, toReplace.shpFile);
+            replace(shxFile, toReplace.getShx(true));
+            replace(dbfFile, toReplace.getDbf(true));
+            replace(cpgFile, toReplace.getCpg(true));
+            replace(prjFile, toReplace.getPrj(true));
+        }
+
+        private static void replace(Path current, Path toReplace) throws 
IOException{
+            if (current == null) {
+                Files.deleteIfExists(toReplace);
+            } else {
+                Files.move(current, toReplace, 
StandardCopyOption.REPLACE_EXISTING);
+            }
+        }
+
         private Path findSibling(String extension) {
             Path candidate = shpFile.getParent().resolve(baseName + "." + 
extension);
             if (java.nio.file.Files.isRegularFile(candidate)) return candidate;
@@ -797,9 +938,14 @@ public final class ShapefileStore extends DataStore 
implements WritableFeatureSe
             return new ChannelDataInput(path.getFileName().toString(), 
channel, ByteBuffer.allocate(8192), false);
         }
 
-        private static ChannelDataOutput openWriteChannel(Path path) throws 
IOException, IllegalArgumentException, DataStoreException {
-            final WritableByteChannel wbc = Files.newByteChannel(path, 
StandardOpenOption.WRITE);
-            return new ChannelDataOutput(path.getFileName().toString(), wbc, 
ByteBuffer.allocate(8000));
+        private static ChannelDataOutput openWriteChannel(Path path, 
OpenOption ... options) throws IOException, IllegalArgumentException, 
DataStoreException {
+            final WritableByteChannel wbc;
+            if (options != null && options.length > 0) {
+                wbc = Files.newByteChannel(path, ArraysExt.append(options, 
StandardOpenOption.WRITE));
+            } else {
+                wbc = Files.newByteChannel(path, StandardOpenOption.WRITE);
+            }
+            return new ChannelDataOutput(path.getFileName().toString(), wbc, 
ByteBuffer.allocate(8192));
         }
     }
 
@@ -896,5 +1042,124 @@ public final class ShapefileStore extends DataStore 
implements WritableFeatureSe
         return env;
     }
 
+    private class Writer {
+
+        private final ShpFiles tempFiles;
+        private final ShapeWriter shpWriter;
+        private final DBFWriter dbfWriter;
+        private final IndexWriter shxWriter;
+        private final ShapeHeader shpHeader;
+        private final DBFHeader dbfHeader;
+        private String defaultGeomName = null;
+        private int inc = 0;
+
+        private Writer(Charset charset) throws DataStoreException{
+            try {
+                tempFiles = files.createTempFiles();
+            } catch (IOException ex) {
+                throw new DataStoreException("Failed to create temp files", 
ex);
+            }
+
+            try {
+                //get original headers and information
+                try (ShapeReader reader = new 
ShapeReader(ShpFiles.openReadChannel(files.shpFile), null)) {
+                    shpHeader = new ShapeHeader(reader.getHeader());
+                }
+                try (DBFReader reader = new 
DBFReader(ShpFiles.openReadChannel(files.dbfFile), charset, null)) {
+                    dbfHeader = new DBFHeader(reader.getHeader());
+                }
+
+                //unchanged files
+                ShpFiles.replace(files.cpgFile, tempFiles.getCpg(true));
+                ShpFiles.replace(files.prjFile, tempFiles.getPrj(true));
+
+                //start new files
+
+                //write shapefile
+                shpWriter = new 
ShapeWriter(ShpFiles.openWriteChannel(tempFiles.shpFile, 
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING));
+                dbfWriter = new 
DBFWriter(ShpFiles.openWriteChannel(tempFiles.getDbf(true), 
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING));
+                shxWriter = new 
IndexWriter(ShpFiles.openWriteChannel(tempFiles.getShx(true), 
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING));
+                shpWriter.write(shpHeader);
+                shxWriter.write(shpHeader);
+                dbfWriter.write(dbfHeader);
+            } catch (IOException ex) {
+                try {
+                    tempFiles.deleteFiles();
+                } catch (IOException e) {
+                    ex.addSuppressed(e);
+                }
+                throw new DataStoreException("Failed to create temp files", 
ex);
+            }
+
+        }
+
+        private void write(Feature feature) throws IOException {
+            inc++; //number starts at 1
+            final ShapeRecord shpRecord = new ShapeRecord();
+            final DBFRecord dbfRecord = new DBFRecord();
+            final long recordStartPosition = shpWriter.getSteamPosition();
+
+            if (defaultGeomName == null) {
+                //search for the geometry name
+                for (PropertyType pt : feature.getType().getProperties(true)) {
+                    if (pt instanceof AttributeType) {
+                        final AttributeType at = (AttributeType) pt;
+                        final String attName = at.getName().toString();
+                        if 
(Geometry.class.isAssignableFrom(at.getValueClass())) {
+                            defaultGeomName = attName;
+                        }
+                    }
+                }
+                if (defaultGeomName == null) {
+                    throw new IOException("Failed to find a geometry attribute 
in given features");
+                }
+            }
+
+            //write geometry
+            Object value = feature.getPropertyValue(defaultGeomName);
+            if (value instanceof Geometry) {
+                shpRecord.geometry = (Geometry) value;
+                shpRecord.recordNumber = inc;
+            } else {
+                throw new IOException("Feature geometry property is not a 
geometry");
+            }
+            shpWriter.write(shpRecord);
+            final long recordEndPosition = shpWriter.getSteamPosition();
+
+            //write index
+            shxWriter.write(Math.toIntExact(recordStartPosition), 
Math.toIntExact(recordEndPosition - recordStartPosition));
+
+            //copy dbf fields
+            dbfRecord.fields = new Object[dbfHeader.fields.length];
+            for (int i = 0; i < dbfRecord.fields.length; i++) {
+                dbfRecord.fields[i] = 
feature.getPropertyValue(dbfHeader.fields[i].fieldName);
+            }
+            dbfWriter.write(dbfRecord);
+        }
+
+        /**
+         * Close file writers and replace original files if true.
+         */
+        private void finish(boolean replaceOriginals) throws IOException {
+            try {
+                shpWriter.close();
+                dbfWriter.close();
+                shxWriter.close();
+                tempFiles.scan();
+                if (replaceOriginals) {
+                    lock.writeLock().lock();
+                    try {
+                        //swap files
+                        tempFiles.replace(files);
+                    } finally {
+                        lock.writeLock().unlock();
+                    }
+                }
+            } finally {
+                tempFiles.deleteFiles();
+            }
+        }
+    }
+
 
 }
diff --git 
a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shp/ShapeGeometryEncoder.java
 
b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shp/ShapeGeometryEncoder.java
index f3ab943720..c069f1bd39 100644
--- 
a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shp/ShapeGeometryEncoder.java
+++ 
b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shp/ShapeGeometryEncoder.java
@@ -22,6 +22,9 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
+
+import org.apache.sis.geometry.GeneralDirectPosition;
+import org.apache.sis.util.ArraysExt;
 import org.locationtech.jts.geom.*;
 import org.locationtech.jts.geom.impl.PackedCoordinateSequence;
 import org.locationtech.jts.algorithm.Orientation;
@@ -119,6 +122,7 @@ public abstract class ShapeGeometryEncoder<T extends 
Geometry> {
 
     /**
      * Decode geometry and store it in ShapeRecord.
+     * This method creates and fill the record bbox if it is null.
      *
      * @param channel to read from
      * @param record to read into
@@ -142,6 +146,8 @@ public abstract class ShapeGeometryEncoder<T extends 
Geometry> {
      */
     public abstract int getEncodedLength(Geometry geom);
 
+    public abstract GeneralEnvelope getBoundingBox(Geometry geom);
+
     /**
      * Read 2D Bounding box from channel.
      *
@@ -174,10 +180,11 @@ public abstract class ShapeGeometryEncoder<T extends 
Geometry> {
      * @param shape to read from
      */
     protected void writeBBox2D(ChannelDataOutput channel, ShapeRecord shape) 
throws IOException {
-        channel.writeDouble(shape.bbox.getMinimum(0));
-        channel.writeDouble(shape.bbox.getMinimum(1));
-        channel.writeDouble(shape.bbox.getMaximum(0));
-        channel.writeDouble(shape.bbox.getMaximum(1));
+        final Envelope env2d = shape.geometry.getEnvelopeInternal();
+        channel.writeDouble(env2d.getMinX());
+        channel.writeDouble(env2d.getMinY());
+        channel.writeDouble(env2d.getMaxX());
+        channel.writeDouble(env2d.getMaxY());
     }
 
     /**
@@ -266,19 +273,28 @@ public abstract class ShapeGeometryEncoder<T extends 
Geometry> {
         }
 
         //Z and M
-        if (nbOrdinates >= 3)  writeLineOrdinates(channel, shape, lines, 2);
-        if (nbOrdinates == 4)  writeLineOrdinates(channel, shape, lines, 3);
+        if (nbOrdinates >= 3)  writeLineOrdinates(channel, shape, lines, 2, 
nbPts);
+        if (nbOrdinates == 4)  writeLineOrdinates(channel, shape, lines, 3, 
nbPts);
     }
 
-    protected void writeLineOrdinates(ChannelDataOutput channel, ShapeRecord 
shape,List<LineString> lines, int ordinateIndex) throws IOException {
-        channel.writeDouble(shape.bbox.getMinimum(ordinateIndex));
-        channel.writeDouble(shape.bbox.getMaximum(ordinateIndex));
+    protected void writeLineOrdinates(ChannelDataOutput channel, ShapeRecord 
shape,List<LineString> lines, int ordinateIndex, int nbPts) throws IOException {
+
+        final double[] values = new double[nbPts];
+        double minK = Double.MAX_VALUE;
+        double maxK = -Double.MAX_VALUE;
+        int i = 0;
         for (LineString line : lines) {
             final CoordinateSequence cs = line.getCoordinateSequence();
             for (int k = 0, kn =cs.size(); k < kn; k++) {
-                channel.writeDouble(cs.getOrdinate(k, ordinateIndex));
+                values[i] = cs.getOrdinate(k, ordinateIndex);
+                minK = Double.min(minK, values[i]);
+                maxK = Double.max(maxK, values[i]);
+                i++;
             }
         }
+        channel.writeDouble(minK);
+        channel.writeDouble(maxK);
+        channel.writeDoubles(values);
     }
 
     protected List<LineString> extractRings(Geometry geom) {
@@ -368,6 +384,51 @@ public abstract class ShapeGeometryEncoder<T extends 
Geometry> {
         }
     }
 
+    protected GeneralEnvelope getLinesBoundingBox(Geometry geom) {
+        final List<LineString> lines = extractRings(geom);
+
+        int nbOrdinate = 0;
+        GeneralEnvelope env = null;
+
+        for (int k = 0, kn = lines.size(); k < kn; k++) {
+            final LineString line = lines.get(k);
+            final CoordinateSequence cs = line.getCoordinateSequence();
+
+            if (nbOrdinate == 0) {
+                nbOrdinate = cs.getDimension();
+            }
+
+            for (int i = 0, n = cs.size(); i < n; i++) {
+                if (env == null) {
+                    env = new GeneralEnvelope(nbOrdinate);
+                    switch (nbOrdinate) {
+                        case 4 :
+                            double m = cs.getOrdinate(i, 3);
+                            env.setRange(3, m, m);
+                        case 3 :
+                            double z = cs.getOrdinate(i, 2);
+                            env.setRange(2, z, z);
+                        case 2 :
+                            double y = cs.getOrdinate(i, 1);
+                            env.setRange(1, y, y);
+                            double x = cs.getOrdinate(i, 0);
+                            env.setRange(0, x, x);
+                    }
+                } else {
+                    switch (nbOrdinate) {
+                        case 4 :
+                            env.add(new 
GeneralDirectPosition(cs.getOrdinate(i,0), cs.getOrdinate(i,1), 
cs.getOrdinate(i,2), cs.getOrdinate(i,3))); break;
+                        case 3 :
+                            env.add(new 
GeneralDirectPosition(cs.getOrdinate(i,0), cs.getOrdinate(i,1), 
cs.getOrdinate(i,2))); break;
+                        case 2 :
+                            env.add(new 
GeneralDirectPosition(cs.getOrdinate(i,0), cs.getOrdinate(i,1))); break;
+                    }
+                }
+            }
+        }
+        return env;
+    }
+
     private static class Null extends ShapeGeometryEncoder<Geometry> {
 
         private static final Null INSTANCE = new Null();
@@ -390,6 +451,10 @@ public abstract class ShapeGeometryEncoder<T extends 
Geometry> {
         public void encode(ChannelDataOutput channel, ShapeRecord shape) 
throws IOException {
         }
 
+        @Override
+        public GeneralEnvelope getBoundingBox(Geometry geom) {
+            return new GeneralEnvelope(0);
+        }
     }
 
     private static class PointXY extends ShapeGeometryEncoder<Point> {
@@ -417,14 +482,22 @@ public abstract class ShapeGeometryEncoder<T extends 
Geometry> {
         public void encode(ChannelDataOutput channel, ShapeRecord shape) 
throws IOException {
             final Point pt = (Point) shape.geometry;
             final Coordinate coord = pt.getCoordinate();
-            channel.writeDouble(coord.getX());
-            channel.writeDouble(coord.getY());
+            final double[] xy = new double[]{coord.getX(), coord.getY()};
+            channel.writeDoubles(xy);
         }
 
         @Override
         public int getEncodedLength(Geometry geom) {
             return 2*8; //2 ordinates
         }
+
+        @Override
+        public GeneralEnvelope getBoundingBox(Geometry geom) {
+            final Point pt = (Point) geom;
+            final Coordinate coord = pt.getCoordinate();
+            final double[] xy = new double[]{coord.getX(), coord.getY()};
+            return new GeneralEnvelope(xy, xy);
+        }
     }
 
     private static class PointXYM extends ShapeGeometryEncoder<Point> {
@@ -453,15 +526,25 @@ public abstract class ShapeGeometryEncoder<T extends 
Geometry> {
         public void encode(ChannelDataOutput channel, ShapeRecord shape) 
throws IOException {
             final Point pt = (Point) shape.geometry;
             final Coordinate coord = pt.getCoordinate();
-            channel.writeDouble(coord.getX());
-            channel.writeDouble(coord.getY());
-            channel.writeDouble(coord.getM());
+            final double[] xym = new double[]{coord.getX(), coord.getY(), 
coord.getM()};
+            channel.writeDoubles(xym);
+            if (shape.bbox == null) {
+                shape.bbox = new GeneralEnvelope(xym,xym);
+            }
         }
 
         @Override
         public int getEncodedLength(Geometry geom) {
             return 3*8; //3 ordinates
         }
+
+        @Override
+        public GeneralEnvelope getBoundingBox(Geometry geom) {
+            final Point pt = (Point) geom;
+            final Coordinate coord = pt.getCoordinate();
+            final double[] xym = new double[]{coord.getX(), coord.getY(), 
coord.getM()};
+            return new GeneralEnvelope(xym, xym);
+        }
     }
 
     private static class PointXYZM extends ShapeGeometryEncoder<Point> {
@@ -493,16 +576,22 @@ public abstract class ShapeGeometryEncoder<T extends 
Geometry> {
         public void encode(ChannelDataOutput channel, ShapeRecord shape) 
throws IOException {
             final Point pt = (Point) shape.geometry;
             final Coordinate coord = pt.getCoordinate();
-            channel.writeDouble(coord.getX());
-            channel.writeDouble(coord.getY());
-            channel.writeDouble(coord.getZ());
-            channel.writeDouble(coord.getM());
+            final double[] xyzm = new double[]{coord.getX(), coord.getY(), 
coord.getZ(), coord.getM()};
+            channel.writeDoubles(xyzm);
         }
 
         @Override
         public int getEncodedLength(Geometry geom) {
             return 4*8; //4 ordinates
         }
+
+        @Override
+        public GeneralEnvelope getBoundingBox(Geometry geom) {
+            final Point pt = (Point) geom;
+            final Coordinate coord = pt.getCoordinate();
+            final double[] xyzm = new double[]{coord.getX(), coord.getY(), 
coord.getZ(), coord.getM()};
+            return new GeneralEnvelope(xyzm, xyzm);
+        }
     }
 
     private static class MultiPointXY extends ShapeGeometryEncoder<MultiPoint> 
{
@@ -529,8 +618,8 @@ public abstract class ShapeGeometryEncoder<T extends 
Geometry> {
             channel.writeInt(nbPts);
             for (int i = 0; i < nbPts; i++) {
                 final Point pt = (Point) geometry.getGeometryN(i);
-                channel.writeDouble(pt.getX());
-                channel.writeDouble(pt.getY());
+                final double[] xy = new double[]{pt.getX(), pt.getY()};
+                channel.writeDoubles(xy);
             }
         }
 
@@ -540,6 +629,20 @@ public abstract class ShapeGeometryEncoder<T extends 
Geometry> {
                  + 4 //nbPts
                  + ((MultiPoint) geom).getNumGeometries() * 2 * 8;
         }
+
+        @Override
+        public GeneralEnvelope getBoundingBox(Geometry geom) {
+            final GeneralEnvelope env = new GeneralEnvelope(2);
+            final MultiPoint pts = (MultiPoint) geom;
+            for (int i = 0, n = pts.getNumGeometries(); i < n; i++) {
+                final Point pt = (Point)pts.getGeometryN(i);
+                final Coordinate coord = pt.getCoordinate();
+                final double[] xy = new double[]{coord.getX(), coord.getY()};
+                if (i == 0) env.setEnvelope(xy[0], xy[1], xy[0], xy[1]);
+                else env.add(new GeneralDirectPosition(xy));
+            }
+            return env;
+        }
     }
 
     private static class MultiPointXYM extends 
ShapeGeometryEncoder<MultiPoint> {
@@ -575,15 +678,21 @@ public abstract class ShapeGeometryEncoder<T extends 
Geometry> {
             channel.writeInt(nbPts);
             for (int i = 0; i < nbPts; i++) {
                 final Point pt = (Point) geometry.getGeometryN(i);
-                channel.writeDouble(pt.getX());
-                channel.writeDouble(pt.getY());
+                final double[] xy = new double[]{pt.getX(), pt.getY()};
+                channel.writeDoubles(xy);
             }
-            channel.writeDouble(shape.bbox.getMinimum(2));
-            channel.writeDouble(shape.bbox.getMaximum(2));
+            final double[] m = new double[nbPts];
+            double minM = Double.MAX_VALUE;
+            double maxM = -Double.MAX_VALUE;
             for (int i = 0; i < nbPts; i++) {
                 final Point pt = (Point) geometry.getGeometryN(i);
-                channel.writeDouble(pt.getCoordinate().getM());
+                m[i] = pt.getCoordinate().getM();
+                minM = Double.min(minM, m[i]);
+                maxM = Double.max(maxM, m[i]);
             }
+            channel.writeDouble(minM);
+            channel.writeDouble(maxM);
+            channel.writeDoubles(m);
         }
 
         @Override
@@ -592,6 +701,20 @@ public abstract class ShapeGeometryEncoder<T extends 
Geometry> {
                  + 4 //nbPts
                  + ((MultiPoint) geom).getNumGeometries() * 3 * 8;
         }
+
+        @Override
+        public GeneralEnvelope getBoundingBox(Geometry geom) {
+            final GeneralEnvelope env = new GeneralEnvelope(3);
+            final MultiPoint pts = (MultiPoint) geom;
+            for (int i = 0, n = pts.getNumGeometries(); i < n; i++) {
+                final Point pt = (Point)pts.getGeometryN(i);
+                final Coordinate coord = pt.getCoordinate();
+                final double[] xym = new double[]{coord.getX(), coord.getY(), 
coord.getM()};
+                if (i == 0) env.setEnvelope(xym[0], xym[1], xym[2], xym[0], 
xym[1], xym[2]);
+                else env.add(new GeneralDirectPosition(xym));
+            }
+            return env;
+        }
     }
 
     private static class MultiPointXYZM extends 
ShapeGeometryEncoder<MultiPoint> {
@@ -634,18 +757,33 @@ public abstract class ShapeGeometryEncoder<T extends 
Geometry> {
                 channel.writeDouble(pt.getX());
                 channel.writeDouble(pt.getY());
             }
-            channel.writeDouble(shape.bbox.getMinimum(2));
-            channel.writeDouble(shape.bbox.getMaximum(2));
+
+            final double[] z = new double[nbPts];
+            double minZ = Double.MAX_VALUE;
+            double maxZ = -Double.MAX_VALUE;
             for (int i = 0; i < nbPts; i++) {
                 final Point pt = (Point) geometry.getGeometryN(i);
-                channel.writeDouble(pt.getCoordinate().getZ());
+                z[i] = pt.getCoordinate().getZ();
+                minZ = Double.min(minZ, z[i]);
+                maxZ = Double.max(maxZ, z[i]);
             }
-            channel.writeDouble(shape.bbox.getMinimum(3));
-            channel.writeDouble(shape.bbox.getMaximum(3));
+            channel.writeDouble(minZ);
+            channel.writeDouble(maxZ);
+            channel.writeDoubles(z);
+
+
+            final double[] m = new double[nbPts];
+            double minM = Double.MAX_VALUE;
+            double maxM = -Double.MAX_VALUE;
             for (int i = 0; i < nbPts; i++) {
                 final Point pt = (Point) geometry.getGeometryN(i);
-                channel.writeDouble(pt.getCoordinate().getM());
+                m[i] = pt.getCoordinate().getM();
+                minM = Double.min(minM, m[i]);
+                maxM = Double.max(maxM, m[i]);
             }
+            channel.writeDouble(minM);
+            channel.writeDouble(maxM);
+            channel.writeDoubles(m);
         }
 
         @Override
@@ -654,6 +792,20 @@ public abstract class ShapeGeometryEncoder<T extends 
Geometry> {
                  + 4 //nbPts
                  + ((MultiPoint) geom).getNumGeometries() * 4 * 8;
         }
+
+        @Override
+        public GeneralEnvelope getBoundingBox(Geometry geom) {
+            final GeneralEnvelope env = new GeneralEnvelope(4);
+            final MultiPoint pts = (MultiPoint) geom;
+            for (int i = 0, n = pts.getNumGeometries(); i < n; i++) {
+                final Point pt = (Point)pts.getGeometryN(i);
+                final Coordinate coord = pt.getCoordinate();
+                final double[] xyzm = new double[]{coord.getX(), coord.getY(), 
coord.getZ(), coord.getM()};
+                if (i == 0) env.setEnvelope(xyzm[0], xyzm[1], xyzm[2], 
xyzm[3], xyzm[0], xyzm[1], xyzm[2], xyzm[3]);
+                else env.add(new GeneralDirectPosition(xyzm));
+            }
+            return env;
+        }
     }
 
     private static class Polyline extends 
ShapeGeometryEncoder<MultiLineString> {
@@ -689,6 +841,11 @@ public abstract class ShapeGeometryEncoder<T extends 
Geometry> {
                  + nbGeom * 4 //offsets table
                  + nbPoints * nbOrdinates * 8; //all ordinates
         }
+
+        @Override
+        public GeneralEnvelope getBoundingBox(Geometry geom) {
+            return getLinesBoundingBox(geom);
+        }
     }
 
     private static class Polygon extends ShapeGeometryEncoder<MultiPolygon> {
@@ -728,6 +885,11 @@ public abstract class ShapeGeometryEncoder<T extends 
Geometry> {
                  + nbGeom * 4 //offsets table
                  + nbPoints * nbOrdinates * 8; //all ordinates
         }
+
+        @Override
+        public GeneralEnvelope getBoundingBox(Geometry geom) {
+            return getLinesBoundingBox(geom);
+        }
     }
 
     private static class MultiPatch extends ShapeGeometryEncoder<MultiPolygon> 
{
@@ -752,5 +914,10 @@ public abstract class ShapeGeometryEncoder<T extends 
Geometry> {
         public int getEncodedLength(Geometry geom) {
             throw new UnsupportedOperationException("Not supported yet.");
         }
+
+        @Override
+        public GeneralEnvelope getBoundingBox(Geometry geom) {
+            throw new UnsupportedOperationException("Not supported yet.");
+        }
     }
 }
diff --git 
a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shp/ShapeRecord.java
 
b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shp/ShapeRecord.java
index b98ddfd0fc..77a0776864 100644
--- 
a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shp/ShapeRecord.java
+++ 
b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shp/ShapeRecord.java
@@ -19,11 +19,15 @@ package org.apache.sis.storage.shapefile.shp;
 import java.awt.geom.Rectangle2D;
 import java.io.IOException;
 import java.nio.ByteOrder;
+
+import org.locationtech.jts.geom.Envelope;
 import org.locationtech.jts.geom.Geometry;
 import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.geometry.Envelope2D;
 import org.apache.sis.io.stream.ChannelDataInput;
 import org.apache.sis.io.stream.ChannelDataOutput;
+import org.locationtech.jts.geom.MultiPoint;
+import org.locationtech.jts.geom.Point;
 
 
 /**
diff --git 
a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shp/ShapeWriter.java
 
b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shp/ShapeWriter.java
index 2459802a3e..cabd09d663 100644
--- 
a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shp/ShapeWriter.java
+++ 
b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/shp/ShapeWriter.java
@@ -43,6 +43,13 @@ public final class ShapeWriter implements AutoCloseable{
         return header;
     }
 
+    /**
+     * @return current position in the stream
+     */
+    public long getSteamPosition() {
+        return channel.getBitOffset();
+    }
+
     /**
      * Header will be copied and modified.
      * Use getHeader to obtain the new header.
@@ -57,11 +64,12 @@ public final class ShapeWriter implements AutoCloseable{
 
     public void write(ShapeRecord record) throws IOException {
         record.write(channel, io);
+        final GeneralEnvelope geomBox = io.getBoundingBox(record.geometry);
         if (bbox == null) {
-            bbox = new GeneralEnvelope(record.bbox.getDimension());
-            bbox.setEnvelope(record.bbox);
+            bbox = new GeneralEnvelope(geomBox.getDimension());
+            bbox.setEnvelope(geomBox);
         } else {
-            bbox.add(record.bbox);
+            bbox.add(geomBox);
         }
     }
 
diff --git 
a/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/ShapefileStoreTest.java
 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/ShapefileStoreTest.java
index 6d0152954f..6fd3fcf34c 100644
--- 
a/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/ShapefileStoreTest.java
+++ 
b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/ShapefileStoreTest.java
@@ -16,13 +16,20 @@
  */
 package org.apache.sis.storage.shapefile;
 
+import java.io.IOException;
 import java.net.URISyntaxException;
 import java.net.URL;
+import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.time.LocalDate;
 import java.util.Iterator;
+import java.util.List;
 import java.util.stream.Stream;
+import org.apache.sis.feature.builder.FeatureTypeBuilder;
+import org.apache.sis.util.Utilities;
+import org.locationtech.jts.geom.Coordinate;
+import org.locationtech.jts.geom.GeometryFactory;
 import org.locationtech.jts.geom.Point;
 import org.apache.sis.filter.DefaultFilterFactory;
 import org.apache.sis.geometry.GeneralEnvelope;
@@ -30,6 +37,7 @@ import org.apache.sis.referencing.CommonCRS;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.FeatureQuery;
 import org.apache.sis.storage.FeatureSet;
+import org.apache.sis.feature.internal.AttributeConvention;
 
 // Test dependencies
 import static org.junit.jupiter.api.Assertions.*;
@@ -49,6 +57,8 @@ import org.opengis.filter.FilterFactory;
  */
 public class ShapefileStoreTest {
 
+    private static final GeometryFactory GF = new GeometryFactory();
+
     @Test
     public void testStream() throws URISyntaxException, DataStoreException {
         final URL url = 
ShapefileStoreTest.class.getResource("/org/apache/sis/storage/shapefile/point.shp");
@@ -171,21 +181,65 @@ public class ShapefileStoreTest {
         try (final ShapefileStore store = new 
ShapefileStore(Paths.get(url.toURI()))) {
             Path[] componentFiles = store.getComponentFiles();
             assertEquals(5, componentFiles.length);
-            componentFiles[0].toString().endsWith("point.shp");
-            componentFiles[1].toString().endsWith("point.shx");
-            componentFiles[2].toString().endsWith("point.dbf");
-            componentFiles[3].toString().endsWith("point.prj");
-            componentFiles[4].toString().endsWith("point.cpg");
+            assertTrue(componentFiles[0].toString().endsWith("point.shp"));
+            assertTrue(componentFiles[1].toString().endsWith("point.shx"));
+            assertTrue(componentFiles[2].toString().endsWith("point.dbf"));
+            assertTrue(componentFiles[3].toString().endsWith("point.prj"));
+            assertTrue(componentFiles[4].toString().endsWith("point.cpg"));
         }
     }
 
     /**
      * Test creating a new shapefile.
      */
-    @Ignore
     @Test
-    public void testCreate() throws URISyntaxException, DataStoreException {
-        //todo
+    public void testCreate() throws URISyntaxException, DataStoreException, 
IOException {
+        final Path temp = Files.createTempFile("test", ".shp");
+        Files.delete(temp);
+        final String name = temp.getFileName().toString().split("\\.")[0];
+        try (final ShapefileStore store = new ShapefileStore(temp)) {
+            Path[] componentFiles = store.getComponentFiles();
+            assertEquals(0, componentFiles.length);
+
+            {//create type
+                final FeatureType type = createType();
+                store.updateType(type);
+            }
+
+            {//check files have been created
+                componentFiles = store.getComponentFiles();
+                assertEquals(5, componentFiles.length);
+                assertTrue(componentFiles[0].toString().endsWith(name+".shp"));
+                assertTrue(componentFiles[1].toString().endsWith(name+".shx"));
+                assertTrue(componentFiles[2].toString().endsWith(name+".dbf"));
+                assertTrue( 
componentFiles[3].toString().endsWith(name+".prj"));
+                assertTrue(componentFiles[4].toString().endsWith(name+".cpg"));
+            }
+
+            {// check created type
+                FeatureType type = store.getType();
+                assertEquals(name, type.getName().toString());
+                System.out.println(type.toString());
+                assertEquals(9, type.getProperties(true).size());
+                assertNotNull(type.getProperty("sis:identifier"));
+                assertNotNull(type.getProperty("sis:envelope"));
+                assertNotNull(type.getProperty("sis:geometry"));
+                final var geomProp = (AttributeType) 
type.getProperty("geometry");
+                final var idProp = (AttributeType) type.getProperty("id");
+                final var textProp = (AttributeType) type.getProperty("text");
+                final var integerProp = (AttributeType) 
type.getProperty("integer");
+                final var floatProp = (AttributeType) 
type.getProperty("float");
+                final var dateProp = (AttributeType) type.getProperty("date");
+                final AttributeType crsChar = (AttributeType) 
geomProp.characteristics().get(AttributeConvention.CRS);
+                
assertTrue(Utilities.equalsIgnoreMetadata(CommonCRS.WGS84.geographic(),crsChar.getDefaultValue()));
+                assertEquals(Point.class, geomProp.getValueClass());
+                assertEquals(Integer.class, idProp.getValueClass());
+                assertEquals(String.class, textProp.getValueClass());
+                assertEquals(Integer.class, integerProp.getValueClass());
+                assertEquals(Double.class, floatProp.getValueClass());
+                assertEquals(LocalDate.class, dateProp.getValueClass());
+            }
+        }
     }
 
     /**
@@ -193,8 +247,22 @@ public class ShapefileStoreTest {
      */
     @Ignore
     @Test
-    public void testAddFeatures() throws URISyntaxException, 
DataStoreException {
-        //todo
+    public void testAddFeatures() throws URISyntaxException, 
DataStoreException, IOException {
+        final Path temp = Files.createTempFile("test", ".shp");
+        Files.delete(temp);
+        try (final ShapefileStore store = new ShapefileStore(temp)) {
+            FeatureType type = createType();
+            store.updateType(type);
+            type = store.getType();
+
+            Feature feature1 = createFeature1(type);
+            Feature feature2 = createFeature2(type);
+            store.add(List.of(feature1, feature2).iterator());
+
+            Object[] result = store.features(false).toArray();
+
+
+        }
     }
 
     /**
@@ -215,4 +283,38 @@ public class ShapefileStoreTest {
         //todo
     }
 
+    private static FeatureType createType() {
+        final FeatureTypeBuilder ftb = new FeatureTypeBuilder();
+        ftb.setName("test");
+        ftb.addAttribute(Integer.class).setName("id");
+        ftb.addAttribute(String.class).setName("text");
+        ftb.addAttribute(Integer.class).setName("integer");
+        ftb.addAttribute(Float.class).setName("float");
+        ftb.addAttribute(LocalDate.class).setName("date");
+        
ftb.addAttribute(Point.class).setName("geometry").setCRS(CommonCRS.WGS84.geographic());
+        return ftb.build();
+    }
+
+    private static Feature createFeature1(FeatureType type) {
+        Feature feature = type.newInstance();
+        feature.setPropertyValue("geometry", GF.createPoint(new 
Coordinate(10,20)));
+        feature.setPropertyValue("id", 1);
+        feature.setPropertyValue("text", "some text 1");
+        feature.setPropertyValue("integer", 123);
+        feature.setPropertyValue("float", 123.456);
+        feature.setPropertyValue("date", LocalDate.of(2023, 5, 12));
+        return feature;
+    }
+
+    private static Feature createFeature2(FeatureType type) {
+        Feature feature = type.newInstance();
+        feature.setPropertyValue("geometry", GF.createPoint(new 
Coordinate(30,40)));
+        feature.setPropertyValue("id", 2);
+        feature.setPropertyValue("text", "some text 2");
+        feature.setPropertyValue("integer", 456);
+        feature.setPropertyValue("float", 456.789);
+        feature.setPropertyValue("date", LocalDate.of(2030, 6, 21));
+        return feature;
+    }
+
 }
diff --git a/incubator/src/org.apache.sis.test.incubator/test/module-info.java 
b/incubator/src/org.apache.sis.test.incubator/test/module-info.java
index 66086cb5c7..fed9415c58 100644
--- a/incubator/src/org.apache.sis.test.incubator/test/module-info.java
+++ b/incubator/src/org.apache.sis.test.incubator/test/module-info.java
@@ -24,4 +24,7 @@ module org.apache.sis.test.incubator {
     requires transitive junit;
     requires transitive org.junit.jupiter.api;
     requires transitive org.opengis.geoapi.conformance;
+    requires org.apache.sis.feature;
+    requires org.apache.sis.storage;
+    requires org.locationtech.jts;
 }

Reply via email to