Dynamic HTML/image/PDF downloads that open in a browser window/tab

I’ve been using FileDownloader and our OnDemandFileDownloader scheme under Vaadin 7.1.10 (and before) with good success except for the lack of support in making it work for non-components (like a MenuItem being clicked, Actions, item clicked in a Select box, etc.).

But we have dynamically generated HTML, images and PDFs (meaning some are generated while others are retrieved from a database – they are not files stored in a file system) that we’d like to be able to click and then show so that they appear in a popup browser window or new browser tab and avoid the file download save/open dialog. Is this possible and what’s the best approach for doing it?

One problem with the Save/Open dialog is that if my default browser for HTML, say is Firefox, but I’m using Chrome or IE, when I click the open option, it often opens the downloaded file in Firefox rather than the browser I was using. That’s confusing to be sure.

If you send the binary data with the correct mime type, the user’s browser will open (or embed) the file, e.g. a PDF file, in the browser window.

Instead of FileDownloader and OnDemandFileDownloader, you could use a StreamResource, as in the example in paragraph
4.4.5. Stream Resources
in the Vaadin book


https://vaadin.com/book/-/page/application.resources.html

This chapter also contains links to the chapter on request handlers, for a more general approach to serve dynamic content.

Yes, we already do something like that, but generally we end up with code like this:

Page.getCurrent().open(sr, “_blank”, true);

to make that stream resource open as a popup window.

That works fine, but that method is deprecated and so the assumption is there’s a “new correct standard” way to allow someone to click on a button to download dynamically generated content that must be done “on the fly” as we cannot actually produce all of the dynamic content for all of the buttons “just in case.”

We have defined a separate download view - the name of the file to download is passsed to the view and in the ViewChangeEvent for the download view, the FileDownloader is created using the appropriate resource. This works pretty well.

I created a class OnDemandBrowserWindowOpener that subclasses BrowserWindowOpener and otherwise mirrors changes made for our OnDemandFileDownloader (subclasses FileDownloader).

It seems to work, but I wanted to see if any Vaadin experts can tell if the code is useful and correct for our purpose, which is to open up a browser window using dynamic content. So we may send back a PDF or HTML file that is retrieved from our database (and thus has no URL or the like).

And like the other, the key reason is we don’t want to actually fetch the dynamic content unless the button is clicked to trigger it being opened up.

Here’s our code. Feedback is welcome:

public class OnDemandBrowserWindowOpener extends BrowserWindowOpener {
    private static final long serialVersionUID = 2448069691276636815L;

    /**
     * Provide both the {@link StreamSource} and the filename in an on-demand way.
     */
    public interface OnDemandStreamSource extends StreamSource {
        public String getFilename ();
    }
    
    public static class OnDemandStreamResource extends StreamResource {
        private static final long serialVersionUID = -1087853524501280897L;

        protected HashMap<String,String> parameterMap;
        
        public OnDemandStreamResource(OnDemandStreamSource streamSource) {
            super(streamSource,"dummyFileName");
        }
        
        public DownloadStream getStream() {
            DownloadStream ds = super.getStream();
            
            if ( parameterMap != null ) {
                for( String key : parameterMap.keySet() ) {
                    ds.setParameter(key, parameterMap.get(key));
                }
            }
            
            // If the cache time is still the default, let's reduce for our dynamic content that shouldn't be cached for a day.
            if ( ds.getCacheTime() == DownloadStream.DEFAULT_CACHETIME ) {
                ds.setCacheTime(10L * 60L * 1000L); // 10 minutes in msecs
            }
            
            return ds;
        }
        
        public void setParameterMap(HashMap<String,String> parameterMap) {
            this.parameterMap = parameterMap;
        }
    }

    protected final OnDemandStreamSource onDemandStreamSource;
    protected HashMap<String,String> parameterMap;
    protected Button buttonToEnableWhenDone;
    protected String contentType;
    protected Integer cacheTime;

    public OnDemandBrowserWindowOpener(OnDemandStreamSource onDemandStreamSource) {
        super(new OnDemandStreamResource(onDemandStreamSource));
        this.onDemandStreamSource = onDemandStreamSource;
    }

    public OnDemandBrowserWindowOpener(Button buttonToEnableWhenDone, OnDemandStreamSource onDemandStreamSource) {
        this(onDemandStreamSource);
        this.buttonToEnableWhenDone = buttonToEnableWhenDone;
    }

    @Override
    public boolean handleConnectorRequest(VaadinRequest request, VaadinResponse response, String path) throws IOException {
        try {
            OnDemandStreamResource sr = getResource();
            sr.setFilename( onDemandStreamSource.getFilename() );
            if ( contentType != null ) {
                sr.setMIMEType(contentType);
            }
            if ( cacheTime != null ) {
                sr.setCacheTime(cacheTime);
            }
            if ( parameterMap != null ) {
                sr.setParameterMap(parameterMap);
            }
            return super.handleConnectorRequest(request, response, path);
        } finally {
            if ( buttonToEnableWhenDone != null ) {
                buttonToEnableWhenDone.setEnabled(true);
            }
        }
    }

    public OnDemandStreamResource getResource() {
        return (OnDemandStreamResource)this.getResource(BrowserWindowOpenerState.locationResource);
    }
    
    public void setContentType(String v) {
        contentType = v;
    }
    
    public void setCacheTime(int msecs) {
        cacheTime = msecs;
    }
    
    public void setStreamSourceParameter(String name, String value) {
        if ( parameterMap == null )
            parameterMap = new HashMap<String,String>();
        parameterMap.put(name, value);
    }
}

We then use it something like this:

            final Button downloadDocumentSubButton = new Button(label);
            downloadDocumentSubButton.setStyleName(Reindeer.BUTTON_LINK);
            downloadDocumentSubButton.setDisableOnClick(true);

            OnDemandBrowserWindowOpener opener = new OnDemandBrowserWindowOpener(downloadDocumentSubButton, new OnDemandBrowserWindowOpener.OnDemandStreamSource() {
                @Override
                public InputStream getStream() {
                        String html = tpd.tpd.getSnapshotDocument(); // this is just how we retrieve the HTML object from the DB
                        byte downloadHtmlData = EsfString.stringToBytes(html);
                        return new BufferedInputStream(new ByteArrayInputStream(downloadHtmlData));
                }

                @Override
                public String getFilename() { // currently not working as expected (not shown in the resulting URL)
                    String htmlFileName;
                    if ( tpd.tpd.hasDocumentFileName() )
                        htmlFileName = tpd.tpd.getDocumentFileName();
                    else {
                        EsfVaadinUI vaadinUi = EsfVaadinUI.getInstance();
                        htmlFileName = vaadinUi.getMsg("TranSnapshotPopupButton.document.filename",tpd.documentName,tpd.partyName);
                    }

                    return htmlFileName;
                }
            
            });
            opener.setContentType(Application.CONTENT_TYPE_HTML+Application.CONTENT_TYPE_CHARSET_UTF_8);
            opener.setCacheTime(3600000); // 3600 secs (1 hour) in msecs
            opener.extend(downloadDocumentSubButton);
            layout.addComponent(downloadDocumentSubButton);    

The resulting URL in my browser window is:
http://localhost/open-eSignFormsVaadin7/ui/APP/connector/0/245/url/dummyFileName

I’ll need to investigate a bit more, as I wanted it to be the filename I return in my getFileName() method, which is called, but the value clearly doesn’t go through and the default “dummyFileName” is still used.

It seems that the URL path is set when the StreamResource is created, so the filename cannot be dynamic, but it seems the actual data retrieval can still be done that way. So we now pass the filename that will be retrieved+generated to our constructor.

It seems to work, but would like any feedback if this is an appropriate approach for a BrowserWindowOpener to produce dynamic responses that you don’t want to actually fetch unless the extended button is clicked.

I most concerned if this sort of scheme (which we also use for dynamic file downloader) doesn’t result in any leaks when the view with the button and extended “opener/downloader” object is used.

Here’s our working OnDemandBrowserWindowOpener as it stands today:

// Copyright (C) 2014 Yozons, Inc.
// Open eSignForms - Web-based electronic contracting software
//
// This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License
// as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
// See the GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License along with this program.  
// If not, see http://open.esignforms.com/agpl.txt or http://www.gnu.org/licenses/.
// Contact information is via the public forums at http://open.esignforms.com or via private email to open-esign@yozons.com.
//
package com.esignforms.open.vaadin.widget;

import java.io.IOException;
import java.util.HashMap;

import com.vaadin.server.BrowserWindowOpener;
import com.vaadin.server.DownloadStream;
import com.vaadin.server.StreamResource;
import com.vaadin.server.StreamResource.StreamSource;
import com.vaadin.server.VaadinRequest;
import com.vaadin.server.VaadinResponse;
import com.vaadin.shared.ui.BrowserWindowOpenerState;
import com.vaadin.ui.Button;


/**
 * OnDemandBrowserWindowOpener is based OnDemandFileDownloader.
 *
 * @author Yozons Inc.
 */
public class OnDemandBrowserWindowOpener extends BrowserWindowOpener {
    private static final long serialVersionUID = -8352749340084392927L;

    /**
     * Provide the {@link StreamSource} in an on-demand way. Keeping in case we need to add methods like getFileName() for file downloader.
     */
    public interface OnDemandStreamSource extends StreamSource {
    }
    
    public static class OnDemandStreamResource extends StreamResource {
        private static final long serialVersionUID = 4582308475950400910L;

        protected HashMap<String,String> parameterMap;
        
        public OnDemandStreamResource(OnDemandStreamSource streamSource, String filename) {
            super(streamSource,filename);
        }
        
        public DownloadStream getStream() {
            DownloadStream ds = super.getStream();
            
            if ( parameterMap != null ) {
                for( String key : parameterMap.keySet() ) {
                    ds.setParameter(key, parameterMap.get(key));
                }
            }
            
            // If the cache time is still the default, let's reduce for our dynamic content that shouldn't be cached for a day.
            if ( ds.getCacheTime() == DownloadStream.DEFAULT_CACHETIME ) {
                ds.setCacheTime(10L * 60L * 1000L); // 10 minutes in msecs
            }
            
            return ds;
        }
        
        public void setParameterMap(HashMap<String,String> parameterMap) {
            this.parameterMap = parameterMap;
        }
    }

    protected final OnDemandStreamSource onDemandStreamSource;
    protected HashMap<String,String> parameterMap;
    protected Button buttonToEnableWhenDone;
    protected String contentType;
    protected Integer cacheTime;

    public OnDemandBrowserWindowOpener(String filename, OnDemandStreamSource onDemandStreamSource) {
        super(new OnDemandStreamResource(onDemandStreamSource,filename));
        this.onDemandStreamSource = onDemandStreamSource;
    }

    public OnDemandBrowserWindowOpener(Button buttonToEnableWhenDone, String filename, OnDemandStreamSource onDemandStreamSource) {
        this(filename,onDemandStreamSource);
        this.buttonToEnableWhenDone = buttonToEnableWhenDone;
    }

    @Override
    public boolean handleConnectorRequest(VaadinRequest request, VaadinResponse response, String path) throws IOException {
        try {
            OnDemandStreamResource sr = getResource();
            if ( contentType != null ) {
                sr.setMIMEType(contentType);
            }
            if ( cacheTime != null ) {
                sr.setCacheTime(cacheTime);
            }
            if ( parameterMap != null ) {
                sr.setParameterMap(parameterMap);
            }
            return super.handleConnectorRequest(request, response, path);
        } finally {
            if ( buttonToEnableWhenDone != null ) {
                buttonToEnableWhenDone.setEnabled(true);
            }
        }
    }

    public OnDemandStreamResource getResource() {
        return (OnDemandStreamResource)this.getResource(BrowserWindowOpenerState.locationResource);
    }
    
    public void setContentType(String v) {
        contentType = v;
    }
    
    public void setCacheTime(int msecs) {
        cacheTime = msecs;
    }
    
    public void setStreamSourceParameter(String name, String value) {
        if ( parameterMap == null )
            parameterMap = new HashMap<String,String>();
        parameterMap.put(name, value);
    }
}