动态创建的内容可供下载,而无需在Vaadin Flow Web应用程序的服务器端写入文件 [英] Dynamically-created content for download without writing a file on server-side in Vaadin Flow web app

查看:202
本文介绍了动态创建的内容可供下载,而无需在Vaadin Flow Web应用程序的服务器端写入文件的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在我的 Vaadin Flow 网络应用程序(版本14或更高版本),我想向我的用户显示下载数据文件的链接.

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.

我知道Vaadin Flow中的 Anchor 小部件.但是,如何将一些动态创建的内容挂接到这样的小部件上?

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

此外,鉴于此数据是动态动态生成的,因此我希望用户计算机上已下载文件的名称默认为某个前缀,后跟YYYYMMDDTHHMMSS格式的当前日期时间.

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.

推荐答案

注意:我不是这方面的专家.我在这里提供的示例代码似乎运行正常.通过研究有限的文档并阅读了网络上的许多其他文章,我将这种解决方案拼凑在一起.矿山可能不是最好的解决方案.

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.

有关更多信息,请参见 动态内容 页面上的Vaadin手册.

For more information, see the Dynamic Content page of the Vaadin manual.

我们在您的问题中有三大部分:

We have three major pieces in your Question:

  • Vaadin Web应用程序页面上的小部件,用于为用户提供下载.
  • 动态内容创建者
  • 在用户计算机上创建的文件的默认名称

我对前两个有一个解决方案,但对第三个没有.

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

如问题中所述,我们确实使用了Anchor小部件(请参见

As mentioned in the Question, we do use the Anchor widget (see Javadoc).

我们在布局上定义一个成员变量.

We define a member variable on our layout.

private Anchor anchor;

我们通过传递 对象.此类在Vaadin中定义.它的工作是包装我们的类,将产生扩展Java类

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.

输入流通过从其read方法返回一个int(其值为目标八位位组的数字编号,0-255)来一次提供一个八位位组的数据.当到达数据末尾时,read返回负数.

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.

在我们的代码中,我们实现了makeStreamOfContent方法以用作InputStream工厂.

In our code, we have implemented a makeStreamOfContent method to act as the InputStream factory.

private InputStream makeInputStreamOfContent ( )
{
    return GenerativeInputStream.make( 4 );
}

在实例化StreamResource时,我们传递了引用该makeInputStreamOfContent方法的方法引用.由于没有输入流或任何数据尚未生成,因此我们在这里变得有点抽象.我们只是在准备舞台;该操作将在稍后发生.

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.

传递给new StreamResource的第一个参数是要在用户客户端计算机上创建的文件的默认名称.在此示例中,我们使用的是report.text的虚构名称.

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

接下来,我们将属性设置为 download 在HTML5 anchor元素上.此属性向浏览器指示我们打算在用户单击链接时下载目标.

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

您可以通过将锚窗口小部件包裹在 Button .

You can display an icon by wrapping the anchor widget inside a Button.

downloadButton = new Button( new Icon( VaadinIcon.DOWNLOAD_ALT ) );
anchor.add( downloadButton );

如果使用这样的图标,则应从Anchor小部件中删除文本标签.而是将任何所需的文本放在Button中.因此,我们将空字符串("")传递给new Anchor,并将标签文本作为第一个参数传递给new Button.

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.

InputStream抽象类提供了除其中一种方法外的所有方法的实现.我们只需要实现

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.

这里是一种可能的实现方式.实例化GenerativeInputStream对象时,传递要生成的行数.一次生成一行数据,然后将八位字节逐个字节地提供给客户端.完成该行后,将生成另一行.因此,我们一次只处理一行就可以节省内存.

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.

提供给客户端的八位位组是组成 UTF-8 的八位位组.我们这一行的文字.预期文本的每个字符都可以包含一个或多个八位字节.如果您不明白这一点,请阅读有趣且内容丰富的文章

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.

我什至在这一点上发布了有关堆栈溢出的问题: 下载文件名默认为用户事件的日期时间在Vaadin Flow应用程序中

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

问题是,在加载页面并实例化Anchor小部件时,链接小部件背后的URL仅创建了一次.之后,当用户正在阅读页面时,时间就过去了.当用户最终单击链接以开始下载时,当前时刻晚于URL中记录的时刻.

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.

似乎没有简单的方法可以将该URL更新为用户的click事件或download事件的当前时刻.

There seems to be no simple way to update that URL to the current moment of the user's click event or download event.

顺便说一句,对于实际工作,我不会使用自己的代码来构建导出的行.我会改用 Apache Commons CSV 编写制表符分隔

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.

这篇关于动态创建的内容可供下载,而无需在Vaadin Flow Web应用程序的服务器端写入文件的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆