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));
|
||||
rows.forEach(r -> {
|
||||
Object text = r.get("REVIEW_TEXT");
|
||||
r.put("REVIEW_TEXT", JsonUtil.readClob(text));
|
||||
Object text = r.get("review_text");
|
||||
r.put("review_text", JsonUtil.readClob(text));
|
||||
});
|
||||
return rows;
|
||||
}
|
||||
|
||||
@@ -30,14 +30,14 @@ public class DaemonController {
|
||||
if (rows.isEmpty()) return Map.of();
|
||||
var row = rows.getFirst();
|
||||
var result = new LinkedHashMap<String, Object>();
|
||||
result.put("scan_enabled", toInt(row.get("SCAN_ENABLED")) == 1);
|
||||
result.put("scan_interval_min", row.get("SCAN_INTERVAL_MIN"));
|
||||
result.put("process_enabled", toInt(row.get("PROCESS_ENABLED")) == 1);
|
||||
result.put("process_interval_min", row.get("PROCESS_INTERVAL_MIN"));
|
||||
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_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("scan_enabled", toInt(row.get("scan_enabled")) == 1);
|
||||
result.put("scan_interval_min", row.get("scan_interval_min"));
|
||||
result.put("process_enabled", toInt(row.get("process_enabled")) == 1);
|
||||
result.put("process_interval_min", row.get("process_interval_min"));
|
||||
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_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);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -230,14 +230,14 @@ public class RestaurantRepository {
|
||||
var rows = namedJdbc.queryForList(
|
||||
"SELECT id FROM restaurants WHERE google_place_id = :gid",
|
||||
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) {
|
||||
var rows = namedJdbc.queryForList(
|
||||
"SELECT id FROM restaurants WHERE name = :n",
|
||||
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,
|
||||
@@ -267,7 +267,7 @@ public class RestaurantRepository {
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
var params = new MapSqlParameterSource();
|
||||
@@ -290,13 +290,13 @@ public class RestaurantRepository {
|
||||
.add(rs.getString("CHANNEL_NAME"));
|
||||
});
|
||||
for (var r : rows) {
|
||||
String id = (String) r.get("ID");
|
||||
String id = (String) r.get("id");
|
||||
r.put("channels", chMap.getOrDefault(id, List.of()));
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
var params = new MapSqlParameterSource();
|
||||
@@ -323,7 +323,7 @@ public class RestaurantRepository {
|
||||
}
|
||||
});
|
||||
for (var r : rows) {
|
||||
String id = (String) r.get("ID");
|
||||
String id = (String) r.get("id");
|
||||
List<String> all = foodsMap.getOrDefault(id, List.of());
|
||||
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));
|
||||
return Map.of(
|
||||
"avg_rating", row.get("AVG_RATING") != null ? ((Number) row.get("AVG_RATING")).doubleValue() : null,
|
||||
"review_count", ((Number) row.get("REVIEW_COUNT")).intValue()
|
||||
"avg_rating", row.get("avg_rating") != null ? ((Number) row.get("avg_rating")).doubleValue() : null,
|
||||
"review_count", ((Number) row.get("review_count")).intValue()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,15 +31,15 @@ public class UserRepository {
|
||||
if (!rows.isEmpty()) {
|
||||
// Update last_login_at
|
||||
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",
|
||||
new MapSqlParameterSource("id", userId));
|
||||
return Map.of(
|
||||
"id", userId,
|
||||
"email", row.getOrDefault("EMAIL", ""),
|
||||
"nickname", row.getOrDefault("NICKNAME", ""),
|
||||
"avatar_url", row.getOrDefault("AVATAR_URL", ""),
|
||||
"is_admin", row.get("IS_ADMIN") != null && ((Number) row.get("IS_ADMIN")).intValue() == 1
|
||||
"email", row.getOrDefault("email", ""),
|
||||
"nickname", row.getOrDefault("nickname", ""),
|
||||
"avatar_url", row.getOrDefault("avatar_url", ""),
|
||||
"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;
|
||||
var row = rows.getFirst();
|
||||
return Map.of(
|
||||
"id", row.get("ID"),
|
||||
"email", row.getOrDefault("EMAIL", ""),
|
||||
"nickname", row.getOrDefault("NICKNAME", ""),
|
||||
"avatar_url", row.getOrDefault("AVATAR_URL", ""),
|
||||
"is_admin", row.get("IS_ADMIN") != null && ((Number) row.get("IS_ADMIN")).intValue() == 1
|
||||
"id", row.get("id"),
|
||||
"email", row.getOrDefault("email", ""),
|
||||
"nickname", row.getOrDefault("nickname", ""),
|
||||
"avatar_url", row.getOrDefault("avatar_url", ""),
|
||||
"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;
|
||||
var row = rows.getFirst();
|
||||
return new DaemonConfig(
|
||||
toInt(row.get("SCAN_ENABLED")) == 1,
|
||||
toInt(row.get("SCAN_INTERVAL_MIN")),
|
||||
toInt(row.get("PROCESS_ENABLED")) == 1,
|
||||
toInt(row.get("PROCESS_INTERVAL_MIN")),
|
||||
toInt(row.get("PROCESS_LIMIT")),
|
||||
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
|
||||
toInt(row.get("scan_enabled")) == 1,
|
||||
toInt(row.get("scan_interval_min")),
|
||||
toInt(row.get("process_enabled")) == 1,
|
||||
toInt(row.get("process_interval_min")),
|
||||
toInt(row.get("process_limit")),
|
||||
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
|
||||
);
|
||||
} catch (Exception e) {
|
||||
log.debug("Cannot read daemon config: {}", e.getMessage());
|
||||
|
||||
@@ -49,13 +49,11 @@ public class SearchService {
|
||||
Set<String> seen = new HashSet<>();
|
||||
var merged = new ArrayList<Map<String, Object>>();
|
||||
for (var r : kw) {
|
||||
String id = (String) r.get("ID");
|
||||
if (id == null) id = (String) r.get("id");
|
||||
String id = (String) r.get("id");
|
||||
if (seen.add(id)) merged.add(r);
|
||||
}
|
||||
for (var r : sem) {
|
||||
String id = (String) r.get("ID");
|
||||
if (id == null) id = (String) r.get("id");
|
||||
String id = (String) r.get("id");
|
||||
if (seen.add(id)) merged.add(r);
|
||||
}
|
||||
result = merged.size() > limit ? merged.subList(0, limit) : merged;
|
||||
@@ -123,11 +121,7 @@ public class SearchService {
|
||||
|
||||
private void attachChannels(List<Map<String, Object>> rows) {
|
||||
List<String> ids = rows.stream()
|
||||
.map(r -> {
|
||||
Object id = r.get("ID");
|
||||
if (id == null) id = r.get("id");
|
||||
return (String) id;
|
||||
})
|
||||
.map(r -> (String) r.get("id"))
|
||||
.filter(Objects::nonNull).toList();
|
||||
if (ids.isEmpty()) return;
|
||||
|
||||
@@ -151,8 +145,7 @@ public class SearchService {
|
||||
.add(rs.getString("CHANNEL_NAME"));
|
||||
});
|
||||
for (var r : rows) {
|
||||
String id = (String) r.get("ID");
|
||||
if (id == null) id = (String) r.get("id");
|
||||
String id = (String) r.get("id");
|
||||
r.put("channels", chMap.getOrDefault(id, List.of()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,8 +154,8 @@ public class YouTubeService {
|
||||
var ch = channelRepo.findByChannelId(channelId);
|
||||
if (ch == null) return null;
|
||||
|
||||
String dbId = (String) ch.get("ID");
|
||||
String titleFilter = (String) ch.get("TITLE_FILTER");
|
||||
String dbId = (String) ch.get("id");
|
||||
String titleFilter = (String) ch.get("title_filter");
|
||||
String after = full ? null : videoRepo.getLatestVideoDate(dbId);
|
||||
Set<String> existing = videoRepo.getExistingVideoIds(dbId);
|
||||
|
||||
|
||||
@@ -6,9 +6,8 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import java.io.Reader;
|
||||
import java.sql.Clob;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
try {
|
||||
return MAPPER.writeValueAsString(value);
|
||||
|
||||
Reference in New Issue
Block a user