This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch raw in repository https://gitbox.apache.org/repos/asf/camel.git
commit 27db86c407b00dc0f04cff914d4101838118d8ac Author: Claus Ibsen <[email protected]> AuthorDate: Wed Aug 21 10:17:58 2024 +0200 CAMEL-21099: source dev console - Make it easier to download as files using raw mode --- .../platform/http/main/MainHttpServer.java | 69 +++++++++++++------ .../java/org/apache/camel/console/DevConsole.java | 3 +- .../camel/impl/console/SourceDevConsole.java | 80 ++++++++++++++++++++-- .../camel/support/console/AbstractDevConsole.java | 16 ++++- 4 files changed, 141 insertions(+), 27 deletions(-) diff --git a/components/camel-platform-http-main/src/main/java/org/apache/camel/component/platform/http/main/MainHttpServer.java b/components/camel-platform-http-main/src/main/java/org/apache/camel/component/platform/http/main/MainHttpServer.java index 6482f0abda3..fb7d8c90bd2 100644 --- a/components/camel-platform-http-main/src/main/java/org/apache/camel/component/platform/http/main/MainHttpServer.java +++ b/components/camel-platform-http-main/src/main/java/org/apache/camel/component/platform/http/main/MainHttpServer.java @@ -33,6 +33,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.TreeSet; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; import io.vertx.core.Handler; @@ -442,7 +443,7 @@ public class MainHttpServer extends ServiceSupport implements CamelContextAware, @Override public void handle(RoutingContext ctx) { - ctx.response().putHeader("content-type", "application/json"); + ctx.response().putHeader("Content-Type", "application/json"); JsonObject root = new JsonObject(); JsonObject jo = new JsonObject(); @@ -543,7 +544,7 @@ public class MainHttpServer extends ServiceSupport implements CamelContextAware, Handler<RoutingContext> handler = new Handler<RoutingContext>() { @Override public void handle(RoutingContext ctx) { - ctx.response().putHeader("content-type", "application/json"); + ctx.response().putHeader("Content-Type", "application/json"); HealthCheckRegistry registry = HealthCheckRegistry.get(camelContext); String level = ctx.request().getParam("exposureLevel"); @@ -740,10 +741,22 @@ public class MainHttpServer extends ServiceSupport implements CamelContextAware, devSub.method(HttpMethod.GET); devSub.produces("text/plain"); devSub.produces("application/json"); + devSub.produces("application/octet-stream"); Handler<RoutingContext> handler = new Handler<RoutingContext>() { @Override public void handle(RoutingContext ctx) { + if (!camelContext.isDevConsole()) { + ctx.response().putHeader("Content-Type", "text/plain"); + ctx.end("Developer Console is not enabled on CamelContext. Set camel.context.dev-console=true in application.properties"); + } + DevConsoleRegistry dcr = camelContext.getCamelContextExtension().getContextPlugin(DevConsoleRegistry.class); + if (dcr == null || !dcr.isEnabled()) { + ctx.response().putHeader("Content-Type", "text/plain"); + ctx.end("Developer Console is not enabled"); + return; + } + String acp = ctx.request().getHeader("Accept"); int pos1 = acp != null ? acp.indexOf("html") : Integer.MAX_VALUE; if (pos1 == -1) { @@ -753,20 +766,13 @@ public class MainHttpServer extends ServiceSupport implements CamelContextAware, if (pos2 == -1) { pos2 = Integer.MAX_VALUE; } - final boolean html = pos1 < pos2; - final boolean json = pos2 < pos1; - final DevConsole.MediaType mediaType = json ? DevConsole.MediaType.JSON : DevConsole.MediaType.TEXT; - - ctx.response().putHeader("content-type", "text/plain"); - - if (!camelContext.isDevConsole()) { - ctx.end("Developer Console is not enabled on CamelContext. Set camel.context.dev-console=true in application.properties"); - } - DevConsoleRegistry dcr = camelContext.getCamelContextExtension().getContextPlugin(DevConsoleRegistry.class); - if (dcr == null || !dcr.isEnabled()) { - ctx.end("Developer Console is not enabled"); - return; - } + // special for download mode where we want to make it easy from a web-browser + final boolean download = "true".equals(ctx.request().getParam("download")); + final boolean raw = download || acp != null && acp.contains("application/octet-stream"); + final boolean html = !raw && pos1 < pos2; + final boolean json = !raw && pos2 < pos1; + final DevConsole.MediaType mediaType + = raw ? DevConsole.MediaType.RAW : json ? DevConsole.MediaType.JSON : DevConsole.MediaType.TEXT; String path = StringHelper.after(ctx.request().path(), "/q/dev/"); String s = path; @@ -805,11 +811,13 @@ public class MainHttpServer extends ServiceSupport implements CamelContextAware, if (!sb.isEmpty()) { String out = sb.toString(); if (html) { - ctx.response().putHeader("content-type", "text/html"); + ctx.response().putHeader("Content-Type", "text/html"); + } else { + ctx.response().putHeader("Content-Type", "text/plain"); } ctx.end(out); } else if (!root.isEmpty()) { - ctx.response().putHeader("content-type", "application/json"); + ctx.response().putHeader("Content-Type", "application/json"); String out = root.toJson(); ctx.end(out); } else { @@ -821,6 +829,7 @@ public class MainHttpServer extends ServiceSupport implements CamelContextAware, params.put(Exchange.HTTP_PATH, path); StringBuilder sb = new StringBuilder(); JsonObject root = new JsonObject(); + final AtomicBoolean found = new AtomicBoolean(); // sort according to index by given id dcr.stream().sorted((o1, o2) -> { @@ -831,25 +840,45 @@ public class MainHttpServer extends ServiceSupport implements CamelContextAware, boolean include = "all".equals(id) || c.getId().equalsIgnoreCase(id); if (include && c.supportMediaType(mediaType)) { Object out = c.call(mediaType, params); + found.set(true); if (out != null && mediaType == DevConsole.MediaType.TEXT) { sb.append(c.getDisplayName()).append(":"); sb.append("\n\n"); sb.append(out); sb.append("\n\n"); + } else if (out != null && mediaType == DevConsole.MediaType.RAW) { + sb.append(out); } else if (out != null && mediaType == DevConsole.MediaType.JSON) { root.put(c.getId(), out); } } }); if (!sb.isEmpty()) { + if (raw) { + ctx.response().putHeader("Content-Type", "application/octet-stream"); + // in raw mode we may download files so put this into header information + String disposition = (String) params.get("Content-Disposition"); + if (disposition != null) { + ctx.response().putHeader("Content-Disposition", disposition); + } + } else { + ctx.response().putHeader("Content-Type", "text/plain"); + } String out = sb.toString(); ctx.end(out); } else if (!root.isEmpty()) { - ctx.response().putHeader("content-type", "application/json"); + // root is json object + ctx.response().putHeader("Content-Type", "application/json"); String out = root.toJson(); ctx.end(out); } else { - ctx.end("Developer Console not found: " + id); + if (!found.get()) { + ctx.response().putHeader("Content-Type", "text/plain"); + ctx.end("Developer Console not found: " + id); + } else { + ctx.response().setStatusCode(204); + ctx.end(); + } } } } diff --git a/core/camel-api/src/main/java/org/apache/camel/console/DevConsole.java b/core/camel-api/src/main/java/org/apache/camel/console/DevConsole.java index bdcdf069b6c..add78556a56 100644 --- a/core/camel-api/src/main/java/org/apache/camel/console/DevConsole.java +++ b/core/camel-api/src/main/java/org/apache/camel/console/DevConsole.java @@ -29,7 +29,8 @@ public interface DevConsole { enum MediaType { TEXT, - JSON + JSON, + RAW } /** diff --git a/core/camel-console/src/main/java/org/apache/camel/impl/console/SourceDevConsole.java b/core/camel-console/src/main/java/org/apache/camel/impl/console/SourceDevConsole.java index fd8aa2973ee..b02b232ce50 100644 --- a/core/camel-console/src/main/java/org/apache/camel/impl/console/SourceDevConsole.java +++ b/core/camel-console/src/main/java/org/apache/camel/impl/console/SourceDevConsole.java @@ -21,6 +21,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import org.apache.camel.Exchange; @@ -55,16 +57,32 @@ public class SourceDevConsole extends AbstractDevConsole { */ public static final String DUMP = "dump"; + /** + * Whether to make downloading the source file easier from a web browser + */ + public static final String DOWNLOAD = "download"; + public SourceDevConsole() { super("camel", "source", "Source", "Dump route source code"); } + @Override + public boolean supportMediaType(MediaType mediaType) { + // also supports raw + return true; + } + @Override protected String doCallText(Map<String, Object> options) { final StringBuilder sb = new StringBuilder(); - boolean dump = "true".equals(options.getOrDefault(DUMP, "true")); + boolean download = "true".equals(options.getOrDefault(DOWNLOAD, "false")); + if (download) { + // use raw mode instead + return doCallRaw(options); + } + boolean dump = "true".equals(options.getOrDefault(DUMP, "true")); Function<ManagedRouteMBean, Object> task = mrb -> { String loc = mrb.getSourceLocation(); if (loc != null) { @@ -73,11 +91,10 @@ public class SourceDevConsole extends AbstractDevConsole { try { Resource resource = PluginHelper.getResourceLoader(getCamelContext()).resolveResource(loc); if (resource != null) { - if (!sb.isEmpty()) { - sb.append("\n"); - } - if (dump) { + if (!sb.isEmpty()) { + sb.append("\n"); + } LineNumberReader reader = new LineNumberReader(resource.getReader()); int i = 0; String t; @@ -143,6 +160,59 @@ public class SourceDevConsole extends AbstractDevConsole { return root; } + @Override + protected String doCallRaw(Map<String, Object> options) { + final StringBuilder sb = new StringBuilder(); + + boolean dump = "true".equals(options.getOrDefault(DUMP, "true")); + final AtomicInteger counter = new AtomicInteger(); + final AtomicReference<String> name = new AtomicReference<>(); + + Function<ManagedRouteMBean, Object> task = mrb -> { + String loc = mrb.getSourceLocation(); + if (loc != null) { + loc = LoggerHelper.stripSourceLocationLineNumber(loc); + StringBuilder code = new StringBuilder(); + try { + String onlyName = LoggerHelper.sourceNameOnly(mrb.getSourceLocation()); + Resource resource = PluginHelper.getResourceLoader(getCamelContext()).resolveResource(loc); + if (resource != null) { + if (dump) { + // if we select only 1 file then remember the filename + if (counter.incrementAndGet() == 1) { + name.set(onlyName); + } else { + name.set(null); + } + if (!sb.isEmpty()) { + sb.append("\n"); + } + String text = IOHelper.loadText(resource.getInputStream()); + code.append(text); + } else { + // list of names + sb.append(onlyName); + } + } + } catch (Exception e) { + // ignore + } + if (!code.isEmpty()) { + sb.append(code); + } + } + sb.append("\n"); + return null; + }; + doCall(options, task); + + if (name.get() != null) { + options.put("Content-Disposition", String.format("attachment; filename=\"%s\"", name.get())); + } + + return sb.toString(); + } + protected void doCall(Map<String, Object> options, Function<ManagedRouteMBean, Object> task) { String path = (String) options.get(Exchange.HTTP_PATH); String subPath = path != null ? StringHelper.after(path, "/") : null; diff --git a/core/camel-support/src/main/java/org/apache/camel/support/console/AbstractDevConsole.java b/core/camel-support/src/main/java/org/apache/camel/support/console/AbstractDevConsole.java index c664c61d3b0..3d14a5050df 100644 --- a/core/camel-support/src/main/java/org/apache/camel/support/console/AbstractDevConsole.java +++ b/core/camel-support/src/main/java/org/apache/camel/support/console/AbstractDevConsole.java @@ -57,7 +57,8 @@ public abstract class AbstractDevConsole extends ServiceSupport implements DevCo @Override public boolean supportMediaType(MediaType mediaType) { - return true; + // text and json supported by default + return mediaType == MediaType.TEXT || mediaType == MediaType.JSON; } @Override @@ -105,6 +106,8 @@ public abstract class AbstractDevConsole extends ServiceSupport implements DevCo try { if (mediaType == MediaType.JSON) { return doCallJson(options); + } else if (mediaType == MediaType.RAW && supportMediaType(MediaType.RAW)) { + return doCallRaw(options); } else { return doCallText(options); } @@ -113,6 +116,17 @@ public abstract class AbstractDevConsole extends ServiceSupport implements DevCo } } + /** + * Invokes and gets the output from this console in raw format. + * + * The returned object can for example be binary data for file downloads + * + * @see DevConsole#call(MediaType, Map) + */ + protected Object doCallRaw(Map<String, Object> options) { + return null; + } + /** * Invokes and gets the output from this console in json format. *
