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;
018
019import javax.annotation.Nonnull;
020import javax.annotation.Nullable;
021import javax.annotation.concurrent.Immutable;
022import javax.annotation.concurrent.ThreadSafe;
023import java.util.Collections;
024import java.util.LinkedHashMap;
025import java.util.LinkedHashSet;
026import java.util.List;
027import java.util.Map;
028import java.util.Objects;
029import java.util.Optional;
030import java.util.Set;
031import java.util.regex.Pattern;
032import java.util.stream.Collectors;
033
034import static com.soklet.Utilities.trimAggressively;
035import static java.lang.String.format;
036import static java.util.Arrays.asList;
037import static java.util.Collections.emptyList;
038import static java.util.Collections.unmodifiableList;
039import static java.util.Objects.requireNonNull;
040import static java.util.stream.Collectors.toList;
041
042/**
043 * A compile-time HTTP URL path declaration associated with an annotated <em>Resource Method</em>, such as {@code /users/{userId}}.
044 * <p>
045 * You may obtain instances via the {@link #withPath(String)} factory method.
046 * <p>
047 * <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>
048 * <p>
049 * {@link ResourcePathDeclaration} instances must start with the {@code /} character and may contain placeholders denoted by single-mustache syntax.
050 * For example, the {@link ResourcePathDeclaration} {@code /users/{userId}} has a placeholder named {@code userId}.
051 * <p>
052 * A {@link ResourcePathDeclaration} is intended for compile-time <em>Resource Method</em> HTTP URL path declarations.
053 * The corresponding runtime type is {@link ResourcePath} and functionality is provided to check if the two "match" via {@link #matches(ResourcePath)}.
054 * <p>
055 * For example, a {@link ResourcePathDeclaration} {@code /users/{userId}} would match {@link ResourcePath} {@code /users/123}.
056 * <p>
057 * <strong>Please note the following restrictions on {@link ResourcePathDeclaration} structure:</strong>
058 * <p>
059 * 1. It is not legal to use the same placeholder name more than once in a {@link ResourcePathDeclaration}.
060 * <p>
061 * For example:
062 * <ul>
063 *  <li>{@code /users/{userId}} is valid resource path</li>
064 *  <li>{@code /users/{userId}/roles/{roleId}} is valid resource path</li>
065 *  <li>{@code /users/{userId}/other/{userId}} is an <em>invalid</em> resource path</li>
066 * </ul>
067 * 2. Placeholders must span the entire {@code /}-delimited path component in which they reside.
068 * <p>
069 * For example:
070 * <ul>
071 *   <li>{@code /users/{userId}} is a valid resource path</li>
072 *   <li>{@code /users/{userId}/details} is a valid resource path</li>
073 *   <li>{@code /users/prefix{userId}} is an <em>invalid</em> resource path</li>
074 * </ul>
075 * <p>
076 * In addition to simple placeholders, this version supports a special "varargs" placeholder indicated by a trailing {@code *}
077 * in the placeholder name. For example, {@code /static/{filePath*}}. When present, the varargs placeholder must appear only once
078 * and as the last component in the path.
079 *
080 * @author <a href="https://www.revetkn.com">Mark Allen</a>
081 */
082@ThreadSafe
083public final class ResourcePathDeclaration {
084        /**
085         * Pattern which matches a placeholder in a path component.
086         * <p>
087         * Placeholders are bracked-enclosed segments of text, for example {@code &#123;languageId&#125;}
088         * <p>
089         * A path component is either literal text or a placeholder. There is no concept of multiple placeholders in a
090         * component.
091         */
092        @Nonnull
093        private static final Pattern COMPONENT_PLACEHOLDER_PATTERN;
094
095        static {
096                COMPONENT_PLACEHOLDER_PATTERN = Pattern.compile("^\\{.+\\}$");
097        }
098
099        @Nonnull
100        private final String path;
101        @Nonnull
102        private final List<Component> components;
103
104        /**
105         * Vends an instance that represents a compile-time path declaration, for example {@code /users/{userId}}.
106         *
107         * @param path a compile-time path declaration that may include placeholders
108         */
109        @Nonnull
110        public static ResourcePathDeclaration withPath(@Nonnull String path) {
111                requireNonNull(path);
112                return new ResourcePathDeclaration(path);
113        }
114
115        private ResourcePathDeclaration(@Nonnull String path) {
116                requireNonNull(path);
117                this.path = normalizePath(path);
118
119                List<Component> components = extractComponents(this.path);
120
121                // Validate varargs: if any component is VARARGS then it must be the last one and only occur once.
122                int varargsCount = 0;
123
124                for (int i = 0; i < components.size(); i++) {
125                        if (components.get(i).getType() == ComponentType.VARARGS) {
126                                varargsCount++;
127
128                                if (i != components.size() - 1)
129                                        throw new IllegalArgumentException(format("Varargs placeholder must be the last component in the path declaration: %s", path));
130                        }
131                }
132
133                Set<String> pathParameterNames = new LinkedHashSet<String>();
134
135                for (var component : components)
136                        if (component.getType() == ComponentType.PLACEHOLDER && !pathParameterNames.add(component.getValue()))
137                                throw new IllegalArgumentException(
138                                                String.format("Duplicate placeholder name '%s' in resource path declaration: %s", component.getValue(), path));
139
140                if (varargsCount > 1)
141                        throw new IllegalArgumentException(format("Only one varargs placeholder is allowed in the path declaration: %s", path));
142
143                this.components = unmodifiableList(components);
144        }
145
146        /**
147         * Gets the {@link ComponentType#VARARGS} component in this declaration, if any.
148         *
149         * @return the {@link ComponentType#VARARGS} component in this declaration, or {@link Optional#empty()} if none exists.
150         */
151        @Nonnull
152        public Optional<Component> getVarargsComponent() {
153                if (getComponents().size() == 0)
154                        return Optional.empty();
155
156                Component lastComponent = getComponents().get(getComponents().size() - 1);
157
158                if (lastComponent.getType() == ComponentType.VARARGS)
159                        return Optional.of(lastComponent);
160
161                return Optional.empty();
162        }
163
164        /**
165         * Does this resource path declaration match the given resource path (taking placeholders/varargs into account, if present)?
166         * <p>
167         * For example, resource path declaration {@code /users/{userId}} would match {@code /users/123}.
168         *
169         * @param resourcePath the resource path against which to match
170         * @return {@code true} if the paths match, {@code false} otherwise
171         */
172        @Nonnull
173        public Boolean matches(@Nonnull ResourcePath resourcePath) {
174                requireNonNull(resourcePath);
175
176                if (resourcePath == ResourcePath.OPTIONS_SPLAT_RESOURCE_PATH)
177                        return false;
178
179                List<Component> declarationComponents = getComponents();
180                List<String> pathComponents = resourcePath.getComponents();
181
182                // If the last declaration component is a varargs placeholder, allow extra path components.
183                if (!declarationComponents.isEmpty() && declarationComponents.get(declarationComponents.size() - 1).getType() == ComponentType.VARARGS) {
184                        if (pathComponents.size() < declarationComponents.size() - 1)
185                                return false;
186
187                        // Check the prefix components
188                        for (int i = 0; i < declarationComponents.size() - 1; i++) {
189                                Component comp = declarationComponents.get(i);
190                                String pathComp = pathComponents.get(i);
191                                if (comp.getType() == ComponentType.LITERAL && !comp.getValue().equals(pathComp))
192                                        return false;
193                        }
194
195                        return true;
196                } else {
197                        if (pathComponents.size() != declarationComponents.size())
198                                return false;
199
200                        for (int i = 0; i < declarationComponents.size(); i++) {
201                                Component comp = declarationComponents.get(i);
202                                String pathComp = pathComponents.get(i);
203
204                                if (comp.getType() == ComponentType.LITERAL && !comp.getValue().equals(pathComp))
205                                        return false;
206                        }
207
208                        return true;
209                }
210        }
211
212        /**
213         * What is the mapping between this resource path declaration's placeholder names to the given resource path's placeholder values?
214         * <p>
215         * For example, placeholder extraction for resource path declaration {@code /users/{userId}} and resource path {@code /users/123}
216         * would result in a value equivalent to {@code Map.of("userId", "123")}.
217         * <p>
218         * Resource path declaration placeholder values are automatically URL-decoded.  For example, placeholder extraction for resource path declaration {@code /users/{userId}}
219         * and resource path {@code /users/ab%20c} would result in a value equivalent to {@code Map.of("userId", "ab c")}.
220         * <p>
221         * Varargs placeholders will combine all remaining path components (joined with @{code /}).
222         *
223         * @param resourcePath runtime version of this resource path declaration, used to provide placeholder values
224         * @return a mapping of placeholder names to values, or the empty map if there were no placeholders
225         * @throws IllegalArgumentException if the provided resource path does not match this resource path declaration, i.e. {@link #matches(ResourcePath)} is {@code false}
226         */
227        @Nonnull
228        public Map<String, String> extractPlaceholders(@Nonnull ResourcePath resourcePath) {
229                requireNonNull(resourcePath);
230
231                if (!matches(resourcePath))
232                        throw new IllegalArgumentException(format("%s is not a match for %s so we cannot extract placeholders", this, resourcePath));
233
234                Map<String, String> placeholders = new LinkedHashMap<>();
235                List<Component> declarationComponents = getComponents();
236                List<String> pathComponents = resourcePath.getComponents();
237
238                // If varargs is present as the last component, process accordingly.
239                if (!declarationComponents.isEmpty() && declarationComponents.get(declarationComponents.size() - 1).getType() == ComponentType.VARARGS) {
240                        // Process all but the last component normally.
241                        for (int i = 0; i < declarationComponents.size() - 1; i++) {
242                                Component comp = declarationComponents.get(i);
243
244                                if (comp.getType() == ComponentType.PLACEHOLDER)
245                                        placeholders.put(comp.getValue(), pathComponents.get(i));
246                        }
247
248                        // For varargs, join all remaining path components.
249                        String varargsValue = pathComponents.subList(declarationComponents.size() - 1, pathComponents.size())
250                                        .stream().collect(Collectors.joining("/"));
251                        placeholders.put(declarationComponents.get(declarationComponents.size() - 1).getValue(), varargsValue);
252                } else {
253                        // Normal processing: one-to-one mapping.
254                        for (int i = 0; i < declarationComponents.size(); i++) {
255                                Component comp = declarationComponents.get(i);
256
257                                if (comp.getType() == ComponentType.PLACEHOLDER)
258                                        placeholders.put(comp.getValue(), pathComponents.get(i));
259                        }
260                }
261
262                return Collections.unmodifiableMap(placeholders);
263        }
264
265        /**
266         * What is the string representation of this resource path declaration?
267         *
268         * @return the string representation of this resource path declaration, which must start with {@code /}
269         */
270        @Nonnull
271        public String getPath() {
272                return this.path;
273        }
274
275        /**
276         * What are the {@code /}-delimited components of this resource path declaration?
277         *
278         * @return the components, or the empty list if this path is equal to {@code /}
279         */
280        @Nonnull
281        public List<Component> getComponents() {
282                return this.components;
283        }
284
285        /**
286         * Is this resource path declaration comprised of all "literal" components (that is, no placeholders)?
287         *
288         * @return {@code true} if this resource path declaration is entirely literal, {@code false} otherwise
289         */
290        @Nonnull
291        public Boolean isLiteral() {
292                for (Component component : components)
293                        if (component.getType() != ComponentType.LITERAL)
294                                return false;
295
296                return true;
297        }
298
299        @Nonnull
300        static String normalizePath(@Nonnull String path) {
301                requireNonNull(path);
302
303                path = trimAggressively(path);
304
305                if (path.length() == 0)
306                        return "/";
307
308                // Remove any duplicate slashes, e.g. //test///something -> /test/something
309                path = path.replaceAll("(/)\\1+", "$1");
310
311                if (!path.startsWith("/"))
312                        path = format("/%s", path);
313
314                if ("/".equals(path))
315                        return path;
316
317                if (path.endsWith("/"))
318                        path = path.substring(0, path.length() - 1);
319
320                return path;
321        }
322
323        /**
324         * Assumes {@code path} is already normalized via {@link #normalizePath(String)}.
325         * <p>
326         * If a component is a placeholder, determines whether it is a varargs placeholder (trailing {@code *}).
327         *
328         * @param path path from which components are extracted
329         * @return logical components of the supplied {@code path}
330         */
331        @Nonnull
332        protected List<Component> extractComponents(@Nonnull String path) {
333                requireNonNull(path);
334
335                if ("/".equals(path))
336                        return emptyList();
337
338                // Strip off leading /
339                path = path.substring(1);
340
341                List<String> parts = asList(path.split("/"));
342
343                return parts.stream().map(part -> {
344                        if (COMPONENT_PLACEHOLDER_PATTERN.matcher(part).matches()) {
345                                // Remove the enclosing '{' and '}'
346                                String inner = part.substring(1, part.length() - 1);
347                                ComponentType type;
348
349                                if (inner.endsWith("*")) {
350                                        type = ComponentType.VARARGS;
351                                        inner = inner.substring(0, inner.length() - 1);
352                                } else {
353                                        type = ComponentType.PLACEHOLDER;
354                                }
355
356                                return Component.with(inner, type);
357                        } else {
358                                return Component.with(part, ComponentType.LITERAL);
359                        }
360                }).collect(toList());
361        }
362
363        @Override
364        public String toString() {
365                return format("%s{path=%s, components=%s}", getClass().getSimpleName(), getPath(), getComponents());
366        }
367
368        @Override
369        public boolean equals(@Nullable Object object) {
370                if (this == object)
371                        return true;
372                if (!(object instanceof ResourcePathDeclaration resourcePathDeclaration))
373                        return false;
374                return Objects.equals(getPath(), resourcePathDeclaration.getPath())
375                                && Objects.equals(getComponents(), resourcePathDeclaration.getComponents());
376        }
377
378        @Override
379        public int hashCode() {
380                return Objects.hash(getPath(), getComponents());
381        }
382
383        /**
384         * How to interpret a {@link Component} of a {@link ResourcePathDeclaration} - is it literal text or a placeholder?
385         * <p>
386         * For example, given the path declaration <code>/languages/&#123;languageId&#125;</code>:
387         * <ul>
388         * <li>{@code ComponentType} at index 0 would be {@code LITERAL}
389         * <li>{@code ComponentType} at index 1 would be {@code PLACEHOLDER}
390         * </ul>
391         * <p>
392         * <strong>Note: this type is not normally used by Soklet applications unless they choose to implement a custom {@link ResourceMethodResolver}.</strong>
393         *
394         * @author <a href="https://www.revetkn.com">Mark Allen</a>
395         * @see ResourcePathDeclaration
396         */
397        public enum ComponentType {
398                /**
399                 * A literal component of a resource path declaration.
400                 * <p>
401                 * For example, given resource path declaration {@code /users/{userId}}, the {@code users} component would be of type {@code LITERAL}.
402                 */
403                LITERAL,
404                /**
405                 * A placeholder component (that is, one whose value is provided at runtime) of a resource path declaration.
406                 * <p>
407                 * For example, given resource path declaration {@code /users/{userId}}, the {@code userId} component would be of type {@code PLACEHOLDER}.
408                 */
409                PLACEHOLDER,
410                /**
411                 * A "varargs" placeholder component that may match multiple path segments.
412                 * <p>
413                 * For example, given resource path declaration {@code /static/{filepath*}}, the {@code filepath*} component would be of type {@code VARARGS}.
414                 */
415                VARARGS
416        }
417
418        /**
419         * Represents a {@code /}-delimited part of a {@link ResourcePathDeclaration}.
420         * <p>
421         * For example, given the path declaration <code>/languages/&#123;languageId&#125;</code>:
422         * <ul>
423         *   <li>{@code Component} 0 would have type {@code LITERAL} and value {@code languages}
424         *   <li>{@code Component} 1 would have type {@code PLACEHOLDER} and value {@code languageId}
425         * </ul>
426         * <p>
427         * You may obtain instances via the {@link #with(String, ComponentType)} factory method.
428         * <p>
429         * <strong>Note: this type is not normally used by Soklet applications unless they choose to implement a custom {@link ResourceMethodResolver}.</strong>
430         *
431         * @author <a href="https://www.revetkn.com">Mark Allen</a>
432         * @see ResourcePathDeclaration
433         */
434        @Immutable
435        public static final class Component {
436                @Nonnull
437                private final String value;
438                @Nonnull
439                private final ComponentType type;
440
441                /**
442                 * Acquires a {@link Component} instance given a {@code value} and {@code type}.
443                 *
444                 * @param value the value of this component
445                 * @param type  the type of this component (literal or placeholder)
446                 * @return a {@link Component} instance
447                 */
448                @Nonnull
449                public static Component with(@Nonnull String value,
450                                                                                                                                 @Nonnull ComponentType type) {
451                        requireNonNull(value);
452                        requireNonNull(type);
453
454                        return new Component(value, type);
455                }
456
457                private Component(@Nonnull String value,
458                                                                                        @Nonnull ComponentType type) {
459                        requireNonNull(value);
460                        requireNonNull(type);
461
462                        this.value = value;
463                        this.type = type;
464                }
465
466                @Override
467                public String toString() {
468                        return format("%s{value=%s, type=%s}", getClass().getSimpleName(), getValue(), getType());
469                }
470
471                @Override
472                public boolean equals(@Nullable Object object) {
473                        if (this == object)
474                                return true;
475
476                        if (!(object instanceof Component component))
477                                return false;
478
479                        return Objects.equals(getValue(), component.getValue())
480                                        && Objects.equals(getType(), component.getType());
481                }
482
483                @Override
484                public int hashCode() {
485                        return Objects.hash(getValue(), getType());
486                }
487
488                /**
489                 * What is the value of this resource path declaration component?
490                 * <p>
491                 * Note that the value of a {@link ComponentType#PLACEHOLDER} component does not include enclosing braces.
492                 * For example, given the path declaration <code>/languages/&#123;languageId&#125;</code>,
493                 * the component at index 1 would have value {@code languageId}, not {@code {languageId}}.
494                 *
495                 * @return the value of this component
496                 */
497                @Nonnull
498                public String getValue() {
499                        return value;
500                }
501
502                /**
503                 * What type of resource path declaration component is this?
504                 *
505                 * @return the type of component, e.g. {@link ComponentType#LITERAL} or {@link ComponentType#PLACEHOLDER}
506                 */
507                @Nonnull
508                public ComponentType getType() {
509                        return type;
510                }
511        }
512}