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

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

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

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

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


person Basil Bourque    schedule 23.03.2020    source источник
comment
По теме: как экспортировать в csv / excel с помощью сетки vaadin?   -  person Basil Bourque    schedule 24.03.2020


Ответы (2)


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


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

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

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

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

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

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

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

private Anchor anchor;

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

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

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

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

Вы можете отобразить значок, заключив виджет привязки внутрь _ 19_.

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 предоставляет реализации всех своих методов, кроме одного. Нам нужно реализовать только _ 29_, чтобы удовлетворить потребности нашего проекта.

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

Октеты, передаваемые клиенту, представляют собой октеты, составляющие текст UTF-8 нашего ряд. Каждый символ предполагаемого текста может состоять из одного или нескольких октетов. Если вы этого не понимаете, прочтите занимательный и информативный пост Абсолютный минимум, который должен знать каждый разработчик программного обеспечения о 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) содержание.

Ресурсы

person Basil Bourque    schedule 23.03.2020

Виритин

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

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

person mstahv    schedule 03.04.2020