Skip to content

Commit

Permalink
[RESTEASY-3483] Implement the new getMatchedResourceTemplate() method…
Browse files Browse the repository at this point in the history
…. Added tests for various application configurations.

https://issues.redhat.com/browse/RESTEASY-3483
Signed-off-by: James R. Perkins <jperkins@redhat.com>
  • Loading branch information
jamezp committed Apr 2, 2024
1 parent 72f9b9d commit bbaf84e
Show file tree
Hide file tree
Showing 20 changed files with 1,423 additions and 61 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/*
* JBoss, Home of Professional Open Source.
*
* Copyright 2024 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.jboss.resteasy.core;

import java.util.Objects;

import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;

import org.jboss.resteasy.plugins.server.servlet.ResteasyContextParameters;
import org.jboss.resteasy.resteasy_jaxrs.i18n.Messages;
import org.jboss.resteasy.spi.config.Configuration;
import org.jboss.resteasy.spi.config.ConfigurationFactory;

/**
* Describes basic information about an {@link Application}.
*
* @author <a href="mailto:jperkins@redhat.com">James R. Perkins</a>
*/
public class ApplicationDescription {

private final Class<? extends Application> type;
private final Application instance;
private final String path;

private ApplicationDescription(final Class<? extends Application> type, final Application instance, final String path) {
this.type = type;
this.instance = instance;
this.path = path;
}

/**
* Returns the class of the application.
*
* @return the class for the application
*/
public Class<? extends Application> type() {
return type;
}

/**
* Returns the instance of the application.
*
* @return the instance of the application
*/
public Application instance() {
return instance;
}

/**
* Returns the path of the application. An empty path is always represented as {@code /}.
*
* @return the path of the application
*/
public String path() {
return path;
}

/**
* Builds an application description.
*/
public static class Builder {
private final Application application;
private Class<? extends Application> type;
private String path;

private Builder(final Application application) {
this.application = application;
}

/**
* Creates a new build based on the application.
*
* @param application the application to create the description for, cannot be {@code null}
*
* @return the builder
*/
public static Builder of(final Application application) {
return new Builder(Objects.requireNonNull(application, () -> Messages.MESSAGES.nullParameter("application")));
}

/**
* Defines the type for the application. If set to {@code null}, the type will be resolved from the
* {@linkplain Application#getClass() application}.
*
* @param type the applications class
*
* @return the builder
*/
public Builder type(final Class<? extends Application> type) {
this.type = type;
return this;
}

/**
* Defines the path of the application. If set to {@code null}, the path is resolved from the
* {@link ApplicationPath}. If the application is not annotated, an attempt to look up the defined mapping is
* done. If neither can be found, the assumed path is {@code /}.
*
* @param path the path for the application
*
* @return the builder
*/
public Builder path(final String path) {
this.path = path;
return this;
}

/**
* Builds the application description.
*
* @return the application description
*/
public ApplicationDescription build() {
if (type == null) {
type = application.getClass();
}
if (path == null) {
final ApplicationPath applicationPath = type.getAnnotation(ApplicationPath.class);
if (applicationPath != null) {
path = applicationPath.value();
if (path.isBlank()) {
path = "/";
}
} else {
// Check for a servlet mapping name
final Configuration configuration = ConfigurationFactory.getInstance().getConfiguration();
final var mapping = configuration
.getOptionalValue(ResteasyContextParameters.RESTEASY_SERVLET_MAPPING_PREFIX, String.class);
path = mapping.filter((v) -> !v.isBlank()).orElse("/");
}
}
return new ApplicationDescription(type, application, path);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -375,16 +375,21 @@ public void merge(ResteasyDeployment other) {

public static Application createApplication(String applicationClass, Dispatcher dispatcher,
ResteasyProviderFactory providerFactory) {
Class<?> clazz = null;
Class<? extends Application> clazz = null;
try {
clazz = Thread.currentThread().getContextClassLoader().loadClass(applicationClass);
clazz = Thread.currentThread().getContextClassLoader().loadClass(applicationClass).asSubclass(Application.class);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}

Application app = (Application) providerFactory.createProviderInstance(clazz);
Application app = providerFactory.createProviderInstance(clazz);
dispatcher.getDefaultContextObjects().put(Application.class, app);
ResteasyContext.pushContext(Application.class, app);
final ApplicationDescription applicationDescription = ApplicationDescription.Builder.of(app)
.type(clazz)
.build();
dispatcher.getDefaultContextObjects().put(ApplicationDescription.class, applicationDescription);
ResteasyContext.pushContext(ApplicationDescription.class, applicationDescription);
PropertyInjector propertyInjector = providerFactory.getInjectorFactory().createPropertyInjector(clazz, providerFactory);
propertyInjector.inject(app, false);
return app;
Expand Down Expand Up @@ -534,6 +539,12 @@ protected boolean registerApplication() {
if (application != null) {
dispatcher.getDefaultContextObjects().put(Application.class, application);
ResteasyContext.getContextDataMap().put(Application.class, application);
// Potentially done twice as the current way this works createApplication() and registerApplication() may
// both be invoked. There is no guarantee of that though.
final ApplicationDescription applicationDescription = ApplicationDescription.Builder.of(application)
.build();
dispatcher.getDefaultContextObjects().put(ApplicationDescription.class, applicationDescription);
ResteasyContext.pushContext(ApplicationDescription.class, applicationDescription);
if (processApplication(application)) {
// Application class registered something so don't use scanning data. See JAX-RS spec for more detail.
useScanning = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.util.Objects;

import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.UriInfo;

import org.jboss.resteasy.specimpl.ResteasyUriInfo;
import org.jboss.resteasy.spi.HttpRequest;
Expand All @@ -14,6 +15,37 @@ public class MatchCache {
public SegmentNode.Match match;
public ResourceInvoker invoker;

private final String pathExpression;

/**
* Use the MatchCache(String) constructor
*
* @see #MatchCache(String)
*/
@Deprecated(forRemoval = true, since = "7.0")
public MatchCache() {
this("/");
}

/**
* Creates a new match cache with the path expression for the match. The path expression is used in the
* {@link UriInfo#getMatchedResourceTemplate()}.
*
* @param pathExpression the path expression from the {@link SegmentNode.Match#expression}
*/
protected MatchCache(final String pathExpression) {
this.pathExpression = (pathExpression == null) ? "/" : pathExpression;
}

/**
* Returns the path expression for this match.
*
* @return the path expression
*/
protected String pathExpression() {
return pathExpression;
}

public static class Key {
public String path;
public int start;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ public void populatePathParams(HttpRequest request, Matcher matcher, String path
ResteasyUriInfo uriInfo = (ResteasyUriInfo) request.getUri();
for (Group group : groups) {
String value = matcher.group(group.group);
// null groups are allowed, but we'll treat them as empty strings
if (value == null) {
value = "";
}
uriInfo.addEncodedPathParameter(group.name, value);
int index = matcher.start(group.group);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import org.jboss.resteasy.core.ResourceMethodInvoker;
import org.jboss.resteasy.plugins.server.servlet.ResteasyContextParameters;
import org.jboss.resteasy.specimpl.ResteasyUriInfo;
import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.ResourceInvoker;

Expand Down Expand Up @@ -64,6 +65,8 @@ public ResourceInvoker match(HttpRequest request, int start) {
if (match != null) {
//System.out.println("*** cache hit: " + key.method + " " + key.path);
request.setAttribute(RESTEASY_CHOSEN_ACCEPT, match.chosen);
// We need to add the matched request template
((ResteasyUriInfo) request.getUri()).addMatchedResourceTemplate(match.pathExpression());
} else {
match = root.match(request, start);
if (match.match != null && match.match.expression.getNumGroups() == 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,10 @@ public Match(final MethodExpression expression, final Matcher matcher) {
}

public MatchCache match(HttpRequest request, int start) {
String path = ((ResteasyUriInfo) request.getUri()).getMatchingPath();
final ResteasyUriInfo uriInfo = (ResteasyUriInfo) request.getUri();
String path = uriInfo.getMatchingPath();
RESTEasyTracingLogger logger = RESTEasyTracingLogger.getInstance(request);
logger.log("MATCH_PATH_FIND", ((ResteasyUriInfo) request.getUri()).getMatchingPath());
logger.log("MATCH_PATH_FIND", path);

if (start < path.length() && path.charAt(start) == '/')
start++;
Expand All @@ -98,9 +99,8 @@ public MatchCache match(HttpRequest request, int start) {
expressionMatched = true;
ResourceInvoker invoker = expression.getInvoker();
if (invoker instanceof ResourceLocatorInvoker) {
MatchCache ctx = new MatchCache();
MatchCache ctx = new MatchCache(expression.getPathExpression());
ctx.invoker = invoker;
ResteasyUriInfo uriInfo = (ResteasyUriInfo) request.getUri();
int length = matcher.start(expression.getNumGroups() + 1);
if (length == -1) {
uriInfo.pushMatchedPath(path);
Expand Down Expand Up @@ -128,6 +128,8 @@ public MatchCache match(HttpRequest request, int start) {
}
expression.populatePathParams(request, matcher, path);
logger.log("MATCH_LOCATOR", invoker.getMethod());
// Add the current matched path expression template
uriInfo.addMatchedResourceTemplate(expression.getPathExpression());
return ctx;
} else {

Expand All @@ -143,6 +145,8 @@ public MatchCache match(HttpRequest request, int start) {
MatchCache match = match(matches, request.getHttpMethod(), request);
if (match.match != null) {
match.match.expression.populatePathParams(request, match.match.matcher, path);
// Add the current matched path expression template
uriInfo.addMatchedResourceTemplate(match.match.expression.getPathExpression());
logger.log("MATCH_PATH_SELECTED", match.match.expression.getRegex());
}
return match;
Expand Down Expand Up @@ -431,7 +435,7 @@ public MatchCache match(List<Match> matches, String httpMethod, HttpRequest requ
throw new DefaultOptionsMethodException(Messages.MESSAGES.noResourceMethodFoundForOptions(),
resBuilder.build());
}
MatchCache cache = new MatchCache();
MatchCache cache = new MatchCache("/");
cache.chosen = acceptType;
cache.match = null;
cache.invoker = new ConstantResourceInvoker(resBuilder.build());
Expand Down Expand Up @@ -497,7 +501,7 @@ public MatchCache match(List<Match> matches, String httpMethod, HttpRequest requ
}
MediaType acceptType = sortEntry.getAcceptType();
request.setAttribute(RESTEASY_CHOSEN_ACCEPT, acceptType);
MatchCache ctx = new MatchCache();
MatchCache ctx = new MatchCache(sortEntry.match.expression.getPathExpression());
ctx.chosen = acceptType;
ctx.match = sortEntry.match;
ctx.invoker = sortEntry.match.expression.invoker;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@

import jakarta.servlet.ServletConfig;
import jakarta.servlet.ServletContext;
import jakarta.ws.rs.core.Application;

import org.jboss.resteasy.core.ApplicationDescription;
import org.jboss.resteasy.spi.ResteasyDeployment;

/**
Expand All @@ -26,6 +28,23 @@ public ResteasyDeployment createDeployment() {
ResteasyDeployment deployment = super.createDeployment();
deployment.getDefaultContextObjects().put(ServletConfig.class, config);
deployment.getDefaultContextObjects().put(ServletContext.class, config.getServletContext());
// If the application has not been defined, then we will check the servlet name. If the servlet name is
// jakarta.ws.rs.Application (per the spec) or jakarta.ws.rs.core.Application, we need to use the context
// parameter name.
String application = getParameter(Application.class.getName());
if (application == null) {
application = getParameter("jakarta.ws.rs.Application");
}
if (application == null && Application.class.getName().equals(config.getServletName())) {
String servletMappingPrefix = getParameter(ResteasyContextParameters.RESTEASY_SERVLET_MAPPING_PREFIX);
if (servletMappingPrefix == null) {
servletMappingPrefix = "";
}
final ApplicationDescription description = ApplicationDescription.Builder.of(new Application())
.path(servletMappingPrefix.trim())
.build();
deployment.getDefaultContextObjects().put(ApplicationDescription.class, description);
}
return deployment;
}

Expand Down

0 comments on commit bbaf84e

Please sign in to comment.