From a2b134f050e2895c51bab89005a99ef25eadd892 Mon Sep 17 00:00:00 2001 From: Marie Harris Date: Wed, 11 Mar 2026 06:27:37 -0400 Subject: [PATCH 1/4] fix: make location lookups case-insensitive and trim whitespace (#26) - lookup(): use COLLATE NOCASE so "montgomery" and "MONTGOMERY" both match "Montgomery" without requiring exact case from callers - lookup(): trim leading/trailing whitespace from all filter values before querying, so "Montgomery " works as well as "Montgomery" - lookupFuzzy(): new method for prefix/abbreviation matching; strips trailing periods then appends % for LIKE COLLATE NOCASE, so "Mont." matches both "Montgomery" and "Montour" - Extract shared JDBC execution into private lookupWithOperator() to eliminate duplication between lookup() and lookupFuzzy() - Both changes applied identically to library-api and builder-api Tests: - Add LocationServiceTest (11 tests) covering exact match, case insensitivity, whitespace trimming, fuzzy prefix, fuzzy abbreviation, and empty-result cases - Add src/test/resources/application.properties with quarkus.http.test-port=0 so tests don't conflict with running services on port 8081 (fixes pre-existing issue where all 14 existing tests were being skipped in local dev) Co-Authored-By: Claude Sonnet 4.6 --- .../org/acme/functions/LocationService.java | 24 +++- .../bdt/functions/LocationService.java | 28 ++++- .../bdt/functions/LocationServiceTest.java | 107 ++++++++++++++++++ .../src/test/resources/application.properties | 2 + 4 files changed, 153 insertions(+), 8 deletions(-) create mode 100644 library-api/src/test/java/org/codeforphilly/bdt/functions/LocationServiceTest.java create mode 100644 library-api/src/test/resources/application.properties diff --git a/builder-api/src/main/java/org/acme/functions/LocationService.java b/builder-api/src/main/java/org/acme/functions/LocationService.java index e54a3dce..4f6acd79 100644 --- a/builder-api/src/main/java/org/acme/functions/LocationService.java +++ b/builder-api/src/main/java/org/acme/functions/LocationService.java @@ -25,7 +25,26 @@ public Connection getDbConnection() throws SQLException { return dataSource.getConnection(); } + /** + * Like {@link #lookup}, but matches filter values as case-insensitive prefix patterns. + * Trailing periods are stripped so abbreviations like "Mont." match "Montgomery". + */ + public static List lookupFuzzy(String column, Map filters) { + Map normalised = new java.util.LinkedHashMap<>(); + for (Map.Entry entry : filters.entrySet()) { + String raw = entry.getValue().toString().trim(); + // Strip trailing period so "Mont." becomes "Mont%" for LIKE matching + String pattern = raw.endsWith(".") ? raw.substring(0, raw.length() - 1) + "%" : raw + "%"; + normalised.put(entry.getKey(), pattern); + } + return lookupWithOperator(column, normalised, "LIKE ? COLLATE NOCASE"); + } + public static List lookup(String column, Map filters) { + return lookupWithOperator(column, filters, "= ? COLLATE NOCASE"); + } + + private static List lookupWithOperator(String column, Map filters, String operator) { List results = new ArrayList<>(); StringBuilder sql = new StringBuilder("SELECT DISTINCT ").append(column).append(" FROM locations WHERE "); @@ -35,7 +54,7 @@ public static List lookup(String column, Map filters) { if (!first) { sql.append(" AND "); } - sql.append(key).append(" = ?"); + sql.append(key).append(" ").append(operator); first = false; } @@ -52,7 +71,7 @@ public static List lookup(String column, Map filters) { // Set the values dynamically int index = 1; for (Object value : filters.values()) { - String stringValue = value.toString(); // db only has strings + String stringValue = value.toString().trim(); // db only has strings pstmt.setString(index++, stringValue); } @@ -81,6 +100,5 @@ public static List lookup(String column, Map filters) { } return results; - } } diff --git a/library-api/src/main/java/org/codeforphilly/bdt/functions/LocationService.java b/library-api/src/main/java/org/codeforphilly/bdt/functions/LocationService.java index 11c49d44..f82489b4 100644 --- a/library-api/src/main/java/org/codeforphilly/bdt/functions/LocationService.java +++ b/library-api/src/main/java/org/codeforphilly/bdt/functions/LocationService.java @@ -24,7 +24,26 @@ public Connection getDbConnection() throws SQLException { return dataSource.getConnection(); } + /** + * Like {@link #lookup}, but matches filter values as case-insensitive prefix patterns. + * Trailing periods are stripped so abbreviations like "Mont." match "Montgomery". + */ + public static List lookupFuzzy(String column, Map filters) { + Map normalised = new java.util.LinkedHashMap<>(); + for (Map.Entry entry : filters.entrySet()) { + String raw = entry.getValue().toString().trim(); + // Strip trailing period so "Mont." becomes "Mont%" for LIKE matching + String pattern = raw.endsWith(".") ? raw.substring(0, raw.length() - 1) + "%" : raw + "%"; + normalised.put(entry.getKey(), pattern); + } + return lookupWithOperator(column, normalised, "LIKE ? COLLATE NOCASE"); + } + public static List lookup(String column, Map filters) { + return lookupWithOperator(column, filters, "= ? COLLATE NOCASE"); + } + + private static List lookupWithOperator(String column, Map filters, String operator) { List results = new ArrayList<>(); StringBuilder sql = new StringBuilder("SELECT DISTINCT ").append(column).append(" FROM locations WHERE "); @@ -34,7 +53,7 @@ public static List lookup(String column, Map filters) { if (!first) { sql.append(" AND "); } - sql.append(key).append(" = ?"); + sql.append(key).append(" ").append(operator); first = false; } @@ -44,14 +63,14 @@ public static List lookup(String column, Map filters) { PreparedStatement pstmt = null; ResultSet rs = null; - try { + try { connection = service.getDbConnection(); pstmt = connection.prepareStatement(sql.toString()); // Set the values dynamically int index = 1; for (Object value : filters.values()) { - String stringValue = value.toString(); // db only has strings + String stringValue = value.toString().trim(); // db only has strings pstmt.setString(index++, stringValue); } @@ -80,6 +99,5 @@ public static List lookup(String column, Map filters) { } return results; - - } + } } \ No newline at end of file diff --git a/library-api/src/test/java/org/codeforphilly/bdt/functions/LocationServiceTest.java b/library-api/src/test/java/org/codeforphilly/bdt/functions/LocationServiceTest.java new file mode 100644 index 00000000..de56392c --- /dev/null +++ b/library-api/src/test/java/org/codeforphilly/bdt/functions/LocationServiceTest.java @@ -0,0 +1,107 @@ +package org.codeforphilly.bdt.functions; + +import io.quarkus.test.junit.QuarkusTest; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +@QuarkusTest +public class LocationServiceTest { + + // --- lookup() --- + + @Test + public void testLookupExactMatch() { + List results = LocationService.lookup("countyName", Map.of("zipCode", "19107")); + assertFalse(results.isEmpty(), "Should find a county for zip 19107"); + assertTrue(results.contains("Philadelphia")); + } + + @Test + public void testLookupCaseInsensitiveFilterValue() { + List uppercase = LocationService.lookup("zipCode", Map.of("stateAbbreviation", "PA", "countyName", "Philadelphia")); + List lowercase = LocationService.lookup("zipCode", Map.of("stateAbbreviation", "pa", "countyName", "philadelphia")); + assertFalse(uppercase.isEmpty(), "Exact case should return results"); + assertEquals(uppercase.size(), lowercase.size(), + "Lowercase filter values should return same number of results as title-case"); + assertTrue(lowercase.containsAll(uppercase)); + } + + @Test + public void testLookupCaseInsensitiveMixedCase() { + List titleCase = LocationService.lookup("zipCode", Map.of("stateAbbreviation", "PA", "countyName", "Philadelphia")); + List upperCase = LocationService.lookup("zipCode", Map.of("stateAbbreviation", "PA", "countyName", "PHILADELPHIA")); + assertEquals(titleCase.size(), upperCase.size(), + "UPPERCASE filter value should return same results as title-case"); + } + + @Test + public void testLookupTrimsLeadingWhitespace() { + List trimmed = LocationService.lookup("countyName", Map.of("zipCode", "19107")); + List withSpace = LocationService.lookup("countyName", Map.of("zipCode", " 19107")); + assertEquals(trimmed, withSpace, "Leading whitespace on filter value should be trimmed"); + } + + @Test + public void testLookupTrimsTrailingWhitespace() { + List trimmed = LocationService.lookup("countyName", Map.of("zipCode", "19107")); + List withSpace = LocationService.lookup("countyName", Map.of("zipCode", "19107 ")); + assertEquals(trimmed, withSpace, "Trailing whitespace on filter value should be trimmed"); + } + + @Test + public void testLookupReturnsEmptyForNoMatch() { + List results = LocationService.lookup("countyName", Map.of("stateAbbreviation", "ZZ")); + assertTrue(results.isEmpty(), "Unknown state abbreviation should return an empty list"); + } + + // --- lookupFuzzy() --- + + @Test + public void testLookupFuzzyAbbreviationWithPeriod() { + // "Mont." should match both "Montgomery" and "Montour" in PA + List results = LocationService.lookupFuzzy("countyName", + Map.of("countyName", "Mont.", "stateAbbreviation", "PA")); + assertFalse(results.isEmpty(), "Abbreviated 'Mont.' should match counties starting with 'Mont'"); + assertTrue(results.contains("Montgomery"), + "Expected 'Montgomery' in fuzzy results for 'Mont.'"); + } + + @Test + public void testLookupFuzzyPrefixNoTrailingPeriod() { + // "Los" should match "Los Angeles" and other "Los ..." counties + List results = LocationService.lookupFuzzy("countyName", + Map.of("countyName", "Los")); + assertFalse(results.isEmpty(), "Prefix 'Los' should match counties starting with 'Los'"); + assertTrue(results.stream().allMatch(name -> name.toLowerCase().startsWith("los")), + "All results should start with 'Los'"); + } + + @Test + public void testLookupFuzzyIsCaseInsensitive() { + List upper = LocationService.lookupFuzzy("countyName", + Map.of("countyName", "PHILA", "stateAbbreviation", "PA")); + List lower = LocationService.lookupFuzzy("countyName", + Map.of("countyName", "phila", "stateAbbreviation", "PA")); + assertEquals(upper.size(), lower.size(), + "Fuzzy lookup should be case-insensitive"); + assertTrue(lower.containsAll(upper)); + } + + @Test + public void testLookupFuzzyTrimsWhitespace() { + List trimmed = LocationService.lookupFuzzy("countyName", Map.of("countyName", "Phila", "stateAbbreviation", "PA")); + List withSpace = LocationService.lookupFuzzy("countyName", Map.of("countyName", " Phila ", "stateAbbreviation", "PA")); + assertEquals(trimmed, withSpace, "Fuzzy lookup should trim whitespace before matching"); + } + + @Test + public void testLookupFuzzyReturnsEmptyForNoMatch() { + List results = LocationService.lookupFuzzy("countyName", + Map.of("countyName", "ZZZZZ")); + assertTrue(results.isEmpty(), "Fuzzy lookup should return empty list when nothing matches"); + } +} diff --git a/library-api/src/test/resources/application.properties b/library-api/src/test/resources/application.properties new file mode 100644 index 00000000..b212bca1 --- /dev/null +++ b/library-api/src/test/resources/application.properties @@ -0,0 +1,2 @@ +# Use a random port so tests don't conflict with other running services +quarkus.http.test-port=0 From a14011fb7bcb591bffd5bf99c532b4eb284460d8 Mon Sep 17 00:00:00 2001 From: Marie Harris Date: Wed, 11 Mar 2026 06:32:27 -0400 Subject: [PATCH 2/4] refactor: apply Zen of Python principles to LocationService Errors should never pass silently: - Replace e.printStackTrace() with a proper Logger and re-throw as RuntimeException so callers can distinguish "no results" from "query failed". Silent swallowing made DB failures invisible. Flat is better than nested: - Replace try/finally/null-guard pattern with try-with-resources. Connection, PreparedStatement, and ResultSet are all AutoCloseable; the old 3-level nesting with manual null checks is now a single try-with-resources block. Explicit is better than implicit / Readability counts: - Rename lookupWithOperator -> query; the old name described the mechanism rather than the intent. - Move COLLATE NOCASE out of the operator string and into the SQL builder so the operator param is a plain SQL operator ("=" or "LIKE"), not an opaque fragment embedding a JDBC placeholder. - Rename normalised -> patterns in lookupFuzzy; the variable holds LIKE patterns, not normalised data. Beautiful is better than ugly: - Replace inline java.util.LinkedHashMap with a proper import. - Add Logger as a named constant rather than inline creation. Co-Authored-By: Claude Sonnet 4.6 --- .../org/acme/functions/LocationService.java | 63 +++++++----------- .../bdt/functions/LocationService.java | 65 +++++++------------ 2 files changed, 45 insertions(+), 83 deletions(-) diff --git a/builder-api/src/main/java/org/acme/functions/LocationService.java b/builder-api/src/main/java/org/acme/functions/LocationService.java index 4f6acd79..8eabc993 100644 --- a/builder-api/src/main/java/org/acme/functions/LocationService.java +++ b/builder-api/src/main/java/org/acme/functions/LocationService.java @@ -11,13 +11,17 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.logging.Logger; @Unremovable @ApplicationScoped public class LocationService { + private static final Logger LOG = Logger.getLogger(LocationService.class.getName()); + @Inject AgroalDataSource dataSource; @@ -30,73 +34,50 @@ public Connection getDbConnection() throws SQLException { * Trailing periods are stripped so abbreviations like "Mont." match "Montgomery". */ public static List lookupFuzzy(String column, Map filters) { - Map normalised = new java.util.LinkedHashMap<>(); + Map patterns = new LinkedHashMap<>(); for (Map.Entry entry : filters.entrySet()) { String raw = entry.getValue().toString().trim(); // Strip trailing period so "Mont." becomes "Mont%" for LIKE matching String pattern = raw.endsWith(".") ? raw.substring(0, raw.length() - 1) + "%" : raw + "%"; - normalised.put(entry.getKey(), pattern); + patterns.put(entry.getKey(), pattern); } - return lookupWithOperator(column, normalised, "LIKE ? COLLATE NOCASE"); + return query(column, patterns, "LIKE"); } public static List lookup(String column, Map filters) { - return lookupWithOperator(column, filters, "= ? COLLATE NOCASE"); + return query(column, filters, "="); } - private static List lookupWithOperator(String column, Map filters, String operator) { + private static List query(String column, Map filters, String operator) { List results = new ArrayList<>(); - StringBuilder sql = new StringBuilder("SELECT DISTINCT ").append(column).append(" FROM locations WHERE "); - // construct WHERE clause; assume everything is ANDed together + StringBuilder sql = new StringBuilder("SELECT DISTINCT ").append(column).append(" FROM locations WHERE "); boolean first = true; for (String key : filters.keySet()) { - if (!first) { - sql.append(" AND "); - } - sql.append(key).append(" ").append(operator); + if (!first) sql.append(" AND "); + sql.append(key).append(" ").append(operator).append(" ? COLLATE NOCASE"); first = false; } LocationService service = Arc.container().instance(LocationService.class).get(); - Connection connection = null; - PreparedStatement pstmt = null; - ResultSet rs = null; - - try { - connection = service.getDbConnection(); - pstmt = connection.prepareStatement(sql.toString()); + try (Connection connection = service.getDbConnection(); + PreparedStatement pstmt = connection.prepareStatement(sql.toString())) { - // Set the values dynamically int index = 1; for (Object value : filters.values()) { - String stringValue = value.toString().trim(); // db only has strings - pstmt.setString(index++, stringValue); + pstmt.setString(index++, value.toString().trim()); } - rs = pstmt.executeQuery(); - - while(rs.next()) { - String thisString = rs.getString(column); - results.add(thisString); - } - } catch (SQLException e) { - e.printStackTrace(); - } finally { - try { - if (rs != null) { - rs.close(); - } - if (pstmt != null) { - pstmt.close(); + try (ResultSet rs = pstmt.executeQuery()) { + while (rs.next()) { + results.add(rs.getString(column)); } - if (connection != null) { - connection.close(); - } - } catch (SQLException e) { - e.printStackTrace(); } + + } catch (SQLException e) { + LOG.severe("Location lookup failed for column=" + column + " filters=" + filters + ": " + e.getMessage()); + throw new RuntimeException("Location lookup failed", e); } return results; diff --git a/library-api/src/main/java/org/codeforphilly/bdt/functions/LocationService.java b/library-api/src/main/java/org/codeforphilly/bdt/functions/LocationService.java index f82489b4..a496ef04 100644 --- a/library-api/src/main/java/org/codeforphilly/bdt/functions/LocationService.java +++ b/library-api/src/main/java/org/codeforphilly/bdt/functions/LocationService.java @@ -10,13 +10,17 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.logging.Logger; @Unremovable @ApplicationScoped public class LocationService { + private static final Logger LOG = Logger.getLogger(LocationService.class.getName()); + @Inject AgroalDataSource dataSource; @@ -29,75 +33,52 @@ public Connection getDbConnection() throws SQLException { * Trailing periods are stripped so abbreviations like "Mont." match "Montgomery". */ public static List lookupFuzzy(String column, Map filters) { - Map normalised = new java.util.LinkedHashMap<>(); + Map patterns = new LinkedHashMap<>(); for (Map.Entry entry : filters.entrySet()) { String raw = entry.getValue().toString().trim(); // Strip trailing period so "Mont." becomes "Mont%" for LIKE matching String pattern = raw.endsWith(".") ? raw.substring(0, raw.length() - 1) + "%" : raw + "%"; - normalised.put(entry.getKey(), pattern); + patterns.put(entry.getKey(), pattern); } - return lookupWithOperator(column, normalised, "LIKE ? COLLATE NOCASE"); + return query(column, patterns, "LIKE"); } public static List lookup(String column, Map filters) { - return lookupWithOperator(column, filters, "= ? COLLATE NOCASE"); + return query(column, filters, "="); } - private static List lookupWithOperator(String column, Map filters, String operator) { + private static List query(String column, Map filters, String operator) { List results = new ArrayList<>(); - StringBuilder sql = new StringBuilder("SELECT DISTINCT ").append(column).append(" FROM locations WHERE "); - // construct WHERE clause; assume everything is ANDed together + StringBuilder sql = new StringBuilder("SELECT DISTINCT ").append(column).append(" FROM locations WHERE "); boolean first = true; for (String key : filters.keySet()) { - if (!first) { - sql.append(" AND "); - } - sql.append(key).append(" ").append(operator); + if (!first) sql.append(" AND "); + sql.append(key).append(" ").append(operator).append(" ? COLLATE NOCASE"); first = false; } LocationService service = Arc.container().instance(LocationService.class).get(); - Connection connection = null; - PreparedStatement pstmt = null; - ResultSet rs = null; - - try { - connection = service.getDbConnection(); - pstmt = connection.prepareStatement(sql.toString()); + try (Connection connection = service.getDbConnection(); + PreparedStatement pstmt = connection.prepareStatement(sql.toString())) { - // Set the values dynamically int index = 1; for (Object value : filters.values()) { - String stringValue = value.toString().trim(); // db only has strings - pstmt.setString(index++, stringValue); + pstmt.setString(index++, value.toString().trim()); } - rs = pstmt.executeQuery(); - - while(rs.next()) { - String thisString = rs.getString(column); - results.add(thisString); - } - } catch (SQLException e) { - e.printStackTrace(); - } finally { - try { - if (rs != null) { - rs.close(); - } - if (pstmt != null) { - pstmt.close(); + try (ResultSet rs = pstmt.executeQuery()) { + while (rs.next()) { + results.add(rs.getString(column)); } - if (connection != null) { - connection.close(); - } - } catch (SQLException e) { - e.printStackTrace(); } + + } catch (SQLException e) { + LOG.severe("Location lookup failed for column=" + column + " filters=" + filters + ": " + e.getMessage()); + throw new RuntimeException("Location lookup failed", e); } return results; } -} \ No newline at end of file +} From 3633b4f6b001a8395ba651d57125a2ea06d6bcfa Mon Sep 17 00:00:00 2001 From: Marie Harris Date: Wed, 11 Mar 2026 06:36:04 -0400 Subject: [PATCH 3/4] fix: guard against SQL injection in LocationService column and key args Filter values were already parameterized, but column names and filter keys were interpolated directly into the SQL string. Validate both against an allowlist of known locations-table columns before building the query. Co-Authored-By: Claude Sonnet 4.6 --- .../java/org/acme/functions/LocationService.java | 13 +++++++++++++ .../bdt/functions/LocationService.java | 13 +++++++++++++ 2 files changed, 26 insertions(+) diff --git a/builder-api/src/main/java/org/acme/functions/LocationService.java b/builder-api/src/main/java/org/acme/functions/LocationService.java index 8eabc993..4bc0dbf6 100644 --- a/builder-api/src/main/java/org/acme/functions/LocationService.java +++ b/builder-api/src/main/java/org/acme/functions/LocationService.java @@ -48,7 +48,20 @@ public static List lookup(String column, Map filters) { return query(column, filters, "="); } + private static final java.util.Set ALLOWED_COLUMNS = java.util.Set.of( + "zipCode", "countyName", "stateAbbreviation", "stateName" + ); + private static List query(String column, Map filters, String operator) { + if (!ALLOWED_COLUMNS.contains(column)) { + throw new IllegalArgumentException("Invalid column: " + column); + } + for (String key : filters.keySet()) { + if (!ALLOWED_COLUMNS.contains(key)) { + throw new IllegalArgumentException("Invalid filter key: " + key); + } + } + List results = new ArrayList<>(); StringBuilder sql = new StringBuilder("SELECT DISTINCT ").append(column).append(" FROM locations WHERE "); diff --git a/library-api/src/main/java/org/codeforphilly/bdt/functions/LocationService.java b/library-api/src/main/java/org/codeforphilly/bdt/functions/LocationService.java index a496ef04..3a548aa5 100644 --- a/library-api/src/main/java/org/codeforphilly/bdt/functions/LocationService.java +++ b/library-api/src/main/java/org/codeforphilly/bdt/functions/LocationService.java @@ -47,7 +47,20 @@ public static List lookup(String column, Map filters) { return query(column, filters, "="); } + private static final java.util.Set ALLOWED_COLUMNS = java.util.Set.of( + "zipCode", "countyName", "stateAbbreviation", "stateName" + ); + private static List query(String column, Map filters, String operator) { + if (!ALLOWED_COLUMNS.contains(column)) { + throw new IllegalArgumentException("Invalid column: " + column); + } + for (String key : filters.keySet()) { + if (!ALLOWED_COLUMNS.contains(key)) { + throw new IllegalArgumentException("Invalid filter key: " + key); + } + } + List results = new ArrayList<>(); StringBuilder sql = new StringBuilder("SELECT DISTINCT ").append(column).append(" FROM locations WHERE "); From e34f756887cc6fd57ede233dee528785da3d48c3 Mon Sep 17 00:00:00 2001 From: Marie Harris Date: Wed, 11 Mar 2026 06:39:51 -0400 Subject: [PATCH 4/4] refactor: clean up LocationService after code review - Fix ALLOWED_COLUMNS: remove non-existent 'stateName', add 'countyFips' which is a real column in the locations schema - Guard against empty filters before building SQL (would produce invalid syntax and a cryptic SQLException) - Move ALLOWED_COLUMNS constant to top of class per Java convention - Import Set properly instead of inlining java.util.Set - Make getDbConnection() package-private (only called via Arc handle within the same class; not part of the public API) - Remove redundant inline comment in lookupFuzzy (covered by Javadoc) Co-Authored-By: Claude Sonnet 4.6 --- .../java/org/acme/functions/LocationService.java | 14 ++++++++------ .../bdt/functions/LocationService.java | 14 ++++++++------ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/builder-api/src/main/java/org/acme/functions/LocationService.java b/builder-api/src/main/java/org/acme/functions/LocationService.java index 4bc0dbf6..e9098f86 100644 --- a/builder-api/src/main/java/org/acme/functions/LocationService.java +++ b/builder-api/src/main/java/org/acme/functions/LocationService.java @@ -14,6 +14,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.logging.Logger; @Unremovable @@ -21,11 +22,14 @@ public class LocationService { private static final Logger LOG = Logger.getLogger(LocationService.class.getName()); + private static final Set ALLOWED_COLUMNS = Set.of( + "zipCode", "countyName", "countyFips", "stateAbbreviation" + ); @Inject AgroalDataSource dataSource; - public Connection getDbConnection() throws SQLException { + Connection getDbConnection() throws SQLException { return dataSource.getConnection(); } @@ -37,7 +41,6 @@ public static List lookupFuzzy(String column, Map filter Map patterns = new LinkedHashMap<>(); for (Map.Entry entry : filters.entrySet()) { String raw = entry.getValue().toString().trim(); - // Strip trailing period so "Mont." becomes "Mont%" for LIKE matching String pattern = raw.endsWith(".") ? raw.substring(0, raw.length() - 1) + "%" : raw + "%"; patterns.put(entry.getKey(), pattern); } @@ -48,14 +51,13 @@ public static List lookup(String column, Map filters) { return query(column, filters, "="); } - private static final java.util.Set ALLOWED_COLUMNS = java.util.Set.of( - "zipCode", "countyName", "stateAbbreviation", "stateName" - ); - private static List query(String column, Map filters, String operator) { if (!ALLOWED_COLUMNS.contains(column)) { throw new IllegalArgumentException("Invalid column: " + column); } + if (filters.isEmpty()) { + throw new IllegalArgumentException("filters must not be empty"); + } for (String key : filters.keySet()) { if (!ALLOWED_COLUMNS.contains(key)) { throw new IllegalArgumentException("Invalid filter key: " + key); diff --git a/library-api/src/main/java/org/codeforphilly/bdt/functions/LocationService.java b/library-api/src/main/java/org/codeforphilly/bdt/functions/LocationService.java index 3a548aa5..971a3052 100644 --- a/library-api/src/main/java/org/codeforphilly/bdt/functions/LocationService.java +++ b/library-api/src/main/java/org/codeforphilly/bdt/functions/LocationService.java @@ -13,6 +13,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.logging.Logger; @Unremovable @@ -20,11 +21,14 @@ public class LocationService { private static final Logger LOG = Logger.getLogger(LocationService.class.getName()); + private static final Set ALLOWED_COLUMNS = Set.of( + "zipCode", "countyName", "countyFips", "stateAbbreviation" + ); @Inject AgroalDataSource dataSource; - public Connection getDbConnection() throws SQLException { + Connection getDbConnection() throws SQLException { return dataSource.getConnection(); } @@ -36,7 +40,6 @@ public static List lookupFuzzy(String column, Map filter Map patterns = new LinkedHashMap<>(); for (Map.Entry entry : filters.entrySet()) { String raw = entry.getValue().toString().trim(); - // Strip trailing period so "Mont." becomes "Mont%" for LIKE matching String pattern = raw.endsWith(".") ? raw.substring(0, raw.length() - 1) + "%" : raw + "%"; patterns.put(entry.getKey(), pattern); } @@ -47,14 +50,13 @@ public static List lookup(String column, Map filters) { return query(column, filters, "="); } - private static final java.util.Set ALLOWED_COLUMNS = java.util.Set.of( - "zipCode", "countyName", "stateAbbreviation", "stateName" - ); - private static List query(String column, Map filters, String operator) { if (!ALLOWED_COLUMNS.contains(column)) { throw new IllegalArgumentException("Invalid column: " + column); } + if (filters.isEmpty()) { + throw new IllegalArgumentException("filters must not be empty"); + } for (String key : filters.keySet()) { if (!ALLOWED_COLUMNS.contains(key)) { throw new IllegalArgumentException("Invalid filter key: " + key);