Given the latest release of Payara, we can now officially use it with Java 11 and Jakarta EE. I'm using this occasion to demonstrate how to create a Jakarta EE backend with a React frontend using TypeScript to up- and download a file. This example also includes a solution to create and bundle a React application with Maven and serve the result with Payara.
The final result will look like the following:
Jakarta EE project setup
The sample project uses Maven, Java 11 and the following dependencies:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>de.rieckpil.blog</groupId> <artifactId>jakarta-ee-react-file-handling</artifactId> <version>1.0-SNAPSHOT</version> <packaging>war</packaging> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <failOnMissingWebXml>false</failOnMissingWebXml> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <jakarta.jakartaee-api.version>8.0.0</jakarta.jakartaee-api.version> <jersey.version>2.29.1</jersey.version> </properties> <dependencies> <dependency> <groupId>jakarta.platform</groupId> <artifactId>jakarta.jakartaee-api</artifactId> <version>${jakarta.jakartaee-api.version}</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.glassfish.jersey.media</groupId> <artifactId>jersey-media-multipart</artifactId> <version>${jersey.version}</version> <scope>provided</scope> </dependency><dependency> <groupId>org.glassfish.jersey.core</groupId> <artifactId>jersey-server</artifactId> <version>${jersey.version}</version> <scope>provided</scope> </dependency> </dependencies> <!-- more --> </project> |
As the JAX-RS specification does not provide a standard for handling file upload as multipart data, I'm including proprietary Jersey dependencies for this. We can mark them with scope provided
, as they are bundled with Payara and therefore don't need to be part of the .war
file.
For this project, I'll demonstrate a solution to build the frontend application with Maven. Furthermore, the Payara Server will then serve the static files for the Single Page Application. We can achieve this while configuring the build
section of our project:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | <project> <!-- dependencies like seen above --> <build> <finalName>jakarta-ee-react-file-handling</finalName> <plugins> <plugin> <groupId>com.github.eirslett</groupId> <artifactId>frontend-maven-plugin</artifactId> <version>1.8.0</version> <executions> <execution> <id>install node and npm</id> <goals> <goal>install-node-and-npm</goal> </goals> <phase>generate-resources</phase> </execution> <execution> <id>npm install</id> <goals> <goal>npm</goal> </goals> <phase>generate-resources</phase> <configuration> <arguments>install</arguments> </configuration> </execution> <execution> <id>npm test</id> <goals> <goal>npm</goal> </goals> <phase>generate-resources</phase> <configuration> <environmentVariables> <CI>true</CI> </environmentVariables> <arguments>test</arguments> </configuration> </execution> <execution> <id>npm build</id> <goals> <goal>npm</goal> </goals> <phase>generate-resources</phase> <configuration> <environmentVariables> <CI>true</CI> </environmentVariables> <arguments>run build</arguments> </configuration> </execution> </executions> <configuration> <workingDirectory>src/main/frontend</workingDirectory> <nodeVersion>v12.13.1</nodeVersion> </configuration> </plugin> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> </plugin> <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>2.22.2</version> </plugin> <plugin> <artifactId>maven-war-plugin</artifactId> <version>3.2.3</version> <configuration> <webResources> <resource> <directory>${project.basedir}/src/main/frontend/build</directory> </resource> </webResources> </configuration> </plugin> </plugins> </build> </project> |
First, the frontend-maven-plugin
takes care of installing all npm dependencies, executing the frontend tests and building the React Single Page Application. It also downloads (if not already present) the correct Node version. You can configure this with nodeVersion
in the configuration
section of the plugin. This will ensure each team member uses the same version to build the project.
The CI=true
is specific for create-react-app. This will ensure to not run the tests and the frontend build in interactive mode and rather finish the process to proceed further Maven plugins.
Finally, we can configure the maven-war-plugin
to include our frontend resources as web resources. When we now build the project with mvn package
, it will build the frontend and backend application and bundle the frontend resources within our .war
file.
Handling files with the Jakarta EE backend
Next, let's have a look at how to handle the file up- and download with our Jakarta EE backend. The backend provides two endpoints: one to upload a file and another to download a random file.
In the first place, we have to register the MultiPartFeature
of Jersey for our application. There are multiple ways to do this. I'm using a JAX-RS
configuration class to achieve this:
1 2 3 4 5 6 | @ApplicationPath("resources") public class JAXRSConfiguration extends ResourceConfig { public JAXRSConfiguration() { packages("de.rieckpil.blog").register(MultiPartFeature.class); } } |
As a result of this, we can now use the proprietary Jersey feature for our JAX-RS resource.
For the file upload, we'll store the file in a list and include the original filename:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | @Path("files") @ApplicationScoped public class FileResource { private List<FileContent> inMemoryFileStore = new ArrayList(); @POST @Consumes(MediaType.MULTIPART_FORM_DATA) public Response uploadNewFile(@FormDataParam("file") InputStream inputStream, @FormDataParam("file") FormDataContentDisposition fileDetails, @Context UriInfo uriInfo) throws IOException { this.inMemoryFileStore.add(new FileContent(fileDetails.getFileName(), inputStream.readAllBytes())); return Response.created(uriInfo.getAbsolutePath()).build(); } } |
The FileContent
class is a simple POJO to store the relevant information:
1 2 3 4 5 6 7 | public class FileContent { private String fileName; private byte[] content; // constructors, getters & setters } |
Our frontend application can request a random file on the same endpoint but has to use HTTP GET. To properly download a file, we have to set the content type to application/octet-stream
and configure some HTTP headers:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | @GET @Produces(MediaType.APPLICATION_OCTET_STREAM) public Response downloadRandomFile() { if (inMemoryFileStore.size() == 0) { return Response.noContent().build(); } FileContent randomFile = inMemoryFileStore.get( ThreadLocalRandom.current().nextInt(0, inMemoryFileStore.size())); return Response .ok(randomFile.getContent()) .type(MediaType.APPLICATION_OCTET_STREAM) .header(HttpHeaders.CONTENT_LENGTH, randomFile.getContent().length) .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + randomFile.getFileName()) .build(); } |
React project setup
Next, let's have a look at the React project setup. With create-react-app we have a great solution for bootstrapping new React applications. I'm using this to place the frontend within the src/main
folder with the following command:
1 | npx create-react-app frontend --template typescript |
To actually create a TypeScript based project, we can use the argument --template typescript
.
For proper components and styling, I'm adding semantic-ui-react
and semantic-ui-css
to this default project:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | { "name": "frontend", "version": "0.1.0", "private": true, "dependencies": { "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^7.1.2", "@types/jest": "^24.0.23", "@types/node": "^12.12.14", "@types/react": "^16.9.15", "@types/react-dom": "^16.9.4", "react": "^16.12.0", "react-dom": "^16.12.0", "react-scripts": "3.3.0", "semantic-ui-css": "^2.4.1", "semantic-ui-react": "^0.88.2", "typescript": "^3.7.3" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" }, "eslintConfig": { "extends": "react-app" }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] } } |
File up- and download from React
Finally, let's have a look at React application written in TypeScript. It contains three components: FileUploadComponent
, FileDownloadComponent
, and App
to wrap everything. All of these are using React's functional component approach.
Let's start with the FileUploadComponent
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | interface FileUploadComponentProps { uploadFile: (file: File) => void } const FileUploadComponent: React.FC<FileUploadComponentProps> = ({uploadFile}) => { const [fileContent, setFileContent] = useState<File | null>(); return <React.Fragment> <Header as='h4'>Upload your file</Header> <Form onSubmit={event => fileContent && uploadFile(fileContent)}> <Form.Group widths='equal'> <Form.Field> <input placeholder='Select a file' type='file' onChange={event => event.target.files && setFileContent(event.target.files.item(0))}/> </Form.Field> <Button type='submit'>Upload</Button> </Form.Group> </Form> </React.Fragment>; }; export default FileUploadComponent; |
Once we upload a file and submit the HTML form, the component will pass the uploaded file to the uploadFile
function.
The FileDownloadComponent
is even simpler, as it executes a download function whenever someone clicks the button:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | interface FileDownloadComponentProps { downloadFile: () => void } const FileDownloadComponent: React.FC<FileDownloadComponentProps> = ({downloadFile}) => { return <React.Fragment> <Header as='h4'>Download a random file</Header> <Form> <Button type='submit' onClick={downloadFile}>Download</Button> </Form> </React.Fragment>; }; export default FileDownloadComponent; |
Last but not least the App component orchestrates everything:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | const App: React.FC = () => { const [statusInformation, setStatusInformation] = useState<StatusInformation>(); return <Container text style={{marginTop: 10}}> <Image src='/jakartaEELogo.png' size='small' centered/> <Header as='h2' textAlign='center'>Jakarta EE & React File Handling</Header> {statusInformation && <Message color={statusInformation.color}>{statusInformation.message}</Message>} <FileUploadComponent uploadFile={file => { setStatusInformation(undefined); new ApiClient() .uploadFile(file) .then(response => setStatusInformation( { message: 'Successfully uploaded the file', color: 'green' })) .catch(error => setStatusInformation({ message: 'Error occurred while uploading file', color: 'red' })) }}/> <Divider/> <FileDownloadComponent downloadFile={() => { setStatusInformation(undefined); new ApiClient() .downloadRandomFile() .then(response => { setStatusInformation({ message: 'Successfully downloaded a random file', color: 'green' }); downloadFileInBrowser(response); }) .catch(error => setStatusInformation({ message: 'Error occurred while downloading file', color: 'red' })) }}/> </Container>; }; export default App; |
Our ApiClient
uses the fetch API for making HTTP requests:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | export class ApiClient { uploadFile(file: File) { let data = new FormData(); data.append('file', file); return fetch('http://localhost:8080/resources/files', { method: 'POST', body: data }); } downloadRandomFile() { return fetch('http://localhost:8080/resources/files'); } } |
To actually download the incoming file from the backend directly, I'm using the following solution:
1 2 3 4 5 6 7 8 9 10 | const downloadFileInBrowser = (response: Response) => { const filename = response.headers.get('Content-Disposition')!.split('filename=')![1]; response.blob().then(blob => { let url = window.URL.createObjectURL(blob); let a = document.createElement('a'); a.href = url; a.download = filename; a.click(); }); }; |
File handling for other project setups
Similar to this example, I've created several guides for handling files with different project setups. Besides Jakarta EE and React file handling, find other examples here:
- Up- and download files with React and Spring Boot
- RESTEasy (WildFly – JAX-RS 2.1) file handling
- Up- and download files with Java EE and Web Components
The source code, with instructions on how to run this example, is available on GitHub.
Have fun up- and downloading files with Jakarta EE and React,
Phil