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 {languageId}} 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/{languageId}</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/{languageId}</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/{languageId}</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}