refs #702: externalize HMM structured data catalog
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.masking;
|
||||
|
||||
public record ManagedMaskingPolicy(
|
||||
String objectName,
|
||||
String policyName
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.structured;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record StructuredDataCatalog(
|
||||
String sourceName,
|
||||
String owner,
|
||||
String pageHelp,
|
||||
String catalogDescription,
|
||||
int rowLimit,
|
||||
List<StructuredDataTable> tables
|
||||
) {
|
||||
}
|
||||
@@ -1,9 +1,17 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.structured;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record StructuredDataTable(
|
||||
String key,
|
||||
String tableName,
|
||||
String businessName,
|
||||
String description
|
||||
String description,
|
||||
List<String> previewColumns,
|
||||
String maskingPolicyName
|
||||
) {
|
||||
|
||||
public StructuredDataTable(String key, String tableName, String businessName, String description) {
|
||||
this(key, tableName, businessName, description, List.of(), null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.cloudhandson.vpdbackoffice.domain.masking.ColumnMaskingRule;
|
||||
import com.cloudhandson.vpdbackoffice.domain.masking.MaskingRule;
|
||||
import com.cloudhandson.vpdbackoffice.domain.masking.MaskingRuleCreateCommand;
|
||||
import com.cloudhandson.vpdbackoffice.domain.masking.MaskingPolicyStatus;
|
||||
import com.cloudhandson.vpdbackoffice.domain.masking.ManagedMaskingPolicy;
|
||||
import com.cloudhandson.vpdbackoffice.domain.masking.UserMaskingRule;
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
@@ -28,7 +29,10 @@ public interface MaskingRuleMapper {
|
||||
|
||||
List<ColumnMaskingRule> findColumnRules();
|
||||
|
||||
List<MaskingPolicyStatus> findPolicyStatuses();
|
||||
List<MaskingPolicyStatus> findPolicyStatuses(
|
||||
@Param("owner") String owner,
|
||||
@Param("policies") List<ManagedMaskingPolicy> policies
|
||||
);
|
||||
|
||||
ColumnMaskingRule findColumnRule(@Param("columnId") long columnId);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.cloudhandson.vpdbackoffice.service;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.domain.masking.ColumnMaskingRule;
|
||||
import com.cloudhandson.vpdbackoffice.domain.masking.MaskingTemplate;
|
||||
import com.cloudhandson.vpdbackoffice.domain.structured.StructuredDataCatalog;
|
||||
import com.cloudhandson.vpdbackoffice.mapper.MaskingRuleMapper;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
@@ -24,37 +25,42 @@ import org.springframework.stereotype.Service;
|
||||
@Service
|
||||
public class MaskingPolicySynchronizer {
|
||||
|
||||
private static final String OWNER = "POC_2";
|
||||
private static final Pattern COLUMN_NAME = Pattern.compile("[A-Z][A-Z0-9_$#]{0,127}");
|
||||
private static final Map<String, String> MANAGED_POLICIES = managedPolicyMap();
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final MaskingRuleMapper mapper;
|
||||
private final StructuredDataCatalog catalog;
|
||||
private final Map<String, String> managedPolicies;
|
||||
|
||||
public MaskingPolicySynchronizer(JdbcTemplate jdbcTemplate, MaskingRuleMapper mapper) {
|
||||
public MaskingPolicySynchronizer(
|
||||
JdbcTemplate jdbcTemplate,
|
||||
MaskingRuleMapper mapper,
|
||||
StructuredDataCatalogProvider catalogProvider
|
||||
) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
private static Map<String, String> managedPolicyMap() {
|
||||
this.catalog = catalogProvider.catalog();
|
||||
Map<String, String> policies = new LinkedHashMap<>();
|
||||
policies.put("KB_CUSTOMERS", "KB_CUSTOMER_PII_REDACT");
|
||||
policies.put("KB_CLAIMS", "KB_CLAIM_AMOUNT_REDACT");
|
||||
policies.put("KB_CONTRACTS", "KB_CONTRACT_PREMIUM_REDACT");
|
||||
policies.put("KB_EXTERNAL_HOLDINGS", "KB_EXT_HOLDING_REDACT");
|
||||
return Collections.unmodifiableMap(policies);
|
||||
catalog.tables().stream()
|
||||
.filter(table -> table.maskingPolicyName() != null)
|
||||
.forEach(table -> policies.put(table.tableName(), table.maskingPolicyName()));
|
||||
this.managedPolicies = Collections.unmodifiableMap(policies);
|
||||
}
|
||||
|
||||
public Set<String> managedObjectNames() {
|
||||
return MANAGED_POLICIES.keySet();
|
||||
return managedPolicies.keySet();
|
||||
}
|
||||
|
||||
public String owner() {
|
||||
return catalog.owner();
|
||||
}
|
||||
|
||||
public boolean isManagedObject(String objectName) {
|
||||
return objectName != null && MANAGED_POLICIES.containsKey(objectName.trim().toUpperCase(Locale.ROOT));
|
||||
return objectName != null && managedPolicies.containsKey(objectName.trim().toUpperCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
static String managedPolicyName(String objectName) {
|
||||
return MANAGED_POLICIES.get(objectName);
|
||||
String managedPolicyName(String objectName) {
|
||||
return managedPolicies.get(objectName);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,9 +73,9 @@ public class MaskingPolicySynchronizer {
|
||||
public MaskingPolicySyncResult synchronize() {
|
||||
Map<String, List<ColumnMaskingRule>> desiredByObject = new LinkedHashMap<>();
|
||||
for (ColumnMaskingRule rule : mapper.findColumnRules()) {
|
||||
if (OWNER.equalsIgnoreCase(rule.owner())
|
||||
if (catalog.owner().equalsIgnoreCase(rule.owner())
|
||||
&& rule.ruleEnabled()
|
||||
&& MANAGED_POLICIES.containsKey(rule.objectName())) {
|
||||
&& managedPolicies.containsKey(rule.objectName())) {
|
||||
desiredByObject.computeIfAbsent(rule.objectName(), ignored -> new ArrayList<>()).add(rule);
|
||||
}
|
||||
}
|
||||
@@ -79,7 +85,7 @@ public class MaskingPolicySynchronizer {
|
||||
int addedColumns = 0;
|
||||
int modifiedColumns = 0;
|
||||
int droppedColumns = 0;
|
||||
for (Map.Entry<String, String> policy : MANAGED_POLICIES.entrySet()) {
|
||||
for (Map.Entry<String, String> policy : managedPolicies.entrySet()) {
|
||||
String objectName = policy.getKey();
|
||||
String policyName = policy.getValue();
|
||||
List<ColumnMaskingRule> desired = desiredByObject.getOrDefault(objectName, List.of());
|
||||
@@ -141,7 +147,7 @@ public class MaskingPolicySynchronizer {
|
||||
SELECT enable
|
||||
FROM redaction_policies
|
||||
WHERE object_owner = ? AND object_name = ? AND policy_name = ?
|
||||
""", String.class, OWNER, objectName, policyName);
|
||||
""", String.class, catalog.owner(), objectName, policyName);
|
||||
return statuses.isEmpty() ? null : statuses.getFirst();
|
||||
}
|
||||
|
||||
@@ -150,7 +156,7 @@ public class MaskingPolicySynchronizer {
|
||||
SELECT column_name
|
||||
FROM redaction_columns
|
||||
WHERE object_owner = ? AND object_name = ?
|
||||
""", String.class, OWNER, objectName).stream()
|
||||
""", String.class, catalog.owner(), objectName).stream()
|
||||
.map(this::requiredColumnName)
|
||||
.toList();
|
||||
}
|
||||
@@ -160,7 +166,7 @@ public class MaskingPolicySynchronizer {
|
||||
BEGIN
|
||||
DBMS_REDACT.DISABLE_POLICY(object_schema => ?, object_name => ?, policy_name => ?);
|
||||
END;
|
||||
""", OWNER, objectName, policyName);
|
||||
""", catalog.owner(), objectName, policyName);
|
||||
}
|
||||
|
||||
private void enablePolicy(String objectName, String policyName) {
|
||||
@@ -168,7 +174,7 @@ public class MaskingPolicySynchronizer {
|
||||
BEGIN
|
||||
DBMS_REDACT.ENABLE_POLICY(object_schema => ?, object_name => ?, policy_name => ?);
|
||||
END;
|
||||
""", OWNER, objectName, policyName);
|
||||
""", catalog.owner(), objectName, policyName);
|
||||
}
|
||||
|
||||
private void dropColumn(String objectName, String policyName, String columnName) {
|
||||
@@ -179,7 +185,7 @@ public class MaskingPolicySynchronizer {
|
||||
action => DBMS_REDACT.DROP_COLUMN, column_name => ?
|
||||
);
|
||||
END;
|
||||
""", OWNER, objectName, policyName, columnName);
|
||||
""", catalog.owner(), objectName, policyName, columnName);
|
||||
}
|
||||
|
||||
private void addPolicy(
|
||||
@@ -251,9 +257,9 @@ public class MaskingPolicySynchronizer {
|
||||
END;
|
||||
""".formatted(functionConstant);
|
||||
if (regexPattern == null) {
|
||||
jdbcTemplate.update(sql, OWNER, objectName, policyName, columnName);
|
||||
jdbcTemplate.update(sql, catalog.owner(), objectName, policyName, columnName);
|
||||
} else {
|
||||
jdbcTemplate.update(sql, OWNER, objectName, policyName, columnName, regexPattern, regexReplacement);
|
||||
jdbcTemplate.update(sql, catalog.owner(), objectName, policyName, columnName, regexPattern, regexReplacement);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -276,9 +282,9 @@ public class MaskingPolicySynchronizer {
|
||||
END;
|
||||
""".formatted(actionConstant, functionConstant);
|
||||
if (regexPattern == null) {
|
||||
jdbcTemplate.update(sql, OWNER, objectName, policyName, columnName);
|
||||
jdbcTemplate.update(sql, catalog.owner(), objectName, policyName, columnName);
|
||||
} else {
|
||||
jdbcTemplate.update(sql, OWNER, objectName, policyName, columnName, regexPattern, regexReplacement);
|
||||
jdbcTemplate.update(sql, catalog.owner(), objectName, policyName, columnName, regexPattern, regexReplacement);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,7 +320,7 @@ public class MaskingPolicySynchronizer {
|
||||
object_schema => ?, object_name => ?, column_name => ?, policy_expression_name => ?
|
||||
);
|
||||
END;
|
||||
""", OWNER, objectName, columnName, expressionName);
|
||||
""", catalog.owner(), objectName, columnName, expressionName);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.cloudhandson.vpdbackoffice.domain.masking.MaskingRule;
|
||||
import com.cloudhandson.vpdbackoffice.domain.masking.MaskingRuleCreateCommand;
|
||||
import com.cloudhandson.vpdbackoffice.domain.masking.MaskingPolicyStatus;
|
||||
import com.cloudhandson.vpdbackoffice.domain.masking.MaskingTemplate;
|
||||
import com.cloudhandson.vpdbackoffice.domain.masking.ManagedMaskingPolicy;
|
||||
import com.cloudhandson.vpdbackoffice.domain.masking.UserMaskingRule;
|
||||
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObject;
|
||||
import com.cloudhandson.vpdbackoffice.mapper.MaskingRuleMapper;
|
||||
@@ -55,9 +56,13 @@ public class MaskingRuleService {
|
||||
return mapper.findColumnRules();
|
||||
}
|
||||
|
||||
/** Reads the actual Oracle Data Redaction state for the three managed KB objects. */
|
||||
/** Reads Oracle Data Redaction state for the objects declared in the JSON catalogue. */
|
||||
public List<MaskingPolicyStatus> findPolicyStatuses() {
|
||||
return mapper.findPolicyStatuses();
|
||||
List<ManagedMaskingPolicy> policies = maskingPolicySynchronizer.managedObjectNames().stream()
|
||||
.map(objectName -> new ManagedMaskingPolicy(
|
||||
objectName, maskingPolicySynchronizer.managedPolicyName(objectName)))
|
||||
.toList();
|
||||
return mapper.findPolicyStatuses(maskingPolicySynchronizer.owner(), policies);
|
||||
}
|
||||
|
||||
public Set<String> managedObjectNames() {
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.cloudhandson.vpdbackoffice.service;
|
||||
import com.cloudhandson.vpdbackoffice.domain.schemametadata.SchemaAnnotation;
|
||||
import com.cloudhandson.vpdbackoffice.domain.schemametadata.SchemaMetadataColumn;
|
||||
import com.cloudhandson.vpdbackoffice.domain.schemametadata.SchemaMetadataView;
|
||||
import com.cloudhandson.vpdbackoffice.domain.structured.StructuredDataCatalog;
|
||||
import com.cloudhandson.vpdbackoffice.domain.structured.StructuredDataTable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
@@ -18,7 +19,6 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
@Service
|
||||
public class SchemaMetadataService {
|
||||
|
||||
private static final String OWNER = "POC_2";
|
||||
private static final int MAX_COMMENT_LENGTH = 4000;
|
||||
private static final int MAX_ANNOTATION_VALUE_LENGTH = 4000;
|
||||
private static final Pattern ORACLE_SIMPLE_NAME = Pattern.compile("[A-Z][A-Z0-9_$#]{0,127}");
|
||||
@@ -35,6 +35,10 @@ public class SchemaMetadataService {
|
||||
return structuredDataService.tables();
|
||||
}
|
||||
|
||||
public StructuredDataCatalog catalog() {
|
||||
return structuredDataService.catalog();
|
||||
}
|
||||
|
||||
public String defaultKey() {
|
||||
return structuredDataService.defaultKey();
|
||||
}
|
||||
@@ -111,7 +115,7 @@ public class SchemaMetadataService {
|
||||
FROM all_tab_comments
|
||||
WHERE owner = ?
|
||||
AND table_name = ?
|
||||
""", (rs, rowNum) -> rs.getString(1), OWNER, tableName);
|
||||
""", (rs, rowNum) -> rs.getString(1), owner(), tableName);
|
||||
return values.isEmpty() ? "" : values.getFirst();
|
||||
}
|
||||
|
||||
@@ -146,7 +150,7 @@ public class SchemaMetadataService {
|
||||
"Y".equalsIgnoreCase(rs.getString("nullable")),
|
||||
nullToEmpty(rs.getString("comments")),
|
||||
annotations.getOrDefault(columnTargetKey(rs.getString("column_name")), List.of())
|
||||
), OWNER, tableName);
|
||||
), owner(), tableName);
|
||||
}
|
||||
|
||||
private Map<String, List<SchemaAnnotation>> annotationsByTarget(String tableName) {
|
||||
@@ -216,7 +220,7 @@ public class SchemaMetadataService {
|
||||
WHERE owner = ?
|
||||
AND table_name = ?
|
||||
AND column_name = ?
|
||||
""", Integer.class, OWNER, tableName, column);
|
||||
""", Integer.class, owner(), tableName, column);
|
||||
if (count == null || count == 0) {
|
||||
throw new AppException("선택한 테이블에 존재하지 않는 컬럼입니다.");
|
||||
}
|
||||
@@ -243,7 +247,11 @@ public class SchemaMetadataService {
|
||||
}
|
||||
|
||||
private String qualifiedTable(String tableName) {
|
||||
return quoteName(OWNER) + "." + quoteName(requireSimpleName(tableName, "table name"));
|
||||
return quoteName(owner()) + "." + quoteName(requireSimpleName(tableName, "table name"));
|
||||
}
|
||||
|
||||
private String owner() {
|
||||
return structuredDataService.owner();
|
||||
}
|
||||
|
||||
private String quoteName(String value) {
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
package com.cloudhandson.vpdbackoffice.service;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.domain.structured.StructuredDataCatalog;
|
||||
import com.cloudhandson.vpdbackoffice.domain.structured.StructuredDataTable;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class StructuredDataCatalogProvider {
|
||||
|
||||
private static final Pattern ORACLE_SIMPLE_NAME = Pattern.compile("[A-Z][A-Z0-9_$#]{0,127}");
|
||||
private static final Pattern TABLE_KEY = Pattern.compile("[a-z][a-z0-9-]{0,63}");
|
||||
private static final int MAX_ROW_LIMIT = 500;
|
||||
|
||||
private final StructuredDataCatalog catalog;
|
||||
|
||||
public StructuredDataCatalogProvider(
|
||||
ObjectMapper objectMapper,
|
||||
@Value("${backoffice.structured-data.catalog-location:classpath:/config/structured-data-catalog.json}")
|
||||
Resource catalogResource
|
||||
) {
|
||||
try (var input = catalogResource.getInputStream()) {
|
||||
this.catalog = validate(objectMapper.readValue(input, StructuredDataCatalog.class));
|
||||
} catch (IOException exception) {
|
||||
throw new IllegalStateException(
|
||||
"정형 데이터 카탈로그 JSON을 읽을 수 없습니다: " + catalogResource.getDescription(), exception);
|
||||
}
|
||||
}
|
||||
|
||||
public StructuredDataCatalog catalog() {
|
||||
return catalog;
|
||||
}
|
||||
|
||||
private StructuredDataCatalog validate(StructuredDataCatalog source) {
|
||||
if (source == null) {
|
||||
throw new IllegalStateException("정형 데이터 카탈로그가 비어 있습니다.");
|
||||
}
|
||||
String sourceName = requireText(source.sourceName(), "sourceName");
|
||||
String owner = requireOracleName(source.owner(), "owner");
|
||||
String pageHelp = requireText(source.pageHelp(), "pageHelp");
|
||||
String catalogDescription = requireText(source.catalogDescription(), "catalogDescription");
|
||||
if (source.rowLimit() < 1 || source.rowLimit() > MAX_ROW_LIMIT) {
|
||||
throw new IllegalStateException("정형 데이터 카탈로그 rowLimit은 1~" + MAX_ROW_LIMIT + " 범위여야 합니다.");
|
||||
}
|
||||
if (source.tables() == null || source.tables().isEmpty()) {
|
||||
throw new IllegalStateException("정형 데이터 카탈로그에는 테이블이 한 개 이상 필요합니다.");
|
||||
}
|
||||
|
||||
Set<String> keys = new HashSet<>();
|
||||
Set<String> tableNames = new HashSet<>();
|
||||
List<StructuredDataTable> tables = source.tables().stream().map(table -> {
|
||||
if (table == null) {
|
||||
throw new IllegalStateException("정형 데이터 카탈로그에 null 테이블 정의가 있습니다.");
|
||||
}
|
||||
String key = requireKey(table.key());
|
||||
String tableName = requireOracleName(table.tableName(), "tableName");
|
||||
if (!keys.add(key)) {
|
||||
throw new IllegalStateException("정형 데이터 카탈로그 key가 중복됩니다: " + key);
|
||||
}
|
||||
if (!tableNames.add(tableName)) {
|
||||
throw new IllegalStateException("정형 데이터 카탈로그 tableName이 중복됩니다: " + tableName);
|
||||
}
|
||||
List<String> previewColumns = table.previewColumns() == null
|
||||
? List.of()
|
||||
: table.previewColumns().stream()
|
||||
.map(column -> requireOracleName(column, "previewColumns"))
|
||||
.distinct()
|
||||
.toList();
|
||||
String maskingPolicyName = table.maskingPolicyName() == null || table.maskingPolicyName().isBlank()
|
||||
? null
|
||||
: requireOracleName(table.maskingPolicyName(), "maskingPolicyName");
|
||||
return new StructuredDataTable(
|
||||
key,
|
||||
tableName,
|
||||
requireText(table.businessName(), "businessName"),
|
||||
requireText(table.description(), "description"),
|
||||
List.copyOf(previewColumns),
|
||||
maskingPolicyName);
|
||||
}).toList();
|
||||
|
||||
return new StructuredDataCatalog(
|
||||
sourceName, owner, pageHelp, catalogDescription, source.rowLimit(), List.copyOf(tables));
|
||||
}
|
||||
|
||||
private String requireKey(String value) {
|
||||
String normalized = requireText(value, "key").toLowerCase(Locale.ROOT);
|
||||
if (!TABLE_KEY.matcher(normalized).matches()) {
|
||||
throw new IllegalStateException("정형 데이터 카탈로그 key 형식이 올바르지 않습니다: " + value);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private String requireOracleName(String value, String field) {
|
||||
String normalized = requireText(value, field).toUpperCase(Locale.ROOT);
|
||||
if (!ORACLE_SIMPLE_NAME.matcher(normalized).matches()) {
|
||||
throw new IllegalStateException("정형 데이터 카탈로그 " + field + " 형식이 올바르지 않습니다: " + value);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private String requireText(String value, String field) {
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new IllegalStateException("정형 데이터 카탈로그 " + field + " 값은 필수입니다.");
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.cloudhandson.vpdbackoffice.service;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.domain.structured.StructuredDataCatalog;
|
||||
import com.cloudhandson.vpdbackoffice.domain.structured.StructuredDataPreview;
|
||||
import com.cloudhandson.vpdbackoffice.domain.structured.StructuredDataTable;
|
||||
import java.util.List;
|
||||
@@ -11,33 +12,35 @@ import org.springframework.stereotype.Service;
|
||||
@Service
|
||||
public class StructuredDataService {
|
||||
|
||||
private static final String OWNER = "POC_2";
|
||||
private static final int ROW_LIMIT = 50;
|
||||
private static final List<StructuredDataTable> TABLES = List.of(
|
||||
new StructuredDataTable("customers", "KB_CUSTOMERS", "고객원장", "고객 기본정보"),
|
||||
new StructuredDataTable("products", "KB_PRODUCTS", "상품원장", "보험상품 마스터"),
|
||||
new StructuredDataTable("contracts", "KB_CONTRACTS", "계약원장", "보험계약 정보"),
|
||||
new StructuredDataTable("coverages", "KB_COVERAGES", "담보원장", "보장·특약 정보"),
|
||||
new StructuredDataTable("claims", "KB_CLAIMS", "청구원장", "보험금 청구·지급 정보"),
|
||||
new StructuredDataTable("external-holdings", "KB_EXTERNAL_HOLDINGS", "외부보유정보 원장", "타사·외부 가입·보유 정보"),
|
||||
new StructuredDataTable("stakeholders", "KB_STAKEHOLDERS", "이해관계자 원장", "역할·담당자 매핑"));
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final StructuredDataCatalog catalog;
|
||||
|
||||
public StructuredDataService(JdbcTemplate jdbcTemplate) {
|
||||
public StructuredDataService(
|
||||
JdbcTemplate jdbcTemplate,
|
||||
StructuredDataCatalogProvider catalogProvider
|
||||
) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
this.catalog = catalogProvider.catalog();
|
||||
}
|
||||
|
||||
public StructuredDataCatalog catalog() {
|
||||
return catalog;
|
||||
}
|
||||
|
||||
public String owner() {
|
||||
return catalog.owner();
|
||||
}
|
||||
|
||||
public List<StructuredDataTable> tables() {
|
||||
return TABLES;
|
||||
return catalog.tables();
|
||||
}
|
||||
|
||||
public String defaultKey() {
|
||||
return TABLES.getFirst().key();
|
||||
return catalog.tables().getFirst().key();
|
||||
}
|
||||
|
||||
public StructuredDataTable requireTable(String key) {
|
||||
return TABLES.stream()
|
||||
return catalog.tables().stream()
|
||||
.filter(table -> table.key().equals(key))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new AppException("선택할 수 없는 정형 데이터 테이블입니다."));
|
||||
@@ -53,17 +56,22 @@ public class StructuredDataService {
|
||||
WHERE owner = ?
|
||||
AND table_name = ?
|
||||
ORDER BY column_id
|
||||
""",
|
||||
(resultSet, rowNum) -> resultSet.getString(1), OWNER, table.tableName());
|
||||
""",
|
||||
(resultSet, rowNum) -> resultSet.getString(1), catalog.owner(), table.tableName());
|
||||
if (columns.isEmpty()) {
|
||||
throw new AppException("정형 데이터 테이블의 컬럼 정보를 찾을 수 없습니다.");
|
||||
}
|
||||
List<String> previewColumns = table.previewColumns().isEmpty() ? columns : table.previewColumns();
|
||||
if (!columns.containsAll(previewColumns)) {
|
||||
throw new AppException("정형 데이터 JSON의 미리보기 컬럼이 실제 테이블과 일치하지 않습니다.");
|
||||
}
|
||||
|
||||
List<Map<String, Object>> rows = jdbcTemplate.queryForList(
|
||||
previewSql(table), ROW_LIMIT);
|
||||
return new StructuredDataPreview(table, columns, rows, ROW_LIMIT);
|
||||
previewSql(table, previewColumns), catalog.rowLimit());
|
||||
return new StructuredDataPreview(table, previewColumns, rows, catalog.rowLimit());
|
||||
} catch (DataAccessException exception) {
|
||||
throw new AppException("정형 데이터를 조회할 수 없습니다. POC_2 조회 권한과 대상 테이블 상태를 확인하세요.");
|
||||
throw new AppException("정형 데이터를 조회할 수 없습니다. " + catalog.sourceName() + "의 "
|
||||
+ catalog.owner() + " 조회 권한과 대상 테이블 상태를 확인하세요.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,16 +79,20 @@ public class StructuredDataService {
|
||||
* The table is selected from a closed application whitelist, so the query
|
||||
* text remains fixed and no request value can become a SQL identifier.
|
||||
*/
|
||||
private String previewSql(StructuredDataTable table) {
|
||||
return switch (table.key()) {
|
||||
case "customers" -> "SELECT * FROM POC_2.KB_CUSTOMERS WHERE ROWNUM <= ?";
|
||||
case "products" -> "SELECT * FROM POC_2.KB_PRODUCTS WHERE ROWNUM <= ?";
|
||||
case "contracts" -> "SELECT * FROM POC_2.KB_CONTRACTS WHERE ROWNUM <= ?";
|
||||
case "coverages" -> "SELECT * FROM POC_2.KB_COVERAGES WHERE ROWNUM <= ?";
|
||||
case "claims" -> "SELECT * FROM POC_2.KB_CLAIMS WHERE ROWNUM <= ?";
|
||||
case "external-holdings" -> "SELECT * FROM POC_2.KB_EXTERNAL_HOLDINGS WHERE ROWNUM <= ?";
|
||||
case "stakeholders" -> "SELECT * FROM POC_2.KB_STAKEHOLDERS WHERE ROWNUM <= ?";
|
||||
default -> throw new AppException("선택할 수 없는 정형 데이터 테이블입니다.");
|
||||
};
|
||||
String previewSql(StructuredDataTable table) {
|
||||
return previewSql(table, table.previewColumns());
|
||||
}
|
||||
|
||||
private String previewSql(StructuredDataTable table, List<String> previewColumns) {
|
||||
StructuredDataTable approved = requireTable(table.key());
|
||||
if (!approved.tableName().equals(table.tableName())) {
|
||||
throw new AppException("선택할 수 없는 정형 데이터 테이블입니다.");
|
||||
}
|
||||
String projection = previewColumns == null || previewColumns.isEmpty()
|
||||
? "*"
|
||||
: previewColumns.stream().map(column -> "\"" + column + "\"")
|
||||
.reduce((left, right) -> left + ", " + right).orElseThrow();
|
||||
return "SELECT " + projection + " FROM \"" + catalog.owner() + "\".\"" + approved.tableName()
|
||||
+ "\" WHERE ROWNUM <= ?";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ public class SchemaMetadataController {
|
||||
@GetMapping("/schema-metadata")
|
||||
public String schemaMetadata(@RequestParam(required = false) String table, Model model) {
|
||||
String selectedKey = table == null || table.isBlank() ? schemaMetadataService.defaultKey() : table;
|
||||
model.addAttribute("catalog", schemaMetadataService.catalog());
|
||||
model.addAttribute("tables", schemaMetadataService.tables());
|
||||
model.addAttribute("selectedKey", selectedKey);
|
||||
try {
|
||||
|
||||
@@ -22,6 +22,7 @@ public class StructuredDataController {
|
||||
Model model
|
||||
) {
|
||||
String selectedKey = table == null || table.isBlank() ? structuredDataService.defaultKey() : table;
|
||||
model.addAttribute("catalog", structuredDataService.catalog());
|
||||
model.addAttribute("tables", structuredDataService.tables());
|
||||
model.addAttribute("selectedKey", selectedKey);
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user