Динамически создаваемый контент для загрузки без записи файла на стороне сервера в веб-приложении Vaadin Flow - PullRequest
2 голосов
/ 24 марта 2020

В моем Vaadin Flow веб-приложении (версия 14 или более поздняя) я хочу предоставить своему пользователю ссылку для загрузки файла данных.

Содержание этой загрузки может быть довольно большим. Поэтому я не хочу материализовать весь контент в памяти сразу. Я хочу создавать фрагменты контента последовательно, загружая загрузку по одному фрагменту за раз, чтобы минимизировать использование памяти. Представьте, например, большое количество строк в базе данных, где мы загружаем по одной строке за раз.

Мне известен виджет Anchor в Vaadin Flow. Но как мне подключить некоторый динамически созданный контент к такому виджету?

Кроме того, учитывая, что эти данные генерируются динамически на лету, я хочу, чтобы имя загруженного файла на компьютере пользователя по умолчанию используется определенный префикс, за которым следует текущая дата-время в формате ГГГГММДДТЧММСС.

Ответы [ 2 ]

3 голосов
/ 24 марта 2020

Предостережение: Я не эксперт по этому вопросу. Мой пример кода, представленный здесь, кажется, работает правильно. Я собрал это решение вместе, изучая ограниченную документацию и читая множество других постов в Интернете. Возможно, мое решение не является лучшим решением.


Для получения дополнительной информации см. Страницу Dynami c Содержимое руководства Vaadin.

У нас есть три основных элемента в вашем Вопросе:

  • Виджет на странице веб-приложения Vaadin, предлагающий пользователю скачать.
  • Dynami c создатель контента
  • Имя по умолчанию для файла, создаваемого на компьютере пользователя

У меня есть решение для первых двух, но не третьего.

Скачать виджет

Как упоминалось в Вопрос, мы используем виджет Anchor (см. Javado c).

Мы определяем переменную-член в нашем макете.

private Anchor anchor;

Мы создаем экземпляр, передавая объект StreamResource. Этот класс определен в Vaadin. Его работа здесь заключается в том, чтобы обернуть класс нашего создания, который будет производить реализацию, расширяющую класс Java InputStream.

Входной поток предоставляет данные по одному октету за раз, возвращая из его read метода int, значение которого равно цифре c число предполагаемого октета, 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" 
    )
;

Далее мы устанавливаем атрибут download для элемента HTML5 anchor. Этот атрибут указывает браузеру, что мы собираемся загрузить цель, когда пользователь щелкает ссылку.

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

Значок можно отобразить, поместив виджет привязки в Button* 1070. *.

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

Dynami c создатель контента

Нам нужно реализовать подкласс InputStream, чтобы предоставить нашему виджету загрузки.

Абстрактный класс InputStream предоставляет реализации всех методов, кроме одного. Нам нужно реализовать только метод read, чтобы удовлетворить потребности нашего проекта.

Вот одна из возможных таких реализаций. Когда вы создаете экземпляр GenerativeInputStream объекта, передайте количество строк, которое вы хотите сгенерировать. Данные генерируются по одной строке за раз, а затем передаются по октету клиенту. Когда закончите с этой строкой, генерируется другая строка. Таким образом, мы сохраняем память, работая только с одной строкой за раз.

Октеты, передаваемые клиенту, являются октетами, составляющими текст UTF-8 нашего ряда. Каждый символ предполагаемого текста может состоять из одного или нескольких октетов. Если вы этого не понимаете, прочитайте занимательный и информативный пост Джоэл Спольски: Абсолютный минимум, который должен знать каждый разработчик программного обеспечения.

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

Имя файла по умолчанию

Я не могу найти способ выполнить sh последнюю часть, по умолчанию имя файла включает момент создания контента.

Я даже опубликовал Вопрос о переполнении стека по этому вопросу: Загрузка с именем файла по умолчанию с указанием даты и времени пользовательского события в приложении Vaadin Flow

Проблема в том, что URL за виджетом ссылки создается один раз, когда страница была загружена и был создан экземпляр виджета Anchor. После этого, пока пользователь читает страницу, проходит время. Когда пользователь в конечном итоге щелкает ссылку, чтобы начать загрузку, текущий момент позже, чем момент, записанный в URL.

Похоже, не существует простого способа обновить этот URL-адрес до текущего момента события щелчка или загрузки пользователя.

Советы

Кстати, для реальной работы я бы не строил экспортированные строки с моим собственным кодом. Вместо этого я бы использовал библиотеку, такую ​​как Apache Commons CSV для записи с разделителями табуляции или значения, разделенные запятыми (CSV) содержание.

Ресурсы

1 голос
/ 03 апреля 2020

Viritin

API Vaadin несколько нелогично для загрузки динамически обслуживаемых файлов. Я предлагаю использовать надстройку типа Flow Viritan для решения проблемы. Проверьте мою летнюю запись в блоге .

Я немного изменил DynamicFileDownloader в flow-viritin . Теперь (начиная с 0.3.5) вы можете переопределить имя файла динамически. Смотрите изменение в GitHub .

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...