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

Bas*_*que 3 java download vaadin dynamically-generated vaadin-flow

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

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

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

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

Bas*_*que 7

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

\n\n
\n\n

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

\n\n

您的问题分为三个主要部分:

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

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

\n\n

下载小部件

\n\n

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

\n\n

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

\n\n
private Anchor anchor;\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n

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

\n\n

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

\n\n
private InputStream makeInputStreamOfContent ( )\n{\n    return GenerativeInputStream.make( 4 );\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n

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

\n\n
anchor = \n    new Anchor( \n        new StreamResource( "report.text" , this :: makeInputStreamOfContent ) , \n        "Download generated content" \n    )\n;\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n
anchor.getElement().setAttribute( "download" , true );\n
Run Code Online (Sandbox Code Playgroud)\n\n

您可以通过将锚点小部件包装在Button.

\n\n
downloadButton = new Button( new Icon( VaadinIcon.DOWNLOAD_ALT ) );\nanchor.add( downloadButton );\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n
anchor = \n    new Anchor( \n        new StreamResource( "report.text" , this :: makeInputStreamOfContent ) , \n        "" \n    )\n;\nanchor.getElement().setAttribute( "download" , true );\ndownloadButton = \n    new Button( \n        "Download generated content" , \n        new Icon( VaadinIcon.DOWNLOAD_ALT ) \n    )\n;\nanchor.add( downloadButton );\n
Run Code Online (Sandbox Code Playgroud)\n\n

动态内容创建者

\n\n

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

\n\n

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

\n\n

这是一种可能的此类实现。当您实例化一个GenerativeInputStream对象时,传递您想要生成的行数。数据一次生成一行,然后将一个字节一个字节地传送给客户端。完成该行后,会生成另一行。因此,我们一次只处理一行来节省内存。

\n\n

提供给客户端的八位字节是构成我们行的UTF-8文本的八位字节。预期文本的每个字符可以由一个或多个八位位组组成。如果您不明白这一点,请阅读Joel Spolsky 撰写的有趣且信息丰富的文章《每个软件开发人员绝对必须了解 Unicode 和字符集的绝对最低限度(没有任何借口!)》 。

\n\n
package work.basil.example;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.charset.Charset;\nimport java.time.Instant;\nimport java.util.Objects;\nimport java.util.Optional;\nimport java.util.UUID;\nimport java.util.function.IntSupplier;\n\n// Generates random data on-the-fly, to simulate generating a report in a business app.\n//\n// The data is delivered to the calling program as an `InputStream`. Data is generated\n// one line (row) at a time. After a line is exhausted (has been delivered octet by octet\n// to the client web browser), the next line is generated. This approach conserves memory\n// without materializing the entire data set into RAM all at once.\n//\n// By Basil Bourque. Use at your own risk.\n// \xc2\xa9 2020 Basil Bourque. This source code may be used by others agreeing to the terms of the ISC License.\n// https://en.wikipedia.org/wiki/ISC_license\npublic class GenerativeInputStream extends InputStream\n{\n    private int rowsLimit, nthRow;\n    InputStream rowInputStream;\n    private IntSupplier supplier;\n    static private String DELIMITER = "\\t";\n    static private String END_OF_LINE = "\\n";\n    static private int END_OF_DATA = - 1;\n\n    // --------|  Constructors  | -------------------\n    private GenerativeInputStream ( int countRows )\n    {\n        this.rowsLimit = countRows;\n        this.nthRow = 0;\n        supplier = ( ) -> this.provideNextInt();\n    }\n\n    // --------|  Static Factory  | -------------------\n    static public GenerativeInputStream make ( int countRows )\n    {\n        var gis = new GenerativeInputStream( countRows );\n        gis.rowInputStream = gis.nextRowInputStream().orElseThrow();\n        return gis;\n    }\n\n    private int provideNextInt ( )\n    {\n        int result = END_OF_DATA;\n\n        if ( Objects.isNull( this.rowInputStream ) )\n        {\n            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.\n        } else  // Else the row input stream is *not*  null, so read next octet.\n        {\n            try\n            {\n                result = rowInputStream.read();\n                // If that row has exhausted all its octets, move on to the next row.\n                if ( result == END_OF_DATA )\n                {\n                    Optional < InputStream > optionalInputStream = this.nextRowInputStream();\n                    if ( optionalInputStream.isEmpty() ) // Receiving an empty optional for the input stream of a row means we have exhausted all the rows.\n                    {\n                        result = END_OF_DATA; // Signal that we are done providing data.\n                    } else\n                    {\n                        rowInputStream = optionalInputStream.get();\n                        result = rowInputStream.read();\n                    }\n                }\n            }\n            catch ( IOException e )\n            {\n                e.printStackTrace();\n            }\n        }\n\n        return result;\n    }\n\n    private Optional < InputStream > nextRowInputStream ( )\n    {\n        Optional < String > row = this.nextRow();\n        // If we have no more rows, signal the end of data feed with an empty optional.\n        if ( row.isEmpty() )\n        {\n            return Optional.empty();\n        } else\n        {\n            InputStream inputStream = new ByteArrayInputStream( row.get().getBytes( Charset.forName( "UTF-8" ) ) );\n            return Optional.of( inputStream );\n        }\n    }\n\n    private Optional < String > nextRow ( )\n    {\n        if ( nthRow <= rowsLimit ) // If we have another row to give, give it.\n        {\n            nthRow++;\n            String rowString = UUID.randomUUID() + DELIMITER + Instant.now().toString() + END_OF_LINE;\n            return Optional.of( rowString );\n        } else // Else we have exhausted the rows. So return empty Optional as a signal.\n        {\n            return Optional.empty();\n        }\n    }\n\n    // --------|  `InputStream`  | -------------------\n    @Override\n    public int read ( ) throws IOException\n    {\n        return this.provideNextInt();\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

默认文件名

\n\n

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

\n\n

我什至在 Stack Overflow 上发布了一个关于这一点的问题:Download with file name defaulting to date-time of user event in Vaadin Flow app

\n\n

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

\n\n

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

\n\n

尖端

\n\n

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

\n\n

资源

\n\n\n