Solve the NPE exception in geotools processing vector data format conversion

1. Background introduction

The project needs to convert shp files of vector data into geojson, which is planned to be implemented through the geotools tool.
geotools: GeoTools is an open source Java library for processing geospatial data and performing spatial analysis. It provides a wealth of GIS (Geographical Information System) functions and tools, which can process different types of geographical data including vector data, raster data, image data, etc., and supports spatial object operations, map projection and coordinate conversion, spatial query and spatial analysis. and other abilities.

2.Introduce dependencies
<dependency>
    <groupId>org.geotools</groupId>
    <artifactId>gt-shapefile</artifactId>
    <version>29.0</version>
</dependency>
<dependency>
    <groupId>org.geotools</groupId>
    <artifactId>gt-geojson</artifactId>
    <version>29.0</version>
</dependency>
3. Code example

The following code converts shp files into geojson format files:

@SneakyThrows
@Test
public void shpToGeo() {<!-- -->

    // shp file
    File file = new File("D:\test\shp\city.shp");
    Map<String, Object> map = new HashMap<>();
    map.put("url", URLs.fileToUrl(file));
    DataStore dataStore = DataStoreFinder.getDataStore(map);

    String typeName = dataStore.getTypeNames()[0];
    SimpleFeatureSource featureSource = dataStore.getFeatureSource(typeName);
    //geojson file
    File geojsonFile = new File("D:\test\shp\city.geojson");

    SimpleFeatureCollection featureCollection = featureSource.getFeatures();
    FileOutputStream geoJsonOutputStream = new FileOutputStream(geojsonFile);
    // data input
    new FeatureJSON().writeFeatureCollection(featureCollection, geoJsonOutputStream);
}
4. Error report details

Running the above code for format conversion may result in an NPE exception:
image.png

5.Cause analysis

Based on the exception information, locate the source directly (in the red box in the above picture) and check the processing logic of SystemUtils.isJavaVersionAtLeast.
The following is the processing logic and code calling link of SystemUtils.isJavaVersionAtLeast:

/**
* Determine whether the java version meets the requirements
*/
public static boolean isJavaVersionAtLeast(JavaVersion requiredVersion) {<!-- -->
    return JAVA_SPECIFICATION_VERSION_AS_ENUM.atLeast(requiredVersion);
}

public boolean atLeast(JavaVersion requiredVersion) {<!-- -->
    return this.value >= requiredVersion.value;
}

// java specified version
JAVA_SPECIFICATION_VERSION_AS_ENUM = JavaVersion.get(JAVA_SPECIFICATION_VERSION);
public static final String J AVA_SPECIFICATION_VERSION = getSystemProperty("java.specification.version");
//Read system properties
private static String getSystemProperty(String property) {<!-- -->
    try {<!-- -->
        return System.getProperty(property);
    } catch (SecurityException var2) {<!-- -->
        System.err.println("Caught a SecurityException reading the system property '" + property + "'; the SystemUtils property value will default to null.");
        return null;
    }
}

The first is to determine the values of JAVA_SPECIFICATION_VERSION_AS_ENUM and requiredVersion. JAVA_SPECIFICATION_VERSION_AS_ENUM here refers to the specified version of java, and requiredVersion refers to the Java version required to run this code. Let’s take a look at where these two values come from.
JAVA_SPECIFICATION_VERSION_AS_ENUM is an enumeration value of JavaVersion (org.apache.commons.lang3 package):

static JavaVersion get(String nom) {<!-- -->
    if ("0.9".equals(nom)) {<!-- -->
        return JAVA_0_9;
    } else if ("1.1".equals(nom)) {<!-- -->
        return JAVA_1_1;
    } else if ("1.2".equals(nom)) {<!-- -->
        return JAVA_1_2;
    } else if ("1.3".equals(nom)) {<!-- -->
        return JAVA_1_3;
    } else if ("1.4".equals(nom)) {<!-- -->
        return JAVA_1_4;
    } else if ("1.5".equals(nom)) {<!-- -->
        return JAVA_1_5;
    } else if ("1.6".equals(nom)) {<!-- -->
        return JAVA_1_6;
    } else if ("1.7".equals(nom)) {<!-- -->
        return JAVA_1_7;
    } else if ("1.8".equals(nom)) {<!-- -->
        return JAVA_1_8;
    } else if ("9".equals(nom)) {<!-- -->
        return JAVA_9;
    } else if ("10".equals(nom)) {<!-- -->
        return JAVA_10;
    } else if (nom == null) {<!-- -->
        return null;
    } else {<!-- -->
        float v = toFloatVersion(nom);
        if ((double)v - 1.0 < 1.0) {<!-- -->
            int firstComma = Math.max(nom.indexOf(46), nom.indexOf(44));
            int end = Math.max(nom.length(), nom.indexOf(44, firstComma));
            if (Float.parseFloat(nom.substring(firstComma + 1, end)) > 0.9F) {<!-- -->
                return JAVA_RECENT;
            }
        }

        return null;
    }
}

From the above code, we can see that the Java version is only judged up to Java10. The results after Java10 directly return null (this is where NPE comes from). The guess is that the version of the lang3 dependency introduced in the project is relatively old (3.7). When the version was released, jdk versions after Java 10 had not yet been released.

Let’s take a look at the value of requiredVersion. According to the error message org.geotools.util.NIOUtilities.clean, the method org.apache.commons.lang3.SystemUtils.isJavaVersionAtLeast triggered NPE. Let’s take a look at the implementation of the clean() method. logic:

public static boolean clean(ByteBuffer buffer) {<!-- -->
    if (buffer != null & amp; & amp; buffer.isDirect()) {<!-- -->
        PrivilegedAction<Boolean> action = SystemUtils.isJavaVersionAtLeast(JavaVersion.JAVA_9) ? () -> {<!-- -->
            return (new CleanupAfterJdk8(buffer)).clean();
        } : () -> {<!-- -->
            return (new CleanupPriorJdk9(buffer)).clean();
        };
        return (Boolean)AccessController.doPrivileged(action);
    } else {<!-- -->
        return true;
    }
}

SystemUtils.isJavaVersionAtLeast(JavaVersion.JAVA_9) is called in the clean() method, which means that the value of the requiredVersion parameter passed to the SystemUtils.isJavaVersionAtLeast(JavaVersion requiredVersion) method is 9.

Based on the above analysis, you can set a breakpoint at the following place to verify:
image.png
According to the above breakpoint information, it can be seen that requiredVersion9, JAVA_SPECIFICATION_VERSION_AS_ENUMnull.

Let’s make a breakpoint here and take a look:
image.png
It can be seen that JAVA_SPECIFICATION_VERSION==11 read from the system properties, which is the jdk version specified by the project, and then when the JAVA_SPECIFICATION_VERSION_AS_ENUM enumeration value is read from the following code, null is returned:

static JavaVersion get(String nom) {<!-- -->
    if ("0.9".equals(nom)) {<!-- -->
        return JAVA_0_9;
    } else if ("1.1".equals(nom)) {<!-- -->
        return JAVA_1_1;
    } else if ("1.2".equals(nom)) {<!-- -->
        return JAVA_1_2;
    } else if ("1.3".equals(nom)) {<!-- -->
        return JAVA_1_3;
    } else if ("1.4".equals(nom)) {<!-- -->
        return JAVA_1_4;
    } else if ("1.5".equals(nom)) {<!-- -->
        return JAVA_1_5;
    } else if ("1.6".equals(nom)) {<!-- -->
        return JAVA_1_6;
    } else if ("1.7".equals(nom)) {<!-- -->
        return JAVA_1_7;
    } else if ("1.8".equals(nom)) {<!-- -->
        return JAVA_1_8;
    } else if ("9".equals(nom)) {<!-- -->
        return JAVA_9;
    } else if ("10".equals(nom)) {<!-- -->
        return JAVA_10;
    } else if (nom == null) {<!-- -->
        return null;
    } else {<!-- -->
        float v = toFloatVersion(nom);
        if ((double)v - 1.0 < 1.0) {<!-- -->
            int firstComma = Math.max(nom.indexOf(46), nom.indexOf(44));
            int end = Math.max(nom.length(), nom.indexOf(44, firstComma));
            if (Float.parseFloat(nom.substring(firstComma + 1, end)) > 0.9F) {<!-- -->
                return JAVA_RECENT;
            }
        }

        return null;
    }
}
6.Solution

The cause of NPE has been analyzed based on the code call chain above. The reason is that the get() method of org.apache.commons.lang3.JavaVersion returns null. The reason is also guessed above, which should be the version of lang3 dependency introduced in the project. It is relatively old (version 3.7). When this version was released, the jdk version after Java10 had not yet been released. Then follow this idea, upgrade the version of commons-lang3, and upgrade commons-lang3 to 3.12.0

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.12.0</version>
</dependency>

After upgrading the version, check the get() method of org.apache.commons.lang3.JavaVersion:

static JavaVersion get(String versionStr) {<!-- -->
    if (versionStr == null) {<!-- -->
        return null;
    } else {<!-- -->
        switch (versionStr) {<!-- -->
            case "0.9":
                return JAVA_0_9;
            case "1.1":
                return JAVA_1_1;
            case "1.2":
                return JAVA_1_2;
            case "1.3":
                return JAVA_1_3;
            case "1.4":
                return JAVA_1_4;
            case "1.5":
                return JAVA_1_5;
            case "1.6":
                return JAVA_1_6;
            case "1.7":
                return JAVA_1_7;
            case "1.8":
                return JAVA_1_8;
            case "9":
                return JAVA_9;
            case "10":
                return JAVA_10;
             case "11":
                 return JAVA_11;
             case "12":
                 return JAVA_12;
             case "13":
                 return JAVA_13;
             case "14":
                 return JAVA_14;
             case "15":
                 return JAVA_15;
             case "16":
                 return JAVA_16;
             case "17":
                 return JAVA_17;
             default:
                 float v = toFloatVersion(versionStr);
                 if ((double)v - 1.0 < 1.0) {<!-- -->
                     int firstComma = Math.max(versionStr.indexOf(46), versionStr.indexOf(44));
                     int end = Math.max(versionStr.length(), versionStr.indexOf(44, firstComma));
                     if (Float.parseFloat(versionStr.substring(firstComma + 1, end)) > 0.9F) {<!-- -->
                         return JAVA_RECENT;
                     }
                 } else if (v > 10.0F) {<!-- -->
                     return JAVA_RECENT;
                 }

                 return null;
         }
     }
}

It can be seen from the above code that the upgraded version of the code can normally determine jdk17 and previous java versions.