javaspring-bootwicket

Download of files not working anymore after Upgrade to SpringBoot 2.6 and Wicket 9.8


So, I upgraded an application from SpringBoot 2.1 to 2.6 and Wicket 8.6 to 9.8 and everything is working fine now except for the download of csv-files. I get no error, the file is created like it should be but it won't get downloaded and I don't know where to look anymore. So I will share the classes of the downloadbutton and the download itself. Maybe someone has an idea.

AjaxFileDownload

import java.io.File;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.behavior.AbstractAjaxBehavior;
import org.apache.wicket.request.IRequestCycle;
import org.apache.wicket.request.handler.resource.ResourceStreamRequestHandler;
import org.apache.wicket.request.resource.ContentDisposition;
import org.apache.wicket.util.file.Files;
import org.apache.wicket.util.resource.FileResourceStream;
import org.apache.wicket.util.resource.IResourceStream;

// Based on: https://cwiki.apache.org/confluence/display/WICKET/AJAX+update+and+file+download+in+one+blow
public class AjaxFileDownload extends AbstractAjaxBehavior {
    private static final long serialVersionUID = -3721931990952363001L;

    private File file;
    private boolean deleteAfter;
    private boolean addAntiCache;

    public AjaxFileDownload() {
        this(false, true);
    }

    public AjaxFileDownload(boolean deleteAfter) {
        this(deleteAfter, true);
    }

    public AjaxFileDownload(boolean deleteAfter, boolean addAntiCache) {
        super();
        this.deleteAfter = deleteAfter;
        this.addAntiCache = addAntiCache;
    }

    // Call this method to initiate the download
    public void initiate(AjaxRequestTarget target) {
        if (file == null) {
            throw new IllegalArgumentException("No file has been provided for download.");
        }
        String url = getCallbackUrl().toString();
        if (addAntiCache) {
            url = url + (url.contains("?") ? "&" : "?");
            url = url + "antiCache=" + System.currentTimeMillis();
        }

        // the timeout is needed to let Wicket release the channel
        target.appendJavaScript("setTimeout(\"window.location.href='" + url + "'\", 100);");
    }

    @Override
    public void onRequest() {
        ResourceStreamRequestHandler handler = new ResourceStreamRequestHandler(getResourceStream(), getFileName()) {
            @Override
            public void respond(IRequestCycle requestCycle) {
                super.respond(requestCycle);

                if (deleteAfter) {
                    Files.remove(file);
                }
            }
        };
        handler.setContentDisposition(ContentDisposition.ATTACHMENT);
        getComponent().getRequestCycle().scheduleRequestHandlerAfterCurrent(handler);
    }

    private String getFileName() {
        return file.getName();
    }

    private IResourceStream getResourceStream() {
        return new FileResourceStream(new org.apache.wicket.util.file.File(file));
    }

    // Set file to download
    public void setFile(File file) {
        this.file = file;
    }
}

initiate gets called by this method

private AbstractButtonPanel createDownloadTemplateButton(String markupId) {
    return new ButtonPanel(markupId) {
        @Override
        protected void buttonClick(final AjaxRequestTarget target) {
            FormExceptionHandler<File> handler = dataHolder -> generateCsvTemplateFile();
            File templateDownload = handler.apply(new WicketExceptionDataholder(target, form, log));

                    fileDownloadBehaviour.setFile(file);
                    fileDownloadBehaviour.initiate(target);
        }

    }.withLinkTextKey("downloadTemplateButton.label");
}

ButtonPanel

    import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.markup.html.AjaxLink;

public abstract class ButtonPanel extends AbstractButtonPanel {

    public ButtonPanel(String markupId) {
        super(markupId);
    }

    @Override
    protected AjaxLink<Void> createAbstractLink(String markupId) {
        return new AjaxLink<>(markupId) {

            @Override
            public void onClick(AjaxRequestTarget target) {
                buttonClick(target);
            }
        };
    }


    protected abstract void buttonClick(AjaxRequestTarget target);

}

AbstractButtonPanel

import com.xyz.uof.frontend.web.bootstrap.css.BootstrapCssEnum;
import com.xyz.uof.frontend.web.bootstrap.css.BootstrapCssEnum.BUTTON;
import java.util.Optional;
import org.apache.commons.lang3.StringUtils;
import org.apache.wicket.Component;
import org.apache.wicket.behavior.AttributeAppender;
import org.apache.wicket.markup.html.WebComponent;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.link.AbstractLink;
import org.apache.wicket.markup.html.panel.Panel;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;


public abstract class AbstractButtonPanel extends Panel {

    /**
     * Change the button to be a link, btn-primary, or what else
     */
    private String buttonSizeClass = BUTTON.SIZE_SMALL;
    private String cssClass = BootstrapCssEnum.BUTTON.PRIMARY;
    private String linkTextKey;
    private String buttonName = "submitButton"; // default
    private IModel<Boolean> visibleModel = Model.of(true);
    private IModel<Boolean> enabledModel = Model.of(true);
    private String iconClass;
    private boolean spinnerActive = false;

    public AbstractButtonPanel(String markupId) {
        super(markupId);
        setOutputMarkupId(true);
    }

    @Override
    protected void onInitialize() {
        super.onInitialize();
        add(createLink("link"));
    }

    @Override
    protected void onConfigure() {
        super.onConfigure();
        setVisibilityAllowed(visibleModel.getObject());
        setEnabled(enabledModel.getObject());
    }

    private AbstractLink createLink(String markupId) {
        AbstractLink ajaxLink = createAbstractLink(markupId);
        ajaxLink.add(new AttributeAppender("class", buttonSizeClass + " " + cssClass, " "));
        ajaxLink.add(createSpinner("spinner", ajaxLink));
        ajaxLink.add(createLabel("label"));
        ajaxLink.add(createIcon("labelIcon"));
        ajaxLink.get("labelIcon").add(new AttributeAppender("class", iconClass, " "));
        return ajaxLink;
    }

    protected abstract AbstractLink createAbstractLink(String markupId);

    private Component createSpinner(String markupId, AbstractLink ajaxLink) {
        if (spinnerActive) {
            ajaxLink.add(new AttributeAppender("onClick", String.format("document.getElementById('%s').childNodes[1].className = 'spinner-grow spinner-grow-sm';", ajaxLink.getMarkupId())));
        }
        return new WebComponent(markupId) {
            @Override
            protected void onConfigure() {
                super.onConfigure();
                getParent().getId();

                setVisibilityAllowed(spinnerActive);
            }
        };
    }
    
    private Label createLabel(String markupId) {
        // Use this model to handle null linkTextKey.
        // Helps to have buttons with glyphicons only
        return new Label(markupId, () -> StringUtils.isNotBlank(linkTextKey) ? getString(linkTextKey) : "") {

            @Override
            protected void onConfigure() {
                super.onConfigure();
                setVisibilityAllowed(StringUtils.isNotBlank(linkTextKey));
            }

        };
    }

    private Component createIcon(String markupId) {
        return new WebComponent(markupId) {
            @Override
            protected void onConfigure() {
                super.onConfigure();
                setVisibilityAllowed(Optional.ofNullable(iconClass).isPresent());
            }
        };
    }

    public AbstractButtonPanel withButtonSizeClass(String buttonSizeClass) {
        this.buttonSizeClass = buttonSizeClass;
        return this;
    }

    public AbstractButtonPanel withCssClass(String cssClass) {
        this.cssClass = cssClass;
        return this;
    }

    // Important for Selenium
    public AbstractButtonPanel withName(String... buttonName) {
        StringBuilder sb = new StringBuilder();
        for (String currentSplit : buttonName) {
            sb.append(currentSplit);
        }
        sb.append(this.buttonName);
        this.buttonName = sb.toString();
        return this;
    }

    public AbstractButtonPanel withLinkTextKey(String linkTextKey) {
        this.linkTextKey = linkTextKey;
        return this;
    }

    public AbstractButtonPanel withVisibleModel(IModel<Boolean> visibleModel) {
        this.visibleModel = visibleModel;
        return this;
    }

    public AbstractButtonPanel withEnabledModel(IModel<Boolean> enabbledModel) {
        this.enabledModel = enabbledModel;
        return this;
    }

    public AbstractButtonPanel withIconName(String iconName) {
        this.iconClass = iconName;
        return this;
    }

    public AbstractButtonPanel withSpinner(boolean spinnerActive) {
        this.spinnerActive = spinnerActive;
        return this;
    }

}

Solution

  • So, the problem was this part

        target.appendJavaScript("setTimeout(\"window.location.href='" + url + "'\", 100);");
    

    Javascript was blocked and I needed to unblock it in another class that extends WicketBootSecuredWebApplication like this

    @Override
    protected void init() {
    super.init();
    getCspSettings().blocking().disabled();
    }
    }