Merge pull request 'RestTemplateConfig 수정, wmts 수정' (#21) from feat/infer_dev_260107 into develop

Reviewed-on: #21
This commit was merged in pull request #21.
This commit is contained in:
2026-01-30 17:13:30 +09:00
3 changed files with 213 additions and 51 deletions

View File

@@ -22,29 +22,45 @@ public class ExternalHttpClient {
private final RestTemplate restTemplate;
private final ObjectMapper objectMapper;
/**
* - responseType이 DTO면: DTO로 역직렬화 - responseType이 String.class면: 응답을 byte[]로 받아 UTF-8 원문 문자열로 반환
* (배열/객체 등 유동 JSON 안전)
*/
public <T> ExternalCallResult<T> call(
String url, HttpMethod method, Object body, HttpHeaders headers, Class<T> responseType) {
HttpHeaders resolvedHeaders = resolveJsonHeaders(headers);
// responseType 기반으로 Accept 동적 세팅
HttpHeaders resolvedHeaders = resolveHeaders(headers, responseType);
logRequestBody(body);
HttpEntity<Object> entity = new HttpEntity<>(body, resolvedHeaders);
try {
// String: raw bytes -> UTF-8 string
if (responseType == String.class) {
ResponseEntity<byte[]> res = restTemplate.exchange(url, method, entity, byte[].class);
String raw =
(res.getBody() == null) ? null : new String(res.getBody(), StandardCharsets.UTF_8);
@SuppressWarnings("unchecked")
T casted = (T) raw; // responseType == String.class 이므로 안전
T casted = (T) raw;
return new ExternalCallResult<>(res.getStatusCodeValue(), true, casted, null);
}
// byte[]: raw bytes로 받고, JSON이면 에러로 처리
if (responseType == byte[].class) {
ResponseEntity<byte[]> res = restTemplate.exchange(url, method, entity, byte[].class);
MediaType ct = res.getHeaders().getContentType();
byte[] bytes = res.getBody();
if (isJsonLike(ct)) {
String err = (bytes == null) ? null : new String(bytes, StandardCharsets.UTF_8);
return new ExternalCallResult<>(res.getStatusCodeValue(), false, null, err);
}
@SuppressWarnings("unchecked")
T casted = (T) bytes;
return new ExternalCallResult<>(res.getStatusCodeValue(), true, casted, null);
}
// DTO 등: 일반 역직렬화
ResponseEntity<T> res = restTemplate.exchange(url, method, entity, responseType);
return new ExternalCallResult<>(res.getStatusCodeValue(), true, res.getBody(), null);
@@ -54,18 +70,42 @@ public class ExternalHttpClient {
}
}
private HttpHeaders resolveJsonHeaders(HttpHeaders headers) {
HttpHeaders h = (headers == null) ? new HttpHeaders() : headers;
// 기존 resolveJsonHeaders를 "동적"으로 교체
private HttpHeaders resolveHeaders(HttpHeaders headers, Class<?> responseType) {
// 원본 headers를 그대로 쓰면 외부에서 재사용할 때 사이드이펙트 날 수 있어서 복사 권장
HttpHeaders h = (headers == null) ? new HttpHeaders() : new HttpHeaders(headers);
// 요청 바디 기본은 JSON이라고 가정 (필요하면 호출부에서 덮어쓰기)
if (h.getContentType() == null) {
h.setContentType(MediaType.APPLICATION_JSON);
}
if (h.getAccept() == null || h.getAccept().isEmpty()) {
// 호출부에서 Accept를 명시했으면 존중
if (h.getAccept() != null && !h.getAccept().isEmpty()) {
return h;
}
// responseType 기반 Accept 자동 지정
if (responseType == byte[].class) {
h.setAccept(
List.of(
MediaType.APPLICATION_OCTET_STREAM,
MediaType.valueOf("application/zip"),
MediaType.APPLICATION_JSON // 실패(JSON 에러 바디) 대비
));
} else {
h.setAccept(List.of(MediaType.APPLICATION_JSON));
}
return h;
}
private boolean isJsonLike(MediaType ct) {
if (ct == null) return false;
return ct.includes(MediaType.APPLICATION_JSON)
|| "application/problem+json".equalsIgnoreCase(ct.toString());
}
private void logRequestBody(Object body) {
try {
if (body != null) {

View File

@@ -18,17 +18,20 @@ public class WmtsLayerInfo {
private List<String> keywords = new ArrayList<>();
private BoundingBox boundingBox;
private List<String> formats = new ArrayList<>();
private List<TileMatrixSetLink> tileMatrixSetLinks = new ArrayList<>();
private List<ResourceUrl> resourceUrls = new ArrayList<>();
private List<Style> styles = new ArrayList<>();
// add (2025-01-30)
private List<String> matrixIds = new ArrayList<>();
private String workspace;
private List<String> matrixIds = new ArrayList<>(); // 20250130
private String workspace; // 20250130
/* ===== convenience methods ===== */
private List<TileMatric> tileMatrices = new ArrayList<>();
// (선택) 기존 add 메서드 유지하고 싶으면 남겨도 됨
public void addTileMatric(TileMatric tileMatric) {
this.tileMatrices.add(tileMatric);
}
public void addMatrixId(String matrixId) {
this.matrixIds.add(matrixId);
@@ -86,6 +89,20 @@ public class WmtsLayerInfo {
private boolean isDefault;
}
/** TileMatrix 정보를 담는 내부 클래스 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class TileMatric {
private String identifier;
private String scaleDenominator;
private String topLeftCorner;
private String tileWidth;
private String tileHeight;
private String matrixWidth;
private String matrixHeight;
}
/** TileMatrixSetLink 정보를 담는 내부 클래스 */
@Data
@NoArgsConstructor

View File

@@ -60,9 +60,9 @@ public class WmtsService {
public List<WmtsLayerInfo> getAllLayers(String geoserverUrl, String workspace) {
List<WmtsLayerInfo> layers = new ArrayList<>();
try {
// 1. XML 문서 로드 및 파싱
String capabilitiesUrl =
geoserverUrl + WMTS_GEOSERVER_URL + workspace + WMTS_CAPABILITIES_URL;
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
DocumentBuilder builder = factory.newDocumentBuilder();
@@ -71,16 +71,18 @@ public class WmtsService {
XPathFactory xPathFactory = XPathFactory.newInstance();
XPath xpath = xPathFactory.newXPath();
// 2. 모든 Layer 노드 검색
String expression = "//*[local-name()='Layer']";
NodeList layerNodes =
(NodeList) xpath.compile(expression).evaluate(doc, XPathConstants.NODESET);
// 3. 모든 레이어를 파싱하여 리스트에 추가
for (int i = 0; i < layerNodes.getLength(); i++) {
Node layerNode = layerNodes.item(i);
String title = getChildValue(layerNode, "Title");
if (title != null && !title.trim().isEmpty()) {
WmtsLayerInfo layerInfo = parseLayerNode(workspace, layerNode, title);
WmtsLayerInfo layerInfo = parseLayerNode(workspace, doc, layerNode, title);
layers.add(layerInfo);
}
}
@@ -94,25 +96,28 @@ public class WmtsService {
}
/**
* WMTS Capabilities URL에서 특정 타이틀의 레이어 정보를 가져옵니다.
* WMTS Capabilities URL에서 특정 타이틀의 레이어 정보를 가져옵니다. // * @param capabilitiesUrl 예:
* http://localhost:8080/geoserver/gwc/service/wmts?REQUEST=GetCapabilities
*
* @param geoserverUrl 예: http://localhost:8080
* @param targetTitle 찾고자 하는 레이어의 Title
* @param targetTitle 찾고자 하는 레이어의 Title (예: "My Maps")
*/
public WmtsLayerInfo getLayerInfoByTitle(
String geoserverUrl, String workspace, String targetTitle) {
try {
// 1. XML 문서 로드 및 파싱
String capabilitiesUrl =
geoserverUrl + WMTS_GEOSERVER_URL + workspace + WMTS_CAPABILITIES_URL;
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
factory.setNamespaceAware(true); // 네임스페이스 인식
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(new URL(capabilitiesUrl).openStream());
XPathFactory xPathFactory = XPathFactory.newInstance();
XPath xpath = xPathFactory.newXPath();
// 2. 모든 Layer 노드 검색 (네임스페이스 무시하고 local-name으로 검색)
// GeoServer WMTS에서 Layer는 <Contents> -> <Layer> 구조임
String expression = "//*[local-name()='Layer']";
NodeList layerNodes =
(NodeList) xpath.compile(expression).evaluate(doc, XPathConstants.NODESET);
@@ -120,9 +125,12 @@ public class WmtsService {
for (int i = 0; i < layerNodes.getLength(); i++) {
Node layerNode = layerNodes.item(i);
// 3. Title 확인
String title = getChildValue(layerNode, "Title");
// 타이틀이 일치하면 객체 매핑 시작
if (title != null && title.trim().equals(targetTitle)) {
return parseLayerNode(workspace, layerNode, title);
return parseLayerNode(workspace, doc, layerNode, title);
}
}
@@ -131,11 +139,12 @@ public class WmtsService {
throw new RuntimeException("WMTS 정보 조회 중 오류 발생: " + e.getMessage());
}
return null;
return null; // 찾지 못한 경우
}
// 레이어 노드를 Java 객체로 변환 (2025-01-30)
private WmtsLayerInfo parseLayerNode(String workspace, Node layerNode, String title) {
// 레이어 노드를 Java 객체로 변환 20250130
private WmtsLayerInfo parseLayerNode(
String workspace, Document doc, Node layerNode, String title) {
WmtsLayerInfo info = new WmtsLayerInfo();
info.setWorkspace(workspace); // 20250130
@@ -155,7 +164,7 @@ public class WmtsService {
// TileMatrixSetLink 파싱 (TileMatrixSet + Zoom Levels)
info.setTileMatrixSetLinks(parseTileMatrixSetLinks(layerNode));
// TileMatrixSetLimits에서 줌 레벨 추출
// TileMatrixSetLimits에서 줌 레벨 추출 (개별 zoom 리스트)
info.setMatrixIds(parseMatrixIds(layerNode)); // 20260130
// ResourceURL 파싱
@@ -164,46 +173,59 @@ public class WmtsService {
// Styles 파싱
info.setStyles(parseStyles(layerNode));
// TileMatrixSet의 TileMatrix 정보 파싱
List<String> tileMatrixSetNames = new ArrayList<>();
if (info.getTileMatrixSetLinks() != null) {
for (WmtsLayerInfo.TileMatrixSetLink link : info.getTileMatrixSetLinks()) {
tileMatrixSetNames.add(link.getTileMatrixSet());
}
}
info.setTileMatrices(parseTileMatrices(doc, tileMatrixSetNames));
return info;
}
// --- Helper Methods ---
private WmtsLayerInfo.BoundingBox parseBoundingBox(Node layerNode) {
// 보통 <ows:WGS84BoundingBox>를 찾음
Node bboxNode = findChildNode(layerNode, "WGS84BoundingBox");
if (bboxNode == null) bboxNode = findChildNode(layerNode, "BoundingBox");
if (bboxNode == null) return null;
if (bboxNode != null) {
WmtsLayerInfo.BoundingBox bbox = new WmtsLayerInfo.BoundingBox();
WmtsLayerInfo.BoundingBox bbox = new WmtsLayerInfo.BoundingBox();
// WGS84는 CRS 속성이 없을 수 있음
bbox.setCrs(getAttributeValue(bboxNode, "crs"));
bbox.setCrs(getAttributeValue(bboxNode, "crs"));
String lowerCorner = getChildValue(bboxNode, "LowerCorner");
String upperCorner = getChildValue(bboxNode, "UpperCorner");
String lowerCorner = getChildValue(bboxNode, "LowerCorner");
String upperCorner = getChildValue(bboxNode, "UpperCorner");
if (lowerCorner != null) {
String[] coords = lowerCorner.split(" ");
bbox.setLowerCornerX(Double.parseDouble(coords[0]));
bbox.setLowerCornerY(Double.parseDouble(coords[1]));
}
if (lowerCorner != null) {
String[] coords = lowerCorner.trim().split("\\s+");
bbox.setLowerCornerX(Double.parseDouble(coords[0]));
bbox.setLowerCornerY(Double.parseDouble(coords[1]));
if (upperCorner != null) {
String[] coords = upperCorner.split(" ");
bbox.setUpperCornerX(Double.parseDouble(coords[0]));
bbox.setUpperCornerY(Double.parseDouble(coords[1]));
}
return bbox;
}
if (upperCorner != null) {
String[] coords = upperCorner.trim().split("\\s+");
bbox.setUpperCornerX(Double.parseDouble(coords[0]));
bbox.setUpperCornerY(Double.parseDouble(coords[1]));
}
return bbox;
return null;
}
private List<WmtsLayerInfo.ResourceUrl> parseResourceUrls(Node layerNode) {
List<WmtsLayerInfo.ResourceUrl> list = new ArrayList<>();
NodeList children = layerNode.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node node = children.item(i);
if (node.getNodeName().contains("ResourceURL")) {
if (node.getNodeName().contains("ResourceURL")) { // local-name check simplification
WmtsLayerInfo.ResourceUrl url = new WmtsLayerInfo.ResourceUrl();
url.setFormat(getAttributeValue(node, "format"));
url.setResourceType(getAttributeValue(node, "resourceType"));
@@ -217,7 +239,6 @@ public class WmtsService {
private List<WmtsLayerInfo.Style> parseStyles(Node layerNode) {
List<WmtsLayerInfo.Style> styles = new ArrayList<>();
NodeList children = layerNode.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node node = children.item(i);
if (node.getNodeName().endsWith("Style")) {
@@ -231,23 +252,34 @@ public class WmtsService {
return styles;
}
/** TileMatrixSetLimits에서 줌 레벨을 추출합니다. 예: "EPSG:4326:0" → "0" */
/**
* TileMatrixSetLimits에서 줌 레벨을 추출합니다. 예: "EPSG:4326:0" → "0", "EPSG:4326:1" → "1"
*
* @param layerNode Layer 노드
* @return 줌 레벨 문자열 리스트
*/
private List<String> parseMatrixIds(Node layerNode) {
List<String> matrixIds = new ArrayList<>();
NodeList children = layerNode.getChildNodes();
// 모든 TileMatrixSetLink 찾기
for (int i = 0; i < children.getLength(); i++) {
Node node = children.item(i);
if (node.getNodeName().contains("TileMatrixSetLink")) {
// TileMatrixSetLimits 찾기
Node limitsNode = findChildNode(node, "TileMatrixSetLimits");
if (limitsNode != null) {
NodeList limitsList = limitsNode.getChildNodes();
// 각 TileMatrixLimits 처리
for (int j = 0; j < limitsList.getLength(); j++) {
Node limitNode = limitsList.item(j);
if (limitNode.getNodeName().contains("TileMatrixLimits")) {
// TileMatrix 또는 Identifier 값 추출
String identifier = getChildValue(limitNode, "TileMatrix");
if (identifier == null) identifier = getChildValue(limitNode, "Identifier");
if (identifier == null) {
identifier = getChildValue(limitNode, "Identifier");
}
// 마지막 콜론 이후 값(줌 레벨) 추출
if (identifier != null && identifier.contains(":")) {
String[] parts = identifier.split(":");
String zoomLevel = parts[parts.length - 1];
@@ -262,16 +294,25 @@ public class WmtsService {
return matrixIds;
}
/**
* TileMatrixSetLink 정보를 파싱합니다. 각 TileMatrixSetLink에서 TileMatrixSet 이름과 줌 레벨들을 추출합니다.
*
* @param layerNode Layer 노드
* @return TileMatrixSetLink 객체 리스트
*/
private List<WmtsLayerInfo.TileMatrixSetLink> parseTileMatrixSetLinks(Node layerNode) {
List<WmtsLayerInfo.TileMatrixSetLink> links = new ArrayList<>();
NodeList children = layerNode.getChildNodes();
// 모든 TileMatrixSetLink 찾기
for (int i = 0; i < children.getLength(); i++) {
Node node = children.item(i);
if (node.getNodeName().contains("TileMatrixSetLink")) {
// TileMatrixSet 이름 추출
String tileMatrixSet = getChildValue(node, "TileMatrixSet");
// 줌 레벨들 추출
List<String> zoomLevels = new ArrayList<>();
Node limitsNode = findChildNode(node, "TileMatrixSetLimits");
if (limitsNode != null) {
@@ -279,9 +320,12 @@ public class WmtsService {
for (int j = 0; j < limitsList.getLength(); j++) {
Node limitNode = limitsList.item(j);
if (limitNode.getNodeName().contains("TileMatrixLimits")) {
// TileMatrix 또는 Identifier 값 추출
String identifier = getChildValue(limitNode, "TileMatrix");
if (identifier == null) identifier = getChildValue(limitNode, "Identifier");
if (identifier == null) {
identifier = getChildValue(limitNode, "Identifier");
}
// 마지막 콜론 이후 값(줌 레벨) 추출
if (identifier != null && identifier.contains(":")) {
String[] parts = identifier.split(":");
String zoomLevel = parts[parts.length - 1];
@@ -291,6 +335,7 @@ public class WmtsService {
}
}
// TileMatrixSetLink 객체 생성 및 추가
if (tileMatrixSet != null) {
WmtsLayerInfo.TileMatrixSetLink link =
new WmtsLayerInfo.TileMatrixSetLink(tileMatrixSet, zoomLevels);
@@ -302,15 +347,73 @@ public class WmtsService {
return links;
}
/**
* Document에서 TileMatrixSet의 TileMatrix 정보를 파싱합니다.
*
* @param doc WMTS Capabilities Document
* @param tileMatrixSetNames 조회할 TileMatrixSet 이름 리스트
* @return TileMatric 객체 리스트
*/
private List<WmtsLayerInfo.TileMatric> parseTileMatrices(
Document doc, List<String> tileMatrixSetNames) {
List<WmtsLayerInfo.TileMatric> allMatrices = new ArrayList<>();
try {
XPathFactory xPathFactory = XPathFactory.newInstance();
XPath xpath = xPathFactory.newXPath();
// 각 TileMatrixSet 이름에 대해 TileMatrix 찾기
for (String tileMatrixSetName : tileMatrixSetNames) {
// TileMatrixSet 찾기
String expression = "//*[local-name()='TileMatrixSet']";
NodeList tileMatrixSetNodes =
(NodeList) xpath.compile(expression).evaluate(doc, XPathConstants.NODESET);
for (int i = 0; i < tileMatrixSetNodes.getLength(); i++) {
Node tileMatrixSetNode = tileMatrixSetNodes.item(i);
String identifier = getChildValue(tileMatrixSetNode, "Identifier");
// 일치하는 TileMatrixSet 찾으면 TileMatrix 파싱
if (tileMatrixSetName.equals(identifier)) {
NodeList children = tileMatrixSetNode.getChildNodes();
for (int j = 0; j < children.getLength(); j++) {
Node node = children.item(j);
if (node.getNodeName().contains("TileMatrix")) {
WmtsLayerInfo.TileMatric tileMatric = new WmtsLayerInfo.TileMatric();
// TileMatrix 정보 추출
tileMatric.setIdentifier(getChildValue(node, "Identifier"));
tileMatric.setScaleDenominator(getChildValue(node, "ScaleDenominator"));
tileMatric.setTopLeftCorner(getChildValue(node, "TopLeftCorner"));
tileMatric.setTileWidth(getChildValue(node, "TileWidth"));
tileMatric.setTileHeight(getChildValue(node, "TileHeight"));
tileMatric.setMatrixWidth(getChildValue(node, "MatrixWidth"));
tileMatric.setMatrixHeight(getChildValue(node, "MatrixHeight"));
allMatrices.add(tileMatric);
}
}
break; // 일치하는 TileMatrixSet 찾았으므로 다음으로
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return allMatrices;
}
// 특정 노드 아래의 자식 태그 값 추출 (예: <Title>값)
private String getChildValue(Node parent, String childName) {
Node child = findChildNode(parent, childName);
return (child != null) ? child.getTextContent() : null;
}
// 특정 노드 아래의 반복되는 자식 구조 값 추출 (예: Keywords -> Keyword)
private List<String> getChildValues(Node parent, String wrapperName, String childName) {
List<String> results = new ArrayList<>();
Node wrapper = findChildNode(parent, wrapperName);
if (wrapper != null) {
NodeList children = wrapper.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
@@ -323,10 +426,10 @@ public class WmtsService {
return results;
}
// Wrapper 없이 바로 반복되는 값 추출 (예: Format)
private List<String> getChildValuesDirect(Node parent, String childName) {
List<String> results = new ArrayList<>();
NodeList children = parent.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node node = children.item(i);
if (node.getNodeName().endsWith(childName)) {
@@ -336,10 +439,12 @@ public class WmtsService {
return results;
}
// 이름으로 자식 노드 찾기 (Local Name 기준)
private Node findChildNode(Node parent, String localName) {
NodeList children = parent.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node node = children.item(i);
// 네임스페이스 접두사(ows:, wmts:)를 무시하고 태그 이름 확인
if (node.getNodeName().endsWith(":" + localName) || node.getNodeName().equals(localName)) {
return node;
}