Skip to content

Commit

Permalink
Merge branch 'latest' into docs/horizontal-layout-slots
Browse files Browse the repository at this point in the history
  • Loading branch information
web-padawan authored Feb 25, 2025
2 parents 5b916c8 + 57df7cb commit ea90678
Show file tree
Hide file tree
Showing 8 changed files with 187 additions and 5 deletions.
1 change: 1 addition & 0 deletions .github/styles/config/vocabularies/Docs/accept.txt
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ TypeScript
[uU]nparsable
[uU]nrendered
[uU]nsecure
[uU]nserializable
(?-i)Vaadin
[vV]alidate
[vV]alidators?
Expand Down
2 changes: 1 addition & 1 deletion articles/components/spreadsheet/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ The Vaadin Spreadsheet has a few inherent limitations. Below is a list of them:
* No provided toolbars, menus, or other controls for formatting cells.
* Limited support for the older XSL formats.
* Constraints related to Apache POI, such as importing and exporting Excel files.
* The component is not serializable due to the internal usage of Apache POI. The `@PreserveOnRefresh` annotation and session replication with Kubernetes Kit are not supported when using Spreadsheet.
* The component is not serializable due to the internal usage of Apache POI. The `@PreserveOnRefresh` annotation is not supported when using Spreadsheet. Session replication with Kubernetes Kit requires to use <<{articles}/tools/kubernetes/session-replication#unserializable-component-wrapper,UnserializableComponentWrapper>>.
* The SUBTOTAL formula is limited to aggregate functions that do not ignore hidden values (i.e., function codes from 1 to 7, as well as 9) because they are not implemented in Apache POI.
* Strict OOXML format is not supported by Apache POI.
* No support for theming the component the same way as other Vaadin components.
Expand Down
21 changes: 18 additions & 3 deletions articles/hilla/guides/from-java-to-react.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -333,12 +333,27 @@ The `useNavigate` hook is used to navigate programmatically. It returns a functi
.TSX
[source,jsx]
----
import { useNavigate } from 'react-router-dom';
import { useSignal } from "@vaadin/hilla-react-signals";
import { Button, NumberField } from "@vaadin/react-components";
import { useNavigate } from "react-router";
export default function HomeView() {
export default function GameView() {
const secretNumber = 42;
const navigate = useNavigate();
const id = useSignal('');
return <Button onClick={() => navigate('/user/123')}>Go to user 123</Button>;
function checkNumber() {
if (Number(id.value) === secretNumber) {
navigate('/win');
} else {
navigate('/lose');
}
}
return <>
<NumberField label="Guess the number" value={id.value} onValueChanged={(e) => id.value = e.detail.value} />
<Button onClick={checkNumber}>Check</Button>
</>;
}
----

Expand Down
37 changes: 37 additions & 0 deletions articles/hilla/guides/uploads-downloads.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
title: File Downloading
page-title: How to implement file downloading in Hilla React applications | Vaadin
description: How to implement file downloading in Hilla React applications.
meta-description: Learn how to implement file downloading in Vaadin Hilla.
order: 73
---


# Downloading Files

Hilla endpoints only respond to POST requests and don't support file downloads. Since Hilla applications use Spring, though, you can leverage Spring's capabilities to implement file downloading.

Select the server mapping that best suits your application. Then create an ad-hoc endpoint to handle file downloads.

Next, configure security using the usual Spring Security configuration semantics.

In the following example, the view downloads a file and passes an ID as a parameter. The endpoint generates the file and sends it to the client. The security is configured so that only authenticated users can download files.

[.example]
--
[source,tsx]
.`download.tsx`
----
include::{root}/frontend/demo/fusion/forms/upload/download.tsx[tags=snippet,indent=0]
----
[source,java]
.`FileDownloadEndpoint.java`
----
include::{root}/src/main/java/com/vaadin/demo/fusion/download/FileDownloadEndpoint.java[tags=snippet,indent=0]
----
[source,java]
.`SecurityConfig.java`
----
include::{root}/src/main/java/com/vaadin/demo/SecurityConfig.java[tags=download,indent=0]
----
--
73 changes: 72 additions & 1 deletion articles/tools/kubernetes/session-replication.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,12 @@ For more information on serialization process, consult the Java documentation on

The following sections contain best practices on how to code a fully serializable Vaadin application.


=== Collaboration Kit

If you're using Collaboration Kit in an application, you may need to follow some <<{articles}/tools/collaboration/advanced/serialization#,specific serialization instructions>>.


=== Serializable Object as UI Component Members

When writing a component, make sure that all of its non-transient members implement [classname]`Serializable`.
Expand Down Expand Up @@ -273,6 +275,75 @@ When a serialization or a deserialization error happens, Kubernetes Kit provides
The `SessionSerializationCallback` interface also provides the `onSerializationSuccess()` and `onDeserializationSuccess()` callback methods which can be used to add custom steps after a successful serialization or deserialization in the same way as for the error callbacks.


=== Unserializable Component Wrapper

A https://github.com/vaadin/kubernetes-kit/blob/main/kubernetes-kit-starter/src/main/java/com/vaadin/kubernetes/starter/sessiontracker/UnserializableComponentWrapper.java[wrapper component] that allows an otherwise unserializable [classname]`com.vaadin.flow.component.Component` to be serialized and deserialized using the provided serializer and deserializer functions.

The unserializable component wrapper should only be used in cases where there's no other option. The preferred way is to make sure that your own components are fully serializable.

During serialization, the serializer generates a serializable state object from the wrapped component. This state object is intended to store serializable and cacheable properties of the component. Upon deserialization, the deserializer reconstructs the component from scratch using the state object, after the entire object graph has been restored. Developers need to ensure the necessary component properties are properly persisted and restored. Also, the component listeners need to be registered again.

Unserializable components are temporarily removed from the component tree during serialization and reinserted after the serialization is completed. Any [classname]`com.vaadin.flow.component.UI` changes caused by their removal and re-addition are silently ignored.

[NOTE]
Any attach or detach listeners registered on the wrapped component are still triggered.

Below is an example using the unserializable component wrapper to wrap a [classname]`com.vaadin.flow.component.spreadsheet.Spreadsheet` component which is not fully serializable:

[source,java]
----
class SpreadsheetView extends VerticalLayout {
public SpreadsheetView() {
setSizeFull();
Spreadsheet spreadsheet = new Spreadsheet();
spreadsheet.createCell(1, 0, "Nicolaus");
spreadsheet.createCell(1, 1, "Copernicus");
configureSpreadsheet(spreadsheet);
var wrapper = new UnserializableComponentWrapper<>(spreadsheet,
SpreadsheetView::serializer, SpreadsheetView::deserializer);
addAndExpand(wrapper);
}
private static WorkbookData serializer(Spreadsheet sheet) {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
sheet.write(baos);
String selection = Optional
.ofNullable(sheet.getCellSelectionManager()
.getSelectedCellRange())
.map(CellRangeAddress::formatAsString).orElse(null);
return new WorkbookData(baos.toByteArray(), selection);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private static Spreadsheet deserializer(WorkbookData data) {
try {
Spreadsheet sheet = new Spreadsheet(
new ByteArrayInputStream(data.data()));
if (data.selection() != null) {
sheet.setSelection(data.selection());
}
configureSpreadsheet(sheet);
return sheet;
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private static void configureSpreadsheet(Spreadsheet sheet) {
sheet.setLocale(Locale.getDefault());
sheet.setWidth("800px");
}
private record WorkbookData(byte[] data,
String selection) implements Serializable {
}
}
----


== Session Replication Issues

Despite applying the mentioned tips, session replication still may fail because of issues during serialization or deserialization.
Expand All @@ -284,7 +355,7 @@ Common issues with serialization and deserialization are presented in the follow

=== SerializedLambda ClassCastException

A common Vaadin application extensively uses lambda expression for components listeners, binder, etc. When serializing and deserializing lambda expressions, it may happen to face [classname]`ClassCastException` with cryptic messages. For instance, `SerializedLambda cannot be cast to class <className>` on serialization, nor can `SerializedLambda be assigned to field <fieldName> of type <className>` on deserialization.
A common Vaadin application extensively uses lambda expression for components listeners, binder, etc. When serializing and deserializing lambda expressions, it may happen to face [classname]`ClassCastException` with cryptic messages. For instance, `SerializedLambda` cannot be cast to class `<className>` on serialization, nor can `SerializedLambda` be assigned to field `<fieldName>` of type `<className>` on deserialization.

Usually, the cause is a self reference. The lambda expression captures an object instance, but the expression is itself a member of the object graph of the captured object.

Expand Down
25 changes: 25 additions & 0 deletions frontend/demo/fusion/forms/upload/download.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useSignal } from '@vaadin/hilla-react-signals';
import { TextField } from '@vaadin/react-components';

export default function DownloadView() {
// tag::snippet[]
const id = useSignal('');

return (
<div className="flex p-m gap-m items-end">
<TextField
label="ID"
value={id.value}
onValueChanged={(e) => {
id.value = e.detail.value;
}}
/>
{id.value && (
<a href={`/download/${id.value}`} download>
Download
</a>
)}
</div>
);
// end::snippet[]
}
7 changes: 7 additions & 0 deletions src/main/java/com/vaadin/demo/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

@EnableWebSecurity
@Configuration
Expand All @@ -12,6 +13,12 @@ public class SecurityConfig extends VaadinWebSecurity {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();

// tag::download[]
// Restrict access to FileDownloadEndpoint to authenticated users
http.authorizeHttpRequests(authorize -> authorize
.requestMatchers(new AntPathRequestMatcher("/download/**")).authenticated());
// end::download[]
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.vaadin.demo.fusion.download;

import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class FileDownloadEndpoint {

// tag::snippet[]
@RequestMapping(value = "/download/{id}", method = RequestMethod.GET)
public ResponseEntity<Resource> downloadFile(@PathVariable("id") String id) {
String content = "File content for ID: " + id;
Resource file = new ByteArrayResource(content.getBytes());

return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"file_" + id + ".txt\"")
.body(file);
}
// end::snippet[]
}

0 comments on commit ea90678

Please sign in to comment.