Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions doc/release-notes/11559-bbox-bigdecimal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## Bug ##

Fixed an issue where geographic bounding box coordinates with more than 5 decimal places could pass validation despite reversed order (due to single-precision float precision loss) and subsequently cause Solr indexing failures with `InvalidShapeException`. Bounding box coordinates are now parsed and compared using `BigDecimal`.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
package edu.harvard.iq.dataverse;

import edu.harvard.iq.dataverse.DatasetFieldType.FieldType;
import java.math.BigDecimal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
Expand Down Expand Up @@ -275,25 +276,39 @@ public static boolean validateBoundingBox(final String westLon, final String eas
boolean returnVal = false;

try {
Float west = verifyBoundingBoxCoordinatesWithinRange(DatasetFieldConstant.westLongitude, westLon);
Float east = verifyBoundingBoxCoordinatesWithinRange(DatasetFieldConstant.eastLongitude, eastLon);
Float north = verifyBoundingBoxCoordinatesWithinRange(DatasetFieldConstant.northLatitude, northLat);
Float south = verifyBoundingBoxCoordinatesWithinRange(DatasetFieldConstant.southLatitude, southLat);
returnVal = west <= east && south <= north;
BigDecimal west = verifyBoundingBoxCoordinatesWithinRange(DatasetFieldConstant.westLongitude, westLon);
BigDecimal east = verifyBoundingBoxCoordinatesWithinRange(DatasetFieldConstant.eastLongitude, eastLon);
BigDecimal north = verifyBoundingBoxCoordinatesWithinRange(DatasetFieldConstant.northLatitude, northLat);
BigDecimal south = verifyBoundingBoxCoordinatesWithinRange(DatasetFieldConstant.southLatitude, southLat);
// Compare with BigDecimal to avoid Float precision loss that let invalid
// high-decimal bounding boxes pass validation and then fail Solr indexing.
returnVal = west.compareTo(east) <= 0 && south.compareTo(north) <= 0;
} catch (IllegalArgumentException e) {
returnVal = false;
}

return returnVal;
}

private static Float verifyBoundingBoxCoordinatesWithinRange(final String name, final String value) throws IllegalArgumentException {
private static BigDecimal verifyBoundingBoxCoordinatesWithinRange(final String name, final String value) throws IllegalArgumentException {
int max = name.equals(DatasetFieldConstant.westLongitude) || name.equals(DatasetFieldConstant.eastLongitude) ? 180 : 90;
int min = max * -1;

final Float returnVal = value != null ? Float.parseFloat(value) : Float.NaN;
if (returnVal.isNaN() || returnVal < min || returnVal > max) {
throw new IllegalArgumentException(String.format("Value (%s) not in range (%s-%s)", returnVal.isNaN() ? "missing" : returnVal, min, max));
if (value == null) {
throw new IllegalArgumentException(String.format("Value (%s) not in range (%s-%s)", "missing", min, max));
}

final BigDecimal returnVal;
try {
returnVal = new BigDecimal(value.trim());
} catch (NumberFormatException e) {
throw new IllegalArgumentException(String.format("Value (%s) not in range (%s-%s)", value, min, max), e);
}

final BigDecimal minVal = BigDecimal.valueOf(min);
final BigDecimal maxVal = BigDecimal.valueOf(max);
if (returnVal.compareTo(minVal) < 0 || returnVal.compareTo(maxVal) > 0) {
throw new IllegalArgumentException(String.format("Value (%s) not in range (%s-%s)", returnVal, min, max));
}
return returnVal;
}
Expand Down
37 changes: 23 additions & 14 deletions src/main/java/edu/harvard/iq/dataverse/search/IndexServiceBean.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
import edu.harvard.iq.dataverse.util.SystemConfig;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
Expand Down Expand Up @@ -1373,23 +1374,31 @@ public SolrInputDocuments toSolrDocs(IndexableDataset indexableDataset, Set<Long
} else if (southLat == null) {
southLat = northLat;
}
//Find the overall bounding box that includes all bounding boxes
if(minWestLon==null || Float.parseFloat(minWestLon) > Float.parseFloat(westLon)) {
minWestLon=westLon;
}
if(maxEastLon==null || Float.parseFloat(maxEastLon) < Float.parseFloat(eastLon)) {
maxEastLon=eastLon;
}
if(minSouthLat==null || Float.parseFloat(minSouthLat) > Float.parseFloat(southLat)) {
minSouthLat=southLat;
}
if(maxNorthLat==null || Float.parseFloat(maxNorthLat) < Float.parseFloat(northLat)) {
maxNorthLat=northLat;
}

if (DatasetFieldValueValidator.validateBoundingBox(westLon, eastLon, northLat, southLat)) {
//W, E, N, S
solrInputDocument.addField(SearchFields.GEOLOCATION, "ENVELOPE(" + westLon + "," + eastLon + "," + northLat + "," + southLat + ")");

//Find the overall bounding box that includes all valid bounding boxes
try {
BigDecimal curWest = new BigDecimal(westLon.trim());
BigDecimal curEast = new BigDecimal(eastLon.trim());
BigDecimal curSouth = new BigDecimal(southLat.trim());
BigDecimal curNorth = new BigDecimal(northLat.trim());

if (minWestLon == null || new BigDecimal(minWestLon.trim()).compareTo(curWest) > 0) {
minWestLon = westLon;
}
if (maxEastLon == null || new BigDecimal(maxEastLon.trim()).compareTo(curEast) < 0) {
maxEastLon = eastLon;
Comment on lines +1388 to +1392
}
if (minSouthLat == null || new BigDecimal(minSouthLat.trim()).compareTo(curSouth) > 0) {
minSouthLat = southLat;
}
if (maxNorthLat == null || new BigDecimal(maxNorthLat.trim()).compareTo(curNorth) < 0) {
maxNorthLat = northLat;
}
} catch (NumberFormatException ignored) {
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,12 +174,33 @@ public void testBoundingBoxValidity() {
assertTrue(DatasetFieldValueValidator.validateBoundingBox("0", "0", "0", "0"));

// invalid tests
assertTrue(!DatasetFieldValueValidator.validateBoundingBox("-180", null, "90", null));
assertTrue(!DatasetFieldValueValidator.validateBoundingBox(null, "180", null, "90"));
assertTrue(!DatasetFieldValueValidator.validateBoundingBox("-180", "180", "90", "junk"));
assertTrue(!DatasetFieldValueValidator.validateBoundingBox("45", "40", "90", "0"));
assertTrue(!DatasetFieldValueValidator.validateBoundingBox("360", "0", "90", "-90"));
assertTrue(!DatasetFieldValueValidator.validateBoundingBox("", "", "", ""));
assertTrue(!DatasetFieldValueValidator.validateBoundingBox(null, null, null, null));
assertFalse(DatasetFieldValueValidator.validateBoundingBox("-180", null, "90", null));
assertFalse(DatasetFieldValueValidator.validateBoundingBox(null, "180", null, "90"));
assertFalse(DatasetFieldValueValidator.validateBoundingBox("-180", "180", "90", "junk"));
assertFalse(DatasetFieldValueValidator.validateBoundingBox("45", "40", "90", "0"));
assertFalse(DatasetFieldValueValidator.validateBoundingBox("360", "0", "90", "-90"));
assertFalse(DatasetFieldValueValidator.validateBoundingBox("", "", "", ""));
assertFalse(DatasetFieldValueValidator.validateBoundingBox(null, null, null, null));

// High-precision coordinates that reverse order must be rejected.
// Float comparison could treat nearly-equal 6+ decimal values as equal and
// incorrectly allow South>North / West>East boxes that later break Solr (#11559).
assertFalse(DatasetFieldValueValidator.validateBoundingBox("-71.116431", "-71.116430", "42.377000", "42.377001"));
assertFalse(DatasetFieldValueValidator.validateBoundingBox("-71.116430", "-71.116431", "42.377001", "42.377000"));
assertFalse(DatasetFieldValueValidator.validateBoundingBox("0.000001", "0.000000", "0.000001", "0.000000"));
assertFalse(DatasetFieldValueValidator.validateBoundingBox("0.000000", "0.000001", "0.000000", "0.000001"));

// High-precision out-of-range bounds
assertFalse(DatasetFieldValueValidator.validateBoundingBox("-180.0000001", "180", "90", "-90"));
assertFalse(DatasetFieldValueValidator.validateBoundingBox("-180", "180.0000001", "90", "-90"));
assertFalse(DatasetFieldValueValidator.validateBoundingBox("-180", "180", "90.0000001", "-90"));
assertFalse(DatasetFieldValueValidator.validateBoundingBox("-180", "180", "90", "-90.0000001"));

// Still valid with many decimals when order is correct
assertTrue(DatasetFieldValueValidator.validateBoundingBox("-71.116431", "-71.116430", "42.377001", "42.377000"));
// High-precision point bounding box (min == max)
assertTrue(DatasetFieldValueValidator.validateBoundingBox("-71.116431", "-71.116431", "42.377000", "42.377000"));
// Leading/trailing whitespace should be trimmed and accepted
assertTrue(DatasetFieldValueValidator.validateBoundingBox(" -71.116431 ", " -71.116430 ", " 42.377001 ", " 42.377000 "));
}
}
Loading