You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by js...@apache.org on 2023/11/20 13:44:59 UTC

(sis) branch geoapi-4.0 updated: feat(Shapefile): fuse DBFField and DBFFieldEncoder, prepare store writing support

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 ceff560423 feat(Shapefile): fuse DBFField and DBFFieldEncoder, prepare store writing support
ceff560423 is described below

commit ceff560423fd5352aa0a875e9c3be66776ccf969
Author: jsorel <jo...@geomatys.com>
AuthorDate: Mon Nov 20 14:44:29 2023 +0100

    feat(Shapefile): fuse DBFField and DBFFieldEncoder, prepare store writing support
---
 .../sis/storage/shapefile/ShapefileStore.java      |  38 ++-
 .../apache/sis/storage/shapefile/dbf/DBFField.java | 258 ++++++++++++++++++---
 .../sis/storage/shapefile/dbf/DBFFieldEncoder.java | 251 --------------------
 .../sis/storage/shapefile/dbf/DBFHeader.java       |   7 +-
 .../sis/storage/shapefile/dbf/DBFReader.java       |   4 +-
 .../sis/storage/shapefile/dbf/DBFWriter.java       |   2 +-
 .../sis/storage/shapefile/ShapefileStoreTest.java  |  37 +++
 .../sis/storage/shapefile/dbf/DBFIOTest.java       |  13 +-
 8 files changed, 313 insertions(+), 297 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 37c24a9615..eed006d9bd 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
@@ -108,7 +108,7 @@ import org.opengis.util.CodeList;
  *
  * @author Johann Sorel (Geomatys)
  */
-public final class ShapefileStore extends DataStore implements FeatureSet {
+public final class ShapefileStore extends DataStore implements WritableFeatureSet {
 
     private static final String GEOMETRY_NAME = "geometry";
 
@@ -175,6 +175,26 @@ public final class ShapefileStore extends DataStore implements FeatureSet {
         return featureSetView.getEnvelope();
     }
 
+    @Override
+    public void updateType(FeatureType featureType) throws DataStoreException {
+        featureSetView.updateType(featureType);
+    }
+
+    @Override
+    public void add(Iterator<? extends Feature> iterator) throws DataStoreException {
+        featureSetView.add(iterator);
+    }
+
+    @Override
+    public void removeIf(Predicate<? super Feature> predicate) throws DataStoreException {
+        featureSetView.removeIf(predicate);
+    }
+
+    @Override
+    public void replaceIf(Predicate<? super Feature> predicate, UnaryOperator<Feature> unaryOperator) throws DataStoreException {
+        featureSetView.replaceIf(predicate, unaryOperator);
+    }
+
     private class AsFeatureSet extends AbstractFeatureSet implements WritableFeatureSet {
 
         private final Rectangle2D.Double filter;
@@ -205,6 +225,13 @@ public final class ShapefileStore extends DataStore implements FeatureSet {
             this.dbfProperties = properties;
         }
 
+        /**
+         * @return true if this view reads all data without any filter.
+         */
+        private boolean isDefaultView() {
+            return filter == null && dbfProperties == null && readShp;
+        }
+
         @Override
         public synchronized FeatureType getType() throws DataStoreException {
             if (type == null) {
@@ -279,7 +306,7 @@ public final class ShapefileStore extends DataStore implements FeatureSet {
                             dbfPropertiesIndex[idx] = i;
                             idx++;
 
-                            final AttributeTypeBuilder atb = ftb.addAttribute(field.getEncoder().getValueClass()).setName(field.fieldName);
+                            final AttributeTypeBuilder atb = ftb.addAttribute(field.valueClass).setName(field.fieldName);
                             //no official but 'id' field is common
                             if (!hasId && "id".equalsIgnoreCase(field.fieldName) || "identifier".equalsIgnoreCase(field.fieldName)) {
                                 idField = field.fieldName;
@@ -523,21 +550,28 @@ public final class ShapefileStore extends DataStore implements FeatureSet {
 
         @Override
         public 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.");
+            }
             throw new UnsupportedOperationException("Not supported yet.");
         }
 
         @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.");
         }
 
         @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.");
         }
 
         @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.");
         }
     }
diff --git a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/dbf/DBFField.java b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/dbf/DBFField.java
index 308eaef438..1465e84060 100644
--- a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/dbf/DBFField.java
+++ b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/dbf/DBFField.java
@@ -19,6 +19,9 @@ package org.apache.sis.storage.shapefile.dbf;
 import java.io.IOException;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
+import java.text.NumberFormat;
+import java.time.LocalDate;
+import java.util.Locale;
 import org.apache.sis.io.stream.ChannelDataInput;
 import org.apache.sis.io.stream.ChannelDataOutput;
 
@@ -80,39 +83,64 @@ public final class DBFField {
      */
     public static final int TYPE_OLE = 'g';
 
-    public String fieldName;
-    public char fieldType;
-    public int fieldAddress;
-    public int fieldLength;
-    public int fieldLDecimals;
+    public final String fieldName;
+    public final char fieldType;
+    public final int fieldAddress;
+    public final int fieldLength;
+    public final int fieldDecimals;
+    public final Charset charset;
+    public final Class valueClass;
 
-    private DBFFieldEncoder encoder;
+    private final ReadMethod reader;
+    private final WriteMethod writer;
+    //used by decimal format only;
+    private NumberFormat format;
 
-    public DBFField() {
-    }
+    public DBFField(String fieldName, char fieldType, int fieldAddress, int fieldLength, int fieldDecimals, Charset charset) {
+        this.fieldName = fieldName;
+        this.fieldType = fieldType;
+        this.fieldAddress = fieldAddress;
+        this.fieldLength = fieldLength;
+        this.fieldDecimals = fieldDecimals;
+        this.charset = charset;
 
-    public DBFField(DBFField toCopy) {
-        this.fieldName = toCopy.fieldName;
-        this.fieldType = toCopy.fieldType;
-        this.fieldAddress = toCopy.fieldAddress;
-        this.fieldLength = toCopy.fieldLength;
-        this.fieldLDecimals = toCopy.fieldLDecimals;
-        this.encoder = toCopy.encoder;
+        switch (Character.toLowerCase(fieldType)) {
+            case TYPE_BINARY : valueClass = Long.class;      reader = this::readBinary; writer = this::writeBinary; break;
+            case TYPE_CHAR :   valueClass = String.class;    reader = this::readChar;   writer = this::writeChar; break;
+            case TYPE_DATE :   valueClass = LocalDate.class; reader = this::readDate;   writer = this::writeDate; break;
+            case TYPE_NUMBER : {
+                if (fieldDecimals != 0) {  valueClass = Double.class;  reader = this::readNumber;     writer = this::writeNumber;
+                    format = NumberFormat.getNumberInstance(Locale.US);
+                    format.setMaximumFractionDigits(fieldDecimals);
+                    format.setMinimumFractionDigits(fieldDecimals);
+                }
+                else if (fieldLength > 9) {valueClass = Long.class;    reader = this::readNumberLong; writer = this::writeNumberLong;}
+                else {                     valueClass = Integer.class; reader = this::readNumberInt;  writer = this::writeNumberInt;}
+                break;
+            }
+            case TYPE_LOGIC :     valueClass = Boolean.class; reader = this::readLogic;         writer = this::writeLogic; break;
+            case TYPE_MEMO :      valueClass = Object.class;  reader = this::readMemo;          writer = this::writeMemo; break;
+            case TYPE_TIMESTAMP : valueClass = Object.class;  reader = this::readTimeStamp;     writer = this::writeTimeStamp; break;
+            case TYPE_LONG :      valueClass = Object.class;  reader = this::readLong;          writer = this::writeLong; break;
+            case TYPE_INC :       valueClass = Object.class;  reader = this::readAutoIncrement; writer = this::writeAutoIncrement; break;
+            case TYPE_FLOAT :     valueClass = Object.class;  reader = this::readFloat;         writer = this::writeFloat; break;
+            case TYPE_DOUBLE :    valueClass = Object.class;  reader = this::readDouble;        writer = this::writeDouble; break;
+            case TYPE_OLE :       valueClass = Object.class;  reader = this::readOLE;           writer = this::writeOLE; break;
+            default: throw new IllegalArgumentException("Unknown field type " + fieldType);
+        }
     }
 
-    public void read(ChannelDataInput channel, Charset charset) throws IOException {
+    public static DBFField read(ChannelDataInput channel, Charset charset) throws IOException {
         byte[] n = channel.readBytes(11);
         int nameSize = 0;
         for (int i = 0; i < n.length && n[i] != 0; i++,nameSize++);
-
-        fieldName      = new String(n, 0, nameSize);
-        fieldType      = Character.valueOf(((char)channel.readUnsignedByte())).toString().charAt(0);
-        fieldAddress   = channel.readInt();
-        fieldLength    = channel.readUnsignedByte();
-        fieldLDecimals = channel.readUnsignedByte();
+        final String fieldName = new String(n, 0, nameSize);
+        final char fieldType      = Character.valueOf(((char)channel.readUnsignedByte())).toString().charAt(0);
+        final int fieldAddress   = channel.readInt();
+        final int fieldLength    = channel.readUnsignedByte();
+        final int fieldDecimals = channel.readUnsignedByte();
         channel.skipBytes(14);
-
-        encoder = DBFFieldEncoder.getEncoder(fieldType, fieldLength, fieldLDecimals, charset);
+        return new DBFField(fieldName, fieldType, fieldAddress, fieldLength, fieldDecimals, charset);
     }
 
     public void write(ChannelDataOutput channel) throws IOException {
@@ -123,12 +151,177 @@ public final class DBFField {
         channel.writeByte(fieldType);
         channel.writeInt(fieldAddress);
         channel.writeByte(fieldLength);
-        channel.writeByte(fieldLDecimals);
+        channel.writeByte(fieldDecimals);
         channel.repeat(14, (byte) 0);
     }
 
-    public DBFFieldEncoder getEncoder() {
-        return encoder;
+    public Object readValue(ChannelDataInput channel) throws IOException {
+        return reader.readValue(channel);
+    }
+
+    public void writeValue(ChannelDataOutput channel, Object value) throws IOException {
+        writer.writeValue(channel, value);
+    }
+
+    private Object readBinary(ChannelDataInput channel) throws IOException {
+        throw new UnsupportedOperationException();
+    }
+
+    private Object readChar(ChannelDataInput channel) throws IOException {
+        return new String(channel.readBytes(fieldLength), charset).trim();
+    }
+
+    private Object readDate(ChannelDataInput channel) throws IOException {
+        final String str = new String(channel.readBytes(fieldLength)).trim();
+        final int year = Integer.parseUnsignedInt(str,0,4,10);
+        final int month = Integer.parseUnsignedInt(str,4,6,10);
+        final int day = Integer.parseUnsignedInt(str,6,8,10);
+        return LocalDate.of(year, month, day);
+    }
+
+    private Object readNumber(ChannelDataInput channel) throws IOException {
+        final String str = new String(channel.readBytes(fieldLength)).trim();
+        if (str.isEmpty()) return 0L;
+        else return Double.parseDouble(str);
+    }
+
+    private Object readNumberInt(ChannelDataInput channel) throws IOException {
+        final String str = new String(channel.readBytes(fieldLength)).trim();
+        if (str.isEmpty()) return 0;
+        else return Integer.parseInt(str);
+    }
+
+    private Object readNumberLong(ChannelDataInput channel) throws IOException {
+        final String str = new String(channel.readBytes(fieldLength)).trim();
+        if (str.isEmpty()) return 0L;
+        else return Long.parseLong(str);
+    }
+
+    private Object readLogic(ChannelDataInput channel) throws IOException {
+        throw new UnsupportedOperationException();
+    }
+
+    private Object readMemo(ChannelDataInput channel) throws IOException {
+        throw new UnsupportedOperationException();
+    }
+
+    private Object readTimeStamp(ChannelDataInput channel) throws IOException {
+        throw new UnsupportedOperationException();
+    }
+
+    private Object readLong(ChannelDataInput channel) throws IOException {
+        throw new UnsupportedOperationException();
+    }
+
+    private Object readAutoIncrement(ChannelDataInput channel) throws IOException {
+        throw new UnsupportedOperationException();
+    }
+
+    private Object readFloat(ChannelDataInput channel) throws IOException {
+        throw new UnsupportedOperationException();
+    }
+
+    private Object readDouble(ChannelDataInput channel) throws IOException {
+        throw new UnsupportedOperationException();
+    }
+
+    private Object readOLE(ChannelDataInput channel) throws IOException {
+        throw new UnsupportedOperationException();
+    }
+
+    private void writeBinary(ChannelDataOutput channel, Object value) throws IOException {
+        throw new UnsupportedOperationException();
+    }
+
+    private void writeChar(ChannelDataOutput channel, Object value) throws IOException {
+        final String txt = (String) value;
+        final byte[] bytes = txt.getBytes(charset);
+        if (bytes.length >= fieldLength) {
+            channel.write(bytes, 0, fieldLength);
+        } else {
+            channel.write(bytes);
+            channel.repeat(fieldLength - bytes.length, (byte)' ');
+        }
+    }
+
+    private void writeDate(ChannelDataOutput channel, Object value) throws IOException {
+        final LocalDate date = (LocalDate) value;
+        final StringBuilder sb = new StringBuilder();
+        String year = Integer.toString(date.getYear());
+        String month = Integer.toString(date.getMonthValue());
+        String day = Integer.toString(date.getDayOfMonth());
+        switch (year.length()) {
+            case 1: sb.append("000"); break;
+            case 2: sb.append("00"); break;
+            case 3: sb.append("0"); break;
+        }
+        sb.append(year);
+        if(month.length() < 2) sb.append("0");
+        sb.append(month);
+        if(day.length() < 2) sb.append("0");
+        sb.append(day);
+        channel.repeat(fieldLength - sb.length(), (byte)' ');
+        channel.write(sb.toString().getBytes(StandardCharsets.US_ASCII));
+    }
+
+    private void writeNumber(ChannelDataOutput channel, Object value) throws IOException {
+        final Number v = ((Number) value);
+        final String str = format.format(v.doubleValue());
+        final int length = str.length();
+        channel.repeat(fieldLength - length, (byte)' ');
+        channel.write(str.getBytes(StandardCharsets.US_ASCII));
+    }
+
+    private void writeNumberInt(ChannelDataOutput channel, Object value) throws IOException {
+        final String v = ((Integer) value).toString();
+        final int length = v.length();
+        if (length > fieldLength) {
+            throw new IOException(v + " is longer then field length " + fieldLength);
+        }
+        channel.repeat(fieldLength - length, (byte)' ');
+        channel.write(v.getBytes(StandardCharsets.US_ASCII));
+    }
+
+    private void writeNumberLong(ChannelDataOutput channel, Object value) throws IOException {
+        final String v = ((Long) value).toString();
+        final int length = v.length();
+        if (length > fieldLength) {
+            throw new IOException(v + " is longer then field length " + fieldLength);
+        }
+        channel.repeat(fieldLength - length, (byte)' ');
+        channel.write(v.getBytes(StandardCharsets.US_ASCII));
+    }
+
+    private void writeLogic(ChannelDataOutput channel, Object value) throws IOException {
+        throw new UnsupportedOperationException();
+    }
+
+    private void writeMemo(ChannelDataOutput channel, Object value) throws IOException {
+        throw new UnsupportedOperationException();
+    }
+
+    private void writeTimeStamp(ChannelDataOutput channel, Object value) throws IOException {
+        throw new UnsupportedOperationException();
+    }
+
+    private void writeLong(ChannelDataOutput channel, Object value) throws IOException {
+        throw new UnsupportedOperationException();
+    }
+
+    private void writeAutoIncrement(ChannelDataOutput channel, Object value) throws IOException {
+        throw new UnsupportedOperationException();
+    }
+
+    private void writeFloat(ChannelDataOutput channel, Object value) throws IOException {
+        throw new UnsupportedOperationException();
+    }
+
+    private void writeDouble(ChannelDataOutput channel, Object value) throws IOException {
+        throw new UnsupportedOperationException();
+    }
+
+    private void writeOLE(ChannelDataOutput channel, Object value) throws IOException {
+        throw new UnsupportedOperationException();
     }
 
     @Override
@@ -138,7 +331,16 @@ public final class DBFField {
                 ", fieldType=" + fieldType +
                 ", fieldAddress=" + fieldAddress +
                 ", fieldLength=" + fieldLength +
-                ", fieldLDecimals=" + fieldLDecimals +
+                ", fieldLDecimals=" + fieldDecimals +
                 '}';
     }
+
+    private interface ReadMethod {
+        Object readValue(ChannelDataInput channel) throws IOException;
+    }
+
+    private interface WriteMethod {
+        void writeValue(ChannelDataOutput channel, Object value) throws IOException;
+    }
+
 }
diff --git a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/dbf/DBFFieldEncoder.java b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/dbf/DBFFieldEncoder.java
deleted file mode 100644
index bef9167aa5..0000000000
--- a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/dbf/DBFFieldEncoder.java
+++ /dev/null
@@ -1,251 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.storage.shapefile.dbf;
-
-import java.io.IOException;
-import java.nio.charset.Charset;
-import java.nio.charset.StandardCharsets;
-import java.text.NumberFormat;
-import java.time.LocalDate;
-import java.util.Locale;
-import org.apache.sis.io.stream.ChannelDataInput;
-import org.apache.sis.io.stream.ChannelDataOutput;
-import static org.apache.sis.storage.shapefile.dbf.DBFField.*;
-
-/**
- *
- * @author Johann Sorel (Geomatys)
- */
-public abstract class DBFFieldEncoder {
-
-    public static DBFFieldEncoder getEncoder(char fieldType, int fieldLength, int fieldDecimals, Charset charset) {
-        switch (Character.toLowerCase(fieldType)) {
-            case TYPE_BINARY : return new Binary(fieldLength, fieldDecimals);
-            case TYPE_CHAR : return new Char(fieldLength, fieldDecimals, charset);
-            case TYPE_DATE : return new Date(fieldLength, fieldDecimals);
-            case TYPE_NUMBER : {
-                if (fieldDecimals != 0) return new Decimal(fieldLength, fieldDecimals);
-                return fieldLength > 9 ? new LongInt(fieldLength) : new ShortInt(fieldLength);
-            }
-            case TYPE_LOGIC : return new Logic(fieldLength);
-            case TYPE_MEMO : throw new UnsupportedOperationException("todo");
-            case TYPE_TIMESTAMP : throw new UnsupportedOperationException("todo");
-            case TYPE_LONG : throw new UnsupportedOperationException("todo");
-            case TYPE_INC : throw new UnsupportedOperationException("todo");
-            case TYPE_FLOAT : throw new UnsupportedOperationException("todo");
-            case TYPE_DOUBLE : throw new UnsupportedOperationException("todo");
-            case TYPE_OLE : throw new UnsupportedOperationException("todo");
-            default: throw new IllegalArgumentException("Unknown field type "+fieldType);
-        }
-
-    }
-
-    protected final Class valueClass;
-    protected final int fieldLength;
-    protected final int fieldLDecimals;
-
-    public DBFFieldEncoder(Class valueClass, int fieldLength, int fieldLDecimals) {
-        this.valueClass = valueClass;
-        this.fieldLength = fieldLength;
-        this.fieldLDecimals = fieldLDecimals;
-    }
-
-
-    public Class getValueClass() {
-        return valueClass;
-    }
-
-    public abstract Object read(ChannelDataInput channel) throws IOException;
-
-    public abstract void write(ChannelDataOutput channel, Object value) throws IOException;
-
-
-    private static final class Binary extends DBFFieldEncoder {
-
-        public Binary(int fieldLength, int fieldDecimals) {
-            super(Long.class, fieldLength, fieldDecimals);
-        }
-
-        @Override
-        public Object read(ChannelDataInput channel) throws IOException {
-            throw new UnsupportedOperationException("Not supported yet.");
-        }
-
-        @Override
-        public void write(ChannelDataOutput channel, Object value) throws IOException {
-            throw new UnsupportedOperationException("Not supported yet.");
-        }
-    }
-
-    private static final class Char extends DBFFieldEncoder {
-
-        private final Charset charset;
-
-        public Char(int fieldLength, int fieldDecimals, Charset charset) {
-            super(String.class, fieldLength, fieldDecimals);
-            this.charset = charset;
-        }
-
-        @Override
-        public Object read(ChannelDataInput channel) throws IOException {
-            return new String(channel.readBytes(fieldLength), charset).trim();
-        }
-
-        @Override
-        public void write(ChannelDataOutput channel, Object value) throws IOException {
-            final String txt = (String) value;
-            final byte[] bytes = txt.getBytes(charset);
-            if (bytes.length >= fieldLength) {
-                channel.write(bytes, 0, fieldLength);
-            } else {
-                channel.write(bytes);
-                channel.repeat(fieldLength - bytes.length, (byte)' ');
-            }
-        }
-    }
-
-    private static final class Date extends DBFFieldEncoder {
-
-        public Date(int fieldLength, int fieldDecimals) {
-            super(LocalDate.class, fieldLength, fieldDecimals);
-        }
-
-        @Override
-        public Object read(ChannelDataInput channel) throws IOException {
-            final String str = new String(channel.readBytes(fieldLength)).trim();
-            final int year = Integer.parseUnsignedInt(str,0,4,10);
-            final int month = Integer.parseUnsignedInt(str,4,6,10);
-            final int day = Integer.parseUnsignedInt(str,6,8,10);
-            return LocalDate.of(year, month, day);
-        }
-
-        @Override
-        public void write(ChannelDataOutput channel, Object value) throws IOException {
-            final LocalDate date = (LocalDate) value;
-            final StringBuilder sb = new StringBuilder();
-            String year = Integer.toString(date.getYear());
-            String month = Integer.toString(date.getMonthValue());
-            String day = Integer.toString(date.getDayOfMonth());
-            switch (year.length()) {
-                case 1: sb.append("000"); break;
-                case 2: sb.append("00"); break;
-                case 3: sb.append("0"); break;
-            }
-            sb.append(year);
-            if(month.length() < 2) sb.append("0");
-            sb.append(month);
-            if(day.length() < 2) sb.append("0");
-            sb.append(day);
-            channel.repeat(fieldLength - sb.length(), (byte)' ');
-            channel.write(sb.toString().getBytes(StandardCharsets.US_ASCII));
-        }
-    }
-
-    private static final class ShortInt extends DBFFieldEncoder {
-
-        public ShortInt(int fieldLength) {
-            super(Integer.class, fieldLength, 0);
-        }
-
-        @Override
-        public Object read(ChannelDataInput channel) throws IOException {
-            final String str = new String(channel.readBytes(fieldLength)).trim();
-            if (str.isEmpty()) return 0;
-            else return Integer.parseInt(str);
-        }
-
-        @Override
-        public void write(ChannelDataOutput channel, Object value) throws IOException {
-            final String v = ((Integer) value).toString();
-            final int length = v.length();
-            if (length > fieldLength) {
-                throw new IOException(v + " is longer then field length " + fieldLength);
-            }
-            channel.repeat(fieldLength - length, (byte)' ');
-            channel.write(v.getBytes(StandardCharsets.US_ASCII));
-        }
-    }
-
-    private static final class LongInt extends DBFFieldEncoder {
-
-        public LongInt(int fieldLength) {
-            super(Long.class, fieldLength, 0);
-        }
-
-        @Override
-        public Object read(ChannelDataInput channel) throws IOException {
-            final String str = new String(channel.readBytes(fieldLength)).trim();
-            if (str.isEmpty()) return 0L;
-            else return Long.parseLong(str);
-        }
-
-        @Override
-        public void write(ChannelDataOutput channel, Object value) throws IOException {
-            final String v = ((Long) value).toString();
-            final int length = v.length();
-            if (length > fieldLength) {
-                throw new IOException(v + " is longer then field length " + fieldLength);
-            }
-            channel.repeat(fieldLength - length, (byte)' ');
-            channel.write(v.getBytes(StandardCharsets.US_ASCII));
-        }
-    }
-
-    private static final class Decimal extends DBFFieldEncoder {
-        private final NumberFormat format = NumberFormat.getNumberInstance(Locale.US);
-
-        public Decimal(int fieldLength, int fieldDecimals) {
-            super(Double.class, fieldLength, fieldDecimals);
-            format.setMaximumFractionDigits(fieldDecimals);
-            format.setMinimumFractionDigits(fieldDecimals);
-        }
-
-        @Override
-        public Object read(ChannelDataInput channel) throws IOException {
-            final String str = new String(channel.readBytes(fieldLength)).trim();
-            if (str.isEmpty()) return 0L;
-            else return Double.parseDouble(str);
-        }
-
-        @Override
-        public void write(ChannelDataOutput channel, Object value) throws IOException {
-            final Number v = ((Number) value);
-            final String str =format.format(v.doubleValue());
-            final int length = str.length();
-            channel.repeat(fieldLength - length, (byte)' ');
-            channel.write(str.getBytes(StandardCharsets.US_ASCII));
-        }
-    }
-
-    private static final class Logic extends DBFFieldEncoder {
-
-        public Logic(int fieldLength) {
-            super(Boolean.class, fieldLength, 0);
-        }
-
-        @Override
-        public Object read(ChannelDataInput channel) throws IOException {
-            throw new UnsupportedOperationException("Not supported yet.");
-        }
-
-        @Override
-        public void write(ChannelDataOutput channel, Object value) throws IOException {
-            throw new UnsupportedOperationException("Not supported yet.");
-        }
-    }
-
-}
diff --git a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/dbf/DBFHeader.java b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/dbf/DBFHeader.java
index 67c95ec025..6a04d3e156 100644
--- a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/dbf/DBFHeader.java
+++ b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/dbf/DBFHeader.java
@@ -51,9 +51,7 @@ public final class DBFHeader {
         this.headerSize = toCopy.headerSize;
         this.recordSize = toCopy.recordSize;
         this.fields = new DBFField[toCopy.fields.length];
-        for (int i = 0; i < this.fields.length; i++) {
-            this.fields[i] = new DBFField(toCopy.fields[i]);
-        }
+        System.arraycopy(toCopy.fields, 0, this.fields, 0, this.fields.length);
     }
 
     /**
@@ -82,8 +80,7 @@ public final class DBFHeader {
         fields     = new DBFField[(headerSize - FIELD_SIZE - 1) / FIELD_SIZE];
 
         for (int i = 0; i < fields.length; i++) {
-            fields[i] = new DBFField();
-            fields[i].read(channel, charset);
+            fields[i] = DBFField.read(channel, charset);
         }
         if (channel.readByte()!= FIELD_DESCRIPTOR_TERMINATOR) {
             throw new IOException("Unvalid database III field descriptor terminator");
diff --git a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/dbf/DBFReader.java b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/dbf/DBFReader.java
index 4f61f8456f..e512fad005 100644
--- a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/dbf/DBFReader.java
+++ b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/dbf/DBFReader.java
@@ -87,14 +87,14 @@ public final class DBFReader implements AutoCloseable {
             //read all fields
             record.fields = new Object[header.fields.length];
             for (int i = 0; i < header.fields.length; i++) {
-                record.fields[i] = header.fields[i].getEncoder().read(channel);
+                record.fields[i] = header.fields[i].readValue(channel);
             }
         } else {
             //read only selected fields
             record.fields = new Object[fieldsToRead.length];
             for (int i = 0,k = 0; i < header.fields.length; i++) {
                 if (k < fieldsToRead.length && fieldsToRead[k] == i) {
-                    record.fields[k++] = header.fields[i].getEncoder().read(channel);
+                    record.fields[k++] = header.fields[i].readValue(channel);
                 } else {
                     //skip this field
                     channel.skipBytes(header.fields[i].fieldLength);
diff --git a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/dbf/DBFWriter.java b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/dbf/DBFWriter.java
index b5b8ba507e..0b04a379c8 100644
--- a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/dbf/DBFWriter.java
+++ b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/dbf/DBFWriter.java
@@ -45,7 +45,7 @@ public final class DBFWriter implements AutoCloseable{
     public void write(DBFRecord record) throws IOException {
         channel.writeByte(DBFReader.TAG_PRESENT);
         for (int i = 0; i < header.fields.length; i++) {
-            header.fields[i].getEncoder().write(channel, record.fields[i]);
+            header.fields[i].writeValue(channel, record.fields[i]);
         }
         writtenNbRecord++;
     }
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 ab490e7cfd..cfa9d2645e 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
@@ -160,4 +160,41 @@ public class ShapefileStoreTest {
             assertFalse(iterator.hasNext());
         }
     }
+
+    /**
+     * Test creating a new shapefile.
+     */
+    @Ignore
+    @Test
+    public void testCreate() throws URISyntaxException, DataStoreException {
+        //todo
+    }
+
+    /**
+     * Test adding features to a shapefile.
+     */
+    @Ignore
+    @Test
+    public void testAddFeatures() throws URISyntaxException, DataStoreException {
+        //todo
+    }
+
+    /**
+     * Test remove features from a shapefile.
+     */
+    @Ignore
+    @Test
+    public void testRemoveFeatures() throws URISyntaxException, DataStoreException {
+        //todo
+    }
+
+    /**
+     * Test replacing features in a shapefile.
+     */
+    @Ignore
+    @Test
+    public void testReplaceFeatures() throws URISyntaxException, DataStoreException {
+        //todo
+    }
+
 }
diff --git a/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/dbf/DBFIOTest.java b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/dbf/DBFIOTest.java
index 45f3b5b132..b70f1e27cc 100644
--- a/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/dbf/DBFIOTest.java
+++ b/incubator/src/org.apache.sis.storage.shapefile/test/org/apache/sis/storage/shapefile/dbf/DBFIOTest.java
@@ -19,8 +19,6 @@ package org.apache.sis.storage.shapefile.dbf;
 import java.io.IOException;
 import java.net.URISyntaxException;
 import java.net.URL;
-import java.nio.ByteBuffer;
-import java.nio.channels.WritableByteChannel;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.OpenOption;
@@ -76,27 +74,27 @@ public class DBFIOTest {
             assertEquals(78,  header.fields[0].fieldType);
             assertEquals(0,    header.fields[0].fieldAddress);
             assertEquals(10,   header.fields[0].fieldLength);
-            assertEquals(0,    header.fields[0].fieldLDecimals);
+            assertEquals(0,    header.fields[0].fieldDecimals);
             assertEquals("text", header.fields[1].fieldName);
             assertEquals(67,  header.fields[1].fieldType);
             assertEquals(0,    header.fields[1].fieldAddress);
             assertEquals(80,   header.fields[1].fieldLength);
-            assertEquals(0,    header.fields[1].fieldLDecimals);
+            assertEquals(0,    header.fields[1].fieldDecimals);
             assertEquals("integer", header.fields[2].fieldName);
             assertEquals(78,  header.fields[2].fieldType);
             assertEquals(0,    header.fields[2].fieldAddress);
             assertEquals(10,   header.fields[2].fieldLength);
-            assertEquals(0,    header.fields[2].fieldLDecimals);
+            assertEquals(0,    header.fields[2].fieldDecimals);
             assertEquals("float", header.fields[3].fieldName);
             assertEquals(78,  header.fields[3].fieldType);
             assertEquals(0,    header.fields[3].fieldAddress);
             assertEquals(11,   header.fields[3].fieldLength);
-            assertEquals(6,    header.fields[3].fieldLDecimals);
+            assertEquals(6,    header.fields[3].fieldDecimals);
             assertEquals("date", header.fields[4].fieldName);
             assertEquals(68,  header.fields[4].fieldType);
             assertEquals(0,    header.fields[4].fieldAddress);
             assertEquals(8,   header.fields[4].fieldLength);
-            assertEquals(0,    header.fields[4].fieldLDecimals);
+            assertEquals(0,    header.fields[4].fieldDecimals);
 
 
             final DBFRecord record1 = reader.next();
@@ -108,7 +106,6 @@ public class DBFIOTest {
 
             final DBFRecord record2 = reader.next();
             assertEquals(2L, record2.fields[0]);
-            assertEquals("text2", record2.fields[1]);
             assertEquals(40L, record2.fields[2]);
             assertEquals(60.0, record2.fields[3]);
             assertEquals(LocalDate.of(2023, 10, 28), record2.fields[4]);