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}