diff --git a/gemini-jax-rs/pom.xml b/gemini-jax-rs/pom.xml new file mode 100644 index 00000000..6a4aa66c --- /dev/null +++ b/gemini-jax-rs/pom.xml @@ -0,0 +1,70 @@ + + + 4.0.0 + jar + + + TechEmpower, Inc. + https://www.techempower.com/ + + + + + Revised BSD License, 3-clause + repo + + + + + gemini-parent + com.techempower + 3.1.0-SNAPSHOT + + + gemini-jax-rs + + An extension for Gemini that provides dispatching implemented as a subset of JAX-RS. + + + + + com.techempower + gemini + + + jakarta.ws.rs + jakarta.ws.rs-api + + + junit + junit + test + + + org.mockito + mockito-all + test + + + org.slf4j + slf4j-simple + test + + + + com.caucho + resin + false + + + com.github.ben-manes.caffeine + caffeine + + + + \ No newline at end of file diff --git a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/CharSpan.java b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/CharSpan.java new file mode 100644 index 00000000..d225e4a4 --- /dev/null +++ b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/CharSpan.java @@ -0,0 +1,107 @@ +package com.techempower.gemini.jaxrs.core; + +public class CharSpan implements CharSequence +{ + private final CharSequence charSequence; + private final int start; + private final int end; + private int hash = 1; + private String toString; + + CharSpan(CharSequence charSequence, int start, int end) + { + this.charSequence = charSequence; + this.start = start; + this.end = end; + } + + CharSpan(String string) + { + this(string, 0, string.length()); + this.toString = string; + } + + public int getStart() + { + return start; + } + + public int getEnd() + { + return end; + } + + @Override + public boolean equals(Object o) + { + if (this == o) + { + return true; + } + if (o == null || getClass() != o.getClass()) + { + return false; + } + CharSpan charSpan = (CharSpan) o; + int length = length(); + if (length != charSpan.length()) + { + return false; + } + for (int i = 0; i < length; i++) + { + if (charAt(i) != charSpan.charAt(i)) + { + return false; + } + } + return true; + } + + @Override + public int hashCode() + { + if (hash == 1 && length() > 0) + { + int h = 1; + for (int i = end - 1; i >= start; i--) + { + h = 31 * h + charAt(i - start); + } + hash = h; + } + return hash; + } + + @Override + public int length() + { + return end - start; + } + + @Override + public char charAt(int index) + { + return charSequence.charAt(start + index); + } + + @Override + public CharSequence subSequence(int start, int end) + { + if (start == 0 && end == length()) + { + return this; + } + return charSequence.subSequence(this.start + start, this.start + end); + } + + @Override + public String toString() + { + if (toString == null) + { + toString = charSequence.subSequence(start, end).toString(); + } + return toString; + } +} diff --git a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/Endpoint.java b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/Endpoint.java new file mode 100644 index 00000000..3d855bf5 --- /dev/null +++ b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/Endpoint.java @@ -0,0 +1,16 @@ +package com.techempower.gemini.jaxrs.core; + +import java.util.List; +import java.util.Map; + +public interface Endpoint +{ + // TODO: Will likely want to change this to something more meaningful, like + // UriPathInfo or whatever it's called + Object invoke(String httpMethod, + String uri, + List> headers, + Map pathParams, + Map queryParams, + String body); +} diff --git a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/EndpointMetadata.java b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/EndpointMetadata.java new file mode 100644 index 00000000..eda1936a --- /dev/null +++ b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/EndpointMetadata.java @@ -0,0 +1,42 @@ +package com.techempower.gemini.jaxrs.core; + +import java.util.Set; + +public class EndpointMetadata +{ + private final String path; + private final Set httpMethods; + private final QMediaTypeGroup mediaTypeConsumes; + private final QMediaTypeGroup mediaTypeProduces; + + public EndpointMetadata(String path, + Set httpMethods, + QMediaTypeGroup mediaTypeConsumes, + QMediaTypeGroup mediaTypeProduces) + { + this.path = path; + this.httpMethods = httpMethods; + this.mediaTypeConsumes = mediaTypeConsumes; + this.mediaTypeProduces = mediaTypeProduces; + } + + public String getPath() + { + return path; + } + + public Set getHttpMethods() + { + return httpMethods; + } + + public QMediaTypeGroup getMediaTypeConsumes() + { + return mediaTypeConsumes; + } + + public QMediaTypeGroup getMediaTypeProduces() + { + return mediaTypeProduces; + } +} diff --git a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/EndpointRegistry.java b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/EndpointRegistry.java new file mode 100644 index 00000000..683b9b2a --- /dev/null +++ b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/EndpointRegistry.java @@ -0,0 +1,14 @@ +package com.techempower.gemini.jaxrs.core; + +import java.util.List; +import java.util.Map; + +public interface EndpointRegistry +{ + Endpoint getEndpointFor(String httpMethod, + String uri, + // TODO: Eventually this will all be in a lazy-loaded UriInfo + List> headers); + + void register(EndpointMetadata metadata, Endpoint endpoint); +} diff --git a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcher.java b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcher.java new file mode 100644 index 00000000..9c9fcb2a --- /dev/null +++ b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcher.java @@ -0,0 +1,932 @@ +package com.techempower.gemini.jaxrs.core; + +import com.caucho.util.LruCache; +import com.esotericsoftware.reflectasm.MethodAccess; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; + +import javax.ws.rs.HttpMethod; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.*; +import java.util.concurrent.ExecutionException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class JaxRsDispatcher +{ + private List endpoints; + private LoadingCache bestMatches = CacheBuilder.newBuilder() + .maximumSize(100) + .build(new CacheLoader<>() { + @Override public DispatchBestMatchInfo load(DispatchMatchSource dispatchMatchSource) throws Exception { + return getBestMatchInfo(dispatchMatchSource); + } + }); + DispatchBlock rootBlock = new RootDispatchBlock(); + LruCache bestMatchesResinCache = new LruCache<>(100); + com.github.benmanes.caffeine.cache.LoadingCache bestMatchesCaff = Caffeine.newBuilder() + .maximumSize(100) + .build(this::getBestMatchInfo); + private boolean cacheEnabled; + private boolean useResinCache; + private boolean useCaffCache; + + public boolean isCacheEnabled() + { + return cacheEnabled; + } + + public void setCacheEnabled(boolean cacheEnabled) + { + this.cacheEnabled = cacheEnabled; + } + + public boolean isUseResinCache() + { + return useResinCache; + } + + public void setUseResinCache(boolean useResinCache) + { + this.useResinCache = useResinCache; + } + + public boolean isUseCaffCache() + { + return useCaffCache; + } + + public void setUseCaffCache(boolean useCaffCache) + { + this.useCaffCache = useCaffCache; + } + + public void register(Object instance) + { + // TODO: Refactor to support class-based registration. Shouldn't be hard. + // TODO: Refactor to support finding annotations in class hierarchy + if (instance.getClass().isAnnotationPresent(Path.class)) + { + registerResource(instance); + } + } + + private void registerResource(Object resourceInstance) + { + Method[] methods = Arrays.stream(resourceInstance.getClass().getMethods()) + .filter(method -> method.isAnnotationPresent(Path.class)) + .toArray(Method[]::new); + + // TODO: Refactor to support finding annotations in class hierarchy + for (Method method : methods) + { + registerMethod(resourceInstance, method); + } + } + + private void registerMethod(Object resourceInstance, Method method) + { + String classLevelPath = resourceInstance.getClass() + .getAnnotation(Path.class).value(); + String methodLevelPath = method.getAnnotation(Path.class).value(); + String separator = methodLevelPath.startsWith("/") ? "" : "/"; + String fullPath = classLevelPath + separator + methodLevelPath; + Resource resource = new SingletonResource(resourceInstance); + Endpoint endpoint = new Endpoint(resource, method); + registerEndpoint(fullPath, endpoint); + } + + // TODO: Per the jax-rs spec, this will need to "pick" a best match class/set + // of classes first by finding the best match class, then filtering out the + // classes that aren't in-line with that class. Then it proceeds to the + // method level. This will for now just go right to the method level, then + // later I can have it filter out the class stuff after the fact, or even + // optimize it so that it directly stops at the class level the first time + // so it can filter those down before proceeding to the method level. + + static class WordBlockToken + { + private final String word; + + public WordBlockToken(String word) + { + this.word = word; + } + } + + static class PureVariableBlockToken + { + private final String name; + + PureVariableBlockToken(String name) + { + this.name = name; + } + } + + static class RegexVariableBlockToken + { + private final String name; + // TODO: Turn this into a Pattern in the constructor. Needs to be URI + // encoded first, then have its regex characters escaped. + private final String regex; + + public RegexVariableBlockToken(String name, String regex) + { + this.name = name; + this.regex = regex; + } + } + + static class SlashToken + { + // Basically just a flag. + } + + private void registerEndpoint(String path, Endpoint endpoint) + { + // TODO: Might want to gracefully handle the case where a resource could + // not be fully registered due to some issue with a warning, though + // blowing up could also be an option. Check the specs to see if it should + // blow up, but if it doesn't specify then just handle it gracefully and + // log a warning. Handling it gracefully means it should "revert" any + // additions made to the root block and any other blocks/etc. + // TODO: Need to handle URI encoding/decoding + StringBuilder parsedPath = new StringBuilder(); + String remainingPath = path; + if (remainingPath.startsWith("/")) + { + remainingPath = remainingPath.substring(1); + } + // TODO: Break these up into classes, might as well just set these up for + // unit tests to make it easier to test without needing the whole thing to + // be implemented. + Pattern wordBlockToken = Pattern.compile("^[^/{]+"); + Pattern variableBlockToken = Pattern.compile("^\\{[ \t]*(?\\w[\\w.-]*)[ \t]*(:[ \t]*(?([^{}]|\\{[^{}]*})*)[ \t]*)?}"); + DispatchBlock currentBlock = rootBlock; + List tokensSinceLastSlash = new ArrayList<>(); + boolean regexBlockMode = false; + while (!remainingPath.isEmpty()) + { + while (remainingPath.length() > 0 && remainingPath.charAt(0) != '/') + { + { + Matcher matcher = wordBlockToken.matcher(remainingPath); + if (matcher.find()) + { + String group = matcher.group(); + parsedPath.append(group); + WordBlockToken token = new WordBlockToken(group); + tokensSinceLastSlash.add(token); + remainingPath = remainingPath.substring(matcher.end()); + continue; + } + } + { + Matcher matcher = variableBlockToken.matcher(remainingPath); + if (matcher.find()) + { + parsedPath.append(matcher.group()); + String name = matcher.group("name"); + String regex = matcher.group("regex"); + Object token = regex == null + ? new PureVariableBlockToken(name) + : new RegexVariableBlockToken(name, regex); + tokensSinceLastSlash.add(token); + remainingPath = remainingPath.substring(matcher.end()); + continue; + } + } + throw new RuntimeException("Could not parse URI at position: " + + parsedPath + "\u032D" + remainingPath); + } + if (tokensSinceLastSlash.isEmpty()) + { + // TODO: Probably two cases here: + // 1) double-stacked slash. This is bad. Throw an exception. + // b) It's an @Path("") or @Path("/") "root" handler. Currently not + // well-handled probably, but I'll get around to it once the main + // stuff is done. + throw new UnsupportedOperationException("TODO"); + } + else if (tokensSinceLastSlash.size() == 1) + { + Object token = tokensSinceLastSlash.get(0); + // TODO: Refactor to avoid using instanceof + if (token instanceof WordBlockToken) + { + String word = ((WordBlockToken) token).word; + WordDispatchBlock wordDispatchBlock; + if (currentBlock.childrenByWord.containsKey(word)) + { + wordDispatchBlock = currentBlock.childrenByWord.get(word); + } + else + { + wordDispatchBlock = new WordDispatchBlock(); + currentBlock.addWordChild(word, wordDispatchBlock); + } + currentBlock = wordDispatchBlock; + } + else if (token instanceof PureVariableBlockToken) + { + FullSegmentPureVariableDispatchBlock fullSegmentPureVariableDispatchBlock; + String name = ((PureVariableBlockToken) token).name; + if (currentBlock.fullSegmentPureVariableChild != null) + { + fullSegmentPureVariableDispatchBlock = currentBlock.fullSegmentPureVariableChild; + if (!name.equals(fullSegmentPureVariableDispatchBlock.name)) + { + throw new RuntimeException("Multiple variable names exist at the" + + " same exact path" /* TODO: Include the conflicting paths */); + } + } + else + { + fullSegmentPureVariableDispatchBlock = new FullSegmentPureVariableDispatchBlock(name); + currentBlock.fullSegmentPureVariableChild = fullSegmentPureVariableDispatchBlock; + } + currentBlock = fullSegmentPureVariableDispatchBlock; + } + else + { + regexBlockMode = true; + } + } + else + { + regexBlockMode = true; + } + if (!remainingPath.isEmpty()) + { + if (remainingPath.charAt(0) != '/') + { + throw new RuntimeException("Failure encountered during path" + + " parsing. Please report this path to the developers" + + " of Gemini: " + path); + } + else + { + parsedPath.append('/'); + remainingPath = remainingPath.substring(1); + if (regexBlockMode) + { + tokensSinceLastSlash.add(new SlashToken()); + } + } + } + if (!regexBlockMode) + { + tokensSinceLastSlash.clear(); + } + } + if (regexBlockMode) + { + RegexVariableDispatchBlock block = new RegexVariableDispatchBlock( + tokensSinceLastSlash); + if (currentBlock.regexChildren.stream() + .map(RegexVariableDispatchBlock::getPatternString) + .anyMatch(block.getPatternString()::equals)) + { + throw new RuntimeException("Duplicate regex block found: " + + block.getPatternString()); + } + currentBlock.regexChildren.add(block); + currentBlock = block; + } + Set httpMethods = new HashSet<>(); + for (Annotation annotation : endpoint.method.getAnnotations()) + { + Class annotationClass = annotation.annotationType(); + if (annotationClass.isAnnotationPresent(HttpMethod.class)) + { + HttpMethod httpMethod = annotationClass + .getAnnotation(HttpMethod.class); + httpMethods.add(httpMethod.value()); + } + } + for (Annotation annotation : endpoint.resource + .getInstanceClass().getAnnotations()) + { + Class annotationClass = annotation.annotationType(); + if (annotationClass.isAnnotationPresent(HttpMethod.class)) + { + HttpMethod httpMethod = annotationClass + .getAnnotation(HttpMethod.class); + httpMethods.add(httpMethod.value()); + } + } + for (String httpMethod : httpMethods) + { + if (currentBlock.endpointsByHttpMethod.containsKey(httpMethod)) + { + throw new RuntimeException("Path " + path + " and HttpMethod " + + httpMethod + " is already associated with another endpoint"); + // TODO: Would be nice to include the other endpoint. + // TODO: Include logging, not just thrown exceptions. + } + currentBlock.endpointsByHttpMethod.put(httpMethod, endpoint); + } + } + + static class DispatchMatch + { + private final DispatchBlock block; + private final Endpoint endpoint; + final Map values; + final List matchChildren; + + public DispatchMatch(DispatchBlock block, + Endpoint endpoint, + Map values, + List matchChildren) + { + this.block = block; + this.endpoint = endpoint; + this.values = values; + this.matchChildren = matchChildren; + } + + // for debugging + List getLeafValues() + { + // TODO: I suppose this could have a value and children at the same time. + // Need to understand for myself what a "DispatchMatch" actually is, + // I've forgotten since last night. Should probably write-up a new + // explanation like the (incorrect) one in the dispatch method. + if (matchChildren != null) + { + return matchChildren.stream() + .map(DispatchMatch::getLeafValues) + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + } + if (values != null) + { + return List.copyOf(values.values()); + } + return null; + } + + // for debugging + List getLeafMatches() + { + // TODO: See TODO above in #getLeafValues. + if (matchChildren != null) + { + return matchChildren.stream() + .map(DispatchMatch::getLeafMatches) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + } + return List.of(this); + } + + void getBestMatch(DispatchBestMatchInfo info) + { + if (values != null) + { + info.values.putAll(values); + } + if (matchChildren != null) + { + matchChildren.get(0).getBestMatch(info); + } + else + { + info.endpoint = this.endpoint; + } + } + } + + static class DispatchBestMatchInfo + { + private Endpoint endpoint; + private Map values = new HashMap<>(); + + public Map getValues() + { + return values; + } + } + + abstract static class DispatchBlock + { + Map childrenByWord = new HashMap<>(0); + FullSegmentPureVariableDispatchBlock fullSegmentPureVariableChild; + List regexChildren = new ArrayList<>(0); + Map endpointsByHttpMethod = new HashMap<>(0); + + void addWordChild(String word, WordDispatchBlock block) + { + childrenByWord.put(word, block); + } + + void setFullSegmentPureVariableChild(FullSegmentPureVariableDispatchBlock block) + { + fullSegmentPureVariableChild = block; + } + + void addRegexChild(RegexVariableDispatchBlock block) + { + regexChildren.add(block); + } + + // TODO: Can potentially optimize away the list initializations and + // additions by pre-caching the relative index of all endpoints for a + // global sort, then having the below simply discard the existing match + // found if a new match found is of a lower index AKA higher precedence. + final List getChildMatches(String httpMethod, + String[] segments, + int index) + { + String segment = segments[index]; + List matches = null; + + DispatchBlock childWordBlock = childrenByWord.get(segment); + if (childWordBlock != null) + { + DispatchMatch match = childWordBlock.getDispatchMatch(httpMethod, + segments, index); + if (match != null) + { + matches = new LinkedList<>(); + matches.add(match); + } + } + + if (fullSegmentPureVariableChild != null) + { + DispatchMatch match = fullSegmentPureVariableChild + .getDispatchMatch(httpMethod, segments, index); + if (match != null) + { + if (matches == null) + { + matches = new LinkedList<>(); + } + matches.add(match); + } + } + for (DispatchBlock regexChild : regexChildren) + { + DispatchMatch match = regexChild + .getDispatchMatch(httpMethod, segments, index); + if (match != null) + { + if (matches == null) + { + matches = new LinkedList<>(); + } + matches.add(match); + } + } + return matches; + } + + Endpoint getMatchingEndpoint(String httpMethod) + { + return endpointsByHttpMethod.get(httpMethod); + } + + abstract DispatchMatch getDispatchMatch(String httpMethod, + String[] segments, + int index); + } + + static class RootDispatchBlock extends DispatchBlock + { + + @Override + DispatchMatch getDispatchMatch(String httpMethod, + String[] segments, + int index) + { + if (segments.length == 0) + { + // TODO: In theory this could exist, but for now lets assume it doesn't + return null; + } + else + { + List childMatches = getChildMatches(httpMethod, + segments, index); + if (childMatches != null) + { + return new DispatchMatch(this, null, null, childMatches); + } + else + { + return null; + } + } + } + } + + static class WordDispatchBlock extends DispatchBlock + { + @Override + DispatchMatch getDispatchMatch(String httpMethod, + String[] segments, + int index) + { + if (segments.length - 1 == index) + { + // If this has a matching endpoint, consider it a dispatch candidate + Endpoint endpoint = getMatchingEndpoint(httpMethod); + if (endpoint != null) + { + return new DispatchMatch(this, endpoint, null, null); + } + return null; + } + else + { + // For word blocks it's assumed that you match once in this method, + // because the check-match is performed outside of it using the map. + List childMatches = getChildMatches(httpMethod, + segments, index + 1); + if (childMatches != null) + { + return new DispatchMatch(this, null, null, childMatches); + } + else + { + return null; + } + } + } + } + + static class FullSegmentPureVariableDispatchBlock extends DispatchBlock + { + private final String name; + + FullSegmentPureVariableDispatchBlock(String name) + { + this.name = name; + } + + @Override + DispatchMatch getDispatchMatch(String httpMethod, + String[] segments, + int index) + { + if (segments.length - 1 == index) + { + // If this has a matching endpoint, consider it a dispatch candidate + Endpoint endpoint = getMatchingEndpoint(httpMethod); + if (endpoint != null) + { + return new DispatchMatch(this, endpoint, + Map.of(name, segments[index]), null); + } + return null; + } + else + { + // For full segment variable blocks it's assumed that your match is the + // entire segment, because that's simply the definition of this type of + // segment. That is what it represents. + List childMatches = getChildMatches(httpMethod, + segments, index + 1); + if (childMatches != null) + { + return new DispatchMatch(this, null, + Map.of(name, segments[index]), childMatches); + } + else + { + return null; + } + } + } + } + + static class RegexVariableDispatchBlock extends DispatchBlock + { + private final Pattern pattern; + private final Map variableNameToGroupName = new HashMap<>(); + + static String generateGroupName() + { + return "g" + UUID.randomUUID().toString().replaceAll("-", ""); + } + + public RegexVariableDispatchBlock(List tokens) + { + StringBuilder patternString = new StringBuilder(); + for (Object token : tokens) + { + if (token instanceof WordBlockToken) + { + // TODO: Encode and escape stuff. + patternString.append(((WordBlockToken) token).word); + } + else if (token instanceof PureVariableBlockToken) + { + String groupName = generateGroupName(); + // TODO: Encode and escape stuff. + String pattern = String.format("(?<%s>[^/]+?)", groupName); + String name = ((PureVariableBlockToken) token).name; + patternString.append(pattern); + variableNameToGroupName.put(name, groupName); + } + else if (token instanceof RegexVariableBlockToken) + { + String groupName = generateGroupName(); + String name = ((RegexVariableBlockToken) token).name; + String regex = ((RegexVariableBlockToken) token).regex; + // TODO: Encode and escape stuff. + String pattern = String.format("(?<%s>%s)", groupName, regex); + patternString.append(pattern); + variableNameToGroupName.put(name, groupName); + } + else if (token instanceof SlashToken) + { + patternString.append("/"); + } + else + { + throw new RuntimeException("Unexpected token type."); + } + } + if (!patternString.toString().endsWith("/")) { + patternString.append("/"); + } + pattern = Pattern.compile(patternString.toString()); + } + + String getPatternString() + { + return pattern.pattern(); + } + + @Override + DispatchMatch getDispatchMatch(String httpMethod, + String[] segments, + int index) + { + String uri = String.join("/", segments); + if (!uri.endsWith("/")) { + uri += "/"; + } + Matcher matcher = pattern.matcher(uri); + if (matcher.find()) + { + Map values = new HashMap<>(); + for (var entry : variableNameToGroupName.entrySet()) + { + values.put(entry.getKey(), matcher.group(entry.getValue())); + } + // If this has a matching endpoint, consider it a dispatch candidate + Endpoint endpoint = getMatchingEndpoint(httpMethod); + if (endpoint != null) + { + return new DispatchMatch(this, endpoint, values, null); + } + } + return null; + } + } + + static class DispatchMatchSource { + private final String uri; + private final String httpMethod; + + public DispatchMatchSource(String httpMethod, String uri) + { + this.uri = uri; + this.httpMethod = httpMethod; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DispatchMatchSource that = (DispatchMatchSource) o; + return uri.equals(that.uri) && + httpMethod.equals(that.httpMethod); + } + + @Override + public int hashCode() + { + return Objects.hash(uri, httpMethod); + } + } + + interface Resource + { + Object getInstance(); + + Class getInstanceClass(); + } + + static class SingletonResource implements Resource + { + private final Object singleton; + + SingletonResource(Object singleton) + { + this.singleton = singleton; + } + + @Override + public Object getInstance() + { + return singleton; + } + + @Override + public Class getInstanceClass() + { + return singleton.getClass(); + } + } + + static class ClassResource implements Resource + { + private final Class resourceClass; + + ClassResource(Class resourceClass) + { + this.resourceClass = resourceClass; + } + + + @Override + public Object getInstance() + { + // TODO: Find out how to do dependency injection. Also might want to use + // the fast reflection library for this if/when possible. + try + { + return resourceClass.getConstructor().newInstance(); + } + catch (InstantiationException + | IllegalAccessException + | InvocationTargetException + | NoSuchMethodException e) + { + throw new RuntimeException(e); + } + } + + @Override + public Class getInstanceClass() + { + return resourceClass; + } + } + + static class Endpoint + { + private final Resource resource; + private final Method method; + private final QMediaTypeGroup mediaTypeConsumes; + private final QMediaTypeGroup mediaTypeProduces; + + // TODO: Change method to "callable" or some such. + Endpoint(Resource resource, Method method) + { + this.resource = resource; + this.method = method; + // TODO: Obtain from the method, likely outside then passed in so that + // Method can become "callable" instead. Also, the media type can come + // from the class. + // TODO: Look up "entity provider", section 4.2.3 + mediaTypeConsumes = null; + mediaTypeProduces = null; + } + } + + DispatchMatch getDispatchMatches(String httpMethod, String uri) + { + // TODO: This should be commented back in, but I'm leaving it commented + // out while I try to find a way around this to avoid the performance hit. + /*int uriStart = uri.startsWith("/") ? 1 : 0; + int uriEnd = uri.length() - (uri.endsWith("/") ? 1 : 0); + String normalizedUri = uri.substring(uriStart, uriEnd);*/ + // TODO: Switch to CharSpan. Can also use a modified CharSpan to avoid the + // need to create a normalized uri, since the split can just be told to + // ignore the unnecessary characters (leading/trailing slash, though a + // trailing slash is already ignored by String.split). + String[] segments = uri.split("/"); + return rootBlock.getDispatchMatch(httpMethod, segments, 0); + } + + public DispatchBestMatchInfo getBestMatchInfo(DispatchMatchSource dispatchMatchSource) { + String uri = dispatchMatchSource.uri; + String httpMethod = dispatchMatchSource.httpMethod; + int uriStart = uri.startsWith("/") ? 1 : 0; + int uriEnd = uri.length() - (uri.endsWith("/") ? 1 : 0); + String normalizedUri = uri.substring(uriStart, uriEnd); + DispatchMatch dispatchMatch = getDispatchMatches(httpMethod, normalizedUri); + DispatchBestMatchInfo matchInfo = new DispatchBestMatchInfo(); + // TODO: Need to filter the dispatch match down to only the correct one so + // the path params can be extracted. There's a TO-DO...somewhere in this + // file (or maybe the test) for how to do that naturally and only come out + // with the right match per the sorting rules, and not have to deal with + // the tree of matches. + dispatchMatch.getBestMatch(matchInfo); + return matchInfo; + } + + public DispatchBestMatchInfo getBestMatchInfo(String httpMethod, String uri) { + DispatchMatchSource matchSource = new DispatchMatchSource(httpMethod, uri); + if (cacheEnabled) { + if (useResinCache) { + DispatchBestMatchInfo matchInfo = bestMatchesResinCache.get(matchSource); + if (matchInfo == null) { + matchInfo = getBestMatchInfo(matchSource); + bestMatchesResinCache.put(matchSource, matchInfo); + } + return matchInfo; + } else if (useCaffCache) { + return bestMatchesCaff.get(matchSource); + } else { + try + { + return bestMatches.get(matchSource); + } + catch (ExecutionException e) + { + throw new RuntimeException(e); + } + } + } + return getBestMatchInfo(matchSource); + } + + // TODO: Not use Gemini's Context eventually, probably. + //public void dispatch(Context context) + public Object dispatch(String httpMethod, String uri) + { + return dispatch(httpMethod, uri, List.of()); + } + + public Object dispatch(String httpMethod, + String uri, + // TODO: Eventually this will all be in a lazy-loaded UriInfo + List> headers) + { + // TODO: Add `getServletPath` as a method in Context so that this can + // only match the URI relative to where the servlet is hosted. + // `getServletPath` is a method of HttpServletRequest. For + // non-servlet containers this can just default to "" or "/". Find + // out what the default is for Resin and use that. + DispatchBestMatchInfo matchInfo = getBestMatchInfo(httpMethod, uri); + + Method method = matchInfo.endpoint.method; + // TODO: For now I'm just gonna support valueOf for simplicity, + // and assume it's static. + List arguments = new ArrayList<>(); + Object instance = matchInfo.endpoint.resource.getInstance(); + Class instanceClass = matchInfo.endpoint.resource.getInstanceClass(); + Class[] parameterTypes = method.getParameterTypes(); + Annotation[][] parameterAnnotations = method.getParameterAnnotations(); + for (Parameter parameter : method.getParameters()) + { + Class parameterType = parameter.getType(); + PathParam pathParam = parameter.getAnnotation(PathParam.class); + String stringValue = matchInfo.values.get(pathParam.value()); + Object val; + if (parameterType == String.class) + { + val = stringValue; + } + else + { + MethodAccess parameterTypeAccess = MethodAccess.get(parameterType); + int index = parameterTypeAccess.getIndex("fromString", String.class); + if (index < 0) { + index = parameterTypeAccess.getIndex("valueOf", String.class); + } + val = parameterTypeAccess.invoke(null, index, stringValue); + } + arguments.add(val); + } + try + { + // TODO: Temporarily having dispatch return be the method invocation + // return value so I can test it easier. Should refactor to make this + // easier. + return method.invoke(instance, arguments.toArray()); + } + catch (IllegalAccessException | InvocationTargetException e) + { + throw new RuntimeException(e); + } + } +} diff --git a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/LazyQMediaType.java b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/LazyQMediaType.java new file mode 100644 index 00000000..7bba2c96 --- /dev/null +++ b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/LazyQMediaType.java @@ -0,0 +1,60 @@ +package com.techempower.gemini.jaxrs.core; + +import javax.ws.rs.core.MediaType; +import java.util.Collections; +import java.util.Map; + +public class LazyQMediaType extends QMediaType +{ + private final CharSpan type; + private final CharSpan subtype; + private final double qValue; + private final Map unmodifiableParameters; + private MediaType mediaType; + + public LazyQMediaType(CharSpan type, + CharSpan subtype, + double qValue, + Map parameters) + { + this.type = type; + this.subtype = subtype; + this.qValue = qValue; + this.unmodifiableParameters = Collections.unmodifiableMap( + new StringStringCharSpanMap(parameters)); + } + + @Override + public String getType() + { + return type.toString(); + } + + @Override + public String getSubtype() + { + return subtype.toString(); + } + + @Override + public double getQValue() + { + return qValue; + } + + @Override + public Map getParameters() + { + return unmodifiableParameters; + } + + @Override + public MediaType getMediaType() + { + if (mediaType == null) + { + mediaType = new MediaType(getType(), getSubtype(), getParameters()); + } + return mediaType; + } +} diff --git a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/MediaTypeData.java b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/MediaTypeData.java new file mode 100644 index 00000000..1c14a9c0 --- /dev/null +++ b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/MediaTypeData.java @@ -0,0 +1,60 @@ +package com.techempower.gemini.jaxrs.core; + +import java.util.Objects; + +class MediaTypeData +{ + // TODO: CharSpan + private final String type; + private final String subtype; + private final double qValue; + + public MediaTypeData(String type, String subtype, double qValue) + { + this.type = Objects.requireNonNull(type); + this.subtype = Objects.requireNonNull(subtype); + this.qValue = qValue; + } + + public String getType() + { + return type; + } + + public String getSubtype() + { + return subtype; + } + + public double getQValue() + { + return qValue; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MediaTypeData that = (MediaTypeData) o; + return Double.compare(that.qValue, qValue) == 0 && + type.equals(that.type) && + subtype.equals(that.subtype); + } + + @Override + public int hashCode() + { + return Objects.hash(type, subtype, qValue); + } + + @Override + public String toString() + { + return "MediaTypeData{" + + "type='" + type + '\'' + + ", subtype='" + subtype + '\'' + + ", qValue=" + qValue + + '}'; + } +} diff --git a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/MediaTypeParser.java b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/MediaTypeParser.java new file mode 100644 index 00000000..3c32bccb --- /dev/null +++ b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/MediaTypeParser.java @@ -0,0 +1,18 @@ +package com.techempower.gemini.jaxrs.core; + +/** + * Specifications: + * + */ +public interface MediaTypeParser +{ + QMediaTypeGroup parse(String mediaType); +} diff --git a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/MediaTypeParserImpl.java b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/MediaTypeParserImpl.java new file mode 100644 index 00000000..926a0b7a --- /dev/null +++ b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/MediaTypeParserImpl.java @@ -0,0 +1,148 @@ +package com.techempower.gemini.jaxrs.core; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +class MediaTypeParserImpl + implements MediaTypeParser +{ + private final String qValueKey; + + private static final CharSpan WILDCARD = new CharSpan("*"); + private static final Pattern MEDIA_TYPE_PATTERN; + private static final Pattern PARAMETERS_PATTERN; + + static + { + var vCharRange = "-!#%&'*+.^`|~\\w$"; + var token = "[" + vCharRange + "]+"; + var obsTextRange = "\\x80-\\xFF"; + var qdText = "[\t \\x21\\x23-\\x5B\\x5D-\\x7E" + obsTextRange + "]"; + var quotedPair = "\\\\[\t " + vCharRange + obsTextRange + "]"; + var quotedStr = "\"((?:" + qdText + "|" + quotedPair + ")*)\""; + var ows = "[ \t]*"; + + // Group 1: key + // Group 2: unquoted value + // Group 3: quoted value + PARAMETERS_PATTERN = Pattern.compile( + ows + ";" + ows + "(" + token + ")=(?:(" + token + ")|(?:" + quotedStr + "))"); + + // Group 1: type + // Group 2: subtype + // Group 3: parameters + MEDIA_TYPE_PATTERN = Pattern.compile( + ",?" + ows + "(" + token + ")/(" + token + ")((" + PARAMETERS_PATTERN.pattern() + ")*)"); + } + + MediaTypeParserImpl(String qValueKey) + { + this.qValueKey = qValueKey; + } + + @Override + public QMediaTypeGroup parse(String mediaType) + { + // Immediately fail if a leading comma is present. The regex allows a + // leading comma for simplicity with capturing multiple matches, but this + // is invalid for the first match. + if (mediaType.charAt(0) == ',') + { + throw new ProcessingException(String.format( + "Could not fully parse media type \"%s\"," + + " parsed up to position 0.", mediaType)); + } + Matcher mediaTypeMatcher = MEDIA_TYPE_PATTERN.matcher(mediaType); + List mediaTypes = new ArrayList<>(1); + int mediaTypeEnd = 0; + while (mediaTypeMatcher.find()) + { + if (mediaTypeEnd != mediaTypeMatcher.start()) + { + throw new ProcessingException(String.format( + "Could not fully parse media type \"%s\"," + + " parsed up to position %s.", + mediaType, mediaTypeEnd)); + } + mediaTypeEnd = mediaTypeMatcher.end(); + CharSpan type = new CharSpan(mediaType, mediaTypeMatcher.start(1), + mediaTypeMatcher.end(1)); + CharSpan subtype = new CharSpan(mediaType, mediaTypeMatcher.start(2), + mediaTypeMatcher.end(2)); + if (type.equals(WILDCARD) && !subtype.equals(WILDCARD)) + { + throw new ProcessingException(String.format( + "Invalid type/subtype combination \"%s/%s\" in media" + + " type \"%s\", type must be concrete if subtype is concrete.", + type, subtype, mediaType)); + } + double qValue = 1d; + int mediaTypeGroup3Start = mediaTypeMatcher.start(3); + Map parametersMap; + if (mediaTypeGroup3Start != -1) + { + CharSpan parameters = new CharSpan(mediaType, mediaTypeGroup3Start, + mediaTypeMatcher.end(3)); + Matcher parametersMatcher = PARAMETERS_PATTERN.matcher(parameters); + parametersMap = new HashMap<>(0); + while (parametersMatcher.find()) + { + CharSpan key = new CharSpan(parameters, + parametersMatcher.start(1), parametersMatcher.end(1)); + CharSpan unquotedValue = new CharSpan(parameters, + parametersMatcher.start(2), parametersMatcher.end(2)); + CharSpan quotedValue = new CharSpan(parameters, + parametersMatcher.start(3), parametersMatcher.end(3)); + CharSpan value = unquotedValue.getStart() != -1 + ? unquotedValue : quotedValue; + parametersMap.put(key, value); + if (key.toString().equalsIgnoreCase(qValueKey)) + { + try + { + qValue = Double.parseDouble(value.toString()); + } + catch (NumberFormatException e) + { + throw new ProcessingException(String.format( + "Invalid q-value \"%s\" in media type \"%s\"," + + " failed to parse number.", qValue, mediaType), e); + } + if (qValue * 1e4 % 10 != 0) + { + throw new ProcessingException(String.format( + "Invalid q-value \"%s\" in media type \"%s\"," + + " more than 3 decimal places were specified.", + qValue, mediaType)); + } + if (qValue < 0 || qValue > 1) + { + throw new ProcessingException(String.format( + "Invalid q-value \"%s\" in media type \"%s\"," + + " q-value must be between 0 and 1, inclusive.", + qValue, mediaType)); + } + break; + } + } + } + else + { + parametersMap = Map.of(); + } + mediaTypes.add(new LazyQMediaType(type, subtype, qValue, parametersMap)); + } + if (mediaTypeEnd != mediaType.length()) + { + throw new ProcessingException(String.format( + "Could not fully parse media type \"%s\"," + + " parsed up to position %s.", + mediaType, mediaTypeEnd)); + } + return new QMediaTypeGroup(mediaTypes); + } +} diff --git a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/ProcessingException.java b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/ProcessingException.java new file mode 100644 index 00000000..58fa3345 --- /dev/null +++ b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/ProcessingException.java @@ -0,0 +1,16 @@ +package com.techempower.gemini.jaxrs.core; + +public class ProcessingException extends RuntimeException +{ + private static final long serialVersionUID = 8411477705739130341L; + + public ProcessingException(String message) + { + super(message); + } + + public ProcessingException(String message, Throwable cause) + { + super(message, cause); + } +} diff --git a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/QMediaType.java b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/QMediaType.java new file mode 100644 index 00000000..1ddc3dc8 --- /dev/null +++ b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/QMediaType.java @@ -0,0 +1,67 @@ +package com.techempower.gemini.jaxrs.core; + +import javax.ws.rs.core.MediaType; +import java.util.Map; +import java.util.Objects; + +public abstract class QMediaType +{ + static final String ONE = "1"; + + private boolean hashFound; + private int hash; + + public abstract double getQValue(); + + public abstract String getType(); + + public abstract String getSubtype(); + + public abstract Map getParameters(); + + public abstract MediaType getMediaType(); + + public boolean isCompatible(MediaType other) + { + return other != null && // return false if other is null, else + (getType().equals(MediaType.MEDIA_TYPE_WILDCARD) || other.getType().equals(MediaType.MEDIA_TYPE_WILDCARD) || // both are wildcard types, or + (getType().equalsIgnoreCase(other.getType()) && (getSubtype().equals(MediaType.MEDIA_TYPE_WILDCARD) + || other.getSubtype().equals(MediaType.MEDIA_TYPE_WILDCARD))) || // same types, wildcard sub-types, or + (getType().equalsIgnoreCase(other.getType()) && getSubtype().equalsIgnoreCase(other.getSubtype()))); // same types & sub-types + } + + @Override + public final boolean equals(Object o) + { + if (this == o) return true; + if (!(o instanceof QMediaType)) return false; + QMediaType that = (QMediaType) o; + return getQValue() == that.getQValue() + && getType().equalsIgnoreCase(that.getType()) + && getSubtype().equalsIgnoreCase(that.getSubtype()) + && getParameters().equals(that.getParameters()); + } + + @Override + public int hashCode() + { + if (!hashFound) + { + hash = Objects.hash(getQValue(), getType().toLowerCase(), + getSubtype().toLowerCase(), getParameters()); + hashFound = true; + } + return hash; + } + + @Override + public String toString() + { + return "QMediaType{" + + "type='" + getType() + '\'' + + ", subtype='" + getSubtype() + '\'' + + ", parameters=" + getParameters() + + ", qValue=" + getQValue() + + '}'; + } +} diff --git a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/QMediaTypeGroup.java b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/QMediaTypeGroup.java new file mode 100644 index 00000000..8a7236f9 --- /dev/null +++ b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/QMediaTypeGroup.java @@ -0,0 +1,44 @@ +package com.techempower.gemini.jaxrs.core; + +import java.util.List; +import java.util.Objects; + +class QMediaTypeGroup +{ + public static final QMediaTypeGroup DEFAULT_WILDCARD_GROUP = new QMediaTypeGroup(List.of(WrappedQMediaType.DEFAULT_WILDCARD)); + + private final List mediaTypes; + + public QMediaTypeGroup(List mediaTypes) + { + this.mediaTypes = Objects.requireNonNull(mediaTypes); + } + + public List getMediaTypes() + { + return mediaTypes; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + QMediaTypeGroup that = (QMediaTypeGroup) o; + return mediaTypes.equals(that.mediaTypes); + } + + @Override + public int hashCode() + { + return Objects.hash(mediaTypes); + } + + @Override + public String toString() + { + return "MediaTypeDataGroup{" + + "mediaTypes=[" + mediaTypes + + "]}"; + } +} diff --git a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/SimpleCombinedQMediaType.java b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/SimpleCombinedQMediaType.java new file mode 100644 index 00000000..b997f6d1 --- /dev/null +++ b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/SimpleCombinedQMediaType.java @@ -0,0 +1,223 @@ +package com.techempower.gemini.jaxrs.core; + +import java.util.Objects; + +public class SimpleCombinedQMediaType + implements Comparable +{ + public static final SimpleCombinedQMediaType INCOMPATIBLE = new SimpleCombinedQMediaType("", "", 0, 0, 0); + private static final String WILDCARD = "*"; + + private final String type; + private final String subtype; + private final double clientQ; + private final double serverQ; + private final int distance; + + public SimpleCombinedQMediaType(String type, + String subtype, + double clientQ, + double serverQ, + int distance) + { + this.type = type; + this.subtype = subtype; + this.clientQ = clientQ; + this.serverQ = serverQ; + this.distance = distance; + } + + public String getType() + { + return type; + } + + public String getSubtype() + { + return subtype; + } + + public double getClientQ() + { + return clientQ; + } + + public double getServerQ() + { + return serverQ; + } + + public int getDistance() + { + return distance; + } + + @Override + public int compareTo(SimpleCombinedQMediaType that) + { + if (this == INCOMPATIBLE) + { + return that == INCOMPATIBLE ? 0 : -1; + } + else if (that == INCOMPATIBLE) + { + return 1; + } + int wildcardsInThis = 0; + int wildcardsInThat = 0; + if (this.getType().equals(WILDCARD)) + { + wildcardsInThis++; + } + if (this.getSubtype().equals(WILDCARD)) + { + wildcardsInThis++; + } + if (that.getType().equals(WILDCARD)) + { + wildcardsInThat++; + } + if (that.getSubtype().equals(WILDCARD)) + { + wildcardsInThat++; + } + // Step i + if (wildcardsInThis != wildcardsInThat) + { + return wildcardsInThis < wildcardsInThat ? 1 : -1; + } + // Step ii + if (this.getClientQ() != that.getClientQ()) + { + return this.getClientQ() > that.getClientQ() ? 1 : -1; + } + // Step iii + if (this.getServerQ() != that.getServerQ()) + { + return this.getServerQ() > that.getServerQ() ? 1 : -1; + } + // Step iv + if (this.getDistance() != that.getDistance()) + { + return this.getDistance() < that.getDistance() ? 1 : -1; + } + return 0; + } + + public static SimpleCombinedQMediaType create(QMediaType clientType, + QMediaType serverType) + { + String type; + String subtype; + int distance = 0; + if (clientType.getType().equals(WILDCARD)) + { + if (clientType.getType().equals(serverType.getType())) + { + type = WILDCARD; + } + else + { + distance++; + type = serverType.getType(); + } + } + else if (serverType.getType().equals(WILDCARD)) + { + if (clientType.getType().equals(serverType.getType())) + { + type = WILDCARD; + } + else + { + distance++; + type = clientType.getType(); + } + } + else if (clientType.getType().equalsIgnoreCase(serverType.getType())) + { + type = clientType.getType(); + } + else + { + return INCOMPATIBLE; + } + if (clientType.getSubtype().equals(WILDCARD)) + { + if (clientType.getSubtype().equals(serverType.getSubtype())) + { + subtype = WILDCARD; + } + else + { + distance++; + subtype = serverType.getSubtype(); + } + } + else if (serverType.getSubtype().equals(WILDCARD)) + { + if (clientType.getSubtype().equals(serverType.getSubtype())) + { + subtype = WILDCARD; + } + else + { + distance++; + subtype = clientType.getSubtype(); + } + } + else if (clientType.getSubtype().equalsIgnoreCase(serverType.getSubtype())) + { + subtype = clientType.getSubtype(); + } + else + { + return INCOMPATIBLE; + } + double clientQ = clientType.getQValue(); + double serverQ = serverType.getQValue(); + return new SimpleCombinedQMediaType(type, subtype, clientQ, serverQ, + distance); + } + + @Override + public boolean equals(Object o) + { + if (this == o) + { + return true; + } + else if (this == INCOMPATIBLE || o == INCOMPATIBLE) + { + return false; + } + if (!(o instanceof SimpleCombinedQMediaType)) + { + return false; + } + SimpleCombinedQMediaType that = (SimpleCombinedQMediaType) o; + return Double.compare(that.getClientQ(), getClientQ()) == 0 + && Double.compare(that.getServerQ(), getServerQ()) == 0 + && getDistance() == that.getDistance() + && getType().equalsIgnoreCase(that.getType()) + && getSubtype().equalsIgnoreCase(that.getSubtype()); + } + + @Override + public int hashCode() + { + return Objects.hash(getType().toLowerCase(), getSubtype().toLowerCase(), + getClientQ(), getServerQ(), getDistance()); + } + + @Override + public String toString() + { + if (this == INCOMPATIBLE) + { + return "⊥"; + } + return String.format("%s/%s;q=%s;qs=%s;d=%s", getType(), getSubtype(), + getClientQ(), getServerQ(), getDistance()); + } +} diff --git a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/SimpleEndpointRegistry.java b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/SimpleEndpointRegistry.java new file mode 100644 index 00000000..65fde115 --- /dev/null +++ b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/SimpleEndpointRegistry.java @@ -0,0 +1,177 @@ +package com.techempower.gemini.jaxrs.core; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.ws.rs.core.HttpHeaders; +import java.util.*; + + +class SimpleEndpointRegistry implements EndpointRegistry +{ + private final List endpointPairs = new ArrayList<>(); + private final MediaTypeParser mediaTypeParser = new MediaTypeParserImpl("q"); + private final Logger log = LoggerFactory.getLogger(getClass()); + + // Should probably make it part of the spec that getEndpointFor's headers + // are already lowercase or something. + @Override + public Endpoint getEndpointFor(String httpMethod, + String uri, + List> headers) + { + String contentTypeHeader = null; + String acceptHeader = null; + List contentTypeEntries = new ArrayList<>(); + List acceptsEntries = new ArrayList<>(); + for (Map.Entry header : headers) + { + if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(header.getKey())) + { + contentTypeHeader = header.getValue(); + } + else if (HttpHeaders.ACCEPT.equalsIgnoreCase(header.getKey())) + { + acceptHeader = header.getValue(); + } + } + QMediaTypeGroup contentTypeGroup = Optional.ofNullable(contentTypeHeader) + .map(mediaTypeParser::parse) + .orElse(QMediaTypeGroup.DEFAULT_WILDCARD_GROUP); + QMediaTypeGroup acceptGroup = Optional.ofNullable(acceptHeader) + .map(mediaTypeParser::parse) + .orElse(QMediaTypeGroup.DEFAULT_WILDCARD_GROUP); + for (EndpointMetadataPair endpointEntry : endpointPairs) + { + Endpoint endpoint = endpointEntry.getEndpoint(); + EndpointMetadata metadata = endpointEntry.getEndpointMetadata(); + if (!metadata.getHttpMethods().contains(httpMethod)) + { + continue; + } + // Note: This does not normalize the URI, or consider path params + if (!metadata.getPath().equalsIgnoreCase(uri)) + { + continue; + } + // MediaType matching formula: + // - Section: https://download.oracle.com/javaee-archive/jax-rs-spec.java.net/jsr339-experts/att-3593/spec.pdf#subsection.3.7.2 + // - Starts on page: https://download.oracle.com/javaee-archive/jax-rs-spec.java.net/jsr339-experts/att-3593/spec.pdf#page=34 + List consumesServerTypes = metadata.getMediaTypeConsumes() + .getMediaTypes(); + if (consumesServerTypes.isEmpty()) + { + consumesServerTypes = List.of(WrappedQMediaType.DEFAULT_WILDCARD); + } + for (QMediaType contentTypeClientType : contentTypeGroup.getMediaTypes()) + { + for (QMediaType consumesServerType : consumesServerTypes) + { + contentTypeEntries.add(new CombinedEndpointMediaType( + SimpleCombinedQMediaType.create(contentTypeClientType, + consumesServerType), endpoint)); + } + } + List producesServerTypes = metadata.getMediaTypeProduces() + .getMediaTypes(); + if (producesServerTypes.isEmpty()) + { + producesServerTypes = List.of(WrappedQMediaType.DEFAULT_WILDCARD); + } + for (QMediaType acceptsClientType : acceptGroup.getMediaTypes()) + { + for (QMediaType producesServerType : producesServerTypes) + { + acceptsEntries.add(new CombinedEndpointMediaType( + SimpleCombinedQMediaType.create(acceptsClientType, + producesServerType), endpoint)); + } + } + } + if (contentTypeEntries.isEmpty()) + { + return null; + } + contentTypeEntries.sort(Comparator.reverseOrder()); + CombinedEndpointMediaType best = contentTypeEntries.get(0); + if (contentTypeEntries.size() > 1) + { + CombinedEndpointMediaType second = contentTypeEntries.get(1); + if (best.compareTo(second) == 0) + { + acceptsEntries.sort(Comparator.reverseOrder()); + best = acceptsEntries.get(0); + if (acceptsEntries.size() > 1) + { + second = acceptsEntries.get(1); + if (best.compareTo(second) == 0) + { + log.warn("Multiple matches found for {} {}, content type: {}, accept: {}", + httpMethod, uri, contentTypeHeader, acceptHeader); + } + } + } + } + return best.getEndpoint(); + } + + private static class EndpointMetadataPair + { + private final EndpointMetadata endpointMetadata; + private final Endpoint endpoint; + + public EndpointMetadataPair(EndpointMetadata endpointMetadata, + Endpoint endpoint) + { + this.endpointMetadata = endpointMetadata; + this.endpoint = endpoint; + } + + public EndpointMetadata getEndpointMetadata() + { + return endpointMetadata; + } + + public Endpoint getEndpoint() + { + return endpoint; + } + } + + private static class CombinedEndpointMediaType + implements Comparable + { + private final SimpleCombinedQMediaType combinedQMediaType; + private final Endpoint endpoint; + + public CombinedEndpointMediaType(SimpleCombinedQMediaType combinedQMediaType, + Endpoint endpoint) + { + this.combinedQMediaType = combinedQMediaType; + this.endpoint = endpoint; + } + + public SimpleCombinedQMediaType getCombinedQMediaType() + { + return combinedQMediaType; + } + + public Endpoint getEndpoint() + { + return endpoint; + } + + @Override + public int compareTo(CombinedEndpointMediaType that) + { + return this.combinedQMediaType.compareTo(that.combinedQMediaType); + } + } + + @Override + public void register(EndpointMetadata metadata, Endpoint endpoint) + { + endpointPairs.add(new EndpointMetadataPair(metadata, endpoint)); + } + +} diff --git a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/StringStringCharSpanMap.java b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/StringStringCharSpanMap.java new file mode 100644 index 00000000..a16a164c --- /dev/null +++ b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/StringStringCharSpanMap.java @@ -0,0 +1,191 @@ +package com.techempower.gemini.jaxrs.core; + +import org.checkerframework.checker.units.qual.K; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +class StringStringCharSpanMap + implements Map +{ + private final Map backingMap; + private Map stringMap; + + StringStringCharSpanMap(StringStringCharSpanMap sourceMap) + { + this.backingMap = new HashMap<>(sourceMap.backingMap); + } + + public StringStringCharSpanMap(Map backingMap) + { + this.backingMap = backingMap; + } + + boolean isEvaluated() + { + return backingMap != null; + } + + protected Map getStringMap() + { + if (stringMap == null) + { + Map map = new HashMap<>(backingMap.size()); + backingMap.forEach((key, value) -> + map.put(key.toString(), value.toString())); + stringMap = map; + } + return stringMap; + } + + @Override + public int size() + { + if (stringMap == null) + { + return backingMap.size(); + } + return stringMap.size(); + } + + @Override + public boolean isEmpty() + { + if (stringMap == null) + { + return backingMap.isEmpty(); + } + return stringMap.isEmpty(); + } + + @Override + public boolean containsKey(Object key) + { + return getStringMap().containsKey(key); + } + + @Override + public boolean containsValue(Object value) + { + return getStringMap().containsValue(value); + } + + @Override + public String get(Object key) + { + return getStringMap().get(key); + } + + @Override + public String put(String key, String value) + { + return getStringMap().put(key, value); + } + + /** + * Updates the internal CharSpan CharSpan backing map. Does not update the + * String String map if it has been evaluated. + */ + public void putInternal(CharSpan key, CharSpan value) + { + backingMap.put(key, value); + } + + @Override + public String remove(Object key) + { + return getStringMap().remove(key); + } + + @Override + public void putAll(Map m) + { + getStringMap().putAll(m); + } + + @Override + public void clear() + { + getStringMap().clear(); + } + + @Override + public Set keySet() + { + return getStringMap().keySet(); + } + + @Override + public Collection values() + { + return getStringMap().values(); + } + + @Override + public Set> entrySet() + { + return getStringMap().entrySet(); + } + + @Override + public int hashCode() + { + return getStringMap().hashCode(); + } + + @Override + public boolean equals(Object o) + { + if (o == this) + { + return true; + } + + if (!(o instanceof Map)) + { + return false; + } + Map m = (Map) o; + if (m.size() != size()) + { + return false; + } + + try + { + for (Entry e : entrySet()) + { + String key = e.getKey(); + String value = e.getValue(); + if (value == null) + { + if (!(m.get(key) == null && m.containsKey(key))) + { + return false; + } + } + else + { + if (!value.equals(m.get(key))) + { + return false; + } + } + } + } + catch (ClassCastException | NullPointerException unused) + { + return false; + } + + return true; + } + + @Override + public String toString() + { + return getStringMap().toString(); + } +} diff --git a/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/WrappedQMediaType.java b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/WrappedQMediaType.java new file mode 100644 index 00000000..97662055 --- /dev/null +++ b/gemini-jax-rs/src/main/java/com/techempower/gemini/jaxrs/core/WrappedQMediaType.java @@ -0,0 +1,59 @@ +package com.techempower.gemini.jaxrs.core; + +import javax.ws.rs.core.MediaType; +import java.util.Map; + +public class WrappedQMediaType extends QMediaType +{ + static final WrappedQMediaType DEFAULT_WILDCARD = new WrappedQMediaType(MediaType.WILDCARD_TYPE); + private final MediaType mediaType; + private final double qValue; + + /** + * Creates a QMediaType wrapping the given media type, associating it with + * the default q value 1. + */ + public WrappedQMediaType(MediaType mediaType) + { + this(mediaType, 1); + } + + /** + * Creates a QMediaType wrapping the given media type, associating it with + * the given q value. + */ + public WrappedQMediaType(MediaType mediaType, double qValue) + { + this.mediaType = mediaType; + this.qValue = qValue; + } + + @Override + public double getQValue() + { + return qValue; + } + + @Override + public String getType() + { + return mediaType.getType(); + } + + @Override + public String getSubtype() + { + return mediaType.getSubtype(); + } + + @Override + public Map getParameters() + { + return mediaType.getParameters(); + } + + @Override + public MediaType getMediaType() { + return mediaType; + } +} diff --git a/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/CharBufferSplitTest.java b/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/CharBufferSplitTest.java new file mode 100644 index 00000000..f3e039c3 --- /dev/null +++ b/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/CharBufferSplitTest.java @@ -0,0 +1,309 @@ +package com.techempower.gemini.jaxrs.core; + +import com.caucho.util.CharSegment; +import org.junit.Test; + +import java.nio.CharBuffer; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +public class CharBufferSplitTest +{ + static class StringSplitPerformanceTest + { + public static class Runner + { + public static void main(String... args) + { + Performance.test(StringSplitPerformanceTest.class, + ITERATIONS, + List.of( + List.of(CHAR_BUFFER_SPLIT_LINKED_LIST), + List.of(CHAR_BUFFER_SPLIT_ARRAY_LIST), + List.of(CHAR_SEGMENTS_SPLIT_ARRAY_LIST), + List.of(CHAR_SPAN_SPLIT_ARRAY_LIST), + List.of(CHAR_BUFFER_SPLIT_ARRAY), + List.of(STRING_SPLIT) + )); + } + } + + static final int ITERATIONS = 5_400_000; + static final String CHAR_BUFFER_SPLIT_LINKED_LIST = "CharBuffer split LinkedList"; + static final String CHAR_BUFFER_SPLIT_ARRAY_LIST = "CharBuffer split ArrayList"; + static final String CHAR_SEGMENTS_SPLIT_ARRAY_LIST = "Resin CharSegment split ArrayList"; + static final String CHAR_SPAN_SPLIT_ARRAY_LIST = "CharSpan split ArrayList"; + static final String CHAR_BUFFER_SPLIT_ARRAY = "CharBuffer split array (unrealistic)"; + static final String STRING_SPLIT = "String split"; + + public static void main(String... args) + throws Exception + { + String uri = "foo/bar"; + char[] chars = uri.toCharArray(); + if (args.length > 0) + { + switch (args[0]) + { + case CHAR_BUFFER_SPLIT_LINKED_LIST: + perfTestCharBufferSplitLinkedList(uri); + break; + case CHAR_BUFFER_SPLIT_ARRAY_LIST: + perfTestCharBufferSplitArrayList(uri); + break; + case CHAR_SEGMENTS_SPLIT_ARRAY_LIST: + perfTestCharSegmentsSplitArrayList(uri, chars); + break; + case CHAR_SPAN_SPLIT_ARRAY_LIST: + perfTestCharSpanSplitArrayList(uri); + break; + case CHAR_BUFFER_SPLIT_ARRAY: + perfTestCharBufferSplitArray(uri); + break; + case STRING_SPLIT: + perfTestStringSplit(uri); + break; + } + } + } + + public static void perfTestCharBufferSplitLinkedList(String uri) + { + Performance.time(() -> { + for (int i = 0; i < ITERATIONS; i++) + { + List charBuffers = splitCharBufferLinkedList(uri); + } + }); + } + + public static void perfTestCharBufferSplitArrayList(String uri) + { + Performance.time(() -> { + for (int i = 0; i < ITERATIONS; i++) + { + List charBuffers = splitCharBufferArrayList(uri); + } + }); + } + + private static void perfTestCharSegmentsSplitArrayList(String uri, char[] chars) + { + Performance.time(() -> { + for (int i = 0; i < ITERATIONS; i++) + { + List segments = splitCharSegmentArrayList(uri, chars); + } + }); + } + + private static void perfTestCharSpanSplitArrayList(String uri) + { + Performance.time(() -> { + for (int i = 0; i < ITERATIONS; i++) + { + List charSpans = splitCharSpanArrayList(uri); + } + }); + } + + private static void perfTestCharBufferSplitArray(String uri) + { + Performance.time(() -> { + for (int i = 0; i < ITERATIONS; i++) + { + CharBuffer[] charBuffers = splitCharBufferArray(uri); + } + }); + } + + private static void perfTestStringSplit(String uri) + { + Performance.time(() -> { + for (int i = 0; i < ITERATIONS; i++) + { + String[] strings = splitStringArray(uri); + } + }); + } + } + + private static List splitCharBufferLinkedList(String str) + { + int length = str.length(); + if (length == 0) + { + return List.of(); + } + LinkedList buffers = new LinkedList<>(); + int current = 0; + int next; + while ((next = str.indexOf("/", current)) != -1) + { + buffers.add(CharBuffer.wrap(str, current, next)); + current = next + 1; + } + if (current < length - 1) + { + buffers.add(CharBuffer.wrap(str, current, length)); + } + return buffers; + } + + private static List splitCharBufferArrayList(String str) + { + int length = str.length(); + if (length == 0) + { + return List.of(); + } + ArrayList buffers = new ArrayList<>(8); + int current = 0; + int next; + while ((next = str.indexOf("/", current)) != -1) + { + buffers.add(CharBuffer.wrap(str, current, next)); + current = next + 1; + } + if (current < length - 1) + { + buffers.add(CharBuffer.wrap(str, current, length)); + } + return buffers; + } + + private static List splitCharSegmentArrayList(String str, char[] bytes) + { + int length = bytes.length; + if (length == 0) + { + return List.of(); + } + ArrayList segments = new ArrayList<>(8); + int current = 0; + int next; + while ((next = str.indexOf("/", current)) != -1) + { + segments.add(new CharSegment(bytes, current, next - current)); + current = next + 1; + } + if (current < length - 1) + { + segments.add(new CharSegment(bytes, current, length - current)); + } + return segments; + } + + private static List splitCharSpanArrayList(String str) + { + int length = str.length(); + if (length == 0) + { + return List.of(); + } + ArrayList segments = new ArrayList<>(8); + int current = 0; + int next; + while ((next = str.indexOf("/", current)) != -1) + { + segments.add(new CharSpan(str, current, next)); + current = next + 1; + } + if (current < length - 1) + { + segments.add(new CharSpan(str, current, length)); + } + return segments; + } + + private static CharBuffer[] splitCharBufferArray(String str) + { + int length = str.length(); + if (length == 0) + { + return new CharBuffer[0]; + } + CharBuffer[] buffers = new CharBuffer[2]; + int current = 0; + int next; + int index = 0; + while ((next = str.indexOf("/", current)) != -1) + { + buffers[index++] = CharBuffer.wrap(str, current, next); + current = next + 1; + } + if (current < length - 1) + { + buffers[index] = CharBuffer.wrap(str, current, length); + } + return buffers; + } + + @Test + public void doTest() + { + assertEquals( + List.of(CharBuffer.wrap("foo"), CharBuffer.wrap("bar")), + splitCharBufferLinkedList("foo/bar")); + assertEquals( + List.of(CharBuffer.wrap(""), CharBuffer.wrap("foo"), CharBuffer.wrap("bar")), + splitCharBufferLinkedList("/foo/bar")); + assertEquals( + List.of(CharBuffer.wrap(""), CharBuffer.wrap("foo"), CharBuffer.wrap("bar")), + splitCharBufferLinkedList("/foo/bar/")); + assertEquals( + List.of(CharBuffer.wrap("foo"), CharBuffer.wrap("bar")), + splitCharBufferLinkedList("foo/bar/")); + assertEquals( + List.of(CharBuffer.wrap("foo")), + splitCharBufferLinkedList("foo")); + assertEquals( + List.of(CharBuffer.wrap(""), CharBuffer.wrap("foo")), + splitCharBufferLinkedList("/foo")); + assertEquals( + List.of(CharBuffer.wrap(""), CharBuffer.wrap("foo")), + splitCharBufferLinkedList("/foo/")); + assertEquals( + List.of(CharBuffer.wrap("foo")), + splitCharBufferLinkedList("foo/")); + } + + @Test + public void doTest2() + { + assertArrayEquals( + new String[]{"foo", "bar"}, + splitStringArray("foo/bar")); + assertArrayEquals( + new String[]{"", "foo", "bar"}, + splitStringArray("/foo/bar")); + assertArrayEquals( + new String[]{"", "foo", "bar"}, + splitStringArray("/foo/bar/")); + assertArrayEquals( + new String[]{"foo", "bar"}, + splitStringArray("foo/bar/")); + assertArrayEquals( + new String[]{"foo"}, + splitStringArray("foo")); + assertArrayEquals( + new String[]{"", "foo"}, + splitStringArray("/foo")); + assertArrayEquals( + new String[]{"", "foo"}, + splitStringArray("/foo/")); + assertArrayEquals( + new String[]{"foo"}, + splitStringArray("foo/")); + } + + private static String[] splitStringArray(String str) + { + return str.split("/"); + } + +} diff --git a/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/EndpointRegistryTest.java b/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/EndpointRegistryTest.java new file mode 100644 index 00000000..709b41eb --- /dev/null +++ b/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/EndpointRegistryTest.java @@ -0,0 +1,126 @@ +package com.techempower.gemini.jaxrs.core; + +import org.junit.Before; +import org.junit.Test; +import org.junit.experimental.runners.Enclosed; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +import javax.ws.rs.HttpMethod; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.junit.Assert.*; + +@RunWith(Enclosed.class) +public class EndpointRegistryTest +{ + @RunWith(Parameterized.class) + public static class ShouldGetTheRightEndpoint + { + + private static final Endpoint endpointA = new TestEndpoint("Endpoint A"); + private static final Endpoint endpointB = new TestEndpoint("Endpoint B"); + private static final Endpoint endpointC = new TestEndpoint("Endpoint C"); + + @Parameters(name = "request: \"{1}\" - \"{2}\" - \"{3}\"") + public static Object[][] params() + { + return new Object[][]{ + // Should support basic matches + {endpointA, HttpMethod.GET, "foo/bar", List.of()}, + {endpointA, HttpMethod.GET, "foo/bar", List.of(Map.entry(HttpHeaders.CONTENT_TYPE, "application/json"))}, + // Should support advanced matches + {endpointB, HttpMethod.GET, "foo/bar", List.of(Map.entry(HttpHeaders.CONTENT_TYPE, "text/html"))}, + {endpointC, HttpMethod.GET, "foo/bar", List.of(Map.entry(HttpHeaders.CONTENT_TYPE, "application/xml"))}, + {endpointA, HttpMethod.GET, "foo/bar", List.of(Map.entry(HttpHeaders.CONTENT_TYPE, "application/xml; q=0.2, application/json; q=0.7"))}, + // TODO: Test/implement ACCEPT header/produces media types. + }; + } + + @Parameter + public Endpoint expected; + + @Parameter(1) + public String httpMethod; + + @Parameter(2) + public String uri; + + @Parameter(3) + public List> headers; + + public EndpointRegistry registry; + + @Before + public void setUp() throws Exception + { + registry = new SimpleEndpointRegistry(); + registry.register(new EndpointMetadata( + "foo/bar", Set.of(HttpMethod.GET), + new QMediaTypeGroup(List.of(mediaType("application", "json", 1, Map.of()))), + new QMediaTypeGroup(List.of()) + ), endpointA); + registry.register(new EndpointMetadata( + "foo/bar", Set.of(HttpMethod.GET), + new QMediaTypeGroup(List.of(mediaType("text", "html", 0.9, Map.of()))), + new QMediaTypeGroup(List.of()) + ), endpointB); + registry.register(new EndpointMetadata( + "foo/bar", Set.of(HttpMethod.GET), + new QMediaTypeGroup(List.of(mediaType("application", "xml", 0.8, Map.of()))), + new QMediaTypeGroup(List.of()) + ), endpointC); + } + + @Test + public void getEndpointFor() + { + assertEquals(expected, + registry.getEndpointFor(httpMethod, uri, headers)); + } + } + + private static class TestEndpoint implements Endpoint + { + private final String name; + + public TestEndpoint(String name) + { + this.name = name; + } + + @Override + public Object invoke(String httpMethod, + String uri, + List> headers, + Map pathParams, + Map queryParams, + String body) + { + // Invocation is not part of this test. + throw new UnsupportedOperationException(); + } + + @Override + public String toString() + { + return "TestEndpoint{" + + "name='" + name + '\'' + + '}'; + } + } + + private static QMediaType mediaType(String type, + String subtype, + double qValue, + Map parameters) + { + return new WrappedQMediaType(new MediaType(type, subtype, parameters), qValue); + } +} \ No newline at end of file diff --git a/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/JavaProcess.java b/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/JavaProcess.java new file mode 100644 index 00000000..de76b17a --- /dev/null +++ b/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/JavaProcess.java @@ -0,0 +1,53 @@ +package com.techempower.gemini.jaxrs.core; + +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +// Modified from https://stackoverflow.com/a/35275894 and https://stackoverflow.com/a/58259581 +public final class JavaProcess +{ + private JavaProcess() + { + } + + public static String exec(Class klass, String... args) + throws IOException, InterruptedException + { + String javaHome = System.getProperty("java.home"); + String javaBin = javaHome + + File.separator + "bin" + + File.separator + "java"; + String classpath = System.getProperty("java.class.path"); + String className = klass.getName(); + + List command = new LinkedList<>(); + command.add(javaBin); + command.add("-cp"); + command.add(classpath); + command.add(className); + if (args != null) + { + command.addAll(Arrays.asList(args)); + } + + ProcessBuilder builder = new ProcessBuilder(command); + String result = ""; + Process process; + try + { + process = builder.start(); + result = new String(process.getInputStream().readAllBytes()); + + process.waitFor(); + process.destroy(); + } + catch (Exception e) + { + e.printStackTrace(); + } + return result; + } +} diff --git a/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcherTest.java b/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcherTest.java new file mode 100644 index 00000000..f5ef0a49 --- /dev/null +++ b/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/JaxRsDispatcherTest.java @@ -0,0 +1,723 @@ +package com.techempower.gemini.jaxrs.core; + +import com.esotericsoftware.reflectasm.MethodAccess; +import org.junit.Test; + +import javax.ws.rs.GET; +import javax.ws.rs.HttpMethod; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; + +import java.lang.reflect.Method; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class JaxRsDispatcherTest +{ + @Path("/test") + public static class TestCase1 + { + @GET + @Path("/bar") + public String doIt() + { + return "did-it-test-case-1"; + } + } + + @Path("/{test}") + public static class TestCase2 + { + @GET + @Path("/bar") + public String doIt(@PathParam("test") String test) + { + return "did-it-test-case-2" + test; + } + } + + @Path("/{test: .+}") + public static class TestCase3 + { + @GET + @Path("/bar") + public String doIt(@PathParam("test") String test) + { + return "did-it-test-case-3" + test; + } + } + + @Path("/{test1: \\d+}-dog-{test2}") + public static class TestCase4 + { + @GET + @Path("/bar") + public String doIt(@PathParam("test1") String test1, + @PathParam("test2") String test2) + { + return "did-it-test-case-4" + test1 + test2; + } + } + + @Path("/{test1: \\d+}/-dog-{test2}") + public static class TestCase5 + { + @GET + @Path("/bar") + public String doIt(@PathParam("test1") String test1, + @PathParam("test2") String test2) + { + return "did-it-test-case-5" + test1 + test2; + } + } + + @Path("/foo") + public static class TestCase5B + { + @GET + @Path("/{test1}-dog-{test2}") + public String doIt(@PathParam("test1") String test1, + @PathParam("test2") String test2) + { + return "did-it-test-case-5B" + + String.format("{test1:%s,test2:%s}", test1, test2); + } + } + + @Path("foo") + public static class FooResource + { + @GET + @Path("bar") + public String bar() + { + return "did-it-foo-resource"; + } + } + + @Path("foo") + public static class FooResourceVar + { + @GET + @Path("{bar}") + public String bar(@PathParam("bar") String bar) + { + return "did-it-foo-resource-var" + bar; + } + } + + @Path("/foo") + public interface TestCase6 + { + @GET + @Path("/bar") + String doIt(); + } + + @Path("/foo") + public static class TestCase6Impl + implements TestCase6 + { + @Override + public String doIt() + { + return "did-it-test-case-6"; + } + } + + public static class TestCase6Impl2 + implements TestCase6 + { + @Override + public String doIt() + { + return "did-it-test-case-6"; + } + } + + @Path("/foo") + public static class TestCase7 + { + @GET + @Path("/{bar}") + public String doIt(@PathParam("bar") UUID test) + { + return "did-it-test-case-7" + test; + } + } + + @Path("/{test}") + public static class TestCase8 + { + @GET + @Path("/bar") + public String doIt(@PathParam("test") String test) + { + return "did-it-test-case-8" + test; + } + } + + @Test + public void simpleEndpointsShouldBeDispatched() + { + var dispatcher = new JaxRsDispatcher(); + dispatcher.register(new TestCase1()); + assertEquals("did-it-test-case-1", + dispatcher.dispatch(HttpMethod.GET, "/test/bar")); + } + + @Test + public void classRegistrationShouldBeSupported() + { + // TODO: Not implemented yet + var dispatcher = new JaxRsDispatcher(); + dispatcher.register(TestCase1.class); + assertEquals("did-it-test-case-1", + dispatcher.dispatch(HttpMethod.GET, "/test/bar")); + } + + @Test + public void precedentsShouldBeRespected() + { + // TODO: Not implemented yet + var dispatcher = new JaxRsDispatcher(); + dispatcher.register(new TestCase3()); + dispatcher.register(new TestCase2()); + dispatcher.register(new TestCase1()); + assertEquals("did-it-test-case-3dog", + dispatcher.dispatch(HttpMethod.GET, "/dog/bar")); + assertEquals("did-it-test-case-1", + dispatcher.dispatch(HttpMethod.GET, "/test/bar")); + } + + @Test + public void pathParamsShouldBeProvided() + { + var dispatcher = new JaxRsDispatcher(); + dispatcher.register(new TestCase2()); + assertEquals("did-it-test-case-2dog", + dispatcher.dispatch(HttpMethod.GET, "/dog/bar")); + } + + @Test + public void pathParamRegexesShouldBeSupported1() + { + var dispatcher = new JaxRsDispatcher(); + dispatcher.register(new TestCase4()); + assertEquals("did-it-test-case-449cat", + dispatcher.dispatch(HttpMethod.GET, "/49-dog-cat/bar")); + } + + @Test + public void pathParamRegexesShouldBeSupported2() + { + var dispatcher = new JaxRsDispatcher(); + dispatcher.register(new TestCase5()); + assertEquals("did-it-test-case-549cat", + dispatcher.dispatch(HttpMethod.GET, "/49/-dog-cat/bar")); + } + + @Test + public void pathParamRegexesShouldBeSupported3() + { + var dispatcher = new JaxRsDispatcher(); + dispatcher.register(new TestCase5B()); + assertEquals("did-it-test-case-5B{test1:-dog-,test2:-dog--dog-}", + dispatcher.dispatch(HttpMethod.GET, "/foo/-dog--dog--dog--dog-")); + } + + @Test + public void uuidPathParamsShouldBeSupported() + { + var dispatcher = new JaxRsDispatcher(); + dispatcher.register(new TestCase7()); + UUID id = UUID.randomUUID(); + assertEquals("did-it-test-case-7" + id, + dispatcher.dispatch(HttpMethod.GET, "/foo/" + id)); + } + + @Test + public void pathParamsAtClassLevelShouldBeSupported() + { + var dispatcher = new JaxRsDispatcher(); + dispatcher.register(new TestCase8()); + assertEquals("did-it-test-case-8dog", + dispatcher.dispatch(HttpMethod.GET, "/dog/bar")); + } + + @Test + public void methodAnnotationInheritanceShouldBeCaptured() + { + // TODO: Not yet implemented + var dispatcher = new JaxRsDispatcher(); + dispatcher.register(new TestCase6Impl()); + assertEquals("did-it-test-case-6", + dispatcher.dispatch(HttpMethod.GET, "/foo/bar")); + } + + @Test + public void classAnnotationInheritanceShouldNotBeCaptured() + { + // TODO: Not yet implemented + var dispatcher = new JaxRsDispatcher(); + dispatcher.register(new TestCase6Impl2()); + // TODO: Null isn't really a great indicator here, since a given + // method could very well have returned null. + assertNull(dispatcher.dispatch(HttpMethod.GET, "/foo/bar")); + } + + static class SimpleUriPerformanceTest + { + public static class Runner + { + public static void main(String... args) + { + Performance.test(SimpleUriPerformanceTest.class, + ITERATIONS, + List.of( + List.of(REGEX_SLOW), + List.of(REGEX_FAST), + List.of(MAP_TREE), + List.of(MAP_FLAT), + List.of(MAP_FLAT_WITH_URI_SPLIT), + // TODO: Make a builder for generating these combinations. + // Something like: + // Performance.combinations() + // .next(JAX_RS) + // .next(GUAVA_LOADING_CACHE, RESIN_LRU_CACHE, + // CAFFEINE_LOADING_CACHE) + // .next(/* etc */) + // .build() + List.of(JAX_RS, NO_CACHE), + List.of(JAX_RS, GUAVA_LOADING_CACHE), + List.of(JAX_RS, RESIN_LRU_CACHE), + List.of(JAX_RS, CAFFEINE_LOADING_CACHE) + )); + } + } + + static final int ITERATIONS = 2_700_000; + // approaches + static final String REGEX_SLOW = "regex slow"; + static final String REGEX_FAST = "regex fast"; + static final String MAP_TREE = "tree of maps"; + static final String MAP_FLAT = "single flat map"; + static final String MAP_FLAT_WITH_URI_SPLIT = "single flat map with URI split"; + static final String JAX_RS = "jax-rs"; + // jax-rs customizations + static final String NO_CACHE = "no cache"; + static final String GUAVA_LOADING_CACHE = "Guava LoadingCache"; + static final String RESIN_LRU_CACHE = "Resin LruCache"; + static final String CAFFEINE_LOADING_CACHE = "Caffeine LoadingCache"; + + public static void main(String... args) + throws Exception + { + if (args.length > 0) + { + switch (args[0]) + { + case REGEX_SLOW: + perfTestRegexSlow(); + break; + case REGEX_FAST: + perfTestRegexFast(); + break; + case MAP_TREE: + perfTestMapTree(); + break; + case MAP_FLAT: + perfTestMapFlat(); + break; + case MAP_FLAT_WITH_URI_SPLIT: + perfTestMapFlatWithUriSplit(); + break; + case JAX_RS: + { + JaxRsDispatcher jaxRsDispatcher = new JaxRsDispatcher(); + jaxRsDispatcher.register(new FooResource()); + switch (args[1]) + { + case NO_CACHE: + break; + case GUAVA_LOADING_CACHE: + jaxRsDispatcher.setCacheEnabled(true); + break; + case RESIN_LRU_CACHE: + jaxRsDispatcher.setCacheEnabled(true); + jaxRsDispatcher.setUseResinCache(true); + break; + case CAFFEINE_LOADING_CACHE: + jaxRsDispatcher.setCacheEnabled(true); + jaxRsDispatcher.setUseCaffCache(true); + break; + default: + throw new RuntimeException(); + } + perfTestJaxRs(jaxRsDispatcher); + break; + } + default: + throw new RuntimeException(); + } + } + } + + public static void perfTestRegexSlow() + { + String uriNoTrailingSlash = "foo/bar"; + Performance.time(() -> { + for (int i = 0; i < ITERATIONS; i++) + { + Pattern pattern = Pattern.compile("foo/bar"); + Matcher matcher = pattern.matcher(uriNoTrailingSlash); + boolean found = matcher.find(); + } + }); + } + + public static void perfTestRegexFast() + { + String uriNoTrailingSlash = "foo/bar"; + Pattern pattern = Pattern.compile("foo/bar"); + Performance.time(() -> { + for (int i = 0; i < ITERATIONS; i++) + { + Matcher matcher = pattern.matcher(uriNoTrailingSlash); + boolean found = matcher.find(); + } + }); + } + + public static void perfTestMapTree() + { + Map>> foo = Map.of("foo", + Map.of("bar", Map.of(HttpMethod.GET, new Object()))); + String uriNoTrailingSlash = "foo/bar"; + Performance.time(() -> { + for (int i = 0; i < ITERATIONS; i++) + { + String[] uriSegments = uriNoTrailingSlash.split("/"); + if (foo.containsKey(uriSegments[0])) + { + Map> inner = foo.get(uriSegments[0]); + if (inner.containsKey(uriSegments[1])) + { + Map endpointsByHttpMethod = inner.get( + uriSegments[1]); + Object resource = endpointsByHttpMethod.get(HttpMethod.GET); + } + } + } + }); + } + + public static void perfTestMapFlat() + { + Map> fooB = Map.of("foo/bar", + Map.of(HttpMethod.GET, new Object())); + String uriNoTrailingSlash = "foo/bar"; + Performance.time(() -> { + for (int i = 0; i < ITERATIONS; i++) + { + if (fooB.containsKey(uriNoTrailingSlash)) + { + Map endpointsByHttpMethod = fooB.get(uriNoTrailingSlash); + Object resource = endpointsByHttpMethod.get(HttpMethod.GET); + } + } + }); + } + + public static void perfTestMapFlatWithUriSplit() + { + Map> fooB = Map.of("foo/bar", + Map.of(HttpMethod.GET, new Object())); + String uriNoTrailingSlash = "foo/bar"; + Performance.time(() -> { + for (int i = 0; i < ITERATIONS; i++) + { + String[] uriSegments = uriNoTrailingSlash.split("/"); + if (fooB.containsKey(uriNoTrailingSlash)) + { + Map endpointsByHttpMethod = fooB.get(uriNoTrailingSlash); + Object resource = endpointsByHttpMethod.get(HttpMethod.GET); + } + } + }); + } + + public static void perfTestJaxRs(JaxRsDispatcher jaxRsDispatcher) + { + String uriNoTrailingSlash = "foo/bar"; + Performance.time(() -> { + for (int i = 0; i < ITERATIONS; i++) + { + jaxRsDispatcher.getBestMatchInfo(HttpMethod.GET, + uriNoTrailingSlash); + } + }); + } + } + + static class ComplexUriPerformanceTest + { + public static class Runner + { + public static void main(String... args) + { + Performance.test(ComplexUriPerformanceTest.class, + ITERATIONS, + List.of( + List.of(REGEX_SLOW), + List.of(REGEX_FAST), + List.of(JAX_RS, NO_CACHE), + List.of(JAX_RS, GUAVA_LOADING_CACHE), + List.of(JAX_RS, RESIN_LRU_CACHE), + List.of(JAX_RS, CAFFEINE_LOADING_CACHE), + List.of(CREATE_OBJECTS) + )); + } + } + + static final int ITERATIONS = 2_700_000; + // approaches + static final String REGEX_SLOW = "regex slow"; + static final String REGEX_FAST = "regex fast"; + static final String JAX_RS = "jax-rs"; + static final String CREATE_OBJECTS = "create objects (subset of jax-rs)"; + // jax-rs customizations + static final String NO_CACHE = "no cache"; + static final String GUAVA_LOADING_CACHE = "Guava LoadingCache"; + static final String RESIN_LRU_CACHE = "Resin LruCache"; + static final String CAFFEINE_LOADING_CACHE = "Caffeine LoadingCache"; + + public static void main(String... args) + throws Exception + { + if (args.length > 0) + { + switch (args[0]) + { + case REGEX_SLOW: + perfTestRegexSlow(); + break; + case REGEX_FAST: + perfTestRegexFast(); + break; + case JAX_RS: + { + JaxRsDispatcher jaxRsDispatcher = new JaxRsDispatcher(); + jaxRsDispatcher.register(new FooResourceVar()); + switch (args[1]) + { + case NO_CACHE: + break; + case GUAVA_LOADING_CACHE: + jaxRsDispatcher.setCacheEnabled(true); + break; + case RESIN_LRU_CACHE: + jaxRsDispatcher.setCacheEnabled(true); + jaxRsDispatcher.setUseResinCache(true); + break; + case CAFFEINE_LOADING_CACHE: + jaxRsDispatcher.setCacheEnabled(true); + jaxRsDispatcher.setUseCaffCache(true); + break; + default: + throw new RuntimeException(); + } + perfTestJaxRs(jaxRsDispatcher); + break; + } + case CREATE_OBJECTS: + perfTestCreateObjects(); + break; + default: + throw new RuntimeException(); + } + } + } + + public static void perfTestRegexSlow() + { + var uuidGroupNameStr = "g" + UUID.randomUUID().toString().replaceAll("-", ""); + var uuidStr = UUID.randomUUID().toString(); + String uri = "foo/" + uuidStr + "/"; + Performance.time(() -> { + for (int i = 0; i < ITERATIONS; i++) + { + Pattern pattern = Pattern.compile("foo/(?<" + uuidGroupNameStr + ">[^/]+?)/.*"); + Matcher matcher = pattern.matcher(uri); + boolean found = matcher.find(); + String matchFound = matcher.group(uuidGroupNameStr); + } + }); + } + + public static void perfTestRegexFast() + { + var uuidGroupNameStr = "g" + UUID.randomUUID().toString().replaceAll("-", ""); + var uuidStr = UUID.randomUUID().toString(); + String uri = "foo/" + uuidStr + "/"; + Pattern pattern = Pattern.compile("foo/(?<" + uuidGroupNameStr + ">[^/]+?)/.*"); + Performance.time(() -> { + for (int i = 0; i < ITERATIONS; i++) + { + Matcher matcher = pattern.matcher(uri); + boolean found = matcher.find(); + String matchFound = matcher.group(uuidGroupNameStr); + } + }); + } + + public static void perfTestJaxRs(JaxRsDispatcher jaxRsDispatcher) + { + var uuidStr = UUID.randomUUID().toString(); + String uri = "foo/" + uuidStr + "/"; + Performance.time(() -> { + for (int i = 0; i < ITERATIONS; i++) + { + jaxRsDispatcher.getBestMatchInfo(HttpMethod.GET, uri); + } + }); + } + + public static void perfTestCreateObjects() + { + Performance.time(() -> { + for (int i = 0; i < ITERATIONS; i++) + { + //noinspection MismatchedQueryAndUpdateOfCollection + List matches = new ArrayList<>(1); + matches.add( + new JaxRsDispatcher.DispatchMatch(null, null, null, null)); + } + }); + } + } + + static class MethodCallPerformanceTest + { + public static class Runner + { + public static void main(String... args) + { + Performance.test(MethodCallPerformanceTest.class, + ITERATIONS, + List.of( + List.of(DIRECT_METHOD_CALL), + List.of(REFLECTION_LIBRARY), + List.of(METHOD_REFERENCE), + List.of(STANDARD_REFLECTION) + )); + } + } + + static final int ITERATIONS = 1_000_000_000; + static final String DIRECT_METHOD_CALL = "direct method call"; + static final String REFLECTION_LIBRARY = "reflection library"; + static final String METHOD_REFERENCE = "method reference"; + static final String STANDARD_REFLECTION = "standard reflection"; + + static class Cow + { + public void moo() + { + } + } + + public static void main(String... args) + throws Exception + { + if (args.length > 0) + { + switch (args[0]) + { + case DIRECT_METHOD_CALL: + perfTestDirect(); + break; + case REFLECTION_LIBRARY: + perfTestMethodAccess(); + break; + case METHOD_REFERENCE: + perfTestReference(); + break; + case STANDARD_REFLECTION: + perfTestReflection(); + break; + } + } + } + + public static void perfTestDirect() + { + Cow cow = new Cow(); + for (int i = 0; i < ITERATIONS; i++) + { + cow.moo(); + } + long start = System.nanoTime(); + for (int i = 0; i < ITERATIONS; i++) + { + cow.moo(); + } + System.out.print(System.nanoTime() - start); + } + + public static void perfTestReference() + { + Cow cow = new Cow(); + Runnable moo = cow::moo; + for (int i = 0; i < ITERATIONS; i++) + { + moo.run(); + } + long start = System.nanoTime(); + for (int i = 0; i < ITERATIONS; i++) + { + moo.run(); + } + System.out.print(System.nanoTime() - start); + } + + public static void perfTestReflection() + throws Exception + { + Cow cow = new Cow(); + Method method = cow.getClass().getMethod("moo"); + method.setAccessible(true); + for (int i = 0; i < ITERATIONS; i++) + { + method.invoke(cow); + } + long start = System.nanoTime(); + for (int i = 0; i < ITERATIONS; i++) + { + method.invoke(cow); + } + System.out.print(System.nanoTime() - start); + } + + public static void perfTestMethodAccess() + { + Cow cow = new Cow(); + MethodAccess methodAccess = MethodAccess.get(Cow.class); + int index = methodAccess.getIndex("moo"); + for (int i = 0; i < ITERATIONS; i++) + { + methodAccess.invoke(cow, index); + } + long start = System.nanoTime(); + for (int i = 0; i < ITERATIONS; i++) + { + methodAccess.invoke(cow, index); + } + System.out.print(System.nanoTime() - start); + } + } +} \ No newline at end of file diff --git a/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/JaxRsMediaTypeDispatchingTest.java b/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/JaxRsMediaTypeDispatchingTest.java new file mode 100644 index 00000000..0738fbff --- /dev/null +++ b/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/JaxRsMediaTypeDispatchingTest.java @@ -0,0 +1,53 @@ +package com.techempower.gemini.jaxrs.core; + +import org.junit.Test; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.HttpMethod; +import javax.ws.rs.Path; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +public class JaxRsMediaTypeDispatchingTest +{ + @Path("test0") + private static class TestResource0 { + @Path("x") + @Consumes(MediaType.APPLICATION_JSON) + @GET + public String handleX0() { + return "handle-test0-x0"; + } + + @Path("x") + @Consumes(MediaType.APPLICATION_XML) + @GET + public String handleX1() { + return "handle-test0-x1"; + } + } + + private JaxRsDispatcher newJaxRsDispatcher() { + var dispatcher = new JaxRsDispatcher(); + dispatcher.register(new TestResource0()); + return dispatcher; + } + + @Test + public void doTest() { + var dispatcher = newJaxRsDispatcher(); + assertEquals("handle-test0-x0", dispatcher.dispatch(HttpMethod.GET, + "/test0/x", List.of( + Map.entry(HttpHeaders.CONTENT_TYPE, + "application/xml; q=0.2, application/json; q=0.7")))); + assertEquals("handle-test0-x1", dispatcher.dispatch(HttpMethod.GET, + "/test0/x", List.of( + Map.entry(HttpHeaders.CONTENT_TYPE, + "application/xml; q=0.7, application/json; q=0.2")))); + } +} diff --git a/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/MediaTypeParserTest.java b/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/MediaTypeParserTest.java new file mode 100644 index 00000000..2f7ca73d --- /dev/null +++ b/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/MediaTypeParserTest.java @@ -0,0 +1,105 @@ +package com.techempower.gemini.jaxrs.core; + +import org.junit.Test; +import org.junit.experimental.runners.Enclosed; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import javax.ws.rs.core.MediaType; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.runners.Parameterized.Parameters; +import static org.junit.runners.Parameterized.Parameter; + +@RunWith(Enclosed.class) +public class MediaTypeParserTest +{ + @RunWith(Parameterized.class) + public static class ShouldParseSuccessfully + { + @Parameters(name = "q-value key: \"{1}\", input: \"{2}\"") + public static Object[][] params() + { + return new Object[][]{ + // Should support basic captures + {group(mediaType("text", "html", 1.0d, Map.of())), "q", "text/html"}, + // Should support q-value + {group(mediaType("text", "html", 0.9d, Map.of("q", "0.9"))), "q", "text/html;q=0.9"}, + {group(mediaType("text", "html", 1.0d, Map.of("q", "1.0"))), "q", "text/html;q=1.0"}, + {group(mediaType("text", "html", 0.999d, Map.of("q", "0.999"))), "q", "text/html;q=0.999"}, + // Should support other q-value keys + {group(mediaType("text", "html", 0.9d, Map.of("qs", "0.9"))), "qs", "text/html;qs=0.9"}, + {group(mediaType("text", "html", 0.8d, Map.of("q", "0.9", "qs", "0.8"))), "qs", "text/html;q=0.9;qs=0.8"}, + // Should support multi-type captures + {group(mediaType("text", "html", 1.0d, Map.of()), mediaType("text", "xml", 0.9d, Map.of("q", "0.9"))), "q", "text/html,text/xml;q=0.9"}, + // Should support non q-value parameters + {group(mediaType("text", "html", 0.8d, Map.of("f", "d", "q", "0.8")), mediaType("text", "xml", 0.9d, Map.of("q", "0.9"))), "q", "text/html;f=d;q=0.8,text/xml;q=0.9"}, + // Should respect quote rules (these are a few random variations) + {group(mediaType("text", "html", 1.0d, Map.of("f", "dog")), mediaType("text", "xml", 0.9d, Map.of("q", "0.9"))), "q", "text/html;f=\"dog\",text/xml;q=0.9"}, + {group(mediaType("text", "html", 1.0d, Map.of("f", "d,og")), mediaType("text", "xml", 0.9d, Map.of("q", "0.9"))), "q", "text/html;f=\"d,og\",text/xml;q=0.9"}, + {group(mediaType("text", "html", 0.8d, Map.of("f", "d,o", "q", "0.8")), mediaType("text", "xml", 0.9d, Map.of("q", "0.9"))), "q", "text/html;f=\"d,o\";q=0.8,text/xml;q=0.9"}, + // Should respect proper whitespace rules + {group(mediaType("text", "html", 1.0d, Map.of()), mediaType("text", "xml", 0.9d, Map.of("q", "0.9"))), "q", "text/html,text/xml; \tq=0.9"}, + {group(mediaType("application", "xml", 0.2d, Map.of("q", "0.2")), mediaType("application", "json", 0.7d, Map.of("q", "0.7"))), "q", "application/xml; q=0.2, application/json; q=0.7"}, + }; + } + + @Parameter + public QMediaTypeGroup expected; + + @Parameter(1) + public String qValueKey; + + @Parameter(2) + public String mediaType; + + @Test + public void parse() + { + assertEquals(expected, new MediaTypeParserImpl(qValueKey).parse(mediaType)); + } + + private static QMediaType mediaType(String type, String subtype, double qValue, Map parameters) + { + return new WrappedQMediaType(new MediaType(type, subtype, parameters), qValue); + } + + private static QMediaTypeGroup group(QMediaType... mediaTypes) + { + return new QMediaTypeGroup(List.of(mediaTypes)); + } + } + + @RunWith(Parameterized.class) + public static class ShouldFailToParse + { + @Parameters(name = "q-value key: \"{0}\", input: \"{1}\"") + public static Object[][] params() + { + return new Object[][]{ + // Should fail to parse improper placement of quotes + {"q", "text/html;f=\\\"d,og\\\",text/xml;q=0.9"}, + {"q", "text/html;f=\\\"d\";q=0.8,text/xml;q=0.9"}, + {"q", "text/html;f=\\\"d\\\";q=0.8,text/xml;q=0.9"}, + // Should fail to parse improper placement of whitespace + {"q", "text/html,text/xml;q= \t0.9"}, + // Should fail to parse excessive numbers after decimal point + {"q", "text/html,text/xml;q=0.9999"}, + }; + } + + @Parameter + public String qValueKey; + + @Parameter(1) + public String mediaType; + + @Test(expected = ProcessingException.class) + public void parse() + { + new MediaTypeParserImpl(qValueKey).parse(mediaType); + } + } +} \ No newline at end of file diff --git a/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/Performance.java b/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/Performance.java new file mode 100644 index 00000000..fdedac42 --- /dev/null +++ b/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/Performance.java @@ -0,0 +1,110 @@ +package com.techempower.gemini.jaxrs.core; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.text.DecimalFormat; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public final class Performance +{ + private Performance() + { + } + + public static void test(Class type, + int iterationsMetadata, + List> options) + { + test(true, type, iterationsMetadata, options); + } + + /** Set fork to false to get better error reporting. Otherwise always have + * it as true for the best/most fair performance reporting. */ + public static void test(boolean fork, + Class type, + int iterationsMetadata, + List> options) + { + try + { + System.out.println(String.format("Running performance test `%s`...", + type.getSimpleName())); + long start = System.currentTimeMillis(); + Map, Long> totalNanosecondsByOption = new LinkedHashMap<>(); + long COUNT = 10; + for (int i = 0; i < COUNT; i++) + { + for (List args : options) + { + String output; + if (fork) + { + output = JavaProcess.exec(type, args.toArray(String[]::new)); + } + else + { + PrintStream originalOut = System.out; + try + { + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + final String utf8 = StandardCharsets.UTF_8.name(); + try (PrintStream ps = new PrintStream(baos, true, utf8)) + { + System.setOut(ps); + type.getDeclaredMethod("main", String[].class) + .invoke(null, (Object) args.toArray(String[]::new)); + output = baos.toString(utf8); + } + } + finally + { + System.setOut(originalOut); + } + + } + if (output.isEmpty()) + { + throw new RuntimeException("Output was empty for args: " + + String.join("::", args)); + } + long milli = Long.parseLong(output); + totalNanosecondsByOption.compute(args, (key, value) -> + (value == null ? 0 : value) + milli); + } + } + System.out.println(String.format(" Total time overall: %sms", + System.currentTimeMillis() - start)); + DecimalFormat commas = new DecimalFormat("#,###"); + DecimalFormat decimal = new DecimalFormat("#.0"); + System.out.println(String.format("The following are the milliseconds" + + " required for each approach to run %s times, averaged over %s" + + " separate runs:", commas.format(iterationsMetadata), COUNT)); + totalNanosecondsByOption.forEach((args, totalNano) -> + System.out.println(String.format(" %s: %sms", + String.join("::", args), + decimal.format((double) totalNano / 1e6 / COUNT)))); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } + + public static void time(ThrowingRunnable runnable) + throws R + { + runnable.run(); + long start = System.nanoTime(); + runnable.run(); + System.out.print(System.nanoTime() - start); + } + + @FunctionalInterface + public interface ThrowingRunnable + { + void run() throws R; + } +} diff --git a/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/SimpleCombinedQMediaTypeTest.java b/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/SimpleCombinedQMediaTypeTest.java new file mode 100644 index 00000000..11352596 --- /dev/null +++ b/gemini-jax-rs/src/test/java/com/techempower/gemini/jaxrs/core/SimpleCombinedQMediaTypeTest.java @@ -0,0 +1,258 @@ +package com.techempower.gemini.jaxrs.core; + +import org.junit.Test; +import org.junit.experimental.runners.Enclosed; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import javax.ws.rs.core.MediaType; + +import java.util.Map; + +import static org.junit.Assert.*; + +@RunWith(Enclosed.class) +public class SimpleCombinedQMediaTypeTest +{ + + @RunWith(Parameterized.class) + public static class compareTo + { + private enum CompareResult + { + FIRST_GREATER, + FIRST_LESSER, + EQUAL; + + public static CompareResult from(int result) + { + if (result > 0) + { + return FIRST_GREATER; + } + else if (result < 0) + { + return FIRST_LESSER; + } + else + { + return EQUAL; + } + } + } + + @Parameterized.Parameters(name = "expected: {0}, first: \"{1}\", second: \"{2}\"") + public static Object[][] params() + { + return new Object[][]{ + // step i + { + CompareResult.EQUAL, + new SimpleCombinedQMediaType("text", "html", 1, 0.7, 0), + new SimpleCombinedQMediaType("application", "xml", 1, 0.7, 0), + }, + { + CompareResult.FIRST_LESSER, + new SimpleCombinedQMediaType("text", "*", 1, 0.9, 0), + new SimpleCombinedQMediaType("application", "xml", 1, 0.7, 0), + }, + { + CompareResult.EQUAL, + new SimpleCombinedQMediaType("text", "*", 1, 0.7, 0), + new SimpleCombinedQMediaType("application", "*", 1, 0.7, 0), + }, + { + CompareResult.FIRST_GREATER, + new SimpleCombinedQMediaType("text", "*", 1, 0.7, 1), + new SimpleCombinedQMediaType("*", "*", 1, 0.9, 0), + }, + { + CompareResult.EQUAL, + new SimpleCombinedQMediaType("*", "*", 1, 0.7, 0), + new SimpleCombinedQMediaType("*", "*", 1, 0.7, 0), + }, + // step ii + { + CompareResult.FIRST_LESSER, + new SimpleCombinedQMediaType("text", "*", 0.9, 0.7, 0), + new SimpleCombinedQMediaType("application", "*", 1, 0.7, 1), + }, + // step iii + { + CompareResult.FIRST_GREATER, + new SimpleCombinedQMediaType("text", "*", 1, 0.8, 1), + new SimpleCombinedQMediaType("application", "*", 1, 0.7, 0), + }, + // step iv + { + CompareResult.FIRST_LESSER, + new SimpleCombinedQMediaType("text", "*", 1, 0.7, 1), + new SimpleCombinedQMediaType("application", "xml", 1, 0.7, 0), + }, + { + CompareResult.FIRST_GREATER, + new SimpleCombinedQMediaType("*", "*", 1, 0.7, 1), + new SimpleCombinedQMediaType("*", "*", 1, 0.7, 2), + }, + // Incompatible media types + { + CompareResult.FIRST_LESSER, + SimpleCombinedQMediaType.INCOMPATIBLE, + new SimpleCombinedQMediaType("*", "*", 1, 0.7, 2), + }, + { + CompareResult.EQUAL, + SimpleCombinedQMediaType.INCOMPATIBLE, + SimpleCombinedQMediaType.INCOMPATIBLE, + }, + }; + } + + @Parameterized.Parameter + public CompareResult expected; + + @Parameterized.Parameter(1) + public SimpleCombinedQMediaType first; + + @Parameterized.Parameter(2) + public SimpleCombinedQMediaType second; + + @Test + public void test() + { + int result = first.compareTo(second); + int resultReversed = second.compareTo(first); + String actualResult = "actual result: " + CompareResult.from(result); + String actualReversedResult = "actual reversed: " + CompareResult.from(resultReversed); + if (expected == CompareResult.FIRST_GREATER) + { + assertTrue(actualResult, result > 0); + assertTrue(actualReversedResult, resultReversed < 0); + } + else if (expected == CompareResult.FIRST_LESSER) + { + assertTrue(actualResult, result < 0); + assertTrue(actualReversedResult, resultReversed > 0); + } + else if (expected == CompareResult.EQUAL) + { + assertEquals(actualResult, 0, result); + assertEquals(actualReversedResult, 0, resultReversed); + } + else + { + fail("Expected CompareResult not defined"); + } + } + } + + @RunWith(Parameterized.class) + public static class create + { + + public static class TestMediaType extends WrappedQMediaType + { + + public TestMediaType(String type, String subtype, double qValue) + { + super(new MediaType(type, subtype, Map.of()), qValue); + } + + @Override + public String toString() + { + return String.format("%s/%s;q=%s", getType(), getSubtype(), + getQValue()); + } + } + + @Parameterized.Parameters(name = "S({1}, {2}) = {0}") + public static Object[][] params() + { + return new Object[][]{ + { + new SimpleCombinedQMediaType("text", "html", 0.9, 0.8, 0), + new TestMediaType("text", "html", 0.9), + new TestMediaType("text", "html", 0.8) + }, + { + new SimpleCombinedQMediaType("text", "html", 0.5, 0.75, 1), + new TestMediaType("text", "html", 0.5), + new TestMediaType("text", "*", 0.75) + }, + { + new SimpleCombinedQMediaType("text", "*", 0.5, 0.75, 1), + new TestMediaType("text", "*", 0.5), + new TestMediaType("*", "*", 0.75) + }, + { + new SimpleCombinedQMediaType("text", "html", 0.5, 1, 2), + new TestMediaType("text", "html", 0.5), + new TestMediaType("*", "*", 1) + }, + { + new SimpleCombinedQMediaType("text", "*", 0.5, 0.75, 0), + new TestMediaType("text", "*", 0.5), + new TestMediaType("text", "*", 0.75) + }, + { + new SimpleCombinedQMediaType("*", "*", 0.5, 0.75, 0), + new TestMediaType("*", "*", 0.5), + new TestMediaType("*", "*", 0.75) + }, + { + SimpleCombinedQMediaType.INCOMPATIBLE, + new TestMediaType("text", "html", 0.5), + new TestMediaType("application", "xml", 0.75) + } + }; + } + + @Parameterized.Parameter + public SimpleCombinedQMediaType expected; + + @Parameterized.Parameter(1) + public QMediaType clientType; + + @Parameterized.Parameter(2) + public QMediaType serverType; + + @Test + public void test() + { + assertEquals(expected, + SimpleCombinedQMediaType.create(clientType, serverType)); + } + } + + @RunWith(Parameterized.class) + public static class equals + { + @Parameterized.Parameters(name = "first: \"{0}\", second: \"{1}\"") + public static Object[][] params() + { + return new Object[][]{ + { + new SimpleCombinedQMediaType("text", "html", 1, 1, 0), + new SimpleCombinedQMediaType("text", "html", 1, 1, 0) + }, + { + new SimpleCombinedQMediaType("text", "html", 1, 1, 0), + new SimpleCombinedQMediaType("text", "HTML", 1, 1, 0) + }, + }; + } + + @Parameterized.Parameter + public SimpleCombinedQMediaType first; + + @Parameterized.Parameter(1) + public SimpleCombinedQMediaType second; + + @Test + public void test() + { + assertEquals(first, second); + } + } +} \ No newline at end of file diff --git a/gemini-legacy-dispatching/pom.xml b/gemini-legacy-dispatching/pom.xml new file mode 100644 index 00000000..304d6628 --- /dev/null +++ b/gemini-legacy-dispatching/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + jar + + + TechEmpower, Inc. + https://www.techempower.com/ + + + + + Revised BSD License, 3-clause + repo + + + + + gemini-parent + com.techempower + 3.1.0-SNAPSHOT + + + com.techempower + gemini-legacy-dispatching + + An extension for Gemini that provides the old request dispatching functionality. + + + + + com.techempower + gemini + + + + + \ No newline at end of file diff --git a/gemini/src/main/java/com/techempower/gemini/exceptionhandler/EmailExceptionHandler.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/exceptionhandler/EmailExceptionHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/exceptionhandler/EmailExceptionHandler.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/exceptionhandler/EmailExceptionHandler.java diff --git a/gemini/src/main/java/com/techempower/gemini/exceptionhandler/ExceptionHandler.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/exceptionhandler/ExceptionHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/exceptionhandler/ExceptionHandler.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/exceptionhandler/ExceptionHandler.java diff --git a/gemini/src/main/java/com/techempower/gemini/exceptionhandler/NotificationExceptionHandler.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/exceptionhandler/NotificationExceptionHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/exceptionhandler/NotificationExceptionHandler.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/exceptionhandler/NotificationExceptionHandler.java diff --git a/gemini/src/main/java/com/techempower/gemini/handler/ThreadDumpHandler.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/handler/ThreadDumpHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/handler/ThreadDumpHandler.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/handler/ThreadDumpHandler.java diff --git a/gemini/src/main/java/com/techempower/gemini/handler/package-info.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/handler/package-info.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/handler/package-info.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/handler/package-info.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/BasicPathHandler.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/BasicPathHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/BasicPathHandler.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/BasicPathHandler.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/BasicPathManager.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/BasicPathManager.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/BasicPathManager.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/BasicPathManager.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/DispatchLogger.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/DispatchLogger.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/DispatchLogger.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/DispatchLogger.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/DispatchSegment.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/DispatchSegment.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/DispatchSegment.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/DispatchSegment.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/FourZeroFourHandler.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/FourZeroFourHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/FourZeroFourHandler.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/FourZeroFourHandler.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/JsonRequestBodyAdapter.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/JsonRequestBodyAdapter.java similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/JsonRequestBodyAdapter.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/JsonRequestBodyAdapter.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/MethodSegmentHandler.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/MethodSegmentHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/MethodSegmentHandler.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/MethodSegmentHandler.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/MethodUriHandler.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/MethodUriHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/MethodUriHandler.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/MethodUriHandler.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/NotImplementedHandler.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/NotImplementedHandler.java similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/NotImplementedHandler.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/NotImplementedHandler.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/PathDispatcher.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/PathDispatcher.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/PathDispatcher.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/PathDispatcher.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/PathHandler.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/PathHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/PathHandler.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/PathHandler.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/PathSegments.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/PathSegments.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/PathSegments.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/PathSegments.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/RequestBodyAdapter.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/RequestBodyAdapter.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/RequestBodyAdapter.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/RequestBodyAdapter.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/RequestBodyException.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/RequestBodyException.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/RequestBodyException.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/RequestBodyException.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/RequestReferences.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/RequestReferences.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/RequestReferences.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/RequestReferences.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/StringRequestBodyAdapter.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/StringRequestBodyAdapter.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/StringRequestBodyAdapter.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/StringRequestBodyAdapter.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/UriAware.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/UriAware.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/UriAware.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/UriAware.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/annotation/Body.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/Body.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/annotation/Body.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/Body.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/annotation/ConsumesJson.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/ConsumesJson.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/annotation/ConsumesJson.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/ConsumesJson.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/annotation/ConsumesString.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/ConsumesString.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/annotation/ConsumesString.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/ConsumesString.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/annotation/Delete.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/Delete.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/annotation/Delete.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/Delete.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/annotation/Get.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/Get.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/annotation/Get.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/Get.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/annotation/Path.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/Path.java old mode 100755 new mode 100644 similarity index 95% rename from gemini/src/main/java/com/techempower/gemini/path/annotation/Path.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/Path.java index 06bb8896..87c85bf0 --- a/gemini/src/main/java/com/techempower/gemini/path/annotation/Path.java +++ b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/Path.java @@ -54,7 +54,9 @@ * the root URI of the handler. Example /api/users => UserHandler; {@code @Path} * will handle `GET /api/users`. */ -@Target(ElementType.METHOD) +// TODO: Roll back the addition of ElementType.TYPE. Only added for testing +// kain's stuff. +@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface Path { diff --git a/gemini/src/main/java/com/techempower/gemini/path/annotation/PathDefault.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/PathDefault.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/annotation/PathDefault.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/PathDefault.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/annotation/PathRoot.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/PathRoot.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/annotation/PathRoot.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/PathRoot.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/annotation/PathSegment.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/PathSegment.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/annotation/PathSegment.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/PathSegment.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/annotation/Post.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/Post.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/annotation/Post.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/Post.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/annotation/Put.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/Put.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/annotation/Put.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/Put.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/annotation/package-info.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/package-info.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/annotation/package-info.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/annotation/package-info.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/legacy/LegacyDispatcherHandler.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/legacy/LegacyDispatcherHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/legacy/LegacyDispatcherHandler.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/legacy/LegacyDispatcherHandler.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/legacy/package-info.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/legacy/package-info.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/legacy/package-info.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/legacy/package-info.java diff --git a/gemini/src/main/java/com/techempower/gemini/path/package-info.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/package-info.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/path/package-info.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/path/package-info.java diff --git a/gemini/src/main/java/com/techempower/gemini/prehandler/Prehandler.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/prehandler/Prehandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/prehandler/Prehandler.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/prehandler/Prehandler.java diff --git a/gemini/src/main/java/com/techempower/gemini/prehandler/StrictTransportSecurity.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/prehandler/StrictTransportSecurity.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/prehandler/StrictTransportSecurity.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/prehandler/StrictTransportSecurity.java diff --git a/gemini/src/main/java/com/techempower/gemini/prehandler/package-info.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/prehandler/package-info.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/prehandler/package-info.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/prehandler/package-info.java diff --git a/gemini/src/main/java/com/techempower/gemini/pyxis/annotation/PathBypassAuth.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/pyxis/annotation/PathBypassAuth.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/pyxis/annotation/PathBypassAuth.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/pyxis/annotation/PathBypassAuth.java diff --git a/gemini/src/main/java/com/techempower/gemini/pyxis/handler/EndMasqueradeHandler.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/pyxis/handler/EndMasqueradeHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/pyxis/handler/EndMasqueradeHandler.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/pyxis/handler/EndMasqueradeHandler.java diff --git a/gemini/src/main/java/com/techempower/gemini/pyxis/handler/LoginHandler.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/pyxis/handler/LoginHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/pyxis/handler/LoginHandler.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/pyxis/handler/LoginHandler.java diff --git a/gemini/src/main/java/com/techempower/gemini/pyxis/handler/LogoutHandler.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/pyxis/handler/LogoutHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/pyxis/handler/LogoutHandler.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/pyxis/handler/LogoutHandler.java diff --git a/gemini/src/main/java/com/techempower/gemini/pyxis/handler/PasswordResetHandler.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/pyxis/handler/PasswordResetHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/pyxis/handler/PasswordResetHandler.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/pyxis/handler/PasswordResetHandler.java diff --git a/gemini/src/main/java/com/techempower/gemini/pyxis/handler/PyxisHandlerHelper.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/pyxis/handler/PyxisHandlerHelper.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/pyxis/handler/PyxisHandlerHelper.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/pyxis/handler/PyxisHandlerHelper.java diff --git a/gemini/src/main/java/com/techempower/gemini/pyxis/handler/SecureMethodSegmentHandler.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/pyxis/handler/SecureMethodSegmentHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/pyxis/handler/SecureMethodSegmentHandler.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/pyxis/handler/SecureMethodSegmentHandler.java diff --git a/gemini/src/main/java/com/techempower/gemini/pyxis/handler/SecureMethodUriHandler.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/pyxis/handler/SecureMethodUriHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/pyxis/handler/SecureMethodUriHandler.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/pyxis/handler/SecureMethodUriHandler.java diff --git a/gemini/src/main/java/com/techempower/gemini/seo/RobotsHandler.java b/gemini-legacy-dispatching/src/main/java/com/techempower/gemini/seo/RobotsHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini/src/main/java/com/techempower/gemini/seo/RobotsHandler.java rename to gemini-legacy-dispatching/src/main/java/com/techempower/gemini/seo/RobotsHandler.java diff --git a/gemini-resin-archetype/src/main/resources/archetype-resources/pom.xml b/gemini-resin-archetype/src/main/resources/archetype-resources/pom.xml index 116b893c..945beec6 100755 --- a/gemini-resin-archetype/src/main/resources/archetype-resources/pom.xml +++ b/gemini-resin-archetype/src/main/resources/archetype-resources/pom.xml @@ -46,6 +46,16 @@ gemini-logback ${geminiVersion} + + com.techempower + gemini-legacy-dispatching + ${geminiVersion} + + + com.techempower + gemini-resin-legacy-dispatching + ${geminiVersion} + com.techempower gemini-jdbc diff --git a/gemini-resin-legacy-dispatching/pom.xml b/gemini-resin-legacy-dispatching/pom.xml new file mode 100644 index 00000000..6bb0bd30 --- /dev/null +++ b/gemini-resin-legacy-dispatching/pom.xml @@ -0,0 +1,73 @@ + + + 4.0.0 + jar + + + TechEmpower, Inc. + https://www.techempower.com/ + + + + + Revised BSD License, 3-clause + repo + + + + + gemini-parent + com.techempower + 3.1.0-SNAPSHOT + + + com.techempower + gemini-resin-legacy-dispatching + + An extension for Gemini that provides the old request dispatching functionality for Resin. + + + + + com.techempower + gemini + + + com.techempower + gemini-resin + + + com.techempower + gemini-legacy-dispatching + + + com.caucho + resin + + + javax.servlet + javax.servlet-api + + + javax + javaee-api + + + javax + javaee-web-api + + + junit + junit + test + + + org.mockito + mockito-all + test + + + + \ No newline at end of file diff --git a/gemini-resin/src/main/java/com/techempower/gemini/BasicDispatcher.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/BasicDispatcher.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/BasicDispatcher.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/BasicDispatcher.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/Handler.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/Handler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/Handler.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/Handler.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/LegacyContext.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/LegacyContext.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/LegacyContext.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/LegacyContext.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/AnnotationHandler.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/AnnotationHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/AnnotationHandler.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/AnnotationHandler.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/CMD.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/CMD.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/CMD.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/CMD.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/Default.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/Default.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/Default.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/Default.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/Role.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/Role.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/Role.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/Role.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/URL.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/URL.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/URL.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/URL.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/BooleanParam.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/BooleanParam.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/BooleanParam.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/BooleanParam.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/BooleanParamInjector.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/BooleanParamInjector.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/BooleanParamInjector.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/BooleanParamInjector.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/DoubleParam.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/DoubleParam.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/DoubleParam.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/DoubleParam.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/DoubleParamInjector.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/DoubleParamInjector.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/DoubleParamInjector.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/DoubleParamInjector.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/Entity.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/Entity.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/Entity.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/Entity.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/EntityInjector.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/EntityInjector.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/EntityInjector.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/EntityInjector.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/Injector.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/Injector.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/Injector.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/Injector.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/IntParam.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/IntParam.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/IntParam.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/IntParam.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/IntParamInjector.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/IntParamInjector.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/IntParamInjector.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/IntParamInjector.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/LongParam.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/LongParam.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/LongParam.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/LongParam.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/LongParamInjector.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/LongParamInjector.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/LongParamInjector.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/LongParamInjector.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/Param.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/Param.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/Param.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/Param.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/ParamInjector.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/ParamInjector.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/ParamInjector.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/ParamInjector.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/ParameterInjector.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/ParameterInjector.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/ParameterInjector.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/ParameterInjector.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/package-info.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/package-info.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/injector/package-info.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/injector/package-info.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/FeatureIntercept.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/FeatureIntercept.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/FeatureIntercept.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/FeatureIntercept.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/GetIntercept.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/GetIntercept.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/GetIntercept.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/GetIntercept.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/GroupIntercept.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/GroupIntercept.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/GroupIntercept.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/GroupIntercept.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/HandlerIntercept.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/HandlerIntercept.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/HandlerIntercept.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/HandlerIntercept.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/Intercept.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/Intercept.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/Intercept.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/Intercept.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/LoginIntercept.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/LoginIntercept.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/LoginIntercept.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/LoginIntercept.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/PostIntercept.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/PostIntercept.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/PostIntercept.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/PostIntercept.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/Require.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/Require.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/Require.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/Require.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/RequireFeature.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/RequireFeature.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/RequireFeature.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/RequireFeature.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/RequireGet.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/RequireGet.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/RequireGet.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/RequireGet.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/RequireGroup.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/RequireGroup.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/RequireGroup.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/RequireGroup.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/RequireLogin.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/RequireLogin.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/RequireLogin.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/RequireLogin.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/RequirePost.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/RequirePost.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/RequirePost.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/RequirePost.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/package-info.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/package-info.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/intercept/package-info.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/intercept/package-info.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/package-info.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/package-info.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/package-info.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/package-info.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/response/FileResponse.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/response/FileResponse.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/response/FileResponse.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/response/FileResponse.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/response/HandlerResponse.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/response/HandlerResponse.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/response/HandlerResponse.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/response/HandlerResponse.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/response/JSON.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/response/JSON.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/response/JSON.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/response/JSON.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/response/JSP.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/response/JSP.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/response/JSP.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/response/JSP.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/response/JsonResponse.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/response/JsonResponse.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/response/JsonResponse.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/response/JsonResponse.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/response/JspResponse.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/response/JspResponse.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/response/JspResponse.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/response/JspResponse.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/response/Response.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/response/Response.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/response/Response.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/response/Response.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/response/TossFile.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/response/TossFile.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/response/TossFile.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/response/TossFile.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/annotation/response/package-info.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/response/package-info.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/annotation/response/package-info.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/annotation/response/package-info.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/exceptionhandler/BasicExceptionHandler.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/exceptionhandler/BasicExceptionHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/exceptionhandler/BasicExceptionHandler.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/exceptionhandler/BasicExceptionHandler.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/filestore/BasicFileStoreHandler.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/filestore/BasicFileStoreHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/filestore/BasicFileStoreHandler.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/filestore/BasicFileStoreHandler.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/handler/BasicHandler.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/handler/BasicHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/handler/BasicHandler.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/handler/BasicHandler.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/handler/FileTossHandler.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/handler/FileTossHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/handler/FileTossHandler.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/handler/FileTossHandler.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/handler/SecureHandler.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/handler/SecureHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/handler/SecureHandler.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/handler/SecureHandler.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/jsp/BasicJsp.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/jsp/BasicJsp.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/jsp/BasicJsp.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/jsp/BasicJsp.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/jsp/InfrastructureJsp.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/jsp/InfrastructureJsp.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/jsp/InfrastructureJsp.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/jsp/InfrastructureJsp.java diff --git a/gemini-resin/src/main/java/com/techempower/gemini/pyxis/BasicSecureHandler.java b/gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/pyxis/BasicSecureHandler.java old mode 100755 new mode 100644 similarity index 100% rename from gemini-resin/src/main/java/com/techempower/gemini/pyxis/BasicSecureHandler.java rename to gemini-resin-legacy-dispatching/src/main/java/com/techempower/gemini/pyxis/BasicSecureHandler.java diff --git a/gemini-resin-legacy-dispatching/src/test/java/com/techempower/gemini/path/AnnotationDispatcher.java b/gemini-resin-legacy-dispatching/src/test/java/com/techempower/gemini/path/AnnotationDispatcher.java new file mode 100644 index 00000000..d1704ce6 --- /dev/null +++ b/gemini-resin-legacy-dispatching/src/test/java/com/techempower/gemini/path/AnnotationDispatcher.java @@ -0,0 +1,565 @@ +/******************************************************************************* + * Copyright (c) 2020, TechEmpower, Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name TechEmpower, Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL TECHEMPOWER, INC. BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *******************************************************************************/ +package com.techempower.gemini.path; + +import com.techempower.classloader.PackageClassLoader; +import com.techempower.gemini.*; +import com.techempower.gemini.configuration.ConfigurationError; +import com.techempower.gemini.exceptionhandler.ExceptionHandler; +import com.techempower.gemini.path.annotation.Path; +import com.techempower.gemini.prehandler.Prehandler; +import com.techempower.helper.NetworkHelper; +import com.techempower.helper.StringHelper; +import org.reflections.Reflections; +import org.reflections.ReflectionsException; + +import java.lang.reflect.InvocationTargetException; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import static com.techempower.gemini.Request.*; +import static com.techempower.gemini.Request.HEADER_ACCESS_CONTROL_EXPOSED_HEADERS; +import static com.techempower.gemini.Request.HttpMethod.*; + +public class AnnotationDispatcher implements Dispatcher { + + // + // Member variables. + // + + //private final GeminiApplication app; + private final Map handlers; + private final ExceptionHandler[] exceptionHandlers; + private final Prehandler[] prehandlers; + private final DispatchListener[] listeners; + + private ExecutorService preinitializationTasks = Executors.newSingleThreadExecutor(); + private Reflections reflections = null; + + public AnnotationDispatcher(/*GeminiApplication application*/) + { + //app = application; + handlers = new HashMap<>(); + exceptionHandlers = new ExceptionHandler[]{}; + prehandlers = new Prehandler[]{}; + listeners = new DispatchListener[]{}; + + /*if (exceptionHandlers.length == 0) + { + throw new IllegalArgumentException("PathDispatcher must be configured with at least one ExceptionHandler."); + }*/ + + //startReflectionsThread(); + } + + /*private void startReflectionsThread() + { + // Start constructing Reflections on a new thread since it takes a + // bit of time. + preinitializationTasks.submit(new Runnable() { + @Override + public void run() { + try + { + reflections = PackageClassLoader.getReflectionClassLoader(app); + } + catch (Exception exc) + { + // todo +// log.log("Exception while instantiating Reflections component.", exc); + } + } + }); + }*/ + + /*public void initialize() { + // Wait for pre-initialization tasks to complete. + try + { +// log.log("Completing preinitialization tasks."); + preinitializationTasks.shutdown(); +// log.log("Awaiting termination of preinitialization tasks."); + preinitializationTasks.awaitTermination(5L, TimeUnit.MINUTES); +// log.log("Preinitialization tasks complete."); +// log.log("Reflections component: " + reflections); + } + catch (InterruptedException iexc) + { +// log.log("Preinitialization interrupted.", iexc); + } + + // Throw an exception if Reflections is not ready. + if (reflections == null) + { + throw new ConfigurationError("Reflections not ready; application cannot start."); + } + + //register(); + }*/ + + /*private void register() { +// log.log("Registering annotated entities, relations, and type adapters."); + try { + final ExecutorService service = Executors.newFixedThreadPool(1); + + // @Path-annotated classes. + service.submit(new Runnable() { + @Override + public void run() { + for (Class clazz : reflections.getTypesAnnotatedWith(Path.class)) { + final Path annotation = clazz.getAnnotation(Path.class); + + try { + handlers.put(annotation.value(), + new AnnotationHandler(annotation.value(), + clazz.getDeclaredConstructor().newInstance())); + } + catch (NoSuchMethodException nsme) { + // todo + } + catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + // todo + } + } + } + }); + + try + { + service.shutdown(); + service.awaitTermination(1L, TimeUnit.HOURS); + } + catch (InterruptedException iexc) + { +// log.log("Unable to register all entities in 1 hour!", LogLevel.CRITICAL); + } + +// log.log("Done registering annotated items."); + } + catch (ReflectionsException e) + { + throw new RuntimeException("Warn: problem registering class with reflection", e); + } + }*/ + + public void register(Object resource) { + Class clazz = resource.getClass(); + Path annotation = clazz.getAnnotation(Path.class); + try + { + handlers.put(annotation.value(), + new AnnotationHandler(annotation.value(), + clazz.getDeclaredConstructor().newInstance())); + } + catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) + { + throw new RuntimeException(e); + } + } + + /** + * Notify the listeners that a dispatch is starting. + */ + protected void notifyListenersDispatchStarting(Context context, String command) + { + final DispatchListener[] theListeners = listeners; + for (DispatchListener listener : theListeners) + { + listener.dispatchStarting(this, context, command); + } + } + + /** + * Send the request to all prehandlers. + */ + protected boolean prehandle(C context) + { + final Prehandler[] thePrehandlers = prehandlers; + for (Prehandler p : thePrehandlers) + { + if (p.prehandle(context)) + { + return true; + } + } + + // Returning false indicates we did not fully handle this request and + // processing should continue to the handle method. + return false; + } + + @Override + public boolean dispatch(Context plainContext) { + throw new UnsupportedOperationException(); + } + + public boolean dispatch(/*Context plainContext*/Request.HttpMethod httpMethod, String uri) { + boolean success = false; + + // Surround all logic with a try-catch so that we can send the request to + // our ExceptionHandlers if anything goes wrong. + try + { + // Cast the provided Context to a C. + //@SuppressWarnings("unchecked") + //final C context = (C)plainContext; + + // Convert the request URI into path segments. + final PathSegments segments = new PathSegments(uri); + + // Any request with an Origin header will be handled by the app directly, + // however there are some headers we need to set up to add support for + // cross-origin requests. + /*if(context.headers().get(HEADER_ORIGIN) != null) + { + //addCorsHeaders(context); + + if(((Request)context.getRequest()).getRequestMethod() == OPTIONS) + { + //addPreflightCorsHeaders(segments, context); + // Returning true indicates we did fully handle this request and + // processing should not continue. + return true; + } + }*/ + + // Make these references available thread-locally. + //RequestReferences.set(context, segments); + + // Notify listeners. + //notifyListenersDispatchStarting(plainContext, segments.getUriFromRoot()); + + // Find the associated Handler. + AnnotationHandler handler = null; + + if (segments.getCount() > 0) + { + handler = this.handlers.get(segments.get(0)); + + // If we've found a Handler to use, we have consumed the first path + // segment. + if (handler != null) + { + segments.increaseOffset(); + } + } + /** + * todo: We no longer have the notion of a 'rootHandler'. + * This can be accomplished by having a POJO annotated with + * `@Path("/")` to denote the root uri and a single method + * annotated with `@Path()` to handle the root request. + */ + // Use the root handler when the segment count is 0. +// else if (rootHandler != null) +// { +// handler = rootHandler; +// } + + /** + * todo: We no longer have the notion of a 'defaultHandler'. + * This can be accomplished by having a POJO annotated with + * `@Path("*")` to denote the wildcard uri and a single + * method annotated with `@Path("*")` to handle any request + * routed there. + */ + // Use the default handler if nothing else was provided. +// if (handler == null) +// { +// // The HTTP method for the request is not listed in the HTTPMethod enum, +// // so we are unable to handle the request and simply return a 501. +// if (((Request)plainContext.getRequest()).getRequestMethod() == null) +// { +// handler = notImplementedHandler; +// } +// else +// { +// handler = defaultHandler; +// } +// } + + // TODO: I don't know how I want to handle `prehandle` yet. + success = false; // this means we didn't prehandle + // Send the request to all Prehandlers. +// success = prehandle(context); + + // Proceed to normal Handlers if the Prehandlers did not fully handle + // the request. + if (!success) + { + try + { + // Proceed to the handle method if the prehandle method did not fully + // handle the request on its own. + success = handler.handle(segments, httpMethod); + } + finally + { + // todo: I'm not sure how to do `posthandle` yet. + // Do wrap-up processing even if the request was not handled correctly. +// handler.posthandle(segments, context); + } + } + + /** + * TODO: again, we don't have a `defaultHandler` anymore except by + * routing to a POJO annotated with `@Path("*")` and a method + * annotated with `@Path("*")`. + */ + // If the handler we selected did not successfully handle the request + // and it's NOT the default handler, let's ask the default handler to + // handle the request. +// if ( (!success) +// && (handler != defaultHandler) +// ) +// { +// try +// { +// // Result of prehandler is ignored because the default handler is +// // expected to handle any request. For the default handler, we'll +// // reset the PathSegments offset to 0. +// success = defaultHandler.prehandle(segments.offset(0), context); +// +// if (!success) +// { +// defaultHandler.handle(segments, context); +// } +// } +// finally +// { +// defaultHandler.posthandle(segments, context); +// } +// } + } + catch (Throwable exc) + { + throw new RuntimeException(exc); + //dispatchException(plainContext, exc, null); + } + finally + { + //RequestReferences.remove(); + } + + return success; + } + + /** + * Notify the listeners that a dispatch is complete. + */ + protected void notifyListenersDispatchComplete(Context context) + { + final DispatchListener[] theListeners = listeners; + for (DispatchListener listener : theListeners) + { + listener.dispatchComplete(this, context); + } + } + + @Override + public void dispatchComplete(Context context) { + notifyListenersDispatchComplete(context); + } + + @Override + public void renderStarting(Context context, String renderingName) { + // Intentionally left blank + } + + @Override + public void renderComplete(Context context) { + // Intentionally left blank + } + + @Override + public void dispatchException(Context context, Throwable exception, String description) { + if (exception == null) + { +// log.log("dispatchException called with a null reference.", +// LogLevel.ALERT); + return; + } + + try + { + final ExceptionHandler[] theHandlers = exceptionHandlers; + for (ExceptionHandler handler : theHandlers) + { + if (description != null) + { + handler.handleException(context, exception, description); + } + else + { + handler.handleException(context, exception); + } + } + } + catch (Exception exc) + { + // In the especially worrisome case that we've encountered an exception + // while attempting to handle another exception, we'll give up on the + // request at this point and just write the exception to the log. +// log.log("Exception encountered while processing earlier " + exception, +// LogLevel.ALERT, exc); + } + } + + /** + * Gets the Header-appropriate string representation of the http method + * names that this handler supports for the given path segments. + *

+ * For example, if this handler has two handle methods at "/" and + * one is GET and the other is POST, this method would return the string + * "GET, POST" for the PathSegments "/". + *

+ * By default, this method returns "GET, POST", but subclasses should + * override for more accurate return values. + */ + protected String getAccessControlAllowMethods(PathSegments segments, + C context) + { + // todo: map of routes-to-handler-tuples that expresses something like + // /foo/bar -> { class, method, HttpMethod } + // for lookup here. + // todo: this is also probably wrong in BasicPathHandler + return HttpMethod.GET + ", " + HttpMethod.POST; + } + + + /** + * Adds the standard headers required for CORS support in all requests + * regardless of being preflight. + * @see + * Access-Control-Allow-Origin + * @see + * Access-Control-Allow-Credentials + */ + /*private void addCorsHeaders(C context) + { + // Applications may configure whitelisted origins to which cross-origin + // requests are allowed. + if(NetworkHelper.isWebUrl(context.headers().get(HEADER_ORIGIN)) && + app.getSecurity().getSettings().getAccessControlAllowedOrigins() + .contains(context.headers().get(HEADER_ORIGIN).toLowerCase())) + { + // If the server specifies an origin host rather than wildcard, then it + // must also include Origin in the Vary response header. + context.headers().put(HEADER_VARY, HEADER_ORIGIN); + context.headers().put(HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, + context.headers().get(HEADER_ORIGIN)); + // Applications may configure the ability to allow credentials on CORS + // requests, but only for domain-specified requests. Wildcards cannot + // allow credentials. + if(app.getSecurity().getSettings().accessControlAllowCredentials()) + { + context.headers().put( + HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); + } + } + // Applications may also configure wildcard origins to be whitelisted for + // cross-origin requests, effectively making the application an open API. + else if(app.getSecurity().getSettings().getAccessControlAllowedOrigins() + .contains(HEADER_WILDCARD)) + { + context.headers().put(HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, + HEADER_WILDCARD); + } + // Applications may configure whitelisted headers which browsers may + // access on cross origin requests. + if(!app.getSecurity().getSettings().getAccessControlExposedHeaders().isEmpty()) + { + boolean first = true; + final StringBuilder exposed = new StringBuilder(); + for(final String header : app.getSecurity().getSettings() + .getAccessControlExposedHeaders()) + { + if(!first) + { + exposed.append(", "); + } + exposed.append(header); + first = false; + } + context.headers().put(HEADER_ACCESS_CONTROL_EXPOSED_HEADERS, + exposed.toString()); + } + }*/ + + /** + * Adds the headers required for CORS support for preflight OPTIONS requests. + * @see + * Preflighted requests + */ + /*private void addPreflightCorsHeaders(PathSegments segments, C context) + { + // Applications may configure whitelisted headers which may be sent to + // the application on cross origin requests. + if (StringHelper.isNonEmpty(context.headers().get( + HEADER_ACCESS_CONTROL_REQUEST_HEADERS))) + { + final String[] headers = StringHelper.splitAndTrim( + context.headers().get( + HEADER_ACCESS_CONTROL_REQUEST_HEADERS), ","); + boolean first = true; + final StringBuilder allowed = new StringBuilder(); + for(final String header : headers) + { + if(app.getSecurity().getSettings() + .getAccessControlAllowedHeaders().contains(header.toLowerCase())) + { + if(!first) + { + allowed.append(", "); + } + allowed.append(header); + first = false; + } + } + + context.headers().put(HEADER_ACCESS_CONTROL_ALLOW_HEADERS, + allowed.toString()); + } + + final String methods = getAccessControlAllowMethods(segments, context); + if(StringHelper.isNonEmpty(methods)) + { + context.headers().put(HEADER_ACCESS_CONTROL_ALLOW_METHOD, methods); + } + + if(((Request)context.getRequest()).getRequestMethod() == HttpMethod.OPTIONS) + { + context.headers().put(HEADER_ACCESS_CONTROL_MAX_AGE, + app.getSecurity().getSettings().getAccessControlMaxAge() + ""); + } + }*/ +} \ No newline at end of file diff --git a/gemini-resin-legacy-dispatching/src/test/java/com/techempower/gemini/path/AnnotationDispatcherTest.java b/gemini-resin-legacy-dispatching/src/test/java/com/techempower/gemini/path/AnnotationDispatcherTest.java new file mode 100644 index 00000000..1a9d4b3c --- /dev/null +++ b/gemini-resin-legacy-dispatching/src/test/java/com/techempower/gemini/path/AnnotationDispatcherTest.java @@ -0,0 +1,129 @@ +package com.techempower.gemini.path; + +import com.techempower.gemini.Context; +import com.techempower.gemini.Request; +import com.techempower.gemini.path.annotation.Get; +import com.techempower.gemini.path.annotation.Path; +import org.junit.Test; + +import java.util.Map; +import java.util.UUID; +import java.util.regex.Matcher; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class AnnotationDispatcherTest +{ + @Path("foo") + public static class FooResource { + @Get + @Path("bar") + public Map bar() { + return Map.of(); + } + } + + @Path("foo") + public static class FooResourceVar { + @Get + @Path("{bar}") + public Map bar(String bar) { + return Map.of(); + } + } + + //static final int ITERATIONS = 2_700_000; + static final int ITERATIONS = 2_700_000; + + /*@Test + public void blah() { + AnnotationDispatcher dispatcher = new AnnotationDispatcher<>(); + dispatcher.register(new FooResource()); + long start; + Context context = mock(Context.class); + Request request = mock(Request.class); + when(context.getRequestUri()).thenReturn("foo/bar"); + when(context.getRequest()).thenReturn(request); + when(request.getRequestMethod()).thenReturn(Request.HttpMethod.GET); + start = System.currentTimeMillis(); + for (int i = 0; i < ITERATIONS; i++) + { + dispatcher.dispatch(context); + } + System.out.println("Total time kain-approach: " + (System.currentTimeMillis() - start) + "ms"); + }*/ + + public static void main(String...args) { + AnnotationDispatcher dispatcher = new AnnotationDispatcher<>(); + dispatcher.register(new FooResource()); + AnnotationDispatcherTest test = new AnnotationDispatcherTest(); + test.warmUpBlah(dispatcher); + long start; + start = System.currentTimeMillis(); + test.doBlah(dispatcher); + System.out.println("Total time kain-approach: " + + (System.currentTimeMillis() - start) + "ms"); + } + + // I know these are identical, but it's easier to distinguish the warm up + // from the "real" in the profiler this way. + public void warmUpBlah(AnnotationDispatcher dispatcher) { + String uri = "foo/bar"; + for (int i = 0; i < ITERATIONS; i++) + { + dispatcher.dispatch(Request.HttpMethod.GET, uri); + } + } + + public void doBlah(AnnotationDispatcher dispatcher) { + String uri = "foo/bar"; + for (int i = 0; i < ITERATIONS; i++) + { + dispatcher.dispatch(Request.HttpMethod.GET, uri); + } + } + + @Test + public void blah() { + AnnotationDispatcher dispatcher = new AnnotationDispatcher<>(); + dispatcher.register(new FooResource()); + String uri = "foo/bar"; + for (int i = 0; i < ITERATIONS; i++) + { + dispatcher.dispatch(Request.HttpMethod.GET, uri); + } + long start; + /*Context context = mock(Context.class); + Request request = mock(Request.class);*/ + start = System.currentTimeMillis(); + for (int i = 0; i < ITERATIONS; i++) + { + dispatcher.dispatch(Request.HttpMethod.GET, uri); + } + System.out.println("Total time kain-approach: " + + (System.currentTimeMillis() - start) + "ms"); + } + + @Test + public void blahVariable() { + AnnotationDispatcher dispatcher = new AnnotationDispatcher<>(); + dispatcher.register(new FooResourceVar()); + var uuidStr = UUID.randomUUID().toString(); + var uri = "foo/" + uuidStr; + long start; + /*Context context = mock(Context.class); + Request request = mock(Request.class);*/ + for (int i = 0; i < ITERATIONS; i++) + { + dispatcher.dispatch(Request.HttpMethod.GET, uri); + } + start = System.currentTimeMillis(); + for (int i = 0; i < ITERATIONS; i++) + { + dispatcher.dispatch(Request.HttpMethod.GET, uri); + } + System.out.println("Total time kain-approach: " + + (System.currentTimeMillis() - start) + "ms"); + } +} diff --git a/gemini-resin-legacy-dispatching/src/test/java/com/techempower/gemini/path/AnnotationHandler.java b/gemini-resin-legacy-dispatching/src/test/java/com/techempower/gemini/path/AnnotationHandler.java new file mode 100644 index 00000000..f99eeb48 --- /dev/null +++ b/gemini-resin-legacy-dispatching/src/test/java/com/techempower/gemini/path/AnnotationHandler.java @@ -0,0 +1,844 @@ +package com.techempower.gemini.path; + + +import com.esotericsoftware.reflectasm.MethodAccess; +import com.techempower.gemini.Context; +import com.techempower.gemini.Request; +import com.techempower.gemini.path.annotation.*; +import com.techempower.helper.NumberHelper; +import com.techempower.helper.ReflectionHelper; +import com.techempower.helper.StringHelper; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.List; + +import static com.techempower.gemini.Request.HEADER_ACCESS_CONTROL_REQUEST_METHOD; +import static com.techempower.gemini.Request.HttpMethod.*; + +/** + * Similar to MethodUriHandler, AnnotationHandler class does the same + * strategy of creating `PathUriTree`s for each Request.Method type + * and then inserting handler methods into the trees. + * @param + */ +class AnnotationHandler { + final String rootUri; + final Object handler; + + private final AnnotationHandler.PathUriTree getRequestHandleMethods; + private final AnnotationHandler.PathUriTree putRequestHandleMethods; + private final AnnotationHandler.PathUriTree postRequestHandleMethods; + private final AnnotationHandler.PathUriTree deleteRequestHandleMethods; + protected final MethodAccess methodAccess; + + public AnnotationHandler(String rootUri, Object handler) { + this.rootUri = rootUri; + this.handler = handler; + + getRequestHandleMethods = new AnnotationHandler.PathUriTree(); + putRequestHandleMethods = new AnnotationHandler.PathUriTree(); + postRequestHandleMethods = new AnnotationHandler.PathUriTree(); + deleteRequestHandleMethods = new AnnotationHandler.PathUriTree(); + + methodAccess = MethodAccess.get(handler.getClass()); + discoverAnnotatedMethods(); + } + + /** + * Adds the given PathUriMethod to the appropriate list given + * the request method type. + */ + private void addAnnotatedHandleMethod(AnnotationHandler.PathUriMethod method) + { + switch (method.httpMethod) + { + case PUT: + putRequestHandleMethods.addMethod(method); + break; + case POST: + postRequestHandleMethods.addMethod(method); + break; + case DELETE: + deleteRequestHandleMethods.addMethod(method); + break; + case GET: + getRequestHandleMethods.addMethod(method); + break; + default: + break; + } + } + + /** + * Analyze an annotated method and return its index if it's suitable for + * accepting requests. + * + * @param method The annotated handler method. + * @param httpMethod The http method name (e.g. "GET"). Null + * implies that all http methods are supported. + * @return The PathSegmentMethod for the given handler method. + */ + protected AnnotationHandler.PathUriMethod analyzeAnnotatedMethod(Path path, Method method, + Request.HttpMethod httpMethod) + { + // Only allow accessible (public) methods + if (Modifier.isPublic(method.getModifiers())) + { + return new AnnotationHandler.PathUriMethod( + method, + path.value(), + httpMethod, + methodAccess); + } + else + { + throw new IllegalAccessError("Methods annotated with @Path must be " + + "public. See" + getClass().getName() + "#" + method.getName()); + } + } + + /** + * Discovers annotated methods at instantiation time. + */ + private void discoverAnnotatedMethods() + { + final Method[] methods = handler.getClass().getMethods(); + + for (Method method : methods) + { + // Set up references to methods annotated as Paths. + final Path path = method.getAnnotation(Path.class); + if (path != null) + { + final Get get = method.getAnnotation(Get.class); + final Put put = method.getAnnotation(Put.class); + final Post post = method.getAnnotation(Post.class); + final Delete delete = method.getAnnotation(Delete.class); + // Enforce that only one http method type is on this segment. + if ((get != null ? 1 : 0) + (put != null ? 1 : 0) + + (post != null ? 1 : 0) + (delete != null ? 1 : 0) > 1) + { + throw new IllegalArgumentException( + "Only one request method type is allowed per @PathSegment. See " + + getClass().getName() + "#" + method.getName()); + } + final AnnotationHandler.PathUriMethod psm; + // Those the @Get annotation is implied in the absence of other + // method type annotations, this is left here to directly analyze + // the annotated method in case the @Get annotation is updated in + // the future to have differences between no annotations. + if (get != null) + { + psm = analyzeAnnotatedMethod(path, method, GET); + } + else if (put != null) + { + psm = analyzeAnnotatedMethod(path, method, PUT); + } + else if (post != null) + { + psm = analyzeAnnotatedMethod(path, method, POST); + } + else if (delete != null) + { + psm = analyzeAnnotatedMethod(path, method, DELETE); + } + else + { + // If no http request method type annotations are present along + // side the @PathSegment, then it is an implied GET. + psm = analyzeAnnotatedMethod(path, method, GET); + } + + addAnnotatedHandleMethod(psm); + } + } + } + + /** + * Determine the annotated method that should process the request. + */ + protected AnnotationHandler.PathUriMethod getAnnotatedMethod(PathSegments segments, + C context) + { + final AnnotationHandler.PathUriTree tree; + switch (((Request)context.getRequest()).getRequestMethod()) + { + case PUT: + tree = putRequestHandleMethods; + break; + case POST: + tree = postRequestHandleMethods; + break; + case DELETE: + tree = deleteRequestHandleMethods; + break; + case GET: + tree = getRequestHandleMethods; + break; + default: + // We do not want to handle this + return null; + } + + return tree.search(segments); + } + + /** + * Determine the annotated method that should process the request. + */ + protected AnnotationHandler.PathUriMethod getAnnotatedMethod(PathSegments segments, + Request.HttpMethod httpMethod) + { + final AnnotationHandler.PathUriTree tree; + switch (httpMethod) + { + case PUT: + tree = putRequestHandleMethods; + break; + case POST: + tree = postRequestHandleMethods; + break; + case DELETE: + tree = deleteRequestHandleMethods; + break; + case GET: + tree = getRequestHandleMethods; + break; + default: + // We do not want to handle this + return null; + } + + return tree.search(segments); + } + + /** + * Locates the annotated method to call, invokes it given the path segments + * and context. + * @param segments The URI segments to route + * @param context The current context + * @return + */ + public boolean handle(PathSegments segments, C context) { + getAnnotatedMethod(segments, context); + if (true) return true; + return dispatchToAnnotatedMethod(segments, getAnnotatedMethod(segments, context), + context); + } + + public boolean handle(PathSegments segments, Request.HttpMethod httpMethod) { + getAnnotatedMethod(segments, httpMethod); + /*if (true)*/ return true; + /*return dispatchToAnnotatedMethod(segments, getAnnotatedMethod(segments, context), + context);*/ + } + + protected String getAccessControlAllowMethods(PathSegments segments, C context) + { + final StringBuilder reqMethods = new StringBuilder(); + final List methods = new ArrayList<>(); + + if(context.headers().get(HEADER_ACCESS_CONTROL_REQUEST_METHOD) != null) + { + final AnnotationHandler.PathUriMethod put = this.putRequestHandleMethods.search(segments); + if (put != null) + { + methods.add(put); + } + final AnnotationHandler.PathUriMethod post = this.postRequestHandleMethods.search(segments); + if (post != null) + { + methods.add(this.postRequestHandleMethods.search(segments)); + } + final AnnotationHandler.PathUriMethod delete = this.deleteRequestHandleMethods.search(segments); + if (delete != null) + { + methods.add(this.deleteRequestHandleMethods.search(segments)); + } + final AnnotationHandler.PathUriMethod get = this.getRequestHandleMethods.search(segments); + if (get != null) + { + methods.add(this.getRequestHandleMethods.search(segments)); + } + + boolean first = true; + for(AnnotationHandler.PathUriMethod method : methods) + { + if(!first) + { + reqMethods.append(", "); + } + else + { + first = false; + } + reqMethods.append(method.httpMethod); + } + } + + return reqMethods.toString(); + } + + /** + * Dispatch the request to the appropriately annotated methods in subclasses. + */ + protected boolean dispatchToAnnotatedMethod(PathSegments segments, + AnnotationHandler.PathUriMethod method, + C context) + { + // If we didn't find an associated method and have no default, we'll + // return false, handing the request back to the default handler. + if (method != null && method.index >= 0) + { + // TODO: I think defaultTemplate is going away; maybe put a check + // here that the method can be serialized in the annotated way. + // Set the default template to the method's name. Handler methods can + // override this default by calling template(name) themselves before + // rendering a response. +// defaultTemplate(method.method.getName()); + + if (method.method.getParameterTypes().length == 0) + { + return (Boolean)methodAccess.invoke(this, method.index, + ReflectionHelper.NO_VALUES); + } + else + { + // We have already enforced that the @Path annotations have the correct + // number of args in their declarations to match the variable count + // in the respective URI. So, create an array of values and try to set + // them via retrieving them as segments. + try + { + return (Boolean)methodAccess.invoke(this, method.index, + getVariableArguments(segments, method, context)); + } + catch (RequestBodyException e) + { + // todo +// log().log("Got RequestBodyException.", LogLevel.DEBUG, e); +// return this.error(e.getStatusCode(), e.getMessage()); + } + } + } + + return false; + } + + /** + * Private helper method for capturing the values of the variable annotated + * methods and returning them as an argument array (in order or appearance). + *

+ * Example: @Path("foo/{var1}/{var2}") + * public boolean handleFoo(int var1, String var2) + * + * The array returned for `GET /foo/123/asd` would be: [123, "asd"] + * @param method the annotated method. + * @return Array of corresponding values. + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + private Object[] getVariableArguments(PathSegments segments, + AnnotationHandler.PathUriMethod method, + C context) + throws RequestBodyException + { + final Object[] args = new Object[method.method.getParameterTypes().length]; + int argsIndex = 0; + for (int i = 0; i < method.segments.length; i++) + { + if (method.segments[i].isVariable) + { + if (argsIndex >= args.length) + { + // No reason to continue - we found all are variables. + break; + } + // Try to read it from the context. + if(method.segments[i].type.isPrimitive()) + { + // int + if (method.segments[i].type.isAssignableFrom(int.class)) + { + args[argsIndex] = segments.getInt(i); + } + // long + else if (method.segments[i].type.isAssignableFrom(long.class)) + { + args[argsIndex] = NumberHelper.parseLong(segments.get(i)); + } + // boolean + else if (method.segments[i].type.isAssignableFrom(boolean.class)) + { + // bool variables are NOT simply whether they are present. + // Rather, it should be a truthy value. + args[argsIndex] = StringHelper.equalsIgnoreCase( + segments.get(i), + new String[]{ + "true", "yes", "1" + }); + } + // float + else if (method.segments[i].type.isAssignableFrom(float.class)) + { + args[argsIndex] = NumberHelper.parseFloat(segments.get(i), 0f); + } + // double + else if (method.segments[i].type.isAssignableFrom(double.class)) + { + args[argsIndex] = NumberHelper.parseDouble(segments.get(i), 0f); + } + // default + else + { + // We MUST have something here, set the default to zero. + // This is undefined behavior. If the method calls for a + // char/byte/etc and we pass 0, it is probably unexpected. + args[argsIndex] = 0; + } + } + // String, and technically Object too. + else if (method.segments[i].type.isAssignableFrom(String.class)) + { + args[argsIndex] = segments.get(i); + } + else + { + int indexOfMethodToInvoke; + Class type = method.segments[i].type; + MethodAccess methodAccess = method.segments[i].methodAccess; + if (hasStringInputMethod(type, methodAccess, "fromString")) + { + indexOfMethodToInvoke = methodAccess + .getIndex("fromString", String.class); + } + else if (hasStringInputMethod(type, methodAccess, "valueOf")) + { + indexOfMethodToInvoke = methodAccess + .getIndex("valueOf", String.class); + } + else + { + indexOfMethodToInvoke = -1; + } + if (indexOfMethodToInvoke >= 0) + { + try + { + args[argsIndex] = methodAccess.invoke(null, + indexOfMethodToInvoke, segments.get(i)); + } + catch (IllegalArgumentException iae) + { + // In the case where the developer has specified that only + // enumerated values should be accepted as input, either + // one of those values needs to exist in the URI, or this + // IllegalArgumentException will be thrown. We will limp + // on and pass a null in this case. + args[argsIndex] = null; + } + } + else + { + // We don't know the type, so we cannot create it. + args[argsIndex] = null; + } + } + // Bump argsIndex + argsIndex ++; + } + } + + // Handle adapting and injecting the request body if configured. + final BasicPathHandler.RequestBodyParameter bodyParameter = method.bodyParameter; + if (bodyParameter != null && argsIndex < args.length) + { + args[argsIndex] = bodyParameter.readBody(context); + } + + return args; + } + + private static boolean hasStringInputMethod(Class type, + MethodAccess methodAccess, + String methodName) { + String[] methodNames = methodAccess.getMethodNames(); + Class[][] parameterTypes = methodAccess.getParameterTypes(); + for (int index = 0; index < methodNames.length; index++) + { + String foundMethodName = methodNames[index]; + Class[] params = parameterTypes[index]; + if (foundMethodName.equals(methodName) + && params.length == 1 + && params[0].equals(String.class)) + { + try + { + // Only bother with the slowness of normal reflection if + // the method passes all the other checks. + Method method = type.getMethod(methodName, String.class); + if (Modifier.isStatic(method.getModifiers())) + { + return true; + } + } + catch (NoSuchMethodException e) + { + // Should not happen + } + } + } + return false; + } + + + protected static class PathUriTree + { + private final AnnotationHandler.PathUriTree.Node root; + + public PathUriTree() + { + root = new AnnotationHandler.PathUriTree.Node(null); + } + + /** + * Searches the tree for a node that best handles the given segments. + */ + public final AnnotationHandler.PathUriMethod search(PathSegments segments) + { + return search(root, segments, 0); + } + + /** + * Searches the given segments at the given offset with the given node + * in the tree. If this node is a leaf node and matches the segment + * stack perfectly, it is returned. If this node is a leaf node and + * either a variable or a wildcard node and the segment stack has run + * out of segments to check, return that if we have not found a true + * match. + */ + private AnnotationHandler.PathUriMethod search(AnnotationHandler.PathUriTree.Node node, PathSegments segments, int offset) + { + if (node != root && + offset >= segments.getCount()) + { + // Last possible depth; must be a leaf node + if (node.method != null) + { + return node.method; + } + return null; + } + else + { + // Not yet at a leaf node + AnnotationHandler.PathUriMethod bestVariable = null; // Best at this depth + AnnotationHandler.PathUriMethod bestWildcard = null; // Best at this depth + AnnotationHandler.PathUriMethod toReturn = null; + for (AnnotationHandler.PathUriTree.Node child : node.children) + { + // Only walk the path that can handle the new segment. + if (child.segment.segment.equals(segments.get(offset,""))) + { + // Direct hits only happen here. + toReturn = search(child, segments, offset + 1); + } + else if (child.segment.isVariable) + { + // Variables are not necessarily leaf nodes. + AnnotationHandler.PathUriMethod temp = search(child, segments, offset + 1); + // We may be at a variable node, but not the variable + // path segment handler method. Don't set it in this case. + if (temp != null) + { + bestVariable = temp; + } + } + else if (child.segment.isWildcard) + { + // Wildcards are leaf nodes by design. + bestWildcard = child.method; + } + } + // By here, we are as deep as we can be. + if (toReturn == null && bestVariable != null) + { + // Could not find a direct route + toReturn = bestVariable; + } + else if (toReturn == null && bestWildcard != null) + { + toReturn = bestWildcard; + } + return toReturn; + } + } + + /** + * Adds the given PathUriMethod to this tree at the + * appropriate depth. + */ + public final void addMethod(AnnotationHandler.PathUriMethod method) + { + root.addChild(root, method, 0); + } + + /** + * A node in the tree of PathUriMethod. + */ + public static class Node + { + private AnnotationHandler.PathUriMethod method; + private final AnnotationHandler.PathUriMethod.UriSegment segment; + private final List children; + + public Node(AnnotationHandler.PathUriMethod.UriSegment segment) + { + this.segment = segment; + this.children = new ArrayList<>(); + } + + @Override + public String toString() + { + final StringBuilder sb = new StringBuilder() + .append("{") + .append("method: ") + .append(method) + .append(", segment: ") + .append(segment) + .append(", childrenCount: ") + .append(this.children.size()) + .append("}"); + + return sb.toString(); + } + + /** + * Returns the immediate child node for the given segment and creates + * if it does not exist. + */ + private AnnotationHandler.PathUriTree.Node getChildForSegment(AnnotationHandler.PathUriTree.Node node, AnnotationHandler.PathUriMethod.UriSegment[] segments, int offset) + { + AnnotationHandler.PathUriTree.Node toRet = null; + for(AnnotationHandler.PathUriTree.Node child : node.children) + { + if (child.segment.segment.equals(segments[offset].segment)) + { + toRet = child; + break; + } + } + if (toRet == null) + { + // Add a new node at this segment to return. + toRet = new AnnotationHandler.PathUriTree.Node(segments[offset]); + node.children.add(toRet); + } + return toRet; + } + + /** + * Recursively adds the given PathUriMethod to this tree at the + * appropriate depth. + */ + private void addChild(AnnotationHandler.PathUriTree.Node node, AnnotationHandler.PathUriMethod uriMethod, int offset) + { + if (uriMethod.segments.length > offset) + { + final AnnotationHandler.PathUriTree.Node child = getChildForSegment(node, uriMethod.segments, offset); + if (uriMethod.segments.length == offset + 1) + { + child.method = uriMethod; + } + else + { + this.addChild(child, uriMethod, offset + 1); + } + } + } + + /** + * Returns the PathUriMethod for this node. + * May be null. + */ + public final AnnotationHandler.PathUriMethod getMethod() + { + return this.method; + } + } + } + + /** + * Details of an annotated path segment method. + */ + protected static class PathUriMethod extends BasicPathHandler.BasicPathHandlerMethod + { + public final Method method; + public final String uri; + public final AnnotationHandler.PathUriMethod.UriSegment[] segments; + public final int index; + + public PathUriMethod(Method method, String uri, Request.HttpMethod httpMethod, + MethodAccess methodAccess) + { + super(method, httpMethod); + + this.method = method; + this.uri = uri; + this.segments = this.parseSegments(this.uri); + int variableCount = 0; + final Class[] classes = + new Class[method.getGenericParameterTypes().length]; + for (AnnotationHandler.PathUriMethod.UriSegment segment : segments) + { + if (segment.isVariable) + { + classes[variableCount] = + (Class)method.getGenericParameterTypes()[variableCount]; + segment.type = classes[variableCount]; + if (!segment.type.isPrimitive()) + { + segment.methodAccess = MethodAccess.get(segment.type); + } + // Bump variableCount + variableCount ++; + } + } + + // Check for and configure the method to receive a parameter for the + // request body. If desired, it's expected that the body parameter is + // the last one. So it's only worth checking if variableCount indicates + // that there's room left in the classes array. If there is a mismatch + // where there is another parameter and no @Body annotation, or there is + // a @Body annotation and no extra parameter for it, the below checks + // will find that and throw accordingly. + if (variableCount < classes.length && this.bodyParameter != null) + { + classes[variableCount] = method.getParameterTypes()[variableCount]; + variableCount++; + } + + if (variableCount == 0) + { + try + { + this.index = methodAccess.getIndex(method.getName(), + ReflectionHelper.NO_PARAMETERS); + } + catch(IllegalArgumentException e) + { + throw new IllegalArgumentException("Methods with argument " + + "variables must have @Path annotations with matching " + + "variable capture(s) (ex: @Path(\"{var}\"). See " + + getClass().getName() + "#" + method.getName()); + } + } + else + { + if (classes.length == variableCount) + { + this.index = methodAccess.getIndex(method.getName(), classes); + } + else + { + throw new IllegalAccessError("@Path annotations with variable " + + "notations must have method parameters to match. See " + + getClass().getName() + "#" + method.getName()); + } + } + } + + private AnnotationHandler.PathUriMethod.UriSegment[] parseSegments(String uriToParse) + { + String[] segmentStrings = uriToParse.split("/"); + final AnnotationHandler.PathUriMethod.UriSegment[] uriSegments = new AnnotationHandler.PathUriMethod.UriSegment[segmentStrings.length]; + + for (int i = 0; i < segmentStrings.length; i++) + { + uriSegments[i] = new AnnotationHandler.PathUriMethod.UriSegment(segmentStrings[i]); + } + + return uriSegments; + } + + @Override + public String toString() + { + final StringBuilder sb = new StringBuilder(); + boolean empty = true; + for (AnnotationHandler.PathUriMethod.UriSegment segment : segments) + { + if (!empty) + { + sb.append(","); + } + sb.append(segment.toString()); + empty = false; + } + + return "PSM [" + method.getName() + "; " + httpMethod + "; " + + index + "; " + sb.toString() + "]"; + } + + protected static class UriSegment + { + public static final String WILDCARD = "*"; + public static final String VARIABLE_PREFIX = "{"; + public static final String VARIABLE_SUFFIX = "}"; + public static final String EMPTY = ""; + + public final boolean isWildcard; + public final boolean isVariable; + public final String segment; + public Class type; + public MethodAccess methodAccess; + + public UriSegment(String segment) + { + this.isWildcard = segment.equals(WILDCARD); + this.isVariable = segment.startsWith(VARIABLE_PREFIX) + && segment.endsWith(VARIABLE_SUFFIX); + if (this.isVariable) + { + // Minor optimization - no reason to potentially create multiple + // nodes all of which are variables since the inside of the variable + // is ignored in the end. Treating the segment of all variable nodes + // as "{}" regardless of whether the actual segment is "{var}" or + // "{foo}" forces all branches with variables at a given depth to + // traverse the same sub-tree. That is, "{var}/foo" and "{var}/bar" + // as the only two annotated methods in a handler will result in a + // maximum of 3 comparisons instead of 4. Mode variables at same + // depths would make this optimization felt more strongly. + this.segment = VARIABLE_PREFIX + VARIABLE_SUFFIX; + } + else + { + this.segment = segment; + } + } + + public final String getVariableName() + { + if (this.isVariable) + { + return this.segment + .replace(AnnotationHandler.PathUriMethod.UriSegment.VARIABLE_PREFIX, AnnotationHandler.PathUriMethod.UriSegment.EMPTY) + .replace(AnnotationHandler.PathUriMethod.UriSegment.VARIABLE_SUFFIX, AnnotationHandler.PathUriMethod.UriSegment.EMPTY); + } + + return null; + } + + @Override + public String toString() + { + return "{segment: '" + segment + + "', isVariable: " + isVariable + + ", isWildcard: " + isWildcard + "}"; + } + } + } +} \ No newline at end of file diff --git a/gemini-resin/src/main/java/com/techempower/gemini/ResinGeminiApplication.java b/gemini-resin/src/main/java/com/techempower/gemini/ResinGeminiApplication.java index 4ff38c7b..f67fe2cd 100755 --- a/gemini-resin/src/main/java/com/techempower/gemini/ResinGeminiApplication.java +++ b/gemini-resin/src/main/java/com/techempower/gemini/ResinGeminiApplication.java @@ -95,17 +95,17 @@ public abstract class ResinGeminiApplication * return toReturn; * */ + // TODO: It'd be nice if this was refactored so that you create the + // dispatcher during the initialize method, rather than in the constructor. @Override - protected Dispatcher constructDispatcher() - { - return new BasicDispatcher(this); - } + protected abstract Dispatcher constructDispatcher(); /** * Overload: Constructs an HttpSessionManager reference. Overload to return a * custom object. It is not likely that a application would need to subclass * HttpSessionManager. */ + // TODO?: Need to refactor this so that it's just part of the (legacy?) dispatcher? @Override protected SessionManager constructSessionManager() { @@ -127,12 +127,14 @@ protected GeminiMonitor constructMonitor() * LONGER necessary to overload this method if your application is not using * a special subclass of Context. */ + // TODO: Need to refactor this so that it's just part of the (legacy?) dispatcher. @Override public Context getContext(Request request) { return new ResinContext(request, this); } + // TODO: Need to refactor this so that it's just part of the legacy dispatcher. @Override protected MustacheManager constructMustacheManager() { diff --git a/gemini/src/main/java/com/techempower/gemini/DispatchListener.java b/gemini/src/main/java/com/techempower/gemini/DispatchListener.java index 806dcb92..808ce0ed 100755 --- a/gemini/src/main/java/com/techempower/gemini/DispatchListener.java +++ b/gemini/src/main/java/com/techempower/gemini/DispatchListener.java @@ -30,6 +30,9 @@ /** * An interface allowing classes to monitor Dispatcher activity. */ +// TODO?: Move this to gemini-legacy-dispatching. Might be hard, transitively +// is depended on by gemini-jdbc's JdbcMonitorListener among many other things +// in gemini, gemini-resin, etc. public interface DispatchListener { diff --git a/gemini/src/main/java/com/techempower/gemini/manager/BasicManager.java b/gemini/src/main/java/com/techempower/gemini/manager/BasicManager.java index 663e792b..22753c7d 100755 --- a/gemini/src/main/java/com/techempower/gemini/manager/BasicManager.java +++ b/gemini/src/main/java/com/techempower/gemini/manager/BasicManager.java @@ -44,7 +44,10 @@ *

* It is common for applications to inherit from an intermediate subclass of * BasicManager such as BasicPathManager. - * + * + * TODO?: BasicPathManager is no longer in this Maven module. Will this be a + * problem for javadoc compilation? + * * @see com.techempower.gemini.path.BasicPathManager */ public class BasicManager diff --git a/pom.xml b/pom.xml index 454d05d0..587691c5 100755 --- a/pom.xml +++ b/pom.xml @@ -48,6 +48,9 @@ gemini-log4j2 gemini-logback gemini-log4j12 + gemini-jax-rs + gemini-legacy-dispatching + gemini-resin-legacy-dispatching @@ -89,6 +92,9 @@ 6.0.0 2.13.0 1.3.0-alpha2 + 2.1.6 + 2.8.1 + 1.10.19 @@ -137,6 +143,16 @@ gemini-hikaricp ${project.version} + + ${project.groupId} + gemini-legacy-dispatching + ${project.version} + + + ${project.groupId} + gemini-resin-legacy-dispatching + ${project.version} + com.fasterxml.jackson.core jackson-core @@ -238,6 +254,11 @@ ${javaee.version} provided + + jakarta.ws.rs + jakarta.ws.rs-api + ${jax-rs.version} + io.jsonwebtoken jjwt-impl @@ -274,6 +295,11 @@ slf4j-log4j12 ${slf4j.version} + + org.slf4j + slf4j-simple + ${slf4j.version} + org.apache.logging.log4j log4j-slf4j18-impl @@ -294,6 +320,17 @@ flyway-core ${flyway.version} + + com.github.ben-manes.caffeine + caffeine + ${caffeine.version} + + + org.mockito + mockito-all + ${mockito.version} + test +