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