Skip to content

Commit

Permalink
feat: add support for file upload
Browse files Browse the repository at this point in the history
Enhances extension compatibility with file upload changes from
vaadin/hilla#3165.
Updates EndpointController to utilize a custom HttpServletRequest,
ensuring seamless integration with Quarkus Multipart Form data handling.
  • Loading branch information
mcollovati committed Feb 3, 2025
1 parent 5e2a573 commit aa5b05e
Show file tree
Hide file tree
Showing 17 changed files with 586 additions and 31 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright 2025 Marco Collovati, Dario Götze
*
* 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 com.github.mcollovati.quarkus.hilla.deployment.asm;

import io.quarkus.gizmo.Gizmo;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.TypeInsnNode;

/**
* Modifies EndpointController class to use the extension MultipartRequest
* wrapper instead of Spring MultipartHttpServletRequest.
*/
public class EndpointControllerVisitor extends ClassVisitor {

public static final String SPRING_MULTIPART_HTTP_SERVLET_REQUEST =
"org/springframework/web/multipart/MultipartHttpServletRequest";
public static final String QH_MULTIPART_HTTP_SERVLET_REQUEST =
"com/github/mcollovati/quarkus/hilla/multipart/MultipartRequest";

public EndpointControllerVisitor(ClassVisitor classVisitor) {
super(Gizmo.ASM_API_VERSION, classVisitor);
}

@Override
public MethodVisitor visitMethod(
int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor superVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
if ("doServeEndpoint".equals(name)) {
return new MethodNode(Gizmo.ASM_API_VERSION, access, name, descriptor, signature, exceptions) {
@Override
public void visitEnd() {
var iterator = instructions.iterator();
TypeInsnNode checkCastNode = AsmUtils.findNextInsnNode(
iterator,
node -> node.getOpcode() == Opcodes.CHECKCAST
&& node.desc.equals(SPRING_MULTIPART_HTTP_SERVLET_REQUEST),
TypeInsnNode.class);
checkCastNode.desc = QH_MULTIPART_HTTP_SERVLET_REQUEST;
MethodInsnNode getParameterNode = AsmUtils.findNextInsnNode(
iterator,
node -> node.getOpcode() == Opcodes.INVOKEINTERFACE
&& node.owner.equals(SPRING_MULTIPART_HTTP_SERVLET_REQUEST)
&& node.name.equals("getParameter"),
MethodInsnNode.class);
getParameterNode.setOpcode(Opcodes.INVOKEVIRTUAL);
getParameterNode.owner = QH_MULTIPART_HTTP_SERVLET_REQUEST;
getParameterNode.itf = false;

MethodInsnNode getFileMapNode = AsmUtils.findNextInsnNode(
iterator,
node -> node.getOpcode() == Opcodes.INVOKEINTERFACE
&& node.owner.equals(SPRING_MULTIPART_HTTP_SERVLET_REQUEST)
&& node.name.equals("getFileMap"),
MethodInsnNode.class);
getFileMapNode.setOpcode(Opcodes.INVOKEVIRTUAL);
getFileMapNode.owner = QH_MULTIPART_HTTP_SERVLET_REQUEST;
getFileMapNode.itf = false;
accept(superVisitor);
}
};
}
return superVisitor;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@ public static void addClassVisitors(BuildProducer<BytecodeTransformerBuildItem>
}));
producer.produce(applicationContextProvider_runOnContext_patch());
producer.produce(endpointCodeGenerator_findBrowserCallables_replacement());
producer.produce(new BytecodeTransformerBuildItem(
"com.vaadin.hilla.EndpointController",
(className, classVisitor) -> new EndpointControllerVisitor(classVisitor)));
}

@SafeVarargs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,20 @@
*/
package com.github.mcollovati.quarkus.hilla.deployment;

import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;

import com.vaadin.hilla.EndpointController;
import com.vaadin.hilla.exception.EndpointValidationException;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import io.restassured.response.ExtractableResponse;
import io.restassured.response.Response;
import org.assertj.core.api.Assertions;
import org.hamcrest.CoreMatchers;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

import com.github.mcollovati.quarkus.hilla.deployment.endpoints.Pojo;
Expand Down Expand Up @@ -71,6 +81,7 @@ void invokeEndpoint_multipleParameters() {
}

@Test
@Disabled("Order does not matter anymore")
void invokeEndpoint_wrongParametersOrder_badRequest() {
givenEndpointRequest(
getEndpointPrefix(),
Expand Down Expand Up @@ -158,6 +169,33 @@ void invokeEndpoint_missingMethodName_notFound() {
.statusCode(404);
}

@Test
void invokeEndpoint_multipart_fileTransfer() throws IOException {
Path tempFile = Files.createTempFile("upload", "txt");
Files.writeString(tempFile, "hello world");
ExtractableResponse<Response> res = RestAssured.given()
.contentType(ContentType.MULTIPART)
.multiPart("/file", tempFile.toFile())
.multiPart(
EndpointController.BODY_PART_NAME,
"""
{ "info": { "id": "UPLOAD-1", "date": "2025-02-02" } }
""")
.cookie("csrfToken", "CSRF_TOKEN")
.header("X-CSRF-Token", "CSRF_TOKEN")
.basePath(getEndpointPrefix())
.when()
.post("UploadEndpoint/upload")
.then()
.assertThat()
.statusCode(200)
.body("info.id", equalTo("UPLOAD-1"))
.body("info.date", equalTo("2025-02-02"))
.extract();
Path uploadedFile = Path.of(URI.create(res.body().path("uri")));
Assertions.assertThat(uploadedFile).hasSameTextualContentAs(tempFile);
}

protected String getEndpointPrefix() {
return TestUtils.DEFAULT_PREFIX;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

import com.github.mcollovati.quarkus.hilla.deployment.endpoints.Pojo;
import com.github.mcollovati.quarkus.hilla.deployment.endpoints.TestBrowserCallable;
import com.github.mcollovati.quarkus.hilla.deployment.endpoints.UploadEndpoint;

class BrowserCallableControllerTest extends AbstractEndpointControllerTest {

Expand All @@ -31,7 +32,7 @@ class BrowserCallableControllerTest extends AbstractEndpointControllerTest {
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withConfigurationResource(testResource("test-application.properties"))
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
.addClasses(TestUtils.class, Pojo.class, TestBrowserCallable.class));
.addClasses(TestUtils.class, Pojo.class, TestBrowserCallable.class, UploadEndpoint.class));

@Override
protected String getEndpointName() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import com.github.mcollovati.quarkus.hilla.QuarkusEndpointConfiguration;
import com.github.mcollovati.quarkus.hilla.deployment.endpoints.Pojo;
import com.github.mcollovati.quarkus.hilla.deployment.endpoints.TestEndpoint;
import com.github.mcollovati.quarkus.hilla.deployment.endpoints.UploadEndpoint;

class CustomPrefixEndpointControllerTest extends AbstractEndpointControllerTest {

Expand All @@ -34,8 +35,8 @@ class CustomPrefixEndpointControllerTest extends AbstractEndpointControllerTest
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withConfigurationResource(testResource("test-application.properties"))
.overrideConfigKey(QuarkusEndpointConfiguration.VAADIN_ENDPOINT_PREFIX, CUSTOM_PREFIX)
.setArchiveProducer(() ->
ShrinkWrap.create(JavaArchive.class).addClasses(TestUtils.class, Pojo.class, TestEndpoint.class));
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
.addClasses(TestUtils.class, Pojo.class, TestEndpoint.class, UploadEndpoint.class));

@Override
protected String getEndpointPrefix() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

import com.github.mcollovati.quarkus.hilla.deployment.endpoints.Pojo;
import com.github.mcollovati.quarkus.hilla.deployment.endpoints.TestEndpoint;
import com.github.mcollovati.quarkus.hilla.deployment.endpoints.UploadEndpoint;

class EndpointControllerTest extends AbstractEndpointControllerTest {

Expand All @@ -30,8 +31,8 @@ class EndpointControllerTest extends AbstractEndpointControllerTest {
@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withConfigurationResource(testResource("test-application.properties"))
.setArchiveProducer(() ->
ShrinkWrap.create(JavaArchive.class).addClasses(TestUtils.class, Pojo.class, TestEndpoint.class));
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
.addClasses(TestUtils.class, Pojo.class, TestEndpoint.class, UploadEndpoint.class));

@Override
protected String getEndpointName() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

import com.github.mcollovati.quarkus.hilla.deployment.endpoints.Pojo;
import com.github.mcollovati.quarkus.hilla.deployment.endpoints.TestBrowserCallable;
import com.github.mcollovati.quarkus.hilla.deployment.endpoints.UploadEndpoint;

class SpringDataExtensionsSupportTest extends AbstractEndpointControllerTest {

Expand All @@ -37,7 +38,7 @@ class SpringDataExtensionsSupportTest extends AbstractEndpointControllerTest {
.setForcedDependencies(
List.of(Dependency.of("io.quarkus", "quarkus-spring-data-jpa", Version.getVersion())))
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
.addClasses(TestUtils.class, Pojo.class, TestBrowserCallable.class));
.addClasses(TestUtils.class, Pojo.class, TestBrowserCallable.class, UploadEndpoint.class));

protected String getEndpointName() {
return ENDPOINT_NAME;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

import com.github.mcollovati.quarkus.hilla.deployment.endpoints.Pojo;
import com.github.mcollovati.quarkus.hilla.deployment.endpoints.TestBrowserCallable;
import com.github.mcollovati.quarkus.hilla.deployment.endpoints.UploadEndpoint;

class SpringDiExtensionsSupportTest extends AbstractEndpointControllerTest {

Expand All @@ -36,7 +37,7 @@ class SpringDiExtensionsSupportTest extends AbstractEndpointControllerTest {
.withConfigurationResource(testResource("test-spring-di-application.properties"))
.setForcedDependencies(List.of(Dependency.of("io.quarkus", "quarkus-spring-di", Version.getVersion())))
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
.addClasses(TestUtils.class, Pojo.class, TestBrowserCallable.class));
.addClasses(TestUtils.class, Pojo.class, TestBrowserCallable.class, UploadEndpoint.class));

protected String getEndpointName() {
return ENDPOINT_NAME;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright 2025 Marco Collovati, Dario Götze
*
* 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 com.github.mcollovati.quarkus.hilla.deployment.endpoints;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDate;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.vaadin.flow.server.auth.AnonymousAllowed;
import com.vaadin.hilla.BrowserCallable;
import org.springframework.web.multipart.MultipartFile;

@BrowserCallable
@AnonymousAllowed
public class UploadEndpoint {

public Object upload(Info info, MultipartFile file) {
try {
Path tempFile = Files.createTempFile("upload", "test");
file.transferTo(tempFile);
return new Result(info, tempFile.toUri().toString());
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}

public record Info(String id, @JsonFormat(pattern = "yyyy-MM-dd") LocalDate date) {}

public record Result(Info info, String uri) {}
}
1 change: 1 addition & 0 deletions commons/hilla-shaded-deps/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
<artifact>org.springframework:spring-web</artifact>
<includes>
<include>org/springframework/http/*.class</include>
<include>org/springframework/web/multipart/MultipartFile.class</include>
</includes>
</filter>
<filter>
Expand Down
Loading

0 comments on commit aa5b05e

Please sign in to comment.