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()); + } }
