Dynamically-created content for download without writing a file on server-side in Vaadin Flow web app

两盒软妹~` 提交于 2020-05-23 10:21:05

问题


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 do not want to materialize the entirety of the content in memory all at once. I want to produce chunks of content in series, feeding the download one chunk at a time, so as to minimize my use of memory. Imagine, for example, a large number of rows from a database, where we feed one row at a time to the download.

I am aware of the Anchor widget in Vaadin Flow. But how do I hook some dynamically-created content to such a widget?

Also, given that this data is being dynamically generated on-the-fly, I want the name of the downloaded file on the user's machine to default to a certain prefix followed by the current date-time in YYYYMMDDTHHMMSS format.


回答1:


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:

  • Widget on Vaadin web app page to offer the user a download.
  • Dynamic content creator
  • Default name of file being created on user’s machine

I have a solution to the first two, but not the third.

Download widget

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 );

Dynamic content creator

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();
    }
}

Default file name

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.

Tips

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.

Resources

  • Forum: Vaadin 10 Let user download a file
  • Forum: Image from byte array
  • Manual: Dynamic Content



回答2:


Viritin

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.



来源:https://stackoverflow.com/questions/60822729/dynamically-created-content-for-download-without-writing-a-file-on-server-side-i

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!