In my Vaadin Flow web app (version 14 or later), I want to present to my user a link to download a data file.
The contents of this download may be quite large. So I
The Vaadin API is somewhat counterintuitive for downloading dynamically served files. I suggest to use add-on like Flow Viritan to solve the issue. Check my year old blog entry.
I modified the DynamicFileDownloader in flow-viritin a bit. Now (since 0.3.5) you can override the file name dynamically. See change in GitHub.
Caveat: I am no expert on this matter. My example code presented here seems to be functioning properly. I cobbled this solution together by studying the limited documentation and reading many other posts on the web. Mine may not be the best solution.
For more information, see the Dynamic Content page of the Vaadin manual.
We have three major pieces in your Question:
I have a solution to the first two, but not the third.
As mentioned in the Question, we do use the Anchor
widget (see Javadoc).
We define a member variable on our layout.
private Anchor anchor;
We instantiate by passing a StreamResource object. This class is defined in Vaadin. Its job here is to wrap a class of our making that will produce an implementation extending the Java class InputStream.
An input stream provides data one octet at a time by returning from its read
method an int
whose value is numeric number of the intended octet, 0-255. When reaching the end of the data, a negative one is returned by read
.
In our code, we have implemented a makeStreamOfContent
method to act as the InputStream
factory.
private InputStream makeInputStreamOfContent ( )
{
return GenerativeInputStream.make( 4 );
}
When instantiating our StreamResource
, we pass a method reference that refers to that makeInputStreamOfContent
method. We are getting a bit abstract here, as no input stream nor any data is yet being generated. We are just setting the stage; the action occurs later.
The first argument passed to new StreamResource
is the default name of the file to be created on the user’s client-side machine. In this example, we are using the unimaginative name of report.text
.
anchor =
new Anchor(
new StreamResource( "report.text" , this :: makeInputStreamOfContent ) ,
"Download generated content"
)
;
Next, we set an attribute of download on the HTML5 anchor
element. This attribute indicates to the browser that we intend to have the target downloaded when the user clicks the link.
anchor.getElement().setAttribute( "download" , true );
You can display an icon by wrapping the anchor widget inside a Button.
downloadButton = new Button( new Icon( VaadinIcon.DOWNLOAD_ALT ) );
anchor.add( downloadButton );
If using an icon like this, you should drop the text label from the Anchor
widget. Instead, place any desired text in the Button
. So we would pass empty string (""
) to new Anchor
, and pass the label text as a first argument to new Button
.
anchor =
new Anchor(
new StreamResource( "report.text" , this :: makeInputStreamOfContent ) ,
""
)
;
anchor.getElement().setAttribute( "download" , true );
downloadButton =
new Button(
"Download generated content" ,
new Icon( VaadinIcon.DOWNLOAD_ALT )
)
;
anchor.add( downloadButton );
We need to implement a InputStream subclass, to give to our download widget.
The InputStream
abstract class provides implementations of all but one of its methods. We need implement only the read method to satisfy the needs of our project.
Here is one possible such implementation. When you instantiate a GenerativeInputStream
object, pass the number of rows you want to generate. Data is generated one row at a time, then fed octet-by-octet to the client. When done with that row, another row is generated. So we conserve memory by working only with one row at a time.
The octets fed to the client are the octets making up the UTF-8 text of our row. Each character of intended text may consist of one or more octets. If you do not understand this, read the entertaining and informative post The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!) by Joel Spolsky.
package work.basil.example;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.time.Instant;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.function.IntSupplier;
// Generates random data on-the-fly, to simulate generating a report in a business app.
//
// The data is delivered to the calling program as an `InputStream`. Data is generated
// one line (row) at a time. After a line is exhausted (has been delivered octet by octet
// to the client web browser), the next line is generated. This approach conserves memory
// without materializing the entire data set into RAM all at once.
//
// By Basil Bourque. Use at your own risk.
// © 2020 Basil Bourque. This source code may be used by others agreeing to the terms of the ISC License.
// https://en.wikipedia.org/wiki/ISC_license
public class GenerativeInputStream extends InputStream
{
private int rowsLimit, nthRow;
InputStream rowInputStream;
private IntSupplier supplier;
static private String DELIMITER = "\t";
static private String END_OF_LINE = "\n";
static private int END_OF_DATA = - 1;
// --------| Constructors | -------------------
private GenerativeInputStream ( int countRows )
{
this.rowsLimit = countRows;
this.nthRow = 0;
supplier = ( ) -> this.provideNextInt();
}
// --------| Static Factory | -------------------
static public GenerativeInputStream make ( int countRows )
{
var gis = new GenerativeInputStream( countRows );
gis.rowInputStream = gis.nextRowInputStream().orElseThrow();
return gis;
}
private int provideNextInt ( )
{
int result = END_OF_DATA;
if ( Objects.isNull( this.rowInputStream ) )
{
result = END_OF_DATA; // Should not reach this point, as we checked for null in the factory method and would have thrown an exception there.
} else // Else the row input stream is *not* null, so read next octet.
{
try
{
result = rowInputStream.read();
// If that row has exhausted all its octets, move on to the next row.
if ( result == END_OF_DATA )
{
Optional < InputStream > optionalInputStream = this.nextRowInputStream();
if ( optionalInputStream.isEmpty() ) // Receiving an empty optional for the input stream of a row means we have exhausted all the rows.
{
result = END_OF_DATA; // Signal that we are done providing data.
} else
{
rowInputStream = optionalInputStream.get();
result = rowInputStream.read();
}
}
}
catch ( IOException e )
{
e.printStackTrace();
}
}
return result;
}
private Optional < InputStream > nextRowInputStream ( )
{
Optional < String > row = this.nextRow();
// If we have no more rows, signal the end of data feed with an empty optional.
if ( row.isEmpty() )
{
return Optional.empty();
} else
{
InputStream inputStream = new ByteArrayInputStream( row.get().getBytes( Charset.forName( "UTF-8" ) ) );
return Optional.of( inputStream );
}
}
private Optional < String > nextRow ( )
{
if ( nthRow <= rowsLimit ) // If we have another row to give, give it.
{
nthRow++;
String rowString = UUID.randomUUID() + DELIMITER + Instant.now().toString() + END_OF_LINE;
return Optional.of( rowString );
} else // Else we have exhausted the rows. So return empty Optional as a signal.
{
return Optional.empty();
}
}
// --------| `InputStream` | -------------------
@Override
public int read ( ) throws IOException
{
return this.provideNextInt();
}
}
I cannot find a way to accomplish the last part, defaulting the name of the file to include the moment when content was generated.
I even posted a Question on Stack Overflow on this point: Download with file name defaulting to date-time of user event in Vaadin Flow app
The problem is that the URL behind the link widget is created once, when the page was loaded and that Anchor
widget was instantiated. After that, while the user is reading the page, time passes. When the user eventually clicks the link to initiate the download, the current moment is later than the moment recorded in the URL.
There seems to be no simple way to update that URL to the current moment of the user's click event or download event.
By the way, for real work I would not be building the exported rows with my own code. I would instead be using a library such as Apache Commons CSV to write the Tab-delimited or Comma-separated values (CSV) content.