From 237ea589d43dba9b4027e095d44ac2242021217e Mon Sep 17 00:00:00 2001 From: "Ahmad K. Bawaneh" Date: Wed, 6 Nov 2024 11:43:43 +0300 Subject: [PATCH] add RestRequestBuilder, add global header, query and path parameters, remove dependency on domino-history --- .../org/dominokit/rest/DominoRestConfig.java | 80 ++++ .../org/dominokit/rest/DominoRestConfig.java | 80 ++++ domino-rest-shared/pom.xml | 5 - .../rest/shared/request/RestConfig.java | 23 + .../rest/shared/request/ServerRequest.java | 5 +- .../rest/shared/request/ServicePath.java | 394 ++++++++++++++++++ .../rest/shared/request/UrlFormatter.java | 7 +- .../request/builder/RestRequestBuilder.java | 4 +- pom.xml | 7 +- 9 files changed, 589 insertions(+), 16 deletions(-) create mode 100644 domino-rest-shared/src/main/java/org/dominokit/rest/shared/request/ServicePath.java diff --git a/domino-rest-client/src/main/java/org/dominokit/rest/DominoRestConfig.java b/domino-rest-client/src/main/java/org/dominokit/rest/DominoRestConfig.java index c7204a6..86217f9 100644 --- a/domino-rest-client/src/main/java/org/dominokit/rest/DominoRestConfig.java +++ b/domino-rest-client/src/main/java/org/dominokit/rest/DominoRestConfig.java @@ -19,7 +19,9 @@ import static java.util.Objects.nonNull; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.dominokit.jackson.JacksonContextProvider; import org.dominokit.rest.js.DefaultServiceRoot; import org.dominokit.rest.js.ServerEventFactory; @@ -69,6 +71,10 @@ public class DominoRestConfig implements RestConfig { private static NullQueryParamStrategy nullQueryParamStrategy = NullQueryParamStrategy.EMPTY; + private static final Map globalPathParams = new HashMap<>(); + private static final Map globalHeaderParams = new HashMap<>(); + private static final Map> globalQueryParams = new HashMap<>(); + /** * Gets and initialize the instance with the default configurations * @@ -270,4 +276,78 @@ public UrlTokenRegexMatcher getUrlTokenRegexMatcher() { return url; }; } + + @Override + public Map getGlobalPathParameters() { + return globalPathParams; + } + + @Override + public Map getGlobalHeaderParameters() { + return globalHeaderParams; + } + + @Override + public Map> getGlobalQueryParameters() { + return globalQueryParams; + } + + @Override + public RestConfig setGlobalPathParameter(String name, String value) { + getGlobalPathParameters().put(name, value); + return this; + } + + @Override + public RestConfig setGlobalPathParameters(Map pathParameters) { + getGlobalPathParameters().putAll(pathParameters); + return this; + } + + @Override + public RestConfig setGlobalHeaderParameter(String name, String value) { + getGlobalHeaderParameters().put(name, value); + return this; + } + + @Override + public RestConfig setGlobalHeaderParameters(Map headerParameters) { + getGlobalHeaderParameters().putAll(headerParameters); + return this; + } + + @Override + public RestConfig setGlobalQueryParameter(String name, String value) { + getGlobalQueryParameters().put(name, new ArrayList<>()); + addGlobalQueryParameter(name, value); + return this; + } + + @Override + public RestConfig addGlobalQueryParameter(String name, String value) { + if (getGlobalQueryParameters().containsKey(name)) { + getGlobalQueryParameters().get(name).add(value); + } else { + setGlobalQueryParameter(name, value); + } + return this; + } + + @Override + public RestConfig setGlobalQueryParameters(Map> parameters) { + parameters + .keySet() + .forEach( + name -> parameters.get(name).forEach(value -> addGlobalQueryParameter(name, value))); + return this; + } + + @Override + public RestConfig addGlobalQueryParameters(Map> parameters) { + parameters.forEach( + (key, values) -> { + values.forEach(value -> addGlobalQueryParameter(key, value)); + }); + return this; + } } diff --git a/domino-rest-jvm/src/main/java/org/dominokit/rest/DominoRestConfig.java b/domino-rest-jvm/src/main/java/org/dominokit/rest/DominoRestConfig.java index eaab975..d6d96aa 100644 --- a/domino-rest-jvm/src/main/java/org/dominokit/rest/DominoRestConfig.java +++ b/domino-rest-jvm/src/main/java/org/dominokit/rest/DominoRestConfig.java @@ -20,7 +20,9 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; @@ -70,6 +72,10 @@ public class DominoRestConfig implements RestConfig { private static NullQueryParamStrategy nullQueryParamStrategy = NullQueryParamStrategy.EMPTY; + private static final Map globalPathParams = new HashMap<>(); + private static final Map globalHeaderParams = new HashMap<>(); + private static final Map> globalQueryParams = new HashMap<>(); + /** * Gets and initialize the instance with the default configurations * @@ -270,4 +276,78 @@ public UrlTokenRegexMatcher getUrlTokenRegexMatcher() { return url; }; } + + @Override + public Map getGlobalPathParameters() { + return globalPathParams; + } + + @Override + public Map getGlobalHeaderParameters() { + return globalHeaderParams; + } + + @Override + public Map> getGlobalQueryParameters() { + return globalQueryParams; + } + + @Override + public RestConfig setGlobalPathParameter(String name, String value) { + getGlobalPathParameters().put(name, value); + return this; + } + + @Override + public RestConfig setGlobalPathParameters(Map pathParameters) { + getGlobalPathParameters().putAll(pathParameters); + return this; + } + + @Override + public RestConfig setGlobalHeaderParameter(String name, String value) { + getGlobalHeaderParameters().put(name, value); + return this; + } + + @Override + public RestConfig setGlobalHeaderParameters(Map headerParameters) { + getGlobalHeaderParameters().putAll(headerParameters); + return this; + } + + @Override + public RestConfig setGlobalQueryParameter(String name, String value) { + getGlobalQueryParameters().put(name, new ArrayList<>()); + addGlobalQueryParameter(name, value); + return this; + } + + @Override + public RestConfig addGlobalQueryParameter(String name, String value) { + if (getGlobalQueryParameters().containsKey(name)) { + getGlobalQueryParameters().get(name).add(value); + } else { + setGlobalQueryParameter(name, value); + } + return this; + } + + @Override + public RestConfig setGlobalQueryParameters(Map> parameters) { + parameters + .keySet() + .forEach( + name -> parameters.get(name).forEach(value -> addGlobalQueryParameter(name, value))); + return this; + } + + @Override + public RestConfig addGlobalQueryParameters(Map> parameters) { + parameters.forEach( + (key, values) -> { + values.forEach(value -> addGlobalQueryParameter(key, value)); + }); + return this; + } } diff --git a/domino-rest-shared/pom.xml b/domino-rest-shared/pom.xml index c442e28..41cf053 100644 --- a/domino-rest-shared/pom.xml +++ b/domino-rest-shared/pom.xml @@ -46,11 +46,6 @@ domino-aggregator-shared ${domino.aggregator.version} - - org.dominokit - domino-history-shared - ${domino.history.version} - javax.annotation javax.annotation-api diff --git a/domino-rest-shared/src/main/java/org/dominokit/rest/shared/request/RestConfig.java b/domino-rest-shared/src/main/java/org/dominokit/rest/shared/request/RestConfig.java index 85e7dc1..da3917b 100644 --- a/domino-rest-shared/src/main/java/org/dominokit/rest/shared/request/RestConfig.java +++ b/domino-rest-shared/src/main/java/org/dominokit/rest/shared/request/RestConfig.java @@ -17,6 +17,7 @@ import java.util.Date; import java.util.List; +import java.util.Map; import org.dominokit.rest.shared.request.service.annotations.DateFormat; /** The global configurations for domino rest. */ @@ -176,6 +177,28 @@ default NullQueryParamStrategy getNullQueryParamStrategy() { UrlTokenRegexMatcher getUrlTokenRegexMatcher(); + Map getGlobalPathParameters(); + + Map getGlobalHeaderParameters(); + + Map> getGlobalQueryParameters(); + + RestConfig setGlobalPathParameter(String name, String value); + + RestConfig setGlobalPathParameters(Map pathParameters); + + RestConfig setGlobalHeaderParameter(String name, String value); + + RestConfig setGlobalHeaderParameters(Map headerParameters); + + RestConfig setGlobalQueryParameter(String name, String value); + + RestConfig addGlobalQueryParameter(String name, String value); + + RestConfig setGlobalQueryParameters(Map> parameters); + + RestConfig addGlobalQueryParameters(Map> parameters); + /** Formatter to format the date parameter based on a patter */ @FunctionalInterface interface DateParamFormatter { diff --git a/domino-rest-shared/src/main/java/org/dominokit/rest/shared/request/ServerRequest.java b/domino-rest-shared/src/main/java/org/dominokit/rest/shared/request/ServerRequest.java index 4488f91..681fb2e 100644 --- a/domino-rest-shared/src/main/java/org/dominokit/rest/shared/request/ServerRequest.java +++ b/domino-rest-shared/src/main/java/org/dominokit/rest/shared/request/ServerRequest.java @@ -314,7 +314,10 @@ public void normalizeUrl() { (isNull(this.serviceRoot) || this.serviceRoot.isEmpty()) ? ServiceRootMatcher.matchedServiceRoot(path) : (this.serviceRoot + path); - UrlFormatter urlFormatter = new UrlFormatter(pathParameters); + Map combinedParams = new HashMap<>(); + combinedParams.putAll(DominoRestContext.make().getConfig().getGlobalPathParameters()); + combinedParams.putAll(pathParameters); + UrlFormatter urlFormatter = new UrlFormatter(combinedParams); this.setUrl(urlFormatter.formatUrl(root)); } } diff --git a/domino-rest-shared/src/main/java/org/dominokit/rest/shared/request/ServicePath.java b/domino-rest-shared/src/main/java/org/dominokit/rest/shared/request/ServicePath.java new file mode 100644 index 0000000..2782754 --- /dev/null +++ b/domino-rest-shared/src/main/java/org/dominokit/rest/shared/request/ServicePath.java @@ -0,0 +1,394 @@ +/* + * Copyright © 2019 Dominokit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.dominokit.rest.shared.request; + +import static java.util.Objects.isNull; +import static java.util.Objects.nonNull; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +public class ServicePath { + + private static final String QUERY_REGEX = "\\?"; + private static final String FRAGMENT_REGEX = "\\#"; + private final String rootPath; + private List paths = new LinkedList<>(); + private List queryParameters = new LinkedList<>(); + private List fragments = new LinkedList<>(); + + /** @param token String, a URL token */ + public ServicePath(String token) { + this("", token); + } + + /** + * @param rootPath String, the root path token + * @param token String, a URL token + */ + public ServicePath(String rootPath, String token) { + if (isNull(token)) throw new IllegalArgumentException(); + this.rootPath = isNull(rootPath) ? "" : rootPath.trim(); + String rebasedToken = rebaseToken(rootPath, token); + this.paths.addAll(asPathsList(rebasedToken)); + this.queryParameters.addAll(asQueryParameters(rebasedToken)); + this.fragments.addAll(parseFragments(rebasedToken)); + } + + private String rebaseToken(String rootPath, String token) { + if (isNull(rootPath) || rootPath.trim().isEmpty() || !token.startsWith(rootPath)) { + return token; + } + return token.substring(rootPath.length()); + } + + private List parseFragments(String token) { + if (token.contains("#") && token.indexOf("#") < token.length() - 1) + return asPathsList(token.split(FRAGMENT_REGEX)[1]); + return new LinkedList<>(); + } + + /** + * @param path The path to check for + * @return true if the path part of the url ends with the specified path otherwise returns + * false + */ + public boolean endsWithPath(String path) { + if (isEmpty(path)) return false; + return endsWith(paths(), asPathsList(path)); + } + + private boolean endsWith(List paths, List targets) { + if (isValidSize(paths, targets)) return matchEnds(paths, targets); + return false; + } + + private boolean matchEnds(List paths, List targets) { + int offset = paths.size() - targets.size(); + return IntStream.range(0, targets.size()) + .allMatch(i -> targets.get(i).equals(paths.get(i + offset))); + } + + /** + * @return a list of Strings representing all paths of a url, e.g + * http://localhost:8080/a/b/c will return a list contains a, b, + * c, + */ + public List paths() { + return paths; + } + + /** + * @return a list of Strings representing all fragments of a url, e.g + * http://localhost:8080/a/b/c#d/e/f will return a list contains d, d, + * f, + */ + public List fragments() { + return fragments; + } + + /** + * @return the path part of a url, e.g http://localhost:8080/a/b/c will return a/b/c + */ + public String path() { + return String.join("/", paths()); + } + + /** @return the string representing the whole query part of a token */ + public String query() { + return queryParameters.stream().map(Parameter::asQueryString).collect(Collectors.joining("&")); + } + + /** + * @param name name of the query parameter + * @return True if the token has a query param that has the specified name, otherwise + * returns false. + */ + public boolean hasQueryParameter(String name) { + Optional param = + queryParameters.stream().filter(parameter -> parameter.key.equals(name)).findFirst(); + + if (param.isPresent()) { + return true; + } else { + return false; + } + } + + /** + * Adds a query parameter with specified name and value to the current token, if a query parameter + * with same name already exists, then replaces its value with the new one + * + * @param name query parameter name + * @param value query parameter value + * @return {@link ServicePath} that contains the new query parameter. + */ + public ServicePath setQueryParameter(String name, String value) { + if (hasQueryParameter(name)) { + removeParameter(name); + } + appendParameter(name, value); + return this; + } + + /** {@inheritDoc} */ + public ServicePath setQueryParameter(String name, List values) { + if (hasQueryParameter(name)) { + removeParameter(name); + } + appendParameter(name, values); + return this; + } + + private Parameter getParameter(String name) { + Optional param = + queryParameters.stream().filter(parameter -> parameter.key.equals(name)).findFirst(); + + if (param.isPresent()) { + return param.get(); + } else { + return null; + } + } + + /** + * Appends a new query parameter to the end of the token query parameters part. + * + * @param name of the query parameter + * @param value of the query parameter + * @return {@link ServicePath} with the new query parameter appended to the end of query part. + */ + public ServicePath appendParameter(String name, String value) { + return appendParameter(name, asList(value)); + } + + /** {@inheritDoc} */ + public ServicePath appendParameter(String name, List values) { + if (nonNull(name) && !name.trim().isEmpty()) { + if (hasQueryParameter(name)) { + getParameter(name).addValues(values); + } else { + this.queryParameters.add(new Parameter(name, values)); + } + } + return this; + } + + /** + * Replaces the first occurrence of a path segment with the replacement + * + * @param path The path segment to be replaced + * @param replacement the new path segment + * @return {@link ServicePath} with path segment replaced by the replacement + */ + public ServicePath replacePath(String path, String replacement) { + List paths = asPathsList(path()); + if (paths.contains(path)) { + int i = paths.indexOf(path); + paths.add(i, replacement); + paths.remove(i + 1); + this.paths = paths; + } + return this; + } + + private List asList(String value) { + List values = new ArrayList<>(); + values.add(value); + return values; + } + + /** + * Removes the query parameter with the specified name + * + * @param name of the parameter to be removed + * @return {@link ServicePath} with the query parameter with the specified name being removed + */ + public ServicePath removeParameter(String name) { + Parameter parameter = getParameter(name); + if (nonNull(parameter)) { + this.queryParameters.remove(parameter); + } + return this; + } + + /** @return the string representing the whole fragment part of a token */ + public String fragment() { + return String.join("/", fragments()); + } + + /** + * @return true if all of token (path part, query part, fragments part) are empty, + * otherwise return false. + */ + public boolean isEmpty() { + return paths.isEmpty() && queryParameters.isEmpty() && fragments.isEmpty(); + } + + /** @return the full string representation of a {@link ServicePath} */ + public String value() { + String path = path(); + String separator = + (getRootPath().isEmpty() + || getRootPath().endsWith("/") + || path.startsWith("/") + || path.isEmpty()) + ? "" + : "/"; + return getRootPath() + separator + noRootValue(); + } + + /** {@inheritDoc} */ + public String noRootValue() { + return path() + appendQuery(query()) + appendFragment(); + } + + private String appendFragment() { + return isEmpty(fragment()) ? "" : "#" + fragment(); + } + + private String appendQuery(String query) { + return isEmpty(query) ? "" : "?" + query; + } + + private List asPathsList(String token) { + if (isNull(token) || isEmpty(token) || token.startsWith("?") || token.startsWith("#")) + return new ArrayList<>(); + return Arrays.stream(splittedPaths(token)) + .filter(p -> !p.isEmpty()) + .collect(Collectors.toCollection(LinkedList::new)); + } + + private String[] splittedPaths(String pathString) { + return parsePathPart(pathString).split("/"); + } + + private String parsePathPart(String pathString) { + return pathString.replace("!", "").split(QUERY_REGEX)[0].split(FRAGMENT_REGEX)[0]; + } + + private boolean isEmpty(String path) { + return isNull(path) || path.isEmpty(); + } + + private boolean isValidSize(List paths, List targets) { + return !targets.isEmpty() && targets.size() <= paths.size(); + } + + private List asQueryParameters(String token) { + + String queryString = queryPart(token); + if (isNull(queryString) || queryString.trim().isEmpty()) { + return new LinkedList<>(); + } + return parsedParameters(queryString); + } + + private List parsedParameters(String queryString) { + + return Stream.of(queryString.split("&")).map(part -> part.split("=")) + .collect( + Collectors.groupingBy( + keyValue -> keyValue[0], + LinkedHashMap::new, + Collectors.mapping(keyValue -> keyValue[1], Collectors.toList()))) + .entrySet().stream() + .map(entry -> new Parameter(entry.getKey(), entry.getValue())) + .collect(Collectors.toCollection(LinkedList::new)); + } + + private String queryPart(String token) { + String query = ""; + if (token.contains("?") && token.indexOf("?") < token.length() - 1) { + String[] parts = token.split(QUERY_REGEX); + + if (parts.length > 1) { + if (parts[1].split(FRAGMENT_REGEX).length > 0) { + query = parts[1].split(FRAGMENT_REGEX)[0]; + } else { + return query; + } + } else { + query = parts[0].split(FRAGMENT_REGEX)[0]; + } + + if (!query.isEmpty() && !query.contains("=")) { + throw new IllegalArgumentException("Query string [" + query + "] is missing '=' operator."); + } + } + return query; + } + + public String getRootPath() { + return rootPath; + } + + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ServicePath)) return false; + ServicePath that = (ServicePath) o; + + return paths.equals(that.paths) + && fragments.equals(that.fragments) + && queryParameters.size() == that.queryParameters.size() + && queryParameters.containsAll(that.queryParameters); + } + + public int hashCode() { + return Objects.hash(paths, queryParameters, fragments); + } + + private static class Parameter { + private String key; + private List value; + + public Parameter(String key, List value) { + this.key = key; + this.value = value; + } + + private void addValues(List moreValues) { + value.addAll(moreValues); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Parameter)) return false; + Parameter parameter = (Parameter) o; + return Objects.equals(key, parameter.key) + && value.size() == parameter.value.size() + && value.containsAll(parameter.value); + } + + @Override + public int hashCode() { + return Objects.hash(key, value); + } + + private String asQueryString() { + return value.stream().map(value -> key + "=" + value).collect(Collectors.joining("&")); + } + } +} diff --git a/domino-rest-shared/src/main/java/org/dominokit/rest/shared/request/UrlFormatter.java b/domino-rest-shared/src/main/java/org/dominokit/rest/shared/request/UrlFormatter.java index 9850d4b..6180e3d 100644 --- a/domino-rest-shared/src/main/java/org/dominokit/rest/shared/request/UrlFormatter.java +++ b/domino-rest-shared/src/main/java/org/dominokit/rest/shared/request/UrlFormatter.java @@ -19,7 +19,6 @@ import java.util.ArrayList; import java.util.Map; -import org.dominokit.domino.history.StateHistoryToken; import org.dominokit.rest.shared.request.exception.PathParameterMissingException; /** Formats the url by adding the query parameters and normalizing path parameters */ @@ -45,7 +44,7 @@ protected String formatUrl(String targetUrl) { String postfix = asTokenString(targetUrl); String prefix = targetUrl.replace(postfix, ""); - StateHistoryToken tempToken = new StateHistoryToken(postfix); + ServicePath tempToken = new ServicePath(postfix); replaceUrlParamsWithArguments(tempToken); @@ -56,11 +55,11 @@ private boolean hasExpressions(String url) { return (url.contains("{") && url.contains("}")) || url.contains(":"); } - private void replaceUrlParamsWithArguments(StateHistoryToken tempToken) { + private void replaceUrlParamsWithArguments(ServicePath tempToken) { replacePaths(tempToken); } - private void replacePaths(StateHistoryToken tempToken) { + private void replacePaths(ServicePath tempToken) { new ArrayList<>(tempToken.paths()) .stream() .filter(this::isExpressionToken) diff --git a/domino-rest-shared/src/main/java/org/dominokit/rest/shared/request/builder/RestRequestBuilder.java b/domino-rest-shared/src/main/java/org/dominokit/rest/shared/request/builder/RestRequestBuilder.java index 66d907d..4c803d5 100644 --- a/domino-rest-shared/src/main/java/org/dominokit/rest/shared/request/builder/RestRequestBuilder.java +++ b/domino-rest-shared/src/main/java/org/dominokit/rest/shared/request/builder/RestRequestBuilder.java @@ -95,8 +95,8 @@ public ServerRequest build(R requestBean) { new RequestMeta(RestRequestBuilder.class, key, requestClass, responseClass), requestBean); request.setHttpMethod(this.method); - request.setAccept(new String[] {this.consumes}); - request.setContentType(new String[] {this.produce}); + request.setAccept(new String[] {this.produce}); + request.setContentType(new String[] {this.consumes}); request.setPath(this.path); request.setServiceRoot(isNull(this.serviceRoot) ? "" : this.serviceRoot); Optional.ofNullable(this.responseReader).ifPresent(request::setResponseReader); diff --git a/pom.xml b/pom.xml index a5ac70e..5ca7ec1 100644 --- a/pom.xml +++ b/pom.xml @@ -88,14 +88,13 @@ 3.0.0-M5 1.1.0 - 2.10.0 + 2.12.0 1.0.3 - 1.0.4 - 1.0.3 + 1.0.5 1.0.2 3.1.0 2.16.0 - 1.2.1 + 1.2.3