繁体   English   中英

动态创建的下载内容,无需在 Vaadin Flow 网络应用程序的服务器端写入文件

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

在我的Vaadin Flow网络应用程序(版本 14 或更高版本)中,我想向我的用户提供一个下载数据文件的链接。

此下载的内容可能相当大。 所以我不想一下子把内存中的全部内容都具体化。 我想连续生成内容块,一次一个块地下载,以尽量减少我对内存的使用。 例如,想象一下来自数据库的大量行,我们一次向下载提供一行。

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

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

警告:我不是这方面的专家。 我在此处提供的示例代码似乎运行正常。 我通过研究有限的文档并阅读网络上的许多其他帖子,将这个解决方案拼凑在一起。 我的可能不是最好的解决方案。


有关更多信息,请参阅 Vaadin 手册的动态内容页面。

我们在您的问题中有三个主要部分:

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

我有前两个的解决方案,但没有第三个。

下载小部件

正如问题中提到的,我们确实使用了Anchor小部件(请参阅Javadoc )。

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

private Anchor anchor;

我们通过传递一个StreamResource对象来实例化。 这个类是在 Vaadin 中定义的。 它在这里的工作是包装一个我们制作的类,它将产生一个扩展 Java 类InputStream

输入流通过从其read方法返回一个int一次提供一个八位字节的数据,该int值是预期八位字节的数字编号,0-255。 当到达数据末尾时, read返回一个负数。

在我们的代码中,我们实现了一个makeStreamOfContent方法来充当InputStream工厂。

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

在实例化我们的StreamResource ,我们传递一个引用该makeInputStreamOfContent方法的方法引用。 我们在这里变得有点抽象,因为尚未生成输入流或任何数据。 我们只是在搭建舞台; 动作稍后发生。

传递给new StreamResource的第一个参数是要在用户客户端计算机上创建的文件的默认名称。 在这个例子中,我们使用了缺乏想象力的report.text名称。

anchor = 
    new Anchor( 
        new StreamResource( "report.text" , this :: makeInputStreamOfContent ) , 
        "Download generated content" 
    )
;

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

anchor.getElement().setAttribute( "download" , true );

您可以通过将锚小部件包装在Button来显示图标。

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

如果使用这样的图标,您应该从Anchor小部件中删除文本标签。 相反,将任何所需的文本放在Button 所以我们将空字符串 ( "" ) 传递给new Anchor ,并将标签文本作为第一个参数传递给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 );

动态内容创建者

我们需要实现一个InputStream子类,以提供给我们的下载小部件。

InputStream抽象类提供除其中一种方法之外的所有方法的实现。 我们只需要实现read方法就可以满足我们项目的需要。

这是一种可能的此类实现。 实例化GenerativeInputStream对象时,传递要生成的行数。 一次生成一行数据,然后逐个八位字节馈送到客户端。 完成该行后,将生成另一行。 所以我们通过一次只处理一行来节省内存。

提供给客户端的八位字节是构成我们行的UTF-8文本的八位字节。 预期文本的每个字符可能由一个或多个八位字节组成。 如果您不理解这一点,请阅读 Joel Spolsky 撰写的有趣且内容丰富的文章“每个软件开发人员绝对、肯定必须了解 Unicode 和字符集(没有借口!)的绝对最低要求”

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

默认文件名

我找不到完成最后一部分的方法,默认文件名包含生成内容的时刻。

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

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

似乎没有简单的方法可以将该 URL 更新为用户点击事件或下载事件的当前时刻。

提示

顺便说一句,对于实际工作,我不会使用自己的代码构建导出的行。 我会改为使用诸如Apache Commons CSV 之类的库来编写制表符分隔逗号分隔值 (CSV) 的内容。

资源

病毒素

Vaadin API 在下载动态提供的文件时有些违反直觉。 我建议使用像Flow Viritan这样的插件来解决这个问题。 检查我一年前的博客条目

稍微修改了flow-viritin 中DynamicFileDownloader 现在(从 0.3.5 开始)您可以动态覆盖文件名。 请参阅GitHub 中的更改

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM