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