Fix Oracle uppercase column keys: return lowercase in all API responses
- Add LowerCaseKeyAdvice (ResponseBodyAdvice) to auto-convert Map keys - Add LowerCaseJdbcTemplate with overridden getColumnMapRowMapper - Update all repository/service code to use lowercase key access - Add lowerKeys utility to JsonUtil Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,53 @@
|
|||||||
|
package com.tasteby.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Primary;
|
||||||
|
import org.springframework.jdbc.core.ColumnMapRowMapper;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.jdbc.core.RowMapper;
|
||||||
|
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
|
||||||
|
|
||||||
|
import javax.sql.DataSource;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom JdbcTemplate that returns lowercase column names.
|
||||||
|
* Oracle returns UPPERCASE column names by default; this normalizes them
|
||||||
|
* so the JSON API returns snake_case keys matching the frontend expectations.
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class JdbcConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@Primary
|
||||||
|
public NamedParameterJdbcTemplate namedParameterJdbcTemplate(DataSource dataSource) {
|
||||||
|
JdbcTemplate jdbc = new LowerCaseJdbcTemplate(dataSource);
|
||||||
|
return new NamedParameterJdbcTemplate(jdbc);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@Primary
|
||||||
|
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
|
||||||
|
return new LowerCaseJdbcTemplate(dataSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JdbcTemplate subclass that uses a lowercase ColumnMapRowMapper.
|
||||||
|
*/
|
||||||
|
static class LowerCaseJdbcTemplate extends JdbcTemplate {
|
||||||
|
LowerCaseJdbcTemplate(DataSource dataSource) {
|
||||||
|
super(dataSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected RowMapper<Map<String, Object>> getColumnMapRowMapper() {
|
||||||
|
return new ColumnMapRowMapper() {
|
||||||
|
@Override
|
||||||
|
protected String getColumnKey(String columnName) {
|
||||||
|
return columnName.toLowerCase();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package com.tasteby.config;
|
||||||
|
|
||||||
|
import org.springframework.core.MethodParameter;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.converter.HttpMessageConverter;
|
||||||
|
import org.springframework.http.server.ServerHttpRequest;
|
||||||
|
import org.springframework.http.server.ServerHttpResponse;
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Automatically converts Oracle UPPERCASE map keys to lowercase in all API responses.
|
||||||
|
*/
|
||||||
|
@RestControllerAdvice(basePackages = "com.tasteby")
|
||||||
|
public class LowerCaseKeyAdvice implements ResponseBodyAdvice<Object> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
|
||||||
|
Class<? extends HttpMessageConverter<?>> selectedConverterType,
|
||||||
|
ServerHttpRequest request, ServerHttpResponse response) {
|
||||||
|
return convertKeys(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private Object convertKeys(Object obj) {
|
||||||
|
if (obj instanceof Map<?, ?> map) {
|
||||||
|
var result = new LinkedHashMap<String, Object>();
|
||||||
|
for (var entry : map.entrySet()) {
|
||||||
|
String key = entry.getKey() instanceof String s ? s.toLowerCase() : String.valueOf(entry.getKey());
|
||||||
|
result.put(key, convertKeys(entry.getValue()));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (obj instanceof List<?> list) {
|
||||||
|
return list.stream().map(this::convertKeys).toList();
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,8 +54,8 @@ public class AdminUserController {
|
|||||||
""";
|
""";
|
||||||
var rows = jdbc.queryForList(sql, new MapSqlParameterSource("u", userId));
|
var rows = jdbc.queryForList(sql, new MapSqlParameterSource("u", userId));
|
||||||
rows.forEach(r -> {
|
rows.forEach(r -> {
|
||||||
Object text = r.get("REVIEW_TEXT");
|
Object text = r.get("review_text");
|
||||||
r.put("REVIEW_TEXT", JsonUtil.readClob(text));
|
r.put("review_text", JsonUtil.readClob(text));
|
||||||
});
|
});
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,14 +30,14 @@ public class DaemonController {
|
|||||||
if (rows.isEmpty()) return Map.of();
|
if (rows.isEmpty()) return Map.of();
|
||||||
var row = rows.getFirst();
|
var row = rows.getFirst();
|
||||||
var result = new LinkedHashMap<String, Object>();
|
var result = new LinkedHashMap<String, Object>();
|
||||||
result.put("scan_enabled", toInt(row.get("SCAN_ENABLED")) == 1);
|
result.put("scan_enabled", toInt(row.get("scan_enabled")) == 1);
|
||||||
result.put("scan_interval_min", row.get("SCAN_INTERVAL_MIN"));
|
result.put("scan_interval_min", row.get("scan_interval_min"));
|
||||||
result.put("process_enabled", toInt(row.get("PROCESS_ENABLED")) == 1);
|
result.put("process_enabled", toInt(row.get("process_enabled")) == 1);
|
||||||
result.put("process_interval_min", row.get("PROCESS_INTERVAL_MIN"));
|
result.put("process_interval_min", row.get("process_interval_min"));
|
||||||
result.put("process_limit", row.get("PROCESS_LIMIT"));
|
result.put("process_limit", row.get("process_limit"));
|
||||||
result.put("last_scan_at", row.get("LAST_SCAN_AT") != null ? row.get("LAST_SCAN_AT").toString() : null);
|
result.put("last_scan_at", row.get("last_scan_at") != null ? row.get("last_scan_at").toString() : null);
|
||||||
result.put("last_process_at", row.get("LAST_PROCESS_AT") != null ? row.get("LAST_PROCESS_AT").toString() : null);
|
result.put("last_process_at", row.get("last_process_at") != null ? row.get("last_process_at").toString() : null);
|
||||||
result.put("updated_at", row.get("UPDATED_AT") != null ? row.get("UPDATED_AT").toString() : null);
|
result.put("updated_at", row.get("updated_at") != null ? row.get("updated_at").toString() : null);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -230,14 +230,14 @@ public class RestaurantRepository {
|
|||||||
var rows = namedJdbc.queryForList(
|
var rows = namedJdbc.queryForList(
|
||||||
"SELECT id FROM restaurants WHERE google_place_id = :gid",
|
"SELECT id FROM restaurants WHERE google_place_id = :gid",
|
||||||
new MapSqlParameterSource("gid", placeId));
|
new MapSqlParameterSource("gid", placeId));
|
||||||
return rows.isEmpty() ? null : (String) rows.getFirst().get("ID");
|
return rows.isEmpty() ? null : (String) rows.getFirst().get("id");
|
||||||
}
|
}
|
||||||
|
|
||||||
private String findIdByName(String name) {
|
private String findIdByName(String name) {
|
||||||
var rows = namedJdbc.queryForList(
|
var rows = namedJdbc.queryForList(
|
||||||
"SELECT id FROM restaurants WHERE name = :n",
|
"SELECT id FROM restaurants WHERE name = :n",
|
||||||
new MapSqlParameterSource("n", name));
|
new MapSqlParameterSource("n", name));
|
||||||
return rows.isEmpty() ? null : (String) rows.getFirst().get("ID");
|
return rows.isEmpty() ? null : (String) rows.getFirst().get("id");
|
||||||
}
|
}
|
||||||
|
|
||||||
private MapSqlParameterSource buildUpsertParams(Map<String, Object> data,
|
private MapSqlParameterSource buildUpsertParams(Map<String, Object> data,
|
||||||
@@ -267,7 +267,7 @@ public class RestaurantRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void attachChannels(List<Map<String, Object>> rows) {
|
private void attachChannels(List<Map<String, Object>> rows) {
|
||||||
List<String> ids = rows.stream().map(r -> (String) r.get("ID")).filter(Objects::nonNull).toList();
|
List<String> ids = rows.stream().map(r -> (String) r.get("id")).filter(Objects::nonNull).toList();
|
||||||
if (ids.isEmpty()) return;
|
if (ids.isEmpty()) return;
|
||||||
|
|
||||||
var params = new MapSqlParameterSource();
|
var params = new MapSqlParameterSource();
|
||||||
@@ -290,13 +290,13 @@ public class RestaurantRepository {
|
|||||||
.add(rs.getString("CHANNEL_NAME"));
|
.add(rs.getString("CHANNEL_NAME"));
|
||||||
});
|
});
|
||||||
for (var r : rows) {
|
for (var r : rows) {
|
||||||
String id = (String) r.get("ID");
|
String id = (String) r.get("id");
|
||||||
r.put("channels", chMap.getOrDefault(id, List.of()));
|
r.put("channels", chMap.getOrDefault(id, List.of()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void attachFoodsMentioned(List<Map<String, Object>> rows) {
|
private void attachFoodsMentioned(List<Map<String, Object>> rows) {
|
||||||
List<String> ids = rows.stream().map(r -> (String) r.get("ID")).filter(Objects::nonNull).toList();
|
List<String> ids = rows.stream().map(r -> (String) r.get("id")).filter(Objects::nonNull).toList();
|
||||||
if (ids.isEmpty()) return;
|
if (ids.isEmpty()) return;
|
||||||
|
|
||||||
var params = new MapSqlParameterSource();
|
var params = new MapSqlParameterSource();
|
||||||
@@ -323,7 +323,7 @@ public class RestaurantRepository {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
for (var r : rows) {
|
for (var r : rows) {
|
||||||
String id = (String) r.get("ID");
|
String id = (String) r.get("id");
|
||||||
List<String> all = foodsMap.getOrDefault(id, List.of());
|
List<String> all = foodsMap.getOrDefault(id, List.of());
|
||||||
r.put("foods_mentioned", all.size() > 10 ? all.subList(0, 10) : all);
|
r.put("foods_mentioned", all.size() > 10 ? all.subList(0, 10) : all);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,8 +101,8 @@ public class ReviewRepository {
|
|||||||
""";
|
""";
|
||||||
var row = jdbc.queryForMap(sql, new MapSqlParameterSource("rid", restaurantId));
|
var row = jdbc.queryForMap(sql, new MapSqlParameterSource("rid", restaurantId));
|
||||||
return Map.of(
|
return Map.of(
|
||||||
"avg_rating", row.get("AVG_RATING") != null ? ((Number) row.get("AVG_RATING")).doubleValue() : null,
|
"avg_rating", row.get("avg_rating") != null ? ((Number) row.get("avg_rating")).doubleValue() : null,
|
||||||
"review_count", ((Number) row.get("REVIEW_COUNT")).intValue()
|
"review_count", ((Number) row.get("review_count")).intValue()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,15 +31,15 @@ public class UserRepository {
|
|||||||
if (!rows.isEmpty()) {
|
if (!rows.isEmpty()) {
|
||||||
// Update last_login_at
|
// Update last_login_at
|
||||||
var row = rows.getFirst();
|
var row = rows.getFirst();
|
||||||
String userId = (String) row.get("ID");
|
String userId = (String) row.get("id");
|
||||||
jdbc.update("UPDATE tasteby_users SET last_login_at = SYSTIMESTAMP WHERE id = :id",
|
jdbc.update("UPDATE tasteby_users SET last_login_at = SYSTIMESTAMP WHERE id = :id",
|
||||||
new MapSqlParameterSource("id", userId));
|
new MapSqlParameterSource("id", userId));
|
||||||
return Map.of(
|
return Map.of(
|
||||||
"id", userId,
|
"id", userId,
|
||||||
"email", row.getOrDefault("EMAIL", ""),
|
"email", row.getOrDefault("email", ""),
|
||||||
"nickname", row.getOrDefault("NICKNAME", ""),
|
"nickname", row.getOrDefault("nickname", ""),
|
||||||
"avatar_url", row.getOrDefault("AVATAR_URL", ""),
|
"avatar_url", row.getOrDefault("avatar_url", ""),
|
||||||
"is_admin", row.get("IS_ADMIN") != null && ((Number) row.get("IS_ADMIN")).intValue() == 1
|
"is_admin", row.get("is_admin") != null && ((Number) row.get("is_admin")).intValue() == 1
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,11 +70,11 @@ public class UserRepository {
|
|||||||
if (rows.isEmpty()) return null;
|
if (rows.isEmpty()) return null;
|
||||||
var row = rows.getFirst();
|
var row = rows.getFirst();
|
||||||
return Map.of(
|
return Map.of(
|
||||||
"id", row.get("ID"),
|
"id", row.get("id"),
|
||||||
"email", row.getOrDefault("EMAIL", ""),
|
"email", row.getOrDefault("email", ""),
|
||||||
"nickname", row.getOrDefault("NICKNAME", ""),
|
"nickname", row.getOrDefault("nickname", ""),
|
||||||
"avatar_url", row.getOrDefault("AVATAR_URL", ""),
|
"avatar_url", row.getOrDefault("avatar_url", ""),
|
||||||
"is_admin", row.get("IS_ADMIN") != null && ((Number) row.get("IS_ADMIN")).intValue() == 1
|
"is_admin", row.get("is_admin") != null && ((Number) row.get("is_admin")).intValue() == 1
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -84,13 +84,13 @@ public class DaemonScheduler {
|
|||||||
if (rows.isEmpty()) return null;
|
if (rows.isEmpty()) return null;
|
||||||
var row = rows.getFirst();
|
var row = rows.getFirst();
|
||||||
return new DaemonConfig(
|
return new DaemonConfig(
|
||||||
toInt(row.get("SCAN_ENABLED")) == 1,
|
toInt(row.get("scan_enabled")) == 1,
|
||||||
toInt(row.get("SCAN_INTERVAL_MIN")),
|
toInt(row.get("scan_interval_min")),
|
||||||
toInt(row.get("PROCESS_ENABLED")) == 1,
|
toInt(row.get("process_enabled")) == 1,
|
||||||
toInt(row.get("PROCESS_INTERVAL_MIN")),
|
toInt(row.get("process_interval_min")),
|
||||||
toInt(row.get("PROCESS_LIMIT")),
|
toInt(row.get("process_limit")),
|
||||||
row.get("LAST_SCAN_AT") instanceof java.sql.Timestamp ts ? ts.toInstant() : null,
|
row.get("last_scan_at") instanceof java.sql.Timestamp ts ? ts.toInstant() : null,
|
||||||
row.get("LAST_PROCESS_AT") instanceof java.sql.Timestamp ts ? ts.toInstant() : null
|
row.get("last_process_at") instanceof java.sql.Timestamp ts ? ts.toInstant() : null
|
||||||
);
|
);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.debug("Cannot read daemon config: {}", e.getMessage());
|
log.debug("Cannot read daemon config: {}", e.getMessage());
|
||||||
|
|||||||
@@ -49,13 +49,11 @@ public class SearchService {
|
|||||||
Set<String> seen = new HashSet<>();
|
Set<String> seen = new HashSet<>();
|
||||||
var merged = new ArrayList<Map<String, Object>>();
|
var merged = new ArrayList<Map<String, Object>>();
|
||||||
for (var r : kw) {
|
for (var r : kw) {
|
||||||
String id = (String) r.get("ID");
|
String id = (String) r.get("id");
|
||||||
if (id == null) id = (String) r.get("id");
|
|
||||||
if (seen.add(id)) merged.add(r);
|
if (seen.add(id)) merged.add(r);
|
||||||
}
|
}
|
||||||
for (var r : sem) {
|
for (var r : sem) {
|
||||||
String id = (String) r.get("ID");
|
String id = (String) r.get("id");
|
||||||
if (id == null) id = (String) r.get("id");
|
|
||||||
if (seen.add(id)) merged.add(r);
|
if (seen.add(id)) merged.add(r);
|
||||||
}
|
}
|
||||||
result = merged.size() > limit ? merged.subList(0, limit) : merged;
|
result = merged.size() > limit ? merged.subList(0, limit) : merged;
|
||||||
@@ -123,11 +121,7 @@ public class SearchService {
|
|||||||
|
|
||||||
private void attachChannels(List<Map<String, Object>> rows) {
|
private void attachChannels(List<Map<String, Object>> rows) {
|
||||||
List<String> ids = rows.stream()
|
List<String> ids = rows.stream()
|
||||||
.map(r -> {
|
.map(r -> (String) r.get("id"))
|
||||||
Object id = r.get("ID");
|
|
||||||
if (id == null) id = r.get("id");
|
|
||||||
return (String) id;
|
|
||||||
})
|
|
||||||
.filter(Objects::nonNull).toList();
|
.filter(Objects::nonNull).toList();
|
||||||
if (ids.isEmpty()) return;
|
if (ids.isEmpty()) return;
|
||||||
|
|
||||||
@@ -151,8 +145,7 @@ public class SearchService {
|
|||||||
.add(rs.getString("CHANNEL_NAME"));
|
.add(rs.getString("CHANNEL_NAME"));
|
||||||
});
|
});
|
||||||
for (var r : rows) {
|
for (var r : rows) {
|
||||||
String id = (String) r.get("ID");
|
String id = (String) r.get("id");
|
||||||
if (id == null) id = (String) r.get("id");
|
|
||||||
r.put("channels", chMap.getOrDefault(id, List.of()));
|
r.put("channels", chMap.getOrDefault(id, List.of()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,8 +154,8 @@ public class YouTubeService {
|
|||||||
var ch = channelRepo.findByChannelId(channelId);
|
var ch = channelRepo.findByChannelId(channelId);
|
||||||
if (ch == null) return null;
|
if (ch == null) return null;
|
||||||
|
|
||||||
String dbId = (String) ch.get("ID");
|
String dbId = (String) ch.get("id");
|
||||||
String titleFilter = (String) ch.get("TITLE_FILTER");
|
String titleFilter = (String) ch.get("title_filter");
|
||||||
String after = full ? null : videoRepo.getLatestVideoDate(dbId);
|
String after = full ? null : videoRepo.getLatestVideoDate(dbId);
|
||||||
Set<String> existing = videoRepo.getExistingVideoIds(dbId);
|
Set<String> existing = videoRepo.getExistingVideoIds(dbId);
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,8 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
|
|
||||||
import java.io.Reader;
|
import java.io.Reader;
|
||||||
import java.sql.Clob;
|
import java.sql.Clob;
|
||||||
import java.util.Collections;
|
import java.util.*;
|
||||||
import java.util.List;
|
import java.util.stream.Collectors;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JSON utility for handling Oracle CLOB JSON fields.
|
* JSON utility for handling Oracle CLOB JSON fields.
|
||||||
@@ -58,6 +57,23 @@ public final class JsonUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert Oracle uppercase column keys to lowercase (e.g. "ID" → "id", "CUISINE_TYPE" → "cuisine_type").
|
||||||
|
*/
|
||||||
|
public static Map<String, Object> lowerKeys(Map<String, Object> row) {
|
||||||
|
if (row == null) return null;
|
||||||
|
var result = new LinkedHashMap<String, Object>(row.size());
|
||||||
|
for (var entry : row.entrySet()) {
|
||||||
|
result.put(entry.getKey().toLowerCase(), entry.getValue());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Map<String, Object>> lowerKeys(List<Map<String, Object>> rows) {
|
||||||
|
if (rows == null) return null;
|
||||||
|
return rows.stream().map(JsonUtil::lowerKeys).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
public static String toJson(Object value) {
|
public static String toJson(Object value) {
|
||||||
try {
|
try {
|
||||||
return MAPPER.writeValueAsString(value);
|
return MAPPER.writeValueAsString(value);
|
||||||
|
|||||||
Reference in New Issue
Block a user