diff --git a/core/src/main/java/com/ztkmkoo/dss/core/actor/rest/DssRestMasterActor.java b/core/src/main/java/com/ztkmkoo/dss/core/actor/rest/DssRestMasterActor.java index f8936162..13e15911 100644 --- a/core/src/main/java/com/ztkmkoo/dss/core/actor/rest/DssRestMasterActor.java +++ b/core/src/main/java/com/ztkmkoo/dss/core/actor/rest/DssRestMasterActor.java @@ -38,7 +38,7 @@ private DssRestPathResolver dssRestPathResolver(List servic final DssRestPathResolver.Builder builder = DssRestPathResolver.builder(); serviceList.forEach(service -> { final ActorRef serviceActor = context.spawn(DssRestServiceActor.create(service), service.getName()); - builder.addServiceActor(service.getMethodType(), service.getPath(), serviceActor); + builder.addService(service, serviceActor); }); return builder.build(); } @@ -54,7 +54,7 @@ private Behavior handlingDssRestMasterActorCommandReq context.getLog().info("DssRestMasterActorCommandRequest: {}", request); - final Optional> optional = dssRestPathResolver.getStaticServiceActor(request.getMethodType(), request.getPath()); + final Optional> optional = dssRestPathResolver.getPathServiceActor(request.getMethodType(), request.getPath()); if (optional.isPresent()) { optional.get().tell(new DssRestServiceActorCommandRequest(request)); } else { diff --git a/core/src/main/java/com/ztkmkoo/dss/core/actor/rest/DssRestPathResolver.java b/core/src/main/java/com/ztkmkoo/dss/core/actor/rest/DssRestPathResolver.java index 1e574bb9..d4bb90a7 100644 --- a/core/src/main/java/com/ztkmkoo/dss/core/actor/rest/DssRestPathResolver.java +++ b/core/src/main/java/com/ztkmkoo/dss/core/actor/rest/DssRestPathResolver.java @@ -1,6 +1,10 @@ package com.ztkmkoo.dss.core.actor.rest; import akka.actor.typed.ActorRef; +import com.ztkmkoo.dss.core.actor.rest.entity.DssRestPath; +import com.ztkmkoo.dss.core.actor.rest.entity.DssRestPathComposition; +import com.ztkmkoo.dss.core.actor.rest.service.DssRestActorService; +import com.ztkmkoo.dss.core.actor.rest.util.DssRestPathUtils; import com.ztkmkoo.dss.core.exception.DssRestServiceMappingException; import com.ztkmkoo.dss.core.message.rest.DssRestServiceActorCommand; import com.ztkmkoo.dss.core.network.rest.enumeration.DssRestMethodType; @@ -17,19 +21,56 @@ public class DssRestPathResolver { private static final Logger logger = LoggerFactory.getLogger(DssRestPathResolver.class); - private final Map>> staticServiceActorMap; + + private final Map> staticPathCompositionMap; + private final Map> dynamicPathCompositionMap; private DssRestPathResolver(Builder builder) { - this.staticServiceActorMap = Collections.unmodifiableMap(builder.staticServiceActorMap); + this.staticPathCompositionMap = Collections.unmodifiableMap(builder.staticPathCompositionMap); + this.dynamicPathCompositionMap = Collections.unmodifiableMap(builder.dynamicPathCompositionMap); + } + + public Optional> getPathServiceActor(DssRestMethodType methodType, String path) { + final String fixedPath = DssRestPathUtils.getFixedPath(path); + final Optional> fixedOptional = getStaticPathServiceActor(methodType, fixedPath); + if (fixedOptional.isPresent()) { + return fixedOptional; + } + + return getDynamicPathServiceActor(methodType, fixedPath); + } + + private Optional> getStaticPathServiceActor(DssRestMethodType methodType, String fixedPath) { + if (!staticPathCompositionMap.containsKey(methodType)) { + return Optional.empty(); + } + + final Map map = staticPathCompositionMap.get(methodType); + + if (!map.containsKey(fixedPath)) { + return Optional.empty(); + } + + return Optional.of(map.get(fixedPath).getServiceActor()); } - public Optional> getStaticServiceActor(DssRestMethodType methodType, String path) { - return Optional - .ofNullable( - staticServiceActorMap - .getOrDefault(methodType, Collections.emptyMap()) - .get(path) - ); + private Optional> getDynamicPathServiceActor(DssRestMethodType methodType, String fixedPath) { + if (!dynamicPathCompositionMap.containsKey(methodType)) { + return Optional.empty(); + } + + final List list = dynamicPathCompositionMap.get(methodType); + if (list.isEmpty()) { + return Optional.empty(); + } + + for (DssRestPathComposition composition : list) { + if(composition.getDssRestPath().match(fixedPath)) { + return Optional.of(composition.getServiceActor()); + } + } + + return Optional.empty(); } public static Builder builder() { @@ -38,33 +79,104 @@ public static Builder builder() { public static class Builder { - private Map>> staticServiceActorMap = new EnumMap<>(DssRestMethodType.class); + private final Map> staticPathCompositionMap = new EnumMap<>(DssRestMethodType.class); + private final Map> dynamicPathCompositionMap = new EnumMap<>(DssRestMethodType.class); - public Builder addServiceActor(DssRestMethodType methodType, String path, ActorRef serviceActor) { - Objects.requireNonNull(methodType); - Objects.requireNonNull(path); + public Builder addService(DssRestActorService service, ActorRef serviceActor) { + Objects.requireNonNull(service); + Objects.requireNonNull(service.getMethodType()); + Objects.requireNonNull(service.getPath()); Objects.requireNonNull(serviceActor); - if (!staticServiceActorMap.containsKey(methodType)) { - staticServiceActorMap.put(methodType, new HashMap<>()); + final String fixedPath = DssRestPathUtils.getFixedPath(service.getPath()); + if (DssRestPathUtils.isDynamicPath(fixedPath)) { + addDynamicPathService(service, serviceActor); + } else { + addStaticPathService(service, serviceActor); + } + + return this; + } + + private void addStaticPathService(DssRestActorService service, ActorRef serviceActor) { + if(!staticPathCompositionMap.containsKey(service.getMethodType())) { + staticPathCompositionMap.put(service.getMethodType(), new HashMap<>()); } - if (staticServiceActorMap.get(methodType).containsKey(path)) { + final Map map = staticPathCompositionMap.get(service.getMethodType()); + final String path = service.getPath(); + if (map.containsKey(path)) { throw new DssRestServiceMappingException("Cannot map same path: " + path); } - staticServiceActorMap.get(methodType).put(path, serviceActor); - return this; + + map.put( + service.getPath(), + DssRestPathComposition + .builder() + .serviceActor(serviceActor) + .dssRestPath( + DssRestPath + .builder() + .path(path) + .build() + ) + .build() + ); } - public DssRestPathResolver build() { - if (!staticServiceActorMap.isEmpty()) { - staticServiceActorMap.forEach((methodType, map) -> map.forEach((path, actorRef) -> - logger.info("Add mapping {} {} to {}", - methodType.name(), path, actorRef.path().name()) - )); + private void addDynamicPathService(DssRestActorService service, ActorRef serviceActor) { + if(!dynamicPathCompositionMap.containsKey(service.getMethodType())) { + dynamicPathCompositionMap.put(service.getMethodType(), new ArrayList<>()); } + final List list = dynamicPathCompositionMap.get(service.getMethodType()); + final String path = service.getPath(); + + list.add( + DssRestPathComposition + .builder() + .serviceActor(serviceActor) + .dssRestPath( + DssRestPath + .builder() + .path(path) + .build() + ) + .build() + ); + } + + public DssRestPathResolver build() { + logStaticMappingInfo(); + logDynamicMappingInfo(); + return new DssRestPathResolver(this); } + + private void logStaticMappingInfo() { + if (staticPathCompositionMap.isEmpty()) { + return; + } + + staticPathCompositionMap.forEach((methodType, map) -> + map.forEach((path, composition) -> + logger.info("Add static mapping: [{}] {} to {}", + methodType, path, composition.getServiceActor().path().name()) + ) + ); + } + + private void logDynamicMappingInfo() { + if (dynamicPathCompositionMap.isEmpty()) { + return; + } + + dynamicPathCompositionMap.forEach((methodType, map) -> + map.forEach(composition -> + logger.info("Add static mapping: [{}] {} to {}", methodType, + composition.getDssRestPath().getRowPath(), composition.getServiceActor().path().name()) + ) + ); + } } } diff --git a/core/src/main/java/com/ztkmkoo/dss/core/actor/rest/entity/DssRestPath.java b/core/src/main/java/com/ztkmkoo/dss/core/actor/rest/entity/DssRestPath.java new file mode 100644 index 00000000..89b78a5c --- /dev/null +++ b/core/src/main/java/com/ztkmkoo/dss/core/actor/rest/entity/DssRestPath.java @@ -0,0 +1,134 @@ +package com.ztkmkoo.dss.core.actor.rest.entity; + +import lombok.Builder; +import lombok.Getter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Serializable; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Project: dss + * Created by: @ztkmkoo(ztkmkoo@gmail.com) + * Date: 20. 4. 13. 오전 2:06 + */ +@Getter +public class DssRestPath implements Serializable { + + private static final long serialVersionUID = 8287023454874773276L; + + private final transient Logger logger = LoggerFactory.getLogger(DssRestPath.class); + + private static final String PATH_SEPARATOR_REG_EX = "\\/"; + private static final String PATH_NORMAL_REG_EX = "[^\\/]+"; + private static final String PATH_DIGITAL_REG_EX = "(\\d+)"; + private static final String PATH_VARIABLE_REG_EX = "\\{[^\\/]+\\}"; + private static final Pattern PATH_NORMAL_PATTERN = Pattern.compile(PATH_NORMAL_REG_EX); + private static final Pattern PATH_VARIABLE_PATTERN = Pattern.compile(PATH_VARIABLE_REG_EX); + + private final Pattern pattern; + private final String rowPath; + private final Map staticPathMap = new HashMap<>(); + + @Builder + private DssRestPath(String path, Map> classMap) { + final String regex = getRegEx(path, Objects.nonNull(classMap) ? classMap : Collections.emptyMap()); + this.pattern = Pattern.compile(regex); + this.rowPath = path; + logger.info("Regex: {}", regex); + } + + private String getRegEx(String path, Map> classMap) { + final StringBuilder sb = new StringBuilder(); + + final List pathElements = pathElements(path); + for (int i = 0; i < pathElements.size(); i++) { + final String pathElement = pathElements.get(i); + if (isPathVariablePattern(pathElement)) { + final String pathVariableName = pathElement.substring(1, pathElement.length() - 1); + + final Class tClass = classMap.getOrDefault(pathVariableName, String.class); + final String regEx = regExFromClassType(tClass); + sb.append(PATH_SEPARATOR_REG_EX).append(regEx); + } else { + sb + .append(PATH_SEPARATOR_REG_EX) + .append(PATH_NORMAL_REG_EX); + staticPathMap.put(i, pathElement); + } + } + + return sb.append("$").toString(); + } + + public boolean match(String path) { + Objects.requireNonNull(pattern); + final String fixedPath = getFixedPath(path); + final Matcher matcher = pattern.matcher(fixedPath); + if (!matcher.find()) { + logger.debug("Path regEx not matched: {}", path); + return false; + } + + logger.debug("Path regEx matched, try to check static path element: {}", path); + return matchStaticPath(fixedPath, staticPathMap); + } + + private boolean matchStaticPath(String path, Map staticPathMap) { + final List pathElements = pathElements(path); + logger.debug("user request path: {}, expected path: {}", pathElements, rowPath); + + for (Map.Entry entry : staticPathMap.entrySet()) { + final int index = entry.getKey(); + final String staticPathElement = entry.getValue(); + + if (index >= pathElements.size()) { + return false; + } + + final String pathElement = pathElements.get(index); + if (!staticPathElement.equals(pathElement)) { + return false; + } + } + + return true; + } + + private static List pathElements(String path) { + final List list = new ArrayList<>(); + + final String fixedPath = getFixedPath(path); + + final Matcher matcher = PATH_NORMAL_PATTERN.matcher(fixedPath); + while (matcher.find()) { + list.add(matcher.group()); + } + + return list; + } + + private static boolean isPathVariablePattern(String pathElement) { + final Matcher matcher = PATH_VARIABLE_PATTERN.matcher(pathElement); + return matcher.find(); + } + + private static String regExFromClassType(Class tClass) { + if (Number.class.isAssignableFrom(tClass)) { + return PATH_DIGITAL_REG_EX; + } else { + return PATH_NORMAL_REG_EX; + } + } + + public static String getFixedPath(String path) { + if (path.contains("?")) { + return path.substring(0, path.indexOf('?')); + } else { + return path; + } + } +} \ No newline at end of file diff --git a/core/src/main/java/com/ztkmkoo/dss/core/actor/rest/entity/DssRestPathComposition.java b/core/src/main/java/com/ztkmkoo/dss/core/actor/rest/entity/DssRestPathComposition.java new file mode 100644 index 00000000..c5e25d5a --- /dev/null +++ b/core/src/main/java/com/ztkmkoo/dss/core/actor/rest/entity/DssRestPathComposition.java @@ -0,0 +1,27 @@ +package com.ztkmkoo.dss.core.actor.rest.entity; + +import akka.actor.typed.ActorRef; +import com.ztkmkoo.dss.core.message.rest.DssRestServiceActorCommand; +import lombok.Builder; +import lombok.Getter; + +import java.io.Serializable; + +/** + * Project: dss + * Created by: @ztkmkoo(ztkmkoo@gmail.com) + * Date: 20. 4. 18. 오전 4:09 + */ +@Getter +public class DssRestPathComposition implements Serializable { + private static final long serialVersionUID = 6447860854358860483L; + + private final DssRestPath dssRestPath; + private final ActorRef serviceActor; + + @Builder + private DssRestPathComposition(DssRestPath dssRestPath, ActorRef serviceActor) { + this.dssRestPath = dssRestPath; + this.serviceActor = serviceActor; + } +} diff --git a/core/src/main/java/com/ztkmkoo/dss/core/actor/rest/util/DssRestPathCompositionUtils.java b/core/src/main/java/com/ztkmkoo/dss/core/actor/rest/util/DssRestPathCompositionUtils.java new file mode 100644 index 00000000..07a2f4d2 --- /dev/null +++ b/core/src/main/java/com/ztkmkoo/dss/core/actor/rest/util/DssRestPathCompositionUtils.java @@ -0,0 +1,11 @@ +package com.ztkmkoo.dss.core.actor.rest.util; + +/** + * Project: dss + * Created by: @ztkmkoo(ztkmkoo@gmail.com) + * Date: 20. 4. 18. 오전 4:11 + */ +public class DssRestPathCompositionUtils { + + private DssRestPathCompositionUtils() {} +} diff --git a/core/src/main/java/com/ztkmkoo/dss/core/actor/rest/util/DssRestPathUtils.java b/core/src/main/java/com/ztkmkoo/dss/core/actor/rest/util/DssRestPathUtils.java new file mode 100644 index 00000000..3eb8936e --- /dev/null +++ b/core/src/main/java/com/ztkmkoo/dss/core/actor/rest/util/DssRestPathUtils.java @@ -0,0 +1,89 @@ +package com.ztkmkoo.dss.core.actor.rest.util; + +import com.ztkmkoo.dss.core.util.StringUtils; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Project: dss + * Created by: @ztkmkoo(ztkmkoo@gmail.com) + * Date: 20. 4. 18. 오전 4:23 + */ +public class DssRestPathUtils { + + // normal static string + private static final String EMPTY_STRING = ""; + + // regEx + private static final String PATH_VARIABLE_REG_EX = "\\{[^\\/]+\\}"; + + // regEx pattern + private static final Pattern PATH_VARIABLE_PATTERN = Pattern.compile(PATH_VARIABLE_REG_EX); + + private DssRestPathUtils() {} + + /** + * get pure uri path without query params + * @param path : uri path + * @return pure uri path + */ + public static String getFixedPath(String path) { + if (containRequestParam(path)) { + return path.substring(0, path.indexOf('?')); + } else { + return path; + } + } + + /** + * get query param as string + * @param path : uri path + * @return query param string + */ + public static String getRequestParamString(String path) { + if (containRequestParam(path)) { + return path.substring(path.indexOf('?') + 1); + } else { + return EMPTY_STRING; + } + } + + /** + * check if contains query params + * @param path : uri path + * @return contains query params return true + */ + public static boolean containRequestParam(String path) { + return path.contains("?"); + } + + public static Map getRequestParams(String query) { + if (StringUtils.isEmpty(query)) { + return Collections.emptyMap(); + } + + final String[] queries = query.split("&"); + final Map map = new HashMap<>(queries.length); + + for (String q : queries) { + if (q.contains("=")) { + final int index = q.indexOf('='); + final String key = q.substring(0, index); + final String value = q.substring(index + 1); + map.put(key, value); + } + } + + return map; + } + + public static boolean isDynamicPath(String path) { + final String fixedPath = getFixedPath(path); + final Matcher matcher = PATH_VARIABLE_PATTERN.matcher(fixedPath); + return matcher.find(); + } +} \ No newline at end of file diff --git a/core/src/test/java/com/ztkmkoo/dss/core/actor/rest/entity/DssRestPathTest.java b/core/src/test/java/com/ztkmkoo/dss/core/actor/rest/entity/DssRestPathTest.java new file mode 100644 index 00000000..b5c3d690 --- /dev/null +++ b/core/src/test/java/com/ztkmkoo/dss/core/actor/rest/entity/DssRestPathTest.java @@ -0,0 +1,62 @@ +package com.ztkmkoo.dss.core.actor.rest.entity; + +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Project: dss + * Created by: @ztkmkoo(ztkmkoo@gmail.com) + * Date: 20. 4. 13. 오후 1:23 + */ +public class DssRestPathTest { + + @Test + public void match() { + final Map> classMap = new HashMap<>(); + classMap.put("name", String.class); + classMap.put("age", Integer.class); + + final DssRestPath dssRestPath = DssRestPath + .builder() + .path("/hi/{name}/{age}") + .classMap(classMap) + .build(); + + assertTrue(dssRestPath.match("/hi/Kebron/34")); + } + + @Test + public void match2() { + final Map> classMap = new HashMap<>(); + classMap.put("name", String.class); + classMap.put("age", Integer.class); + + final DssRestPath dssRestPath = DssRestPath + .builder() + .path("/hi/{name}/{age}") + .classMap(classMap) + .build(); + + assertFalse(dssRestPath.match("/hi/Kebron/thirtyFour")); + } + + @Test + public void match3() { + final Map> classMap = new HashMap<>(); + classMap.put("name", String.class); + classMap.put("age", Integer.class); + + final DssRestPath dssRestPath = DssRestPath + .builder() + .path("/hi/{name}/{age}/test") + .classMap(classMap) + .build(); + + assertTrue(dssRestPath.match("/hi/Kebron/15/test?id=ztkmkoo")); + } +} \ No newline at end of file diff --git a/core/src/test/java/com/ztkmkoo/dss/core/actor/rest/util/DssRestPathUtilsTest.java b/core/src/test/java/com/ztkmkoo/dss/core/actor/rest/util/DssRestPathUtilsTest.java new file mode 100644 index 00000000..0bd25fb5 --- /dev/null +++ b/core/src/test/java/com/ztkmkoo/dss/core/actor/rest/util/DssRestPathUtilsTest.java @@ -0,0 +1,46 @@ +package com.ztkmkoo.dss.core.actor.rest.util; + +import org.junit.Test; + +import java.util.Map; + +import static org.junit.Assert.*; + +/** + * Project: dss + * Created by: @ztkmkoo(ztkmkoo@gmail.com) + * Date: 20. 4. 18. 오전 4:36 + */ +public class DssRestPathUtilsTest { + + private final String path = "/test?id=z"; + + @Test + public void getFixedPath() { + final String fixedPath = DssRestPathUtils.getFixedPath(path); + + assertEquals("/test", fixedPath); + } + + @Test + public void getRequestParamString() { + final String paramString = DssRestPathUtils.getRequestParamString(path); + + assertEquals("id=z", paramString); + } + + @Test + public void containRequestParam() { + assertTrue(DssRestPathUtils.containRequestParam(path)); + } + + @Test + public void getRequestParams() { + final String paramString = DssRestPathUtils.getRequestParamString(path); + final Map param = DssRestPathUtils.getRequestParams(paramString); + + assertNotNull(param); + assertEquals(1, param.size()); + assertEquals("z", param.get("id")); + } +} \ No newline at end of file