This is an automated email from the ASF dual-hosted git repository. ahuber pushed a commit to branch v4 in repository https://gitbox.apache.org/repos/asf/causeway.git
The following commit(s) were added to refs/heads/v4 by this push: new 0feaec87457 CAUSEWAY-3901: bit of refactoring; unable to fix download issue with long running blob action 0feaec87457 is described below commit 0feaec8745728a0e5aa78f2a2e17d34374a48dd2 Author: Andi Huber <ahu...@apache.org> AuthorDate: Fri Aug 29 13:49:49 2025 +0200 CAUSEWAY-3901: bit of refactoring; unable to fix download issue with long running blob action --- .../components/widgets/actionlink/ActionLink.java | 5 +- ...dHandlerFactory.java => LobRequestHandler.java} | 71 +++++++++++----------- .../causeway/viewer/wicket/ui/exec/Mediator.java | 35 +++++------ .../viewer/wicket/ui/exec/MediatorFactory.java | 10 +-- 4 files changed, 53 insertions(+), 68 deletions(-) diff --git a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/widgets/actionlink/ActionLink.java b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/widgets/actionlink/ActionLink.java index 25a6d1b7229..1d09eba3ade 100644 --- a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/widgets/actionlink/ActionLink.java +++ b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/widgets/actionlink/ActionLink.java @@ -232,8 +232,9 @@ private void startDialogWithParams(final AjaxRequestTarget target) { castTo(ActionPromptWithExtraContent.class, actionPrompt) .ifPresent(promptWithExtraContent->{ - BSGridPanelFactory.extraContentForMixin(promptWithExtraContent.getExtraContentId(), actionModel) - .ifPresent(gridPanel->promptWithExtraContent.setExtraContentPanel(gridPanel, target)); + BSGridPanelFactory + .extraContentForMixin(promptWithExtraContent.getExtraContentId(), actionModel) + .ifPresent(gridPanel->promptWithExtraContent.setExtraContentPanel(gridPanel, target)); }); } diff --git a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/exec/DownloadHandlerFactory.java b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/exec/LobRequestHandler.java similarity index 63% rename from viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/exec/DownloadHandlerFactory.java rename to viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/exec/LobRequestHandler.java index 9eeca477cc1..134c1862487 100644 --- a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/exec/DownloadHandlerFactory.java +++ b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/exec/LobRequestHandler.java @@ -21,8 +21,10 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.io.Serializable; import java.time.Duration; +import org.apache.wicket.request.IRequestCycle; import org.apache.wicket.request.IRequestHandler; import org.apache.wicket.request.handler.resource.ResourceStreamRequestHandler; import org.apache.wicket.request.resource.ContentDisposition; @@ -30,32 +32,50 @@ import org.apache.wicket.util.resource.IResourceStream; import org.apache.wicket.util.resource.ResourceStreamNotFoundException; import org.apache.wicket.util.resource.StringResourceStream; +import org.jspecify.annotations.Nullable; import org.apache.causeway.applib.value.Blob; import org.apache.causeway.applib.value.Clob; import org.apache.causeway.applib.value.NamedWithMimeType; +import org.apache.causeway.commons.internal.exceptions._Exceptions; import org.apache.causeway.core.metamodel.spec.feature.ObjectAction; -import lombok.experimental.UtilityClass; +public record LobRequestHandler( + NamedWithMimeType lob, + /** + * Duration for which the resource will be cached by the browser. + * Set to Duration.ZERO to disable browser caching. + */ + @Nullable Duration cacheDuration) implements IRequestHandler, Serializable { -@UtilityClass -final class DownloadHandlerFactory { - - public IRequestHandler downloadHandler( + public static LobRequestHandler downloadHandler( final ObjectAction action, final Object value) { - if(value instanceof Clob clob) { - return handlerFor(action, resourceStreamFor(clob), clob); - } - if(value instanceof Blob blob) { - return handlerFor(action, resourceStreamFor(blob), blob); + if(value instanceof NamedWithMimeType lob) { + return new LobRequestHandler(lob, action.getSemantics().isIdempotentOrCachable() + ? null + : Duration.ZERO); } return null; } + @Override + public void respond(IRequestCycle requestCycle) { + var handler = new ResourceStreamRequestHandler( + lob instanceof Blob blob + ? resourceStream(blob) + : lob instanceof Clob clob + ? resourceStream(clob) + : resourceStreamUnmatched(), + lob.name()); + handler.setContentDisposition(ContentDisposition.ATTACHMENT); + handler.setCacheDuration(cacheDuration); + handler.respond(requestCycle); + } + // -- HELPER - private IResourceStream resourceStreamFor(final Blob blob) { + private IResourceStream resourceStream(Blob blob) { final IResourceStream resourceStream = new AbstractResourceStream() { private static final long serialVersionUID = 1L; @Override public InputStream getInputStream() throws ResourceStreamNotFoundException { @@ -70,35 +90,12 @@ private IResourceStream resourceStreamFor(final Blob blob) { return resourceStream; } - private IResourceStream resourceStreamFor(final Clob clob) { + private IResourceStream resourceStream(Clob clob) { return new StringResourceStream(clob.chars(), clob.mimeType().toString()); } - private IRequestHandler handlerFor( - final ObjectAction action, - final IResourceStream resourceStream, - final NamedWithMimeType namedWithMimeType) { - var handler = - new ResourceStreamRequestHandler(resourceStream, namedWithMimeType.name()); - handler.setContentDisposition(ContentDisposition.ATTACHMENT); - - //CAUSEWAY-1619, prevent clients from caching the response content - return action.getSemantics().isIdempotentOrCachable() - ? handler - : enforceNoCacheOnClientSide(handler); - } - - // -- CLIENT SIDE CACHING ASPECTS ... - - private static IRequestHandler enforceNoCacheOnClientSide(final IRequestHandler downloadHandler){ - if(downloadHandler==null) { - return downloadHandler; - } - if(downloadHandler instanceof ResourceStreamRequestHandler) - ((ResourceStreamRequestHandler) downloadHandler) - .setCacheDuration(Duration.ZERO); - - return downloadHandler; + private IResourceStream resourceStreamUnmatched() { + throw _Exceptions.unmatchedCase(lob); } } diff --git a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/exec/Mediator.java b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/exec/Mediator.java index b708cd3f019..428181c499a 100644 --- a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/exec/Mediator.java +++ b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/exec/Mediator.java @@ -18,21 +18,17 @@ */ package org.apache.causeway.viewer.wicket.ui.exec; -import java.time.Duration; - import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.behavior.AbstractAjaxBehavior; import org.apache.wicket.request.IRequestHandler; import org.apache.wicket.request.Url; import org.apache.wicket.request.cycle.RequestCycle; -import org.apache.wicket.request.handler.resource.ResourceStreamRequestHandler; -import org.apache.wicket.request.resource.ContentDisposition; -import org.apache.wicket.util.resource.IResourceStream; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; import org.apache.causeway.applib.value.OpenUrlStrategy; import org.apache.causeway.commons.internal.exceptions._Exceptions; +import org.apache.causeway.commons.io.TextUtils; import org.apache.causeway.core.metamodel.context.MetaModelContext; import org.apache.causeway.core.metamodel.object.ManagedObject; import org.apache.causeway.viewer.wicket.model.models.ActionModel; @@ -129,20 +125,24 @@ void handle() { case SCHEDULE_HANDLER -> { var requestCycle = RequestCycle.get(); var ajaxTarget = requestCycle.find(AjaxRequestTarget.class).orElse(null); + final IRequestHandler requestHandler = handler(); if (ajaxTarget == null) { // non-Ajax request => just stream the Lob to the browser // or if this is a no-arg action, there also will be no parent for the component - requestCycle.scheduleRequestHandlerAfterCurrent(handler()); + requestCycle.scheduleRequestHandlerAfterCurrent(requestHandler); return; } // otherwise, // Ajax request => respond with a redirect to be able to stream the Lob to the client - final IRequestHandler requestHandler = handler(); - if(requestHandler instanceof ResourceStreamRequestHandler scheduledHandler) { - var streamingBehavior = new StreamAfterAjaxResponseBehavior(scheduledHandler); + if(requestHandler instanceof LobRequestHandler lobRequestHandler) { + var streamingBehavior = new StreamAfterAjaxResponseBehavior(lobRequestHandler); ajaxTarget.getPage().add(streamingBehavior); - scheduleJs(ajaxTarget, javascriptFor_sameWindow(streamingBehavior.getCallbackUrl()), 100); + + var relativeDownloadPageUri = TextUtils.cutter(streamingBehavior.getCallbackUrl().toString()) + .keepAfterLast("/") + .getValue(); + scheduleJs(ajaxTarget, javascriptFor_sameWindow(relativeDownloadPageUri), 10); } else if(requestHandler instanceof RedirectRequestHandlerWithOpenUrlStrategy redirectHandler) { var fullUrl = expanded(requestCycle, redirectHandler.getRedirectUrl()); var js = redirectHandler.getOpenUrlStrategy().isNewWindow() @@ -200,22 +200,15 @@ private static void scheduleJs(final AjaxRequestTarget target, final String js, private static class StreamAfterAjaxResponseBehavior extends AbstractAjaxBehavior { private static final long serialVersionUID = 1L; - private final String fileName; - private final IResourceStream resourceStream; - private final Duration cacheDuration; + private final LobRequestHandler lobRequestHandler; - public StreamAfterAjaxResponseBehavior(final ResourceStreamRequestHandler scheduledHandler) { - this.fileName = scheduledHandler.getFileName(); - this.resourceStream = scheduledHandler.getResourceStream(); - this.cacheDuration = scheduledHandler.getCacheDuration(); + StreamAfterAjaxResponseBehavior(final LobRequestHandler lobRequestHandler) { + this.lobRequestHandler = lobRequestHandler; } @Override public void onRequest() { - var handler = new ResourceStreamRequestHandler(resourceStream, fileName); - handler.setCacheDuration(cacheDuration); - handler.setContentDisposition(ContentDisposition.ATTACHMENT); var page = getComponent(); - page.getRequestCycle().scheduleRequestHandlerAfterCurrent(handler); + page.getRequestCycle().scheduleRequestHandlerAfterCurrent(lobRequestHandler); page.remove(this); } } diff --git a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/exec/MediatorFactory.java b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/exec/MediatorFactory.java index 71f1d451cbb..8de2add58a6 100644 --- a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/exec/MediatorFactory.java +++ b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/exec/MediatorFactory.java @@ -135,16 +135,10 @@ private Mediator actionResultResponse( var pageRedirectRequest = PageRedirectRequest.forPage(ValuePage.class, valuePage); return Mediator.toPage(pageRedirectRequest); } - case VALUE_BLOB: { - final Object value = resultAdapter.getPojo(); - IRequestHandler handler = - DownloadHandlerFactory.downloadHandler(actionModel.getAction(), value); - return Mediator.withHandler(handler); - } + case VALUE_BLOB: case VALUE_CLOB: { final Object value = resultAdapter.getPojo(); - IRequestHandler handler = - DownloadHandlerFactory.downloadHandler(actionModel.getAction(), value); + IRequestHandler handler = LobRequestHandler.downloadHandler(actionModel.getAction(), value); return Mediator.withHandler(handler); } case VALUE_LOCALRESPATH_AJAX: {