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