Skip to content

Commit

Permalink
Support @Encoded annotation on REST Client Reactive
Browse files Browse the repository at this point in the history
These changes add support for the `@Encoded` annotation for PATH and QUERY params. 

As stated in the Encoded annotation javadocs, we can use this annotation at class and method level too, so we can use it in REST Client reactive like this. 

Fix quarkusio#23961
  • Loading branch information
Sgitario authored and sberyozkin committed Jun 21, 2023
1 parent a8b1b77 commit 57f70ac
Show file tree
Hide file tree
Showing 17 changed files with 531 additions and 43 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import static org.jboss.resteasy.reactive.common.processor.EndpointIndexer.extractProducesConsumesValues;
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.COMPLETION_STAGE;
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.CONSUMES;
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.ENCODED;
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.FORM_PARAM;
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.MULTI;
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.OBJECT;
Expand Down Expand Up @@ -58,6 +59,7 @@
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.MultivaluedHashMap;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.UriBuilder;
import jakarta.ws.rs.ext.ParamConverter;
import jakarta.ws.rs.ext.ParamConverterProvider;

Expand Down Expand Up @@ -100,6 +102,7 @@
import org.jboss.resteasy.reactive.common.core.GenericTypeMapping;
import org.jboss.resteasy.reactive.common.core.ResponseBuilderFactory;
import org.jboss.resteasy.reactive.common.core.Serialisers;
import org.jboss.resteasy.reactive.common.jaxrs.UriBuilderImpl;
import org.jboss.resteasy.reactive.common.model.MaybeRestClientInterface;
import org.jboss.resteasy.reactive.common.model.MethodParameter;
import org.jboss.resteasy.reactive.common.model.ParameterType;
Expand Down Expand Up @@ -806,15 +809,21 @@ A more full example of generated client (with sub-resource) can is at the bottom
MethodDescriptor constructorDesc = MethodDescriptor.ofConstructor(name, WebTarget.class.getName(), List.class);
try (ClassRestClientContext classContext = new ClassRestClientContext(name, constructorDesc, generatedClasses,
RestClientBase.class, Closeable.class.getName(), restClientInterface.getClassName())) {

classContext.constructor.invokeSpecialMethod(MethodDescriptor.ofConstructor(RestClientBase.class, List.class),
classContext.constructor.getThis(), classContext.constructor.getMethodParam(1));

AssignableResultHandle baseTarget = classContext.constructor.createVariable(WebTarget.class);
AssignableResultHandle baseTarget = classContext.constructor.createVariable(WebTargetImpl.class);
if (restClientInterface.isEncoded()) {
classContext.constructor.assign(baseTarget,
disableEncodingForWebTarget(classContext.constructor, classContext.constructor.getMethodParam(0)));
} else {
classContext.constructor.assign(baseTarget, classContext.constructor.getMethodParam(0));
}

classContext.constructor.assign(baseTarget,
classContext.constructor.invokeInterfaceMethod(
MethodDescriptor.ofMethod(WebTarget.class, "path", WebTarget.class, String.class),
classContext.constructor.getMethodParam(0),
classContext.constructor.invokeVirtualMethod(
MethodDescriptor.ofMethod(WebTargetImpl.class, "path", WebTargetImpl.class, String.class),
baseTarget,
classContext.constructor.load(restClientInterface.getPath())));
FieldDescriptor baseTargetField = classContext.classCreator
.getFieldCreator("baseTarget", WebTargetImpl.class.getName())
Expand Down Expand Up @@ -893,6 +902,9 @@ A more full example of generated client (with sub-resource) can is at the bottom
AssignableResultHandle methodTarget = methodCreator.createVariable(WebTarget.class);
methodCreator.assign(methodTarget,
methodCreator.readInstanceField(webTargetForMethod, methodCreator.getThis()));
if (!restClientInterface.isEncoded() && method.isEncoded()) {
methodCreator.assign(methodTarget, disableEncodingForWebTarget(methodCreator, methodTarget));
}

Integer bodyParameterIdx = null;
Map<MethodDescriptor, ResultHandle> invocationBuilderEnrichers = new HashMap<>();
Expand All @@ -903,8 +915,20 @@ A more full example of generated client (with sub-resource) can is at the bottom

AssignableResultHandle formParams = null;

boolean lastEncodingEnabledByParam = true;
for (int paramIdx = 0; paramIdx < method.getParameters().length; ++paramIdx) {
MethodParameter param = method.getParameters()[paramIdx];
if (!restClientInterface.isEncoded() && !method.isEncoded()) {
boolean needsDisabling = isParamAlreadyEncoded(param);
if (lastEncodingEnabledByParam && needsDisabling) {
methodCreator.assign(methodTarget, disableEncodingForWebTarget(methodCreator, methodTarget));
lastEncodingEnabledByParam = false;
} else if (!lastEncodingEnabledByParam && !needsDisabling) {
methodCreator.assign(methodTarget, enableEncodingForWebTarget(methodCreator, methodTarget));
lastEncodingEnabledByParam = true;
}
}

if (param.parameterType == ParameterType.QUERY) {
//TODO: converters

Expand Down Expand Up @@ -1080,6 +1104,40 @@ A more full example of generated client (with sub-resource) can is at the bottom

}

/**
* The @Encoded annotation is only supported in path/query/matrix/form params.
*/
private boolean isParamAlreadyEncoded(MethodParameter param) {
return param.encoded
&& (param.parameterType == ParameterType.PATH
|| param.parameterType == ParameterType.QUERY
|| param.parameterType == ParameterType.FORM
|| param.parameterType == ParameterType.MATRIX);
}

private ResultHandle disableEncodingForWebTarget(BytecodeCreator creator, ResultHandle baseTarget) {
return setEncodingForWebTarget(creator, baseTarget, false);
}

private ResultHandle enableEncodingForWebTarget(BytecodeCreator creator, ResultHandle baseTarget) {
return setEncodingForWebTarget(creator, baseTarget, true);
}

private ResultHandle setEncodingForWebTarget(BytecodeCreator creator, ResultHandle baseTarget, boolean encode) {
ResultHandle webTarget = creator.invokeVirtualMethod(
MethodDescriptor.ofMethod(WebTargetImpl.class, "clone", WebTargetImpl.class), baseTarget);

ResultHandle uriBuilderImpl = creator.invokeVirtualMethod(
MethodDescriptor.ofMethod(WebTargetImpl.class, "getUriBuilderUnsafe", UriBuilderImpl.class),
webTarget);

creator.invokeVirtualMethod(
MethodDescriptor.ofMethod(UriBuilderImpl.class, "encode", UriBuilder.class, boolean.class),
uriBuilderImpl, creator.load(encode));

return webTarget;
}

private boolean isMultipart(String[] consumes, MethodParameter[] methodParameters) {
if (consumes != null) {
for (String mimeType : consumes) {
Expand Down Expand Up @@ -1201,8 +1259,16 @@ private void handleSubResourceMethod(List<JaxrsClientReactiveEnricherBuildItem>
AssignableResultHandle constructorTarget = createWebTargetForMethod(ownerContext.constructor, ownerTarget,
method);

boolean encodingEnabled = true;

FieldDescriptor forMethodTargetDesc = ownerContext.classCreator
.getFieldCreator("targetInOwner" + methodIndex, WebTargetImpl.class).getFieldDescriptor();
if (subInterface.hasDeclaredAnnotation(ENCODED)) {
ownerContext.constructor.assign(constructorTarget,
disableEncodingForWebTarget(ownerContext.constructor, constructorTarget));
encodingEnabled = false;
}

ownerContext.constructor.writeInstanceField(forMethodTargetDesc, ownerContext.constructor.getThis(),
constructorTarget);

Expand All @@ -1214,9 +1280,26 @@ private void handleSubResourceMethod(List<JaxrsClientReactiveEnricherBuildItem>
AssignableResultHandle client = createRestClientField(name, ownerContext.classCreator, ownerMethod);
AssignableResultHandle webTarget = ownerMethod.createVariable(WebTarget.class);
ownerMethod.assign(webTarget, ownerMethod.readInstanceField(forMethodTargetDesc, ownerMethod.getThis()));

if (encodingEnabled && method.isEncoded()) {
ownerMethod.assign(webTarget, disableEncodingForWebTarget(ownerMethod, webTarget));
}

// Setup Path param from current method
boolean lastEncodingEnabledByParam = true;
for (int i = 0; i < method.getParameters().length; i++) {
MethodParameter param = method.getParameters()[i];
if (encodingEnabled && !method.isEncoded()) {
boolean needsDisabling = isParamAlreadyEncoded(param);
if (lastEncodingEnabledByParam && needsDisabling) {
ownerMethod.assign(webTarget, disableEncodingForWebTarget(ownerMethod, webTarget));
lastEncodingEnabledByParam = false;
} else if (!lastEncodingEnabledByParam && !needsDisabling) {
ownerMethod.assign(webTarget, enableEncodingForWebTarget(ownerMethod, webTarget));
lastEncodingEnabledByParam = true;
}
}

if (param.parameterType == ParameterType.PATH) {
ResultHandle paramValue = ownerMethod.getMethodParam(i);
// methodTarget = methodTarget.resolveTemplate(paramname, paramvalue);
Expand Down Expand Up @@ -1310,17 +1393,34 @@ private void handleSubResourceMethod(List<JaxrsClientReactiveEnricherBuildItem>
AssignableResultHandle methodTarget = subMethodCreator.createVariable(WebTarget.class);
subMethodCreator.assign(methodTarget,
subMethodCreator.readInstanceField(subMethodTarget, subMethodCreator.getThis()));
if (encodingEnabled && subMethod.isEncoded()) {
subMethodCreator.assign(methodTarget, disableEncodingForWebTarget(subMethodCreator, methodTarget));
}

ResultHandle bodyParameterValue = null;
AssignableResultHandle formParams = null;
Map<MethodDescriptor, ResultHandle> invocationBuilderEnrichers = new HashMap<>();

boolean lastEncodingEnabledBySubParam = true;
int inheritedParamIndex = 0;
for (SubResourceParameter subParamField : subParamFields) {
inheritedParamIndex++;
MethodParameter param = subParamField.methodParameter;
ResultHandle paramValue = subMethodCreator.readInstanceField(subParamField.field,
subMethodCreator.getThis());
if (encodingEnabled && !subMethod.isEncoded()) {
boolean needsDisabling = isParamAlreadyEncoded(param);
if (lastEncodingEnabledBySubParam && needsDisabling) {
subMethodCreator.assign(methodTarget,
disableEncodingForWebTarget(subMethodCreator, methodTarget));
lastEncodingEnabledBySubParam = false;
} else if (!lastEncodingEnabledBySubParam && !needsDisabling) {
subMethodCreator.assign(methodTarget,
enableEncodingForWebTarget(subMethodCreator, methodTarget));
lastEncodingEnabledBySubParam = true;
}
}

if (param.parameterType == ParameterType.QUERY) {
//TODO: converters

Expand Down Expand Up @@ -1435,6 +1535,19 @@ private void handleSubResourceMethod(List<JaxrsClientReactiveEnricherBuildItem>
// handle sub-method parameters:
for (int paramIdx = 0; paramIdx < subMethod.getParameters().length; ++paramIdx) {
MethodParameter param = subMethod.getParameters()[paramIdx];
if (encodingEnabled && !subMethod.isEncoded()) {
boolean needsDisabling = isParamAlreadyEncoded(param);
if (lastEncodingEnabledBySubParam && needsDisabling) {
subMethodCreator.assign(methodTarget,
disableEncodingForWebTarget(subMethodCreator, methodTarget));
lastEncodingEnabledBySubParam = false;
} else if (!lastEncodingEnabledBySubParam && !needsDisabling) {
subMethodCreator.assign(methodTarget,
enableEncodingForWebTarget(subMethodCreator, methodTarget));
lastEncodingEnabledBySubParam = true;
}
}

if (param.parameterType == ParameterType.QUERY) {
//TODO: converters

Expand Down Expand Up @@ -2338,7 +2451,17 @@ private void addSubBeanParamData(MethodInfo jandexMethod, BytecodeCreator method
BytecodeCreator invoEnricher = invocationBuilderEnricher.ifNotNull(invocationBuilderEnricher.getMethodParam(1))
.trueBranch();

boolean encodingEnabled = true;
for (Item item : beanParamItems) {

if (encodingEnabled && item.isEncoded()) {
creator.assign(target, disableEncodingForWebTarget(creator, target));
encodingEnabled = false;
} else if (!encodingEnabled && !item.isEncoded()) {
creator.assign(target, enableEncodingForWebTarget(creator, target));
encodingEnabled = true;
}

switch (item.type()) {
case BEAN_PARAM:
BeanParamItem beanParamItem = (BeanParamItem) item;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package io.quarkus.rest.client.reactive.encoded;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.net.URI;

import jakarta.ws.rs.Encoded;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.QueryParam;

import org.eclipse.microprofile.rest.client.RestClientBuilder;
import org.jboss.resteasy.reactive.ClientWebApplicationException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;
import io.quarkus.test.common.http.TestHTTPResource;

public class ClientWithPathParamAndEncodedTest {

@RegisterExtension
static final QuarkusUnitTest TEST = new QuarkusUnitTest();

@TestHTTPResource
URI baseUri;

@Test
public void testClientWithoutEncoded() {
ClientWithoutEncoded client = RestClientBuilder.newBuilder()
.baseUri(baseUri)
.build(ClientWithoutEncoded.class);
ClientWebApplicationException ex = assertThrows(ClientWebApplicationException.class, () -> client.call("a/b"));
assertTrue(ex.getMessage().contains("Not Found"));
}

@Test
public void testClientWithEncodedInParameter() {
ClientWithEncodedInParameter client = RestClientBuilder.newBuilder()
.baseUri(baseUri)
.build(ClientWithEncodedInParameter.class);
assertEquals("Hello A/B", client.call("a/b"));
assertEquals("Hello A/B/C", client.sub().call("b/c"));
}

@Test
public void testClientWithEncodedInMethod() {
ClientWithEncodedInMethod client = RestClientBuilder.newBuilder()
.baseUri(baseUri)
.build(ClientWithEncodedInMethod.class);
assertEquals("Hello A/B", client.call("a/b"));
assertEquals("Hello A/B/C", client.sub1().call("b/c"));
assertEquals("Hello A/B/C", client.sub2().call("b/c"));
assertEquals("Hello A/B/C", client.sub3().call("b/c"));
}

@Test
public void testClientWithEncodedInClass() {
ClientWithEncodedInClass client = RestClientBuilder.newBuilder()
.baseUri(baseUri)
.build(ClientWithEncodedInClass.class);
assertEquals("Hello A/B", client.call("a/b"));
assertEquals("Hello A/B/C", client.sub().call("b/c"));
}

@Path("/server")
public interface ClientWithoutEncoded {
@GET
@Path("/{path}")
String call(@PathParam("path") String path);

@GET
@Path("/{path}")
String callWithQuery(@PathParam("path") String path, @QueryParam("query") String query);
}

@Path("/server")
public interface ClientWithEncodedInParameter {
@GET
@Path("/{path}")
String call(@Encoded @PathParam("path") String path);

@Path("/a")
SubClientWithEncodedInParameter sub();
}

@Path("/server")
public interface ClientWithEncodedInMethod {
@Encoded
@GET
@Path("/{path}")
String call(@PathParam("path") String path);

@Encoded
@Path("/a")
SubClientWithoutEncoded sub1();

@Path("/a")
SubClientWithEncodedInMethod sub2();

@Path("/a")
SubClientWithEncodedInClass sub3();
}

@Encoded
@Path("/server")
public interface ClientWithEncodedInClass {
@GET
@Path("/{path}")
String call(@PathParam("path") String path);

@Path("/a")
SubClientWithoutEncoded sub();
}

public interface SubClientWithoutEncoded {
@GET
@Path("/{path}")
String call(@PathParam("path") String path);
}

public interface SubClientWithEncodedInMethod {
@Encoded
@GET
@Path("/{path}")
String call(@PathParam("path") String path);
}

@Encoded
public interface SubClientWithEncodedInClass {
@GET
@Path("/{path}")
String call(@PathParam("path") String path);
}

public interface SubClientWithEncodedInParameter {
@GET
@Path("/{path}")
String call(@Encoded @PathParam("path") String path);
}

@Path("/server")
static class Resource {
@GET
@Path("/a/b")
public String get() {
return "Hello A/B";
}

@GET
@Path("/a/b/c")
public String getForSubResource() {
return "Hello A/B/C";
}
}
}
Loading

0 comments on commit 57f70ac

Please sign in to comment.