001/*
002 * Copyright 2022-2025 Revetware LLC.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package com.soklet.core;
018
019import javax.annotation.Nonnull;
020import javax.annotation.Nullable;
021import javax.annotation.concurrent.Immutable;
022import javax.annotation.concurrent.ThreadSafe;
023import java.net.URLDecoder;
024import java.nio.charset.StandardCharsets;
025import java.util.Collections;
026import java.util.LinkedHashMap;
027import java.util.List;
028import java.util.Map;
029import java.util.Objects;
030import java.util.regex.Pattern;
031
032import static com.soklet.core.Utilities.trimAggressively;
033import static java.lang.String.format;
034import static java.util.Arrays.asList;
035import static java.util.Collections.emptyList;
036import static java.util.Collections.unmodifiableList;
037import static java.util.Objects.requireNonNull;
038import static java.util.stream.Collectors.toList;
039
040/**
041 * A compile-time HTTP URL path declaration associated with an annotated <em>Resource Method</em>, such as {@code /users/{userId}}.
042 * <p>
043 * You may obtain instances via the {@link #of(String)} factory method.
044 * <p>
045 * <strong>Note: this type is not normally used by Soklet applications unless they support <a href="https://www.soklet.com/docs/server-sent-events">Server-Sent Events</a> or choose to implement a custom {@link ResourceMethodResolver}.</strong>
046 * <p>
047 * {@link ResourcePathDeclaration} instances must start with the {@code /} character and may contain placeholders denoted by single-mustache syntax.
048 * For example, the {@link ResourcePathDeclaration} {@code /users/{userId}} has a placeholder named {@code userId}.
049 * <p>
050 * A {@link ResourcePathDeclaration} is intended for compile-time <em>Resource Method</em> HTTP URL path declarations.
051 * The corresponding runtime type is {@link ResourcePath} and functionality is provided to check if the two "match" via {@link #matches(ResourcePath)}.
052 * <p>
053 * For example, a {@link ResourcePathDeclaration} {@code /users/{userId}} would match {@link ResourcePath} {@code /users/123}.
054 * <p>
055 * <strong>Please note the following restrictions on {@link ResourcePathDeclaration} structure:</strong>
056 * <p>
057 * 1. It is not legal to use the same placeholder name more than once in a {@link ResourcePathDeclaration}.
058 * <p>
059 * For example:
060 * <ul>
061 *  <li>{@code /users/{userId}} is valid resource path</li>
062 *  <li>{@code /users/{userId}/roles/{roleId}} is valid resource path</li>
063 *  <li>{@code /users/{userId}/other/{userId}} is an <em>invalid</em> resource path</li>
064 * </ul>
065 * 2. Placeholders must span the entire {@code /}-delimited path component in which they reside.
066 * <p>
067 * For example:
068 * <ul>
069 *   <li>{@code /users/{userId}} is a valid resource path</li>
070 *   <li>{@code /users/{userId}/details} is a valid resource path</li>
071 *   <li>{@code /users/prefix{userId}} is an <em>invalid</em> resource path</li>
072 * </ul>
073 *
074 * @author <a href="https://www.revetkn.com">Mark Allen</a>
075 */
076@ThreadSafe
077public class ResourcePathDeclaration {
078        /**
079         * Pattern which matches a placeholder in a path component.
080         * <p>
081         * Placeholders are bracked-enclosed segments of text, for example {@code &#123;languageId&#125;}
082         * <p>
083         * A path component is either literal text or a placeholder. There is no concept of multiple placeholders in a
084         * component.
085         */
086        @Nonnull
087        private static final Pattern COMPONENT_PLACEHOLDER_PATTERN;
088
089        static {
090                COMPONENT_PLACEHOLDER_PATTERN = Pattern.compile("^\\{.+\\}$");
091        }
092
093        @Nonnull
094        private final String path;
095        @Nonnull
096        private final List<Component> components;
097
098        /**
099         * Vends an instance that represents a compile-time path declaration, for example {@code /users/{userId}}.
100         *
101         * @param path a compile-time path declaration that may include placeholders
102         */
103        @Nonnull
104        public static ResourcePathDeclaration of(@Nonnull String path) {
105                requireNonNull(path);
106                return new ResourcePathDeclaration(path);
107        }
108
109        protected ResourcePathDeclaration(@Nonnull String path) {
110                requireNonNull(path);
111                this.path = normalizePath(path);
112                this.components = unmodifiableList(extractComponents(this.path));
113        }
114
115        /**
116         * Does this resource path declaration match the given resource path (taking placeholders into account, if present)?
117         * <p>
118         * For example, resource path declaration {@code /users/{userId}} would match {@code /users/123}.
119         *
120         * @param resourcePath the resource path against which to match
121         * @return {@code true} if the paths match, {@code false} otherwise
122         */
123        @Nonnull
124        public Boolean matches(@Nonnull ResourcePath resourcePath) {
125                requireNonNull(resourcePath);
126
127                if (resourcePath.getComponents().size() != getComponents().size())
128                        return false;
129
130                for (int i = 0; i < resourcePath.getComponents().size(); ++i) {
131                        String resourcePathComponent = resourcePath.getComponents().get(i);
132                        Component resourcePathDeclarationComponent = getComponents().get(i);
133
134                        if (resourcePathDeclarationComponent.getType() == ComponentType.PLACEHOLDER)
135                                continue;
136
137                        if (!resourcePathDeclarationComponent.getValue().equals(resourcePathComponent))
138                                return false;
139                }
140
141                return true;
142        }
143
144        /**
145         * What is the mapping between this resource path declaration's placeholder names to the given resource path's placeholder values?
146         * <p>
147         * For example, placeholder extraction for resource path declaration {@code /users/{userId}} and resource path {@code /users/123}
148         * would result in a value equivalent to {@code Map.of("userId", "123")}.
149         * <p>
150         * Resource path declaration placeholder values are automatically URL-decoded.  For example, placeholder extraction for resource path declaration {@code /users/{userId}}
151         * and resource path {@code /users/ab%20c} would result in a value equivalent to {@code Map.of("userId", "ab c")}.
152         *
153         * @param resourcePath runtime version of this resource path declaration, used to provide placeholder values
154         * @return a mapping of placeholder names to values, or the empty map if there were no placeholders
155         * @throws IllegalArgumentException if the provided resource path does not match this resource path declaration, i.e. {@link #matches(ResourcePath)} is {@code false}
156         */
157        @Nonnull
158        public Map<String, String> extractPlaceholders(@Nonnull ResourcePath resourcePath) {
159                requireNonNull(resourcePath);
160
161                if (!matches(resourcePath))
162                        throw new IllegalArgumentException(format("%s is not a match for %s so we cannot extract placeholders", this,
163                                        resourcePath));
164
165                // No placeholders? Nothing to do
166                if (isLiteral())
167                        return Map.of();
168
169                Map<String, String> placeholders = new LinkedHashMap<>(resourcePath.getComponents().size());
170
171                for (int i = 0; i < resourcePath.getComponents().size(); ++i) {
172                        String resourcePathComponent = resourcePath.getComponents().get(i);
173                        Component resourcePathDeclarationComponent = getComponents().get(i);
174
175                        if (resourcePathDeclarationComponent.getType() == ComponentType.PLACEHOLDER)
176                                placeholders.put(resourcePathDeclarationComponent.getValue(), URLDecoder.decode(resourcePathComponent, StandardCharsets.UTF_8));
177                }
178
179                return Collections.unmodifiableMap(placeholders);
180        }
181
182        /**
183         * What is the string representation of this resource path declaration?
184         *
185         * @return the string representation of this resource path declaration, which must start with {@code /}
186         */
187        @Nonnull
188        public String getPath() {
189                return this.path;
190        }
191
192        /**
193         * What are the {@code /}-delimited components of this resource path declaration?
194         *
195         * @return the components, or the empty list if this path is equal to {@code /}
196         */
197        @Nonnull
198        public List<Component> getComponents() {
199                return this.components;
200        }
201
202        /**
203         * Is this resource path declaration comprised of all "literal" components (that is, no placeholders)?
204         *
205         * @return {@code true} if this resource path declaration is entirely literal, {@code false} otherwise
206         */
207        @Nonnull
208        public Boolean isLiteral() {
209                for (Component component : components)
210                        if (component.getType() != ComponentType.LITERAL)
211                                return false;
212
213                return true;
214        }
215
216        @Nonnull
217        static String normalizePath(@Nonnull String path) {
218                requireNonNull(path);
219
220                path = trimAggressively(path);
221
222                if (path.length() == 0)
223                        return "/";
224
225                // Remove any duplicate slashes, e.g. //test///something -> /test/something
226                path = path.replaceAll("(/)\\1+", "$1");
227
228                if (!path.startsWith("/"))
229                        path = format("/%s", path);
230
231                if ("/".equals(path))
232                        return path;
233
234                if (path.endsWith("/"))
235                        path = path.substring(0, path.length() - 1);
236
237                return path;
238        }
239
240        /**
241         * Assumes {@code path} is already normalized via {@link #normalizePath(String)}.
242         *
243         * @param path (nonnull) Path from which components are extracted
244         * @return Logical components of the supplied {@code path}
245         */
246        @Nonnull
247        protected List<Component> extractComponents(@Nonnull String path) {
248                requireNonNull(path);
249
250                if ("/".equals(path))
251                        return emptyList();
252
253                // Strip off leading /
254                path = path.substring(1);
255
256                List<String> values = asList(path.split("/"));
257
258                return values.stream().map(value -> {
259                        ComponentType type = ComponentType.LITERAL;
260
261                        if (COMPONENT_PLACEHOLDER_PATTERN.matcher(value).matches()) {
262                                type = ComponentType.PLACEHOLDER;
263                                value = value.substring(1, value.length() - 1);
264                        }
265
266                        return new Component(value, type);
267                }).collect(toList());
268        }
269
270        @Override
271        public String toString() {
272                return format("%s{path=%s, components=%s}", getClass().getSimpleName(), getPath(), getComponents());
273        }
274
275        @Override
276        public boolean equals(@Nullable Object object) {
277                if (this == object)
278                        return true;
279
280                if (!(object instanceof ResourcePathDeclaration resourcePathDeclaration))
281                        return false;
282
283                return Objects.equals(getPath(), resourcePathDeclaration.getPath()) && Objects.equals(getComponents(), resourcePathDeclaration.getComponents());
284        }
285
286        @Override
287        public int hashCode() {
288                return Objects.hash(getPath(), getComponents());
289        }
290
291        /**
292         * How to interpret a {@link Component} of a {@link ResourcePathDeclaration} - is it literal text or a placeholder?
293         * <p>
294         * For example, given the path declaration <code>/languages/&#123;languageId&#125;</code>:
295         * <ul>
296         * <li>{@code ComponentType} at index 0 would be {@code LITERAL}
297         * <li>{@code ComponentType} at index 1 would be {@code PLACEHOLDER}
298         * </ul>
299         * <p>
300         * <strong>Note: this type is not normally used by Soklet applications unless they choose to implement a custom {@link ResourceMethodResolver}.</strong>
301         *
302         * @author <a href="https://www.revetkn.com">Mark Allen</a>
303         * @see ResourcePathDeclaration
304         */
305        public enum ComponentType {
306                /**
307                 * A literal component of a resource path declaration.
308                 * <p>
309                 * For example, given resource path declaration {@code /users/{userId}}, the {@code users} component would be of type {@code LITERAL}.
310                 */
311                LITERAL,
312                /**
313                 * A placeholder component (that is, one whose value is provided at runtime) of a resource path declaration.
314                 * <p>
315                 * For example, given resource path declaration {@code /users/{userId}}, the {@code userId} component would be of type {@code PLACEHOLDER}.
316                 */
317                PLACEHOLDER
318        }
319
320        /**
321         * Represents a {@code /}-delimited part of a {@link ResourcePathDeclaration}.
322         * <p>
323         * For example, given the path declaration <code>/languages/&#123;languageId&#125;</code>:
324         * <ul>
325         * <li>{@code Component} 0 would have type {@code LITERAL} and value {@code languages}
326         * <li>{@code Component} 1 would have type {@code PLACEHOLDER} and value {@code languageId}
327         * </ul>
328         * <p>
329         * <strong>Note: this type is not normally used by Soklet applications unless they choose to implement a custom {@link ResourceMethodResolver}.</strong>
330         *
331         * @author <a href="https://www.revetkn.com">Mark Allen</a>
332         * @see ResourcePathDeclaration
333         */
334        @Immutable
335        public static class Component {
336                @Nonnull
337                private final String value;
338                @Nonnull
339                private final ComponentType type;
340
341                public Component(@Nonnull String value,
342                                                                                 @Nonnull ComponentType type) {
343                        requireNonNull(value);
344                        requireNonNull(type);
345
346                        this.value = value;
347                        this.type = type;
348                }
349
350                @Override
351                public String toString() {
352                        return format("%s{value=%s, type=%s}", getClass().getSimpleName(), getValue(), getType());
353                }
354
355                @Override
356                public boolean equals(@Nullable Object object) {
357                        if (this == object)
358                                return true;
359
360                        if (!(object instanceof Component component))
361                                return false;
362
363                        return Objects.equals(getValue(), component.getValue()) && Objects.equals(getType(), component.getType());
364                }
365
366                @Override
367                public int hashCode() {
368                        return Objects.hash(getValue(), getType());
369                }
370
371                /**
372                 * What is the value of this resource path declaration component?
373                 * <p>
374                 * Note that the value of a {@link ComponentType#PLACEHOLDER} component does not include enclosing braces.
375                 * For example, given the path declaration <code>/languages/&#123;languageId&#125;</code>,
376                 * the component at index 1 would have value {@code languageId}, not {@code {languageId}}.
377                 *
378                 * @return the value of this component
379                 */
380                @Nonnull
381                public String getValue() {
382                        return value;
383                }
384
385                /**
386                 * What type of resource path declaration component is this?
387                 *
388                 * @return the type of component, e.g. {@link ComponentType#LITERAL} or {@link ComponentType#PLACEHOLDER}
389                 */
390                @Nonnull
391                public ComponentType getType() {
392                        return type;
393                }
394        }
395}