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

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

commit a6960cfd0e6c7c621e2690ff11d02218f57f77ee
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Tue May 5 15:44:42 2026 +0200

    Resolve a `NullPointerException` during deserialization
    by avoiding to compute `wrapAroundChanges` too early.
    
    https://issues.apache.org/jira/browse/SIS-632
---
 .../operation/AbstractCoordinateOperation.java     | 60 ++++++----------------
 .../operation/AbstractSingleOperation.java         |  3 +-
 .../operation/DefaultPassThroughOperation.java     |  2 -
 .../test/org/apache/sis/referencing/CRSTest.java   | 32 ++++++++++++
 4 files changed, 50 insertions(+), 47 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/AbstractCoordinateOperation.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/AbstractCoordinateOperation.java
index 02a364de93..436f95a10b 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/AbstractCoordinateOperation.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/AbstractCoordinateOperation.java
@@ -22,8 +22,6 @@ import java.util.Objects;
 import java.util.Optional;
 import java.util.Collection;
 import java.util.logging.Logger;
-import java.io.IOException;
-import java.io.ObjectInputStream;
 import jakarta.xml.bind.Unmarshaller;
 import jakarta.xml.bind.annotation.XmlType;
 import jakarta.xml.bind.annotation.XmlSeeAlso;
@@ -108,7 +106,7 @@ import org.opengis.coordinate.CoordinateSet;
  * synchronization.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.6
+ * @version 1.7
  * @since   0.6
  */
 @XmlType(name = "AbstractCoordinateOperationType", propOrder = {
@@ -135,7 +133,7 @@ public class AbstractCoordinateOperation extends 
AbstractIdentifiedObject implem
     static final Logger LOGGER = 
Logger.getLogger(Loggers.COORDINATE_OPERATION);
 
     /**
-     * The source CRS, or {@code null} if not available.
+     * The source <abbr>CRS</abbr>, or {@code null} if not available.
      *
      * <p><b>Consider this field as final!</b>
      * This field is non-final only for the convenience of constructors and 
for initialization
@@ -147,7 +145,7 @@ public class AbstractCoordinateOperation extends 
AbstractIdentifiedObject implem
     CoordinateReferenceSystem sourceCRS;
 
     /**
-     * The target CRS, or {@code null} if not available.
+     * The target <abbr>CRS</abbr>, or {@code null} if not available.
      *
      * <p><b>Consider this field as final!</b>
      * This field is non-final only for the convenience of constructors and 
for initialization
@@ -210,10 +208,11 @@ public class AbstractCoordinateOperation extends 
AbstractIdentifiedObject implem
      * This is usually the longitude axis when the source CRS uses the [-180 … 
+180]° range and the target
      * CRS uses the [0 … 360]° range, or the converse. If there is no change, 
then this is an empty set.
      *
+     * <p>This is initially {@code null} and computed when first needed.</p>
+     *
      * @see #getWrapAroundChanges()
-     * @see #computeTransientFields()
      */
-    private transient Set<Integer> wrapAroundChanges;
+    private transient volatile Set<Integer> wrapAroundChanges;
 
     /**
      * The inverse of this coordinate operation, computed when first needed. 
This is stored for making
@@ -382,30 +381,6 @@ check:      for (int isTarget=0; ; isTarget++) {        // 
0 == source check; 1
                 }
             }
         }
-        computeTransientFields();
-    }
-
-    /**
-     * Computes the {@link #wrapAroundChanges} field after we verified that 
the coordinate operation is valid.
-     */
-    final void computeTransientFields() {
-        if (sourceCRS != null && targetCRS != null) {
-            wrapAroundChanges = 
CoordinateOperations.wrapAroundChanges(sourceCRS, 
targetCRS.getCoordinateSystem());
-        } else {
-            wrapAroundChanges = Set.of();
-        }
-    }
-
-    /**
-     * Computes transient fields after deserialization.
-     *
-     * @param  in  the input stream from which to deserialize a coordinate 
operation.
-     * @throws IOException if an I/O error occurred while reading or if the 
stream contains invalid data.
-     * @throws ClassNotFoundException if the class serialized on the stream is 
not on the module path.
-     */
-    private void readObject(final ObjectInputStream in) throws IOException, 
ClassNotFoundException {
-        in.defaultReadObject();
-        computeTransientFields();
     }
 
     /**
@@ -428,9 +403,7 @@ check:      for (int isTarget=0; ; isTarget++) {        // 
0 == source check; 1
         coordinateOperationAccuracy = 
operation.getCoordinateOperationAccuracy();
         transform                   = operation.getMathTransform();
         if (operation instanceof AbstractCoordinateOperation) {
-            wrapAroundChanges = ((AbstractCoordinateOperation) 
operation).wrapAroundChanges;
-        } else {
-            computeTransientFields();
+            wrapAroundChanges = ((AbstractCoordinateOperation) 
operation).getWrapAroundChanges();
         }
     }
 
@@ -806,7 +779,16 @@ check:      for (int isTarget=0; ; isTarget++) {        // 
0 == source check; 1
      */
     @SuppressWarnings("ReturnOfCollectionOrArrayField")
     public Set<Integer> getWrapAroundChanges() {
-        return wrapAroundChanges;
+        Set<Integer> changes = wrapAroundChanges;
+        if (changes == null) {
+            if (sourceCRS != null && targetCRS != null) {
+                changes = CoordinateOperations.wrapAroundChanges(sourceCRS, 
targetCRS.getCoordinateSystem());
+            } else {
+                changes = Set.of();
+            }
+            wrapAroundChanges = changes;
+        }
+        return changes;
     }
 
     /**
@@ -1178,12 +1160,4 @@ check:      for (int isTarget=0; ; isTarget++) {        
// 0 == source check; 1
             
ImplementationHelper.propertyAlreadySet(AbstractCoordinateOperation.class, 
"setAccuracy", "coordinateOperationAccuracy");
         }
     }
-
-    /**
-     * Invoked by JAXB after unmarshalling.
-     * May be overridden by subclasses.
-     */
-    void afterUnmarshal(Unmarshaller unmarshaller, Object parent) {
-        computeTransientFields();
-    }
 }
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/AbstractSingleOperation.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/AbstractSingleOperation.java
index 06a2aa9256..3d954d8889 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/AbstractSingleOperation.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/AbstractSingleOperation.java
@@ -456,9 +456,8 @@ class AbstractSingleOperation extends 
AbstractCoordinateOperation implements Sin
      *
      * @see <a href="http://issues.apache.org/jira/browse/SIS-291";>SIS-291</a>
      */
-    @Override
+    @SuppressWarnings("LocalVariableHidesMemberVariable")
     final void afterUnmarshal(Unmarshaller unmarshaller, Object parent) {
-        super.afterUnmarshal(unmarshaller, parent);
         if (parameters == null && method != null) {
             final ParameterDescriptorGroup descriptor = method.getParameters();
             if (descriptor != null && descriptor.descriptors().isEmpty()) {
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefaultPassThroughOperation.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefaultPassThroughOperation.java
index 79855d7e65..2199e104d0 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefaultPassThroughOperation.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/DefaultPassThroughOperation.java
@@ -362,10 +362,8 @@ public class DefaultPassThroughOperation extends 
AbstractCoordinateOperation imp
      * Invoked by JAXB after unmarshalling. If needed, this method tries to 
infer source/target CRS
      * of the nested operation from the source/target CRS of the enclosing 
pass-through operation.
      */
-    @Override
     @SuppressWarnings("LocalVariableHidesMemberVariable")
     final void afterUnmarshal(Unmarshaller unmarshaller, Object parent) {
-        super.afterUnmarshal(unmarshaller, parent);
         /*
          * State validation. The `missing` string will be used in exception 
message
          * at the end of this method if a required component is reported 
missing.
diff --git 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/CRSTest.java
 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/CRSTest.java
index b919234ef2..ac6cda0794 100644
--- 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/CRSTest.java
+++ 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/CRSTest.java
@@ -21,6 +21,12 @@ import java.util.HashMap;
 import java.util.Arrays;
 import java.util.BitSet;
 import java.util.List;
+import java.io.Serializable;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
 import org.opengis.util.FactoryException;
 import org.opengis.util.NoSuchIdentifierException;
 import org.opengis.referencing.NoSuchAuthorityCodeException;
@@ -47,6 +53,7 @@ import org.apache.sis.referencing.crs.HardCodedCRS;
 import org.apache.sis.referencing.operation.HardCodedConversions;
 import static org.apache.sis.test.Assertions.assertEqualsIgnoreMetadata;
 import static org.apache.sis.test.Assertions.assertMessageContains;
+import static org.apache.sis.test.Assertions.assertMultilinesEquals;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
 import org.opengis.referencing.ObjectDomain;
@@ -540,4 +547,29 @@ public final class CRSTest extends TestCaseWithLogs {
         IdentifiedObjectsTest.testLookupWMS();
         loggings.assertNoUnexpectedLog();
     }
+
+    /**
+     * Tests serialization of an object created from the <abbr>EPSG</abbr> 
database.
+     * This test uses {@code EPSG:3857} (Pseudo-Mercator), which should be 
available
+     * even in absence of connection to the <abbr>EPSG</abbr> database.
+     *
+     * @throws FactoryException if an error occurred while creating the 
<abbr>CRS</abbr>.
+     * @throws IOException if an error occurred during serialization or 
deserialization.
+     * @throws ClassNotFoundException if an error occurred while resolving the 
class.
+     */
+    @Test
+    public void testSerialization() throws FactoryException, IOException, 
ClassNotFoundException {
+        final CoordinateReferenceSystem crs = CRS.forCode("EPSG:3857");
+        assertInstanceOf(Serializable.class, crs);
+        final var buffer = new ByteArrayOutputStream();
+        try (var output = new ObjectOutputStream(buffer)) {
+            output.writeObject(crs);
+        }
+        final CoordinateReferenceSystem read;
+        try (var input = new ObjectInputStream(new 
ByteArrayInputStream(buffer.toByteArray()))) {
+            read = assertInstanceOf(CoordinateReferenceSystem.class, 
input.readObject());
+        }
+        // Cannot test for strict equality because serialization replaced some 
objects.
+        assertMultilinesEquals(crs.toString(), read.toString());
+    }
 }

Reply via email to