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.impl; 018 019import com.soklet.annotation.DELETE; 020import com.soklet.annotation.DELETEs; 021import com.soklet.annotation.GET; 022import com.soklet.annotation.GETs; 023import com.soklet.annotation.HEAD; 024import com.soklet.annotation.HEADs; 025import com.soklet.annotation.OPTIONS; 026import com.soklet.annotation.OPTIONSes; 027import com.soklet.annotation.PATCH; 028import com.soklet.annotation.PATCHes; 029import com.soklet.annotation.POST; 030import com.soklet.annotation.POSTs; 031import com.soklet.annotation.PUT; 032import com.soklet.annotation.PUTs; 033import com.soklet.annotation.Resource; 034import com.soklet.annotation.ServerSentEventSource; 035import com.soklet.annotation.ServerSentEventSources; 036import com.soklet.core.HttpMethod; 037import com.soklet.core.Request; 038import com.soklet.core.ResourceMethod; 039import com.soklet.core.ResourceMethodResolver; 040import com.soklet.core.ResourcePath; 041import com.soklet.core.ResourcePathDeclaration; 042import com.soklet.internal.classindex.ClassIndex; 043 044import javax.annotation.Nonnull; 045import javax.annotation.Nullable; 046import javax.annotation.concurrent.ThreadSafe; 047import java.lang.annotation.Annotation; 048import java.lang.reflect.Method; 049import java.util.Collections; 050import java.util.HashMap; 051import java.util.HashSet; 052import java.util.Map; 053import java.util.Map.Entry; 054import java.util.Objects; 055import java.util.Optional; 056import java.util.Set; 057import java.util.SortedMap; 058import java.util.TreeMap; 059import java.util.stream.Collectors; 060 061import static java.lang.String.format; 062import static java.util.Objects.requireNonNull; 063 064/** 065 * @author <a href="https://www.revetkn.com">Mark Allen</a> 066 */ 067@ThreadSafe 068public class DefaultResourceMethodResolver implements ResourceMethodResolver { 069 @Nonnull 070 private static final DefaultResourceMethodResolver SHARED_INSTANCE; 071 072 static { 073 SHARED_INSTANCE = new DefaultResourceMethodResolver(); 074 } 075 076 @Nonnull 077 private final Set<Method> methods; 078 @Nonnull 079 private final Map<HttpMethod, Set<Method>> methodsByHttpMethod; 080 @Nonnull 081 private final Map<Method, Set<HttpMethodResourcePathDeclaration>> httpMethodResourcePathDeclarationsByMethod; 082 @Nonnull 083 private final Set<ResourceMethod> resourceMethods; 084 085 @Nonnull 086 public static DefaultResourceMethodResolver sharedInstance() { 087 return SHARED_INSTANCE; 088 } 089 090 public DefaultResourceMethodResolver() { 091 this(ClassIndex.getAnnotated(Resource.class).parallelStream().collect(Collectors.toSet()), null); 092 } 093 094 public DefaultResourceMethodResolver(@Nullable Set<Class<?>> resourceClasses) { 095 this(resourceClasses, null); 096 } 097 098 public DefaultResourceMethodResolver(@Nullable Set<Class<?>> resourceClasses, 099 @Nullable Set<Method> methods) { 100 Set<Method> allMethods = new HashSet<>(); 101 102 if (resourceClasses != null) 103 allMethods.addAll(extractMethods(resourceClasses)); 104 105 if (methods != null) 106 allMethods.addAll(methods); 107 108 this.methods = Collections.unmodifiableSet(allMethods); 109 this.methodsByHttpMethod = Collections.unmodifiableMap(createMethodsByHttpMethod(getMethods())); 110 this.httpMethodResourcePathDeclarationsByMethod = Collections.unmodifiableMap(createHttpMethodResourcePathDeclarationsByMethod(getMethods())); 111 112 // Collect up all resource methods into a single set for easy access 113 Set<ResourceMethod> resourceMethods = new HashSet<>(); 114 115 for (Entry<HttpMethod, Set<Method>> entry : this.methodsByHttpMethod.entrySet()) { 116 HttpMethod httpMethod = entry.getKey(); 117 Set<Method> currentMethods = entry.getValue(); 118 119 if (currentMethods == null) 120 continue; 121 122 for (Method method : currentMethods) { 123 Set<HttpMethodResourcePathDeclaration> httpMethodResourcePathDeclarations = this.httpMethodResourcePathDeclarationsByMethod.get(method); 124 125 if (httpMethodResourcePathDeclarations == null) 126 continue; 127 128 for (HttpMethodResourcePathDeclaration httpMethodResourcePathDeclaration : httpMethodResourcePathDeclarations) { 129 ResourcePathDeclaration resourcePathDeclaration = httpMethodResourcePathDeclaration.getResourcePathDeclaration(); 130 Boolean serverSentEventSource = httpMethodResourcePathDeclaration.isServerSentEventSource(); 131 ResourceMethod resourceMethod = ResourceMethod.withComponents(httpMethod, resourcePathDeclaration, method, serverSentEventSource); 132 resourceMethods.add(resourceMethod); 133 } 134 } 135 } 136 137 this.resourceMethods = Collections.unmodifiableSet(resourceMethods); 138 } 139 140 @Nonnull 141 @Override 142 public Optional<ResourceMethod> resourceMethodForRequest(@Nonnull Request request) { 143 requireNonNull(request); 144 145 Set<Method> methods = getMethodsByHttpMethod().get(request.getHttpMethod()); 146 147 if (methods == null) 148 return Optional.empty(); 149 150 ResourcePath resourcePath = request.getResourcePath(); 151 Set<ResourceMethod> matchingResourceMethods = new HashSet<>(4); // Normally there are few (if any) potential matches 152 153 // TODO: faster matching via path component tree structure instead of linear scan 154 for (Entry<Method, Set<HttpMethodResourcePathDeclaration>> entry : getHttpMethodResourcePathDeclarationsByMethod().entrySet()) { 155 Method method = entry.getKey(); 156 Set<HttpMethodResourcePathDeclaration> httpMethodResourcePathDeclarations = entry.getValue(); 157 158 for (HttpMethodResourcePathDeclaration httpMethodResourcePathDeclaration : httpMethodResourcePathDeclarations) 159 if (httpMethodResourcePathDeclaration.getHttpMethod().equals(request.getHttpMethod()) 160 && resourcePath.matches(httpMethodResourcePathDeclaration.getResourcePathDeclaration())) 161 matchingResourceMethods.add(ResourceMethod.withComponents(request.getHttpMethod(), httpMethodResourcePathDeclaration.getResourcePathDeclaration(), method, httpMethodResourcePathDeclaration.isServerSentEventSource())); 162 } 163 164 // Simple case - exact route match 165 if (matchingResourceMethods.size() == 1) 166 return matchingResourceMethods.stream().findFirst(); 167 168 // Multiple matches are OK so long as one is more specific than any others. 169 // If none are a match, we have a problem 170 if (matchingResourceMethods.size() > 1) { 171 Set<ResourceMethod> mostSpecificResourceMethods = mostSpecificResourceMethods(request, matchingResourceMethods); 172 173 if (mostSpecificResourceMethods.size() == 1) 174 return mostSpecificResourceMethods.stream().findFirst(); 175 176 throw new RuntimeException(format("Multiple routes match '%s %s'. Ambiguous matches were:\n%s", request.getHttpMethod().name(), request.getResourcePath().getPath(), 177 matchingResourceMethods.stream() 178 .map(matchingResourceMethod -> matchingResourceMethod.getMethod().toString()) 179 .collect(Collectors.joining("\n")))); 180 } 181 182 return Optional.empty(); 183 } 184 185 @Nonnull 186 protected Map<Method, Set<HttpMethodResourcePathDeclaration>> createHttpMethodResourcePathDeclarationsByMethod(@Nonnull Set<Method> methods) { 187 requireNonNull(methods); 188 189 Map<Method, Set<HttpMethodResourcePathDeclaration>> httpMethodResourcePathDeclarationsByMethod = new HashMap<>(); 190 191 for (Method method : methods) { 192 Set<HttpMethodResourcePathDeclaration> matchedHttpMethodResourcePathDeclarations = new HashSet<>(); 193 194 for (Annotation annotation : method.getAnnotations()) { 195 if (annotation instanceof GET) { 196 matchedHttpMethodResourcePathDeclarations.add(new HttpMethodResourcePathDeclaration(HttpMethod.GET, ResourcePathDeclaration.of 197 (((GET) annotation).value()))); 198 } else if (annotation instanceof POST) { 199 matchedHttpMethodResourcePathDeclarations.add(new HttpMethodResourcePathDeclaration(HttpMethod.POST, ResourcePathDeclaration.of 200 (((POST) annotation).value()))); 201 } else if (annotation instanceof PUT) { 202 matchedHttpMethodResourcePathDeclarations.add(new HttpMethodResourcePathDeclaration(HttpMethod.PUT, ResourcePathDeclaration.of 203 (((PUT) annotation).value()))); 204 } else if (annotation instanceof PATCH) { 205 matchedHttpMethodResourcePathDeclarations.add(new HttpMethodResourcePathDeclaration(HttpMethod.PATCH, ResourcePathDeclaration.of 206 (((PATCH) annotation).value()))); 207 } else if (annotation instanceof DELETE) { 208 matchedHttpMethodResourcePathDeclarations.add(new HttpMethodResourcePathDeclaration(HttpMethod.DELETE, ResourcePathDeclaration.of 209 (((DELETE) annotation).value()))); 210 } else if (annotation instanceof OPTIONS) { 211 matchedHttpMethodResourcePathDeclarations.add(new HttpMethodResourcePathDeclaration(HttpMethod.OPTIONS, ResourcePathDeclaration.of 212 (((OPTIONS) annotation).value()))); 213 } else if (annotation instanceof HEAD) { 214 matchedHttpMethodResourcePathDeclarations.add(new HttpMethodResourcePathDeclaration(HttpMethod.HEAD, ResourcePathDeclaration.of 215 (((HEAD) annotation).value()))); 216 } else if (annotation instanceof ServerSentEventSource) { 217 matchedHttpMethodResourcePathDeclarations.add(new HttpMethodResourcePathDeclaration(HttpMethod.GET, ResourcePathDeclaration.of 218 (((ServerSentEventSource) annotation).value()), true)); 219 } else if (annotation instanceof GETs) { 220 for (GET get : ((GETs) annotation).value()) 221 matchedHttpMethodResourcePathDeclarations.add(new HttpMethodResourcePathDeclaration(HttpMethod.GET, ResourcePathDeclaration.of 222 (get.value()))); 223 } else if (annotation instanceof POSTs) { 224 for (POST post : ((POSTs) annotation).value()) 225 matchedHttpMethodResourcePathDeclarations.add(new HttpMethodResourcePathDeclaration(HttpMethod.POST, ResourcePathDeclaration.of 226 (post.value()))); 227 } else if (annotation instanceof PUTs) { 228 for (PUT put : ((PUTs) annotation).value()) 229 matchedHttpMethodResourcePathDeclarations.add(new HttpMethodResourcePathDeclaration(HttpMethod.PUT, ResourcePathDeclaration.of 230 (put.value()))); 231 } else if (annotation instanceof PATCHes) { 232 for (PATCH patch : ((PATCHes) annotation).value()) 233 matchedHttpMethodResourcePathDeclarations.add(new HttpMethodResourcePathDeclaration(HttpMethod.PATCH, ResourcePathDeclaration.of 234 (patch.value()))); 235 } else if (annotation instanceof DELETEs) { 236 for (DELETE delete : ((DELETEs) annotation).value()) 237 matchedHttpMethodResourcePathDeclarations.add(new HttpMethodResourcePathDeclaration(HttpMethod.DELETE, ResourcePathDeclaration.of 238 (delete.value()))); 239 } else if (annotation instanceof OPTIONSes) { 240 for (OPTIONS options : ((OPTIONSes) annotation).value()) 241 matchedHttpMethodResourcePathDeclarations.add(new HttpMethodResourcePathDeclaration(HttpMethod.OPTIONS, ResourcePathDeclaration.of 242 (options.value()))); 243 } else if (annotation instanceof HEADs) { 244 for (HEAD head : ((HEADs) annotation).value()) 245 matchedHttpMethodResourcePathDeclarations.add(new HttpMethodResourcePathDeclaration(HttpMethod.HEAD, ResourcePathDeclaration.of 246 (head.value()))); 247 } else if (annotation instanceof ServerSentEventSources) { 248 for (ServerSentEventSource serverSentEventSource : ((ServerSentEventSources) annotation).value()) 249 matchedHttpMethodResourcePathDeclarations.add(new HttpMethodResourcePathDeclaration(HttpMethod.GET, ResourcePathDeclaration.of 250 (serverSentEventSource.value()), true)); 251 } 252 253 Set<HttpMethodResourcePathDeclaration> httpMethodResourcePathDeclarations = 254 httpMethodResourcePathDeclarationsByMethod.computeIfAbsent(method, k -> new HashSet<>()); 255 httpMethodResourcePathDeclarations.addAll(matchedHttpMethodResourcePathDeclarations); 256 } 257 } 258 259 return httpMethodResourcePathDeclarationsByMethod; 260 } 261 262 @Nonnull 263 protected Set<ResourceMethod> mostSpecificResourceMethods(@Nonnull Request request, 264 @Nonnull Set<ResourceMethod> resourceMethods) { 265 requireNonNull(request); 266 requireNonNull(resourceMethods); 267 268 SortedMap<Long, Set<ResourceMethod>> resourceMethodsByPlaceholderComponentCount = new TreeMap<>(); 269 270 for (ResourceMethod resourceMethod : resourceMethods) { 271 Set<HttpMethodResourcePathDeclaration> httpMethodResourcePathDeclarations = getHttpMethodResourcePathDeclarationsByMethod().get(resourceMethod.getMethod()); 272 273 if (httpMethodResourcePathDeclarations == null || httpMethodResourcePathDeclarations.size() == 0) 274 continue; 275 276 for (HttpMethodResourcePathDeclaration httpMethodResourcePathDeclaration : httpMethodResourcePathDeclarations) { 277 if (httpMethodResourcePathDeclaration.getHttpMethod() != request.getHttpMethod()) 278 continue; 279 280 long literalComponentCount = httpMethodResourcePathDeclaration.getResourcePathDeclaration().getComponents().stream().filter(component -> component.getType() == ResourcePathDeclaration.ComponentType.PLACEHOLDER).count(); 281 Set<ResourceMethod> resourceMethodsWithEquivalentComponentCount = resourceMethodsByPlaceholderComponentCount.computeIfAbsent(literalComponentCount, k -> new HashSet<>()); 282 283 resourceMethodsWithEquivalentComponentCount.add(resourceMethod); 284 } 285 } 286 287 return resourceMethodsByPlaceholderComponentCount.size() == 0 ? Collections.emptySet() : 288 resourceMethodsByPlaceholderComponentCount.get(resourceMethodsByPlaceholderComponentCount.keySet().stream().findFirst().get()); 289 } 290 291 @Nonnull 292 protected Map<HttpMethod, Set<Method>> createMethodsByHttpMethod(@Nonnull Set<Method> methods) { 293 requireNonNull(methods); 294 295 Map<HttpMethod, Set<Method>> methodsByMethod = new HashMap<>(); 296 297 for (Method method : methods) { 298 for (Annotation annotation : method.getAnnotations()) { 299 HttpMethod httpMethod = null; 300 301 if (annotation instanceof GET || annotation instanceof GETs) 302 httpMethod = HttpMethod.GET; 303 else if (annotation instanceof POST || annotation instanceof POSTs) 304 httpMethod = HttpMethod.POST; 305 else if (annotation instanceof PUT || annotation instanceof PUTs) 306 httpMethod = HttpMethod.PUT; 307 else if (annotation instanceof PATCH || annotation instanceof PATCHes) 308 httpMethod = HttpMethod.PATCH; 309 else if (annotation instanceof DELETE || annotation instanceof DELETEs) 310 httpMethod = HttpMethod.DELETE; 311 else if (annotation instanceof OPTIONS || annotation instanceof OPTIONSes) 312 httpMethod = HttpMethod.OPTIONS; 313 else if (annotation instanceof HEAD || annotation instanceof HEADs) 314 httpMethod = HttpMethod.HEAD; 315 else if (annotation instanceof ServerSentEventSource || annotation instanceof ServerSentEventSources) 316 httpMethod = HttpMethod.GET; 317 318 if (httpMethod == null) 319 continue; 320 321 Set<Method> httpMethodMethods = methodsByMethod.computeIfAbsent(httpMethod, k -> new HashSet<>()); 322 httpMethodMethods.add(method); 323 } 324 } 325 326 return methodsByMethod; 327 } 328 329 @Nonnull 330 protected Set<Method> extractMethods(@Nonnull Set<Class<?>> resourceClasses) { 331 requireNonNull(resourceClasses); 332 333 Set<Method> methods = new HashSet<>(); 334 335 for (Class<?> resourceClass : resourceClasses) 336 for (Method method : resourceClass.getMethods()) 337 for (Annotation annotation : method.getAnnotations()) 338 if (annotation instanceof GET 339 || annotation instanceof POST 340 || annotation instanceof PUT 341 || annotation instanceof PATCH 342 || annotation instanceof DELETE 343 || annotation instanceof OPTIONS 344 || annotation instanceof HEAD 345 || annotation instanceof ServerSentEventSource 346 || annotation instanceof GETs 347 || annotation instanceof POSTs 348 || annotation instanceof PUTs 349 || annotation instanceof PATCHes 350 || annotation instanceof DELETEs 351 || annotation instanceof OPTIONSes 352 || annotation instanceof HEADs 353 || annotation instanceof ServerSentEventSources) 354 methods.add(method); 355 356 return methods; 357 } 358 359 @Nonnull 360 @Override 361 public Set<ResourceMethod> getResourceMethods() { 362 return this.resourceMethods; 363 } 364 365 @Nonnull 366 public Set<Method> getMethods() { 367 return this.methods; 368 } 369 370 @Nonnull 371 public Map<Method, Set<HttpMethodResourcePathDeclaration>> getHttpMethodResourcePathDeclarationsByMethod() { 372 return this.httpMethodResourcePathDeclarationsByMethod; 373 } 374 375 @Nonnull 376 protected Map<HttpMethod, Set<Method>> getMethodsByHttpMethod() { 377 return this.methodsByHttpMethod; 378 } 379 380 @ThreadSafe 381 protected static class HttpMethodResourcePathDeclaration { 382 @Nonnull 383 private final HttpMethod httpMethod; 384 @Nonnull 385 private final ResourcePathDeclaration resourcePathDeclaration; 386 @Nonnull 387 private final Boolean serverSentEventSource; 388 389 public HttpMethodResourcePathDeclaration(@Nonnull HttpMethod httpMethod, 390 @Nonnull ResourcePathDeclaration resourcePathDeclaration) { 391 this(httpMethod, resourcePathDeclaration, false); 392 } 393 394 public HttpMethodResourcePathDeclaration(@Nonnull HttpMethod httpMethod, 395 @Nonnull ResourcePathDeclaration resourcePathDeclaration, 396 @Nonnull Boolean serverSentEventSource) { 397 requireNonNull(httpMethod); 398 requireNonNull(resourcePathDeclaration); 399 requireNonNull(serverSentEventSource); 400 401 this.httpMethod = httpMethod; 402 this.resourcePathDeclaration = resourcePathDeclaration; 403 this.serverSentEventSource = serverSentEventSource; 404 } 405 406 @Override 407 public String toString() { 408 return format("%s{httpMethod=%s, resourcePathDeclaration=%s, serverSentEventSource=%s}", getClass().getSimpleName(), 409 getHttpMethod(), getResourcePathDeclaration(), isServerSentEventSource()); 410 } 411 412 @Override 413 public boolean equals(@Nullable Object object) { 414 if (this == object) 415 return true; 416 417 if (!(object instanceof HttpMethodResourcePathDeclaration httpMethodResourcePathDeclaration)) 418 return false; 419 420 return Objects.equals(getHttpMethod(), httpMethodResourcePathDeclaration.getHttpMethod()) 421 && Objects.equals(getResourcePathDeclaration(), httpMethodResourcePathDeclaration.getResourcePathDeclaration()) 422 && Objects.equals(isServerSentEventSource(), httpMethodResourcePathDeclaration.isServerSentEventSource()); 423 } 424 425 @Override 426 public int hashCode() { 427 return Objects.hash(getHttpMethod(), getResourcePathDeclaration(), isServerSentEventSource()); 428 } 429 430 @Nonnull 431 public HttpMethod getHttpMethod() { 432 return this.httpMethod; 433 } 434 435 @Nonnull 436 public ResourcePathDeclaration getResourcePathDeclaration() { 437 return this.resourcePathDeclaration; 438 } 439 440 @Nonnull 441 public Boolean isServerSentEventSource() { 442 return this.serverSentEventSource; 443 } 444 } 445}