package com.ktds.act.apigw.api;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.cloud.context.scope.refresh.RefreshScopeRefreshedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import com.ktds.act.apigw.api.config.PortalApi;
import com.ktds.act.apigw.api.db.entity.AdmApiDply;
import com.ktds.act.apigw.api.db.service.AdmApiService;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Component
@Endpoint(id = "api")// /info/api로 변경 가능한지
//InitializingBean Container가 뜨기 전에 설정해주는 값.
public class ApiFactory implements InitializingBean {
private static Map<String, String> uriMap = null; // key -> meth:uriIn, value -> sysId.id
private static Map<String, AdmApiDply> apiMap = null; // key -> sysId.id, value -> ApiDto
private static List<String> portalUriList = null;
private static List<String> portalApiList = null;
private static LocalDateTime lastModifiedDate = null;
private static PathMatcher pathMatcher = new AntPathMatcher();
@Autowired
private PortalApi portalApi;
@Autowired
private AdmApiService admApiService;
/**
* Refresh API configuration
* 내부 연동 API는 Application.yaml에서 관리
* @param e
*/
@EventListener
public void refresh(RefreshScopeRefreshedEvent e) {
log.info("Refresh ApiFactory");
afterPropertiesSet();
}
/**
* API synchronization
*/
@Scheduled(fixedDelayString = "${common.api.schedule.dbSync}", timeUnit = TimeUnit.SECONDS)
public void synchronizeDB() {
// 마지막 동기화 시점 시간을 비교하여 데이터 동기화 처리
LocalDateTime nowTime = LocalDateTime.now();
List<AdmApiDply> dbApiList = null;
String apiUri = "";
String sysIdApiId = "";
Boolean newApiCheck = false;
// 이전 DB 동기화 데이터 건수
int oldTotCnt = apiMap.size();
// DB 동기화
if (lastModifiedDate == null) {
dbApiList = admApiService.getAdmApiList("DPLY");
} else {
dbApiList = admApiService.getAdmApiList(lastModifiedDate);
}
// // 최신 DB 동기화 데이터 건수
// int totCnt = (int) apiRepository.count();
// 추가, 변경분 반영
if(dbApiList.size() > 0) {
for (AdmApiDply apiEntity : dbApiList) {
sysIdApiId = apiEntity.getSysId().concat(".").concat(apiEntity.getApiId()).concat(apiEntity.getVer());
if("DPLY".equals(apiEntity.getDplyType())){
if((apiMap.size() > 0) && !apiMap.containsKey(sysIdApiId) && !newApiCheck) {
newApiCheck = true;
}
for(String meth : apiEntity.getMeth()) {
apiUri = meth.concat(":").concat(apiEntity.getIn());
// URI 정보 추가
uriMap.put(apiUri, sysIdApiId);
}
for(String uriMapKey : uriMap.keySet()) {
if(uriMap.get(uriMapKey).equals(sysIdApiId)) {
if(!apiEntity.getMeth().contains(uriMapKey.split(":")[0])) {
// 기존 URI 정보 삭제
uriMap.remove(uriMapKey);
}
}
}
// API 정보 추가
apiMap.put(sysIdApiId, apiEntity);
log.info("ApiFactory.synchronizeDB - Load : " + apiEntity.toString());
}else{
for(String meth : apiEntity.getMeth()) {
apiUri = meth.concat(":").concat(apiEntity.getIn());
// URI 정보 추가
uriMap.remove(apiUri);
}
apiMap.remove(sysIdApiId);
}
// // 마지막 동기화 시간 현행화
// if (lastModifiedDate == null || (apiEntity.getDplyDt() != null && apiEntity.getDplyDt().isAfter(lastModifiedDate))) {
// lastModifiedDate = nowTime;
// }
}
// 마지막 동기화 시간 현행화
AdmApiDply lastApi = dbApiList.stream().max((o1, o2) -> o1.getDplyDt().compareTo(o2.getDplyDt())).get();
lastModifiedDate = lastApi.getDplyDt();
}
// // DB 삭제분 반영
// if((oldTotCnt > 0) && ((totCnt < oldTotCnt) || ((totCnt >= oldTotCnt) && newApiCheck))) {
// List<String> sysIdAndApiIdList = apiRepository.findSysIdAndApiId();
// // DB에 기존 정보가 없는 경우
// for(String apiMapKey : apiMap.keySet()) {
// if(!sysIdAndApiIdList.contains(apiMapKey)) {
// for(String meth : apiMap.get(apiMapKey).getMeth()) {
// apiUri = meth.concat(":").concat(apiMap.get(apiMapKey).getUriIn());
// // 기존 URI 정보 삭제
// // portal url에 속하지 않을 때만 적용
// if(!portalUriList.contains(apiUri)){
// uriMap.remove(apiUri);
// log.info("ApiFactory.synchronizeDB - Remove : " + apiMapKey);
// }
// }
// // 기존 API 정보 삭제
// // portal api에 속하지 않을 때만 적용
// if(!portalApiList.contains(apiMapKey)){
// apiMap.remove(apiMapKey);
// }
// }
// }
// }
}
/**
* DB, Custom API Loading
*/
@Override
public void afterPropertiesSet() {
log.info("ApiFactory.afterPropertiesSet - Start");
uriMap = new ConcurrentHashMap<String, String>();
apiMap = new ConcurrentHashMap<String, AdmApiDply>();
portalUriList = new ArrayList<>();
portalApiList = new ArrayList<>();
// YAML에 정의된 API 동기화
var portalApiDtos = portalApi.getApi();
String sysIdApiId="";
String apiUri="";
for(AdmApiDply portalApi: portalApiDtos){
sysIdApiId = portalApi.getSysId().concat(".").concat(portalApi.getApiId()).concat(portalApi.getVer());
for(String meth : portalApi.getMeth()) {
apiUri = meth.concat(":").concat(portalApi.getIn());
uriMap.put(apiUri, sysIdApiId);
portalUriList.add(apiUri);
}
apiMap.put(sysIdApiId, portalApi);
portalApiList.add(sysIdApiId);
log.info("Portal Api synchronization : " + portalApi.toString());
} // db에 해당 정보 없어서 밑에 메소드 실행 시 삭제되어버림 => 해당 사항들만 삭제 안되도록 셋팅 필요
synchronizeDB();
// Custom API 동기화
// for (ApiInfo apiVo : customApiList.getApiList()) {
// for (String method : apiVo.getMeth().split("/")) {
// String apiUri = method.concat(":").concat(apiVo.getUriIn());
// apiMap.put(apiUri, apiVo);
// }
// }
log.info("ApiFactory.afterPropertiesSet - End");
}
/**
* API search by method and URI
*
* @param method
* @param uri
* @return api
* **API에 대한 식별 uriIn을 매핑해주는 부분**
*/
public static AdmApiDply getApi(String method, String uri) {
AdmApiDply api = null;
String apiUri = method.concat(":").concat(uri);
try{
if (uriMap.containsKey(apiUri)) {
api = apiMap.get(uriMap.get(apiUri));
} else {
//이 부분에 대해서 path param
List<String> matchingPatterns = new ArrayList<>();
for (String key : uriMap.keySet()) {
String apiPattern = key.split(":")[1];
String apiMethod = key.split(":")[0];
if(checkAPISimilarityWithMethodAndUri(method, uri, apiPattern, apiMethod)){
matchingPatterns.add(apiPattern);
}
}
String bestMatch = findBestMatchPattern(uri, matchingPatterns);
api = findBestMatchApi(method, uri, api, bestMatch);
}
}catch(Exception e) {
e.printStackTrace();
}
return api;
}
public static AdmApiDply getApiByApiId(String apiId) {
AdmApiDply api = null;
for (String key : apiMap.keySet()) {
String[] SysIdNApiId = key.split("\\.");
if (SysIdNApiId[1].equals(apiId)) {
api = apiMap.get(key);
break;
}
}
return api;
}
private static AdmApiDply findBestMatchApi(String method, String uri, AdmApiDply api, String bestMatch) {
if (bestMatch != null) {
api = apiMap.get(uriMap.get(method.concat(":").concat(bestMatch)));
if (api == null) {
if (bestMatch.endsWith("/")) {
api = apiMap.get(uriMap.get(method.concat(":").concat(bestMatch.substring(0, bestMatch.length() - 1))));
}
if (api == null) {
throw new IllegalStateException(
"Could not find handler for best pattern match [" + bestMatch + "]");
}
}
}
return api;
}
/**
* matchingPatterns에 저장된 패턴을 탐색하면서 현재 uri와 가장 적합한 패턴을 찾아 반환한다.
*
*
* @param uri
* @param matchingPatterns
* @return String
*/
private static String findBestMatchPattern(String uri, List<String> matchingPatterns) {
String bestMatch=null;
// comparator 기준 : 더 구체적인지 여부. 패턴이 비슷한지 여부 x. 패턴 비슷한 지 여부는 아래 match에서 확인
Comparator<String> patternComparator = pathMatcher.getPatternComparator(uri);
if (!matchingPatterns.isEmpty()) {
matchingPatterns.sort(patternComparator);
bestMatch = matchingPatterns.get(0);
}
return bestMatch;
}
/**
* 호출된 api의 method와 uriMap에 저장된 method 정보가 같고,
* uriMap에 저장된 uri 패턴에 호출된 api의 uri가 포함된다면 true를 반환한다.
*
* @param method
* @param uri
* @param apiPattern
* @param apiMethod
* @return api
*/
private static boolean checkAPISimilarityWithMethodAndUri(String method, String uri, String apiPattern, String apiMethod) {
//uriMap에 저장된 uri가 /* 으로 끝나는 패턴일 경우, * 하나 더 붙여준다(AntPathMatcher 패턴 처리를 맞추기 위해)
if(apiPattern.endsWith("/*")) {
apiPattern = apiPattern.concat("*");
}
return pathMatcher.match(apiPattern, uri) && apiMethod.equals(method);
}
@ReadOperation
public Map<String, AdmApiDply> apiFeatures() {
return apiMap;
}
}
handlerFactory → 컴파일해서 스프링 컨테이너에 올리면 게이트웨이 입장에서는 그냥 가져다 쓰게 되는 것이다.
HandlerFactory → bodyrefboolmap 이걸 하느냐 안하느냐에 따라서 소도차이가 많이 난다.
핸들러에서 → DPLY가지고만 동기화하다가 스케줄링 돌릴때는 DPLY외에는 모두 지우게 된다. 스프링 컨테이너에서 삭제
응답에서 바디를 무조건 식별할수 있어야 한다? 구체적으로? → 식별
→ 그럴려면 앞에 무조건 nettyWriteResponseFilter → order -1