diff --git a/core/runtime/src/main/java/io/quarkus/runtime/annotations/JsonRpcDescription.java b/core/runtime/src/main/java/io/quarkus/runtime/annotations/JsonRpcDescription.java new file mode 100644 index 0000000000000..4e56abd205997 --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/annotations/JsonRpcDescription.java @@ -0,0 +1,24 @@ +package io.quarkus.runtime.annotations; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Adds metadata to a JsonRPC method to control its behavior and appearance. + */ +@Retention(RUNTIME) +@Target({ METHOD, PARAMETER }) +@Documented +public @interface JsonRpcDescription { + + /** + * @return the description text + */ + String value(); + +} \ No newline at end of file diff --git a/core/runtime/src/main/java/io/quarkus/runtime/annotations/JsonRpcUsage.java b/core/runtime/src/main/java/io/quarkus/runtime/annotations/JsonRpcUsage.java new file mode 100644 index 0000000000000..59a48e3061db8 --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/annotations/JsonRpcUsage.java @@ -0,0 +1,18 @@ +package io.quarkus.runtime.annotations; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Defines where the JsonRPC method should be available. + */ +@Retention(RUNTIME) +@Target({ METHOD }) +@Documented +public @interface JsonRpcUsage { + Usage[] value(); +} \ No newline at end of file diff --git a/core/runtime/src/main/java/io/quarkus/runtime/annotations/Usage.java b/core/runtime/src/main/java/io/quarkus/runtime/annotations/Usage.java new file mode 100644 index 0000000000000..2f995d41e776c --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/annotations/Usage.java @@ -0,0 +1,20 @@ +package io.quarkus.runtime.annotations; + +import java.util.EnumSet; + +public enum Usage { + DEV_UI, + DEV_MCP; + + public static EnumSet onlyDevUI() { + return EnumSet.of(Usage.DEV_UI); + } + + public static EnumSet onlyDevMCP() { + return EnumSet.of(Usage.DEV_MCP); + } + + public static EnumSet devUIandDevMCP() { + return EnumSet.of(Usage.DEV_UI, Usage.DEV_MCP); + } +} \ No newline at end of file diff --git a/extensions/devui/deployment-spi/src/main/java/io/quarkus/devui/spi/AbstractDevUIBuildItem.java b/extensions/devui/deployment-spi/src/main/java/io/quarkus/devui/spi/AbstractDevUIBuildItem.java index 617949f65e906..679801dca6f41 100644 --- a/extensions/devui/deployment-spi/src/main/java/io/quarkus/devui/spi/AbstractDevUIBuildItem.java +++ b/extensions/devui/deployment-spi/src/main/java/io/quarkus/devui/spi/AbstractDevUIBuildItem.java @@ -1,6 +1,6 @@ package io.quarkus.devui.spi; -import java.util.List; +import java.lang.invoke.MethodHandle; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; @@ -8,6 +8,7 @@ import io.quarkus.builder.item.MultiBuildItem; import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; import io.quarkus.deployment.util.ArtifactInfoUtil; +import io.quarkus.maven.dependency.ArtifactKey; /** * For All DEV UI Build Item, we need to distinguish between the extensions, and the internal usage of Dev UI @@ -16,30 +17,33 @@ public abstract class AbstractDevUIBuildItem extends MultiBuildItem { private final Class callerClass; private String extensionIdentifier = null; - + private ArtifactKey artifactKey; private static final String DOT = "."; - private final String customIdentifier; + private final boolean isInternal; public AbstractDevUIBuildItem() { this(null); } public AbstractDevUIBuildItem(String customIdentifier) { - this.customIdentifier = customIdentifier; - - if (this.customIdentifier == null) { + this.extensionIdentifier = customIdentifier; + isInternal = customIdentifier == null; + if (customIdentifier == null) { // Get the class that will be used to auto-detect the name StackWalker stackWalker = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE); - List stackFrames = stackWalker.walk(frames -> frames.collect(Collectors.toList())); + stackWalker.walk(frames -> frames.collect(Collectors.toList())); Optional stackFrame = stackWalker.walk(frames -> frames .filter(frame -> (!frame.getDeclaringClass().getPackageName().startsWith("io.quarkus.devui.spi") - && !frame.getDeclaringClass().getPackageName().startsWith("io.quarkus.devui.deployment"))) + && !frame.getDeclaringClass().getPackageName().startsWith("io.quarkus.devui.deployment") + && !frame.getDeclaringClass().equals(MethodHandle.class))) .findFirst()); if (stackFrame.isPresent()) { this.callerClass = stackFrame.get().getDeclaringClass(); + if (this.callerClass == null) + this.extensionIdentifier = DEV_UI; } else { throw new RuntimeException("Could not detect extension identifier automatically"); } @@ -48,26 +52,28 @@ public AbstractDevUIBuildItem(String customIdentifier) { } } - public String getExtensionPathName(CurateOutcomeBuildItem curateOutcomeBuildItem) { - if (this.customIdentifier != null) { - return customIdentifier; - } - if (this.callerClass == null) { - return DEV_UI; + public ArtifactKey getArtifactKey(CurateOutcomeBuildItem curateOutcomeBuildItem) { + if (this.artifactKey == null) { + if (callerClass != null) { + Map.Entry groupIdAndArtifactId = ArtifactInfoUtil.groupIdAndArtifactId(callerClass, + curateOutcomeBuildItem); + this.artifactKey = ArtifactKey.ga(groupIdAndArtifactId.getKey(), groupIdAndArtifactId.getValue()); + } } + return this.artifactKey; + } + public String getExtensionPathName(CurateOutcomeBuildItem curateOutcomeBuildItem) { if (this.extensionIdentifier == null) { - - Map.Entry groupIdAndArtifactId = ArtifactInfoUtil.groupIdAndArtifactId(callerClass, - curateOutcomeBuildItem); - this.extensionIdentifier = groupIdAndArtifactId.getKey() + DOT + groupIdAndArtifactId.getValue(); + ArtifactKey ak = getArtifactKey(curateOutcomeBuildItem); + this.extensionIdentifier = ak.getGroupId() + DOT + ak.getArtifactId(); } return this.extensionIdentifier; } public boolean isInternal() { - return this.customIdentifier != null; + return this.isInternal; } public static final String DEV_UI = "devui"; diff --git a/extensions/devui/deployment-spi/src/main/java/io/quarkus/devui/spi/DevUIContent.java b/extensions/devui/deployment-spi/src/main/java/io/quarkus/devui/spi/DevUIContent.java index b42542de52c2c..533c488be5847 100644 --- a/extensions/devui/deployment-spi/src/main/java/io/quarkus/devui/spi/DevUIContent.java +++ b/extensions/devui/deployment-spi/src/main/java/io/quarkus/devui/spi/DevUIContent.java @@ -10,11 +10,13 @@ public class DevUIContent { private final String fileName; private final byte[] template; private final Map data; + private final Map descriptions; private DevUIContent(DevUIContent.Builder builder) { this.fileName = builder.fileName; this.template = builder.template; this.data = builder.data; + this.descriptions = builder.descriptions; } public String getFileName() { @@ -29,6 +31,10 @@ public Map getData() { return data; } + public Map getDescriptions() { + return descriptions; + } + public static Builder builder() { return new Builder(); } @@ -37,6 +43,7 @@ public static class Builder { private String fileName; private byte[] template; private Map data; + private Map descriptions; private Builder() { this.data = new HashMap<>(); @@ -69,6 +76,11 @@ public Builder addData(String key, Object value) { return this; } + public Builder descriptions(Map descriptions) { + this.descriptions = descriptions; + return this; + } + public DevUIContent build() { if (fileName == null) { throw new RuntimeException( diff --git a/extensions/devui/deployment-spi/src/main/java/io/quarkus/devui/spi/buildtime/BuildTimeAction.java b/extensions/devui/deployment-spi/src/main/java/io/quarkus/devui/spi/buildtime/BuildTimeAction.java index 1ecd86f160f20..cc27f97d63527 100644 --- a/extensions/devui/deployment-spi/src/main/java/io/quarkus/devui/spi/buildtime/BuildTimeAction.java +++ b/extensions/devui/deployment-spi/src/main/java/io/quarkus/devui/spi/buildtime/BuildTimeAction.java @@ -10,6 +10,7 @@ * Define a action that can be executed against the deployment classpath in runtime * This means a call will still be make with Json-RPC to the backend, but fall through to this action */ +@Deprecated public class BuildTimeAction { private final String methodName; diff --git a/extensions/devui/deployment-spi/src/main/java/io/quarkus/devui/spi/buildtime/BuildTimeActionBuildItem.java b/extensions/devui/deployment-spi/src/main/java/io/quarkus/devui/spi/buildtime/BuildTimeActionBuildItem.java index c9dd802b105ac..34427c18c70f3 100644 --- a/extensions/devui/deployment-spi/src/main/java/io/quarkus/devui/spi/buildtime/BuildTimeActionBuildItem.java +++ b/extensions/devui/deployment-spi/src/main/java/io/quarkus/devui/spi/buildtime/BuildTimeActionBuildItem.java @@ -1,21 +1,30 @@ package io.quarkus.devui.spi.buildtime; import java.util.ArrayList; +import java.util.EnumSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.function.BiFunction; import java.util.function.Function; import io.quarkus.devui.spi.AbstractDevUIBuildItem; +import io.quarkus.devui.spi.buildtime.jsonrpc.AbstractJsonRpcMethod; +import io.quarkus.devui.spi.buildtime.jsonrpc.DeploymentJsonRpcMethod; +import io.quarkus.devui.spi.buildtime.jsonrpc.RecordedJsonRpcMethod; import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.annotations.Usage; /** * Holds any Build time actions for Dev UI the extension has */ public final class BuildTimeActionBuildItem extends AbstractDevUIBuildItem { - private final List actions = new ArrayList<>(); - private final List subscriptions = new ArrayList<>(); + private final List deploymentActions = new ArrayList<>(); + private final List deploymentSubscriptions = new ArrayList<>(); + + private final List recordedActions = new ArrayList<>(); + private final List recordedSubscriptions = new ArrayList<>(); public BuildTimeActionBuildItem() { super(); @@ -25,44 +34,221 @@ public BuildTimeActionBuildItem(String customIdentifier) { super(customIdentifier); } - private void addAction(BuildTimeAction buildTimeAction) { - this.actions.add(buildTimeAction); + public List getDeploymentActions() { + return this.deploymentActions; + } + + public List getRecordedActions() { + return this.recordedActions; + } + + public List getDeploymentSubscriptions() { + return deploymentSubscriptions; + } + + public List getRecordedSubscriptions() { + return recordedSubscriptions; + } + + public ActionBuilder actionBuilder() { + return new ActionBuilder(); + } + + public SubscriptionBuilder subscriptionBuilder() { + return new SubscriptionBuilder(); } + @Deprecated public void addAction(String methodName, Function, T> action) { - this.addAction(new BuildTimeAction(methodName, action)); + this.addAction(new DeploymentJsonRpcMethod(methodName, null, Usage.onlyDevUI(), action)); } + @Deprecated public void addAssistantAction(String methodName, BiFunction, T> action) { - this.addAction(new BuildTimeAction(methodName, action)); + this.addAction(new DeploymentJsonRpcMethod(methodName, null, Usage.onlyDevUI(), action)); } + @Deprecated public void addAction(String methodName, RuntimeValue runtimeValue) { - this.addAction(new BuildTimeAction(methodName, runtimeValue)); - } - - public List getActions() { - return actions; - } - - public void addSubscription(BuildTimeAction buildTimeAction) { - this.subscriptions.add(buildTimeAction); + this.addAction(new RecordedJsonRpcMethod(methodName, null, Usage.onlyDevUI(), runtimeValue)); } + @Deprecated public void addSubscription(String methodName, Function, T> action) { - this.addSubscription(new BuildTimeAction(methodName, action)); + this.addSubscription(new DeploymentJsonRpcMethod(methodName, null, Usage.onlyDevUI(), action)); } + @Deprecated public void addSubscription(String methodName, RuntimeValue runtimeValue) { - this.addSubscription(new BuildTimeAction(methodName, runtimeValue)); + this.addSubscription(new RecordedJsonRpcMethod(methodName, null, Usage.onlyDevUI(), runtimeValue)); + } + + private BuildTimeActionBuildItem addAction(DeploymentJsonRpcMethod deploymentJsonRpcMethod) { + this.deploymentActions.add(deploymentJsonRpcMethod); + return this; + } + + private BuildTimeActionBuildItem addAction(RecordedJsonRpcMethod recordedJsonRpcMethod) { + this.recordedActions.add(recordedJsonRpcMethod); + return this; + } + + private BuildTimeActionBuildItem addSubscription(DeploymentJsonRpcMethod deploymentJsonRpcMethod) { + this.deploymentSubscriptions.add(deploymentJsonRpcMethod); + return this; + } + + private BuildTimeActionBuildItem addSubscription(RecordedJsonRpcMethod recordedJsonRpcMethod) { + this.recordedSubscriptions.add(recordedJsonRpcMethod); + return this; + } + + public final class ActionBuilder { + private String methodName; + private String description; + private Map parameters = new LinkedHashMap<>(); + private EnumSet usage; + private Function, ?> function; + private BiFunction, ?> assistantFunction; + private RuntimeValue runtimeValue; + + public ActionBuilder methodName(String methodName) { + this.methodName = methodName; + return this; + } + + public ActionBuilder description(String description) { + this.description = description; + return this; + } + + public ActionBuilder parameter(String name, String description) { + return parameter(name, String.class, description); + } + + public ActionBuilder parameter(String name, Class type, String description) { + this.parameters.put(name, new AbstractJsonRpcMethod.Parameter(type, description)); + return this; + } + + public ActionBuilder usage(EnumSet usage) { + this.usage = usage; + return this; + } + + public ActionBuilder function(Function, T> function) { + if (this.runtimeValue != null || this.assistantFunction != null) + throw new IllegalStateException("Only one of runtimeValue, function or assistantFunction is allowed"); + this.function = function; + return this; + } + + public ActionBuilder assistantFunction(BiFunction, ?> assistantFunction) { + if (this.function != null || this.runtimeValue != null) + throw new IllegalStateException("Only one of runtimeValue, function or assistantFunction is allowed"); + this.assistantFunction = assistantFunction; + return this; + } + + public ActionBuilder runtime(RuntimeValue runtimeValue) { + if (this.function != null || this.assistantFunction != null) + throw new IllegalStateException("Only one of runtimeValue, function or assistantFunction is allowed"); + this.runtimeValue = runtimeValue; + return this; + } + + public BuildTimeActionBuildItem build() { + if (methodName == null || methodName.isBlank()) + throw new IllegalStateException("methodName must be provided"); + if (parameters.isEmpty()) + parameters = null; + if (function != null) { + return addAction( + new DeploymentJsonRpcMethod(methodName, description, parameters, autoUsage(usage, description), + function)); + } else if (runtimeValue != null) { + return addAction( + new RecordedJsonRpcMethod(methodName, description, autoUsage(usage, description), runtimeValue)); + } else if (assistantFunction != null) { + return addAction( + new DeploymentJsonRpcMethod(methodName, description, parameters, autoUsage(usage, description), + assistantFunction)); + } else { + throw new IllegalStateException("Either function, assistantFunction or runtimeValue must be provided"); + } + } + } + + public final class SubscriptionBuilder { + private String methodName; + private String description; + private Map parameters = new LinkedHashMap<>();; + private EnumSet usage; + private Function, ?> function; + private RuntimeValue runtimeValue; + + public SubscriptionBuilder methodName(String methodName) { + this.methodName = methodName; + return this; + } + + public SubscriptionBuilder description(String description) { + this.description = description; + return this; + } + + public SubscriptionBuilder parameter(String name, String description) { + return parameter(name, String.class, description); + } + + public SubscriptionBuilder parameter(String name, Class type, String description) { + this.parameters.put(name, new AbstractJsonRpcMethod.Parameter(type, description)); + return this; + } + + public SubscriptionBuilder usage(EnumSet usage) { + this.usage = usage; + return this; + } + + public SubscriptionBuilder function(Function, T> function) { + this.function = function; + return this; + } + + public SubscriptionBuilder runtime(RuntimeValue runtimeValue) { + this.runtimeValue = runtimeValue; + return this; + } + + public void build() { + if (methodName == null || methodName.isBlank()) + throw new IllegalStateException("methodName must be provided"); + if (parameters.isEmpty()) + parameters = null; + if (function != null) { + addSubscription(new DeploymentJsonRpcMethod(methodName, description, parameters, autoUsage(usage, description), + function)); + } else if (runtimeValue != null) { + addSubscription( + new RecordedJsonRpcMethod(methodName, description, autoUsage(usage, description), runtimeValue)); + } else { + throw new IllegalStateException("Either function or runtimeValue must be provided"); + } + } } - public List getSubscriptions() { - return subscriptions; + private EnumSet autoUsage(EnumSet usage, String description) { + if (usage == null && description == null) { + usage = Usage.onlyDevUI(); + } else if (usage == null) { + usage = Usage.devUIandDevMCP(); + } + return usage; } } diff --git a/extensions/devui/deployment-spi/src/main/java/io/quarkus/devui/spi/buildtime/BuildTimeData.java b/extensions/devui/deployment-spi/src/main/java/io/quarkus/devui/spi/buildtime/BuildTimeData.java new file mode 100644 index 0000000000000..6d5ceae38fef6 --- /dev/null +++ b/extensions/devui/deployment-spi/src/main/java/io/quarkus/devui/spi/buildtime/BuildTimeData.java @@ -0,0 +1,38 @@ +package io.quarkus.devui.spi.buildtime; + +/** + * Holds the actual data and optionally a description for Build Time Data + */ +public class BuildTimeData { + private Object content; + private String description; + + public BuildTimeData() { + + } + + public BuildTimeData(Object content) { + this(content, null); + } + + public BuildTimeData(Object content, String description) { + this.content = content; + this.description = description; + } + + public Object getContent() { + return content; + } + + public void setContent(Object content) { + this.content = content; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } +} \ No newline at end of file diff --git a/extensions/devui/deployment-spi/src/main/java/io/quarkus/devui/spi/buildtime/QuteTemplateBuildItem.java b/extensions/devui/deployment-spi/src/main/java/io/quarkus/devui/spi/buildtime/QuteTemplateBuildItem.java index 7148cc43e2f98..277acb7024d7f 100644 --- a/extensions/devui/deployment-spi/src/main/java/io/quarkus/devui/spi/buildtime/QuteTemplateBuildItem.java +++ b/extensions/devui/deployment-spi/src/main/java/io/quarkus/devui/spi/buildtime/QuteTemplateBuildItem.java @@ -33,22 +33,24 @@ public List getTemplateDatas() { } public void add(String templatename, Map data) { - templateDatas.add(new TemplateData(templatename, templatename, data)); // By default the template is used for only one file. + templateDatas.add(new TemplateData(templatename, templatename, data, Map.of())); // By default the template is used for only one file. } - public void add(String templatename, String fileName, Map data) { - templateDatas.add(new TemplateData(templatename, fileName, data)); + public void add(String templatename, String fileName, Map data, Map descriptions) { + templateDatas.add(new TemplateData(templatename, fileName, data, descriptions)); } public static class TemplateData { final String templateName; final String fileName; final Map data; + final Map descriptions; - private TemplateData(String templateName, String fileName, Map data) { + private TemplateData(String templateName, String fileName, Map data, Map descriptions) { this.templateName = templateName; this.fileName = fileName; this.data = data; + this.descriptions = descriptions; } public String getTemplateName() { @@ -62,5 +64,9 @@ public String getFileName() { public Map getData() { return data; } + + public Map getDescriptions() { + return descriptions; + } } } diff --git a/extensions/devui/deployment-spi/src/main/java/io/quarkus/devui/spi/buildtime/jsonrpc/AbstractJsonRpcMethod.java b/extensions/devui/deployment-spi/src/main/java/io/quarkus/devui/spi/buildtime/jsonrpc/AbstractJsonRpcMethod.java new file mode 100644 index 0000000000000..bc839704473a9 --- /dev/null +++ b/extensions/devui/deployment-spi/src/main/java/io/quarkus/devui/spi/buildtime/jsonrpc/AbstractJsonRpcMethod.java @@ -0,0 +1,114 @@ +package io.quarkus.devui.spi.buildtime.jsonrpc; + +import java.util.EnumSet; +import java.util.LinkedHashMap; +import java.util.Map; + +import io.quarkus.runtime.annotations.Usage; + +/** + * Base class for json-rpc methods + */ +public abstract class AbstractJsonRpcMethod { + + private String methodName; + private String description; + private Map parameters; + private EnumSet usage; + + public AbstractJsonRpcMethod() { + } + + public AbstractJsonRpcMethod(String methodName, String description, + EnumSet usage) { + this.methodName = methodName; + this.description = description; + this.usage = usage; + } + + public AbstractJsonRpcMethod(String methodName, String description, Map parameters, + EnumSet usage) { + this.methodName = methodName; + this.description = description; + this.parameters = parameters; + this.usage = usage; + } + + public String getMethodName() { + return methodName; + } + + public void setMethodName(String methodName) { + this.methodName = methodName; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Map getParameters() { + return parameters; + } + + public void setParameters(Map parameters) { + this.parameters = parameters; + } + + public void addParameter(String name, String description) { + if (this.parameters == null) + this.parameters = new LinkedHashMap<>(); + this.parameters.put(name, new Parameter(String.class, description)); + } + + public void addParameter(String name, Class type, String description) { + if (this.parameters == null) + this.parameters = new LinkedHashMap<>(); + this.parameters.put(name, new Parameter(type, description)); + } + + public boolean hasParameters() { + return this.parameters != null && !this.parameters.isEmpty(); + } + + public EnumSet getUsage() { + return usage; + } + + public void setUsage(EnumSet usage) { + this.usage = usage; + } + + public static class Parameter { + private Class type; + private String description; + + public Parameter() { + + } + + public Parameter(Class type, String description) { + this.type = type; + this.description = description; + } + + public Class getType() { + return type; + } + + public void setType(Class type) { + this.type = type; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + } +} \ No newline at end of file diff --git a/extensions/devui/deployment-spi/src/main/java/io/quarkus/devui/spi/buildtime/jsonrpc/DeploymentJsonRpcMethod.java b/extensions/devui/deployment-spi/src/main/java/io/quarkus/devui/spi/buildtime/jsonrpc/DeploymentJsonRpcMethod.java new file mode 100644 index 0000000000000..582a201fd8dba --- /dev/null +++ b/extensions/devui/deployment-spi/src/main/java/io/quarkus/devui/spi/buildtime/jsonrpc/DeploymentJsonRpcMethod.java @@ -0,0 +1,78 @@ +package io.quarkus.devui.spi.buildtime.jsonrpc; + +import java.util.EnumSet; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Function; + +import io.quarkus.runtime.annotations.Usage; + +/** + * Deployment json-rpc methods. Here we need to know the function to call + */ +public final class DeploymentJsonRpcMethod extends AbstractJsonRpcMethod { + + private Function, ?> action; + private BiFunction, ?> assistantAction; + + public DeploymentJsonRpcMethod() { + } + + public DeploymentJsonRpcMethod(String methodName, + String description, + EnumSet usage, + Function, ?> action) { + super(methodName, description, usage); + this.action = action; + } + + public DeploymentJsonRpcMethod(String methodName, + String description, + Map parameters, + EnumSet usage, + Function, ?> action) { + super(methodName, description, parameters, usage); + this.action = action; + } + + public DeploymentJsonRpcMethod(String methodName, + String description, + EnumSet usage, + BiFunction, ?> assistantAction) { + super(methodName, description, usage); + this.assistantAction = assistantAction; + } + + public DeploymentJsonRpcMethod(String methodName, + String description, + Map parameters, + EnumSet usage, + BiFunction, ?> assistantAction) { + super(methodName, description, parameters, usage); + this.assistantAction = assistantAction; + } + + public Function, ?> getAction() { + return action; + } + + public void setAction(Function, ?> action) { + this.action = action; + } + + public boolean hasAction() { + return this.action != null; + } + + public BiFunction, ?> getAssistantAction() { + return assistantAction; + } + + public void setAssistantAction(BiFunction, ?> assistantAction) { + this.assistantAction = assistantAction; + } + + public boolean hasAssistantAction() { + return this.assistantAction != null; + } +} diff --git a/extensions/devui/deployment-spi/src/main/java/io/quarkus/devui/spi/buildtime/jsonrpc/RecordedJsonRpcMethod.java b/extensions/devui/deployment-spi/src/main/java/io/quarkus/devui/spi/buildtime/jsonrpc/RecordedJsonRpcMethod.java new file mode 100644 index 0000000000000..2e891694da8a8 --- /dev/null +++ b/extensions/devui/deployment-spi/src/main/java/io/quarkus/devui/spi/buildtime/jsonrpc/RecordedJsonRpcMethod.java @@ -0,0 +1,33 @@ +package io.quarkus.devui.spi.buildtime.jsonrpc; + +import java.util.EnumSet; + +import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.annotations.Usage; + +/** + * Recorded json-rpc methods. Here we need to know the recorded value + */ +public final class RecordedJsonRpcMethod extends AbstractJsonRpcMethod { + + private RuntimeValue runtimeValue; + + public RecordedJsonRpcMethod() { + } + + public RecordedJsonRpcMethod(String methodName, + String description, + EnumSet usage, + RuntimeValue runtimeValue) { + super(methodName, description, usage); + this.runtimeValue = runtimeValue; + } + + public RuntimeValue getRuntimeValue() { + return runtimeValue; + } + + public void setRuntimeValue(RuntimeValue runtimeValue) { + this.runtimeValue = runtimeValue; + } +} \ No newline at end of file diff --git a/extensions/devui/deployment-spi/src/main/java/io/quarkus/devui/spi/buildtime/jsonrpc/RuntimeJsonRpcMethod.java b/extensions/devui/deployment-spi/src/main/java/io/quarkus/devui/spi/buildtime/jsonrpc/RuntimeJsonRpcMethod.java new file mode 100644 index 0000000000000..c81ce910fcaf4 --- /dev/null +++ b/extensions/devui/deployment-spi/src/main/java/io/quarkus/devui/spi/buildtime/jsonrpc/RuntimeJsonRpcMethod.java @@ -0,0 +1,57 @@ +package io.quarkus.devui.spi.buildtime.jsonrpc; + +import java.util.EnumSet; +import java.util.Map; + +import io.quarkus.runtime.annotations.Usage; + +/** + * Runtime json-rpc methods. Here we need to know the CDI bean to call + */ +public final class RuntimeJsonRpcMethod extends AbstractJsonRpcMethod { + + private Class bean; + private boolean blocking; + private boolean nonBlocking; + + public RuntimeJsonRpcMethod() { + super(); + } + + public RuntimeJsonRpcMethod(String methodName, + String description, + Map parameters, + EnumSet usage, + Class bean, + boolean blocking, + boolean nonBlocking) { + super(methodName, description, parameters, usage); + this.bean = bean; + this.blocking = blocking; + this.nonBlocking = nonBlocking; + } + + public Class getBean() { + return bean; + } + + public void setBean(Class bean) { + this.bean = bean; + } + + public boolean isExplicitlyBlocking() { + return blocking; + } + + public void setExplicitlyBlocking(boolean blocking) { + this.blocking = blocking; + } + + public boolean isExplicitlyNonBlocking() { + return nonBlocking; + } + + public void setExplicitlyNonBlocking(boolean nonBlocking) { + this.nonBlocking = nonBlocking; + } +} \ No newline at end of file diff --git a/extensions/devui/deployment-spi/src/main/java/io/quarkus/devui/spi/page/AbstractPageBuildItem.java b/extensions/devui/deployment-spi/src/main/java/io/quarkus/devui/spi/page/AbstractPageBuildItem.java index 4412614f191ac..ef23914bd475c 100644 --- a/extensions/devui/deployment-spi/src/main/java/io/quarkus/devui/spi/page/AbstractPageBuildItem.java +++ b/extensions/devui/deployment-spi/src/main/java/io/quarkus/devui/spi/page/AbstractPageBuildItem.java @@ -7,13 +7,14 @@ import java.util.Map; import io.quarkus.devui.spi.AbstractDevUIBuildItem; +import io.quarkus.devui.spi.buildtime.BuildTimeData; /** * Any of card, menu or footer pages */ public abstract class AbstractPageBuildItem extends AbstractDevUIBuildItem { - protected final Map buildTimeData; + protected final Map buildTimeData; protected final List pageBuilders; protected String headlessComponentLink = null; @@ -54,10 +55,14 @@ public boolean hasPages() { } public void addBuildTimeData(String fieldName, Object fieldData) { - this.buildTimeData.put(fieldName, fieldData); + this.addBuildTimeData(fieldName, fieldData, null); } - public Map getBuildTimeData() { + public void addBuildTimeData(String fieldName, Object fieldData, String description) { + this.buildTimeData.put(fieldName, new BuildTimeData(fieldData, description)); + } + + public Map getBuildTimeData() { return this.buildTimeData; } diff --git a/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/BuildTimeConstBuildItem.java b/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/BuildTimeConstBuildItem.java index eb12c3edbc508..9b350f7c09dcc 100644 --- a/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/BuildTimeConstBuildItem.java +++ b/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/BuildTimeConstBuildItem.java @@ -4,19 +4,20 @@ import java.util.Map; import io.quarkus.devui.spi.AbstractDevUIBuildItem; +import io.quarkus.devui.spi.buildtime.BuildTimeData; /** * Write javascript file containing const vars with build time data */ public final class BuildTimeConstBuildItem extends AbstractDevUIBuildItem { - private final Map buildTimeData; + private final Map buildTimeData; public BuildTimeConstBuildItem() { this(new HashMap<>()); } - public BuildTimeConstBuildItem(Map buildTimeData) { + public BuildTimeConstBuildItem(Map buildTimeData) { super(); this.buildTimeData = buildTimeData; } @@ -25,20 +26,24 @@ public BuildTimeConstBuildItem(String customIdentifier) { this(customIdentifier, new HashMap<>()); } - public BuildTimeConstBuildItem(String customIdentifier, Map buildTimeData) { + public BuildTimeConstBuildItem(String customIdentifier, Map buildTimeData) { super(customIdentifier); this.buildTimeData = buildTimeData; } public void addBuildTimeData(String fieldName, Object fieldData) { - this.buildTimeData.put(fieldName, fieldData); + this.addBuildTimeData(fieldName, fieldData, null); } - public void addAllBuildTimeData(Map buildTimeData) { + public void addBuildTimeData(String fieldName, Object fieldData, String description) { + this.buildTimeData.put(fieldName, new BuildTimeData(fieldData, description)); + } + + public void addAllBuildTimeData(Map buildTimeData) { this.buildTimeData.putAll(buildTimeData); } - public Map getBuildTimeData() { + public Map getBuildTimeData() { return this.buildTimeData; } diff --git a/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/BuildTimeContentProcessor.java b/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/BuildTimeContentProcessor.java index da4558003c0ec..091d7c247f532 100644 --- a/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/BuildTimeContentProcessor.java +++ b/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/BuildTimeContentProcessor.java @@ -65,10 +65,12 @@ import io.quarkus.devui.spi.AbstractDevUIBuildItem; import io.quarkus.devui.spi.Constants; import io.quarkus.devui.spi.DevUIContent; -import io.quarkus.devui.spi.buildtime.BuildTimeAction; import io.quarkus.devui.spi.buildtime.BuildTimeActionBuildItem; +import io.quarkus.devui.spi.buildtime.BuildTimeData; import io.quarkus.devui.spi.buildtime.QuteTemplateBuildItem; import io.quarkus.devui.spi.buildtime.StaticContentBuildItem; +import io.quarkus.devui.spi.buildtime.jsonrpc.DeploymentJsonRpcMethod; +import io.quarkus.devui.spi.buildtime.jsonrpc.RecordedJsonRpcMethod; import io.quarkus.devui.spi.page.AbstractPageBuildItem; import io.quarkus.devui.spi.page.CardPageBuildItem; import io.quarkus.devui.spi.page.FooterPageBuildItem; @@ -76,7 +78,6 @@ import io.quarkus.devui.spi.page.Page; import io.quarkus.devui.spi.page.PageBuilder; import io.quarkus.maven.dependency.ResolvedDependency; -import io.quarkus.runtime.RuntimeValue; import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; import io.vertx.core.json.jackson.DatabindCodec; @@ -87,6 +88,7 @@ public class BuildTimeContentProcessor { private static final Logger log = Logger.getLogger(BuildTimeContentProcessor.class); + private static final String UNDERSCORE = "_"; private static final String SLASH = "/"; private static final String BUILD_TIME_PATH = "dev-ui-templates/build-time"; private static final String ES_MODULE_SHIMS = "es-module-shims"; @@ -189,7 +191,7 @@ void mapPageBuildTimeData(List cards, for (CardPageBuildItem card : cards) { String extensionPathName = card.getExtensionPathName(curateOutcomeBuildItem); - Map buildTimeData = getBuildTimeDataForCard(curateOutcomeBuildItem, card); + Map buildTimeData = getBuildTimeDataForCard(curateOutcomeBuildItem, card); if (!buildTimeData.isEmpty()) { buildTimeConstProducer.produce( new BuildTimeConstBuildItem(extensionPathName, buildTimeData)); @@ -197,7 +199,7 @@ void mapPageBuildTimeData(List cards, } for (MenuPageBuildItem menu : menus) { String extensionPathName = menu.getExtensionPathName(curateOutcomeBuildItem); - Map buildTimeData = getBuildTimeDataForPage(menu); + Map buildTimeData = getBuildTimeDataForPage(menu); if (!buildTimeData.isEmpty()) { buildTimeConstProducer.produce( new BuildTimeConstBuildItem(extensionPathName, buildTimeData)); @@ -205,7 +207,7 @@ void mapPageBuildTimeData(List cards, } for (FooterPageBuildItem footer : footers) { String extensionPathName = footer.getExtensionPathName(curateOutcomeBuildItem); - Map buildTimeData = getBuildTimeDataForPage(footer); + Map buildTimeData = getBuildTimeDataForPage(footer); if (!buildTimeData.isEmpty()) { buildTimeConstProducer.produce( new BuildTimeConstBuildItem(extensionPathName, buildTimeData)); @@ -221,53 +223,66 @@ DeploymentMethodBuildItem mapDeploymentMethods( final boolean assistantIsAvailable = capabilities.isPresent(Capability.ASSISTANT); - List methodNames = new ArrayList<>(); - List subscriptionNames = new ArrayList<>(); - Map recordedValues = new HashMap<>(); + Map methods = new HashMap<>(); + Map subscriptions = new HashMap<>(); + Map recordedMethods = new HashMap<>(); + Map recordedSubscriptions = new HashMap<>(); + for (BuildTimeActionBuildItem actions : buildTimeActions) { String extensionPathName = actions.getExtensionPathName(curateOutcomeBuildItem); - for (BuildTimeAction bta : actions.getActions()) { - String fullName = extensionPathName + "." + bta.getMethodName(); - if (bta.hasRuntimeValue()) { - recordedValues.put(fullName, bta.getRuntimeValue()); - methodNames.add(fullName); - } else if (bta.hasAction()) { - DevConsoleManager.register(fullName, bta.getAction()); - methodNames.add(fullName); - } else if (bta.hasAssistantAction() && assistantIsAvailable) { - DevConsoleManager.register(fullName, bta.getAssistantAction()); - methodNames.add(fullName); + + // Build time methods + for (DeploymentJsonRpcMethod deploymentJsonRpcMethod : actions.getDeploymentActions()) { + String fullName = extensionPathName + UNDERSCORE + deploymentJsonRpcMethod.getMethodName(); + if (deploymentJsonRpcMethod.hasAction()) { + DevConsoleManager.register(fullName, deploymentJsonRpcMethod.getAction()); + } else if (deploymentJsonRpcMethod.hasAssistantAction()) { + DevConsoleManager.register(fullName, deploymentJsonRpcMethod.getAssistantAction()); } + deploymentJsonRpcMethod.setMethodName(fullName); + methods.put(fullName, deploymentJsonRpcMethod); + } + + // Build time recorded values + for (RecordedJsonRpcMethod recordedJsonRpcMethod : actions.getRecordedActions()) { + String fullName = extensionPathName + UNDERSCORE + recordedJsonRpcMethod.getMethodName(); + recordedJsonRpcMethod.setMethodName(fullName); + recordedMethods.put(fullName, recordedJsonRpcMethod); } - for (BuildTimeAction bts : actions.getSubscriptions()) { - String fullName = extensionPathName + "." + bts.getMethodName(); - if (bts.hasRuntimeValue()) { - recordedValues.put(fullName, bts.getRuntimeValue()); - subscriptionNames.add(fullName); - } else if (bts.hasAction()) { - DevConsoleManager.register(fullName, bts.getAction()); - subscriptionNames.add(fullName); - } else if (bts.hasAssistantAction() && assistantIsAvailable) { - DevConsoleManager.register(fullName, bts.getAssistantAction()); - subscriptionNames.add(fullName); + + // Build time subscriptions + for (DeploymentJsonRpcMethod deploymentJsonRpcSubscription : actions.getDeploymentSubscriptions()) { + String fullName = extensionPathName + UNDERSCORE + deploymentJsonRpcSubscription.getMethodName(); + if (deploymentJsonRpcSubscription.hasAction()) { + DevConsoleManager.register(fullName, deploymentJsonRpcSubscription.getAction()); + } else if (deploymentJsonRpcSubscription.hasAssistantAction()) { + DevConsoleManager.register(fullName, deploymentJsonRpcSubscription.getAssistantAction()); } + deploymentJsonRpcSubscription.setMethodName(fullName); + subscriptions.put(fullName, deploymentJsonRpcSubscription); + } + // Build time recorded subscription + for (RecordedJsonRpcMethod recordedJsonRpcSubscription : actions.getRecordedSubscriptions()) { + String fullName = extensionPathName + UNDERSCORE + recordedJsonRpcSubscription.getMethodName(); + recordedJsonRpcSubscription.setMethodName(fullName); + recordedSubscriptions.put(fullName, recordedJsonRpcSubscription); } } - return new DeploymentMethodBuildItem(methodNames, subscriptionNames, recordedValues); + return new DeploymentMethodBuildItem(methods, subscriptions, recordedMethods, recordedSubscriptions); } - private Map getBuildTimeDataForPage(AbstractPageBuildItem pageBuildItem) { - Map m = new HashMap<>(); + private Map getBuildTimeDataForPage(AbstractPageBuildItem pageBuildItem) { + Map m = new HashMap<>(); if (pageBuildItem.hasBuildTimeData()) { m.putAll(pageBuildItem.getBuildTimeData()); } return m; } - private Map getBuildTimeDataForCard(CurateOutcomeBuildItem curateOutcomeBuildItem, + private Map getBuildTimeDataForCard(CurateOutcomeBuildItem curateOutcomeBuildItem, CardPageBuildItem pageBuildItem) { - Map m = getBuildTimeDataForPage(pageBuildItem); + Map m = getBuildTimeDataForPage(pageBuildItem); if (pageBuildItem.getOptionalCard().isPresent()) { // Make the pages available for the custom card @@ -280,7 +295,7 @@ private Map getBuildTimeDataForCard(CurateOutcomeBuildItem curat pages.add(pageBuilder.build()); } - m.put("pages", pages); + m.put("pages", new BuildTimeData(pages)); } return m; } @@ -311,13 +326,19 @@ void createBuildTimeConstJsTemplate(DevUIConfig config, InternalImportMapBuildItem internalImportMapBuildItem = new InternalImportMapBuildItem(); var mapper = DatabindCodec.mapper().writerWithDefaultPrettyPrinter(); + Map descriptions = new HashMap<>(); for (BuildTimeConstBuildItem buildTimeConstBuildItem : buildTimeConstBuildItems) { Map data = new HashMap<>(); if (buildTimeConstBuildItem.hasBuildTimeData()) { - for (Map.Entry pageData : buildTimeConstBuildItem.getBuildTimeData().entrySet()) { + for (Map.Entry pageData : buildTimeConstBuildItem.getBuildTimeData().entrySet()) { try { + String ns = buildTimeConstBuildItem.getExtensionPathName(curateOutcomeBuildItem); String key = pageData.getKey(); - String value = mapper.writeValueAsString(pageData.getValue()); + String value = mapper.writeValueAsString(pageData.getValue().getContent()); + String description = pageData.getValue().getDescription(); + if (description != null) { + descriptions.put(ns + "/" + key, description); + } data.put(key, value); } catch (JsonProcessingException ex) { log.error("Could not create Json Data for Dev UI page", ex); @@ -330,7 +351,7 @@ void createBuildTimeConstJsTemplate(DevUIConfig config, String ref = buildTimeConstBuildItem.getExtensionPathName(curateOutcomeBuildItem) + "-data"; String file = ref + ".js"; - quteTemplateBuildItem.add("build-time-data.js", file, qutedata); + quteTemplateBuildItem.add("build-time-data.js", file, qutedata, descriptions); internalImportMapBuildItem.add(ref, contextRoot + file); } } @@ -433,6 +454,7 @@ void loadAllBuildTimeTemplates(BuildProducer buildTimeCo String templateName = e.getTemplateName(); // Relative to BUILD_TIME_PATH Map data = e.getData(); + Map descriptions = e.getDescriptions(); String resourceName = BUILD_TIME_PATH + SLASH + templateName; String fileName = e.getFileName(); // TODO: What if we find more than one ? @@ -444,6 +466,7 @@ void loadAllBuildTimeTemplates(BuildProducer buildTimeCo .fileName(fileName) .template(templateContent) .addData(data) + .descriptions(descriptions) .build(); contentPerExtension.add(content); } @@ -564,7 +587,8 @@ private void addFooterTabBuildTimeData(BuildTimeConstBuildItem internalBuildTime .componentLink("qwc-test-log.js").build(); footerTabs.add(testLog); - // This is only needed when extension developers work on an extension, so we only included it if you build from source. + // This is only needed when extension developers work on an extension, so we only included it if you build from source, + // or in the case of non-core extensions, extension developers can set a config flag if (Version.getVersion().equalsIgnoreCase("999-SNAPSHOT") || devUIConfig.showJsonRpcLog()) { Page devUiLog = Page.webComponentPageBuilder().internal() .namespace("devui-jsonrpcstream") diff --git a/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/DeploymentMethodBuildItem.java b/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/DeploymentMethodBuildItem.java index b394b54f16929..04a44eaa54c39 100644 --- a/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/DeploymentMethodBuildItem.java +++ b/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/DeploymentMethodBuildItem.java @@ -1,28 +1,33 @@ package io.quarkus.devui.deployment; -import java.util.List; import java.util.Map; import io.quarkus.builder.item.SimpleBuildItem; -import io.quarkus.runtime.RuntimeValue; +import io.quarkus.devui.spi.buildtime.jsonrpc.DeploymentJsonRpcMethod; +import io.quarkus.devui.spi.buildtime.jsonrpc.RecordedJsonRpcMethod; /** * Hold add discovered build time methods that can be executed via json-rpc */ public final class DeploymentMethodBuildItem extends SimpleBuildItem { - private final List methods; - private final List subscriptions; - private final Map recordedValues; + private final Map methods; + private final Map subscriptions; + private final Map recordedMethods; + private final Map recordedSubscriptions; - public DeploymentMethodBuildItem(List methods, List subscriptions, - Map recordedValues) { + public DeploymentMethodBuildItem(Map methods, + Map subscriptions, + Map recordedMethods, Map recordedSubscriptions) { this.methods = methods; this.subscriptions = subscriptions; - this.recordedValues = recordedValues; + this.recordedMethods = recordedMethods; + this.recordedSubscriptions = recordedSubscriptions; } - public List getMethods() { + // Methods + + public Map getMethods() { return this.methods; } @@ -30,7 +35,9 @@ public boolean hasMethods() { return this.methods != null && !this.methods.isEmpty(); } - public List getSubscriptions() { + // Subscriptions + + public Map getSubscriptions() { return this.subscriptions; } @@ -38,11 +45,23 @@ public boolean hasSubscriptions() { return this.subscriptions != null && !this.subscriptions.isEmpty(); } - public Map getRecordedValues() { - return this.recordedValues; + // Recorded Methods + + public Map getRecordedMethods() { + return this.recordedMethods; + } + + public boolean hasRecordedMethods() { + return this.recordedMethods != null && !this.recordedMethods.isEmpty(); + } + + // Recorded Subscriptions + + public Map getRecordedSubscriptions() { + return this.recordedSubscriptions; } - public boolean hasRecordedValues() { - return this.recordedValues != null && !this.recordedValues.isEmpty(); + public boolean hasRecordedSubscriptions() { + return this.recordedSubscriptions != null && !this.recordedSubscriptions.isEmpty(); } } diff --git a/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/DevMCPConfig.java b/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/DevMCPConfig.java new file mode 100644 index 0000000000000..b4597b666cd85 --- /dev/null +++ b/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/DevMCPConfig.java @@ -0,0 +1,17 @@ +package io.quarkus.devui.deployment; + +import io.quarkus.runtime.annotations.ConfigRoot; +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefault; + +@ConfigRoot +@ConfigMapping(prefix = "quarkus.dev-mcp") +public interface DevMCPConfig { + + /** + * Enable/disable the Dev MCP Server + */ + @WithDefault("false") + boolean enabled(); + +} diff --git a/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/DevUIProcessor.java b/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/DevUIProcessor.java index 15f4d3e1c0fe9..e5bac6b22ab82 100644 --- a/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/DevUIProcessor.java +++ b/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/DevUIProcessor.java @@ -15,6 +15,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; +import java.util.EnumSet; import java.util.Enumeration; import java.util.HashMap; import java.util.LinkedHashMap; @@ -22,10 +23,15 @@ import java.util.Map; import java.util.Properties; import java.util.Scanner; +import java.util.Set; +import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; import org.jboss.jandex.IndexView; @@ -42,6 +48,7 @@ import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.Capability; import io.quarkus.deployment.IsLocalDevelopment; +import io.quarkus.deployment.IsNormal; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.ExecutionTime; @@ -49,23 +56,29 @@ import io.quarkus.deployment.builditem.AdditionalIndexedClassesBuildItem; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.LaunchModeBuildItem; +import io.quarkus.deployment.builditem.RemovedResourceBuildItem; import io.quarkus.deployment.builditem.ShutdownContextBuildItem; import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; import io.quarkus.dev.console.DevConsoleManager; import io.quarkus.devui.deployment.extension.Codestart; import io.quarkus.devui.deployment.extension.Extension; import io.quarkus.devui.deployment.jsonrpc.DevUIDatabindCodec; +import io.quarkus.devui.runtime.DevUIBuildTimeStaticService; import io.quarkus.devui.runtime.DevUIRecorder; import io.quarkus.devui.runtime.VertxRouteInfoService; import io.quarkus.devui.runtime.comms.JsonRpcRouter; import io.quarkus.devui.runtime.jsonrpc.JsonRpcMethod; -import io.quarkus.devui.runtime.jsonrpc.JsonRpcMethodName; import io.quarkus.devui.runtime.jsonrpc.json.JsonMapper; import io.quarkus.devui.spi.DevUIContent; import io.quarkus.devui.spi.JsonRPCProvidersBuildItem; import io.quarkus.devui.spi.buildtime.BuildTimeActionBuildItem; +import io.quarkus.devui.spi.buildtime.BuildTimeData; import io.quarkus.devui.spi.buildtime.FooterLogBuildItem; import io.quarkus.devui.spi.buildtime.StaticContentBuildItem; +import io.quarkus.devui.spi.buildtime.jsonrpc.AbstractJsonRpcMethod; +import io.quarkus.devui.spi.buildtime.jsonrpc.DeploymentJsonRpcMethod; +import io.quarkus.devui.spi.buildtime.jsonrpc.RecordedJsonRpcMethod; +import io.quarkus.devui.spi.buildtime.jsonrpc.RuntimeJsonRpcMethod; import io.quarkus.devui.spi.page.CardPageBuildItem; import io.quarkus.devui.spi.page.FooterPageBuildItem; import io.quarkus.devui.spi.page.LibraryLink; @@ -80,6 +93,9 @@ import io.quarkus.maven.dependency.GACTV; import io.quarkus.maven.dependency.ResolvedDependency; import io.quarkus.qute.Qute; +import io.quarkus.runtime.annotations.JsonRpcDescription; +import io.quarkus.runtime.annotations.JsonRpcUsage; +import io.quarkus.runtime.annotations.Usage; import io.quarkus.runtime.util.ClassPathUtils; import io.quarkus.vertx.http.deployment.FilterBuildItem; import io.quarkus.vertx.http.deployment.HttpRootPathBuildItem; @@ -103,8 +119,8 @@ public class DevUIProcessor { private static final String FOOTER_LOG_NAMESPACE = "devui-footer-log"; private static final String DEVUI = "dev-ui"; + private static final String UNDERSCORE = "_"; private static final String SLASH = "/"; - private static final String DOT = "."; private static final String SLASH_ALL = SLASH + "*"; private static final String JSONRPC = "json-rpc-ws"; @@ -142,6 +158,7 @@ public class DevUIProcessor { @Record(ExecutionTime.STATIC_INIT) void registerDevUiHandlers( DevUIConfig devUIConfig, + BeanContainerBuildItem beanContainer, MvnpmBuildItem mvnpmBuildItem, List devUIRoutesBuildItems, List staticContentBuildItems, @@ -172,7 +189,7 @@ void registerDevUiHandlers( routeProducer.produce( nonApplicationRootPathBuildItem .routeBuilder().route(DEVUI + SLASH + JSONRPC) - .handler(recorder.communicationHandler()) + .handler(recorder.devUIWebSocketHandler()) .build()); // Static handler for components @@ -207,7 +224,7 @@ void registerDevUiHandlers( for (StaticContentBuildItem staticContentBuildItem : staticContentBuildItems) { Map urlAndPath = new HashMap<>(); - + Map descriptions = new HashMap<>(); List content = staticContentBuildItem.getContent(); for (DevUIContent c : content) { String parsedContent = Qute.fmt(new String(c.getTemplate()), c.getData()); @@ -216,8 +233,12 @@ void registerDevUiHandlers( Files.writeString(tempFile, parsedContent); urlAndPath.put(c.getFileName(), tempFile.toString()); + if (c.getDescriptions() != null && !c.getDescriptions().isEmpty()) { + descriptions.putAll(c.getDescriptions()); + } } - Handler buildTimeStaticHandler = recorder.buildTimeStaticHandler(basepath, urlAndPath); + Handler buildTimeStaticHandler = recorder.buildTimeStaticHandler(beanContainer.getValue(), basepath, + urlAndPath, descriptions); routeProducer.produce( nonApplicationRootPathBuildItem.routeBuilder().route(DEVUI + SLASH_ALL) @@ -258,16 +279,16 @@ void registerDevUiHandlers( // Redirect naked to welcome if there is no index.html if (!hasOwnIndexHtml()) { routeProducer.produce(httpRootPathBuildItem.routeBuilder() - .orderedRoute("/", Integer.MAX_VALUE) + .orderedRoute(SLASH, Integer.MAX_VALUE) .handler(recorder.redirect(contextRoot, "welcome")) .build()); } } private boolean hasOwnIndexHtml() { - ClassLoader tccl = Thread.currentThread().getContextClassLoader(); try { - Enumeration jarsWithIndexHtml = tccl.getResources("META-INF/resources/index.html"); + Enumeration jarsWithIndexHtml = Thread.currentThread().getContextClassLoader() + .getResources("META-INF/resources/index.html"); return jarsWithIndexHtml.hasMoreElements(); } catch (IOException ex) { throw new UncheckedIOException(ex); @@ -275,7 +296,8 @@ private boolean hasOwnIndexHtml() { } /** - * This makes sure the JsonRPC Classes for both the internal Dev UI and extensions is available as a bean and on the index. + * This makes sure the Runtime JsonRPC Classes for both the internal Dev UI and extensions is available as a bean and on the + * index. */ @BuildStep(onlyIf = IsLocalDevelopment.class) void additionalBean(BuildProducer additionalBeanProducer, @@ -283,7 +305,6 @@ void additionalBean(BuildProducer additionalBeanProduce List jsonRPCProvidersBuildItems) { additionalBeanProducer.produce(AdditionalBeanBuildItem.builder() - .addBeanClass(JsonRpcRouter.class) .addBeanClass(VertxRouteInfoService.class) .setUnremovable().build()); @@ -307,6 +328,10 @@ void additionalBean(BuildProducer additionalBeanProduce .setDefaultScope(BuiltinScope.APPLICATION.getName()) .setUnremovable().build()); + additionalBeanProducer.produce(AdditionalBeanBuildItem.builder() + .addBeanClass(DevUIBuildTimeStaticService.class) + .setDefaultScope(BuiltinScope.APPLICATION.getName()) + .setUnremovable().build()); } /** @@ -327,94 +352,124 @@ void findAllJsonRPCMethods(BuildProducer jsonRPC IndexView index = combinedIndexBuildItem.getIndex(); - Map> extensionMethodsMap = new HashMap<>(); // All methods so that we can build the reflection - - List requestResponseMethods = new ArrayList<>(); // All requestResponse methods for validation on the client side - List subscriptionMethods = new ArrayList<>(); // All subscription methods for validation on the client side + Map runtimeMethodsMap = new HashMap<>();// All methods to execute against the runtime classpath + Map runtimeSubscriptionsMap = new HashMap<>();// All subscriptions to execute against the runtime classpath // Let's use the Jandex index to find all methods for (JsonRPCProvidersBuildItem jsonRPCProvidersBuildItem : jsonRPCProvidersBuildItems) { Class clazz = jsonRPCProvidersBuildItem.getJsonRPCMethodProviderClass(); String extension = jsonRPCProvidersBuildItem.getExtensionPathName(curateOutcomeBuildItem); - Map jsonRpcMethods = new HashMap<>(); - if (extensionMethodsMap.containsKey(extension)) { - jsonRpcMethods = extensionMethodsMap.get(extension); - } ClassInfo classInfo = index.getClassByName(DotName.createSimple(clazz.getName())); + if (classInfo != null) {// skip if not found + for (MethodInfo method : classInfo.methods()) { + // Ignore constructor, Only allow public methods, Only allow method with response + if (!method.name().equals(CONSTRUCTOR) && Modifier.isPublic(method.flags()) + && method.returnType().kind() != Type.Kind.VOID) { + + String methodName = extension + UNDERSCORE + method.name(); + + Map parameters = new LinkedHashMap<>(); // Keep the order + for (int i = 0; i < method.parametersCount(); i++) { + Type parameterType = method.parameterType(i); + Class parameterClass = toClass(parameterType); + String parameterName = method.parameterName(i); + parameters.put(parameterName, new AbstractJsonRpcMethod.Parameter(parameterClass, null)); // TODO: description + } - List methods = classInfo.methods(); - - for (MethodInfo method : methods) { - if (!method.name().equals(CONSTRUCTOR)) { // Ignore constructor - if (Modifier.isPublic(method.flags())) { // Only allow public methods - if (method.returnType().kind() != Type.Kind.VOID) { // Only allow method with response + // Look for @JsonRpcUsage annotation + EnumSet usage = EnumSet.noneOf(Usage.class); + AnnotationInstance jsonRpcUsageAnnotation = method.annotation(DotName.createSimple(JsonRpcUsage.class)); + if (jsonRpcUsageAnnotation != null) { + AnnotationInstance[] usageArray = jsonRpcUsageAnnotation.value().asNestedArray(); - // Create list of available methods for the Javascript side. - if (method.returnType().name().equals(DotName.createSimple(Multi.class.getName()))) { - subscriptionMethods.add(extension + DOT + method.name()); - } else { - requestResponseMethods.add(extension + DOT + method.name()); + for (AnnotationInstance usageInstance : usageArray) { + String usageStr = usageInstance.value().asEnum(); + usage.add(Usage.valueOf(usageStr)); } + } - // Also create the map to pass to the runtime for the relection calls - JsonRpcMethodName jsonRpcMethodName = new JsonRpcMethodName(method.name()); - if (method.parametersCount() > 0) { - Map params = new LinkedHashMap<>(); // Keep the order - for (int i = 0; i < method.parametersCount(); i++) { - Type parameterType = method.parameterType(i); - Class parameterClass = toClass(parameterType); - String parameterName = method.parameterName(i); - params.put(parameterName, parameterClass); - } - JsonRpcMethod jsonRpcMethod = new JsonRpcMethod(clazz, method.name(), params); - jsonRpcMethod.setExplicitlyBlocking(method.hasAnnotation(Blocking.class)); - jsonRpcMethod - .setExplicitlyNonBlocking(method.hasAnnotation(NonBlocking.class)); - jsonRpcMethods.put(jsonRpcMethodName, jsonRpcMethod); - } else { - JsonRpcMethod jsonRpcMethod = new JsonRpcMethod(clazz, method.name(), null); - jsonRpcMethod.setExplicitlyBlocking(method.hasAnnotation(Blocking.class)); - jsonRpcMethod - .setExplicitlyNonBlocking(method.hasAnnotation(NonBlocking.class)); - jsonRpcMethods.put(jsonRpcMethodName, jsonRpcMethod); + // Look for @JsonRpcDescription annotation + String description = null; + AnnotationInstance jsonRpcDescriptionAnnotation = method + .annotation(DotName.createSimple(JsonRpcDescription.class)); + if (jsonRpcDescriptionAnnotation != null) { + AnnotationValue descriptionValue = jsonRpcDescriptionAnnotation.value(); + if (descriptionValue != null && !descriptionValue.asString().isBlank()) { + description = descriptionValue.asString(); + usage = Usage.devUIandDevMCP(); } + } else { + usage = Usage.onlyDevUI(); } + + RuntimeJsonRpcMethod runtimeJsonRpcMethod = new RuntimeJsonRpcMethod(methodName, description, + parameters, + usage, clazz, + method.hasAnnotation(Blocking.class), method.hasAnnotation(NonBlocking.class)); + + // Create list of available methods for the Javascript side. + if (method.returnType().name().equals(DotName.createSimple(Multi.class.getName()))) { + runtimeSubscriptionsMap.put(methodName, runtimeJsonRpcMethod); + } else { + runtimeMethodsMap.put(methodName, runtimeJsonRpcMethod); + } + } } } - - if (!jsonRpcMethods.isEmpty()) { - extensionMethodsMap.put(extension, jsonRpcMethods); - } } - if (deploymentMethodBuildItem.hasMethods()) { - requestResponseMethods.addAll(deploymentMethodBuildItem.getMethods()); - } + jsonRPCMethodsProvider.produce(new JsonRPCRuntimeMethodsBuildItem(runtimeMethodsMap, runtimeSubscriptionsMap)); - if (deploymentMethodBuildItem.hasSubscriptions()) { - subscriptionMethods.addAll(deploymentMethodBuildItem.getSubscriptions()); - } - - if (!extensionMethodsMap.isEmpty()) { - jsonRPCMethodsProvider.produce(new JsonRPCRuntimeMethodsBuildItem(extensionMethodsMap)); - } + // Get all names for UI validation + Set allMethodsNames = Stream + .> of(runtimeMethodsMap, deploymentMethodBuildItem.getMethods(), + deploymentMethodBuildItem.getRecordedMethods()) + .flatMap(m -> m.keySet().stream()) + .collect(Collectors.toSet()); + Set allSubscriptionNames = Stream + .> of(runtimeSubscriptionsMap, deploymentMethodBuildItem.getSubscriptions(), + deploymentMethodBuildItem.getRecordedSubscriptions()) + .flatMap(m -> m.keySet().stream()) + .collect(Collectors.toSet()); BuildTimeConstBuildItem methodInfo = new BuildTimeConstBuildItem("devui-jsonrpc"); - - if (!subscriptionMethods.isEmpty()) { - methodInfo.addBuildTimeData("jsonRPCSubscriptions", subscriptionMethods); + if (!allSubscriptionNames.isEmpty()) { + methodInfo.addBuildTimeData("jsonRPCSubscriptions", allSubscriptionNames); } - if (!requestResponseMethods.isEmpty()) { - methodInfo.addBuildTimeData("jsonRPCMethods", requestResponseMethods); + if (!allMethodsNames.isEmpty()) { + methodInfo.addBuildTimeData("jsonRPCMethods", allMethodsNames); } - buildTimeConstProducer.produce(methodInfo); } + @BuildStep(onlyIf = IsNormal.class) + void cleanProd(BuildProducer producer, + List jsonRPCProvidersBuildItems, + CurateOutcomeBuildItem curateOutcomeBuildItem) { + + List removedResourceBuildItems = new ArrayList<>(); + + for (JsonRPCProvidersBuildItem jsonRPCProvidersBuildItem : jsonRPCProvidersBuildItems) { + + ArtifactKey artifactKey = jsonRPCProvidersBuildItem.getArtifactKey(curateOutcomeBuildItem); + if (artifactKey != null) { + removedResourceBuildItems.add(new RemovedResourceBuildItem(artifactKey, + Set.of(jsonRPCProvidersBuildItem.getJsonRPCMethodProviderClass().getName()))); + } else if (jsonRPCProvidersBuildItem.getJsonRPCMethodProviderClass().getName() + .startsWith("io.quarkus.devui.runtime") + || jsonRPCProvidersBuildItem.getJsonRPCMethodProviderClass().getName() + .startsWith("io.quarkus.vertx.http.runtime")) { + removedResourceBuildItems.add(new RemovedResourceBuildItem(INTERNAL_KEY, + Set.of(jsonRPCProvidersBuildItem.getJsonRPCMethodProviderClass().getName()))); + } + } + producer.produce(removedResourceBuildItems); + } + @BuildStep(onlyIf = IsLocalDevelopment.class) @Record(ExecutionTime.RUNTIME_INIT) void createJsonRpcRouter(DevUIRecorder recorder, @@ -423,19 +478,73 @@ void createJsonRpcRouter(DevUIRecorder recorder, DeploymentMethodBuildItem deploymentMethodBuildItem) { if (jsonRPCMethodsBuildItem != null) { - Map> extensionMethodsMap = jsonRPCMethodsBuildItem - .getExtensionMethodsMap(); + Map runtimeMethodsMap = jsonRPCMethodsBuildItem.getRuntimeMethodsMap(); + Map runtimeSubscriptionsMap = jsonRPCMethodsBuildItem.getRuntimeSubscriptionsMap(); DevConsoleManager.setGlobal(DevUIRecorder.DEV_MANAGER_GLOBALS_JSON_MAPPER_FACTORY, JsonMapper.Factory.deploymentLinker().createLinkData(new DevUIDatabindCodec.Factory())); - recorder.createJsonRpcRouter(beanContainer.getValue(), extensionMethodsMap, deploymentMethodBuildItem.getMethods(), - deploymentMethodBuildItem.getSubscriptions(), deploymentMethodBuildItem.getRecordedValues()); + + recorder.createJsonRpcRouter(beanContainer.getValue(), + runtimeToJsonRpcMethods(runtimeMethodsMap), + runtimeToJsonRpcMethods(runtimeSubscriptionsMap), + deploymentToJsonRpcMethods(deploymentMethodBuildItem.getMethods()), + deploymentToJsonRpcMethods(deploymentMethodBuildItem.getSubscriptions()), + recordedToJsonRpcMethods(deploymentMethodBuildItem.getRecordedMethods()), + recordedToJsonRpcMethods(deploymentMethodBuildItem.getRecordedSubscriptions())); } } - /** - * This build all the pages for dev ui, based on the extension included - */ + private Map runtimeToJsonRpcMethods(Map m) { + return mapToJsonRpcMethods(m, this::runtimeToJsonRpcMethod); + } + + private Map deploymentToJsonRpcMethods(Map m) { + return mapToJsonRpcMethods(m, this::toJsonRpcMethod); + } + + private Map recordedToJsonRpcMethods(Map m) { + return mapToJsonRpcMethods(m, this::recordedToJsonRpcMethod); + } + + private Map mapToJsonRpcMethods( + Map input, + Function converter) { + + return input.entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, e -> converter.apply(e.getValue()))); + } + + private JsonRpcMethod runtimeToJsonRpcMethod(RuntimeJsonRpcMethod i) { + JsonRpcMethod o = toJsonRpcMethod(i); + + o.setBean(i.getBean()); + o.setIsExplicitlyBlocking(i.isExplicitlyBlocking()); + o.setIsExplicitlyNonBlocking(i.isExplicitlyNonBlocking()); + + return o; + } + + private JsonRpcMethod recordedToJsonRpcMethod(RecordedJsonRpcMethod i) { + JsonRpcMethod o = toJsonRpcMethod(i); + o.setRuntimeValue(i.getRuntimeValue()); + return o; + } + + private JsonRpcMethod toJsonRpcMethod(AbstractJsonRpcMethod i) { + JsonRpcMethod o = new JsonRpcMethod(); + + o.setMethodName(i.getMethodName()); + o.setDescription(i.getDescription()); + o.setUsage(List.copyOf(i.getUsage())); + if (i.hasParameters()) { + for (Map.Entry ip : i.getParameters().entrySet()) { + o.addParameter(ip.getKey(), ip.getValue().getType(), ip.getValue().getDescription()); + } + } + + return o; + } + @BuildStep(onlyIf = IsLocalDevelopment.class) void processFooterLogs(BuildProducer buildTimeActionProducer, BuildProducer footerPageProducer, @@ -450,27 +559,35 @@ void processFooterLogs(BuildProducer buildTimeActionPr BuildTimeActionBuildItem devServiceLogActions = new BuildTimeActionBuildItem(FOOTER_LOG_NAMESPACE); if (footerLogBuildItem.hasRuntimePublisher()) { - devServiceLogActions.addSubscription(name + "Log", footerLogBuildItem.getRuntimePublisher()); + devServiceLogActions.subscriptionBuilder() + .methodName(name + "Log") + .description("Streams the " + name + " log") + .runtime(footerLogBuildItem.getRuntimePublisher()) + .build(); } else { - devServiceLogActions.addSubscription(name + "Log", ignored -> { - try { - return footerLogBuildItem.getPublisher(); - } catch (Exception e) { - throw new RuntimeException(e); - } - }); + devServiceLogActions.subscriptionBuilder() + .methodName(name + "Log") + .description("Streams the " + name + " log") + .function(ignored -> { + try { + return footerLogBuildItem.getPublisher(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }) + .build(); } devServiceLogs.add(devServiceLogActions); // Create the Footer in the Dev UI - WebComponentPageBuilder log = Page.webComponentPageBuilder().internal() + WebComponentPageBuilder footerLogComponent = Page.webComponentPageBuilder().internal() .namespace(FOOTER_LOG_NAMESPACE) .icon("font-awesome-regular:file-lines") .title(capitalizeFirstLetter(footerLogBuildItem.getName())) .metadata("jsonRpcMethodName", footerLogBuildItem.getName() + "Log") .componentLink("qwc-footer-log.js"); - FooterPageBuildItem footerPageBuildItem = new FooterPageBuildItem(FOOTER_LOG_NAMESPACE, log); + FooterPageBuildItem footerPageBuildItem = new FooterPageBuildItem(FOOTER_LOG_NAMESPACE, footerLogComponent); footers.add(footerPageBuildItem); } @@ -606,7 +723,7 @@ void getAllExtensions(List cardPageBuildItems, // Add all card links List cardPageBuilders = cardPageBuildItem.getPages(); - Map buildTimeData = cardPageBuildItem.getBuildTimeData(); + Map buildTimeData = cardPageBuildItem.getBuildTimeData(); for (PageBuilder pageBuilder : cardPageBuilders) { Page page = buildFinalPage(pageBuilder, extension, buildTimeData); if (!page.isAssistantPage() || assistantIsAvailable) { @@ -649,7 +766,7 @@ void getAllExtensions(List cardPageBuildItems, MenuPageBuildItem menuPageBuildItem = menuPagesMap.get(namespace); List menuPageBuilders = menuPageBuildItem.getPages(); - Map buildTimeData = menuPageBuildItem.getBuildTimeData(); + Map buildTimeData = menuPageBuildItem.getBuildTimeData(); for (PageBuilder pageBuilder : menuPageBuilders) { Page page = buildFinalPage(pageBuilder, extension, buildTimeData); if (!page.isAssistantPage() || assistantIsAvailable) { @@ -664,11 +781,11 @@ void getAllExtensions(List cardPageBuildItems, // Tabs in the footer if (footerPagesMap.containsKey(namespace)) { - List fbis = footerPagesMap.get(namespace); + List fbis = footerPagesMap.remove(namespace); for (FooterPageBuildItem footerPageBuildItem : fbis) { List footerPageBuilders = footerPageBuildItem.getPages(); - Map buildTimeData = footerPageBuildItem.getBuildTimeData(); + Map buildTimeData = footerPageBuildItem.getBuildTimeData(); for (PageBuilder pageBuilder : footerPageBuilders) { Page page = buildFinalPage(pageBuilder, extension, buildTimeData); if (!page.isAssistantPage() || assistantIsAvailable) { @@ -698,23 +815,22 @@ void getAllExtensions(List cardPageBuildItems, for (Map.Entry> footer : footerPagesMap.entrySet()) { List fbis = footer.getValue(); for (FooterPageBuildItem footerPageBuildItem : fbis) { - if (footerPageBuildItem.isInternal()) { - Extension deploymentOnlyExtension = new Extension(); - deploymentOnlyExtension.setName(footer.getKey()); - deploymentOnlyExtension.setNamespace(FOOTER_LOG_NAMESPACE); - - List footerPageBuilders = footerPageBuildItem.getPages(); - - for (PageBuilder pageBuilder : footerPageBuilders) { - pageBuilder.namespace(deploymentOnlyExtension.getNamespace()); - pageBuilder.extension(deploymentOnlyExtension.getName()); - pageBuilder.internal(); - Page page = pageBuilder.build(); - deploymentOnlyExtension.addFooterPage(page); - } - footerTabExtensions.add(deploymentOnlyExtension); + Extension deploymentOnlyExtension = new Extension(); + deploymentOnlyExtension.setName(footer.getKey()); + deploymentOnlyExtension.setNamespace(FOOTER_LOG_NAMESPACE); + + List footerPageBuilders = footerPageBuildItem.getPages(); + + for (PageBuilder pageBuilder : footerPageBuilders) { + pageBuilder.namespace(deploymentOnlyExtension.getNamespace()); + pageBuilder.extension(deploymentOnlyExtension.getName()); + pageBuilder.internal(); + Page page = pageBuilder.build(); + deploymentOnlyExtension.addFooterPage(page); } + + footerTabExtensions.add(deploymentOnlyExtension); } } } @@ -903,27 +1019,31 @@ private String getNamespace(ArtifactKey artifactKey) { return namespace; } - private Page buildFinalPage(PageBuilder pageBuilder, Extension extension, Map buildTimeData) { + private Page buildFinalPage(PageBuilder pageBuilder, Extension extension, Map buildTimeData) { pageBuilder.namespace(extension.getNamespace()); pageBuilder.extension(extension.getName()); // TODO: Have a nice factory way to load this... // Some preprocessing for certain builds if (pageBuilder.getClass().equals(QuteDataPageBuilder.class)) { - return buildQutePage(pageBuilder, extension, buildTimeData); + return buildQutePage(pageBuilder, buildTimeData); } return pageBuilder.build(); } - private Page buildQutePage(PageBuilder pageBuilder, Extension extension, Map buildTimeData) { + private Page buildQutePage(PageBuilder pageBuilder, Map buildTimeData) { try { QuteDataPageBuilder quteDataPageBuilder = (QuteDataPageBuilder) pageBuilder; String templatePath = quteDataPageBuilder.getTemplatePath(); ClassPathUtils.consumeAsPaths(templatePath, p -> { try { String template = Files.readString(p); - String fragment = Qute.fmt(template, buildTimeData); + Map contentMap = buildTimeData.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + e -> e.getValue().getContent())); + String fragment = Qute.fmt(template, contentMap); pageBuilder.metadata("htmlFragment", fragment); } catch (IOException ex) { throw new UncheckedIOException(ex); @@ -972,7 +1092,6 @@ private Map> getFooterPagesMap(CurateOutcomeBu List pages) { Map> m = new HashMap<>(); for (FooterPageBuildItem pageBuildItem : pages) { - String key = pageBuildItem.getExtensionPathName(curateOutcomeBuildItem); if (m.containsKey(key)) { m.get(key).add(pageBuildItem); @@ -1019,4 +1138,6 @@ public int compare(Extension t, Extension t1) { } } }; + + private static final ArtifactKey INTERNAL_KEY = ArtifactKey.ga("io.quarkus", "quarkus-vertx-http"); } diff --git a/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/InternalPageBuildItem.java b/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/InternalPageBuildItem.java index 3789d0461dc0e..ef73ebefb86a0 100644 --- a/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/InternalPageBuildItem.java +++ b/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/InternalPageBuildItem.java @@ -6,6 +6,7 @@ import java.util.Map; import io.quarkus.builder.item.MultiBuildItem; +import io.quarkus.devui.spi.buildtime.BuildTimeData; import io.quarkus.devui.spi.page.Page; import io.quarkus.devui.spi.page.PageBuilder; @@ -17,7 +18,7 @@ public final class InternalPageBuildItem extends MultiBuildItem { private final String namespaceLabel; private final int position; private final List pages = new ArrayList<>(); - private final Map buildTimeData = new HashMap<>(); + private final Map buildTimeData = new HashMap<>(); private final String menuActionComponent; private String headlessComponentLink = null; @@ -37,7 +38,11 @@ public void addPage(PageBuilder page) { } public void addBuildTimeData(String key, Object value) { - this.buildTimeData.put(key, value); + this.addBuildTimeData(key, value, null); + } + + public void addBuildTimeData(String key, Object value, String description) { + this.buildTimeData.put(key, new BuildTimeData(value, description)); } public List getPages() { @@ -56,7 +61,7 @@ public String getNamespaceLabel() { return namespaceLabel; } - public Map getBuildTimeData() { + public Map getBuildTimeData() { return buildTimeData; } diff --git a/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/JsonRPCRuntimeMethodsBuildItem.java b/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/JsonRPCRuntimeMethodsBuildItem.java index 5dad9ee1534aa..47fa1c1ee5365 100644 --- a/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/JsonRPCRuntimeMethodsBuildItem.java +++ b/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/JsonRPCRuntimeMethodsBuildItem.java @@ -3,21 +3,27 @@ import java.util.Map; import io.quarkus.builder.item.SimpleBuildItem; -import io.quarkus.devui.runtime.jsonrpc.JsonRpcMethod; -import io.quarkus.devui.runtime.jsonrpc.JsonRpcMethodName; +import io.quarkus.devui.spi.buildtime.jsonrpc.RuntimeJsonRpcMethod; /** - * Simple holder for all discovered Json RPC Methods + * Simple holder for all discovered Json RPC Runtime Endpoints */ public final class JsonRPCRuntimeMethodsBuildItem extends SimpleBuildItem { - private final Map> extensionMethodsMap; + private final Map runtimeMethodsMap; + private final Map runtimeSubscriptionsMap; - public JsonRPCRuntimeMethodsBuildItem(Map> extensionMethodsMap) { - this.extensionMethodsMap = extensionMethodsMap; + public JsonRPCRuntimeMethodsBuildItem(Map runtimeMethodsMap, + Map runtimeSubscriptionsMap) { + this.runtimeMethodsMap = runtimeMethodsMap; + this.runtimeSubscriptionsMap = runtimeSubscriptionsMap; } - public Map> getExtensionMethodsMap() { - return extensionMethodsMap; + public Map getRuntimeMethodsMap() { + return runtimeMethodsMap; + } + + public Map getRuntimeSubscriptionsMap() { + return runtimeSubscriptionsMap; } } diff --git a/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/ide/IdeProcessor.java b/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/ide/IdeProcessor.java index f1afb9886275e..8fd754566abfa 100644 --- a/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/ide/IdeProcessor.java +++ b/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/ide/IdeProcessor.java @@ -48,21 +48,28 @@ void createOpenInIDEService(BuildProducer buildTimeAct // For Dev UI (like from the server log) BuildTimeActionBuildItem ideActions = new BuildTimeActionBuildItem(NAMESPACE); - ideActions.addAction("open", map -> { - String fileName = map.get("fileName"); - String lang = map.get("lang"); - String lineNumber = map.get("lineNumber"); - - if (fileName != null && fileName.startsWith(FILE_PROTOCOL)) { - fileName = fileName.substring(FILE_PROTOCOL.length()); - return typicalProcessLaunch(fileName, lineNumber, ide); - } else { - if (isNullOrEmpty(fileName) || isNullOrEmpty(lang)) { - return false; - } - return typicalProcessLaunch(fileName, lang, lineNumber, ide); - } - }); + ideActions.actionBuilder() + .methodName("open") + .description("Opens a certain workspace item in the user's IDE") + .parameter("fileName", "The filename that should be opened") + .parameter("lang", "The language of that file, example java or js") + .parameter("lineNumber", "The lineNumber where the cursor should be in the IDE. Use 0 if unknown") + .function(map -> { + String fileName = map.get("fileName"); + String lang = map.get("lang"); + String lineNumber = map.get("lineNumber"); + + if (fileName != null && fileName.startsWith(FILE_PROTOCOL)) { + fileName = fileName.substring(FILE_PROTOCOL.length()); + return typicalProcessLaunch(fileName, lineNumber, ide); + } else { + if (isNullOrEmpty(fileName) || isNullOrEmpty(lang)) { + return false; + } + return typicalProcessLaunch(fileName, lang, lineNumber, ide); + } + }) + .build(); buildTimeActionProducer.produce(ideActions); } diff --git a/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/logstream/LogStreamProcessor.java b/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/logstream/LogStreamProcessor.java index 87a28f68801be..2de39f128a245 100644 --- a/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/logstream/LogStreamProcessor.java +++ b/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/logstream/LogStreamProcessor.java @@ -55,79 +55,107 @@ void registerBuildTimeActions(BuildProducer buildTimeA BuildTimeActionBuildItem keyStrokeActions = new BuildTimeActionBuildItem(namespace); - keyStrokeActions.addAction("forceRestart", ignored -> { - RuntimeUpdatesProcessor.INSTANCE.doScan(true, true); - return Map.of(); - }); - - keyStrokeActions.addAction("rerunAllTests", ignored -> { - if (testsDisabled(launchModeBuildItem, ts)) { - return Map.of(); - } - if (ts.get().isStarted()) { - ts.get().runAllTests(); - return Map.of(); - } else { - ts.get().start(); - return Map.of("running", ts.get().isRunning()); - } - }); - - keyStrokeActions.addAction("rerunFailedTests", ignored -> { - if (testsDisabled(launchModeBuildItem, ts)) { - return Map.of(); - } - ts.get().runFailedTests(); - return Map.of(); - }); - - keyStrokeActions.addAction("toggleBrokenOnly", ignored -> { - if (testsDisabled(launchModeBuildItem, ts)) { - return Map.of(); - } - boolean brokenOnlyMode = ts.get().toggleBrokenOnlyMode(); - return Map.of("brokenOnlyMode", brokenOnlyMode); - }); - - keyStrokeActions.addAction("printFailures", ignored -> { - if (testsDisabled(launchModeBuildItem, ts)) { - return Map.of(); - } - ts.get().printFullResults(); - return Map.of(); - }); - - keyStrokeActions.addAction("toggleTestOutput", ignored -> { - if (testsDisabled(launchModeBuildItem, ts)) { - return Map.of(); - } - boolean isTestOutput = ts.get().toggleTestOutput(); - return Map.of("isTestOutput", isTestOutput); - }); - - keyStrokeActions.addAction("toggleInstrumentationReload", ignored -> { - boolean instrumentationEnabled = RuntimeUpdatesProcessor.INSTANCE.toggleInstrumentation(); - return Map.of("instrumentationEnabled", instrumentationEnabled); - }); - - keyStrokeActions.addAction("pauseTests", ignored -> { - if (testsDisabled(launchModeBuildItem, ts)) { - return Map.of(); - } - if (ts.get().isStarted()) { - ts.get().stop(); - return Map.of("running", ts.get().isRunning()); - } - return Map.of(); - }); - - keyStrokeActions.addAction("toggleLiveReload", ignored -> { - if (testsDisabled(launchModeBuildItem, ts)) { - return Map.of(); - } - boolean liveReloadEnabled = ts.get().toggleLiveReloadEnabled(); - return Map.of("liveReloadEnabled", liveReloadEnabled); - }); + keyStrokeActions.actionBuilder() + .methodName("forceRestart") + .description("This force a Quarkus application restart (like pressing 's' in the console)") + .function(ignored -> { + RuntimeUpdatesProcessor.INSTANCE.doScan(true, true); + return Map.of(); + }) + .build(); + + keyStrokeActions.actionBuilder() + .methodName("rerunAllTests") + .function(ignored -> { + if (testsDisabled(launchModeBuildItem, ts)) { + return Map.of(); + } + if (ts.get().isStarted()) { + ts.get().runAllTests(); + return Map.of(); + } else { + ts.get().start(); + return Map.of("running", ts.get().isRunning()); + } + }) + .build(); + + keyStrokeActions.actionBuilder() + .methodName("rerunFailedTests") + .function(ignored -> { + if (testsDisabled(launchModeBuildItem, ts)) { + return Map.of(); + } + ts.get().runFailedTests(); + return Map.of(); + }) + .build(); + + keyStrokeActions.actionBuilder() + .methodName("toggleBrokenOnly") + .function(ignored -> { + if (testsDisabled(launchModeBuildItem, ts)) { + return Map.of(); + } + boolean brokenOnlyMode = ts.get().toggleBrokenOnlyMode(); + return Map.of("brokenOnlyMode", brokenOnlyMode); + }) + .build(); + + keyStrokeActions.actionBuilder() + .methodName("printFailures") + .function(ignored -> { + if (testsDisabled(launchModeBuildItem, ts)) { + return Map.of(); + } + ts.get().printFullResults(); + return Map.of(); + }) + .build(); + + keyStrokeActions.actionBuilder() + .methodName("toggleTestOutput") + .function(ignored -> { + if (testsDisabled(launchModeBuildItem, ts)) { + return Map.of(); + } + boolean isTestOutput = ts.get().toggleTestOutput(); + return Map.of("isTestOutput", isTestOutput); + }) + .build(); + + keyStrokeActions.actionBuilder() + .methodName("toggleInstrumentationReload") + .function(ignored -> { + boolean instrumentationEnabled = RuntimeUpdatesProcessor.INSTANCE.toggleInstrumentation(); + return Map.of("instrumentationEnabled", instrumentationEnabled); + }) + .build(); + + keyStrokeActions.actionBuilder() + .methodName("pauseTests") + .function(ignored -> { + if (testsDisabled(launchModeBuildItem, ts)) { + return Map.of(); + } + if (ts.get().isStarted()) { + ts.get().stop(); + return Map.of("running", ts.get().isRunning()); + } + return Map.of(); + }) + .build(); + + keyStrokeActions.actionBuilder() + .methodName("toggleLiveReload") + .function(ignored -> { + if (testsDisabled(launchModeBuildItem, ts)) { + return Map.of(); + } + boolean liveReloadEnabled = ts.get().toggleLiveReloadEnabled(); + return Map.of("liveReloadEnabled", liveReloadEnabled); + }) + .build(); buildTimeActionProducer.produce(keyStrokeActions); } diff --git a/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/menu/BuildMetricsProcessor.java b/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/menu/BuildMetricsProcessor.java index b595f89a65d5e..71184452b2d03 100644 --- a/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/menu/BuildMetricsProcessor.java +++ b/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/menu/BuildMetricsProcessor.java @@ -1,16 +1,26 @@ package io.quarkus.devui.deployment.menu; +import static io.quarkus.deployment.annotations.ExecutionTime.RUNTIME_INIT; + +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.arc.processor.DotNames; import io.quarkus.deployment.IsLocalDevelopment; import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.BuildSteps; +import io.quarkus.deployment.pkg.builditem.BuildSystemTargetBuildItem; import io.quarkus.devui.deployment.InternalPageBuildItem; +import io.quarkus.devui.runtime.build.BuildMetricsDevUIRecorder; +import io.quarkus.devui.runtime.build.BuildMetricsJsonRPCService; +import io.quarkus.devui.spi.JsonRPCProvidersBuildItem; import io.quarkus.devui.spi.page.Page; /** * This creates Build Metrics Page */ +@BuildSteps(onlyIf = { IsLocalDevelopment.class }) public class BuildMetricsProcessor { - @BuildStep(onlyIf = IsLocalDevelopment.class) + @BuildStep InternalPageBuildItem createBuildMetricsPages() { InternalPageBuildItem buildMetricsPages = new InternalPageBuildItem("Build Metrics", 50); @@ -29,4 +39,26 @@ InternalPageBuildItem createBuildMetricsPages() { return buildMetricsPages; } + + @BuildStep + @io.quarkus.deployment.annotations.Record(RUNTIME_INIT) + public void create(BuildMetricsDevUIRecorder recorder, + BuildSystemTargetBuildItem buildSystemTarget) { + recorder.setBuildMetricsPath(buildSystemTarget.getOutputDirectory().resolve("build-metrics.json").toString()); + } + + @BuildStep + AdditionalBeanBuildItem additionalBeans() { + return AdditionalBeanBuildItem + .builder() + .addBeanClass(BuildMetricsJsonRPCService.class) + .setUnremovable() + .setDefaultScope(DotNames.APPLICATION_SCOPED) + .build(); + } + + @BuildStep + JsonRPCProvidersBuildItem createJsonRPCService() { + return new JsonRPCProvidersBuildItem("devui-build-metrics", BuildMetricsJsonRPCService.class); + } } diff --git a/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/menu/ConfigurationProcessor.java b/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/menu/ConfigurationProcessor.java index febc4eacfcf40..305151e9a4429 100644 --- a/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/menu/ConfigurationProcessor.java +++ b/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/menu/ConfigurationProcessor.java @@ -104,28 +104,44 @@ void registerBuildTimeActions( BuildTimeActionBuildItem configActions = new BuildTimeActionBuildItem(NAMESPACE); - configActions.addAction("updateProperty", map -> { - Map values = Collections.singletonMap(map.get("name"), map.get("value")); - updateConfig(values); - return true; - }); - configActions.addAction("updateProperties", map -> { - String type = map.get("type"); - - if (type.equalsIgnoreCase("properties")) { - String content = map.get("content"); - - Properties p = new Properties(); - try (StringReader sr = new StringReader(content)) { - p.load(sr); // Validate - setConfig(content); + configActions.actionBuilder() + .methodName("updateProperty") + .description("Update a configuration/property in the Quarkus application") + .parameter("name", "The name of the configuration/property to update") + .parameter("value", "The new value for the configuration/property") + .function(map -> { + Map values = Collections.singletonMap(map.get("name"), map.get("value")); + updateConfig(values); return true; - } catch (IOException ex) { + }) + .build(); + + configActions.actionBuilder() + .methodName("updateProperties") + .description("Update multiple configurations/properties in the Quarkus application") + .parameter("type", + "The type should always be 'properties' as the content should be the contents of serialized properties object") + .parameter("content", + "The string value of serialized properties, with the keys being the name of the configuration/property and the value the new value for that configuration/property") + .function(map -> { + String type = map.get("type"); + + if (type.equalsIgnoreCase("properties")) { + String content = map.get("content"); + + Properties p = new Properties(); + try (StringReader sr = new StringReader(content)) { + p.load(sr); // Validate + setConfig(content); + return true; + } catch (IOException ex) { + return false; + } + } return false; - } - } - return false; - }); + }) + .build(); + buildTimeActionProducer.produce(configActions); syntheticBeanProducer.produce( diff --git a/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/menu/ContinuousTestingProcessor.java b/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/menu/ContinuousTestingProcessor.java index c92deea7ddd75..1c2c94b2678af 100644 --- a/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/menu/ContinuousTestingProcessor.java +++ b/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/menu/ContinuousTestingProcessor.java @@ -16,7 +16,6 @@ import io.quarkus.deployment.dev.testing.TestRunResults; import io.quarkus.deployment.dev.testing.TestSupport; import io.quarkus.dev.spi.DevModeType; -import io.quarkus.dev.testing.results.TestRunResultsInterface; import io.quarkus.devui.deployment.InternalPageBuildItem; import io.quarkus.devui.runtime.continuoustesting.ContinuousTestingJsonRPCService; import io.quarkus.devui.runtime.continuoustesting.ContinuousTestingRecorder; @@ -92,174 +91,200 @@ private boolean testsDisabled(LaunchModeBuildItem launchModeBuildItem, Optional< } private void registerStartMethod(LaunchModeBuildItem launchModeBuildItem, BuildTimeActionBuildItem actions) { - actions.addAction("start", ignored -> { - - try { - Optional ts = TestSupport.instance(); - if (testsDisabled(launchModeBuildItem, ts)) { - return false; - } - TestSupport testSupport = ts.get(); - - if (testSupport.isStarted()) { - return false; // Already running - } else { - testSupport.start(); - return true; - } - } catch (Exception e) { - throw new RuntimeException(e); - } - }); + actions.actionBuilder() + .methodName("start") + .description("Start the Continuous Testing") + .function(ignored -> { + try { + Optional ts = TestSupport.instance(); + if (testsDisabled(launchModeBuildItem, ts)) { + return false; + } + TestSupport testSupport = ts.get(); + + if (testSupport.isStarted()) { + return false; // Already running + } else { + testSupport.start(); + return true; + } + } catch (Exception e) { + throw new RuntimeException(e); + } + }) + .build(); } private void registerStopMethod(LaunchModeBuildItem launchModeBuildItem, BuildTimeActionBuildItem actions) { - actions.addAction("stop", ignored -> { - - try { - Optional ts = TestSupport.instance(); - if (testsDisabled(launchModeBuildItem, ts)) { - return false; - } - TestSupport testSupport = ts.get(); - - if (testSupport.isStarted()) { - testSupport.stop(); - return true; - } else { - return false; // Already running - } - } catch (Exception e) { - throw new RuntimeException(e); - } - }); + actions.actionBuilder() + .methodName("stop") + .description("Stop the Continuous Testing") + .function(ignored -> { + try { + Optional ts = TestSupport.instance(); + if (testsDisabled(launchModeBuildItem, ts)) { + return false; + } + TestSupport testSupport = ts.get(); + + if (testSupport.isStarted()) { + testSupport.stop(); + return true; + } else { + return false; // Already running + } + } catch (Exception e) { + throw new RuntimeException(e); + } + }) + .build(); } private void registerRunAllMethod(LaunchModeBuildItem launchModeBuildItem, BuildTimeActionBuildItem actions) { - actions.addAction("runAll", ignored -> { - - try { - Optional ts = TestSupport.instance(); - if (testsDisabled(launchModeBuildItem, ts)) { - return false; - } - TestSupport testSupport = ts.get(); - testSupport.runAllTests(); - return true; - } catch (Exception e) { - throw new RuntimeException(e); - } - }); + actions.actionBuilder() + .methodName("runAll") + .description("Run all tests in Continuous Testing if it's started") + .function(ignored -> { + try { + Optional ts = TestSupport.instance(); + if (testsDisabled(launchModeBuildItem, ts)) { + return false; + } + TestSupport testSupport = ts.get(); + testSupport.runAllTests(); + return true; + } catch (Exception e) { + throw new RuntimeException(e); + } + }) + .build(); } private void registerRunFailedMethod(LaunchModeBuildItem launchModeBuildItem, BuildTimeActionBuildItem actions) { - actions.addAction("runFailed", ignored -> { - - try { - Optional ts = TestSupport.instance(); - if (testsDisabled(launchModeBuildItem, ts)) { - return false; - } - TestSupport testSupport = ts.get(); - testSupport.runFailedTests(); - return true; - } catch (Exception e) { - throw new RuntimeException(e); - } - }); + actions.actionBuilder() + .methodName("runFailed") + .description("Run all failed tests in Continuous Testing if it's started") + .function(ignored -> { + try { + Optional ts = TestSupport.instance(); + if (testsDisabled(launchModeBuildItem, ts)) { + return false; + } + TestSupport testSupport = ts.get(); + testSupport.runFailedTests(); + return true; + } catch (Exception e) { + throw new RuntimeException(e); + } + }) + .build(); } private void registerToggleBrokenOnlyMethod(LaunchModeBuildItem launchModeBuildItem, BuildTimeActionBuildItem actions) { - actions.addAction("toggleBrokenOnly", ignored -> { - - try { - Optional ts = TestSupport.instance(); - if (testsDisabled(launchModeBuildItem, ts)) { - return false; - } - TestSupport testSupport = ts.get(); - return testSupport.toggleBrokenOnlyMode(); - } catch (Exception e) { - throw new RuntimeException(e); - } - }); + actions.actionBuilder() + .methodName("toggleBrokenOnly") + .description("Toggle broken only in Continuous Testing") + .function(ignored -> { + try { + Optional ts = TestSupport.instance(); + if (testsDisabled(launchModeBuildItem, ts)) { + return false; + } + TestSupport testSupport = ts.get(); + return testSupport.toggleBrokenOnlyMode(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }) + .build(); } private void registerToggleInstrumentationMethod(LaunchModeBuildItem launchModeBuildItem, BuildTimeActionBuildItem actions) { - actions.addAction("toggleInstrumentation", ignored -> { - - try { - Optional ts = TestSupport.instance(); - if (testsDisabled(launchModeBuildItem, ts)) { - return false; - } - TestSupport testSupport = ts.get(); - return testSupport.toggleInstrumentation(); - } catch (Exception e) { - throw new RuntimeException(e); - } - }); + actions.actionBuilder() + .methodName("toggleInstrumentation") + .description("Toggle instrumentation in Continuous Testing") + .function(ignored -> { + try { + Optional ts = TestSupport.instance(); + if (testsDisabled(launchModeBuildItem, ts)) { + return false; + } + TestSupport testSupport = ts.get(); + return testSupport.toggleInstrumentation(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }) + .build(); } private void registerGetStatusMethod(LaunchModeBuildItem launchModeBuildItem, BuildTimeActionBuildItem actions) { - actions.addAction("getStatus", ignored -> { - try { - Optional ts = TestSupport.instance(); - if (testsDisabled(launchModeBuildItem, ts)) { - return null; - } - TestSupport testSupport = ts.get(); - TestSupport.RunStatus status = testSupport.getStatus(); - - if (status == null) { - return null; - } - - Map testStatus = new HashMap<>(); - - long lastRun = status.getLastRun(); - testStatus.put("lastRun", lastRun); - if (lastRun > 0) { - TestRunResults result = testSupport.getResults(); - testStatus.put("testsFailed", result.getCurrentFailedCount()); - testStatus.put("testsPassed", result.getCurrentPassedCount()); - testStatus.put("testsSkipped", result.getCurrentSkippedCount()); - testStatus.put("testsRun", result.getFailedCount() + result.getPassedCount()); - testStatus.put("totalTestsFailed", result.getFailedCount()); - testStatus.put("totalTestsPassed", result.getPassedCount()); - testStatus.put("totalTestsSkipped", result.getSkippedCount()); - } - //get running last, as otherwise if the test completes in the meantime you could see - //both running and last run being the same number - testStatus.put("running", status.getRunning()); - return testStatus; - } catch (Exception e) { - throw new RuntimeException(e); - } - }); + actions.actionBuilder() + .methodName("getStatus") + .description("Get the status of Continuous Testing") + .function(ignored -> { + try { + Optional ts = TestSupport.instance(); + if (testsDisabled(launchModeBuildItem, ts)) { + return null; + } + TestSupport testSupport = ts.get(); + TestSupport.RunStatus status = testSupport.getStatus(); + + if (status == null) { + return null; + } + + Map testStatus = new HashMap<>(); + + long lastRun = status.getLastRun(); + testStatus.put("lastRun", lastRun); + if (lastRun > 0) { + TestRunResults result = testSupport.getResults(); + testStatus.put("testsFailed", result.getCurrentFailedCount()); + testStatus.put("testsPassed", result.getCurrentPassedCount()); + testStatus.put("testsSkipped", result.getCurrentSkippedCount()); + testStatus.put("testsRun", result.getFailedCount() + result.getPassedCount()); + testStatus.put("totalTestsFailed", result.getFailedCount()); + testStatus.put("totalTestsPassed", result.getPassedCount()); + testStatus.put("totalTestsSkipped", result.getSkippedCount()); + } + //get running last, as otherwise if the test completes in the meantime you could see + //both running and last run being the same number + testStatus.put("running", status.getRunning()); + return testStatus; + } catch (Exception e) { + throw new RuntimeException(e); + } + }) + .build(); } private void registerGetResultsMethod(LaunchModeBuildItem launchModeBuildItem, BuildTimeActionBuildItem actions) { - actions. addAction("getResults", ignored -> { - try { - Optional ts = TestSupport.instance(); - if (testsDisabled(launchModeBuildItem, ts)) { - return null; - } - TestSupport testSupport = ts.get(); - TestRunResults testRunResults = testSupport.getResults(); - - if (testRunResults == null) { - return null; - } - - return testRunResults; - - } catch (Exception e) { - throw new RuntimeException(e); - } - }); + actions.actionBuilder() + .methodName("getResults") + .description("Get the results of a Continuous testing test run") + .function(ignored -> { + try { + Optional ts = TestSupport.instance(); + if (testsDisabled(launchModeBuildItem, ts)) { + return null; + } + TestSupport testSupport = ts.get(); + TestRunResults testRunResults = testSupport.getResults(); + + if (testRunResults == null) { + return null; + } + + return testRunResults; + + } catch (Exception e) { + throw new RuntimeException(e); + } + }) + .build(); } private static final String NAMESPACE = "devui-continuous-testing"; diff --git a/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/menu/DependenciesProcessor.java b/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/menu/DependenciesProcessor.java index 4f514674e73e3..60471bb9992a3 100644 --- a/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/menu/DependenciesProcessor.java +++ b/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/menu/DependenciesProcessor.java @@ -53,7 +53,8 @@ void createAppDeps(BuildProducer menuProducer, buildTree(curateOutcomeBuildItem.getApplicationModel(), root, Optional.of(allGavs), Optional.empty()); page.addBuildTimeData("root", root); - page.addBuildTimeData("allGavs", allGavs); + page.addBuildTimeData("allGavs", allGavs, + "This is a list of all the GAVs (groupId, artifactId, version) of all the dependencies of this Quarkus application"); menuProducer.produce(page); } @@ -64,19 +65,26 @@ void createBuildTimeActions(BuildProducer buildTimeAct CurateOutcomeBuildItem curateOutcomeBuildItem) { if (isEnabled()) { BuildTimeActionBuildItem pathToTargetAction = new BuildTimeActionBuildItem(NAMESPACE); - pathToTargetAction.addAction("pathToTarget", p -> { - String target = p.get("target"); - Root root = new Root(); - root.rootId = curateOutcomeBuildItem.getApplicationModel().getAppArtifact().toCompactCoords(); - - if (target == null || target.isBlank()) { - buildTree(curateOutcomeBuildItem.getApplicationModel(), root, Optional.empty(), Optional.empty()); - } else { - buildTree(curateOutcomeBuildItem.getApplicationModel(), root, Optional.empty(), Optional.of(target)); - } - - return root; - }); + pathToTargetAction.actionBuilder() + .methodName("pathToTarget") + .description( + "The get the dependency path to a certain target. This is useful when wanting to know what dependency/dependencies are pulling in a certain libtrary") + .parameter("target", "The target as a GAV string (groupId:artifactId:version") + .function(p -> { + String target = p.get("target"); + Root root = new Root(); + root.rootId = curateOutcomeBuildItem.getApplicationModel().getAppArtifact().toCompactCoords(); + + if (target == null || target.isBlank()) { + buildTree(curateOutcomeBuildItem.getApplicationModel(), root, Optional.empty(), Optional.empty()); + } else { + buildTree(curateOutcomeBuildItem.getApplicationModel(), root, Optional.empty(), + Optional.of(target)); + } + + return root; + }) + .build(); buildTimeActionProducer.produce(pathToTargetAction); } @@ -134,7 +142,7 @@ private static void addDependency(ResolvedDependency rd, Root root, List n link.source = node.id; link.target = dep.toCompactCoords(); link.type = type; - link.direct = (link.source == root.rootId); + link.direct = (link.source.equals(root.rootId)); links.add(link); } } @@ -163,7 +171,7 @@ private static void addDependency(DepNode dep, Root root, List nodes, List link.source = dependent.resolvedDep.toCompactCoords(); link.target = node.id; link.type = dependent.resolvedDep.isRuntimeCp() ? "runtime" : "deployment"; - link.direct = (link.source == root.rootId); + link.direct = (link.source.equals(root.rootId)); links.add(link); } } diff --git a/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/menu/DevServicesProcessor.java b/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/menu/DevServicesProcessor.java index 123814cb775bb..068af7f32c7bd 100644 --- a/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/menu/DevServicesProcessor.java +++ b/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/menu/DevServicesProcessor.java @@ -42,7 +42,8 @@ InternalPageBuildItem createDevServicesPages(BuildProducer services = getServices(devServiceDescriptions, otherDevServices); - devServicesPages.addBuildTimeData("devServices", services); + devServicesPages.addBuildTimeData("devServices", services, + "All the DevServices started by this Quarkus app, including information on container (if any) and the config that is being set automatically"); if (launchModeBuildItem.getDevModeType().isPresent() && launchModeBuildItem.getDevModeType().get().equals(DevModeType.LOCAL) diff --git a/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/menu/ExtensionsProcessor.java b/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/menu/ExtensionsProcessor.java index b424cec380a7c..64304dc13dcf3 100644 --- a/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/menu/ExtensionsProcessor.java +++ b/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/menu/ExtensionsProcessor.java @@ -44,7 +44,9 @@ InternalPageBuildItem createExtensionsPages(ExtensionsBuildItem extensionsBuildI ExtensionGroup.active, extensionsBuildItem.getActiveExtensions(), ExtensionGroup.inactive, extensionsBuildItem.getInactiveExtensions()); - extensionsPages.addBuildTimeData("extensions", response); + extensionsPages.addBuildTimeData("extensions", response, "All the extensions added to this Quarkus application. " + + "Some extensions are 'active' meaning they have actions in Dev UI, and some are 'inactive', meaning they will be listed in Dev UI, but a user can not perform any actions." + + "For both active and inactive all sorts of information is available about the extension, like it's name, URL to the guide, GAV and much more"); // Page extensionsPages.addPage(Page.webComponentPageBuilder() @@ -77,129 +79,150 @@ void createBuildTimeActions(BuildProducer buildTimeAct } private void getCategories(BuildTimeActionBuildItem buildTimeActions) { - buildTimeActions.addAction(new Object() { - }.getClass().getEnclosingMethod().getName(), ignored -> { - return CompletableFuture.supplyAsync(() -> { - try { - QuarkusCommandOutcome outcome = new ListCategories(getQuarkusProject()) - .format("object") - .execute(); - - if (outcome.isSuccess()) { - return outcome.getResult(); - } - } catch (QuarkusCommandException ex) { - throw new RuntimeException(ex); - } - return null; - }); - }); + buildTimeActions + .actionBuilder().methodName(new Object() { + }.getClass().getEnclosingMethod().getName()) + .description("Get all available categories for the Quarkus Extension List") + .function(ignored -> { + return CompletableFuture.supplyAsync(() -> { + try { + QuarkusCommandOutcome outcome = new ListCategories(getQuarkusProject()) + .format("object") + .execute(); + + if (outcome.isSuccess()) { + return outcome.getResult(); + } + } catch (QuarkusCommandException ex) { + throw new RuntimeException(ex); + } + return null; + }); + }) + .build(); } private void getInstallableExtensions(BuildTimeActionBuildItem buildTimeActions) { - buildTimeActions.addAction(new Object() { - }.getClass().getEnclosingMethod().getName(), ignored -> { - return CompletableFuture.supplyAsync(() -> { - try { - QuarkusCommandOutcome outcome = new ListExtensions(getQuarkusProject()) - .installed(false) - .all(false) - .format("object") - .execute(); - - if (outcome.isSuccess()) { - return outcome.getResult(); - } - - return null; - } catch (QuarkusCommandException e) { - throw new RuntimeException(e); - } - }); - }); + buildTimeActions + .actionBuilder().methodName(new Object() { + }.getClass().getEnclosingMethod().getName()) + .description( + "Get all extensions that can be added to the current project (i.e it's not currently added to the pom)") + .function(ignored -> { + return CompletableFuture.supplyAsync(() -> { + try { + QuarkusCommandOutcome outcome = new ListExtensions(getQuarkusProject()) + .installed(false) + .all(false) + .format("object") + .execute(); + + if (outcome.isSuccess()) { + return outcome.getResult(); + } + + return null; + } catch (QuarkusCommandException e) { + throw new RuntimeException(e); + } + }); + }) + .build(); } private void getInstalledNamespaces(BuildTimeActionBuildItem buildTimeActions) { - buildTimeActions.addAction(new Object() { - }.getClass().getEnclosingMethod().getName(), ignored -> { - return CompletableFuture.supplyAsync(() -> { - try { - QuarkusCommandOutcome outcome = new ListExtensions(getQuarkusProject()) - .installed(true) - .all(false) - .format("object") - .execute(); - - if (outcome.isSuccess()) { - - List extensionList = (List) outcome - .getResult(); - - List namespaceList = new ArrayList<>(); - - if (!extensionList.isEmpty()) { - for (io.quarkus.registry.catalog.Extension e : extensionList) { - String groupId = e.getArtifact().getGroupId(); - String artifactId = e.getArtifact().getArtifactId(); - namespaceList.add(groupId + "." + artifactId); + buildTimeActions + .actionBuilder().methodName(new Object() { + }.getClass().getEnclosingMethod().getName()) + .description("Get all extensions that is already part of the current project (i.e it's currently in the pom)") + .function(ignored -> { + return CompletableFuture.supplyAsync(() -> { + try { + QuarkusCommandOutcome outcome = new ListExtensions(getQuarkusProject()) + .installed(true) + .all(false) + .format("object") + .execute(); + + if (outcome.isSuccess()) { + + List extensionList = (List) outcome + .getResult(); + + List namespaceList = new ArrayList<>(); + + if (!extensionList.isEmpty()) { + for (io.quarkus.registry.catalog.Extension e : extensionList) { + String groupId = e.getArtifact().getGroupId(); + String artifactId = e.getArtifact().getArtifactId(); + namespaceList.add(groupId + "." + artifactId); + } + } + return namespaceList; } + + return null; + } catch (IllegalStateException e) { + return null; + } catch (QuarkusCommandException e) { + throw new RuntimeException(e); } - return namespaceList; - } - - return null; - } catch (IllegalStateException e) { - return null; - } catch (QuarkusCommandException e) { - throw new RuntimeException(e); - } - }); - }); + }); + }) + .build(); } private void removeExtension(BuildTimeActionBuildItem buildTimeActions) { - buildTimeActions.addAction(new Object() { - }.getClass().getEnclosingMethod().getName(), params -> { - return CompletableFuture.supplyAsync(() -> { - String extensionArtifactId = params.get("extensionArtifactId"); - try { - QuarkusCommandOutcome outcome = new RemoveExtensions(getQuarkusProject()) - .extensions(Set.of(extensionArtifactId)) - .execute(); - - if (outcome.isSuccess()) { - return true; - } else { - return false; - } - } catch (QuarkusCommandException e) { - throw new RuntimeException(e); - } - }); - }); + buildTimeActions + .actionBuilder().methodName(new Object() { + }.getClass().getEnclosingMethod().getName()) + .description("Remove a certain extension from the current project (i.e remove it from the pom)") + .parameter("extensionArtifactId", + "The gav string of the extension to remove in format groupId:artifactId:version") + .function(params -> { + return CompletableFuture.supplyAsync(() -> { + String extensionArtifactId = params.get("extensionArtifactId"); + try { + QuarkusCommandOutcome outcome = new RemoveExtensions(getQuarkusProject()) + .extensions(Set.of(extensionArtifactId)) + .execute(); + + if (outcome.isSuccess()) { + return true; + } else { + return false; + } + } catch (QuarkusCommandException e) { + throw new RuntimeException(e); + } + }); + }) + .build(); } private void addExtension(BuildTimeActionBuildItem buildTimeActions) { - buildTimeActions.addAction(new Object() { - }.getClass().getEnclosingMethod().getName(), params -> { - return CompletableFuture.supplyAsync(() -> { - String extensionArtifactId = params.get("extensionArtifactId"); - - try { - QuarkusCommandOutcome outcome = new AddExtensions(getQuarkusProject()) - .extensions(Set.of(extensionArtifactId)) - .execute(); - - if (outcome.isSuccess()) { - return true; - } else { - return false; - } - } catch (QuarkusCommandException e) { - throw new RuntimeException(e); - } - }); - }); + buildTimeActions + .actionBuilder().methodName(new Object() { + }.getClass().getEnclosingMethod().getName()) + .description("Adds a certain extension to the current project (i.e add it from the pom)") + .parameter("extensionArtifactId", + "The gav string of the extension to remove in format groupId:artifactId:version") + .function(params -> { + return CompletableFuture.supplyAsync(() -> { + String extensionArtifactId = params.get("extensionArtifactId"); + + try { + QuarkusCommandOutcome outcome = new AddExtensions(getQuarkusProject()) + .extensions(Set.of(extensionArtifactId)) + .execute(); + + return outcome.isSuccess(); + } catch (QuarkusCommandException e) { + throw new RuntimeException(e); + } + }); + }) + .build(); } private QuarkusProject getQuarkusProject() { diff --git a/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/menu/MCPProcessor.java b/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/menu/MCPProcessor.java new file mode 100644 index 0000000000000..132b71b18fcce --- /dev/null +++ b/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/menu/MCPProcessor.java @@ -0,0 +1,89 @@ +package io.quarkus.devui.deployment.menu; + +import java.io.IOException; +import java.util.List; + +import io.quarkus.builder.Version; +import io.quarkus.deployment.IsDevelopment; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.builditem.LaunchModeBuildItem; +import io.quarkus.devui.deployment.DevMCPConfig; +import io.quarkus.devui.deployment.InternalPageBuildItem; +import io.quarkus.devui.runtime.DevUIRecorder; +import io.quarkus.devui.runtime.mcp.DevMcpJsonRpcService; +import io.quarkus.devui.runtime.mcp.McpResourcesService; +import io.quarkus.devui.runtime.mcp.McpToolsService; +import io.quarkus.devui.spi.JsonRPCProvidersBuildItem; +import io.quarkus.devui.spi.page.Page; +import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; +import io.quarkus.vertx.http.deployment.RouteBuildItem; + +public class MCPProcessor { + + private static final String DEVMCP = "dev-mcp"; + + private static final String NS_MCP = "devmcp"; + private static final String NS_RESOURCES = "resources"; + private static final String NS_TOOLS = "tools"; + + @BuildStep(onlyIf = IsDevelopment.class) + void createMCPPage(BuildProducer internalPageProducer, + NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, DevMCPConfig devMCPConfig) { + if (devMCPConfig.enabled()) { + InternalPageBuildItem mcpServerPage = new InternalPageBuildItem("Dev MCP", 80); + + // Pages + mcpServerPage.addPage(Page.webComponentPageBuilder() + .namespace(NS_MCP) + .title("Info") + .icon("font-awesome-solid:robot") + .componentLink("qwc-dev-mcp-info.js")); + + mcpServerPage.addPage(Page.webComponentPageBuilder() + .namespace(NS_MCP) + .title("Tools") + .icon("font-awesome-solid:screwdriver-wrench") + .componentLink("qwc-dev-mcp-tools.js")); + + mcpServerPage.addPage(Page.webComponentPageBuilder() + .namespace(NS_MCP) + .title("Resources") + .icon("font-awesome-solid:file-invoice") + .componentLink("qwc-dev-mcp-resources.js")); + internalPageProducer.produce(mcpServerPage); + } + } + + @BuildStep(onlyIf = IsDevelopment.class) + @io.quarkus.deployment.annotations.Record(ExecutionTime.STATIC_INIT) + void registerDevUiHandlers( + BuildProducer routeProducer, + DevUIRecorder recorder, + LaunchModeBuildItem launchModeBuildItem, + NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, + DevMCPConfig devMCPConfig) throws IOException { + + if (launchModeBuildItem.isNotLocalDevModeType() || !devMCPConfig.enabled()) { + return; + } + + // Streamable HTTP for JsonRPC comms + routeProducer.produce( + nonApplicationRootPathBuildItem + .routeBuilder().route(DEVMCP) + .handler(recorder.mcpStreamableHTTPHandler(Version.getVersion())) + .build()); + + } + + @BuildStep(onlyIf = IsDevelopment.class) + void createMCPJsonRPCService(BuildProducer bp, DevMCPConfig devMCPConfig) { + if (devMCPConfig.enabled()) { + bp.produce(List.of(new JsonRPCProvidersBuildItem(NS_RESOURCES, McpResourcesService.class), + new JsonRPCProvidersBuildItem(NS_TOOLS, McpToolsService.class), + new JsonRPCProvidersBuildItem(NS_MCP, DevMcpJsonRpcService.class))); + } + } +} diff --git a/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/menu/ReadmeProcessor.java b/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/menu/ReadmeProcessor.java index 93a9483672da7..5f1cf1c32e517 100644 --- a/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/menu/ReadmeProcessor.java +++ b/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/menu/ReadmeProcessor.java @@ -30,8 +30,7 @@ void createReadmePage(BuildProducer internalPageProducer) if (readme != null) { InternalPageBuildItem readmePage = new InternalPageBuildItem("Readme", 51); - readmePage.addBuildTimeData("readme", readme); - + readmePage.addBuildTimeData("readme", readme, "The current readme of this Quarkus Application."); readmePage.addPage(Page.webComponentPageBuilder() .namespace(NS) .title("Readme") diff --git a/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/menu/WorkspaceProcessor.java b/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/menu/WorkspaceProcessor.java index af3d3dac5058c..66b1fa174041d 100644 --- a/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/menu/WorkspaceProcessor.java +++ b/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/menu/WorkspaceProcessor.java @@ -168,61 +168,93 @@ void createBuildTimeActions(Optional workspaceBuildItem, .build())) .collect(Collectors.toMap(Action::getId, action -> action, (a, b) -> a)); - buildItemActions.addAction("getWorkspaceItems", (t) -> { - return workspaceBuildItem.get().getWorkspaceItems(); - }); - - buildItemActions.addAction("getWorkspaceActions", (t) -> { - return actionMap.values().stream() - .filter(action -> assistantIsAvailable || !action.isAssistant()) - .map(action -> new WorkspaceAction(action.getId(), action.getLabel(), action.getFilter(), - action.getDisplay(), action.getDisplayType(), action.isAssistant())) - .sorted(Comparator.comparing(WorkspaceAction::label)) - .collect(Collectors.toList()); - }); - - buildItemActions.addAction("executeAction", (Map t) -> { - String actionId = t.get("actionId"); - if (actionId != null) { - Path path = Path.of(URI.create(t.get("path"))); - Action actionToExecute = actionMap.get(actionId); - Path convertedPath = (Path) actionToExecute.getPathConverter().apply(path); - - Object result; - if (actionToExecute.isAssistant()) { - Assistant assistant = DevConsoleManager.getGlobal(DevConsoleManager.DEV_MANAGER_GLOBALS_ASSISTANT); - result = actionToExecute.getAssistantFunction().apply(assistant, t); - } else { - result = actionToExecute.getFunction().apply(t); - } + buildItemActions.actionBuilder() + .methodName("getWorkspaceItems") + .description( + "Gets all the items in the current workspace of the Quarkus project. This will return a list of workspace items, where a workspace item has a name and a path to the file") + .function((t) -> { + return workspaceBuildItem.get().getWorkspaceItems(); + }) + .build(); + + buildItemActions.actionBuilder() + .methodName("getWorkspaceActions") + .description("Gets all the actions that can be performed on a item in the workspace") + .function((t) -> { + return actionMap.values().stream() + .map(action -> new WorkspaceAction(action.getId(), action.getLabel(), action.getFilter(), + action.getDisplay(), action.getDisplayType(), action.isAssistant())) + .sorted(Comparator.comparing(WorkspaceAction::label)) + .collect(Collectors.toList()); + }) + .build(); + + buildItemActions.actionBuilder() + .methodName("executeAction") + .description("Execute a certain action on a workspace item") + .parameter("actionId", + "The actionId as defined in the `getWorkspaceActions`. Each workspace action has a unique id") + .parameter("path", + "The path, as a URI in String format, to the workspace item that this action should be peformed on") + .function((Map t) -> { + String actionId = t.get("actionId"); + if (actionId != null) { + Path path = Path.of(URI.create(t.get("path"))); + Action actionToExecute = actionMap.get(actionId); + Path convertedPath = (Path) actionToExecute.getPathConverter().apply(path); + + Object result; + if (actionToExecute.isAssistant()) { + Assistant assistant = DevConsoleManager + .getGlobal(DevConsoleManager.DEV_MANAGER_GLOBALS_ASSISTANT); + result = actionToExecute.getAssistantFunction().apply(assistant, t); + } else { + result = actionToExecute.getFunction().apply(t); + } - if (result != null && result instanceof CompletionStage stage) { - return stage - .thenApply(res -> new WorkspaceActionResult(convertedPath, res, actionToExecute.isAssistant())); - } else { - return new WorkspaceActionResult(convertedPath, result, actionToExecute.isAssistant()); - } - } - return null; - }); - - buildItemActions.addAction("getWorkspaceItemContent", (Map params) -> { - if (params.containsKey("path")) { - Path path = Paths.get(URI.create(params.get("path"))); - return readContents(path); - } - return null; - }); - - buildItemActions.addAction("saveWorkspaceItemContent", (Map params) -> { - if (params.containsKey("content")) { - String content = params.get("content"); - Path path = Paths.get(URI.create(params.get("path"))); - writeContent(path, content); - return new SavedResult(workspaceBuildItem.get().getRootPath().relativize(path).toString(), true, null); - } - return new SavedResult(null, false, "Invalid input"); - }); + if (result != null && result instanceof CompletionStage stage) { + return stage + .thenApply(res -> new WorkspaceActionResult(convertedPath, res, + actionToExecute.isAssistant())); + } else { + return new WorkspaceActionResult(convertedPath, result, actionToExecute.isAssistant()); + } + } + return null; + }) + .build(); + + buildItemActions.actionBuilder() + .methodName("getWorkspaceItemContent") + .description( + "The the content of a certain workspace item. This returns a WorkspaceContent that has String type, String content, boolean isBinary fields") + .parameter("path", "The path, as a URI in String format, to the workspace item that content is requested") + .function((Map params) -> { + if (params.containsKey("path")) { + Path path = Paths.get(URI.create(params.get("path"))); + return readContents(path); + } + return null; + }) + .build(); + + buildItemActions.actionBuilder() + .methodName("saveWorkspaceItemContent") + .description("This will add or update an item in the workspace") + .parameter("content", "The new or updated content in String format") + .parameter("path", + "The path, as a URI in String format, to the workspace item that should be created or updated") + .function((Map params) -> { + if (params.containsKey("content")) { + String content = params.get("content"); + Path path = Paths.get(URI.create(params.get("path"))); + writeContent(path, content); + return new SavedResult(workspaceBuildItem.get().getRootPath().relativize(path).toString(), true, + null); + } + return new SavedResult(null, false, "Invalid input"); + }) + .build(); buildTimeActionProducer.produce(buildItemActions); } diff --git a/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/welcome/WelcomeProcessor.java b/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/welcome/WelcomeProcessor.java index 31c0710cdddf2..4f569bf757a3a 100644 --- a/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/welcome/WelcomeProcessor.java +++ b/extensions/devui/deployment/src/main/java/io/quarkus/devui/deployment/welcome/WelcomeProcessor.java @@ -30,7 +30,8 @@ InternalPageBuildItem createWelcomePages(CurateOutcomeBuildItem curateOutcomeBui InternalPageBuildItem welcomePageBuildItem = new InternalPageBuildItem("Welcome", 99999); - welcomePageBuildItem.addBuildTimeData("welcomeData", createWelcomeData(curateOutcomeBuildItem, extensionsBuildItem)); + welcomePageBuildItem.addBuildTimeData("welcomeData", createWelcomeData(curateOutcomeBuildItem, extensionsBuildItem), + "Contains high level information about the Quarkus application, including the configFile, resourcesDir, sourceDir and selectedExtensions"); welcomePageBuildItem.addPage(Page.webComponentPageBuilder() .namespace("devui-welcome") diff --git a/extensions/devui/deployment/src/test/java/io/quarkus/vertx/http/devmcp/DevMcpTest.java b/extensions/devui/deployment/src/test/java/io/quarkus/vertx/http/devmcp/DevMcpTest.java new file mode 100644 index 0000000000000..419c59fbe977a --- /dev/null +++ b/extensions/devui/deployment/src/test/java/io/quarkus/vertx/http/devmcp/DevMcpTest.java @@ -0,0 +1,193 @@ +package io.quarkus.vertx.http.devmcp; + +import java.util.function.Supplier; + +import org.hamcrest.CoreMatchers; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusDevModeTest; +import io.quarkus.vertx.http.testrunner.HelloResource; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; + +public class DevMcpTest { + + @RegisterExtension + static QuarkusDevModeTest test = new QuarkusDevModeTest() + .setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class).addClasses(HelloResource.class) + .add(new StringAsset("quarkus.dev-mcp.enabled=true"), + "application.properties"); + } + }); + + @Test + public void testInitialize() { + String jsonBody = """ + { + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": { + "name": "JUnitTestClient", + "version": "1.0.0" + } + } + } + """; + + RestAssured + .given() + .contentType(ContentType.JSON) + .body(jsonBody) + .when() + .post("/q/dev-mcp") + .then() + .statusCode(200) + .log().all() + .body("id", CoreMatchers.equalTo(1)) + .body("result.serverInfo.name", CoreMatchers.equalTo("Quarkus Dev MCP")); + + } + + @Test + public void testInitializedNotification() { + String jsonBody = """ + { + "jsonrpc": "2.0", + "method": "notifications/initialized" + } + """; + + RestAssured + .given() + .contentType(ContentType.JSON) + .body(jsonBody) + .when() + .post("/q/dev-mcp") + .then() + .statusCode(202); + } + + @Test + public void testToolsList() { + String jsonBody = """ + { + "jsonrpc": "2.0", + "id": 3, + "method": "tools/list" + } + """; + + RestAssured + .given() + .contentType(ContentType.JSON) + .body(jsonBody) + .when() + .post("/q/dev-mcp") + .then() + .statusCode(200) + .log().all() + .body("id", CoreMatchers.equalTo(3)) + .body("jsonrpc", CoreMatchers.equalTo("2.0")) + .body("result.tools.name", CoreMatchers.hasItem("devui-logstream_getLogger")); + + } + + @Test + public void testToolsCall() { + String jsonBody = """ + { + "jsonrpc": "2.0", + "id": 4, + "method": "tools/call", + "params": { + "name": "devui-logstream_getLogger", + "arguments": { + "loggerName": "io.quarkus" + } + } + } + """; + + RestAssured + .given() + .contentType(ContentType.JSON) + .body(jsonBody) + .when() + .post("/q/dev-mcp") + .then() + .statusCode(200) + .log().all() + .body("id", CoreMatchers.equalTo(4)) + .body("jsonrpc", CoreMatchers.equalTo("2.0")) + .body("result.content.type", CoreMatchers.hasItem("text")) + .body("result.content.text", CoreMatchers.notNullValue()); + + } + + @Test + public void testResourcesList() { + String jsonBody = """ + { + "jsonrpc": "2.0", + "id": 5, + "method": "resources/list" + } + """; + + RestAssured + .given() + .contentType(ContentType.JSON) + .body(jsonBody) + .when() + .post("/q/dev-mcp") + .then() + .statusCode(200) + .log().all() + .body("id", CoreMatchers.equalTo(5)) + .body("jsonrpc", CoreMatchers.equalTo("2.0")) + .body("result.resources.name", CoreMatchers.hasItem("devui/extensions")) + .body("result.resources.uri", CoreMatchers.hasItem("quarkus://resource/build-time/devui/extensions")); + + } + + @Test + public void testResourcesRead() { + String jsonBody = """ + { + "jsonrpc": "2.0", + "id": 6, + "method": "resources/read", + "params": { + "uri": "quarkus://resource/build-time/devui/extensions" + } + } + """; + + RestAssured + .given() + .contentType(ContentType.JSON) + .body(jsonBody) + .when() + .post("/q/dev-mcp") + .then() + .statusCode(200) + .log().all() + .body("id", CoreMatchers.equalTo(6)) + .body("jsonrpc", CoreMatchers.equalTo("2.0")) + .body("result.contents.uri", CoreMatchers.hasItem("quarkus://resource/build-time/devui/extensions")) + .body("result.contents.text", CoreMatchers.notNullValue()); + + } + +} diff --git a/extensions/devui/resources/src/main/resources/dev-ui/controller/jsonrpc.js b/extensions/devui/resources/src/main/resources/dev-ui/controller/jsonrpc.js index 9f8f54f108231..456407f3ec8c4 100644 --- a/extensions/devui/resources/src/main/resources/dev-ui/controller/jsonrpc.js +++ b/extensions/devui/resources/src/main/resources/dev-ui/controller/jsonrpc.js @@ -137,11 +137,11 @@ export class JsonRpc { const origMethod = target[prop]; - if (typeof origMethod == 'undefined') { + if (typeof origMethod === 'undefined') { return function (...args) { var uid = JsonRpc.messageCounter++; - let method = this._extensionName + "." + prop.toString(); + let method = this._extensionName + "_" + prop.toString(); let params = new Object(); if (args.length > 0) { diff --git a/extensions/devui/resources/src/main/resources/dev-ui/qwc/qwc-continuous-testing.js b/extensions/devui/resources/src/main/resources/dev-ui/qwc/qwc-continuous-testing.js index 5f19ffede26ad..f83b2c4799727 100644 --- a/extensions/devui/resources/src/main/resources/dev-ui/qwc/qwc-continuous-testing.js +++ b/extensions/devui/resources/src/main/resources/dev-ui/qwc/qwc-continuous-testing.js @@ -283,7 +283,7 @@ export class QwcContinuousTesting extends QwcHotReloadElement { linkText="Read more about Continuous Testing"> ${this._renderPlayButton()} - ` + `; } } diff --git a/extensions/devui/resources/src/main/resources/dev-ui/qwc/qwc-dev-mcp-info.js b/extensions/devui/resources/src/main/resources/dev-ui/qwc/qwc-dev-mcp-info.js new file mode 100644 index 0000000000000..9ef287d8e11f3 --- /dev/null +++ b/extensions/devui/resources/src/main/resources/dev-ui/qwc/qwc-dev-mcp-info.js @@ -0,0 +1,101 @@ +import { QwcHotReloadElement, html, css} from 'qwc-hot-reload-element'; +import 'qwc-no-data'; +import { basepath } from 'devui-data'; +import { JsonRpc } from 'jsonrpc'; +import '@vaadin/grid'; + +/** + * This component show details on the MCP Server + */ +export class QwcDevMCPInfo extends QwcHotReloadElement { + jsonRpc = new JsonRpc("devmcp"); + + static styles = css` + .serverDetails { + display: flex; + gap: 20px; + padding-top: 40px; + } + + .serverDetailsText { + display: flex; + flex-direction: column; + gap: 3px; + } + .connected { + display: flex; + flex-direction: column; + gap:10px; + padding: 5px; + } + `; + + static properties = { + _mcpPath: {state: false}, + _connectedClients: {state: true} + } + + constructor() { + super(); + this._mcpPath = null; + this._connectedClients = null; + } + + connectedCallback() { + super.connectedCallback(); + this._mcpPath = window.location.origin + basepath.replace("/dev-ui", "/dev-mcp"); + this._getConnectedClients(); + this._observer = this.jsonRpc.getConnectedClientStream().onNext(jsonRpcResponse => { + this._getConnectedClients(); + }); + } + + disconnectedCallback() { + this._observer.cancel(); + super.disconnectedCallback(); + } + + render() { + if(this._connectedClients){ + return html`
+ ${this._renderServerDetails()} + + + + +
`; + }else{ + return html` + ${this._renderServerDetails()} + + `; + } + } + + hotReload(){ + this._getConnectedClients(); + } + + _getConnectedClients(){ + this.jsonRpc.getConnectedClients().then(jsonRpcResponse => { + this._connectedClients = jsonRpcResponse.result; + }); + + + } + + _renderServerDetails(){ + return html`
+ +
+ Connect to the Quarkus Dev MCP Server with: + Protocol: Remote Streamable HTTP + URL: ${this._mcpPath} +
+ +
`; + } +} +customElements.define('qwc-dev-mcp-info', QwcDevMCPInfo); \ No newline at end of file diff --git a/extensions/devui/resources/src/main/resources/dev-ui/qwc/qwc-dev-mcp-resources.js b/extensions/devui/resources/src/main/resources/dev-ui/qwc/qwc-dev-mcp-resources.js new file mode 100644 index 0000000000000..c0454dcf094b1 --- /dev/null +++ b/extensions/devui/resources/src/main/resources/dev-ui/qwc/qwc-dev-mcp-resources.js @@ -0,0 +1,186 @@ +import { LitElement, html, css} from 'lit'; +import '@vaadin/progress-bar'; +import { JsonRpc } from 'jsonrpc'; +import '@vaadin/grid'; +import { columnBodyRenderer } from '@vaadin/grid/lit.js'; +import '@vaadin/grid/vaadin-grid-sort-column.js'; +import '@vaadin/tabs'; +import '@vaadin/tabsheet'; +import { observeState } from 'lit-element-state'; +import { themeState } from 'theme-state'; +import '@qomponent/qui-code-block'; +import '@vaadin/dialog'; +import { dialogHeaderRenderer, dialogRenderer } from '@vaadin/dialog/lit.js'; + +/** + * This component show all available resources for MCP clients + */ +export class QwcDevMCPResources extends observeState(LitElement) { + jsonRpc = new JsonRpc("resources"); + + static styles = css` + :host { + height: 100%; + display:flex; + width: 100%; + } + + vaadin-tabsheet { + width: 100%; + height: 100%; + } + + vaadin-grid { + height: 100%; + } + `; + + static properties = { + _resources: {state: true}, + _selectedResource: {state: true}, + _selectedResourceContent: {state: true}, + _busyReading: {state: true} + } + + constructor() { + super(); + this._selectedResource = []; + this._selectedResourceContent = null; + this._busyReading = false; + } + + connectedCallback() { + super.connectedCallback(); + this._loadResources(); + } + + render() { + if (this._resources) { + return html`${this._renderResources()}`; + }else{ + return this._renderProgressBar("Fetching resources..."); + } + } + + _renderResources(){ + + let dialogTitle = ""; + if(this._selectedResource.length>0)dialogTitle = this._selectedResource[0].name; + + return html` + html` + + + + `, + [] + )} + ${dialogRenderer(() => this._renderResourceContent(), this._selectedResource)} + > + + + List + Raw json + +
+ + + + + + +
+
+
+ + +
+ + `; + } + + _renderResourceContent(){ + return html`
+ + `; + } + + _loadResources(){ + this.jsonRpc.list().then(jsonRpcResponse => { + this._resources = jsonRpcResponse.result; + }); + } + + _closeDialog(){ + this._selectedResourceContent = null; + } + + _readSelectedResourceContents(){ + if(this._selectedResource.length>0 && !this._busyReading){ + + this._busyReading = true; + this.jsonRpc.read({uri:this._selectedResource[0].uri}).then(jsonRpcResponse => { + + if(jsonRpcResponse.result.contents.length>0){ + let c = jsonRpcResponse.result.contents[0].text; + if(this._isJsonSerializable(c)){ + this._selectedResourceContent = JSON.stringify(c, null, 2); + }else { + this._selectedResourceContent = c; + } + }else{ + this._selectedResourceContent = "No data found"; + } + this._busyReading = false; + }); + } + } + + _isJsonSerializable(value) { + return value !== null && (typeof value === 'object'); + } + + _renderProgressBar(title){ + return html` +
+
${title}
+ +
`; + } + +} +customElements.define('qwc-dev-mcp-resources', QwcDevMCPResources); \ No newline at end of file diff --git a/extensions/devui/resources/src/main/resources/dev-ui/qwc/qwc-dev-mcp-tools.js b/extensions/devui/resources/src/main/resources/dev-ui/qwc/qwc-dev-mcp-tools.js new file mode 100644 index 0000000000000..f9d11c50cc0cc --- /dev/null +++ b/extensions/devui/resources/src/main/resources/dev-ui/qwc/qwc-dev-mcp-tools.js @@ -0,0 +1,272 @@ +import { QwcHotReloadElement, html, css} from 'qwc-hot-reload-element'; +import '@vaadin/progress-bar'; +import { JsonRpc } from 'jsonrpc'; +import '@vaadin/grid'; +import { columnBodyRenderer } from '@vaadin/grid/lit.js'; +import '@vaadin/grid/vaadin-grid-sort-column.js'; +import '@vaadin/tabs'; +import '@vaadin/tabsheet'; +import '@vaadin/text-field'; +import '@vaadin/button'; +import '@vaadin/dialog'; +import { dialogHeaderRenderer, dialogRenderer } from '@vaadin/dialog/lit.js'; +import '@vaadin/vertical-layout'; +import 'qui-themed-code-block'; +import '@qomponent/qui-badge'; + +/** + * This component show all available tools for MCP clients + */ +export class QwcDevMCPTools extends QwcHotReloadElement { + jsonRpc = new JsonRpc("tools"); + + static styles = css` + :host { + height: 100%; + display:flex; + width: 100%; + } + + vaadin-tabsheet { + width: 100%; + height: 100%; + } + + vaadin-grid { + height: 100%; + } + `; + + static properties = { + _tools: {state: true}, + _showInputDialog: {state: true, type: Boolean}, + _selectedTool: {state: true}, + _inputvalues: { type: Object }, + _toolResult: {state: true} + } + + constructor() { + super(); + this._showInputDialog = false; + this._selectedTool = []; + this._inputvalues = new Map(); + this._toolResult = null; + } + + connectedCallback() { + super.connectedCallback(); + this._loadTools(); + this._inputvalues.clear(); + } + + render() { + if (this._tools) { + return html`${this._renderTools()}`; + }else{ + return html` +
+
Fetching tools...
+ +
+ `; + } + } + + hotReload(){ + this._loadTools(); + this._inputvalues.clear(); + } + + _renderTools(){ + return html` + html` + + + + `, + [] + )} + ${dialogRenderer(() => this._renderToolResult())} + > + + html` + + + + `, + [] + )} + ${dialogRenderer(this._renderToolInput)} + > + + + + List + Raw json + +
+ + + + + + + +
+
+
+ + +
+ + `; + } + + _renderToolResult(){ + return html`
+ + `; + } + + _renderToolInput(){ + if(this._selectedTool.length>0){ + let prop = this._selectedTool[0]; + + const keys = Object.keys(prop.inputSchema.properties); + + return html` + ${keys.map( + (key) => html` + this._updateSelectedValue(prop.name, key, e)} + @blur=${(e) => this._updateSelectedValue(prop.name, key, e)} + > + ` + )} + Test + `; + } + } + + _closeDialog(){ + this._toolResult = null; + this._showInputDialog = false; + } + + _noOfParameterRenderer(prop) { + let parameters = Object.keys(prop.inputSchema.properties); + const propertyCount = parameters.length; + if(propertyCount>0) { + return html`${propertyCount}`; + } + } + + _updateSelectedValue(name, key, e){ + let params = new Map(); + if(this._inputvalues.has(name)){ + params = this._inputvalues.get(name); + } + + params.set(key, e.target.value); + + this._inputvalues.set(name, params); + + } + + _getInputValuesAndTest(prop){ + this._showInputDialog = false; + let params = null; + if(this._inputvalues.has(prop.name)){ + params = this._inputvalues.get(prop.name); + this._testJsonRpcCall(prop, params); + } + + } + + _testJsonRpcCall(prop, params){ + const [namespace, method] = prop.name.split('_'); + + let rpcClient = new JsonRpc(namespace); + + if(params){ + rpcClient[method](Object.fromEntries(params)).then(jsonRpcResponse => { + this._setToolResult(jsonRpcResponse.result); + }); + }else { + rpcClient[method]().then(jsonRpcResponse => { + this._setToolResult(jsonRpcResponse.result); + }); + } + } + + _setToolResult(result){ + if(this._isJsonSerializable(result)){ + this._toolResult = JSON.stringify(result, null, 2); + }else { + this._toolResult = result; + } + } + + _loadTools(){ + this.jsonRpc.list().then(jsonRpcResponse => { + this._tools = jsonRpcResponse.result; + }); + } + + _isJsonSerializable(value) { + return value !== null && (typeof value === 'object'); + } + +} +customElements.define('qwc-dev-mcp-tools', QwcDevMCPTools); \ No newline at end of file diff --git a/extensions/devui/resources/src/main/resources/dev-ui/qwc/qwc-workspace.js b/extensions/devui/resources/src/main/resources/dev-ui/qwc/qwc-workspace.js index 6cef6cb15474c..b5c6a9a9148fa 100644 --- a/extensions/devui/resources/src/main/resources/dev-ui/qwc/qwc-workspace.js +++ b/extensions/devui/resources/src/main/resources/dev-ui/qwc/qwc-workspace.js @@ -105,15 +105,6 @@ export class QwcWorkspace extends observeState(QwcHotReloadElement) { justify-content: end; padding-right: 10px; } - - .assistant { - position: absolute; - top: 0; - right: 0; - padding-top: 8px; - padding-right: 16px; - z-index:9; - } .actionResult { display: flex; @@ -177,6 +168,7 @@ export class QwcWorkspace extends observeState(QwcHotReloadElement) { } disconnectedCallback() { + document.body.style.cursor = 'default'; RouterController.unregisterGuardedComponent(this); window.removeEventListener('beforeunload', this._beforeUnloadHandler); super.disconnectedCallback(); @@ -296,7 +288,7 @@ export class QwcWorkspace extends observeState(QwcHotReloadElement) { _renderResultDialog(){ if(this._actionResult && this._actionResult.content && this._actionResult.display === "dialog"){ - return html``; + return html``; }else{ return html`
@@ -394,24 +386,19 @@ export class QwcWorkspace extends observeState(QwcHotReloadElement) { } _renderTextContent(){ - return html` - ${this._renderAssistantWarningInline()}`; - } - - _renderAssistantWarningInline(){ - if(this._selectedWorkspaceItem.isAssistant){ - return html``; - } + `; } _renderAssistantWarning(){ - if(this._actionResult.isAssistant){ + if((this._selectedWorkspaceItem && this._selectedWorkspaceItem.isAssistant) || this._actionResult && this._actionResult.isAssistant){ return html``; } } @@ -465,6 +452,7 @@ export class QwcWorkspace extends observeState(QwcHotReloadElement) { _actionSelected(e){ this._showActionProgress = true; + document.body.style.cursor = 'progress'; this._clearActionResult(); let actionId = e.detail.value.id; @@ -523,6 +511,7 @@ export class QwcWorkspace extends observeState(QwcHotReloadElement) { this._actionResult.displayType = e.detail.value.displayType; } this._showActionProgress = false; + document.body.style.cursor = 'default'; }); } @@ -659,6 +648,7 @@ export class QwcWorkspace extends observeState(QwcHotReloadElement) { this._clearActionResult(); this._showActionProgress = false; + document.body.style.cursor = 'default'; } shouldConfirmAwayNavigation(){ diff --git a/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/DevUIBuildTimeStaticHandler.java b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/DevUIBuildTimeStaticHandler.java index ba83eaaadc121..daab2f5a8b632 100644 --- a/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/DevUIBuildTimeStaticHandler.java +++ b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/DevUIBuildTimeStaticHandler.java @@ -3,7 +3,8 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Map; + +import jakarta.enterprise.inject.spi.CDI; import io.vertx.core.Handler; import io.vertx.core.buffer.Buffer; @@ -13,36 +14,12 @@ * Handler to return the "static" content created a build time */ public class DevUIBuildTimeStaticHandler implements Handler { - private Map urlAndPath; - private String basePath; // Like /q/dev-ui - - public DevUIBuildTimeStaticHandler() { - - } - - public DevUIBuildTimeStaticHandler(String basePath, Map urlAndPath) { - this.basePath = basePath; - this.urlAndPath = urlAndPath; - } - - public Map getUrlAndPath() { - return urlAndPath; - } - - public void setUrlAndPath(Map urlAndPath) { - this.urlAndPath = urlAndPath; - } - - public String getBasePath() { - return basePath; - } - - public void setBasePath(String basePath) { - this.basePath = basePath; - } @Override public void handle(RoutingContext event) { + + DevUIBuildTimeStaticService buildTimeStaticService = CDI.current().select(DevUIBuildTimeStaticService.class).get(); + String normalizedPath = event.normalizedPath(); if (normalizedPath.contains(SLASH)) { int si = normalizedPath.lastIndexOf(SLASH) + 1; @@ -50,8 +27,9 @@ public void handle(RoutingContext event) { String fileName = normalizedPath.substring(si); // TODO: Handle params ? - if (path.startsWith(basePath) && this.urlAndPath.containsKey(fileName)) { - String pathOnDisk = this.urlAndPath.get(fileName); + if (path.startsWith(buildTimeStaticService.getBasePath()) + && buildTimeStaticService.getUrlAndPath().containsKey(fileName)) { + String pathOnDisk = buildTimeStaticService.getUrlAndPath().get(fileName); try { byte[] content = Files.readAllBytes(Path.of(pathOnDisk)); diff --git a/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/DevUIBuildTimeStaticService.java b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/DevUIBuildTimeStaticService.java new file mode 100644 index 0000000000000..cf782593160d2 --- /dev/null +++ b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/DevUIBuildTimeStaticService.java @@ -0,0 +1,31 @@ +package io.quarkus.devui.runtime; + +import java.util.HashMap; +import java.util.Map; + +/** + * Holds information of Build Time static data + */ +public class DevUIBuildTimeStaticService { + private final Map urlAndPath = new HashMap<>(); + private final Map descriptions = new HashMap<>(); + private String basePath; // Like /q/dev-ui + + public void addData(String basePath, Map urlAndPath, Map descriptions) { + this.basePath = basePath; + this.urlAndPath.putAll(urlAndPath); + this.descriptions.putAll(descriptions); + } + + public Map getUrlAndPath() { + return urlAndPath; + } + + public Map getDescriptions() { + return descriptions; + } + + public String getBasePath() { + return basePath; + } +} \ No newline at end of file diff --git a/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/DevUIRecorder.java b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/DevUIRecorder.java index 75204fc2d88a9..7bad23cd42ecc 100644 --- a/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/DevUIRecorder.java +++ b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/DevUIRecorder.java @@ -21,11 +21,11 @@ import io.quarkus.arc.runtime.BeanContainer; import io.quarkus.dev.console.DevConsoleManager; import io.quarkus.devui.runtime.comms.JsonRpcRouter; +import io.quarkus.devui.runtime.js.DevUIWebSocketHandler; import io.quarkus.devui.runtime.jsonrpc.JsonRpcMethod; -import io.quarkus.devui.runtime.jsonrpc.JsonRpcMethodName; import io.quarkus.devui.runtime.jsonrpc.json.JsonMapper; import io.quarkus.devui.runtime.jsonrpc.json.JsonTypeAdapter; -import io.quarkus.runtime.RuntimeValue; +import io.quarkus.devui.runtime.mcp.McpHttpHandler; import io.quarkus.runtime.ShutdownContext; import io.quarkus.runtime.annotations.Recorder; import io.quarkus.vertx.http.runtime.devmode.FileSystemStaticHandler; @@ -46,16 +46,16 @@ public void shutdownTask(ShutdownContext shutdownContext, String devUIBasePath) } public void createJsonRpcRouter(BeanContainer beanContainer, - Map> extensionMethodsMap, - List deploymentMethods, - List deploymentSubscriptions, - Map recordedValues) { + Map runtimeMethods, + Map runtimeSubscriptions, + Map deploymentMethods, + Map deploymentSubscriptions, + Map recordedMethods, + Map recordedSubscriptions) { + JsonRpcRouter jsonRpcRouter = beanContainer.beanInstance(JsonRpcRouter.class); - jsonRpcRouter.populateJsonRPCRuntimeMethods(extensionMethodsMap); - jsonRpcRouter.setJsonRPCDeploymentActions(deploymentMethods, deploymentSubscriptions); - if (recordedValues != null && !recordedValues.isEmpty()) { - jsonRpcRouter.setRecordedValues(recordedValues); - } + jsonRpcRouter.populateJsonRpcEndpoints(runtimeMethods, runtimeSubscriptions, deploymentMethods, deploymentSubscriptions, + recordedMethods, recordedSubscriptions); jsonRpcRouter.initializeCodec(createJsonMapper()); } @@ -68,7 +68,7 @@ private JsonMapper createJsonMapper() { // We need to pass some information so that the mapper, who lives in the deployment classloader, // knows how to deal with JsonObject/JsonArray/JsonBuffer, who live in the runtime classloader. return factory.create(new JsonTypeAdapter<>(JsonObject.class, JsonObject::getMap, JsonObject::new), - new JsonTypeAdapter>(JsonArray.class, JsonArray::getList, JsonArray::new), + new JsonTypeAdapter<>(JsonArray.class, JsonArray::getList, JsonArray::new), new JsonTypeAdapter<>(Buffer.class, buffer -> BASE64_ENCODER.encodeToString(buffer.getBytes()), text -> { try { return Buffer.buffer(BASE64_DECODER.decode(text)); @@ -78,8 +78,12 @@ private JsonMapper createJsonMapper() { })); } - public Handler communicationHandler() { - return new DevUIWebSocket(); + public Handler devUIWebSocketHandler() { + return new DevUIWebSocketHandler(); + } + + public Handler mcpStreamableHTTPHandler(String quarkusVersion) { + return new McpHttpHandler(quarkusVersion, createJsonMapper()); } public Handler uiHandler(String finalDestination, @@ -92,8 +96,14 @@ public Handler uiHandler(String finalDestination, return handler; } - public Handler buildTimeStaticHandler(String basePath, Map urlAndPath) { - return new DevUIBuildTimeStaticHandler(basePath, urlAndPath); + public Handler buildTimeStaticHandler(BeanContainer beanContainer, + String basePath, + Map urlAndPath, + Map descriptions) { + DevUIBuildTimeStaticService buildTimeStaticService = beanContainer.beanInstance(DevUIBuildTimeStaticService.class); + buildTimeStaticService.addData(basePath, urlAndPath, descriptions); + + return new DevUIBuildTimeStaticHandler(); } public Handler endpointInfoHandler(String basePath) { diff --git a/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/comms/JsonRpcResponseWriter.java b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/comms/JsonRpcResponseWriter.java new file mode 100644 index 0000000000000..d22f2e113d941 --- /dev/null +++ b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/comms/JsonRpcResponseWriter.java @@ -0,0 +1,15 @@ +package io.quarkus.devui.runtime.comms; + +public interface JsonRpcResponseWriter { + + void write(String message); + + void close(); + + boolean isOpen(); + + boolean isClosed(); + + Object decorateObject(Object object, MessageType messageType); + +} \ No newline at end of file diff --git a/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/comms/JsonRpcRouter.java b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/comms/JsonRpcRouter.java index 103329b259e88..21e0bbc18d9da 100644 --- a/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/comms/JsonRpcRouter.java +++ b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/comms/JsonRpcRouter.java @@ -18,12 +18,11 @@ import io.quarkus.arc.DefaultBean; import io.quarkus.assistant.runtime.dev.Assistant; import io.quarkus.dev.console.DevConsoleManager; +import io.quarkus.devui.runtime.js.JavaScriptResponseWriter; import io.quarkus.devui.runtime.jsonrpc.JsonRpcCodec; import io.quarkus.devui.runtime.jsonrpc.JsonRpcMethod; -import io.quarkus.devui.runtime.jsonrpc.JsonRpcMethodName; import io.quarkus.devui.runtime.jsonrpc.JsonRpcRequest; import io.quarkus.devui.runtime.jsonrpc.json.JsonMapper; -import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.StartupEvent; import io.smallrye.mutiny.Multi; import io.smallrye.mutiny.Uni; @@ -37,19 +36,24 @@ */ public class JsonRpcRouter { - private final Map subscriptions = new ConcurrentHashMap<>(); + private final Map activeSubscriptions = new ConcurrentHashMap<>(); // Map json-rpc method to java in runtime classpath - private final Map jsonRpcToRuntimeClassPathJava = new HashMap<>(); + private Map runtimeMethodsMap; + // Map json-rpc subscriptions to java in runtime classpath + private Map runtimeSubscriptionMap; // Map json-rpc method to java in deployment classpath - private final List jsonRpcMethodToDeploymentClassPathJava = new ArrayList<>(); + private Map deploymentMethodsMap; // Map json-rpc subscriptions to java in deployment classpath - private final List jsonRpcSubscriptionToDeploymentClassPathJava = new ArrayList<>(); + private Map deploymentSubscriptionsMap; + // Map json-rpc methods responses that is recorded - private final Map recordedValues = new HashMap<>(); + private Map recordedMethodsMap; + // Map json-rpc subscriptions responses that is recorded + private Map recordedSubscriptionsMap; - private static final List SESSIONS = Collections.synchronizedList(new ArrayList<>()); + private static final List SESSIONS = Collections.synchronizedList(new ArrayList<>()); private JsonRpcCodec codec; @Produces @@ -59,52 +63,30 @@ public Optional defaultAssistant() { } /** - * This gets called on build to build into of the classes we are going to call in runtime + * This gets populated at build time so the the routes knows all json-rpc endpoints. * - * @param extensionMethodsMap + * @param runtimeMethods + * @param runtimeSubscriptions + * @param deploymentMethods + * @param deploymentSubscriptions + * @param recordedMethods + * @param recordedSubscriptions */ - public void populateJsonRPCRuntimeMethods(Map> extensionMethodsMap) { - for (Map.Entry> extension : extensionMethodsMap.entrySet()) { - String extensionName = extension.getKey(); - Map jsonRpcMethods = extension.getValue(); - for (Map.Entry method : jsonRpcMethods.entrySet()) { - JsonRpcMethodName methodName = method.getKey(); - JsonRpcMethod jsonRpcMethod = method.getValue(); - - @SuppressWarnings("unchecked") - Object providerInstance = Arc.container().select(jsonRpcMethod.getClazz()).get(); - - try { - Method javaMethod; - Map params = null; - if (jsonRpcMethod.hasParams()) { - params = jsonRpcMethod.getParams(); - javaMethod = providerInstance.getClass().getMethod(jsonRpcMethod.getMethodName(), - params.values().toArray(new Class[] {})); - } else { - javaMethod = providerInstance.getClass().getMethod(jsonRpcMethod.getMethodName()); - } - ReflectionInfo reflectionInfo = new ReflectionInfo(jsonRpcMethod.getClazz(), providerInstance, javaMethod, - params, jsonRpcMethod.getExplicitlyBlocking(), jsonRpcMethod.getExplicitlyNonBlocking()); - String jsonRpcMethodName = extensionName + DOT + methodName; - jsonRpcToRuntimeClassPathJava.put(jsonRpcMethodName, reflectionInfo); - } catch (NoSuchMethodException | SecurityException ex) { - throw new RuntimeException(ex); - } - } - } - } + public void populateJsonRpcEndpoints(Map runtimeMethods, + Map runtimeSubscriptions, + Map deploymentMethods, + Map deploymentSubscriptions, + Map recordedMethods, + Map recordedSubscriptions) { - public void setJsonRPCDeploymentActions(List methods, List subscriptions) { - this.jsonRpcMethodToDeploymentClassPathJava.clear(); - this.jsonRpcMethodToDeploymentClassPathJava.addAll(methods); - this.jsonRpcSubscriptionToDeploymentClassPathJava.clear(); - this.jsonRpcSubscriptionToDeploymentClassPathJava.addAll(subscriptions); - } + this.runtimeMethodsMap = enhanceRuntimeJsonRpcEndpoints(runtimeMethods); + this.runtimeSubscriptionMap = enhanceRuntimeJsonRpcEndpoints(runtimeSubscriptions); + + this.deploymentMethodsMap = deploymentMethods; + this.deploymentSubscriptionsMap = deploymentSubscriptions; - public void setRecordedValues(Map recordedValues) { - this.recordedValues.clear(); - this.recordedValues.putAll(recordedValues); + this.recordedMethodsMap = recordedMethods; + this.recordedSubscriptionsMap = recordedSubscriptions; } public void initializeCodec(JsonMapper jsonMapper) { @@ -112,127 +94,110 @@ public void initializeCodec(JsonMapper jsonMapper) { } public void addSocket(ServerWebSocket socket) { - SESSIONS.add(socket); + JavaScriptResponseWriter writer = new JavaScriptResponseWriter(socket); + SESSIONS.add(writer); socket.textMessageHandler((e) -> { JsonRpcRequest jsonRpcRequest = codec.readRequest(e); - route(jsonRpcRequest, socket); + route(jsonRpcRequest, writer); }).closeHandler((e) -> { purge(); }); purge(); } + @Produces + public JsonRpcCodec getJsonRpcCodec() { + return this.codec; + } + + public Map getRuntimeMethodsMap() { + return runtimeMethodsMap; + } + + public Map getRuntimeSubscriptionMap() { + return runtimeSubscriptionMap; + } + + public Map getDeploymentMethodsMap() { + return deploymentMethodsMap; + } + + public Map getDeploymentSubscriptionsMap() { + return deploymentSubscriptionsMap; + } + + public Map getRecordedMethodsMap() { + return recordedMethodsMap; + } + + public Map getRecordedSubscriptionsMap() { + return recordedSubscriptionsMap; + } + void onStart(@Observes StartupEvent ev) { purge(); - for (ServerWebSocket s : new ArrayList<>(SESSIONS)) { - if (!s.isClosed()) { - codec.writeResponse(s, -1, LocalDateTime.now().toString(), MessageType.HotReload); + for (JsonRpcResponseWriter jrrw : new ArrayList<>(SESSIONS)) { + if (!jrrw.isClosed()) { + codec.writeResponse(jrrw, -1, LocalDateTime.now().toString(), MessageType.HotReload); } } } private void purge() { - for (ServerWebSocket s : new ArrayList<>(SESSIONS)) { - if (s.isClosed()) { - SESSIONS.remove(s); - } - } + SESSIONS.removeIf(JsonRpcResponseWriter::isClosed); } @Inject Logger logger; @SuppressWarnings("unchecked") - private void route(JsonRpcRequest jsonRpcRequest, ServerWebSocket s) { + public void route(JsonRpcRequest jsonRpcRequest, JsonRpcResponseWriter jrrw) { String jsonRpcMethodName = jsonRpcRequest.getMethod(); - if (jsonRpcMethodName.equalsIgnoreCase(UNSUBSCRIBE)) {// First check some internal methods - this.routeUnsubscribe(jsonRpcRequest, s); - } else if (this.jsonRpcToRuntimeClassPathJava.containsKey(jsonRpcMethodName)) { // Route to extension (runtime) - this.routeToRuntime(jsonRpcRequest, s); - } else if (this.jsonRpcMethodToDeploymentClassPathJava.contains(jsonRpcMethodName) - || this.jsonRpcSubscriptionToDeploymentClassPathJava.contains(jsonRpcMethodName)) { // Route to extension (deployment) - this.routeToDeployment(jsonRpcRequest, s); + if (jsonRpcMethodName.equalsIgnoreCase(UNSUBSCRIBE)) { // TODO: Move to protocol specific ? + // This is a Dev UI subscription that terminated + this.routeToDevUIUnsubscribe(jsonRpcRequest, jrrw); + } else if (this.runtimeMethodsMap.containsKey(jsonRpcMethodName)) { + // This is a Runtime method that needs to route to the extension + this.routeToRuntimeMethod(jsonRpcRequest, jrrw); + } else if (this.runtimeSubscriptionMap.containsKey(jsonRpcMethodName)) { + // This is a Runtime subscription that needs to route to the extension + this.routeToRuntimeSubscription(jsonRpcRequest, jrrw); + } else if (this.deploymentMethodsMap.containsKey(jsonRpcMethodName) + || this.deploymentSubscriptionsMap.containsKey(jsonRpcMethodName) + || this.recordedMethodsMap.containsKey(jsonRpcMethodName) + || this.recordedSubscriptionsMap.containsKey(jsonRpcMethodName)) { + // This is Deployment method that needs to route to the extension + this.routeToDeployment(jsonRpcRequest, jrrw); } else { - // Method not found - codec.writeMethodNotFoundResponse(s, jsonRpcRequest.getId(), jsonRpcMethodName); + // This is an error. Method not found + codec.writeMethodNotFoundResponse(jrrw, jsonRpcRequest.getId(), jsonRpcMethodName); } } - private void routeUnsubscribe(JsonRpcRequest jsonRpcRequest, ServerWebSocket s) { - if (this.subscriptions.containsKey(jsonRpcRequest.getId())) { - Cancellable cancellable = this.subscriptions.remove(jsonRpcRequest.getId()); + private void routeToDevUIUnsubscribe(JsonRpcRequest jsonRpcRequest, JsonRpcResponseWriter jrrw) { + if (this.activeSubscriptions.containsKey(jsonRpcRequest.getId())) { + Cancellable cancellable = this.activeSubscriptions.remove(jsonRpcRequest.getId()); cancellable.cancel(); } - codec.writeResponse(s, jsonRpcRequest.getId(), null, MessageType.Void); + codec.writeResponse(jrrw, jsonRpcRequest.getId(), null, MessageType.Void); } - private void routeToRuntime(JsonRpcRequest jsonRpcRequest, ServerWebSocket s) { - String jsonRpcMethodName = jsonRpcRequest.getMethod(); - ReflectionInfo reflectionInfo = this.jsonRpcToRuntimeClassPathJava.get(jsonRpcMethodName); - Object target = Arc.container().select(reflectionInfo.bean).get(); - - if (reflectionInfo.isReturningMulti()) { - this.routeToRuntimeSubscription(jsonRpcRequest, s, jsonRpcMethodName, reflectionInfo, target); - } else { - // The invocation will return a Uni - this.routeToRuntimeMethod(jsonRpcRequest, s, jsonRpcMethodName, reflectionInfo, target); - } - } - - private void routeToRuntimeSubscription(JsonRpcRequest jsonRpcRequest, ServerWebSocket s, String jsonRpcMethodName, - ReflectionInfo reflectionInfo, Object target) { - - if (this.subscriptions.containsKey(jsonRpcRequest.getId())) { - // Cancel and resubscribe - Cancellable cancellable = this.subscriptions.remove(jsonRpcRequest.getId()); - cancellable.cancel(); - } - - Multi multi; - try { - if (jsonRpcRequest.hasParams()) { - Object[] args = getArgsAsObjects(reflectionInfo.params, jsonRpcRequest); - multi = (Multi) reflectionInfo.method.invoke(target, args); - } else { - multi = (Multi) reflectionInfo.method.invoke(target); - } - } catch (Exception e) { - logger.errorf(e, "Unable to invoke method %s using JSON-RPC, request was: %s", jsonRpcMethodName, - jsonRpcRequest); - codec.writeErrorResponse(s, jsonRpcRequest.getId(), jsonRpcMethodName, e); - return; - } - - Cancellable cancellable = multi.subscribe() - .with( - item -> { - codec.writeResponse(s, jsonRpcRequest.getId(), item, MessageType.SubscriptionMessage); - }, - failure -> { - codec.writeErrorResponse(s, jsonRpcRequest.getId(), jsonRpcMethodName, failure); - this.subscriptions.remove(jsonRpcRequest.getId()); - }, - () -> this.subscriptions.remove(jsonRpcRequest.getId())); - - this.subscriptions.put(jsonRpcRequest.getId(), cancellable); - codec.writeResponse(s, jsonRpcRequest.getId(), null, MessageType.Void); - } - - private void routeToRuntimeMethod(JsonRpcRequest jsonRpcRequest, ServerWebSocket s, String jsonRpcMethodName, - ReflectionInfo reflectionInfo, Object target) { + private void routeToRuntimeMethod(JsonRpcRequest jsonRpcRequest, JsonRpcResponseWriter jrrw) { + JsonRpcMethod runtimeJsonRpcMethod = this.runtimeMethodsMap.get(jsonRpcRequest.getMethod()); + Object target = Arc.container().select(runtimeJsonRpcMethod.getBean()).get(); // Lookup bean Uni uni; try { + Object[] args = new Object[0]; if (jsonRpcRequest.hasParams()) { - Object[] args = getArgsAsObjects(reflectionInfo.params, jsonRpcRequest); - uni = invoke(reflectionInfo, target, args); - } else { - uni = invoke(reflectionInfo, target, new Object[0]); + args = getArgsAsObjects(runtimeJsonRpcMethod.getParameters(), jsonRpcRequest); } + uni = invoke(runtimeJsonRpcMethod, target, args); + } catch (Exception e) { - logger.errorf(e, "Unable to invoke method %s using JSON-RPC, request was: %s", jsonRpcMethodName, + logger.errorf(e, "Unable to invoke method %s using JSON-RPC, request was: %s", jsonRpcRequest.getMethod(), jsonRpcRequest); - codec.writeErrorResponse(s, jsonRpcRequest.getId(), jsonRpcMethodName, e); + codec.writeErrorResponse(jrrw, jsonRpcRequest.getId(), jsonRpcRequest.getMethod(), e); return; } uni.subscribe() @@ -242,13 +207,13 @@ private void routeToRuntimeMethod(JsonRpcRequest jsonRpcRequest, ServerWebSocket Object response = jsonRpcMessage.getResponse(); if (jsonRpcMessage.isAlreadySerialized()) { // The message response was already serialized, write text directly to socket - s.writeTextMessage("{\"id\":" + jsonRpcRequest.getId() + ",\"result\":{\"messageType\":\"" + jrrw.write("{\"id\":" + jsonRpcRequest.getId() + ",\"result\":{\"messageType\":\"" + jsonRpcMessage.getMessageType().name() + "\",\"object\":" + response + "}}"); } else { - codec.writeResponse(s, jsonRpcRequest.getId(), response, jsonRpcMessage.getMessageType()); + codec.writeResponse(jrrw, jsonRpcRequest.getId(), response, jsonRpcMessage.getMessageType()); } } else { - codec.writeResponse(s, jsonRpcRequest.getId(), item, MessageType.Response); + codec.writeResponse(jrrw, jsonRpcRequest.getId(), item, MessageType.Response); } }, failure -> { Throwable actualFailure; @@ -263,25 +228,66 @@ private void routeToRuntimeMethod(JsonRpcRequest jsonRpcRequest, ServerWebSocket } else { actualFailure = failure; } - codec.writeErrorResponse(s, jsonRpcRequest.getId(), jsonRpcMethodName, actualFailure); + codec.writeErrorResponse(jrrw, jsonRpcRequest.getId(), jsonRpcRequest.getMethod(), actualFailure); }); } - private void routeToDeployment(JsonRpcRequest jsonRpcRequest, ServerWebSocket s) { + private void routeToRuntimeSubscription(JsonRpcRequest jsonRpcRequest, JsonRpcResponseWriter jrrw) { + JsonRpcMethod runtimeJsonRpcSubscription = this.runtimeSubscriptionMap.get(jsonRpcRequest.getMethod()); + Object target = Arc.container().select(runtimeJsonRpcSubscription.getBean()).get(); // Lookup bean + + if (this.activeSubscriptions.containsKey(jsonRpcRequest.getId())) { + // Cancel and resubscribe + Cancellable cancellable = this.activeSubscriptions.remove(jsonRpcRequest.getId()); + cancellable.cancel(); + } + + Multi multi; + try { + if (jsonRpcRequest.hasParams()) { + Object[] args = getArgsAsObjects(runtimeJsonRpcSubscription.getParameters(), jsonRpcRequest); + multi = (Multi) runtimeJsonRpcSubscription.getJavaMethod().invoke(target, args); + } else { + multi = (Multi) runtimeJsonRpcSubscription.getJavaMethod().invoke(target); + } + } catch (Exception e) { + logger.errorf(e, "Unable to invoke method %s using JSON-RPC, request was: %s", jsonRpcRequest.getMethod(), + jsonRpcRequest); + codec.writeErrorResponse(jrrw, jsonRpcRequest.getId(), jsonRpcRequest.getMethod(), e); + return; + } + + Cancellable cancellable = multi.subscribe() + .with( + item -> { + codec.writeResponse(jrrw, jsonRpcRequest.getId(), item, MessageType.SubscriptionMessage); + }, + failure -> { + codec.writeErrorResponse(jrrw, jsonRpcRequest.getId(), jsonRpcRequest.getMethod(), failure); + this.activeSubscriptions.remove(jsonRpcRequest.getId()); + }, + () -> this.activeSubscriptions.remove(jsonRpcRequest.getId())); + + this.activeSubscriptions.put(jsonRpcRequest.getId(), cancellable); + codec.writeResponse(jrrw, jsonRpcRequest.getId(), null, MessageType.Void); + } + + private void routeToDeployment(JsonRpcRequest jsonRpcRequest, JsonRpcResponseWriter jrrw) { String jsonRpcMethodName = jsonRpcRequest.getMethod(); - if (this.subscriptions.containsKey(jsonRpcRequest.getId())) { + if (this.activeSubscriptions.containsKey(jsonRpcRequest.getId())) { // Cancel and resubscribe - Cancellable cancellable = this.subscriptions.remove(jsonRpcRequest.getId()); + Cancellable cancellable = this.activeSubscriptions.remove(jsonRpcRequest.getId()); cancellable.cancel(); } Object returnedObject = null; - if (this.recordedValues.containsKey(jsonRpcMethodName)) { - returnedObject = this.recordedValues.get(jsonRpcMethodName).getValue(); + if (this.recordedMethodsMap.containsKey(jsonRpcMethodName)) { + returnedObject = this.recordedMethodsMap.get(jsonRpcMethodName).getRuntimeValue().getValue(); + } else if (this.recordedSubscriptionsMap.containsKey(jsonRpcMethodName)) { + returnedObject = this.recordedSubscriptionsMap.get(jsonRpcMethodName).getRuntimeValue().getValue(); } else { - returnedObject = DevConsoleManager.invoke(jsonRpcMethodName, - getArgsAsMap(jsonRpcRequest)); + returnedObject = DevConsoleManager.invoke(jsonRpcMethodName, getArgsAsMap(jsonRpcRequest)); } if (returnedObject != null) { // Support for Mutiny is diffcult because we are between the runtime and deployment classpath. @@ -292,72 +298,63 @@ private void routeToDeployment(JsonRpcRequest jsonRpcRequest, ServerWebSocket s) Cancellable cancellable = Multi.createFrom().publisher(publisher).subscribe() .with( item -> { - codec.writeResponse(s, jsonRpcRequest.getId(), item, MessageType.SubscriptionMessage); + codec.writeResponse(jrrw, jsonRpcRequest.getId(), item, MessageType.SubscriptionMessage); }, failure -> { - codec.writeErrorResponse(s, jsonRpcRequest.getId(), jsonRpcMethodName, failure); - this.subscriptions.remove(jsonRpcRequest.getId()); + codec.writeErrorResponse(jrrw, jsonRpcRequest.getId(), jsonRpcMethodName, failure); + this.activeSubscriptions.remove(jsonRpcRequest.getId()); }, - () -> this.subscriptions.remove(jsonRpcRequest.getId())); + () -> this.activeSubscriptions.remove(jsonRpcRequest.getId())); - this.subscriptions.put(jsonRpcRequest.getId(), cancellable); - codec.writeResponse(s, jsonRpcRequest.getId(), null, MessageType.Void); + this.activeSubscriptions.put(jsonRpcRequest.getId(), cancellable); + codec.writeResponse(jrrw, jsonRpcRequest.getId(), null, MessageType.Void); } else if (returnedObject instanceof CompletionStage) { CompletionStage future = (CompletionStage) returnedObject; future.thenAccept(r -> { - codec.writeResponse(s, jsonRpcRequest.getId(), r, + codec.writeResponse(jrrw, jsonRpcRequest.getId(), r, MessageType.Response); }).exceptionally(throwable -> { - codec.writeErrorResponse(s, jsonRpcRequest.getId(), jsonRpcMethodName, throwable); + codec.writeErrorResponse(jrrw, jsonRpcRequest.getId(), jsonRpcMethodName, throwable); return null; }); } else { - codec.writeResponse(s, jsonRpcRequest.getId(), returnedObject, + codec.writeResponse(jrrw, jsonRpcRequest.getId(), returnedObject, MessageType.Response); } } } - private Uni invoke(ReflectionInfo info, Object target, Object[] args) { - if (info.isReturningUni()) { + private Uni invoke(JsonRpcMethod runtimeJsonRpcMethod, Object target, Object[] args) { + Uni uni; + if (runtimeJsonRpcMethod.isReturningUni()) { try { - Uni uni = ((Uni) info.method.invoke(target, args)); - if (info.isExplicitlyBlocking()) { - return uni.runSubscriptionOn(Infrastructure.getDefaultExecutor()); - } else { - return uni; - } + uni = ((Uni) runtimeJsonRpcMethod.getJavaMethod().invoke(target, args)); } catch (Exception e) { return Uni.createFrom().failure(e); } - } else if (info.isReturningCompletionStage()) { + } else if (runtimeJsonRpcMethod.isReturningCompletableFuture() || runtimeJsonRpcMethod.isReturningCompletionStage()) { try { - Uni uni = Uni.createFrom() - .completionStage(Unchecked.supplier(() -> (CompletionStage) info.method.invoke(target, args))); - if (info.isExplicitlyBlocking()) { - return uni.runSubscriptionOn(Infrastructure.getDefaultExecutor()); - } else { - return uni; - } + uni = Uni.createFrom() + .completionStage((CompletionStage) runtimeJsonRpcMethod.getJavaMethod().invoke(target, args)); } catch (Exception e) { return Uni.createFrom().failure(e); } } else { - - Uni uni = Uni.createFrom().item(Unchecked.supplier(() -> info.method.invoke(target, args))); - if (!info.isExplicitlyNonBlocking()) { - return uni.runSubscriptionOn(Infrastructure.getDefaultExecutor()); - } else { - return uni; - } + uni = Uni.createFrom() + .item(Unchecked.supplier(() -> runtimeJsonRpcMethod.getJavaMethod().invoke(target, args))); + } + if (!runtimeJsonRpcMethod.isIsExplicitlyNonBlocking()) { + return uni.runSubscriptionOn(Infrastructure.getDefaultExecutor()); + } else { + return uni; } } - private Object[] getArgsAsObjects(Map params, JsonRpcRequest jsonRpcRequest) { + private Object[] getArgsAsObjects(Map parameters, JsonRpcRequest jsonRpcRequest) { List objects = new ArrayList<>(); - for (Map.Entry expectedParams : params.entrySet()) { + for (Map.Entry expectedParams : parameters.entrySet()) { String paramName = expectedParams.getKey(); - Class paramType = expectedParams.getValue(); + Class paramType = expectedParams.getValue().getType(); Object param = jsonRpcRequest.getParam(paramName, paramType); objects.add(param); } @@ -375,7 +372,35 @@ public JsonMapper getJsonMapper() { return codec.getJsonMapper(); } - private static final String DOT = "."; private static final String UNSUBSCRIBE = "unsubscribe"; + /** + * This goes though all runtime endpoints and get the correct Java method + */ + private Map enhanceRuntimeJsonRpcEndpoints( + Map runtimeMethods) { + for (Map.Entry method : runtimeMethods.entrySet()) { + JsonRpcMethod jsonRpcMethod = method.getValue(); + + @SuppressWarnings("unchecked") + Object providerInstance = Arc.container().select(jsonRpcMethod.getBean()).get(); // This is just here so that we can get the methods + + try { + Method javaMethod; + if (jsonRpcMethod.hasParameters()) { + Class[] types = jsonRpcMethod.getParameters().values().stream() + .map(p -> p.getType()) + .toArray(Class[]::new); + javaMethod = providerInstance.getClass().getMethod(jsonRpcMethod.getJavaMethodName(), types); + } else { + javaMethod = providerInstance.getClass().getMethod(jsonRpcMethod.getJavaMethodName()); + } + jsonRpcMethod.setJavaMethod(javaMethod); + } catch (NoSuchMethodException | SecurityException ex) { + throw new RuntimeException(ex); + } + } + return runtimeMethods; + } + } diff --git a/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/config/ConfigJsonRPCService.java b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/config/ConfigJsonRPCService.java index d3139bfd9c481..94595bdd537ee 100644 --- a/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/config/ConfigJsonRPCService.java +++ b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/config/ConfigJsonRPCService.java @@ -8,6 +8,7 @@ import jakarta.inject.Inject; import io.quarkus.dev.console.DevConsoleManager; +import io.quarkus.runtime.annotations.JsonRpcDescription; import io.quarkus.vertx.http.runtime.devmode.ConfigDescription; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; @@ -22,6 +23,7 @@ public JsonArray getAllConfiguration() { return new JsonArray(configDescriptionBean.getAllConfig()); } + @JsonRpcDescription("Get all configurations and their values for the Quarkus application") public JsonObject getAllValues() { JsonObject values = new JsonObject(); for (ConfigDescription configDescription : configDescriptionBean.getAllConfig()) { @@ -30,6 +32,7 @@ public JsonObject getAllValues() { return values; } + @JsonRpcDescription("Get the project properties for the Quarkus application") public JsonObject getProjectProperties() { JsonObject response = new JsonObject(); try { diff --git a/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/continuoustesting/ContinuousTestingJsonRPCService.java b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/continuoustesting/ContinuousTestingJsonRPCService.java index 4fc11a6da1124..3ae4c18ae91d6 100644 --- a/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/continuoustesting/ContinuousTestingJsonRPCService.java +++ b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/continuoustesting/ContinuousTestingJsonRPCService.java @@ -30,7 +30,7 @@ public class ContinuousTestingJsonRPCService implements Consumer invoke("devui-continuous-testing.getResults"); + final var results = DevConsoleManager. invoke("devui-continuous-testing_getResults"); final List passedTests = new LinkedList<>(); final List failedTests = new LinkedList<>(); final List skippedTests = new LinkedList<>(); diff --git a/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/DevUIWebSocket.java b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/js/DevUIWebSocketHandler.java similarity index 81% rename from extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/DevUIWebSocket.java rename to extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/js/DevUIWebSocketHandler.java index 85b0ff595f304..0f42fedf2845f 100644 --- a/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/DevUIWebSocket.java +++ b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/js/DevUIWebSocketHandler.java @@ -1,4 +1,4 @@ -package io.quarkus.devui.runtime; +package io.quarkus.devui.runtime.js; import jakarta.enterprise.inject.spi.CDI; @@ -13,8 +13,8 @@ /** * This is the main entry point for Dev UI Json RPC communication */ -public class DevUIWebSocket implements Handler { - private static final Logger LOG = Logger.getLogger(DevUIWebSocket.class.getName()); +public class DevUIWebSocketHandler implements Handler { + private static final Logger LOG = Logger.getLogger(DevUIWebSocketHandler.class.getName()); @Override public void handle(RoutingContext event) { @@ -26,7 +26,7 @@ public void handle(AsyncResult event) { ServerWebSocket socket = event.result(); addSocket(socket); } else { - LOG.debug("Failed to connect to dev ui communication server", event.cause()); + LOG.debug("Failed to connect to dev ui ws server", event.cause()); } } }); @@ -40,7 +40,7 @@ private void addSocket(ServerWebSocket session) { JsonRpcRouter jsonRpcRouter = CDI.current().select(JsonRpcRouter.class).get(); jsonRpcRouter.addSocket(session); } catch (IllegalStateException ise) { - LOG.debug("Failed to connect to dev ui communication server, " + ise.getMessage()); + LOG.debug("Failed to connect to dev ui ws server, " + ise.getMessage()); } } diff --git a/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/js/JavaScriptResponseWriter.java b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/js/JavaScriptResponseWriter.java new file mode 100644 index 0000000000000..656906d95a245 --- /dev/null +++ b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/js/JavaScriptResponseWriter.java @@ -0,0 +1,40 @@ +package io.quarkus.devui.runtime.js; + +import io.quarkus.devui.runtime.comms.JsonRpcResponseWriter; +import io.quarkus.devui.runtime.comms.MessageType; +import io.vertx.core.http.ServerWebSocket; + +public class JavaScriptResponseWriter implements JsonRpcResponseWriter { + private final ServerWebSocket socket; + + public JavaScriptResponseWriter(ServerWebSocket socket) { + this.socket = socket; + } + + @Override + public void write(String message) { + if (!socket.isClosed()) { + socket.writeTextMessage(message); + } + } + + @Override + public void close() { + socket.close(); + } + + @Override + public boolean isOpen() { + return !socket.isClosed(); + } + + @Override + public boolean isClosed() { + return socket.isClosed(); + } + + @Override + public Object decorateObject(Object object, MessageType messageType) { + return new Result(messageType.name(), object); + } +} \ No newline at end of file diff --git a/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/js/Result.java b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/js/Result.java new file mode 100644 index 0000000000000..9c5e93a016561 --- /dev/null +++ b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/js/Result.java @@ -0,0 +1,22 @@ +package io.quarkus.devui.runtime.js; + +/** + * A UI (JavaScript) specific response that contains some more details + */ +public class Result { + public final String messageType; + public final Object object; + + public Result(String messageType, Object object) { + this.messageType = messageType; + this.object = object; + } + + @Override + public String toString() { + return "Result{" + + "messageType='" + messageType + '\'' + + ", object=" + object + + '}'; + } +} diff --git a/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcCodec.java b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcCodec.java index a6de6811aa99d..d96f25214bee8 100644 --- a/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcCodec.java +++ b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcCodec.java @@ -5,42 +5,48 @@ import org.jboss.logging.Logger; +import io.quarkus.devui.runtime.comms.JsonRpcResponseWriter; import io.quarkus.devui.runtime.comms.MessageType; import io.quarkus.devui.runtime.jsonrpc.json.JsonMapper; -import io.vertx.core.http.ServerWebSocket; import io.vertx.core.json.JsonObject; public final class JsonRpcCodec { private static final Logger LOG = Logger.getLogger(JsonRpcCodec.class); private final JsonMapper jsonMapper; + private final JsonRpcRequestCreator jsonRpcRequestCreator; public JsonRpcCodec(JsonMapper jsonMapper) { this.jsonMapper = jsonMapper; + this.jsonRpcRequestCreator = new JsonRpcRequestCreator(jsonMapper); } public JsonRpcRequest readRequest(String json) { - return new JsonRpcRequest(jsonMapper, (JsonObject) jsonMapper.fromString(json, Object.class)); + return jsonRpcRequestCreator.create((JsonObject) jsonMapper.fromString(json, Object.class)); } - public void writeResponse(ServerWebSocket socket, int id, Object object, MessageType messageType) { - writeResponse(socket, new JsonRpcResponse(id, - new JsonRpcResponse.Result(messageType.name(), object))); + public JsonRpcRequest readMCPRequest(String json) { + return jsonRpcRequestCreator.mcpCreate((JsonObject) jsonMapper.fromString(json, Object.class)); } - public void writeMethodNotFoundResponse(ServerWebSocket socket, int id, String jsonRpcMethodName) { - writeResponse(socket, new JsonRpcResponse(id, + public void writeResponse(JsonRpcResponseWriter writer, int id, Object object, MessageType messageType) { + Object decoratedObject = writer.decorateObject(object, messageType); + writeResponse(writer, new JsonRpcResponse(id, decoratedObject)); + } + + public void writeMethodNotFoundResponse(JsonRpcResponseWriter writer, int id, String jsonRpcMethodName) { + writeResponse(writer, new JsonRpcResponse(id, new JsonRpcResponse.Error(METHOD_NOT_FOUND, "Method [" + jsonRpcMethodName + "] not found"))); } - public void writeErrorResponse(ServerWebSocket socket, int id, String jsonRpcMethodName, Throwable exception) { + public void writeErrorResponse(JsonRpcResponseWriter writer, int id, String jsonRpcMethodName, Throwable exception) { LOG.error("Error in JsonRPC Call", exception); - writeResponse(socket, new JsonRpcResponse(id, + writeResponse(writer, new JsonRpcResponse(id, new JsonRpcResponse.Error(INTERNAL_ERROR, "Method [" + jsonRpcMethodName + "] failed: " + exception.getMessage()))); } - private void writeResponse(ServerWebSocket socket, JsonRpcResponse response) { - socket.writeTextMessage(jsonMapper.toString(response, true)); + private void writeResponse(JsonRpcResponseWriter writer, JsonRpcResponse response) { + writer.write(jsonMapper.toString(response, true)); } public JsonMapper getJsonMapper() { diff --git a/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcMethod.java b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcMethod.java index c93235e6edd23..f56cf6a4ff02f 100644 --- a/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcMethod.java +++ b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcMethod.java @@ -1,11 +1,27 @@ package io.quarkus.devui.runtime.jsonrpc; +import java.lang.reflect.Method; +import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.annotations.Usage; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; public final class JsonRpcMethod { - private Class clazz; + private Class bean; private String methodName; - private Map params; + private String description; + private Method javaMethod; + private Map parameters; + + private List usage; + + private RuntimeValue runtimeValue; private boolean isExplicitlyBlocking; private boolean isExplicitlyNonBlocking; @@ -13,59 +29,147 @@ public final class JsonRpcMethod { public JsonRpcMethod() { } - public JsonRpcMethod(Class clazz, String methodName, Map params) { - this.clazz = clazz; - this.methodName = methodName; - this.params = params; + public Class getBean() { + return bean; } - public Class getClazz() { - return clazz; + public void setBean(Class bean) { + this.bean = bean; } public String getMethodName() { return methodName; } - public Map getParams() { - return params; + public String getJavaMethodName() { + if (methodName.contains(UNDERSCORE)) { + return methodName.substring(methodName.indexOf(UNDERSCORE) + 1); + } + return methodName; + } + + public void setMethodName(String methodName) { + this.methodName = methodName; } - public boolean hasParams() { - return this.params != null && !this.params.isEmpty(); + public String getDescription() { + return description; } - public void setClazz(Class clazz) { - this.clazz = clazz; + public void setDescription(String description) { + this.description = description; } - public void setMethodName(String methodName) { - this.methodName = methodName; + public List getUsage() { + return usage; + } + + public void setUsage(List usage) { + this.usage = usage; + } + + public Method getJavaMethod() { + return javaMethod; + } + + public void setJavaMethod(Method javaMethod) { + this.javaMethod = javaMethod; + } + + public Map getParameters() { + return parameters; + } + + public void setParameters(Map parameters) { + this.parameters = parameters; + } + + public void addParameter(String name, String description) { + if (this.parameters == null) + this.parameters = new LinkedHashMap<>(); + this.parameters.put(name, new Parameter(String.class, description)); } - public void setParams(Map params) { - this.params = params; + public void addParameter(String name, Class type, String description) { + if (this.parameters == null) + this.parameters = new LinkedHashMap<>(); + this.parameters.put(name, new Parameter(type, description)); } - public boolean getExplicitlyBlocking() { + public boolean hasParameters() { + return this.parameters != null && !this.parameters.isEmpty(); + } + + public RuntimeValue getRuntimeValue() { + return runtimeValue; + } + + public void setRuntimeValue(RuntimeValue runtimeValue) { + this.runtimeValue = runtimeValue; + } + + public boolean isIsExplicitlyBlocking() { return isExplicitlyBlocking; } - public void setExplicitlyBlocking(boolean blocking) { - isExplicitlyBlocking = blocking; + public void setIsExplicitlyBlocking(boolean isExplicitlyBlocking) { + this.isExplicitlyBlocking = isExplicitlyBlocking; } - public boolean getExplicitlyNonBlocking() { + public boolean isIsExplicitlyNonBlocking() { return isExplicitlyNonBlocking; } - public void setExplicitlyNonBlocking(boolean nonblocking) { - isExplicitlyNonBlocking = nonblocking; + public void setIsExplicitlyNonBlocking(boolean isExplicitlyNonBlocking) { + this.isExplicitlyNonBlocking = isExplicitlyNonBlocking; + } + + public boolean isReturningMulti() { + return javaMethod.getReturnType().getName().equals(Multi.class.getName()); + } + + public boolean isReturningUni() { + return javaMethod.getReturnType().getName().equals(Uni.class.getName()); + } + + public boolean isReturningCompletionStage() { + return javaMethod.getReturnType().getName().equals(CompletionStage.class.getName()); } - @Override - public String toString() { - return clazz.getName() + ":" + methodName + "(" + params + ")"; + public boolean isReturningCompletableFuture() { + return javaMethod.getReturnType().getName().equals(CompletableFuture.class.getName()); } + public static class Parameter { + private Class type; + private String description; + + public Parameter() { + + } + + public Parameter(Class type, String description) { + this.type = type; + this.description = description; + } + + public Class getType() { + return type; + } + + public void setType(Class type) { + this.type = type; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + } + + private static final String UNDERSCORE = "_"; + } diff --git a/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcRequest.java b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcRequest.java index 3be57a1afaae4..ca97b76f0b5f3 100644 --- a/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcRequest.java +++ b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcRequest.java @@ -1,64 +1,58 @@ package io.quarkus.devui.runtime.jsonrpc; -import static io.quarkus.devui.runtime.jsonrpc.JsonRpcKeys.ID; -import static io.quarkus.devui.runtime.jsonrpc.JsonRpcKeys.JSONRPC; -import static io.quarkus.devui.runtime.jsonrpc.JsonRpcKeys.METHOD; -import static io.quarkus.devui.runtime.jsonrpc.JsonRpcKeys.PARAMS; -import static io.quarkus.devui.runtime.jsonrpc.JsonRpcKeys.VERSION; - import java.util.Map; -import io.quarkus.devui.runtime.jsonrpc.json.JsonMapper; -import io.vertx.core.json.JsonObject; - -public class JsonRpcRequest { - - private final JsonMapper jsonMapper; - private final JsonObject jsonObject; +public final class JsonRpcRequest { + private int id; + private String jsonrpc = JsonRpcKeys.VERSION; + private String method; + private Map params; - JsonRpcRequest(JsonMapper jsonMapper, JsonObject jsonObject) { - this.jsonMapper = jsonMapper; - this.jsonObject = jsonObject; + public int getId() { + return id; } - public int getId() { - return jsonObject.getInteger(ID); + public void setId(int id) { + this.id = id; } public String getJsonrpc() { - String value = jsonObject.getString(JSONRPC); - if (value != null) { - return value; - } - return VERSION; + return jsonrpc; + } + + public void setJsonrpc(String jsonrpc) { + this.jsonrpc = jsonrpc; } public String getMethod() { - return jsonObject.getString(METHOD); + return method; + } + + public void setMethod(String method) { + this.method = method; + } + + public Map getParams() { + return params; + } + + public void setParams(Map params) { + this.params = params; } public boolean hasParams() { - return this.getParams() != null; + return this.params != null && !this.params.isEmpty(); } - public Map getParams() { - JsonObject paramsObject = jsonObject.getJsonObject(PARAMS); - if (paramsObject != null && paramsObject.getMap() != null && !paramsObject.getMap().isEmpty()) { - return paramsObject.getMap(); - } - return null; + public boolean hasParam(String key) { + return this.params != null && this.params.containsKey(key); } public T getParam(String key, Class paramType) { - Map params = getParams(); - if (params == null || !params.containsKey(key)) { - return null; + if (hasParam(key)) { + return (T) this.params.get(key); } - return jsonMapper.fromValue(params.get(key), paramType); + return null; } - @Override - public String toString() { - return jsonMapper.toString(jsonObject, true); - } } diff --git a/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcRequestCreator.java b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcRequestCreator.java new file mode 100644 index 0000000000000..fd30505596802 --- /dev/null +++ b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcRequestCreator.java @@ -0,0 +1,79 @@ +package io.quarkus.devui.runtime.jsonrpc; + +import static io.quarkus.devui.runtime.jsonrpc.JsonRpcKeys.ID; +import static io.quarkus.devui.runtime.jsonrpc.JsonRpcKeys.JSONRPC; +import static io.quarkus.devui.runtime.jsonrpc.JsonRpcKeys.METHOD; +import static io.quarkus.devui.runtime.jsonrpc.JsonRpcKeys.PARAMS; + +import java.util.Map; + +import io.quarkus.devui.runtime.jsonrpc.json.JsonMapper; +import io.vertx.core.json.JsonObject; + +public class JsonRpcRequestCreator { + private final JsonMapper jsonMapper; + + JsonRpcRequestCreator(JsonMapper jsonMapper) { + this.jsonMapper = jsonMapper; + } + + public JsonRpcRequest create(JsonObject jsonObject) { + JsonRpcRequest jsonRpcRequest = new JsonRpcRequest(); + jsonRpcRequest.setId(jsonObject.getInteger(ID)); + if (jsonObject.containsKey(JSONRPC)) { + jsonRpcRequest.setJsonrpc(jsonObject.getString(JSONRPC)); + } + jsonRpcRequest.setMethod(jsonObject.getString(METHOD)); + if (jsonObject.containsKey(PARAMS)) { + jsonRpcRequest.setParams(jsonObject.getJsonObject(PARAMS).getMap()); + } + + return jsonRpcRequest; + } + + // TODO: Repeat of above + public JsonRpcRequest mcpCreate(JsonObject jsonObject) { + JsonRpcRequest jsonRpcRequest = new JsonRpcRequest(); + if (jsonObject.containsKey(ID)) { + jsonRpcRequest.setId(jsonObject.getInteger(ID)); + } + if (jsonObject.containsKey(JSONRPC)) { + jsonRpcRequest.setJsonrpc(jsonObject.getString(JSONRPC)); + } + + jsonRpcRequest.setMethod(jsonObject.getString(METHOD)); + if (jsonObject.containsKey(PARAMS)) { + + Map map = jsonObject.getJsonObject(PARAMS).getMap(); + map.remove("_meta"); + map.remove("cursor"); + jsonRpcRequest.setParams(map); + } + + return remap(jsonRpcRequest); + } + + public JsonRpcRequest remap(JsonRpcRequest jsonRpcRequest) { + if (jsonRpcRequest.getMethod().equalsIgnoreCase(TOOLS_SLASH_CALL)) { + + Map params = jsonRpcRequest.getParams(); + String mappedName = (String) params.remove("name"); + Map mappedParams = (Map) params.remove("arguments"); + + JsonRpcRequest mapped = new JsonRpcRequest(); + mapped.setId(jsonRpcRequest.getId()); + mapped.setJsonrpc(jsonRpcRequest.getJsonrpc()); + mapped.setMethod(mappedName); + mapped.setParams(mappedParams); + + return mapped; + } + return jsonRpcRequest; + } + + public String toJson(JsonRpcRequest jsonRpcRequest) { + return jsonMapper.toString(jsonRpcRequest, true); + } + + private static final String TOOLS_SLASH_CALL = "tools/call"; +} diff --git a/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcResponse.java b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcResponse.java index fe7687c638e52..76c857aa692e7 100644 --- a/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcResponse.java +++ b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcResponse.java @@ -6,15 +6,21 @@ public final class JsonRpcResponse { // Public for serialization public final int id; - public final Result result; + public final Object result; public final Error error; - public JsonRpcResponse(int id, Result result) { + public JsonRpcResponse(int id, Object result) { this.id = id; this.result = result; this.error = null; } + // public JsonRpcResponse(int id, Result result) { + // this.id = id; + // this.result = result; + // this.error = null; + // } + public JsonRpcResponse(int id, Error error) { this.id = id; this.result = null; @@ -34,23 +40,15 @@ public String toString() { '}'; } - public static final class Result { - public final String messageType; - public final Object object; - - public Result(String messageType, Object object) { - this.messageType = messageType; - this.object = object; - } - - @Override - public String toString() { - return "Result{" + - "messageType='" + messageType + '\'' + - ", object=" + object + - '}'; - } - } + // public static final class Content { + // public final Object content; + // public final boolean isError; + // + // public Content(boolean isError, Object objects) { + // this.isError = isError; + // this.content = objects; + // } + // } public static final class Error { public final int code; diff --git a/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/logstream/LogStreamJsonRPCService.java b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/logstream/LogStreamJsonRPCService.java index 7f2f7e369e76d..4e13007119ffa 100644 --- a/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/logstream/LogStreamJsonRPCService.java +++ b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/logstream/LogStreamJsonRPCService.java @@ -10,6 +10,7 @@ import org.jboss.logmanager.Logger; import io.quarkus.arc.Arc; +import io.quarkus.runtime.annotations.JsonRpcDescription; import io.smallrye.common.annotation.NonBlocking; import io.smallrye.mutiny.Multi; import io.vertx.core.json.JsonObject; @@ -26,18 +27,21 @@ public String ping() { } @NonBlocking + @JsonRpcDescription("Get a short Quarkus application log file history (last 60 lines)") public List history() { LogStreamBroadcaster logStreamBroadcaster = Arc.container().instance(LogStreamBroadcaster.class).get(); LinkedBlockingQueue history = logStreamBroadcaster.getHistory(); return new ArrayList<>(history); } + @JsonRpcDescription("Stream the Quarkus application log file") public Multi streamLog() { LogStreamBroadcaster logStreamBroadcaster = Arc.container().instance(LogStreamBroadcaster.class).get(); return logStreamBroadcaster.getLogStream(); } @NonBlocking + @JsonRpcDescription("Get all the available loggers in this Quarkus application") public List getLoggers() { LogContext logContext = LogContext.getLogContext(); List values = new ArrayList<>(); @@ -53,6 +57,7 @@ public List getLoggers() { } @NonBlocking + @JsonRpcDescription("Get a specific logger in this Quarkus application") public JsonObject getLogger(String loggerName) { LogContext logContext = LogContext.getLogContext(); if (loggerName != null && !loggerName.isEmpty()) { @@ -66,6 +71,7 @@ public JsonObject getLogger(String loggerName) { } @NonBlocking + @JsonRpcDescription("Update a specific logger's log level in this Quarkus application") public JsonObject updateLogLevel(String loggerName, String levelValue) { LogContext logContext = LogContext.getLogContext(); Logger logger = logContext.getLogger(loggerName); diff --git a/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/mcp/DevMcpJsonRpcService.java b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/mcp/DevMcpJsonRpcService.java new file mode 100644 index 0000000000000..e8da3e39a6d28 --- /dev/null +++ b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/mcp/DevMcpJsonRpcService.java @@ -0,0 +1,39 @@ +package io.quarkus.devui.runtime.mcp; + +import java.util.HashSet; +import java.util.Set; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.operators.multi.processors.BroadcastProcessor; + +/** + * Normal Dev UI Json RPC Service for the Dev MPC Screen + */ +@ApplicationScoped +public class DevMcpJsonRpcService { + + private final BroadcastProcessor connectedClientStream = BroadcastProcessor.create(); + + // TODO: We need to be able to deregister a client if the connection drops. Mayby ping it ? + + private final Set connectedClients = new HashSet<>(); + + public Set getConnectedClients() { + if (!this.connectedClients.isEmpty()) { + return this.connectedClients; + } + return null; + } + + public Multi getConnectedClientStream() { + return connectedClientStream; + } + + public void addClientInfo(McpClientInfo clientInfo) { + this.connectedClients.add(clientInfo); + this.connectedClientStream.onNext(clientInfo); + } + +} diff --git a/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/mcp/McpBuiltinMethods.java b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/mcp/McpBuiltinMethods.java new file mode 100644 index 0000000000000..646ddd8de157e --- /dev/null +++ b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/mcp/McpBuiltinMethods.java @@ -0,0 +1,11 @@ +package io.quarkus.devui.runtime.mcp; + +public interface McpBuiltinMethods { + public static final String INITIALIZE = "initialize"; + public static final String NOTIFICATION = "notification"; + public static final String TOOLS_LIST = "tools/list"; + public static final String TOOLS_CALL = "tools/call"; + + public static final String RESOURCES_LIST = "resources/list"; + public static final String RESOURCES_READ = "resources/read"; +} diff --git a/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/mcp/McpClientInfo.java b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/mcp/McpClientInfo.java new file mode 100644 index 0000000000000..ceccb4d9dce11 --- /dev/null +++ b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/mcp/McpClientInfo.java @@ -0,0 +1,80 @@ +package io.quarkus.devui.runtime.mcp; + +import java.util.Map; +import java.util.Objects; + +public class McpClientInfo { + + private String name; + private String version; + + public McpClientInfo() { + } + + public McpClientInfo(String name, String version) { + this.name = name; + this.version = version; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + @Override + public int hashCode() { + int hash = 7; + hash = 73 * hash + Objects.hashCode(this.name); + hash = 73 * hash + Objects.hashCode(this.version); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final McpClientInfo other = (McpClientInfo) obj; + if (!Objects.equals(this.name, other.name)) { + return false; + } + return Objects.equals(this.version, other.version); + } + + @Override + public String toString() { + return "McpClientInfo{" + "name=" + name + ", version=" + version + '}'; + } + + static McpClientInfo fromMap(Map map) { + if (map != null) { + McpClientInfo ci = new McpClientInfo(); + ci.setName((String) map.getOrDefault(NAME, UNKNOWN)); + ci.setVersion((String) map.getOrDefault(VERSION, ZERO)); + return ci; + } + return null; + } + + private static final String NAME = "name"; + private static final String VERSION = "version"; + private static final String UNKNOWN = "Unknown MCP Client"; + private static final String ZERO = "0"; +} diff --git a/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/mcp/McpHttpHandler.java b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/mcp/McpHttpHandler.java new file mode 100644 index 0000000000000..6da27f687ba6d --- /dev/null +++ b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/mcp/McpHttpHandler.java @@ -0,0 +1,134 @@ +package io.quarkus.devui.runtime.mcp; + +import java.util.Map; + +import jakarta.enterprise.inject.spi.CDI; + +import org.jboss.logging.Logger; + +import io.quarkus.devui.runtime.comms.JsonRpcRouter; +import io.quarkus.devui.runtime.comms.MessageType; +import io.quarkus.devui.runtime.jsonrpc.JsonRpcCodec; +import io.quarkus.devui.runtime.jsonrpc.JsonRpcRequest; +import io.quarkus.devui.runtime.jsonrpc.json.JsonMapper; +import io.quarkus.devui.runtime.mcp.model.InitializeResponse; +import io.vertx.core.Handler; +import io.vertx.core.http.HttpMethod; +import io.vertx.ext.web.RoutingContext; + +/** + * Alternative Json RPC communication using Streamable HTTP for MCP + */ +public class McpHttpHandler implements Handler { + private static final Logger LOG = Logger.getLogger(McpHttpHandler.class.getName()); + private final String quarkusVersion; + private final JsonMapper jsonMapper; + private JsonRpcCodec codec; + + public McpHttpHandler(String quarkusVersion, JsonMapper jsonMapper) { + this.quarkusVersion = quarkusVersion; + this.jsonMapper = jsonMapper; + this.codec = new JsonRpcCodec(jsonMapper); + } + + @Override + public void handle(RoutingContext ctx) { + // TODO: + // Servers MUST validate the Origin header on all incoming connections to prevent DNS rebinding attacks + // When running locally, servers SHOULD bind only to localhost (127.0.0.1) rather than all network interfaces (0.0.0.0) + // Servers SHOULD implement proper authentication for all connections + + //The client MUST use HTTP POST to send JSON-RPC messages to the MCP endpoint. + //The client MUST include an Accept header, listing both application/json and text/event-stream as supported content types. + // The body of the POST request MUST be a single JSON-RPC request, notification, or response. + if (ctx.request().method().equals(HttpMethod.GET)) { // TODO: Also check Accept header + handleSSEInitRequest(ctx); + } else if (ctx.request().method().equals(HttpMethod.POST)) { // TODO: Also check Accept header + handleMCPJsonRPCRequest(ctx); + } + + } + + private void handleSSEInitRequest(RoutingContext ctx) { + // TODO: Add SSE Support + // The client MAY issue an HTTP GET to the MCP endpoint. + // This can be used to open an SSE stream, allowing the server to communicate to the client, + // without the client first sending data via HTTP POST. + // The client MUST include an Accept header, listing text/event-stream as a supported content type. + + // ctx.response() + // .putHeader("Content-Type", "text/event-stream; charset=utf-8") + // .putHeader("Cache-Control", "no-cache") + // .putHeader("Connection", "keep-alive") + // .setChunked(true); + // + // try { + // JsonRpcRouter jsonRpcRouter = CDI.current().select(JsonRpcRouter.class).get(); + // jsonRpcRouter.addSseSession(ctx); + // } catch (IllegalStateException e) { + // LOG.debug("Failed to connect to dev sse server", e); + // ctx.response().end(); + // } + + // The server MUST either return Content-Type: text/event-stream in response to this HTTP GET, + // or else return HTTP 405 Method Not Allowed, indicating that the server does not offer an SSE stream at this endpoint. + + ctx.response() + .setStatusCode(405) + .putHeader("Allow", "POST") + .putHeader("Content-Type", "text/plain") + .end("Method Not Allowed"); + + } + + private void handleMCPJsonRPCRequest(RoutingContext ctx) { + ctx.request().handler(buffer -> { + JsonRpcRouter jsonRpcRouter = CDI.current().select(JsonRpcRouter.class).get(); + String input = buffer.toString(); + + JsonRpcRequest jsonRpcRequest = codec.readMCPRequest(input); + + String methodName = jsonRpcRequest.getMethod(); + McpResponseWriter writer = new McpResponseWriter(ctx.response(), this.jsonMapper, methodName); + // First see if this a protocol specific method + + if (methodName.equalsIgnoreCase(McpBuiltinMethods.INITIALIZE)) { + // This is a MCP server that initialize + this.routeToMCPInitialize(jsonRpcRequest, codec, writer); + } else if (methodName.startsWith(McpBuiltinMethods.NOTIFICATION)) { + // This is a MCP notification + this.routeToMCPNotification(jsonRpcRequest, codec, writer); + } else if (methodName.equalsIgnoreCase(McpBuiltinMethods.TOOLS_LIST) || + methodName.equalsIgnoreCase(McpBuiltinMethods.RESOURCES_LIST) || + methodName.equalsIgnoreCase(McpBuiltinMethods.RESOURCES_READ)) { + jsonRpcRequest.setMethod(methodName.replace(SLASH, UNDERSCORE)); + jsonRpcRouter.route(jsonRpcRequest, writer); + } else { + // This is a normal extension method + jsonRpcRouter.route(jsonRpcRequest, writer); + } + }); + } + + private void routeToMCPInitialize(JsonRpcRequest jsonRpcRequest, JsonRpcCodec codec, McpResponseWriter writer) { + if (jsonRpcRequest.hasParam(CLIENT_INFO)) { + Map map = jsonRpcRequest.getParam(CLIENT_INFO, Map.class); + DevMcpJsonRpcService devMcpJsonRpcService = CDI.current().select(DevMcpJsonRpcService.class).get(); + devMcpJsonRpcService.addClientInfo(McpClientInfo.fromMap(map)); + } + String input = jsonMapper.toString(jsonRpcRequest, true); + codec.writeResponse(writer, jsonRpcRequest.getId(), new InitializeResponse(this.quarkusVersion), MessageType.Void); + } + + private void routeToMCPNotification(JsonRpcRequest jsonRpcRequest, JsonRpcCodec codec, McpResponseWriter writer) { + String jsonRpcMethodName = jsonRpcRequest.getMethod(); + String notification = jsonRpcMethodName.substring(McpBuiltinMethods.NOTIFICATION.length() + 2); + // TODO: Do something with the notification ? + + writer.getResponse().setStatusCode(202).end(); + } + + private static final String SLASH = "/"; + private static final String UNDERSCORE = "_"; + private static final String CLIENT_INFO = "clientInfo"; +} diff --git a/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/mcp/McpResourcesService.java b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/mcp/McpResourcesService.java new file mode 100644 index 0000000000000..1d18c53ebef4a --- /dev/null +++ b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/mcp/McpResourcesService.java @@ -0,0 +1,222 @@ +package io.quarkus.devui.runtime.mcp; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import io.quarkus.devui.runtime.DevUIBuildTimeStaticService; +import io.quarkus.devui.runtime.comms.JsonRpcRouter; +import io.quarkus.devui.runtime.jsonrpc.JsonRpcMethod; +import io.quarkus.devui.runtime.jsonrpc.json.JsonMapper; +import io.quarkus.devui.runtime.mcp.model.resource.Content; +import io.quarkus.devui.runtime.mcp.model.resource.Resource; +import io.quarkus.runtime.annotations.JsonRpcDescription; +import io.quarkus.runtime.annotations.Usage; + +/** + * This expose all Dev UI BuildTimeData and Recorded values as MCP Resources + * + * @see https://modelcontextprotocol.io/specification/2024-11-05/server/resources#listing-resources + * @see https://modelcontextprotocol.io/specification/2024-11-05/server/resources#reading-resources + * + * TODO: We can also expose the WorkItems here using resourceTemplates. See + * https://modelcontextprotocol.io/specification/2024-11-05/server/resources#resource-templates + * TODO: We can also send a changed notification on hot reload. See + * https://modelcontextprotocol.io/specification/2024-11-05/server/resources#list-changed-notification + */ +@ApplicationScoped +public class McpResourcesService { + + @Inject + JsonRpcRouter jsonRpcRouter; + + @Inject + DevUIBuildTimeStaticService buildTimeStaticService; + + private Map buildTimeData = null; // TODO: Find a way to set the description here. + private Map recordedMethods = null; + // TODO: Add support for subscriptions + + @JsonRpcDescription("This list all resources available for MCP") + public Map> list() { + + if (this.buildTimeData == null) + fetchBuildTimeData(); + if (this.recordedMethods == null) + fecthRecordedData(); + + List resources = new ArrayList<>(); + + // Add all buildtime data + resources.addAll(toBuildTimeDataResourceList()); + // Add all recorded values as methods + resources.addAll(toRecordedResourceList()); + + return Map.of("resources", resources); + } + + @JsonRpcDescription("This reads a certain resource given the uri as provided by resources/list") + public Map> read(String uri) { + String subUri = uri.substring(URI_SCHEME.length()); + if (subUri.startsWith(SUB_SCHEME_BUILD_TIME)) { + return readBuildTimeData(uri); + } else if (subUri.startsWith(SUB_SCHEME_RECORDED)) { + return readRecordedData(uri); + } else { + throw new UncheckedIOException(uri + " not found", new IOException()); + } + } + + private List toRecordedResourceList() { + List resources = new ArrayList<>(); + + for (JsonRpcMethod recordedJsonRpcMethod : this.recordedMethods.values()) { + if (recordedJsonRpcMethod.getUsage().contains(Usage.DEV_MCP)) { + Resource resource = new Resource(); + resource.uri = URI_SCHEME + SUB_SCHEME_RECORDED + recordedJsonRpcMethod.getMethodName(); + resource.name = recordedJsonRpcMethod.getMethodName(); + + if (recordedJsonRpcMethod.getDescription() != null && !recordedJsonRpcMethod.getDescription().isBlank()) { + resource.description = recordedJsonRpcMethod.getDescription(); + } + resources.add(resource); + } + } + + return resources; + } + + private List toBuildTimeDataResourceList() { + List resources = new ArrayList<>(); + + for (Map.Entry method : this.buildTimeData.entrySet()) { + Resource resource = new Resource(); + resource.uri = URI_SCHEME + SUB_SCHEME_BUILD_TIME + method.getKey(); + resource.name = method.getKey(); + resource.description = method.getValue(); + resources.add(resource); + } + + return resources; + } + + private Set extractBuildTimeDataMethods(String ns, String jsContent) { + Set result = new LinkedHashSet<>(); + + Matcher matcher = BTD_PATTERN.matcher(jsContent); + + while (matcher.find()) { + String name = matcher.group(1); + String jsonValue = matcher.group(2).trim(); + if (!isEmptyValue(jsonValue)) { // No worth in adding empty resources + result.add(ns + SLASH + name); + } + } + + return result; + } + + private boolean isEmptyValue(String value) { + return value == null || + value.trim().isEmpty() || + value.trim().matches("^\\[\\s*]$"); + } + + private Map> readBuildTimeData(String uri) { + JsonMapper jsonMapper = jsonRpcRouter.getJsonMapper(); + + String method = uri.substring((URI_SCHEME + SUB_SCHEME_BUILD_TIME).length()); + String[] split = method.split(SLASH); + String ns = split[0]; + String constt = split[1]; + String filename = ns + DASH_DATA_DOT_JS; + String path = buildTimeStaticService.getUrlAndPath().get(filename); + try { + String jsContent = Files.readString(Paths.get(path)); + Content content = new Content(); + content.uri = uri; + content.text = jsonMapper.toString(getBuildTimeDataConstValue(jsContent, constt), true); + return Map.of("contents", List.of(content)); + } catch (IOException ex) { + throw new UncheckedIOException("Could not read " + path, ex); + } + + } + + private Map> readRecordedData(String uri) { + JsonMapper jsonMapper = jsonRpcRouter.getJsonMapper(); + + String method = uri.substring((URI_SCHEME + SUB_SCHEME_RECORDED).length()); + Content content = new Content(); + content.uri = uri; + content.text = jsonMapper.toString(recordedMethods.get(method).getRuntimeValue().getValue(), true); + // TODO: Handle Futures Unis and Multies + + return Map.of("contents", List.of(content)); + } + + private void fetchBuildTimeData() { + this.buildTimeData = new LinkedHashMap<>(); + + Map descriptions = buildTimeStaticService.getDescriptions(); + + Map urlAndPath = buildTimeStaticService.getUrlAndPath(); + for (Map.Entry kv : urlAndPath.entrySet()) { + if (kv.getKey().endsWith(DASH_DATA_DOT_JS)) { + try { + String key = kv.getKey().substring(0, kv.getKey().length() - DASH_DATA_DOT_JS.length()); + + if (!key.equalsIgnoreCase("devui-jsonrpc")) { // We ignore this namespace, as this is the same as tools/list + String content = Files.readString(Paths.get(kv.getValue())); + Set methodNames = extractBuildTimeDataMethods(key, content); + for (String methodName : methodNames) { + if (descriptions.containsKey(methodName)) { + this.buildTimeData.put(methodName, descriptions.get(methodName)); // Only if a description exist + } + } + } + } catch (IOException ex) { + // Ignore ? + ex.printStackTrace(); + } + } + } + } + + // TODO: Find a simpler way. + private String getBuildTimeDataConstValue(String jsContent, String constName) { + String patternString = "export const " + Pattern.quote(constName) + "\\s*=\\s*([^;]+);"; + Pattern pattern = Pattern.compile(patternString, Pattern.DOTALL); + Matcher matcher = pattern.matcher(jsContent); + + if (matcher.find()) { + return matcher.group(1).trim(); + } + + return "Error: Data not found for " + constName; + } + + private void fecthRecordedData() { + this.recordedMethods = jsonRpcRouter.getRecordedMethodsMap(); + } + + private static final Pattern BTD_PATTERN = Pattern.compile("export const (\\w+)\\s*=\\s*([^;]+);", Pattern.DOTALL); + + private static final String URI_SCHEME = "quarkus://resource/"; + private static final String SUB_SCHEME_BUILD_TIME = "build-time/"; + private static final String SUB_SCHEME_RECORDED = "recorded/"; + private static final String SLASH = "/"; + private static final String DASH_DATA_DOT_JS = "-data.js"; +} diff --git a/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/mcp/McpResponseWriter.java b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/mcp/McpResponseWriter.java new file mode 100644 index 0000000000000..e9437fb035064 --- /dev/null +++ b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/mcp/McpResponseWriter.java @@ -0,0 +1,69 @@ +package io.quarkus.devui.runtime.mcp; + +import java.nio.charset.StandardCharsets; + +import io.quarkus.devui.runtime.comms.JsonRpcResponseWriter; +import io.quarkus.devui.runtime.comms.MessageType; +import io.quarkus.devui.runtime.jsonrpc.json.JsonMapper; +import io.quarkus.devui.runtime.mcp.model.tool.CallToolResult; +import io.vertx.core.http.HttpServerResponse; + +/** + * Write the response for MCP Call. Depending on the original method request, we might need to wrap this into an expected output + */ +public class McpResponseWriter implements JsonRpcResponseWriter { + private final HttpServerResponse response; + private final String requestMethodName; + private final JsonMapper jsonMapper; + + public McpResponseWriter(HttpServerResponse response, JsonMapper jsonMapper, String requestMethodName) { + this.response = response; + this.jsonMapper = jsonMapper; + this.requestMethodName = requestMethodName; + } + + @Override + public void write(String message) { + String output = message + "\n\n"; + int length = output.getBytes(StandardCharsets.UTF_8).length; + + if (!response.closed()) { + response.putHeader("Content-Type", "application/json") + .putHeader("Content-Length", String.valueOf(length)) + .end(output); + } + } + + @Override + public void close() { + response.end(); + } + + @Override + public boolean isOpen() { + return !response.closed(); + } + + @Override + public boolean isClosed() { + return response.closed(); + } + + public HttpServerResponse getResponse() { + return this.response; + } + + @Override + public Object decorateObject(Object object, MessageType messageType) { + if (requestMethodName.equalsIgnoreCase(McpBuiltinMethods.INITIALIZE) || + requestMethodName.equalsIgnoreCase(McpBuiltinMethods.NOTIFICATION) || + requestMethodName.equalsIgnoreCase(McpBuiltinMethods.TOOLS_LIST) || + requestMethodName.equalsIgnoreCase(McpBuiltinMethods.RESOURCES_LIST) || + requestMethodName.equalsIgnoreCase(McpBuiltinMethods.RESOURCES_READ)) { + return object; + } else { // Anyting else is a Tools call that we need to wrap in a call result objectc + String text = jsonMapper.toString(object, true); + return new CallToolResult(text); + } + } +} diff --git a/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/mcp/McpToolsService.java b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/mcp/McpToolsService.java new file mode 100644 index 0000000000000..2529ad079f4cf --- /dev/null +++ b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/mcp/McpToolsService.java @@ -0,0 +1,117 @@ +package io.quarkus.devui.runtime.mcp; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import io.quarkus.devui.runtime.comms.JsonRpcRouter; +import io.quarkus.devui.runtime.jsonrpc.JsonRpcMethod; +import io.quarkus.devui.runtime.mcp.model.tool.Tool; +import io.quarkus.runtime.annotations.JsonRpcDescription; +import io.quarkus.runtime.annotations.Usage; + +/** + * This exposes all Dev UI Runtime and Deployment JsonRPC Methods as MCP Tools + * + * @see https://modelcontextprotocol.io/specification/2024-11-05/server/tools + */ +@ApplicationScoped +public class McpToolsService { + + @Inject + JsonRpcRouter jsonRpcRouter; + + @JsonRpcDescription("This list all tools available for MCP") + public Map> list() { + List tools = toToolList(jsonRpcRouter.getRuntimeMethodsMap().values(), + jsonRpcRouter.getDeploymentMethodsMap().values()); + // TODO: Add support for subscriptions + return Map.of("tools", tools); + } + + private List toToolList(Collection runtimeMethods, + Collection deploymentMethods) { + List tools = new ArrayList<>(); + for (JsonRpcMethod runtimeJsonRpcMethod : runtimeMethods) { + addTool(tools, runtimeJsonRpcMethod); + } + for (JsonRpcMethod deploymentJsonRpcMethod : deploymentMethods) { + addTool(tools, deploymentJsonRpcMethod); + } + return tools; + } + + private void addTool(List tools, JsonRpcMethod method) { + Tool tool = toTool(method); + if (tool != null) { + tools.add(tool); + } + } + + private Tool toTool(JsonRpcMethod jsonRpcMethod) { + + if (!jsonRpcMethod.getUsage().contains(Usage.DEV_MCP)) { + return null; + } + + Tool tool = new Tool(); + tool.name = jsonRpcMethod.getMethodName(); + if (jsonRpcMethod.getDescription() != null && !jsonRpcMethod.getDescription().isBlank()) { + tool.description = jsonRpcMethod.getDescription(); + } + + Map props = new LinkedHashMap<>(); + List required = new ArrayList<>(); + + if (jsonRpcMethod.hasParameters()) { + for (Map.Entry parameter : jsonRpcMethod.getParameters().entrySet()) { + Map prop = new HashMap<>(); + JsonRpcMethod.Parameter p = parameter.getValue(); + prop.put("type", mapJavaTypeToJsonType(p.getType())); + if (p.getDescription() != null && !p.getDescription().isBlank()) { + prop.put("description", p.getDescription()); + } + required.add(parameter.getKey()); // TODO: Check for optional here + props.put(parameter.getKey(), prop); + + } + } + + tool.inputSchema = Map.of( + "type", "object", + "properties", props, + "required", required); + + return tool; + } + + private String mapJavaTypeToJsonType(Class clazz) { + if (clazz == null) + return "string"; // fallback + + if (clazz == String.class) + return "string"; + if (clazz == Boolean.class || clazz == boolean.class) + return "boolean"; + if (clazz == Integer.class || clazz == int.class + || clazz == Long.class || clazz == long.class + || clazz == Short.class || clazz == short.class + || clazz == Byte.class || clazz == byte.class) + return "integer"; + if (clazz == Double.class || clazz == double.class + || clazz == Float.class || clazz == float.class) + return "number"; + if (clazz.isArray() || List.class.isAssignableFrom(clazz)) + return "array"; + if (Map.class.isAssignableFrom(clazz)) + return "object"; + + return "string"; // fallback for enums, complex types, etc. + } +} diff --git a/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/mcp/model/InitializeResponse.java b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/mcp/model/InitializeResponse.java new file mode 100644 index 0000000000000..e5fbcfd33f3db --- /dev/null +++ b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/mcp/model/InitializeResponse.java @@ -0,0 +1,39 @@ +package io.quarkus.devui.runtime.mcp.model; + +/** + * The Initialize response as per MCP Spec (2025-03-26) + */ +public class InitializeResponse { + public String protocolVersion = "2025-03-26"; + public Capabilities capabilities = new Capabilities(); + public ServerInfo serverInfo; + public String instructions = "This MCP Server expose internals of a Running (Dev Mode) Quarkus application"; + + public InitializeResponse(String quarkusVersion) { + this.serverInfo = new ServerInfo(quarkusVersion); + } + + public class Capabilities { + public Resources resources = new Resources(); + public Tools tools = new Tools(); + } + + public class Resources { + public boolean subscribe = false; + public boolean listChanged = false; + } + + public class Tools { + public boolean subscribe = false; + public boolean listChanged = false; + } + + public class ServerInfo { + public String name = "Quarkus Dev MCP"; + public String version; + + ServerInfo(String version) { + this.version = version; + } + } +} \ No newline at end of file diff --git a/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/mcp/model/resource/Content.java b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/mcp/model/resource/Content.java new file mode 100644 index 0000000000000..95c4f133734a4 --- /dev/null +++ b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/mcp/model/resource/Content.java @@ -0,0 +1,14 @@ +package io.quarkus.devui.runtime.mcp.model.resource; + +/** + * Defines a MCP Content + * + * @see https://modelcontextprotocol.io/docs/concepts/resources + */ +public class Content { + private static final String MIME_TYPE_JSON = "application/json"; + + public String uri; + public String text; + public String mimeType = MIME_TYPE_JSON; // default +} \ No newline at end of file diff --git a/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/mcp/model/resource/Resource.java b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/mcp/model/resource/Resource.java new file mode 100644 index 0000000000000..d6948ffc60db4 --- /dev/null +++ b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/mcp/model/resource/Resource.java @@ -0,0 +1,15 @@ +package io.quarkus.devui.runtime.mcp.model.resource; + +/** + * Defines a MCP Resource + * + * @see https://modelcontextprotocol.io/docs/concepts/resources + */ +public class Resource { + private static final String MIME_TYPE_JSON = "application/json"; + + public String uri; + public String name; + public String description; + public String mimeType = MIME_TYPE_JSON; // default +} diff --git a/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/mcp/model/tool/CallToolResult.java b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/mcp/model/tool/CallToolResult.java new file mode 100644 index 0000000000000..2765153225fe3 --- /dev/null +++ b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/mcp/model/tool/CallToolResult.java @@ -0,0 +1,32 @@ +package io.quarkus.devui.runtime.mcp.model.tool; + +import java.util.List; + +public class CallToolResult { + private List content; + private Boolean isError = false; + + public CallToolResult() { + + } + + public CallToolResult(String text) { + this.content = List.of(new TextContent(text)); + } + + public List getContent() { + return content; + } + + public void setContent(List content) { + this.content = content; + } + + public Boolean getIsError() { + return isError; + } + + public void setIsError(Boolean isError) { + this.isError = isError; + } +} diff --git a/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/mcp/model/tool/TextContent.java b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/mcp/model/tool/TextContent.java new file mode 100644 index 0000000000000..98c85ab659032 --- /dev/null +++ b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/mcp/model/tool/TextContent.java @@ -0,0 +1,30 @@ +package io.quarkus.devui.runtime.mcp.model.tool; + +public class TextContent { + private String type = "text"; + private String text; + + public TextContent() { + + } + + public TextContent(String text) { + this.text = text; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } +} diff --git a/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/mcp/model/tool/Tool.java b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/mcp/model/tool/Tool.java new file mode 100644 index 0000000000000..03df97fe85e55 --- /dev/null +++ b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/mcp/model/tool/Tool.java @@ -0,0 +1,14 @@ +package io.quarkus.devui.runtime.mcp.model.tool; + +import java.util.Map; + +/** + * Defines a MCP Tool + */ +public class Tool { + public String name; + public String description; + public Map inputSchema; + public Map annotations; + +} \ No newline at end of file diff --git a/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/reportissues/ReportIssuesJsonRPCService.java b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/reportissues/ReportIssuesJsonRPCService.java index 34d303e7ef282..2f5e6a72ab517 100644 --- a/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/reportissues/ReportIssuesJsonRPCService.java +++ b/extensions/devui/runtime/src/main/java/io/quarkus/devui/runtime/reportissues/ReportIssuesJsonRPCService.java @@ -18,6 +18,7 @@ import org.eclipse.microprofile.config.inject.ConfigProperty; import org.jboss.logging.Logger; +import io.quarkus.runtime.annotations.JsonRpcDescription; import io.smallrye.common.os.OS; import io.vertx.core.json.JsonObject; @@ -30,6 +31,7 @@ public class ReportIssuesJsonRPCService { @ConfigProperty(name = "quarkus.devui.report-issues.url", defaultValue = "https://github.com/quarkusio/quarkus/issues/new?labels=kind%2Fbug&template=bug_report.yml") String reportURL; + @JsonRpcDescription("Creates a url that if opened, will go to issue report for the Quarkus project with some of the value pre-filled") public JsonObject reportBug() { URLBuilder urlBuilder = new URLBuilder(reportURL); gatherInfo(urlBuilder); diff --git a/extensions/devui/test-spi/src/main/java/io/quarkus/devui/tests/DevUIJsonRPCTest.java b/extensions/devui/test-spi/src/main/java/io/quarkus/devui/tests/DevUIJsonRPCTest.java index f3ca88632c0a8..18d1cf70d20c5 100644 --- a/extensions/devui/test-spi/src/main/java/io/quarkus/devui/tests/DevUIJsonRPCTest.java +++ b/extensions/devui/test-spi/src/main/java/io/quarkus/devui/tests/DevUIJsonRPCTest.java @@ -178,7 +178,7 @@ private String createJsonRPCRequest(int id, String methodName, Map p : params.entrySet()) { diff --git a/extensions/info/deployment/src/main/java/io/quarkus/info/deployment/InfoDevUIProcessor.java b/extensions/info/deployment/src/main/java/io/quarkus/info/deployment/InfoDevUIProcessor.java index 8681b2dbb948d..bc4fd2d6e14f2 100644 --- a/extensions/info/deployment/src/main/java/io/quarkus/info/deployment/InfoDevUIProcessor.java +++ b/extensions/info/deployment/src/main/java/io/quarkus/info/deployment/InfoDevUIProcessor.java @@ -37,7 +37,6 @@ void create(BuildProducer cardPageProducer, .isJsonContent(); CardPageBuildItem cardBuildItem = new CardPageBuildItem(); - cardBuildItem.addBuildTimeData("infoUrl", path); cardBuildItem.addPage(infoPage); cardBuildItem.addPage(rawPage); cardPageProducer.produce(cardBuildItem); diff --git a/extensions/info/deployment/src/main/java/io/quarkus/info/deployment/InfoProcessor.java b/extensions/info/deployment/src/main/java/io/quarkus/info/deployment/InfoProcessor.java index 72a59153d8194..f005f911ac823 100644 --- a/extensions/info/deployment/src/main/java/io/quarkus/info/deployment/InfoProcessor.java +++ b/extensions/info/deployment/src/main/java/io/quarkus/info/deployment/InfoProcessor.java @@ -37,6 +37,7 @@ import io.quarkus.deployment.builditem.ApplicationInfoBuildItem; import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem; +import io.quarkus.devui.spi.buildtime.BuildTimeActionBuildItem; import io.quarkus.info.BuildInfo; import io.quarkus.info.GitInfo; import io.quarkus.info.JavaInfo; @@ -46,6 +47,7 @@ import io.quarkus.info.runtime.InfoRecorder; import io.quarkus.info.runtime.spi.InfoContributor; import io.quarkus.maven.dependency.ResolvedDependency; +import io.quarkus.runtime.RuntimeValue; import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; import io.quarkus.vertx.http.deployment.spi.RouteBuildItem; @@ -279,6 +281,7 @@ RouteBuildItem defineRoute(InfoBuildTimeConfig buildTimeConfig, List contributors, NonApplicationRootPathBuildItem nonApplicationRootPathBuildItem, BuildProducer unremovableBeanBuildItemBuildProducer, + BuildProducer buildTimeActionProducer, InfoRecorder recorder) { LinkedHashMap buildTimeInfo = new LinkedHashMap<>(); @@ -296,9 +299,18 @@ RouteBuildItem defineRoute(InfoBuildTimeConfig buildTimeConfig, unremovableBeanBuildItemBuildProducer.produce(UnremovableBeanBuildItem.beanTypes(InfoContributor.class)); + RuntimeValue> finalBuildInfo = recorder.getFinalBuildInfo(buildTimeInfo, infoContributors); + + buildTimeActionProducer.produce(new BuildTimeActionBuildItem().actionBuilder() + .methodName("getApplicationAndEnvironmentInfo") + .description( + "Information about the environment where this Quarkus application is running. " + + "Things like Operating System, Java version Git information and application details is available") + .runtime(finalBuildInfo).build()); + return RouteBuildItem.newManagementRoute(buildTimeConfig.path()) .withRoutePathConfigKey("quarkus.info.path") - .withRequestHandler(recorder.handler(buildTimeInfo, infoContributors)) + .withRequestHandler(recorder.handler(finalBuildInfo)) .displayOnNotFoundPage("Info") .build(); } diff --git a/extensions/info/deployment/src/main/resources/dev-ui/qwc-info.js b/extensions/info/deployment/src/main/resources/dev-ui/qwc-info.js index e2363fb765291..b631b8cb3f85b 100644 --- a/extensions/info/deployment/src/main/resources/dev-ui/qwc-info.js +++ b/extensions/info/deployment/src/main/resources/dev-ui/qwc-info.js @@ -1,7 +1,7 @@ import { LitElement, html, css} from 'lit'; -import {unsafeHTML} from 'lit/directives/unsafe-html.js'; +import { unsafeHTML } from 'lit/directives/unsafe-html.js'; import { columnBodyRenderer } from '@vaadin/grid/lit.js'; -import { infoUrl } from 'build-time-data'; +import { JsonRpc } from 'jsonrpc'; import '@vaadin/progress-bar'; import '@qomponent/qui-card'; import '@vaadin/icon'; @@ -11,6 +11,8 @@ import '@vaadin/icon'; */ export class QwcInfo extends LitElement { + jsonRpc = new JsonRpc(this); + static styles = css` :host { display: grid; @@ -46,19 +48,14 @@ export class QwcInfo extends LitElement { constructor() { super(); - this._infoUrl = infoUrl; this._info = null; } async connectedCallback() { super.connectedCallback(); - await this.load(); - } - - async load() { - const response = await fetch(this._infoUrl); - const data = await response.json(); - this._info = data; + this.jsonRpc.getApplicationAndEnvironmentInfo().then(jsonRpcResponse => { + this._info = jsonRpcResponse.result; + }); } render() { diff --git a/extensions/info/runtime/src/main/java/io/quarkus/info/runtime/InfoRecorder.java b/extensions/info/runtime/src/main/java/io/quarkus/info/runtime/InfoRecorder.java index 0411426bf63ee..bc904bdbfcfa2 100644 --- a/extensions/info/runtime/src/main/java/io/quarkus/info/runtime/InfoRecorder.java +++ b/extensions/info/runtime/src/main/java/io/quarkus/info/runtime/InfoRecorder.java @@ -17,6 +17,7 @@ import io.quarkus.info.JavaInfo; import io.quarkus.info.OsInfo; import io.quarkus.info.runtime.spi.InfoContributor; +import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.annotations.Recorder; import io.vertx.core.Handler; import io.vertx.core.http.HttpHeaders; @@ -30,8 +31,33 @@ public class InfoRecorder { private static final Logger log = Logger.getLogger(InfoRecorder.class); - public Handler handler(Map buildTimeInfo, List knownContributors) { - return new InfoHandler(buildTimeInfo, knownContributors); + public RuntimeValue> getFinalBuildInfo(Map buildTimeInfo, + List knownContributors) { + Map finalBuildInfo = new HashMap<>(buildTimeInfo); + for (InfoContributor contributor : knownContributors) { + String key = contributor.name(); + if (finalBuildInfo.containsKey(key)) { + log.warn( + "Info key " + key + " contains duplicate values. This can lead to unpredictable values being used"); + } + //TODO: we might want this to be done lazily + // also, do we want to merge information or simply replace like we are doing here? + finalBuildInfo.put(key, contributor.data()); + } + for (InstanceHandle handler : Arc.container().listAll(InfoContributor.class)) { + InfoContributor contributor = handler.get(); + String key = contributor.name(); + if (finalBuildInfo.containsKey(key)) { + log.warn( + "Info key " + key + " contains duplicate values. This can lead to unpredictable values being used"); + } + finalBuildInfo.put(key, contributor.data()); + } + return new RuntimeValue(finalBuildInfo); + } + + public Handler handler(RuntimeValue> finalBuildInfo) { + return new InfoHandler(finalBuildInfo.getValue()); } public Supplier gitInfoSupplier(String branch, String latestCommitId, String latestCommitTime) { @@ -152,27 +178,8 @@ public String vendorVersion() { private static class InfoHandler implements Handler { private final Map finalBuildInfo; - public InfoHandler(Map buildTimeInfo, List knownContributors) { - this.finalBuildInfo = new HashMap<>(buildTimeInfo); - for (InfoContributor contributor : knownContributors) { - String key = contributor.name(); - if (finalBuildInfo.containsKey(key)) { - log.warn( - "Info key " + key + " contains duplicate values. This can lead to unpredictable values being used"); - } - //TODO: we might want this to be done lazily - // also, do we want to merge information or simply replace like we are doing here? - finalBuildInfo.put(key, contributor.data()); - } - for (InstanceHandle handler : Arc.container().listAll(InfoContributor.class)) { - InfoContributor contributor = handler.get(); - String key = contributor.name(); - if (finalBuildInfo.containsKey(key)) { - log.warn( - "Info key " + key + " contains duplicate values. This can lead to unpredictable values being used"); - } - finalBuildInfo.put(key, contributor.data()); - } + public InfoHandler(Map finalBuildInfo) { + this.finalBuildInfo = finalBuildInfo; } @Override diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ResourceNotFoundData.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ResourceNotFoundData.java index 641749f91f619..57e1cc245ea06 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ResourceNotFoundData.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ResourceNotFoundData.java @@ -26,6 +26,7 @@ import org.jboss.logging.Logger; import io.quarkus.runtime.TemplateHtmlBuilder; +import io.quarkus.runtime.annotations.JsonRpcDescription; import io.quarkus.runtime.util.ClassPathUtils; import io.smallrye.common.annotation.NonBlocking; import io.vertx.core.json.JsonArray; @@ -136,6 +137,7 @@ public String getHTMLContent() { } @NonBlocking + @JsonRpcDescription("Information on endpoints exposed by this application in Dev Mode. This includes Quarkus endpoints and application endpoints") public JsonObject getJsonContent() { List combinedRoutes = getCombinedRoutes(); JsonObject infoMap = new JsonObject();