From 4425670343ea2553119d108631fbb05cfb7e9a81 Mon Sep 17 00:00:00 2001 From: macbook Date: Thu, 20 Mar 2025 06:52:21 +0100 Subject: [PATCH 1/6] upload recent changes --- .../svm/hosted/prophet/ProphetPlugin.java | 96 ++- .../hosted/prophet/RestCallExtraction.java | 4 +- .../oracle/svm/hosted/prophet/RestDump.java | 33 +- .../prophet/WebsocketCallExtraction.java | 624 ++++++++++++++++++ .../svm/hosted/prophet/model/Module.java | 33 +- .../prophet/model/WebsocketConnection.java | 91 +++ .../prophet/model/WebsocketEndpoint.java | 87 +++ .../prophet/model/WebsocketMessageType.java | 102 +++ .../prophet/model/WebsocketParameter.java | 48 ++ 9 files changed, 1093 insertions(+), 25 deletions(-) create mode 100644 substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/WebsocketCallExtraction.java create mode 100644 substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketConnection.java create mode 100644 substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketEndpoint.java create mode 100644 substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketMessageType.java create mode 100644 substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketParameter.java diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/ProphetPlugin.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/ProphetPlugin.java index 6ef92d1ee70f..29e07e4bb382 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/ProphetPlugin.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/ProphetPlugin.java @@ -1,5 +1,6 @@ package com.oracle.svm.hosted.prophet; +import java.io.BufferedReader; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.FileWriter; @@ -7,12 +8,16 @@ import java.net.URL; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import com.oracle.svm.hosted.prophet.model.WebsocketConnection; +import com.oracle.svm.hosted.prophet.model.WebsocketEndpoint; +import com.oracle.svm.hosted.prophet.model.WebsocketMessageType; import org.graalvm.compiler.options.Option; import com.oracle.graal.pointsto.meta.AnalysisMetaAccess; @@ -73,6 +78,9 @@ public static class Options { @Option(help = "Where to store the restcall output")// public static final HostedOptionKey ProphetRestCallOutputFile = new HostedOptionKey<>(null); + @Option(help = "Where to store the websocketConnections output")// + public static final HostedOptionKey ProphetWebsocketConnectionsOutputFile = new HostedOptionKey<>(null); + @Option(help = "Where to store the endpoint output")// public static final HostedOptionKey ProphetEndpointOutputFile = new HostedOptionKey<>(null); @@ -96,6 +104,7 @@ public static void run(ImageClassLoader loader, AnalysisUniverse aUniverse, Anal Module module = plugin.doRun(); RestDump restDump = new RestDump(); restDump.writeOutRestCalls(module.getRestCalls(), Options.ProphetRestCallOutputFile.getValue()); + restDump.writeOutWebsocketConnections(module.getWebsocketConnections(), Options.ProphetWebsocketConnectionsOutputFile.getValue()); restDump.writeOutEndpoints(module.getEndpoints(), Options.ProphetEndpointOutputFile.getValue()); dumpModule(module); @@ -123,10 +132,26 @@ private static void dumpModule(Module module) { private Module doRun() { URL enumeration = loader.getClassLoader().getResource("application.yml"); if (enumeration != null) { - try { - this.propMap = new org.yaml.snakeyaml.Yaml().load(new FileReader(enumeration.getFile())); + try (BufferedReader reader = new BufferedReader(new FileReader(enumeration.getFile()))) { + StringBuilder yamlContent = new StringBuilder(); + String line; + + // Read until the first '---' separator or end of file + while ((line = reader.readLine()) != null) { + if (line.trim().equals("---")) { + break; + } + yamlContent.append(line).append("\n"); + } + + // Parse the YAML content before the separator + this.propMap = new org.yaml.snakeyaml.Yaml().load(yamlContent.toString()); + } catch (FileNotFoundException e) { throw new RuntimeException(e); + } catch (IOException e) { + this.propMap = new HashMap<>(); + e.printStackTrace(); } } @@ -134,9 +159,64 @@ private Module doRun() { return processClasses(classes); } + private Set linkWebsocketEndpointsAndMessageTypes(Set websocketEndpointsList, Set websocketMessageTypesList) { + Set websocketConnections = new HashSet(); + + for (WebsocketEndpoint endpoint : websocketEndpointsList) { + if (websocketMessageTypesList.isEmpty()) { + WebsocketConnection connection = new WebsocketConnection( + endpoint.getParentMethod(), + endpoint.getReturnType(), + endpoint.getUri(), + endpoint.isCollection(), + endpoint.getConnectionInClassName(), + endpoint.getMsName(), + endpoint.getParam() + ); + websocketConnections.add(connection); + } else { + boolean handlerMatched = false; + for (WebsocketMessageType messageType : websocketMessageTypesList) { + String endpointHandler = endpoint.getWsHandler(); + String messageTypeHandler = messageType.getWsHandler(); + if (endpointHandler.equals(messageTypeHandler)) { + WebsocketConnection connection = new WebsocketConnection( + endpoint.getParentMethod() != null ? endpoint.getParentMethod() : messageType.getParentMethod(), + messageType.getWsDataType(), + endpoint.getUri(), + endpoint.isCollection(), + endpoint.getConnectionInClassName() != null ? endpoint.getConnectionInClassName() : messageType.getConnectionInClassName(), + endpoint.getMsName() != null ? endpoint.getMsName() : messageType.getMsName(), + endpoint.getParam() != null ? endpoint.getParam() : messageType.getParam() + ); + websocketConnections.add(connection); + handlerMatched = true; + break; + } + } + if (!handlerMatched) { + WebsocketConnection connection = new WebsocketConnection( + endpoint.getParentMethod(), + endpoint.getReturnType(), + endpoint.getUri(), + endpoint.isCollection(), + endpoint.getConnectionInClassName(), + endpoint.getMsName(), + endpoint.getParam() + ); + websocketConnections.add(connection); + } + } + } + return websocketConnections; + } + private Module processClasses(List> classes) { var entities = new HashSet(); Set restCallList = new HashSet(); + Set websocketConnectionsList = new HashSet(); + Set websocketEndpointsList = new HashSet(); + Set websocketMessageTypesList = new HashSet(); Set endpointList = new HashSet(); logger.info("Amount of classes = " + classes.size()); @@ -145,13 +225,21 @@ private Module processClasses(List> classes) { Optional ent = EntityExtraction.extractClassEntityCalls(clazz, metaAccess, bb); ent.ifPresent(entities::add); Set restCalls = RestCallExtraction.extractClassRestCalls(clazz, metaAccess, bb, this.propMap, Options.ProphetMicroserviceName.getValue()); + Set websocketConnection = WebsocketCallExtraction.extractClassWebsocketConnection(clazz, metaAccess, bb, this.propMap, Options.ProphetMicroserviceName.getValue()); + Set websocketEndpoints = WebsocketCallExtraction.extractClassWebsocketEndpoints(clazz, metaAccess, bb, this.propMap, Options.ProphetMicroserviceName.getValue()); + Set websocketMessageTypes = WebsocketCallExtraction.extractClassWebsocketMessageTypes(clazz, metaAccess, bb, this.propMap, Options.ProphetMicroserviceName.getValue()); restCallList.addAll(restCalls); + websocketEndpointsList.addAll(websocketEndpoints); + websocketMessageTypesList.addAll(websocketMessageTypes); + websocketConnectionsList.addAll(websocketConnection); // ENDPOINT EXTRACTION HERE Set endpoints = EndpointExtraction.extractEndpoints(clazz, metaAccess, bb, Options.ProphetMicroserviceName.getValue()); endpointList.addAll(endpoints); - } - return new Module(new Name(msName), entities, restCallList, endpointList); + + websocketConnectionsList.addAll(linkWebsocketEndpointsAndMessageTypes(websocketEndpointsList, websocketMessageTypesList)); + + return new Module(new Name(msName), entities, restCallList, websocketConnectionsList, endpointList); } private List> filterRelevantClasses() { diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/RestCallExtraction.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/RestCallExtraction.java index 290176a7b4f3..f7d837faffae 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/RestCallExtraction.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/RestCallExtraction.java @@ -50,6 +50,9 @@ public static Set extractClassRestCalls(Class clazz, AnalysisMetaAc AnalysisType analysisType = metaAccess.lookupJavaType(clazz); try { for (AnalysisMethod method : ((AnalysisMethod[]) analysisType.getDeclaredMethods())) { + if(method.isAbstract()){ + continue; + } try { // if (!method.getQualifiedName().contains("getExams")){ // continue; @@ -113,7 +116,6 @@ else if (v instanceof ConstantNode && !v.isNullConstant() && !v.isIllegalConstan } //MIGHT be URI or portion of URI else{ - DirectSubstrateObjectConstant dsoc = (DirectSubstrateObjectConstant)cn.getValue(); URI += dsoc.getObject().toString(); } diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/RestDump.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/RestDump.java index 17b229200f73..5950af6fd78d 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/RestDump.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/RestDump.java @@ -1,6 +1,7 @@ package com.oracle.svm.hosted.prophet; import com.oracle.svm.hosted.prophet.model.Endpoint; import com.oracle.svm.hosted.prophet.model.RestCall; +import com.oracle.svm.hosted.prophet.model.WebsocketConnection; import java.io.IOException; import java.util.Set; @@ -11,27 +12,41 @@ import java.io.FileWriter; public class RestDump { - + //RESTCALL CSV ORDER SHOULD BE //msName, restCallInClassName, parentMethod, uri, httpMethod, returnType, isPath, isBody, paramType, paramCount, isCollection - //ENDPOINT CSV ORDER SHOULD BE + //ENDPOINT CSV ORDER SHOULD BE //msName, endpointInClassName, parentMethod, arguments, path, httpMethod, returnType, isCollection - - //TO-DO: add header row to csv output!!! - public void writeOutRestCalls(Set restCalls, String outputFile){ + //WebsocketConnection CSV ORDER SHOULD BE + //msName, connectionInClassName, parentMethod, uri, returnType, param, isCollection - if (outputFile == null){ + //TO-DO: add header row to csv output!!! + public void writeOutRestCalls(Set restCalls, String outputFile) { + if (outputFile == null) { throw new RuntimeException("ProphetRestCallOutputFile option was not provided"); } - try (BufferedWriter writer = new BufferedWriter(new FileWriter(outputFile))){ - for (RestCall rc : restCalls){ + try (BufferedWriter writer = new BufferedWriter(new FileWriter(outputFile))) { + for (RestCall rc : restCalls) { writer.write(rc.toString() + "\n"); } - }catch(IOException ex){ + } catch (IOException ex) { ex.printStackTrace(); } + } + public void writeOutWebsocketConnections(Set websocketConnections, String outputFile) { + if (outputFile == null) { + throw new RuntimeException("ProphetWebsocketConnectionsOutputFile option was not provided"); + } + try (BufferedWriter writer = new BufferedWriter(new FileWriter(outputFile))) { + for (WebsocketConnection wc : websocketConnections) { + writer.write(wc.toString() + "\n"); + } + } catch (IOException ex) { + ex.printStackTrace(); + } } + public void writeOutEndpoints(Set endpoints, String outputFile){ if (outputFile == null){ throw new RuntimeException("ProphetEndpointOutputFile option was not provided"); diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/WebsocketCallExtraction.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/WebsocketCallExtraction.java new file mode 100644 index 000000000000..748ccadb19f9 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/WebsocketCallExtraction.java @@ -0,0 +1,624 @@ +package com.oracle.svm.hosted.prophet; + +import com.oracle.graal.pointsto.meta.AnalysisField; +import com.oracle.graal.pointsto.meta.AnalysisMetaAccess; +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.graal.pointsto.meta.AnalysisType; +import com.oracle.graal.reachability.ReachabilityAnalysisMethod; +import com.oracle.svm.core.meta.DirectSubstrateObjectConstant; +import com.oracle.svm.hosted.analysis.Inflation; +import com.oracle.svm.hosted.prophet.model.WebsocketConnection; +import com.oracle.svm.hosted.prophet.model.WebsocketEndpoint; +import com.oracle.svm.hosted.prophet.model.WebsocketMessageType; +import com.oracle.svm.hosted.prophet.model.WebsocketParameter; +import jdk.vm.ci.meta.PrimitiveConstant; +import jdk.vm.ci.meta.ResolvedJavaMethod; +import jdk.vm.ci.meta.ResolvedJavaMethod.Parameter; +import org.graalvm.compiler.core.common.type.ObjectStamp; +import org.graalvm.compiler.graph.Node; +import org.graalvm.compiler.graph.NodeInputList; +import org.graalvm.compiler.graph.iterators.NodeIterable; +import org.graalvm.compiler.nodes.BeginNode; +import org.graalvm.compiler.nodes.CallTargetNode; +import org.graalvm.compiler.nodes.ConstantNode; +import org.graalvm.compiler.nodes.Invoke; +import org.graalvm.compiler.nodes.InvokeWithExceptionNode; +import org.graalvm.compiler.nodes.NodeView; +import org.graalvm.compiler.nodes.PiNode; +import org.graalvm.compiler.nodes.StructuredGraph; +import org.graalvm.compiler.nodes.ValueNode; +import org.graalvm.compiler.nodes.java.LoadFieldNode; +import org.graalvm.compiler.nodes.virtual.AllocatedObjectNode; +import org.graalvm.compiler.nodes.virtual.CommitAllocationNode; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class WebsocketCallExtraction { + + /* + NOTE: + 'msRoot' can be obtained in Utils or RAD + 'source' can be obtained in RAD repo in the RadSourceService file in generateWebsocketEntityContext method where getSourceFiles is + */ + private final static String WEBSOCKET_TEXT_MESSAGE = "org.springframework.web.socket.TextMessage"; + + static String URI = ""; + + public static Class extractHostedClass(Object dynamicHub) throws NoSuchFieldException, IllegalAccessException { + // The hosted class is typically stored in a field of the DynamicHub + Field hostedClassField = dynamicHub.getClass().getDeclaredField("hostedJavaClass"); + + // Make the field accessible, since it is usually private + hostedClassField.setAccessible(true); + + // Retrieve the actual class type from the DynamicHub + return (Class) hostedClassField.get(dynamicHub); + } + + // Helper class to hold both URI and handler data + private static class ExtractionResult { + StringBuilder uriBuilder; + StringBuilder handlerBuilder; + + public ExtractionResult(StringBuilder uriBuilder, StringBuilder handlerBuilder) { + this.uriBuilder = uriBuilder; + this.handlerBuilder = handlerBuilder; + } + } + + private static ExtractionResult logPrecedingNodesRecursiveURIandHandler( + Node currentNode, Set visitedNodes, StringBuilder uriBuilder, StringBuilder handlerBuilder) { + if (visitedNodes.contains(currentNode)) { + return new ExtractionResult(uriBuilder, handlerBuilder); + } + + visitedNodes.add(currentNode); + + System.out.println("Node: " + currentNode + ", Class: " + currentNode.getClass().getName()); + + // Check if the current node is the specific `Invoke#StringConcatHelper.simpleConcat` node + if (currentNode instanceof InvokeWithExceptionNode) { + InvokeWithExceptionNode invokeNode = (InvokeWithExceptionNode) currentNode; + if (invokeNode.callTarget().targetName().contains("StringConcatHelper.simpleConcat")) { + System.out.println("Found target Invoke: " + invokeNode); + + // Process the concatenation components + NodeInputList concatArgs = invokeNode.callTarget().arguments(); + for (ValueNode concatArg : concatArgs) { + if (concatArg instanceof ConstantNode) { + ConstantNode constantNode = (ConstantNode) concatArg; + uriBuilder.append(constantNode.asJavaConstant().toValueString()); + } + } + System.out.println("Concatenated URI: " + uriBuilder.toString()); + } + } + + // Check if the current node is an Invoke for handler instantiation + if (currentNode instanceof InvokeWithExceptionNode) { + InvokeWithExceptionNode invokeNode = (InvokeWithExceptionNode) currentNode; + if (invokeNode.callTarget().targetName().contains(".")) { // Look for constructors + System.out.println("Found handler constructor: " + invokeNode); + + // Extract the handler class name + handlerBuilder.append(invokeNode.callTarget().targetName().split("\\.")[0]); // Assuming format Class. + System.out.println("Handler: " + handlerBuilder.toString()); + } + } + + // Continue traversing predecessors + for (Node predecessor : currentNode.cfgPredecessors()) { + logPrecedingNodesRecursive(predecessor, visitedNodes, uriBuilder, handlerBuilder); + } + + return new ExtractionResult(uriBuilder, handlerBuilder); + } + + private static StringBuilder logPrecedingNodesRecursive(Node currentNode, Set visitedNodes, StringBuilder uriBuilder, StringBuilder handlerBuilder) { + if (visitedNodes.contains(currentNode)) { + return uriBuilder; + } + + visitedNodes.add(currentNode); + + System.out.println("Node: " + currentNode + ", Class: " + currentNode.getClass().getName()); + + // Check if the current node is the specific `Invoke#StringConcatHelper.simpleConcat` node + if (currentNode instanceof InvokeWithExceptionNode) { + InvokeWithExceptionNode invokeNode = (InvokeWithExceptionNode) currentNode; + if (invokeNode.callTarget().targetName().contains("StringConcatHelper.simpleConcat")) { + System.out.println("Found target Invoke: " + invokeNode); + + // Process the concatenation components + NodeInputList concatArgs = invokeNode.callTarget().arguments(); + for (ValueNode concatArg : concatArgs) { + if (concatArg instanceof ConstantNode) { + ConstantNode constantNode = (ConstantNode) concatArg; + uriBuilder.append(constantNode.asJavaConstant().toValueString()); + } + } + System.out.println("Concatenated URI: " + uriBuilder.toString()); + } + } + + // Check if the current node is an Invoke for handler instantiation + if (currentNode instanceof InvokeWithExceptionNode) { + InvokeWithExceptionNode invokeNode = (InvokeWithExceptionNode) currentNode; + if (invokeNode.callTarget().targetName().contains(".")) { // Look for constructors + System.out.println("Found handler constructor: " + invokeNode); + + // Extract the handler class name + handlerBuilder.append(invokeNode.callTarget().targetName().split("\\.")[0]); // Assuming format Class. + System.out.println("Handler: " + handlerBuilder.toString()); + } + } + + // Continue traversing predecessors + for (Node predecessor : currentNode.cfgPredecessors()) { + logPrecedingNodesRecursive(predecessor, visitedNodes, uriBuilder, handlerBuilder); + } + return uriBuilder; + } + + public static Set extractClassWebsocketConnection(Class clazz, AnalysisMetaAccess metaAccess, Inflation bb, Map propMap, String msName) { + Set websocketConnections = new HashSet<>(); + AnalysisType analysisType = metaAccess.lookupJavaType(clazz); + + try { + for (AnalysisMethod method : ((AnalysisMethod[]) analysisType.getDeclaredMethods())) { + + if (method.isAbstract()) { + continue; + } + + try { + StructuredGraph decodedGraph = ReachabilityAnalysisMethod.getDecodedGraph(bb, method); + + String uri = null; + String returnType = null; + WebsocketParameter param = null; + boolean isCollection = false; + StringBuilder uriBuilder = new StringBuilder(); + + // Loop through all nodes in the graph + for (Node node : decodedGraph.getNodes()) { + // Detect URI.create invocations + if (node instanceof Invoke) { + Invoke invoke = (Invoke) node; + AnalysisMethod targetMethod = (AnalysisMethod) invoke.getTargetMethod(); + + if (targetMethod.getQualifiedName().contains("URI.create")) { + CallTargetNode callTargetNode = invoke.callTarget(); + NodeInputList arguments = callTargetNode.arguments(); + + // Extract URI from its arguments + + // Extract URI parts (either constant or concatenated) + for (ValueNode arg : arguments) { + if (arg instanceof ConstantNode) { + ConstantNode constantNode = (ConstantNode) arg; + if (constantNode.getValue() instanceof DirectSubstrateObjectConstant) { + DirectSubstrateObjectConstant dsoc = (DirectSubstrateObjectConstant) constantNode.getValue(); + uriBuilder.append(dsoc.getObject().toString()); + } + } else if (arg instanceof Invoke) { + Invoke argInvoke = (Invoke) arg; + AnalysisMethod argTargetMethod = (AnalysisMethod) argInvoke.getTargetMethod(); + + // Handle string concatenation or StringBuilder.append + if (argTargetMethod.getQualifiedName().contains("StringConcatHelper.simpleConcat") + || argTargetMethod.getQualifiedName().contains("StringBuilder.append")) { + + // Process the concatenation components + NodeInputList concatArgs = argInvoke.callTarget().arguments(); + for (ValueNode concatArg : concatArgs) { + if (concatArg instanceof ConstantNode) { + ConstantNode concatConstant = (ConstantNode) concatArg; + if (concatConstant.getValue() instanceof DirectSubstrateObjectConstant) { + uriBuilder.append(((DirectSubstrateObjectConstant) concatConstant.getValue()).getObject().toString()); + } + } + } + } + } + } + + uri = uriBuilder.toString(); + System.out.println("Extracted WebSocket URI: " + uri); + } + } + } + + // Store extracted data in WebsocketConnection class + if (uri != null || returnType != null) { + String parentMethod = cleanParentMethod(method.getQualifiedName()); +// websocketConnections.add(new WebsocketConnection(parentMethod, returnType, uri, isCollection, clazz.getCanonicalName(), msName, param)); + + // Logging + System.out.println("PARENT METHOD = " + parentMethod); + System.out.println("RETURN TYPE = " + returnType); + System.out.println("URI = " + uri); + System.out.println("IS COLLECTION = " + isCollection); + } + } catch (Exception | LinkageError ex) { + ex.printStackTrace(); + } + } + } catch (Exception | LinkageError ex) { + ex.printStackTrace(); + } + return websocketConnections; + } + + public static Set extractClassWebsocketEndpoints(Class clazz, AnalysisMetaAccess metaAccess, Inflation bb, Map propMap, String msName) { + Set websocketEndpoints = new HashSet<>(); + AnalysisType analysisType = metaAccess.lookupJavaType(clazz); + + try { + for (AnalysisMethod method : ((AnalysisMethod[]) analysisType.getDeclaredMethods())) { + if (method.isAbstract()) { + continue; + } + try { + StructuredGraph decodedGraph = ReachabilityAnalysisMethod.getDecodedGraph(bb, method); + + String uri = null; + String wsHandler = null; + String returnType = null; + + WebsocketParameter param = null; + boolean isCollection = false; + StringBuilder uriBuilder = new StringBuilder(); + StringBuilder handlerBuilder = new StringBuilder(); + + // Loop through all nodes in the graph + for (Node node : decodedGraph.getNodes()) { + // Detect URI.create invocations + if (node instanceof Invoke) { + Invoke invoke = (Invoke) node; + AnalysisMethod targetMethod = (AnalysisMethod) invoke.getTargetMethod(); + + if (targetMethod.getQualifiedName().contains("WebSocketHandlerRegistry.addHandler")) { + // Log all nodes that precede this invoke node + Set visitedNodes = new HashSet<>(); + + // Start recursive traversal and logging + ExtractionResult extractionResult = logPrecedingNodesRecursiveURIandHandler(node, visitedNodes, uriBuilder, handlerBuilder); + uri = extractionResult.uriBuilder.toString(); + wsHandler = extractionResult.handlerBuilder.toString(); + } + + // Detect WebSocketClient.doHandshake method + if (targetMethod.getQualifiedName().contains("WebSocketClient.doHandshake")) { + System.out.println("Detected WebSocketClient.doHandshake invocation."); + + // Extract handler from the invocation parameters + CallTargetNode callTargetNode = invoke.callTarget(); + NodeInputList arguments = callTargetNode.arguments(); + + if (arguments.size() > 1) { + uri = extractURI(callTargetNode, propMap); + System.out.println("Extracted URI: " + URI); + } + + for (ValueNode arg : arguments) { + // Check if the argument is an AllocatedObjectNode (potential handler) + if (arg instanceof AllocatedObjectNode) { + AllocatedObjectNode allocatedObject = (AllocatedObjectNode) arg; + ObjectStamp objectStamp = (ObjectStamp) allocatedObject.stamp(NodeView.DEFAULT); + wsHandler = objectStamp.type().toJavaName(); + System.out.println("Extracted WebSocket Handler: " + wsHandler); + } + } + } + } + } + + // Store extracted data in WebsocketConnection class + if (uri != null || wsHandler != null) { + String parentMethod = cleanParentMethod(method.getQualifiedName()); + websocketEndpoints.add(new WebsocketEndpoint(parentMethod, returnType, uri, isCollection, clazz.getCanonicalName(), msName, param, wsHandler)); + + // Logging + System.out.println("PARENT METHOD = " + parentMethod); + System.out.println("RETURN TYPE = " + wsHandler); + System.out.println("URI = " + uri); + System.out.println("IS COLLECTION = " + isCollection); + } + } catch (Exception | LinkageError ex) { + ex.printStackTrace(); + } + } + } catch (Exception | LinkageError ex) { + ex.printStackTrace(); + } + return websocketEndpoints; + } + + public static Set extractClassWebsocketMessageTypes(Class clazz, AnalysisMetaAccess metaAccess, Inflation bb, Map propMap, String msName) { + Set websocketMessageTypes = new HashSet<>(); + AnalysisType analysisType = metaAccess.lookupJavaType(clazz); + + try { + for (AnalysisMethod method : ((AnalysisMethod[]) analysisType.getDeclaredMethods())) { + if (method.isAbstract()) { + continue; + } + try { + StructuredGraph decodedGraph = ReachabilityAnalysisMethod.getDecodedGraph(bb, method); + + String wsDataType = null; + String wsHandler = null; + boolean isCollection = false; + StringBuilder uriBuilder = new StringBuilder(); + WebsocketParameter param = null; + + // Loop through all nodes in the graph + for (Node node : decodedGraph.getNodes()) { + // Detect URI.create invocations + if (node instanceof Invoke) { + Invoke invoke = (Invoke) node; + AnalysisMethod targetMethod = (AnalysisMethod) invoke.getTargetMethod(); + + if (targetMethod.getQualifiedName().contains("AbstractWebSocketMessage.getPayload")) { + System.out.println("Detected WebSocket getPayload() call"); + + // Detect the readValue() call and extract the payload type (e.g., ChatMessage) + for (Node nextNode : decodedGraph.getNodes()) { + if (nextNode instanceof Invoke) { + Invoke nextInvoke = (Invoke) nextNode; + AnalysisMethod nextTargetMethod = (AnalysisMethod) nextInvoke.getTargetMethod(); + + if (nextTargetMethod.getQualifiedName().contains("ObjectMapper.readValue")) { + CallTargetNode nextCallTargetNode = nextInvoke.callTarget(); + NodeInputList nextArguments = nextCallTargetNode.arguments(); + + for (ValueNode arg : nextArguments) { + if (arg instanceof ConstantNode) { + ConstantNode constantNode = (ConstantNode) arg; + Object payloadObject = constantNode.getValue(); + + if (payloadObject instanceof DirectSubstrateObjectConstant) { + DirectSubstrateObjectConstant dsoc = (DirectSubstrateObjectConstant) payloadObject; + Object actualMessage = dsoc.getObject(); + + if (actualMessage instanceof com.oracle.svm.core.hub.DynamicHub) { + Class actualClass = extractHostedClass(actualMessage); + wsDataType = actualClass.getSimpleName(); + System.out.println("Actual message type: " + wsDataType); + } + } + } + } + } + } + } + } + + } + } + + wsHandler = clazz.getSimpleName(); + + // Store extracted data in WebsocketConnection class + if (wsHandler != null && wsDataType != null) { + String parentMethod = cleanParentMethod(method.getQualifiedName()); + websocketMessageTypes.add(new WebsocketMessageType(wsDataType, msName, wsHandler, parentMethod, isCollection, clazz.getCanonicalName(), param)); + + // Logging + System.out.println("PARENT METHOD = " + parentMethod); + System.out.println("RETURN TYPE = " + wsHandler); + System.out.println("IS COLLECTION = " + isCollection); + } + } catch (Exception | LinkageError ex) { + ex.printStackTrace(); + } + } + } catch (Exception | LinkageError ex) { + ex.printStackTrace(); + } + return websocketMessageTypes; + } + + + private static WebsocketParameter setIfBodyAndType(WebsocketParameter param, CallTargetNode node) { + for (ValueNode arg : node.arguments()) { + if (arg instanceof PiNode) { + for (Node inputNode : ((PiNode) arg).inputs()) { + if (inputNode instanceof Invoke) { + param = setIfBodyAndType(param, ((Invoke) inputNode).callTarget()); + } + } + } else if (arg instanceof Invoke) { + param = handleIfInvokeInWebsocketParam(param, arg); + } else { + } + } + return param; + } + + //nodes passed into here are only if they are instanceof Invoke + private static WebsocketParameter handleIfInvokeInWebsocketParam(WebsocketParameter param, ValueNode node) { +// System.out.println("\targ is an invoke and = " + node); + for (Node inNode : node.inputs()) { +// System.out.println("\t\tinvoke input = " + inNode); + if (inNode instanceof Invoke) { + param = handleIfInvokeInWebsocketParam(param, ((ValueNode) inNode)); + } + } +// System.out.println("\t\tpredecessor = " + node.predecessor() + ", class = " + node.predecessor().getClass()); + Node predecessor = node.predecessor(); + if (predecessor instanceof BeginNode && predecessor.predecessor() instanceof Invoke) { +// System.out.println("\t\t\tpredecessor instance of Begin and predecessor.BeginNode is an invoke"); +// System.out.println("\t\t\tpredecessor of BeginNode = " + predecessor.predecessor()); + Node bNodePredecessor = predecessor.predecessor(); + + if (((Invoke) predecessor.predecessor()).callTarget().targetMethod().toString().contains(WebsocketCallExtraction.WEBSOCKET_TEXT_MESSAGE)) { +// System.out.println("callTarget = " + ((Invoke)predecessor.predecessor()).callTarget()); + int inputAmnt = 0; + for (Node ctIn : ((Invoke) predecessor.predecessor()).callTarget().inputs()) { +// System.out.println("ctIn = " + ctIn); + inputAmnt++; + } + + //if virtualnode(?) has a zero but Allocated node has 3 inputs, there is a body with param. Seems there is always two inputs by default. Whatever the inputs minus 2 is how many params I think + int paramCount = inputAmnt - 2; + if (paramCount > 0) { + param.setParamCount(param.getParamCount() + paramCount); + param.setIsBody(true); + } + CommitAllocationNode caNode = (CommitAllocationNode) bNodePredecessor.predecessor(); + + + // for (Node caNodeInput : caNode.inputs()){ + // System.out.println("caNode input = " + caNodeInput); + // if (caNodeInput.toString().matches(".*VirtualInstance\\([0-9]*\\) HttpEntity")){ + // System.out.println("match found!"); + // System.out.println("between parentheses " + extractVirtualInstance(caNodeInput.toString())); + // //extract that number + // } + // } + + // int httpEntityValsCount = ((CommitAllocationNode)bNodePredecessor.predecessor()).getValues().size(); + // param.setParamCount(param.getParamCount() + httpEntityValsCount - 1); + // param.setIsBody(true); + } + // for (Node inNode : bNodePredecessor.inputs()){ + // System.out.println("\t\t\t\tinNode inputs = " + inNode); + // if (inNode instanceof Invoke){ + // param = handleIfInvokeInRESTParam(param, ((ValueNode)inNode)); + // } + // } + // System.out.println("bNodePredecessor predecessor = " + ((CommitAllocationNode)bNodePredecessor.predecessor()).getValues()); + // for (ValueNode vn : ((CommitAllocationNode)bNodePredecessor.predecessor()).getValues()){ + // System.out.println("vn constant node = " + (ConstantNode)vn + ", value " + ((ConstantNode)vn).getValue()); + // } + // System.out.println("HttpEntity params"); + // param.setParamCount(param.getParamCount() + ((CommitAllocationNode)bNodePredecessor.predecessor()).getValues() - 1); //-1 because one of those is the headers + + param = setIfBodyAndType(param, ((Invoke) predecessor.predecessor()).callTarget()); + } else { + param = setIfBodyAndType(param, ((Invoke) node).callTarget()); + } + return param; + } + + private static String extractURI(CallTargetNode node, Map propMap){ + // System.out.println("NODE CALL TARGET: " + node); + // System.out.println("NODE CALL TARGET ARGS: " + node.arguments()); + String uriPortion = ""; + + /* + * Loop over the arguments in the call target node + * if the node in the argument is an Invoke, call its target + * else if node is a loadfieldnode, go over annotations and get 'value' annotation + * get value based off prop map + */ + for (ValueNode arg : node.arguments()){ + NodeIterable inputsList = arg.inputs(); + if (arg instanceof LoadFieldNode){ + // System.out.println("arg is a LOAD_FIELD_NODE, arg = " + arg); + LoadFieldNode loadfieldNode = (LoadFieldNode) arg; + AnalysisField field = (AnalysisField) loadfieldNode.field(); + + for (java.lang.annotation.Annotation annotation : field.getWrapped().getAnnotations()) { + if (annotation.annotationType().getName().contains("Value")) { + // System.out.println("Load field with value annotation"); + // System.out.println("methods = " + annotation.annotationType().getMethods()); + try{ + Method valueMethod = annotation.annotationType().getMethod("value"); + valueMethod.setAccessible(true); + String res = ""; + if (propMap != null){ + res = tryResolve(((String)valueMethod.invoke(annotation)), propMap); + } + uriPortion = uriPortion + res; + }catch(Exception ex){ + System.err.println("ERROR = " + ex); + } + } + } + + } + else if (arg instanceof PiNode){ + // System.out.println(arg + " is a PiNode"); + // System.out.println("pi node inputs: " + ((PiNode)arg).inputs()); + for (Node inputNode : ((PiNode)arg).inputs()){ + if (inputNode instanceof Invoke){ + // System.out.println(inputNode + " is Invoke"); + uriPortion = uriPortion + extractURI(((Invoke)inputNode).callTarget(), propMap); + } + } + } + else if (arg instanceof ConstantNode){ + ConstantNode cn = (ConstantNode)arg; + //PrimitiveConstants can not be converted to DirectSubstrateObjectConstant + if (!(cn.getValue() instanceof PrimitiveConstant)){ + DirectSubstrateObjectConstant dsoc = (DirectSubstrateObjectConstant)cn.getValue(); + uriPortion = uriPortion + dsoc.getObject().toString(); + } + + } + else if (arg instanceof Invoke){ + // System.out.println("arg = " + arg + " && is an instance of invoke"); + uriPortion = uriPortion + extractURI(((Invoke)arg).callTarget(), propMap); + } + else{ + for (Node n : inputsList){ + if (n instanceof Invoke){; + uriPortion = uriPortion + extractURI(((Invoke)n).callTarget(), propMap); + } + } + } + + } + return uriPortion; + } + + /** + * extract the method the rest call is being in + * + * @param input the method's qualified name + * @return the method the call is being made in + */ + private static String cleanParentMethod(String input) { + String parentMethod = null; + + parentMethod = input.substring(0, input.indexOf("(")); + return parentMethod; + } + + //TO-DO: find a safer way to cast Map value + @SuppressWarnings("unchecked") + private static String tryResolve(String expr, Map propMap) { + + String mergedKey = expr.substring(2, expr.length() - 1); + String[] path = mergedKey.split("\\."); + Map curr = propMap; + for (int i = 0; i < path.length; i++) { + String key = path[i]; + Object value = curr.get(key); + if (value == null) { + return null; + } + if (value instanceof String && i == path.length - 1) { + return ((String) value); + } + if (value instanceof Map) { + try { + curr = ((Map) value); + } catch (ClassCastException ex) { + ex.printStackTrace(); + } + } + } + return null; + } + +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/Module.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/Module.java index 81d8c69f39c6..b9afa42f3489 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/Module.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/Module.java @@ -8,33 +8,37 @@ public class Module { private Set entities; private Set restCalls; + private Set websocketConnections; private Set endpoints; - public Module(Name name, Set entities, Set restCalls, Set endpoints) { + public Module(Name name, Set entities, Set restCalls, Set websocketConnections, Set endpoints) { this.name = name; this.entities = entities; this.restCalls = restCalls; + this.websocketConnections = websocketConnections; this.endpoints = endpoints; } public String shortSummary() { return "Module(name=" + - name + - ",entities=" + - entities.size() + - ",restcalls=" + - restCalls.size() + - ",endpoints=" + - endpoints.size() + - ')'; + name + + ",entities=" + + entities.size() + + ",restcalls=" + + restCalls.size() + + ",websocketConnections=" + + websocketConnections.size() + + ",endpoints=" + + endpoints.size() + + ')'; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("MODULE NAME = ").append(name).append("\n").append("\nENTITIES = \n").append(setToString(entities)) - .append("\nREST_CALLS = \n").append(setToString(restCalls)).append("\nENDPOINTS = \n").append(setToString(endpoints)) - .append('\n'); + .append("\nREST_CALLS = \n").append(setToString(restCalls)).append("\nWEBSOCKET_CONNECTIONS = \n").append(setToString(websocketConnections)).append("\nENDPOINTS = \n").append(setToString(endpoints)) + .append('\n'); return sb.toString(); } @@ -67,6 +71,10 @@ public Set getRestCalls() { return restCalls; } + public Set getWebsocketConnections() { + return websocketConnections; + } + public Set getEndpoints() { return endpoints; } @@ -75,6 +83,9 @@ public Set getEndpoints() { public void setRestCalls(Set restCalls) { this.restCalls = restCalls; } + public void setWebsocketConnections(Set websocketConnections) { + this.websocketConnections = websocketConnections; + } public void setEndpoints(Set endpoints) { this.endpoints = endpoints; diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketConnection.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketConnection.java new file mode 100644 index 000000000000..2e1027a961ae --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketConnection.java @@ -0,0 +1,91 @@ +package com.oracle.svm.hosted.prophet.model; + +public class WebsocketConnection { + + private String parentMethod; + private String returnType; + private String uri; + private boolean isCollection; + private String connectionInClassName; + private String msName; + private WebsocketParameter param; + + public WebsocketConnection(String parentMethod, String returnType, String uri, Boolean isCollection, + String connectionInClassName, String msName, WebsocketParameter param) { + + this.msName = msName; + this.connectionInClassName = connectionInClassName; + this.parentMethod = parentMethod; + this.uri = uri; + this.returnType = returnType; + this.param = param; + this.isCollection = isCollection; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(this.msName != null ? this.msName : "").append(",") + .append(connectionInClassName != null ? connectionInClassName : "").append(",") + .append(parentMethod != null ? parentMethod : "").append(",") + .append(uri != null ? uri : "").append(",") + .append(returnType != null ? returnType : "").append(",") + .append(param != null ? param : "").append(",") + .append(isCollection); + return sb.toString(); + } + + // Getter methods + public WebsocketParameter getParam(){ + return this.param; + } + + public String getMsName() { + return this.msName; + } + + public String getConnectionInClassName() { + return this.connectionInClassName; + } + + public String getParentMethod() { + return parentMethod; + } + + public String getReturnType() { + return returnType; + } + + public String getUri() { + return uri; + } + + public boolean isCollection() { + return isCollection; + } + + // Setter methods + public void setMsName(String msName) { + this.msName = msName; + } + + public void setConnectionInClassName(String className) { + this.connectionInClassName = className; + } + + public void setParentMethod(String parentMethod) { + this.parentMethod = parentMethod; + } + + public void setReturnType(String returnType) { + this.returnType = returnType; + } + + public void setUri(String uri) { + this.uri = uri; + } + + public void setCollection(boolean isCollection) { + this.isCollection = isCollection; + } +} \ No newline at end of file diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketEndpoint.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketEndpoint.java new file mode 100644 index 000000000000..2ddaa90b0fa9 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketEndpoint.java @@ -0,0 +1,87 @@ +package com.oracle.svm.hosted.prophet.model; + +public class WebsocketEndpoint { + + private String uri; + private String msName; + private String wsHandler; + private String parentMethod; + private String returnType; + private boolean isCollection; + private String connectionInClassName; + private WebsocketParameter param; + + public WebsocketEndpoint(String parentMethod, String returnType, String uri, Boolean isCollection, + String connectionInClassName, String msName, WebsocketParameter param, String wsHandler) { + + this.parentMethod = parentMethod; + this.returnType = returnType; + this.uri = uri; + this.isCollection = isCollection; + this.connectionInClassName = connectionInClassName; + this.msName = msName; + this.param = param; + this.wsHandler = wsHandler; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(this.msName != null ? this.msName : "").append(",") + .append(connectionInClassName != null ? connectionInClassName : "").append(",") + .append(parentMethod != null ? parentMethod : "").append(",") + .append(uri != null ? uri : "").append(",") + .append(returnType != null ? returnType : "").append(",") + .append(param != null ? param.getIsPath() : "").append(",") + .append(param != null ? param.getIsBody() : "").append(",") + .append(param != null ? param.getParamType() : "").append(",") + .append(param != null ? param.getParamCount() : "").append(",") + .append(isCollection); + return sb.toString(); + } + + public String getMsName() { + return this.msName; + } + + public String getUri() { + return uri; + } + + public String getWsHandler() { + return wsHandler; + } + + public String getParentMethod() { + return parentMethod; + } + + public String getReturnType() { + return returnType; + } + + public boolean isCollection() { + return isCollection; + } + + public String getConnectionInClassName() { + return connectionInClassName; + } + + public WebsocketParameter getParam() { + return param; + } + + // Setter methods + public void setMsName(String msName) { + this.msName = msName; + } + + public void setWsHandler(String wsHandler) { + this.wsHandler = wsHandler; + } + + public void setUri(String uri) { + this.uri = uri; + } +} \ No newline at end of file diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketMessageType.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketMessageType.java new file mode 100644 index 000000000000..6f77bc02bd44 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketMessageType.java @@ -0,0 +1,102 @@ +package com.oracle.svm.hosted.prophet.model; + +public class WebsocketMessageType { + + private String wsDataType; + private String msName; + private String wsHandler; + private String parentMethod; + private String returnType; + private boolean isCollection; + private String connectionInClassName; + private WebsocketParameter param; + + public WebsocketMessageType(String wsDataType, String msName, String wsHandler, String parentMethod, + boolean isCollection, String connectionInClassName, + WebsocketParameter param) { + this.wsDataType = wsDataType; + this.msName = msName; + this.wsHandler = wsHandler; + this.parentMethod = parentMethod; + this.isCollection = isCollection; + this.connectionInClassName = connectionInClassName; + this.param = param; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(this.msName != null ? this.msName : "").append(",") + .append(parentMethod != null ? parentMethod : "").append(",") + .append(wsDataType != null ? wsDataType : "").append(",") + .append(isCollection).append(",") + .append(connectionInClassName != null ? connectionInClassName : "").append(",") + .append(param != null ? param.toString() : ""); + return sb.toString(); + } + + public String getWsDataType() { + return wsDataType; + } + + public String getMsName() { + return msName; + } + + public String getWsHandler() { + return wsHandler; + } + + public String getParentMethod() { + return parentMethod; + } + + public String getReturnType() { + return returnType; + } + + public boolean isCollection() { + return isCollection; + } + + public String getConnectionInClassName() { + return connectionInClassName; + } + + public WebsocketParameter getParam() { + return param; + } + + // Setter methods + public void setWsDataType(String wsDataType) { + this.wsDataType = wsDataType; + } + + public void setMsName(String msName) { + this.msName = msName; + } + + public void setWsHandler(String wsHandler) { + this.wsHandler = wsHandler; + } + + public void setParentMethod(String parentMethod) { + this.parentMethod = parentMethod; + } + + public void setReturnType(String returnType) { + this.returnType = returnType; + } + + public void setCollection(boolean isCollection) { + this.isCollection = isCollection; + } + + public void setConnectionInClassName(String connectionInClassName) { + this.connectionInClassName = connectionInClassName; + } + + public void setParam(WebsocketParameter param) { + this.param = param; + } +} \ No newline at end of file diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketParameter.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketParameter.java new file mode 100644 index 000000000000..bcbc922e52e8 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketParameter.java @@ -0,0 +1,48 @@ +package com.oracle.svm.hosted.prophet.model; + +public class WebsocketParameter { + + private Boolean isBody; + private Boolean isPath; + private String paramType; + private int paramCount = 0; + + public WebsocketParameter(Boolean isBody, Boolean isPath){ + this.isBody = isBody; + this.isPath = isPath; + } + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("isBody " + isBody + ", isPath = " + isPath + ", paramType = " + paramType + ", paramCount = " + paramCount); + return sb.toString(); + } + public void setParamType(String type){ + this.paramType = type; + } + public String getParamType() { + return this.paramType; + } + public Boolean getIsBody() { + return this.isBody; + } + public void setIsBody(Boolean isBody) { + this.isBody = isBody; + } + + public Boolean getIsPath() { + return this.isPath; + } + + public void setIsPath(Boolean isPath) { + this.isPath = isPath; + } + public int getParamCount() { + return this.paramCount; + } + + public void setParamCount(int c) { + this.paramCount = c; + } + +} From 711e11d8dc5a34db14b6d96cec20469a27c0a274 Mon Sep 17 00:00:00 2001 From: macbook Date: Mon, 24 Mar 2025 06:40:10 +0100 Subject: [PATCH 2/6] add a stomp support --- .../svm/hosted/prophet/ProphetPlugin.java | 35 ++-- .../oracle/svm/hosted/prophet/RestDump.java | 14 ++ .../prophet/WebsocketCallExtraction.java | 175 +++++++++--------- .../svm/hosted/prophet/model/Module.java | 17 +- .../prophet/model/WebsocketConnection.java | 8 +- .../prophet/model/WebsocketEndpoint.java | 3 - 6 files changed, 143 insertions(+), 109 deletions(-) diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/ProphetPlugin.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/ProphetPlugin.java index 29e07e4bb382..709047a787c9 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/ProphetPlugin.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/ProphetPlugin.java @@ -81,6 +81,9 @@ public static class Options { @Option(help = "Where to store the websocketConnections output")// public static final HostedOptionKey ProphetWebsocketConnectionsOutputFile = new HostedOptionKey<>(null); + @Option(help = "Where to store the websocketEndpoints output")// + public static final HostedOptionKey ProphetWebsocketEndpointsOutputFile = new HostedOptionKey<>(null); + @Option(help = "Where to store the endpoint output")// public static final HostedOptionKey ProphetEndpointOutputFile = new HostedOptionKey<>(null); @@ -106,6 +109,7 @@ public static void run(ImageClassLoader loader, AnalysisUniverse aUniverse, Anal restDump.writeOutRestCalls(module.getRestCalls(), Options.ProphetRestCallOutputFile.getValue()); restDump.writeOutWebsocketConnections(module.getWebsocketConnections(), Options.ProphetWebsocketConnectionsOutputFile.getValue()); restDump.writeOutEndpoints(module.getEndpoints(), Options.ProphetEndpointOutputFile.getValue()); + restDump.writeOutWebsocketEndpoints(module.getWebsocketEndpoints(), Options.ProphetWebsocketEndpointsOutputFile.getValue()); dumpModule(module); logger.info("Final summary: " + module.shortSummary()); @@ -159,56 +163,59 @@ private Module doRun() { return processClasses(classes); } - private Set linkWebsocketEndpointsAndMessageTypes(Set websocketEndpointsList, Set websocketMessageTypesList) { - Set websocketConnections = new HashSet(); + private Set linkWebsocketEndpointsAndMessageTypes(Set websocketEndpointsList, Set websocketMessageTypesList) { + Set websocketEndpoints = new HashSet(); for (WebsocketEndpoint endpoint : websocketEndpointsList) { if (websocketMessageTypesList.isEmpty()) { - WebsocketConnection connection = new WebsocketConnection( + WebsocketEndpoint connection = new WebsocketEndpoint( endpoint.getParentMethod(), endpoint.getReturnType(), endpoint.getUri(), endpoint.isCollection(), endpoint.getConnectionInClassName(), endpoint.getMsName(), - endpoint.getParam() + endpoint.getParam(), + endpoint.getWsHandler() ); - websocketConnections.add(connection); + websocketEndpoints.add(connection); } else { boolean handlerMatched = false; for (WebsocketMessageType messageType : websocketMessageTypesList) { String endpointHandler = endpoint.getWsHandler(); String messageTypeHandler = messageType.getWsHandler(); if (endpointHandler.equals(messageTypeHandler)) { - WebsocketConnection connection = new WebsocketConnection( + WebsocketEndpoint connection = new WebsocketEndpoint( endpoint.getParentMethod() != null ? endpoint.getParentMethod() : messageType.getParentMethod(), messageType.getWsDataType(), endpoint.getUri(), endpoint.isCollection(), endpoint.getConnectionInClassName() != null ? endpoint.getConnectionInClassName() : messageType.getConnectionInClassName(), endpoint.getMsName() != null ? endpoint.getMsName() : messageType.getMsName(), - endpoint.getParam() != null ? endpoint.getParam() : messageType.getParam() + endpoint.getParam() != null ? endpoint.getParam() : messageType.getParam(), + endpoint.getWsHandler() ); - websocketConnections.add(connection); + websocketEndpoints.add(connection); handlerMatched = true; break; } } if (!handlerMatched) { - WebsocketConnection connection = new WebsocketConnection( + WebsocketEndpoint connection = new WebsocketEndpoint( endpoint.getParentMethod(), endpoint.getReturnType(), endpoint.getUri(), endpoint.isCollection(), endpoint.getConnectionInClassName(), endpoint.getMsName(), - endpoint.getParam() + endpoint.getParam(), + endpoint.getWsHandler() ); - websocketConnections.add(connection); + websocketEndpoints.add(connection); } } } - return websocketConnections; + return websocketEndpoints; } private Module processClasses(List> classes) { @@ -237,9 +244,9 @@ private Module processClasses(List> classes) { endpointList.addAll(endpoints); } - websocketConnectionsList.addAll(linkWebsocketEndpointsAndMessageTypes(websocketEndpointsList, websocketMessageTypesList)); + websocketEndpointsList.addAll(linkWebsocketEndpointsAndMessageTypes(websocketEndpointsList, websocketMessageTypesList)); - return new Module(new Name(msName), entities, restCallList, websocketConnectionsList, endpointList); + return new Module(new Name(msName), entities, restCallList, websocketConnectionsList, websocketEndpointsList, endpointList); } private List> filterRelevantClasses() { diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/RestDump.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/RestDump.java index 5950af6fd78d..7bc419d59eef 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/RestDump.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/RestDump.java @@ -2,6 +2,7 @@ import com.oracle.svm.hosted.prophet.model.Endpoint; import com.oracle.svm.hosted.prophet.model.RestCall; import com.oracle.svm.hosted.prophet.model.WebsocketConnection; +import com.oracle.svm.hosted.prophet.model.WebsocketEndpoint; import java.io.IOException; import java.util.Set; @@ -47,6 +48,19 @@ public void writeOutWebsocketConnections(Set websocketConne } } + public void writeOutWebsocketEndpoints(Set websocketEndpoints, String outputFile) { + if (outputFile == null) { + throw new RuntimeException("ProphetWebsocketEndpointOutputFile option was not provided"); + } + try (BufferedWriter writer = new BufferedWriter(new FileWriter(outputFile))) { + for (WebsocketEndpoint wc : websocketEndpoints) { + writer.write(wc.toString() + "\n"); + } + } catch (IOException ex) { + ex.printStackTrace(); + } + } + public void writeOutEndpoints(Set endpoints, String outputFile){ if (outputFile == null){ throw new RuntimeException("ProphetEndpointOutputFile option was not provided"); diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/WebsocketCallExtraction.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/WebsocketCallExtraction.java index 748ccadb19f9..019a65845dfd 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/WebsocketCallExtraction.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/WebsocketCallExtraction.java @@ -12,8 +12,6 @@ import com.oracle.svm.hosted.prophet.model.WebsocketMessageType; import com.oracle.svm.hosted.prophet.model.WebsocketParameter; import jdk.vm.ci.meta.PrimitiveConstant; -import jdk.vm.ci.meta.ResolvedJavaMethod; -import jdk.vm.ci.meta.ResolvedJavaMethod.Parameter; import org.graalvm.compiler.core.common.type.ObjectStamp; import org.graalvm.compiler.graph.Node; import org.graalvm.compiler.graph.NodeInputList; @@ -24,20 +22,20 @@ import org.graalvm.compiler.nodes.Invoke; import org.graalvm.compiler.nodes.InvokeWithExceptionNode; import org.graalvm.compiler.nodes.NodeView; +import org.graalvm.compiler.nodes.ParameterNode; import org.graalvm.compiler.nodes.PiNode; import org.graalvm.compiler.nodes.StructuredGraph; import org.graalvm.compiler.nodes.ValueNode; import org.graalvm.compiler.nodes.java.LoadFieldNode; import org.graalvm.compiler.nodes.virtual.AllocatedObjectNode; import org.graalvm.compiler.nodes.virtual.CommitAllocationNode; +import org.graalvm.compiler.nodes.virtual.VirtualArrayNode; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.HashSet; import java.util.Map; import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; public class WebsocketCallExtraction { @@ -169,6 +167,7 @@ private static StringBuilder logPrecedingNodesRecursive(Node currentNode, Set extractClassWebsocketConnection(Class clazz, AnalysisMetaAccess metaAccess, Inflation bb, Map propMap, String msName) { Set websocketConnections = new HashSet<>(); AnalysisType analysisType = metaAccess.lookupJavaType(clazz); + String wsHandler = null; try { for (AnalysisMethod method : ((AnalysisMethod[]) analysisType.getDeclaredMethods())) { @@ -193,52 +192,55 @@ public static Set extractClassWebsocketConnection(Class Invoke invoke = (Invoke) node; AnalysisMethod targetMethod = (AnalysisMethod) invoke.getTargetMethod(); - if (targetMethod.getQualifiedName().contains("URI.create")) { + // Briefly explained: + // 1) Check if there are at least two arguments. + // 2) If the second argument is AllocatedObjectNode, extract the type name from its stamp. + if (targetMethod.getQualifiedName().contains("WebSocketStompClient.connect")) { + CallTargetNode ct = invoke.callTarget(); + if (!ct.arguments().isEmpty()) { + uri = extractURI(ct, propMap); + } + if (ct.arguments().size() > 1) { + ValueNode secondArg = ct.arguments().get(2); + if (secondArg instanceof ParameterNode) { + ParameterNode paramNode = (ParameterNode) secondArg; + ObjectStamp stamp = (ObjectStamp) paramNode.uncheckedStamp(); + wsHandler = stamp.type().toJavaName(); + } + } + } + + // Detect WebSocketClient.doHandshake method + if (targetMethod.getQualifiedName().contains("WebSocketClient.doHandshake")) { + System.out.println("Detected WebSocketClient.doHandshake invocation."); + + // Extract handler from the invocation parameters CallTargetNode callTargetNode = invoke.callTarget(); NodeInputList arguments = callTargetNode.arguments(); - // Extract URI from its arguments + if (arguments.size() > 1) { + uri = extractURI(callTargetNode, propMap); + System.out.println("Extracted URI: " + URI); + } - // Extract URI parts (either constant or concatenated) for (ValueNode arg : arguments) { - if (arg instanceof ConstantNode) { - ConstantNode constantNode = (ConstantNode) arg; - if (constantNode.getValue() instanceof DirectSubstrateObjectConstant) { - DirectSubstrateObjectConstant dsoc = (DirectSubstrateObjectConstant) constantNode.getValue(); - uriBuilder.append(dsoc.getObject().toString()); - } - } else if (arg instanceof Invoke) { - Invoke argInvoke = (Invoke) arg; - AnalysisMethod argTargetMethod = (AnalysisMethod) argInvoke.getTargetMethod(); - - // Handle string concatenation or StringBuilder.append - if (argTargetMethod.getQualifiedName().contains("StringConcatHelper.simpleConcat") - || argTargetMethod.getQualifiedName().contains("StringBuilder.append")) { - - // Process the concatenation components - NodeInputList concatArgs = argInvoke.callTarget().arguments(); - for (ValueNode concatArg : concatArgs) { - if (concatArg instanceof ConstantNode) { - ConstantNode concatConstant = (ConstantNode) concatArg; - if (concatConstant.getValue() instanceof DirectSubstrateObjectConstant) { - uriBuilder.append(((DirectSubstrateObjectConstant) concatConstant.getValue()).getObject().toString()); - } - } - } - } + // Check if the argument is an AllocatedObjectNode (potential handler) + if (arg instanceof AllocatedObjectNode) { + AllocatedObjectNode allocatedObject = (AllocatedObjectNode) arg; + ObjectStamp objectStamp = (ObjectStamp) allocatedObject.stamp(NodeView.DEFAULT); + wsHandler = objectStamp.type().toJavaName(); + System.out.println("Extracted WebSocket Handler: " + wsHandler); } } - - uri = uriBuilder.toString(); - System.out.println("Extracted WebSocket URI: " + uri); } + } } // Store extracted data in WebsocketConnection class if (uri != null || returnType != null) { String parentMethod = cleanParentMethod(method.getQualifiedName()); -// websocketConnections.add(new WebsocketConnection(parentMethod, returnType, uri, isCollection, clazz.getCanonicalName(), msName, param)); + websocketConnections.add(new WebsocketConnection(parentMethod, returnType, uri, isCollection, clazz.getCanonicalName(), msName, param, wsHandler)); // Logging System.out.println("PARENT METHOD = " + parentMethod); @@ -294,28 +296,14 @@ public static Set extractClassWebsocketEndpoints(Class cla wsHandler = extractionResult.handlerBuilder.toString(); } - // Detect WebSocketClient.doHandshake method - if (targetMethod.getQualifiedName().contains("WebSocketClient.doHandshake")) { - System.out.println("Detected WebSocketClient.doHandshake invocation."); - - // Extract handler from the invocation parameters - CallTargetNode callTargetNode = invoke.callTarget(); - NodeInputList arguments = callTargetNode.arguments(); - - if (arguments.size() > 1) { - uri = extractURI(callTargetNode, propMap); - System.out.println("Extracted URI: " + URI); - } - - for (ValueNode arg : arguments) { - // Check if the argument is an AllocatedObjectNode (potential handler) - if (arg instanceof AllocatedObjectNode) { - AllocatedObjectNode allocatedObject = (AllocatedObjectNode) arg; - ObjectStamp objectStamp = (ObjectStamp) allocatedObject.stamp(NodeView.DEFAULT); - wsHandler = objectStamp.type().toJavaName(); - System.out.println("Extracted WebSocket Handler: " + wsHandler); - } + // **New** check for StompEndpointRegistry.addEndpoint + if (targetMethod.getQualifiedName().contains("StompEndpointRegistry.addEndpoint")) { + // Extract the first parameter as a URI + CallTargetNode ct = invoke.callTarget(); + if (!ct.arguments().isEmpty()) { + uri = extractURI(ct, propMap); } + wsHandler = "StompEndpointHandler"; } } } @@ -366,6 +354,16 @@ public static Set extractClassWebsocketMessageTypes(Class< Invoke invoke = (Invoke) node; AnalysisMethod targetMethod = (AnalysisMethod) invoke.getTargetMethod(); + if (targetMethod.getQualifiedName().contains("StompSessionHandlerAdapter.getPayloadType")) { + // Retrieve the return type + AnalysisType retType = (AnalysisType) targetMethod.getSignature().getReturnType(null); + if (retType != null) { + wsDataType = retType.getName(); + // Extract the handler name from the declaring class + wsHandler = targetMethod.getDeclaringClass().getName(); + } + } + if (targetMethod.getQualifiedName().contains("AbstractWebSocketMessage.getPayload")) { System.out.println("Detected WebSocket getPayload() call"); @@ -400,7 +398,6 @@ public static Set extractClassWebsocketMessageTypes(Class< } } } - } } @@ -473,34 +470,7 @@ private static WebsocketParameter handleIfInvokeInWebsocketParam(WebsocketParame param.setParamCount(param.getParamCount() + paramCount); param.setIsBody(true); } - CommitAllocationNode caNode = (CommitAllocationNode) bNodePredecessor.predecessor(); - - - // for (Node caNodeInput : caNode.inputs()){ - // System.out.println("caNode input = " + caNodeInput); - // if (caNodeInput.toString().matches(".*VirtualInstance\\([0-9]*\\) HttpEntity")){ - // System.out.println("match found!"); - // System.out.println("between parentheses " + extractVirtualInstance(caNodeInput.toString())); - // //extract that number - // } - // } - - // int httpEntityValsCount = ((CommitAllocationNode)bNodePredecessor.predecessor()).getValues().size(); - // param.setParamCount(param.getParamCount() + httpEntityValsCount - 1); - // param.setIsBody(true); } - // for (Node inNode : bNodePredecessor.inputs()){ - // System.out.println("\t\t\t\tinNode inputs = " + inNode); - // if (inNode instanceof Invoke){ - // param = handleIfInvokeInRESTParam(param, ((ValueNode)inNode)); - // } - // } - // System.out.println("bNodePredecessor predecessor = " + ((CommitAllocationNode)bNodePredecessor.predecessor()).getValues()); - // for (ValueNode vn : ((CommitAllocationNode)bNodePredecessor.predecessor()).getValues()){ - // System.out.println("vn constant node = " + (ConstantNode)vn + ", value " + ((ConstantNode)vn).getValue()); - // } - // System.out.println("HttpEntity params"); - // param.setParamCount(param.getParamCount() + ((CommitAllocationNode)bNodePredecessor.predecessor()).getValues() - 1); //-1 because one of those is the headers param = setIfBodyAndType(param, ((Invoke) predecessor.predecessor()).callTarget()); } else { @@ -559,11 +529,38 @@ else if (arg instanceof PiNode){ else if (arg instanceof ConstantNode){ ConstantNode cn = (ConstantNode)arg; //PrimitiveConstants can not be converted to DirectSubstrateObjectConstant - if (!(cn.getValue() instanceof PrimitiveConstant)){ - DirectSubstrateObjectConstant dsoc = (DirectSubstrateObjectConstant)cn.getValue(); - uriPortion = uriPortion + dsoc.getObject().toString(); + if (cn.asJavaConstant() != null && cn.asJavaConstant().isNull()) { + // Skip, it's null + } else if (!(cn.getValue() instanceof PrimitiveConstant)) { + DirectSubstrateObjectConstant dsoc = (DirectSubstrateObjectConstant) cn.getValue(); + if (dsoc.getObject() != null) { + uriPortion += dsoc.getObject().toString(); + } + } + } + else if (arg instanceof AllocatedObjectNode) { + // Handle allocated objects, which may contain constant values + AllocatedObjectNode allocatedObject = (AllocatedObjectNode) arg; + for (Node input : allocatedObject.inputs()) { + if (input instanceof CommitAllocationNode) { + CommitAllocationNode varr = (CommitAllocationNode) input; + for (ValueNode element : varr.getValues()) { + + if (element instanceof ConstantNode) { + ConstantNode cn = (ConstantNode) element; + // PrimitiveConstants cannot be converted to DirectSubstrateObjectConstant + if (cn.asJavaConstant() != null && cn.asJavaConstant().isNull()) { + // Skip, it's null + } else if (!(cn.getValue() instanceof PrimitiveConstant)) { + DirectSubstrateObjectConstant dsoc = (DirectSubstrateObjectConstant) cn.getValue(); + if (dsoc.getObject() != null) { + uriPortion += dsoc.getObject().toString(); + } + } + } + } + } } - } else if (arg instanceof Invoke){ // System.out.println("arg = " + arg + " && is an instance of invoke"); diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/Module.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/Module.java index b9afa42f3489..b9794bfbbfa9 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/Module.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/Module.java @@ -9,13 +9,15 @@ public class Module { private Set entities; private Set restCalls; private Set websocketConnections; + private Set websocketEndpoints; private Set endpoints; - public Module(Name name, Set entities, Set restCalls, Set websocketConnections, Set endpoints) { + public Module(Name name, Set entities, Set restCalls, Set websocketConnections, Set websocketEndpoints, Set endpoints) { this.name = name; this.entities = entities; this.restCalls = restCalls; this.websocketConnections = websocketConnections; + this.websocketEndpoints = websocketEndpoints; this.endpoints = endpoints; } @@ -28,6 +30,8 @@ public String shortSummary() { restCalls.size() + ",websocketConnections=" + websocketConnections.size() + + ",websocketEndpoints=" + + websocketEndpoints.size() + ",endpoints=" + endpoints.size() + ')'; @@ -37,7 +41,7 @@ public String shortSummary() { public String toString() { StringBuilder sb = new StringBuilder(); sb.append("MODULE NAME = ").append(name).append("\n").append("\nENTITIES = \n").append(setToString(entities)) - .append("\nREST_CALLS = \n").append(setToString(restCalls)).append("\nWEBSOCKET_CONNECTIONS = \n").append(setToString(websocketConnections)).append("\nENDPOINTS = \n").append(setToString(endpoints)) + .append("\nREST_CALLS = \n").append(setToString(restCalls)).append("\nWEBSOCKET_CONNECTIONS = \n").append(setToString(websocketConnections)).append("\nWEBSOCKET_ENDPOINTS = \n").append(setToString(websocketEndpoints)).append("\nENDPOINTS = \n").append(setToString(endpoints)) .append('\n'); return sb.toString(); } @@ -75,6 +79,10 @@ public Set getWebsocketConnections() { return websocketConnections; } + public Set getWebsocketEndpoints() { + return websocketEndpoints; + } + public Set getEndpoints() { return endpoints; } @@ -83,10 +91,15 @@ public Set getEndpoints() { public void setRestCalls(Set restCalls) { this.restCalls = restCalls; } + public void setWebsocketConnections(Set websocketConnections) { this.websocketConnections = websocketConnections; } + public void setWebsocketEndpoints(Set websocketEndpoints) { + this.websocketEndpoints = websocketEndpoints; + } + public void setEndpoints(Set endpoints) { this.endpoints = endpoints; } diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketConnection.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketConnection.java index 2e1027a961ae..4393a1a8fec4 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketConnection.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketConnection.java @@ -4,6 +4,7 @@ public class WebsocketConnection { private String parentMethod; private String returnType; + private String wsHandler; private String uri; private boolean isCollection; private String connectionInClassName; @@ -11,7 +12,7 @@ public class WebsocketConnection { private WebsocketParameter param; public WebsocketConnection(String parentMethod, String returnType, String uri, Boolean isCollection, - String connectionInClassName, String msName, WebsocketParameter param) { + String connectionInClassName, String msName, WebsocketParameter param, String wsHandler) { this.msName = msName; this.connectionInClassName = connectionInClassName; @@ -20,6 +21,7 @@ public WebsocketConnection(String parentMethod, String returnType, String uri, B this.returnType = returnType; this.param = param; this.isCollection = isCollection; + this.wsHandler = wsHandler; } @Override @@ -88,4 +90,8 @@ public void setUri(String uri) { public void setCollection(boolean isCollection) { this.isCollection = isCollection; } + + public String getWsHandler() { + return wsHandler; + } } \ No newline at end of file diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketEndpoint.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketEndpoint.java index 2ddaa90b0fa9..8e3b7169ef70 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketEndpoint.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketEndpoint.java @@ -32,9 +32,6 @@ public String toString() { .append(parentMethod != null ? parentMethod : "").append(",") .append(uri != null ? uri : "").append(",") .append(returnType != null ? returnType : "").append(",") - .append(param != null ? param.getIsPath() : "").append(",") - .append(param != null ? param.getIsBody() : "").append(",") - .append(param != null ? param.getParamType() : "").append(",") .append(param != null ? param.getParamCount() : "").append(",") .append(isCollection); return sb.toString(); From ba395c054058e084d96996e221eb00309e953b5f Mon Sep 17 00:00:00 2001 From: macbook Date: Sun, 30 Mar 2025 21:04:25 +0200 Subject: [PATCH 3/6] add a graphQL support --- .../hosted/prophet/GraphQLCallExtraction.java | 354 ++++++++++++++++++ .../prophet/GraphQLEndpointExtraction.java | 137 +++++++ .../svm/hosted/prophet/ProphetPlugin.java | 22 +- .../oracle/svm/hosted/prophet/RestDump.java | 28 ++ .../svm/hosted/prophet/model/GraphQLCall.java | 93 +++++ .../hosted/prophet/model/GraphQLEndpoint.java | 110 ++++++ .../svm/hosted/prophet/model/Module.java | 21 +- 7 files changed, 760 insertions(+), 5 deletions(-) create mode 100644 substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/GraphQLCallExtraction.java create mode 100644 substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/GraphQLEndpointExtraction.java create mode 100644 substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/GraphQLCall.java create mode 100644 substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/GraphQLEndpoint.java diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/GraphQLCallExtraction.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/GraphQLCallExtraction.java new file mode 100644 index 000000000000..bca17aa5b06f --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/GraphQLCallExtraction.java @@ -0,0 +1,354 @@ +package com.oracle.svm.hosted.prophet; + +import com.oracle.graal.pointsto.meta.AnalysisField; +import com.oracle.graal.pointsto.meta.AnalysisMetaAccess; +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.graal.pointsto.meta.AnalysisType; +import com.oracle.graal.reachability.ReachabilityAnalysisMethod; +import com.oracle.svm.core.meta.DirectSubstrateObjectConstant; +import com.oracle.svm.hosted.analysis.Inflation; +import com.oracle.svm.hosted.prophet.model.GraphQLCall; +import com.oracle.svm.hosted.prophet.model.RESTParameter; +import jdk.vm.ci.meta.PrimitiveConstant; +import org.graalvm.compiler.graph.Node; +import org.graalvm.compiler.graph.NodeInputList; +import org.graalvm.compiler.graph.iterators.NodeIterable; +import org.graalvm.compiler.nodes.CallTargetNode; +import org.graalvm.compiler.nodes.ConstantNode; +import org.graalvm.compiler.nodes.Invoke; +import org.graalvm.compiler.nodes.PiNode; +import org.graalvm.compiler.nodes.StructuredGraph; +import org.graalvm.compiler.nodes.ValueNode; +import org.graalvm.compiler.nodes.java.LoadFieldNode; + +import java.lang.reflect.Method; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import static com.oracle.svm.hosted.prophet.WebsocketCallExtraction.extractHostedClass; + +public class GraphQLCallExtraction { + + /* + NOTE: + 'msRoot' can be obtained in Utils or RAD + 'source' can be obtained in RAD repo in the RadSourceService file in generateRestEntityContext method where getSourceFiles is + */ + private final static String GraphQLClient = "GraphQlClient"; + private final static String RetrieveMethod = "retrieve"; + private final static String RetrieveSyncMethod = "retrieveSync"; + private final static String VariableMethod = "variable"; + private final static String ToEntityMethod = "toEntity"; + private final static String ToEntityListMethod = "toEntityList"; + private final static String DocumentMethod = "document"; + + private static Set graphqlCalls = new HashSet<>(); + + public static Set extractClassRestCalls(Class clazz, AnalysisMetaAccess metaAccess, Inflation bb, Map propMap, String msName) { + AnalysisType analysisType = metaAccess.lookupJavaType(clazz); + try { + for (AnalysisMethod method : ((AnalysisMethod[]) analysisType.getDeclaredMethods())) { + if (method.isAbstract()) { + continue; + } + try { + + String URI = ""; + String RETURN_TYPE = null; + Boolean callIsCollection = false; + String PARENT_METHOD = ""; + String param = ""; + String document = ""; + + StructuredGraph decodedGraph = ReachabilityAnalysisMethod.getDecodedGraph(bb, method); + for (Node node : decodedGraph.getNodes()) { + if (node instanceof Invoke) { + Invoke invoke = (Invoke) node; + AnalysisMethod targetMethod = ((AnalysisMethod) invoke.getTargetMethod()); + + String qualifiedName = targetMethod.getQualifiedName(); + if (qualifiedName.contains(GraphQLClient)) { + if (qualifiedName.contains(RetrieveMethod) || qualifiedName.contains(RetrieveSyncMethod)) { + + PARENT_METHOD = cleanParentMethod(method.getQualifiedName()); + CallTargetNode callTargetNode = invoke.callTarget(); + NodeInputList arguments = callTargetNode.arguments(); + + for (ValueNode v : arguments) { + if (v instanceof Invoke) { + URI += extractURI(((Invoke) v).callTarget(), propMap); + } else if (v instanceof ConstantNode && !v.isNullConstant() && !v.isIllegalConstant()) { + ConstantNode cn = (ConstantNode) v; + + DirectSubstrateObjectConstant dsoc = (DirectSubstrateObjectConstant) cn.getValue(); + URI += dsoc.getObject().toString(); + } + } + + } + + if (qualifiedName.contains(VariableMethod)) { + + CallTargetNode callTargetNode = invoke.callTarget(); + NodeInputList arguments = callTargetNode.arguments(); + + for (ValueNode v : arguments) { + if (v instanceof Invoke) { + param = extractURI(((Invoke) v).callTarget(), propMap); + } else if (v instanceof ConstantNode && !v.isNullConstant() && !v.isIllegalConstant()) { + ConstantNode cn = (ConstantNode) v; + + DirectSubstrateObjectConstant dsoc = (DirectSubstrateObjectConstant) cn.getValue(); + param = dsoc.getObject().toString(); + } + } + } + + if (qualifiedName.contains(DocumentMethod)) { + + CallTargetNode callTargetNode = invoke.callTarget(); + NodeInputList arguments = callTargetNode.arguments(); + + for (ValueNode v : arguments) { + if (v instanceof Invoke) { + document = extractURI(((Invoke) v).callTarget(), propMap); + } else if (v instanceof ConstantNode && !v.isNullConstant() && !v.isIllegalConstant()) { + ConstantNode cn = (ConstantNode) v; + + DirectSubstrateObjectConstant dsoc = (DirectSubstrateObjectConstant) cn.getValue(); + document = dsoc.getObject().toString(); + } + } + + document = escapeForCSV(document); + } + + if (qualifiedName.contains(ToEntityMethod) || qualifiedName.contains(ToEntityListMethod)) { + + CallTargetNode callTargetNode = invoke.callTarget(); + NodeInputList arguments = callTargetNode.arguments(); + + for (ValueNode v : arguments) { + if (v instanceof ConstantNode && !v.isNullConstant() && !v.isIllegalConstant()) { + ConstantNode cn = (ConstantNode) v; + Object payloadObject = cn.getValue(); + + if (payloadObject instanceof DirectSubstrateObjectConstant) { + DirectSubstrateObjectConstant dsoc = (DirectSubstrateObjectConstant) payloadObject; + Object possibleClass = dsoc.getObject(); + if (possibleClass instanceof com.oracle.svm.core.hub.DynamicHub) { + Class actualClass = extractHostedClass(possibleClass); + RETURN_TYPE = actualClass.getName(); // full path + } + } + } + } + } + } + } + } + if (URI != null && !URI.isEmpty()) { + graphqlCalls.add(new GraphQLCall(PARENT_METHOD, RETURN_TYPE, URI, callIsCollection, + clazz.getCanonicalName(), msName, param, document)); + } + } catch (Exception | LinkageError ex) { + ex.printStackTrace(); + } + } + } catch (Exception | LinkageError ex) { + ex.printStackTrace(); + } + return graphqlCalls; + } + + // Escapes double quotes and wraps the string in quotes for CSV safety + private static String escapeForCSV(String input) { + if (input == null || input.isEmpty()) return ""; // Return empty string if input is null or empty + + return input.replace("\"", "").replace("\n", ""); + } + + + private static RESTParameter getParamDetails(CallTargetNode node, String URI) { + + RESTParameter param = new RESTParameter(false, false); + //check if URI has slashes then it has path parameters + //check for slashes at end of string + int count = 0; + boolean slashFound = false; + for (int i = 0; i < URI.length(); i++) { + char c = URI.charAt(i); + if (i == URI.length() - 1 && c == '/') { + count++; + } else if (c == '/' && URI.charAt(i + 1) == '/') { + count++; + } + } + param.setParamCount(count); + if (count > 0) { + param.setIsPath(true); + } + param = setIfBodyAndType(param, node); + + return param; + } + + //assumes there is only one HTTP_ENTITY object in each REST call method + private static RESTParameter setIfBodyAndType(RESTParameter param, CallTargetNode node) { + + for (ValueNode arg : node.arguments()) { + + if (arg instanceof PiNode) { + + for (Node inputNode : ((PiNode) arg).inputs()) { + + if (inputNode instanceof Invoke) { + param = setIfBodyAndType(param, ((Invoke) inputNode).callTarget()); + + } + } + } else if (arg instanceof Invoke) { + +// param = handleIfInvokeInRESTParam(param, arg); + } else { + + } + } + return param; + } + + private static String cleanReturnType(String returnType) { + String parsedType = null; + if (returnType == null || returnType.equals("null")) { + return parsedType; + } + //remove 'class [L' example: 'class [Ljava.lang.Object]' -> 'java.lang.Object' + if (isCollection(returnType)) { + parsedType = returnType.substring(8); + } + //remove 'class ' example: 'class [Ljava.lang.Object]' -> '[Ljava.lang.Object' + else { + parsedType = returnType.substring(6); + } + return parsedType; + } + + private static boolean isCollection(String returnType) { + if (returnType == null || returnType.equals("null")) { + return false; + } + //graal api indicates collections in return type with "class [L" before the type name + return returnType.startsWith("class [L"); + } + + private static String extractURI(CallTargetNode node, Map propMap) { + // System.out.println("NODE CALL TARGET: " + node); + // System.out.println("NODE CALL TARGET ARGS: " + node.arguments()); + String uriPortion = ""; + + /* + * Loop over the arguments in the call target node + * if the node in the argument is an Invoke, call its target + * else if node is a loadfieldnode, go over annotations and get 'value' annotation + * get value based off prop map + */ + for (ValueNode arg : node.arguments()) { + NodeIterable inputsList = arg.inputs(); + if (arg instanceof LoadFieldNode) { + // System.out.println("arg is a LOAD_FIELD_NODE, arg = " + arg); + LoadFieldNode loadfieldNode = (LoadFieldNode) arg; + AnalysisField field = (AnalysisField) loadfieldNode.field(); + + for (java.lang.annotation.Annotation annotation : field.getWrapped().getAnnotations()) { + if (annotation.annotationType().getName().contains("Value")) { + // System.out.println("Load field with value annotation"); + // System.out.println("methods = " + annotation.annotationType().getMethods()); + try { + Method valueMethod = annotation.annotationType().getMethod("value"); + valueMethod.setAccessible(true); + String res = ""; + if (propMap != null) { + res = tryResolve(((String) valueMethod.invoke(annotation)), propMap); + } + uriPortion = uriPortion + res; + } catch (Exception ex) { + System.err.println("ERROR = " + ex); + } + } + } + + } else if (arg instanceof PiNode) { + // System.out.println(arg + " is a PiNode"); + // System.out.println("pi node inputs: " + ((PiNode)arg).inputs()); + for (Node inputNode : ((PiNode) arg).inputs()) { + if (inputNode instanceof Invoke) { + // System.out.println(inputNode + " is Invoke"); + uriPortion = uriPortion + extractURI(((Invoke) inputNode).callTarget(), propMap); + } + } + } else if (arg instanceof ConstantNode) { + ConstantNode cn = (ConstantNode) arg; + //PrimitiveConstants can not be converted to DirectSubstrateObjectConstant + if (!(cn.getValue() instanceof PrimitiveConstant)) { + DirectSubstrateObjectConstant dsoc = (DirectSubstrateObjectConstant) cn.getValue(); + uriPortion = uriPortion + dsoc.getObject().toString(); + } + + } else if (arg instanceof Invoke) { + // System.out.println("arg = " + arg + " && is an instance of invoke"); + uriPortion = uriPortion + extractURI(((Invoke) arg).callTarget(), propMap); + } else { + for (Node n : inputsList) { + if (n instanceof Invoke) { + ; + uriPortion = uriPortion + extractURI(((Invoke) n).callTarget(), propMap); + } + } + } + + } + return uriPortion; + } + + /** + * extract the method the rest call is being in + * + * @param input the method's qualified name + * @return the method the call is being made in + */ + private static String cleanParentMethod(String input) { + String parentMethod = null; + + parentMethod = input.substring(0, input.indexOf("(")); + return parentMethod; + } + + //TO-DO: find a safer way to cast Map value + @SuppressWarnings("unchecked") + private static String tryResolve(String expr, Map propMap) { + + String mergedKey = expr.substring(2, expr.length() - 1); + String[] path = mergedKey.split("\\."); + Map curr = propMap; + for (int i = 0; i < path.length; i++) { + String key = path[i]; + Object value = curr.get(key); + if (value == null) { + return null; + } + if (value instanceof String && i == path.length - 1) { + return ((String) value); + } + if (value instanceof Map) { + try { + curr = ((Map) value); + } catch (ClassCastException ex) { + ex.printStackTrace(); + } + } + } + return null; + } + +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/GraphQLEndpointExtraction.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/GraphQLEndpointExtraction.java new file mode 100644 index 000000000000..4241ed3b61a2 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/GraphQLEndpointExtraction.java @@ -0,0 +1,137 @@ +package com.oracle.svm.hosted.prophet; + +import com.oracle.graal.pointsto.meta.AnalysisMetaAccess; +import com.oracle.graal.pointsto.meta.AnalysisMethod; +import com.oracle.graal.pointsto.meta.AnalysisType; +import com.oracle.svm.hosted.analysis.Inflation; +import com.oracle.svm.hosted.prophet.model.Endpoint; +import com.oracle.svm.hosted.prophet.model.GraphQLEndpoint; +import jdk.vm.ci.meta.ResolvedJavaMethod.Parameter; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +public class GraphQLEndpointExtraction { + + private final static String QUERY_MAPPING = "org.springframework.graphql.data.method.annotation.QueryMapping"; + private final static String MUTATION_MAPPING = "org.springframework.graphql.data.method.annotation.MutationMapping"; + + // annotations for controller to get endpoints + private static final Set controllerAnnotationNames = new HashSet<>(Arrays.asList("QueryMapping", "MutationMapping")); + + public static Set extractEndpoints(Class clazz, AnalysisMetaAccess metaAccess, Inflation bb, String msName) { + AnalysisType analysisType = metaAccess.lookupJavaType(clazz); + Set endpoints = new HashSet(); + try { + + for (AnalysisMethod method : ((AnalysisMethod[]) analysisType.getDeclaredMethods())) { + try { + // What I will need to extract: String httpMethod, String parentMethod, String + // arguments, String returnType + Annotation[] annotations = method.getWrapped().getAnnotations(); + for (Annotation annotation : annotations) { + + ArrayList parameterAnnotationsList = new ArrayList<>(); + String httpMethod = null, parentMethod = null, returnTypeResult = null, path = ""; + boolean returnTypeCollection = false, isEndpoint = false; + if (controllerAnnotationNames.contains(annotation.annotationType().getSimpleName())) { + isEndpoint = true; + // Code to get the parentMethod attribute: + // following the rad-source format for the parentMethod JSON need to + // parse before the first parenthesis + parentMethod = method.getQualifiedName().substring(0, method.getQualifiedName().indexOf("(")); + path = method.getName(); // Save parentMethod as path + if (annotation.annotationType().getName().startsWith(QUERY_MAPPING)) { + httpMethod = "QUERY"; + } else if (annotation.annotationType().getName().startsWith(MUTATION_MAPPING)) { + httpMethod = "MUTATION"; + } + + parameterAnnotationsList = extractArguments(method); + returnTypeResult = extractReturnType(method); + if (returnTypeResult.startsWith("[L") && isCollection(returnTypeResult)) { + returnTypeCollection = true; + returnTypeResult = returnTypeResult.substring(2); + } else { + returnTypeCollection = isCollection(returnTypeResult); + } + // Special case for request mapping + } + + if (isEndpoint) { + endpoints.add(new GraphQLEndpoint(httpMethod, parentMethod, parameterAnnotationsList, returnTypeResult, path, returnTypeCollection, clazz.getCanonicalName(), msName)); + } + } + + } catch (Exception | LinkageError ex) { + ex.printStackTrace(); + } + } + } catch (Exception | LinkageError ex) { + ex.printStackTrace(); + } + + return endpoints; + } + + private static boolean isCollection(String returnType) { + if (returnType == null || returnType.equals("null")) { + return false; + } + // graal api indicates collections in return type with "class [L" OR + return returnType.startsWith("[L") || returnType.matches(".*[<].*[>]"); + } + + /** + * Method extracts and cleans the return type value of a controller method (based on a + * collection or object/primitive data type) + * + * @param method an AnalysisMethod + * @return the method's return type as a string value + */ + public static String extractReturnType(AnalysisMethod method) { + Method javaMethod = (Method) method.getJavaMethod(); + Type returnType = javaMethod.getGenericReturnType(); + + if (returnType.toString().length() == 5 && returnType.toString().substring(0, 5).equalsIgnoreCase("class")) { + return returnType.toString().substring(6); + } else { + return returnType.toString(); + } + + } + + public static ArrayList extractArguments(AnalysisMethod method) { + + // Code to get the argument attribute: + // Example: "arguments": "[@PathVariable Integer id]", + ArrayList parameterAnnotationsList = new ArrayList<>(); + Parameter[] params = method.getParameters(); + Annotation[][] annotations1 = method.getParameterAnnotations(); + + for (int i = 0; i < params.length; i++) { + Annotation[] annotations2 = annotations1[i]; + // Parameter Annotations (e.g., @PathVariable) are optional, thus can be empty (null) + String parameterAnnotation = ""; + for (int j = 0; j < annotations2.length; j++) { + Annotation annotation3 = annotations2[j]; + parameterAnnotation += "@" + annotation3.annotationType().getSimpleName(); + } + String parameterType = params[i].getParameterizedType().toString(); + String parameterName = params[i].getName(); + String simpleParameterType = parameterType.substring(parameterType.lastIndexOf(".") + 1); + String fullParameter = parameterAnnotation + " " + simpleParameterType + " " + parameterName; + parameterAnnotationsList.add(fullParameter); + } + + return parameterAnnotationsList; + + } + +} \ No newline at end of file diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/ProphetPlugin.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/ProphetPlugin.java index 709047a787c9..1cf70af2e3a9 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/ProphetPlugin.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/ProphetPlugin.java @@ -15,6 +15,8 @@ import java.util.Optional; import java.util.Set; +import com.oracle.svm.hosted.prophet.model.GraphQLCall; +import com.oracle.svm.hosted.prophet.model.GraphQLEndpoint; import com.oracle.svm.hosted.prophet.model.WebsocketConnection; import com.oracle.svm.hosted.prophet.model.WebsocketEndpoint; import com.oracle.svm.hosted.prophet.model.WebsocketMessageType; @@ -84,6 +86,12 @@ public static class Options { @Option(help = "Where to store the websocketEndpoints output")// public static final HostedOptionKey ProphetWebsocketEndpointsOutputFile = new HostedOptionKey<>(null); + @Option(help = "Where to store the graphqlCalls output")// + public static final HostedOptionKey ProphetGraphQLCallOutputFile = new HostedOptionKey<>(null); + + @Option(help = "Where to store the graphqlEndpoints output")// + public static final HostedOptionKey ProphetGraphQLEndpointOutputFile = new HostedOptionKey<>(null); + @Option(help = "Where to store the endpoint output")// public static final HostedOptionKey ProphetEndpointOutputFile = new HostedOptionKey<>(null); @@ -110,6 +118,9 @@ public static void run(ImageClassLoader loader, AnalysisUniverse aUniverse, Anal restDump.writeOutWebsocketConnections(module.getWebsocketConnections(), Options.ProphetWebsocketConnectionsOutputFile.getValue()); restDump.writeOutEndpoints(module.getEndpoints(), Options.ProphetEndpointOutputFile.getValue()); restDump.writeOutWebsocketEndpoints(module.getWebsocketEndpoints(), Options.ProphetWebsocketEndpointsOutputFile.getValue()); + restDump.writeOutGraphQLCalls(module.getGraphQLCalls(), Options.ProphetGraphQLCallOutputFile.getValue()); + restDump.writeOutGraphQLEndpoints(module.getGraphQLEndpoints(), Options.ProphetGraphQLEndpointOutputFile.getValue()); + dumpModule(module); logger.info("Final summary: " + module.shortSummary()); @@ -221,10 +232,12 @@ private Set linkWebsocketEndpointsAndMessageTypes(Set> classes) { var entities = new HashSet(); Set restCallList = new HashSet(); + Set graphQLCallList = new HashSet(); Set websocketConnectionsList = new HashSet(); Set websocketEndpointsList = new HashSet(); Set websocketMessageTypesList = new HashSet(); Set endpointList = new HashSet(); + Set graphQLEndpointList = new HashSet(); logger.info("Amount of classes = " + classes.size()); for (Class clazz : classes) { @@ -232,21 +245,26 @@ private Module processClasses(List> classes) { Optional ent = EntityExtraction.extractClassEntityCalls(clazz, metaAccess, bb); ent.ifPresent(entities::add); Set restCalls = RestCallExtraction.extractClassRestCalls(clazz, metaAccess, bb, this.propMap, Options.ProphetMicroserviceName.getValue()); + restCallList.addAll(restCalls); + Set GraphQLCalls = GraphQLCallExtraction.extractClassRestCalls(clazz, metaAccess, bb, this.propMap, Options.ProphetMicroserviceName.getValue()); + graphQLCallList.addAll(GraphQLCalls); Set websocketConnection = WebsocketCallExtraction.extractClassWebsocketConnection(clazz, metaAccess, bb, this.propMap, Options.ProphetMicroserviceName.getValue()); Set websocketEndpoints = WebsocketCallExtraction.extractClassWebsocketEndpoints(clazz, metaAccess, bb, this.propMap, Options.ProphetMicroserviceName.getValue()); Set websocketMessageTypes = WebsocketCallExtraction.extractClassWebsocketMessageTypes(clazz, metaAccess, bb, this.propMap, Options.ProphetMicroserviceName.getValue()); - restCallList.addAll(restCalls); + websocketEndpointsList.addAll(websocketEndpoints); websocketMessageTypesList.addAll(websocketMessageTypes); websocketConnectionsList.addAll(websocketConnection); // ENDPOINT EXTRACTION HERE Set endpoints = EndpointExtraction.extractEndpoints(clazz, metaAccess, bb, Options.ProphetMicroserviceName.getValue()); + Set graphQLEndpoints = GraphQLEndpointExtraction.extractEndpoints(clazz, metaAccess, bb, Options.ProphetMicroserviceName.getValue()); + graphQLEndpointList.addAll(graphQLEndpoints); endpointList.addAll(endpoints); } websocketEndpointsList.addAll(linkWebsocketEndpointsAndMessageTypes(websocketEndpointsList, websocketMessageTypesList)); - return new Module(new Name(msName), entities, restCallList, websocketConnectionsList, websocketEndpointsList, endpointList); + return new Module(new Name(msName), entities, restCallList, websocketConnectionsList, websocketEndpointsList, endpointList, graphQLCallList, graphQLEndpointList); } private List> filterRelevantClasses() { diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/RestDump.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/RestDump.java index 7bc419d59eef..0549630ff956 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/RestDump.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/RestDump.java @@ -1,5 +1,7 @@ package com.oracle.svm.hosted.prophet; import com.oracle.svm.hosted.prophet.model.Endpoint; +import com.oracle.svm.hosted.prophet.model.GraphQLCall; +import com.oracle.svm.hosted.prophet.model.GraphQLEndpoint; import com.oracle.svm.hosted.prophet.model.RestCall; import com.oracle.svm.hosted.prophet.model.WebsocketConnection; import com.oracle.svm.hosted.prophet.model.WebsocketEndpoint; @@ -73,4 +75,30 @@ public void writeOutEndpoints(Set endpoints, String outputFile){ ex.printStackTrace(); } } + + public void writeOutGraphQLEndpoints(Set endpoints, String outputFile){ + if (outputFile == null){ + throw new RuntimeException("ProphetGraphQLEndpointOutputFile option was not provided"); + } + try (BufferedWriter writer = new BufferedWriter(new FileWriter(outputFile))){ + for (GraphQLEndpoint ep : endpoints){ + writer.write(ep.toString() + "\n"); + } + }catch(IOException ex){ + ex.printStackTrace(); + } + } + + public void writeOutGraphQLCalls(Set graphQLCalls, String outputFile) { + if (outputFile == null) { + throw new RuntimeException("ProphetGraphQLCallOutputFile option was not provided"); + } + try (BufferedWriter writer = new BufferedWriter(new FileWriter(outputFile))) { + for (GraphQLCall rc : graphQLCalls) { + writer.write(rc.toString() + "\n"); + } + } catch (IOException ex) { + ex.printStackTrace(); + } + } } diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/GraphQLCall.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/GraphQLCall.java new file mode 100644 index 000000000000..0dca0b797c9e --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/GraphQLCall.java @@ -0,0 +1,93 @@ +package com.oracle.svm.hosted.prophet.model; + +public class GraphQLCall { + + private String parentMethod; + private String returnType; + private String uri; + private boolean isCollection; + private String graphqlCallInClassName; + private String msName; + private String param; + private String document; + + public GraphQLCall(String parentMethod, + String returnType, String uri, Boolean isCollection, + String graphqlCallInClassName, String msName, String param, String document) { + + this.parentMethod = parentMethod; + this.returnType = returnType; + this.uri = uri; + this.isCollection = isCollection; + this.graphqlCallInClassName = graphqlCallInClassName; + this.msName = msName; + this.param = param; + this.document = document; + } + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(this.msName).append(",").append(graphqlCallInClassName).append(",").append(parentMethod).append(",").append(uri) + .append(",").append(returnType).append(",") + .append(param).append(",").append(isCollection).append(",").append(document); + return sb.toString(); + } + // Getter methods + public String getParam(){ + return this.param; + } + public String getMsName() { + return this.msName; + } + public String getgraphqlCallInClassName() { + return this.graphqlCallInClassName; + } + + public String getParentMethod() { + return parentMethod; + } + + public String getReturnType() { + return returnType; + } + + public String getUri() { + return uri; + } + + public String getDocument() { + return document; + } + + public boolean isCollection() { + return isCollection; + } + + // Setter methods + public void setMsName(String msName) { + this.msName = msName; + } + + public void setGraphqlCallInClassName(String className) { + this.graphqlCallInClassName = className; + } + public void setParentMethod(String parentMethod) { + this.parentMethod = parentMethod; + } + + public void setReturnType(String returnType) { + this.returnType = returnType; + } + + public void setUri(String uri) { + this.uri = uri; + } + + public void setDocument(String document) { + this.document = document; + } + + public void setCollection(boolean isCollection) { + this.isCollection = isCollection; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/GraphQLEndpoint.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/GraphQLEndpoint.java new file mode 100644 index 000000000000..4600a4834d28 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/GraphQLEndpoint.java @@ -0,0 +1,110 @@ +package com.oracle.svm.hosted.prophet.model; + +import java.util.List; + +public class GraphQLEndpoint { + + private String graphQLMethod; + private String parentMethod; + private List arguments; + private String returnType; + private String path; + private boolean isCollection; + private String endpointInClassName; + private String msName; + + public GraphQLEndpoint(String graphQLMethod, String parentMethod, List args, + String returnType, String path, Boolean isCollection, + String endpointInClassName, String msName) { + + this.graphQLMethod = graphQLMethod; + this.parentMethod = parentMethod; + this.arguments = args; + this.returnType = returnType; + this.path = path; + this.isCollection = isCollection; + this.endpointInClassName = endpointInClassName; + this.msName = msName; + + } + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(this.msName).append(",").append(endpointInClassName).append(",").append(parentMethod).append(",") + .append(toStringModified(arguments)).append(",").append(path).append(",").append(graphQLMethod) + .append(",").append(returnType).append(",").append(isCollection); + return sb.toString(); + } + private String toStringModified(List args){ + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < args.size(); i++){ + String str = args.get(i); + sb.append(str.replaceAll(" ", "_")); + if (i < args.size() - 1){ + sb.append("&"); + } + } + + return sb.toString(); + } + // Getter methods + public String getGraphQLMethod() { + return graphQLMethod; + } + public String getMsName() { + return this.msName; + } + public String getEndpointInClassName() { + return this.endpointInClassName; + } + public String getParentMethod() { + return parentMethod; + } + + public List getArguments() { + return arguments; + } + + public String getReturnType() { + return returnType; + } + + public String getPath() { + return path; + } + + public boolean isCollection() { + return isCollection; + } + + // Setter methods + public void setGraphQLMethod(String httpMethod) { + this.graphQLMethod = httpMethod; + } + public void setMsName(String msName) { + this.msName = msName; + } + public void setEndpointInClassName(String className) { + this.endpointInClassName = className; + } + public void setParentMethod(String parentMethod) { + this.parentMethod = parentMethod; + } + + public void setArguments(List arguments) { + this.arguments = arguments; + } + + public void setReturnType(String returnType) { + this.returnType = returnType; + } + + public void setPath(String path) { + this.path = path; + } + + public void setCollection(boolean isCollection) { + this.isCollection = isCollection; + } +} diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/Module.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/Module.java index b9794bfbbfa9..22bbb81cf944 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/Module.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/Module.java @@ -11,14 +11,18 @@ public class Module { private Set websocketConnections; private Set websocketEndpoints; private Set endpoints; + private Set graphQLCalls; + private Set graphQLEndpoints; - public Module(Name name, Set entities, Set restCalls, Set websocketConnections, Set websocketEndpoints, Set endpoints) { + public Module(Name name, Set entities, Set restCalls, Set websocketConnections, Set websocketEndpoints, Set endpoints, Set graphQLCalls, Set graphQLEndpoints) { this.name = name; this.entities = entities; this.restCalls = restCalls; this.websocketConnections = websocketConnections; this.websocketEndpoints = websocketEndpoints; this.endpoints = endpoints; + this.graphQLCalls = graphQLCalls; + this.graphQLEndpoints = graphQLEndpoints; } public String shortSummary() { @@ -34,6 +38,10 @@ public String shortSummary() { websocketEndpoints.size() + ",endpoints=" + endpoints.size() + + ",graphQLCalls=" + + graphQLCalls.size() + + ",graphQLEndpoints=" + + graphQLEndpoints.size() + ')'; } @@ -41,8 +49,7 @@ public String shortSummary() { public String toString() { StringBuilder sb = new StringBuilder(); sb.append("MODULE NAME = ").append(name).append("\n").append("\nENTITIES = \n").append(setToString(entities)) - .append("\nREST_CALLS = \n").append(setToString(restCalls)).append("\nWEBSOCKET_CONNECTIONS = \n").append(setToString(websocketConnections)).append("\nWEBSOCKET_ENDPOINTS = \n").append(setToString(websocketEndpoints)).append("\nENDPOINTS = \n").append(setToString(endpoints)) - .append('\n'); + .append("\nREST_CALLS = \n").append(setToString(restCalls)).append("\nWEBSOCKET_CONNECTIONS = \n").append(setToString(websocketConnections)).append("\nWEBSOCKET_ENDPOINTS = \n").append(setToString(websocketEndpoints)).append("\nENDPOINTS = \n").append(setToString(endpoints)).append("\nGRAPHQL_CALLS = \n").append(setToString(graphQLCalls)).append("\nGRAPHQL_ENDPOINTS = \n").append(setToString(graphQLEndpoints)).append('\n'); return sb.toString(); } @@ -87,6 +94,14 @@ public Set getEndpoints() { return endpoints; } + public Set getGraphQLCalls() { + return graphQLCalls; + } + + public Set getGraphQLEndpoints() { + return graphQLEndpoints; + } + // Setter methods public void setRestCalls(Set restCalls) { this.restCalls = restCalls; From 3a6a0f8d7b61cdbc90c0f06b036d282d33e975a0 Mon Sep 17 00:00:00 2001 From: macbook Date: Mon, 31 Mar 2025 01:10:27 +0200 Subject: [PATCH 4/6] optimize the websocket stomp detection --- .../svm/hosted/prophet/ProphetPlugin.java | 76 ++++++------- .../prophet/WebsocketCallExtraction.java | 102 ++++++++++++++++-- .../prophet/model/WebsocketEndpoint.java | 4 + .../prophet/model/WebsocketMessageType.java | 9 -- 4 files changed, 130 insertions(+), 61 deletions(-) diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/ProphetPlugin.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/ProphetPlugin.java index 1cf70af2e3a9..f72c55c46d4c 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/ProphetPlugin.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/ProphetPlugin.java @@ -33,6 +33,8 @@ import com.oracle.svm.hosted.prophet.model.Name; import com.oracle.svm.hosted.prophet.model.RestCall; +import static com.oracle.svm.hosted.prophet.WebsocketCallExtraction.extractClassStompMessageTypes; + public class ProphetPlugin { private final ImageClassLoader loader; @@ -175,58 +177,47 @@ private Module doRun() { } private Set linkWebsocketEndpointsAndMessageTypes(Set websocketEndpointsList, Set websocketMessageTypesList) { - Set websocketEndpoints = new HashSet(); for (WebsocketEndpoint endpoint : websocketEndpointsList) { - if (websocketMessageTypesList.isEmpty()) { - WebsocketEndpoint connection = new WebsocketEndpoint( - endpoint.getParentMethod(), - endpoint.getReturnType(), - endpoint.getUri(), - endpoint.isCollection(), - endpoint.getConnectionInClassName(), - endpoint.getMsName(), - endpoint.getParam(), - endpoint.getWsHandler() - ); - websocketEndpoints.add(connection); - } else { + if (!websocketMessageTypesList.isEmpty()) { + + boolean handlerMatched = false; + List returnTypes = new ArrayList<>(); + for (WebsocketMessageType messageType : websocketMessageTypesList) { + returnTypes.add(messageType.getWsDataType()); + if (endpoint.getWsHandler().equals(messageType.getWsHandler())) { + endpoint.setReturnType(messageType.getWsDataType()); + handlerMatched = true; + break; + } + } + if (!handlerMatched) { + endpoint.setReturnType(returnTypes.toString()); + } + } + } + return websocketEndpointsList; + } + + private Set linkWebsocketConnectionsAndMessageTypes(Set websocketConnectionsList, Set websocketMessageTypesList) { + for (WebsocketConnection connection : websocketConnectionsList) { + if (!websocketMessageTypesList.isEmpty()) { boolean handlerMatched = false; + List returnTypes = new ArrayList<>(); for (WebsocketMessageType messageType : websocketMessageTypesList) { - String endpointHandler = endpoint.getWsHandler(); - String messageTypeHandler = messageType.getWsHandler(); - if (endpointHandler.equals(messageTypeHandler)) { - WebsocketEndpoint connection = new WebsocketEndpoint( - endpoint.getParentMethod() != null ? endpoint.getParentMethod() : messageType.getParentMethod(), - messageType.getWsDataType(), - endpoint.getUri(), - endpoint.isCollection(), - endpoint.getConnectionInClassName() != null ? endpoint.getConnectionInClassName() : messageType.getConnectionInClassName(), - endpoint.getMsName() != null ? endpoint.getMsName() : messageType.getMsName(), - endpoint.getParam() != null ? endpoint.getParam() : messageType.getParam(), - endpoint.getWsHandler() - ); - websocketEndpoints.add(connection); + returnTypes.add(messageType.getWsDataType()); + if (connection.getWsHandler().equals(messageType.getWsHandler())) { + connection.setReturnType(messageType.getWsDataType()); handlerMatched = true; break; } } if (!handlerMatched) { - WebsocketEndpoint connection = new WebsocketEndpoint( - endpoint.getParentMethod(), - endpoint.getReturnType(), - endpoint.getUri(), - endpoint.isCollection(), - endpoint.getConnectionInClassName(), - endpoint.getMsName(), - endpoint.getParam(), - endpoint.getWsHandler() - ); - websocketEndpoints.add(connection); + connection.setReturnType(returnTypes.toString()); } } } - return websocketEndpoints; + return websocketConnectionsList; } private Module processClasses(List> classes) { @@ -255,6 +246,7 @@ private Module processClasses(List> classes) { websocketEndpointsList.addAll(websocketEndpoints); websocketMessageTypesList.addAll(websocketMessageTypes); websocketConnectionsList.addAll(websocketConnection); + // ENDPOINT EXTRACTION HERE Set endpoints = EndpointExtraction.extractEndpoints(clazz, metaAccess, bb, Options.ProphetMicroserviceName.getValue()); Set graphQLEndpoints = GraphQLEndpointExtraction.extractEndpoints(clazz, metaAccess, bb, Options.ProphetMicroserviceName.getValue()); @@ -262,7 +254,9 @@ private Module processClasses(List> classes) { endpointList.addAll(endpoints); } - websocketEndpointsList.addAll(linkWebsocketEndpointsAndMessageTypes(websocketEndpointsList, websocketMessageTypesList)); + websocketEndpointsList = linkWebsocketEndpointsAndMessageTypes(websocketEndpointsList, websocketMessageTypesList); + websocketConnectionsList = linkWebsocketConnectionsAndMessageTypes(websocketConnectionsList, websocketMessageTypesList); + return new Module(new Name(msName), entities, restCallList, websocketConnectionsList, websocketEndpointsList, endpointList, graphQLCallList, graphQLEndpointList); } diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/WebsocketCallExtraction.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/WebsocketCallExtraction.java index 019a65845dfd..e34232fb613b 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/WebsocketCallExtraction.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/WebsocketCallExtraction.java @@ -24,6 +24,7 @@ import org.graalvm.compiler.nodes.NodeView; import org.graalvm.compiler.nodes.ParameterNode; import org.graalvm.compiler.nodes.PiNode; +import org.graalvm.compiler.nodes.ReturnNode; import org.graalvm.compiler.nodes.StructuredGraph; import org.graalvm.compiler.nodes.ValueNode; import org.graalvm.compiler.nodes.java.LoadFieldNode; @@ -338,6 +339,7 @@ public static Set extractClassWebsocketMessageTypes(Class< if (method.isAbstract()) { continue; } + try { StructuredGraph decodedGraph = ReachabilityAnalysisMethod.getDecodedGraph(bb, method); @@ -347,6 +349,41 @@ public static Set extractClassWebsocketMessageTypes(Class< StringBuilder uriBuilder = new StringBuilder(); WebsocketParameter param = null; + if (method.getQualifiedName().contains(".getPayloadType")) { + + Class parentClass = clazz.getSuperclass(); + + if (clazz.getName().equals("org.springframework.messaging.simp.stomp.StompSessionHandlerAdapter") || (parentClass != null && parentClass.getName().equals("org.springframework.messaging.simp.stomp.StompSessionHandlerAdapter"))) { + for (Node node : decodedGraph.getNodes()) { + + if (node instanceof ReturnNode) { + ReturnNode returnNode = (ReturnNode) node; + ValueNode returnVal = returnNode.result(); + if (returnVal instanceof ConstantNode) { + ConstantNode constantNode = (ConstantNode) returnVal; + Object payloadObject = constantNode.getValue(); + + if (payloadObject instanceof DirectSubstrateObjectConstant) { + DirectSubstrateObjectConstant dsoc = (DirectSubstrateObjectConstant) payloadObject; + Object actualMessage = dsoc.getObject(); + + if (actualMessage instanceof com.oracle.svm.core.hub.DynamicHub) { + Class actualClass = extractHostedClass(actualMessage); + wsDataType = actualClass.getSimpleName(); + System.out.println("Actual message type: " + wsDataType); + } + } + } + + } + } + } + + System.out.println("Detected StompSessionHandler.getPayloadType call"); + } + + + // Loop through all nodes in the graph for (Node node : decodedGraph.getNodes()) { // Detect URI.create invocations @@ -354,16 +391,6 @@ public static Set extractClassWebsocketMessageTypes(Class< Invoke invoke = (Invoke) node; AnalysisMethod targetMethod = (AnalysisMethod) invoke.getTargetMethod(); - if (targetMethod.getQualifiedName().contains("StompSessionHandlerAdapter.getPayloadType")) { - // Retrieve the return type - AnalysisType retType = (AnalysisType) targetMethod.getSignature().getReturnType(null); - if (retType != null) { - wsDataType = retType.getName(); - // Extract the handler name from the declaring class - wsHandler = targetMethod.getDeclaringClass().getName(); - } - } - if (targetMethod.getQualifiedName().contains("AbstractWebSocketMessage.getPayload")) { System.out.println("Detected WebSocket getPayload() call"); @@ -424,7 +451,60 @@ public static Set extractClassWebsocketMessageTypes(Class< } - private static WebsocketParameter setIfBodyAndType(WebsocketParameter param, CallTargetNode node) { + public static Set extractClassStompMessageTypes(Class clazz, AnalysisMetaAccess metaAccess, Inflation bb, Map propMap, String msName, String handlerClass) { + Set websocketMessageTypes = new HashSet<>(); + AnalysisType analysisType = metaAccess.lookupJavaType(clazz); + + try { + for (AnalysisMethod method : ((AnalysisMethod[]) analysisType.getDeclaredMethods())) { + if (method.isAbstract()) { + continue; + } + + try { + StructuredGraph decodedGraph = ReachabilityAnalysisMethod.getDecodedGraph(bb, method); + + String substring = handlerClass + ".getPayloadType"; + + if (method.getQualifiedName().contains(substring)) { + + + for (Node node : decodedGraph.getNodes()) { + + if (node instanceof ReturnNode) { + ReturnNode returnNode = (ReturnNode) node; + ValueNode returnVal = returnNode.result(); + if (returnVal instanceof ConstantNode) { + ConstantNode constantNode = (ConstantNode) returnVal; + Object payloadObject = constantNode.getValue(); + + if (payloadObject instanceof DirectSubstrateObjectConstant) { + DirectSubstrateObjectConstant dsoc = (DirectSubstrateObjectConstant) payloadObject; + Object actualMessage = dsoc.getObject(); + + if (actualMessage instanceof com.oracle.svm.core.hub.DynamicHub) { + Class actualClass = extractHostedClass(actualMessage); + websocketMessageTypes.add(actualClass.getSimpleName()); + } + } + } + + } + } + } + } catch (Exception | LinkageError ex) { + ex.printStackTrace(); + } + } + } catch (Exception | LinkageError ex) { + ex.printStackTrace(); + } + return websocketMessageTypes; + } + + + + private static WebsocketParameter setIfBodyAndType(WebsocketParameter param, CallTargetNode node) { for (ValueNode arg : node.arguments()) { if (arg instanceof PiNode) { for (Node inputNode : ((PiNode) arg).inputs()) { diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketEndpoint.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketEndpoint.java index 8e3b7169ef70..960b808540ad 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketEndpoint.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketEndpoint.java @@ -78,6 +78,10 @@ public void setWsHandler(String wsHandler) { this.wsHandler = wsHandler; } + public void setReturnType(String returnType) { + this.returnType = returnType; + } + public void setUri(String uri) { this.uri = uri; } diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketMessageType.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketMessageType.java index 6f77bc02bd44..44b58843872e 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketMessageType.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketMessageType.java @@ -6,7 +6,6 @@ public class WebsocketMessageType { private String msName; private String wsHandler; private String parentMethod; - private String returnType; private boolean isCollection; private String connectionInClassName; private WebsocketParameter param; @@ -51,10 +50,6 @@ public String getParentMethod() { return parentMethod; } - public String getReturnType() { - return returnType; - } - public boolean isCollection() { return isCollection; } @@ -84,10 +79,6 @@ public void setParentMethod(String parentMethod) { this.parentMethod = parentMethod; } - public void setReturnType(String returnType) { - this.returnType = returnType; - } - public void setCollection(boolean isCollection) { this.isCollection = isCollection; } From a346e1b0add9d6dcaf75c3ae31d946c1773deaf8 Mon Sep 17 00:00:00 2001 From: Vsevolod Pokhvalenko Date: Fri, 11 Apr 2025 08:47:46 +0200 Subject: [PATCH 5/6] optimize code, add comments --- .../hosted/prophet/GraphQLCallExtraction.java | 187 ++++++-------- .../prophet/GraphQLEndpointExtraction.java | 88 +++++-- .../svm/hosted/prophet/ProphetPlugin.java | 99 ++++++-- ...ava => WebsocketConnectionExtraction.java} | 240 +++++++++++------- 4 files changed, 380 insertions(+), 234 deletions(-) rename substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/{WebsocketCallExtraction.java => WebsocketConnectionExtraction.java} (77%) diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/GraphQLCallExtraction.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/GraphQLCallExtraction.java index bca17aa5b06f..6713f6196cbe 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/GraphQLCallExtraction.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/GraphQLCallExtraction.java @@ -8,7 +8,6 @@ import com.oracle.svm.core.meta.DirectSubstrateObjectConstant; import com.oracle.svm.hosted.analysis.Inflation; import com.oracle.svm.hosted.prophet.model.GraphQLCall; -import com.oracle.svm.hosted.prophet.model.RESTParameter; import jdk.vm.ci.meta.PrimitiveConstant; import org.graalvm.compiler.graph.Node; import org.graalvm.compiler.graph.NodeInputList; @@ -26,25 +25,45 @@ import java.util.Map; import java.util.Set; -import static com.oracle.svm.hosted.prophet.WebsocketCallExtraction.extractHostedClass; +import static com.oracle.svm.hosted.prophet.WebsocketConnectionExtraction.extractHostedClass; public class GraphQLCallExtraction { - /* - NOTE: - 'msRoot' can be obtained in Utils or RAD - 'source' can be obtained in RAD repo in the RadSourceService file in generateRestEntityContext method where getSourceFiles is - */ - private final static String GraphQLClient = "GraphQlClient"; - private final static String RetrieveMethod = "retrieve"; - private final static String RetrieveSyncMethod = "retrieveSync"; - private final static String VariableMethod = "variable"; - private final static String ToEntityMethod = "toEntity"; - private final static String ToEntityListMethod = "toEntityList"; - private final static String DocumentMethod = "document"; + // Constants representing key method and class names used in GraphQL call extraction. + // These are used to identify specific methods and operations in the analyzed code. + private final static String GraphQLClient = "GraphQlClient"; // Represents the GraphQL client class. + private final static String RetrieveMethod = "retrieve"; // Method for retrieving data asynchronously. + private final static String RetrieveSyncMethod = "retrieveSync"; // Method for retrieving data synchronously. + private final static String VariableMethod = "variable"; // Method for setting variables in GraphQL queries. + private final static String ToEntityMethod = "toEntity"; // Method for mapping the response to a single entity. + private final static String ToEntityListMethod = "toEntityList"; // Method for mapping the response to a list of entities. + private final static String DocumentMethod = "document"; // Method for specifying the GraphQL document or query. private static Set graphqlCalls = new HashSet<>(); + /** + * Extracts GraphQL call details from a given class by analyzing its methods and their associated + * graphs. This method identifies specific GraphQL-related invocations, such as `retrieve`, + * `variable`, `document`, and `toEntity`, to extract relevant information like URI, parameters, + * and return types. + * + * The method uses GraalVM's analysis tools to decode the method graphs and traverse their nodes + * to detect relevant invocations. Extracted data is stored in `GraphQLCall` objects and returned + * as a set. + * + * Key Features: + * - Detects `GraphQlClient` invocations to identify GraphQL operations. + * - Extracts URIs, parameters, and documents from method arguments. + * - Identifies return types by analyzing `toEntity` and `toEntityList` calls. + * - Handles nested invocations and resolves dynamic values using a property map (`propMap`). + * + * @param clazz The class to analyze for GraphQL call details. + * @param metaAccess The meta-access interface for type and method analysis. + * @param bb The inflation object used for decoding graphs. + * @param propMap A map of properties for resolving dynamic values (e.g., placeholders in URIs). + * @param msName The name of the microservice or module being analyzed. + * @return A set of `GraphQLCall` objects containing extracted GraphQL call details. + */ public static Set extractClassRestCalls(Class clazz, AnalysisMetaAccess metaAccess, Inflation bb, Map propMap, String msName) { AnalysisType analysisType = metaAccess.lookupJavaType(clazz); try { @@ -162,108 +181,47 @@ public static Set extractClassRestCalls(Class clazz, AnalysisMet return graphqlCalls; } - // Escapes double quotes and wraps the string in quotes for CSV safety + /** + * Escapes a string for CSV by removing double quotes and newline characters. + * If the input is null or empty, it returns an empty string. + * + * @param input The input string to be escaped. + * @return The escaped string with double quotes and newlines removed. + */ private static String escapeForCSV(String input) { - if (input == null || input.isEmpty()) return ""; // Return empty string if input is null or empty + if (input == null || input.isEmpty()) return ""; return input.replace("\"", "").replace("\n", ""); } - - private static RESTParameter getParamDetails(CallTargetNode node, String URI) { - - RESTParameter param = new RESTParameter(false, false); - //check if URI has slashes then it has path parameters - //check for slashes at end of string - int count = 0; - boolean slashFound = false; - for (int i = 0; i < URI.length(); i++) { - char c = URI.charAt(i); - if (i == URI.length() - 1 && c == '/') { - count++; - } else if (c == '/' && URI.charAt(i + 1) == '/') { - count++; - } - } - param.setParamCount(count); - if (count > 0) { - param.setIsPath(true); - } - param = setIfBodyAndType(param, node); - - return param; - } - - //assumes there is only one HTTP_ENTITY object in each REST call method - private static RESTParameter setIfBodyAndType(RESTParameter param, CallTargetNode node) { - - for (ValueNode arg : node.arguments()) { - - if (arg instanceof PiNode) { - - for (Node inputNode : ((PiNode) arg).inputs()) { - - if (inputNode instanceof Invoke) { - param = setIfBodyAndType(param, ((Invoke) inputNode).callTarget()); - - } - } - } else if (arg instanceof Invoke) { - -// param = handleIfInvokeInRESTParam(param, arg); - } else { - - } - } - return param; - } - - private static String cleanReturnType(String returnType) { - String parsedType = null; - if (returnType == null || returnType.equals("null")) { - return parsedType; - } - //remove 'class [L' example: 'class [Ljava.lang.Object]' -> 'java.lang.Object' - if (isCollection(returnType)) { - parsedType = returnType.substring(8); - } - //remove 'class ' example: 'class [Ljava.lang.Object]' -> '[Ljava.lang.Object' - else { - parsedType = returnType.substring(6); - } - return parsedType; - } - - private static boolean isCollection(String returnType) { - if (returnType == null || returnType.equals("null")) { - return false; - } - //graal api indicates collections in return type with "class [L" before the type name - return returnType.startsWith("class [L"); - } - + /** + * Extracts a URI portion from a `CallTargetNode` by analyzing its arguments and traversing + * through various node types such as `LoadFieldNode`, `PiNode`, `ConstantNode`, and others. + * The method resolves dynamic values using a property map (`propMap`) and recursively processes + * nested nodes to construct the full URI. + * + * Key Features: + * - Handles `LoadFieldNode` to extract annotations and resolve values using `propMap`. + * - Processes `PiNode` and its inputs recursively to extract URI components. + * - Handles `ConstantNode` to extract constant values. + * - Recursively processes `Invoke` nodes to extract nested URI portions. + * - Skips unsupported or null node types gracefully. + * + * @param node The `CallTargetNode` to analyze for URI extraction. + * @param propMap A map of properties for resolving dynamic values (e.g., placeholders in URIs). + * @return A string representing the extracted URI portion. + */ private static String extractURI(CallTargetNode node, Map propMap) { - // System.out.println("NODE CALL TARGET: " + node); - // System.out.println("NODE CALL TARGET ARGS: " + node.arguments()); String uriPortion = ""; - /* - * Loop over the arguments in the call target node - * if the node in the argument is an Invoke, call its target - * else if node is a loadfieldnode, go over annotations and get 'value' annotation - * get value based off prop map - */ for (ValueNode arg : node.arguments()) { NodeIterable inputsList = arg.inputs(); if (arg instanceof LoadFieldNode) { - // System.out.println("arg is a LOAD_FIELD_NODE, arg = " + arg); LoadFieldNode loadfieldNode = (LoadFieldNode) arg; AnalysisField field = (AnalysisField) loadfieldNode.field(); for (java.lang.annotation.Annotation annotation : field.getWrapped().getAnnotations()) { if (annotation.annotationType().getName().contains("Value")) { - // System.out.println("Load field with value annotation"); - // System.out.println("methods = " + annotation.annotationType().getMethods()); try { Method valueMethod = annotation.annotationType().getMethod("value"); valueMethod.setAccessible(true); @@ -279,24 +237,19 @@ private static String extractURI(CallTargetNode node, Map propMa } } else if (arg instanceof PiNode) { - // System.out.println(arg + " is a PiNode"); - // System.out.println("pi node inputs: " + ((PiNode)arg).inputs()); for (Node inputNode : ((PiNode) arg).inputs()) { if (inputNode instanceof Invoke) { - // System.out.println(inputNode + " is Invoke"); uriPortion = uriPortion + extractURI(((Invoke) inputNode).callTarget(), propMap); } } } else if (arg instanceof ConstantNode) { ConstantNode cn = (ConstantNode) arg; - //PrimitiveConstants can not be converted to DirectSubstrateObjectConstant if (!(cn.getValue() instanceof PrimitiveConstant)) { DirectSubstrateObjectConstant dsoc = (DirectSubstrateObjectConstant) cn.getValue(); uriPortion = uriPortion + dsoc.getObject().toString(); } } else if (arg instanceof Invoke) { - // System.out.println("arg = " + arg + " && is an instance of invoke"); uriPortion = uriPortion + extractURI(((Invoke) arg).callTarget(), propMap); } else { for (Node n : inputsList) { @@ -312,10 +265,12 @@ private static String extractURI(CallTargetNode node, Map propMa } /** - * extract the method the rest call is being in + * Extracts the method name from a fully qualified method signature by removing + * the parameter list. For example, given "com.example.Class.method(String)", + * it will return "com.example.Class.method". * - * @param input the method's qualified name - * @return the method the call is being made in + * @param input The fully qualified method signature. + * @return The method name without the parameter list. */ private static String cleanParentMethod(String input) { String parentMethod = null; @@ -324,7 +279,21 @@ private static String cleanParentMethod(String input) { return parentMethod; } - //TO-DO: find a safer way to cast Map value + /** + * Resolves a placeholder expression (e.g., `${key.subkey}`) by traversing a nested map (`propMap`). + * The method extracts the key path from the expression, splits it into parts, and navigates + * through the map to find the corresponding value. + * + * Key Features: + * - Supports nested key resolution using dot-separated paths. + * - Returns the resolved value as a string if found. + * - Handles invalid map structures gracefully with error logging. + * - Returns `null` if the key is not found or the value is not a string. + * + * @param expr The placeholder expression to resolve (e.g., `${key.subkey}`). + * @param propMap A map containing the key-value pairs for resolution. + * @return The resolved string value, or `null` if resolution fails. + */ @SuppressWarnings("unchecked") private static String tryResolve(String expr, Map propMap) { diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/GraphQLEndpointExtraction.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/GraphQLEndpointExtraction.java index 4241ed3b61a2..056a6f03202e 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/GraphQLEndpointExtraction.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/GraphQLEndpointExtraction.java @@ -19,12 +19,35 @@ public class GraphQLEndpointExtraction { + /** + * Constants and a set for identifying GraphQL controller annotations. + * These annotations are used to determine if a method is a GraphQL endpoint. + * - QUERY_MAPPING: Represents the annotation for GraphQL query methods. + * - MUTATION_MAPPING: Represents the annotation for GraphQL mutation methods. + * - controllerAnnotationNames: A set containing the simple names of the supported annotations. + */ private final static String QUERY_MAPPING = "org.springframework.graphql.data.method.annotation.QueryMapping"; private final static String MUTATION_MAPPING = "org.springframework.graphql.data.method.annotation.MutationMapping"; - - // annotations for controller to get endpoints private static final Set controllerAnnotationNames = new HashSet<>(Arrays.asList("QueryMapping", "MutationMapping")); + /** + * Extracts GraphQL endpoints from a given class by analyzing its methods and annotations. + * This method identifies methods annotated with GraphQL-specific annotations (e.g., `QueryMapping`, `MutationMapping`) + * and collects their metadata, such as the GraphQL method type, parent method, return type, and parameters. + * + * Key Features: + * - Iterates through all declared methods of the class. + * - Checks for supported GraphQL annotations to determine if a method is an endpoint. + * - Extracts method metadata, including parameter annotations, return type, and path. + * - Handles collection return types and nested annotations. + * - Returns a set of `GraphQLEndpoint` objects representing the extracted endpoints. + * + * @param clazz The class to analyze for GraphQL endpoints. + * @param metaAccess Provides access to meta-information about the class. + * @param bb An `Inflation` object for additional analysis context. + * @param msName The name of the microservice or module being analyzed. + * @return A set of `GraphQLEndpoint` objects representing the extracted endpoints. + */ public static Set extractEndpoints(Class clazz, AnalysisMetaAccess metaAccess, Inflation bb, String msName) { AnalysisType analysisType = metaAccess.lookupJavaType(clazz); Set endpoints = new HashSet(); @@ -32,25 +55,20 @@ public static Set extractEndpoints(Class clazz, AnalysisMeta for (AnalysisMethod method : ((AnalysisMethod[]) analysisType.getDeclaredMethods())) { try { - // What I will need to extract: String httpMethod, String parentMethod, String - // arguments, String returnType Annotation[] annotations = method.getWrapped().getAnnotations(); for (Annotation annotation : annotations) { ArrayList parameterAnnotationsList = new ArrayList<>(); - String httpMethod = null, parentMethod = null, returnTypeResult = null, path = ""; + String graphqlMethod = null, parentMethod = null, returnTypeResult = null, path = ""; boolean returnTypeCollection = false, isEndpoint = false; if (controllerAnnotationNames.contains(annotation.annotationType().getSimpleName())) { isEndpoint = true; - // Code to get the parentMethod attribute: - // following the rad-source format for the parentMethod JSON need to - // parse before the first parenthesis parentMethod = method.getQualifiedName().substring(0, method.getQualifiedName().indexOf("(")); - path = method.getName(); // Save parentMethod as path + path = method.getName(); if (annotation.annotationType().getName().startsWith(QUERY_MAPPING)) { - httpMethod = "QUERY"; + graphqlMethod = "QUERY"; } else if (annotation.annotationType().getName().startsWith(MUTATION_MAPPING)) { - httpMethod = "MUTATION"; + graphqlMethod = "MUTATION"; } parameterAnnotationsList = extractArguments(method); @@ -61,11 +79,10 @@ public static Set extractEndpoints(Class clazz, AnalysisMeta } else { returnTypeCollection = isCollection(returnTypeResult); } - // Special case for request mapping } if (isEndpoint) { - endpoints.add(new GraphQLEndpoint(httpMethod, parentMethod, parameterAnnotationsList, returnTypeResult, path, returnTypeCollection, clazz.getCanonicalName(), msName)); + endpoints.add(new GraphQLEndpoint(graphqlMethod, parentMethod, parameterAnnotationsList, returnTypeResult, path, returnTypeCollection, clazz.getCanonicalName(), msName)); } } @@ -80,20 +97,34 @@ public static Set extractEndpoints(Class clazz, AnalysisMeta return endpoints; } + /** + * Determines if the given return type represents a collection. + * This method checks for array types (indicated by "[L") or generic collection types + * (indicated by the presence of angle brackets "<...>"). + * + * @param returnType The return type as a string. + * @return `true` if the return type is a collection, otherwise `false`. + */ private static boolean isCollection(String returnType) { if (returnType == null || returnType.equals("null")) { - return false; + return false; // Not a collection if null or "null" } - // graal api indicates collections in return type with "class [L" OR + // Check for array or generic collection types return returnType.startsWith("[L") || returnType.matches(".*[<].*[>]"); } + /** - * Method extracts and cleans the return type value of a controller method (based on a - * collection or object/primitive data type) + * Extracts the return type of a given method as a string. + * This method retrieves the generic return type of the provided `AnalysisMethod` + * and processes it to return a simplified representation. * - * @param method an AnalysisMethod - * @return the method's return type as a string value + * Key Features: + * - Handles return types that are classes by removing the "class " prefix. + * - Returns the full string representation of the return type for other cases. + * + * @param method The `AnalysisMethod` whose return type is to be extracted. + * @return A string representing the return type of the method. */ public static String extractReturnType(AnalysisMethod method) { Method javaMethod = (Method) method.getJavaMethod(); @@ -107,17 +138,30 @@ public static String extractReturnType(AnalysisMethod method) { } + /** + * Extracts the arguments of a given method along with their annotations, types, and names. + * This method processes the parameters of the provided `AnalysisMethod` and constructs + * a list of strings representing each parameter in the format: + * + * `@AnnotationName ParameterType ParameterName` + * + * Key Features: + * - Iterates through all parameters of the method. + * - Collects annotations for each parameter and appends them to the result. + * - Extracts the simple type name and parameter name for clarity. + * - Returns a list of formatted strings representing the method's parameters. + * + * @param method The `AnalysisMethod` whose parameters are to be extracted. + * @return A list of strings representing the parameters with annotations, types, and names. + */ public static ArrayList extractArguments(AnalysisMethod method) { - // Code to get the argument attribute: - // Example: "arguments": "[@PathVariable Integer id]", ArrayList parameterAnnotationsList = new ArrayList<>(); Parameter[] params = method.getParameters(); Annotation[][] annotations1 = method.getParameterAnnotations(); for (int i = 0; i < params.length; i++) { Annotation[] annotations2 = annotations1[i]; - // Parameter Annotations (e.g., @PathVariable) are optional, thus can be empty (null) String parameterAnnotation = ""; for (int j = 0; j < annotations2.length; j++) { Annotation annotation3 = annotations2[j]; diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/ProphetPlugin.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/ProphetPlugin.java index f72c55c46d4c..7f07b60f78e3 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/ProphetPlugin.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/ProphetPlugin.java @@ -33,8 +33,6 @@ import com.oracle.svm.hosted.prophet.model.Name; import com.oracle.svm.hosted.prophet.model.RestCall; -import static com.oracle.svm.hosted.prophet.WebsocketCallExtraction.extractClassStompMessageTypes; - public class ProphetPlugin { private final ImageClassLoader loader; @@ -113,6 +111,15 @@ public static void run(ImageClassLoader loader, AnalysisUniverse aUniverse, Anal logger.info("Analyzing all classes in the " + basePackage + " package."); logger.info("Creating module " + msName); + /** + * Initializes the ProphetPlugin and processes the classes to extract metadata. + * This block performs the following: + * - Creates a new instance of `ProphetPlugin` with the provided parameters. + * - Executes the `doRun` method to process the classes and generate a `Module` object. + * - Uses `RestDump` to write out the extracted data (REST calls, WebSocket connections, + * WebSocket endpoints, GraphQL calls, GraphQL endpoints, and general endpoints) + * to their respective output files as specified in the options. + */ var plugin = new ProphetPlugin(loader, aUniverse, metaAccess, bb, basePackage, msName); Module module = plugin.doRun(); RestDump restDump = new RestDump(); @@ -176,50 +183,106 @@ private Module doRun() { return processClasses(classes); } + /** + * Links WebSocket endpoints with their corresponding message types. + * This method iterates through a list of WebSocket endpoints and attempts to match + * each endpoint's handler with the handler of a WebSocket message type. If a match is found, + * the endpoint's return type is set to the message type's data type. If no match is found, + * the endpoint's return type is set to a list of all available message types. + * + * @param websocketEndpointsList A set of WebSocket endpoints to process. + * @param websocketMessageTypesList A set of WebSocket message types to match against. + * @return The updated set of WebSocket endpoints with linked return types. + */ private Set linkWebsocketEndpointsAndMessageTypes(Set websocketEndpointsList, Set websocketMessageTypesList) { + // Iterate through each WebSocket endpoint for (WebsocketEndpoint endpoint : websocketEndpointsList) { + // Check if there are any message types to process if (!websocketMessageTypesList.isEmpty()) { - boolean handlerMatched = false; - List returnTypes = new ArrayList<>(); + boolean handlerMatched = false; // Flag to track if a handler match is found + List returnTypes = new ArrayList<>(); // List to store all message type data types + + // Iterate through each WebSocket message type for (WebsocketMessageType messageType : websocketMessageTypesList) { - returnTypes.add(messageType.getWsDataType()); + returnTypes.add(messageType.getWsDataType()); // Collect the data type of the message type + + // Check if the endpoint's handler matches the message type's handler if (endpoint.getWsHandler().equals(messageType.getWsHandler())) { - endpoint.setReturnType(messageType.getWsDataType()); - handlerMatched = true; - break; + endpoint.setReturnType(messageType.getWsDataType()); // Set the return type to the matched data type + handlerMatched = true; // Mark that a match was found + break; // Exit the loop as a match is found } } + + // If no handler match was found, set the return type to the list of all available data types if (!handlerMatched) { - endpoint.setReturnType(returnTypes.toString()); + endpoint.setReturnType(returnTypes.toString()); } } } + // Return the updated list of WebSocket endpoints return websocketEndpointsList; } + /** + * Links WebSocket connections with their corresponding message types. + * This method iterates through a list of WebSocket connections and attempts to match + * each connection's handler with the handler of a WebSocket message type. If a match is found, + * the connection's return type is set to the message type's data type. If no match is found, + * the connection's return type is set to a list of all available message types. + * + * @param websocketConnectionsList A set of WebSocket connections to process. + * @param websocketMessageTypesList A set of WebSocket message types to match against. + * @return The updated set of WebSocket connections with linked return types. + */ private Set linkWebsocketConnectionsAndMessageTypes(Set websocketConnectionsList, Set websocketMessageTypesList) { + // Iterate through each WebSocket connection for (WebsocketConnection connection : websocketConnectionsList) { + // Check if there are any message types to process if (!websocketMessageTypesList.isEmpty()) { - boolean handlerMatched = false; - List returnTypes = new ArrayList<>(); + boolean handlerMatched = false; // Flag to track if a handler match is found + List returnTypes = new ArrayList<>(); // List to store all message type data types + + // Iterate through each WebSocket message type for (WebsocketMessageType messageType : websocketMessageTypesList) { - returnTypes.add(messageType.getWsDataType()); + returnTypes.add(messageType.getWsDataType()); // Collect the data type of the message type + + // Check if the connection's handler matches the message type's handler if (connection.getWsHandler().equals(messageType.getWsHandler())) { - connection.setReturnType(messageType.getWsDataType()); - handlerMatched = true; - break; + connection.setReturnType(messageType.getWsDataType()); // Set the return type to the matched data type + handlerMatched = true; // Mark that a match was found + break; // Exit the loop as a match is found } } + + // If no handler match was found, set the return type to the list of all available data types if (!handlerMatched) { connection.setReturnType(returnTypes.toString()); } } } + // Return the updated list of WebSocket connections return websocketConnectionsList; } + /** + * Processes a list of classes to extract various types of metadata and relationships. + * This method iterates through the provided classes and performs the following: + * - Extracts entities, REST calls, GraphQL calls, WebSocket connections, WebSocket endpoints, + * WebSocket message types, and general endpoints. + * - Links WebSocket endpoints and connections with their corresponding message types. + * - Aggregates all extracted data into a `Module` object for further processing or output. + * + * Key Features: + * - Handles multiple types of metadata extraction for each class. + * - Ensures WebSocket endpoints and connections are properly linked to their message types. + * - Logs the number of classes being processed for debugging purposes. + * + * @param classes A list of classes to analyze and process. + * @return A `Module` object containing all extracted metadata and relationships. + */ private Module processClasses(List> classes) { var entities = new HashSet(); Set restCallList = new HashSet(); @@ -239,9 +302,9 @@ private Module processClasses(List> classes) { restCallList.addAll(restCalls); Set GraphQLCalls = GraphQLCallExtraction.extractClassRestCalls(clazz, metaAccess, bb, this.propMap, Options.ProphetMicroserviceName.getValue()); graphQLCallList.addAll(GraphQLCalls); - Set websocketConnection = WebsocketCallExtraction.extractClassWebsocketConnection(clazz, metaAccess, bb, this.propMap, Options.ProphetMicroserviceName.getValue()); - Set websocketEndpoints = WebsocketCallExtraction.extractClassWebsocketEndpoints(clazz, metaAccess, bb, this.propMap, Options.ProphetMicroserviceName.getValue()); - Set websocketMessageTypes = WebsocketCallExtraction.extractClassWebsocketMessageTypes(clazz, metaAccess, bb, this.propMap, Options.ProphetMicroserviceName.getValue()); + Set websocketConnection = WebsocketConnectionExtraction.extractClassWebsocketConnection(clazz, metaAccess, bb, this.propMap, Options.ProphetMicroserviceName.getValue()); + Set websocketEndpoints = WebsocketConnectionExtraction.extractClassWebsocketEndpoints(clazz, metaAccess, bb, this.propMap, Options.ProphetMicroserviceName.getValue()); + Set websocketMessageTypes = WebsocketConnectionExtraction.extractClassWebsocketMessageTypes(clazz, metaAccess, bb, this.propMap, Options.ProphetMicroserviceName.getValue()); websocketEndpointsList.addAll(websocketEndpoints); websocketMessageTypesList.addAll(websocketMessageTypes); diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/WebsocketCallExtraction.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/WebsocketConnectionExtraction.java similarity index 77% rename from substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/WebsocketCallExtraction.java rename to substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/WebsocketConnectionExtraction.java index e34232fb613b..2b5bde463a84 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/WebsocketCallExtraction.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/WebsocketConnectionExtraction.java @@ -30,7 +30,6 @@ import org.graalvm.compiler.nodes.java.LoadFieldNode; import org.graalvm.compiler.nodes.virtual.AllocatedObjectNode; import org.graalvm.compiler.nodes.virtual.CommitAllocationNode; -import org.graalvm.compiler.nodes.virtual.VirtualArrayNode; import java.lang.reflect.Field; import java.lang.reflect.Method; @@ -38,7 +37,7 @@ import java.util.Map; import java.util.Set; -public class WebsocketCallExtraction { +public class WebsocketConnectionExtraction { /* NOTE: @@ -49,6 +48,17 @@ public class WebsocketCallExtraction { static String URI = ""; + /** + * Extracts the hosted Java class from a given DynamicHub object. + * This method uses reflection to access the private field `hostedJavaClass` + * within the provided DynamicHub instance, making it accessible and retrieving + * its value as a `Class` object. + * + * @param dynamicHub The DynamicHub object containing the hosted Java class. + * @return The hosted Java class as a `Class` object. + * @throws NoSuchFieldException If the `hostedJavaClass` field is not found. + * @throws IllegalAccessException If the field cannot be accessed. + */ public static Class extractHostedClass(Object dynamicHub) throws NoSuchFieldException, IllegalAccessException { // The hosted class is typically stored in a field of the DynamicHub Field hostedClassField = dynamicHub.getClass().getDeclaredField("hostedJavaClass"); @@ -60,7 +70,11 @@ public static Class extractHostedClass(Object dynamicHub) throws NoSuchFieldE return (Class) hostedClassField.get(dynamicHub); } - // Helper class to hold both URI and handler data + /** + * Helper class to encapsulate both URI and handler data during extraction. + * This class provides a convenient way to store and manage the URI and handler + * information as `StringBuilder` objects. + */ private static class ExtractionResult { StringBuilder uriBuilder; StringBuilder handlerBuilder; @@ -71,6 +85,17 @@ public ExtractionResult(StringBuilder uriBuilder, StringBuilder handlerBuilder) } } + /** + * Recursively traverses the graph of nodes to extract URI and handler information. + * This method identifies specific node types, such as `InvokeWithExceptionNode`, + * to process concatenation components for URIs and handler instantiation details. + * + * @param currentNode The current node being analyzed. + * @param visitedNodes A set of nodes already visited to prevent infinite loops. + * @param uriBuilder A `StringBuilder` to accumulate the extracted URI components. + * @param handlerBuilder A `StringBuilder` to accumulate the extracted handler details. + * @return An `ExtractionResult` containing the concatenated URI and handler information. + */ private static ExtractionResult logPrecedingNodesRecursiveURIandHandler( Node currentNode, Set visitedNodes, StringBuilder uriBuilder, StringBuilder handlerBuilder) { if (visitedNodes.contains(currentNode)) { @@ -119,6 +144,21 @@ private static ExtractionResult logPrecedingNodesRecursiveURIandHandler( return new ExtractionResult(uriBuilder, handlerBuilder); } + /** + * Recursively traverses the graph of nodes to extract URI and handler information. + * This method identifies specific node types, such as `InvokeWithExceptionNode`, + * to process concatenation components for URIs and handler instantiation details. + * + * The method uses a `Set` to track visited nodes and avoid infinite loops during + * the traversal. It appends extracted URI components to the `uriBuilder` and + * handler details to the `handlerBuilder`. + * + * @param currentNode The current node being analyzed. + * @param visitedNodes A set of nodes already visited to prevent infinite loops. + * @param uriBuilder A `StringBuilder` to accumulate the extracted URI components. + * @param handlerBuilder A `StringBuilder` to accumulate the extracted handler details. + * @return A `StringBuilder` containing the concatenated URI components. + */ private static StringBuilder logPrecedingNodesRecursive(Node currentNode, Set visitedNodes, StringBuilder uriBuilder, StringBuilder handlerBuilder) { if (visitedNodes.contains(currentNode)) { return uriBuilder; @@ -165,6 +205,23 @@ private static StringBuilder logPrecedingNodesRecursive(Node currentNode, Set extractClassWebsocketConnection(Class clazz, AnalysisMetaAccess metaAccess, Inflation bb, Map propMap, String msName) { Set websocketConnections = new HashSet<>(); AnalysisType analysisType = metaAccess.lookupJavaType(clazz); @@ -259,6 +316,23 @@ public static Set extractClassWebsocketConnection(Class return websocketConnections; } + /** + * Extracts WebSocket endpoint details from a given class by analyzing its methods and their + * associated graphs. This method identifies specific WebSocket-related invocations, such as + * `WebSocketHandlerRegistry.addHandler` and `StompEndpointRegistry.addEndpoint`, to extract + * URI and handler information. + * + * The method uses GraalVM's analysis tools to decode the method graphs and traverse their nodes + * to detect relevant invocations. Extracted data is stored in `WebsocketEndpoint` objects and + * returned as a set. + * + * @param clazz The class to analyze for WebSocket endpoint details. + * @param metaAccess The meta-access interface for type and method analysis. + * @param bb The inflation object used for decoding graphs. + * @param propMap A map of properties for resolving dynamic values (e.g., URIs). + * @param msName The name of the microservice or module being analyzed. + * @return A set of `WebsocketEndpoint` objects containing extracted endpoint details. + */ public static Set extractClassWebsocketEndpoints(Class clazz, AnalysisMetaAccess metaAccess, Inflation bb, Map propMap, String msName) { Set websocketEndpoints = new HashSet<>(); AnalysisType analysisType = metaAccess.lookupJavaType(clazz); @@ -330,6 +404,30 @@ public static Set extractClassWebsocketEndpoints(Class cla return websocketEndpoints; } + /** + * Extracts WebSocket message type details from a given class by analyzing its methods and their + * associated graphs. This method identifies specific WebSocket-related invocations, such as + * `AbstractWebSocketMessage.getPayload` and `ObjectMapper.readValue`, to extract payload type + * information. + * + * The method uses GraalVM's analysis tools to decode the method graphs and traverse their nodes + * to detect relevant invocations. Extracted data is stored in `WebsocketMessageType` objects and + * returned as a set. + * + * Key Features: + * - Detects `.getPayloadType` calls in `StompSessionHandlerAdapter` or its subclasses to extract + * the payload type. + * - Identifies `AbstractWebSocketMessage.getPayload` and `ObjectMapper.readValue` calls to extract + * the payload type (e.g., `ChatMessage`). + * - Uses reflection to extract hosted class information from `DynamicHub` objects. + * + * @param clazz The class to analyze for WebSocket message type details. + * @param metaAccess The meta-access interface for type and method analysis. + * @param bb The inflation object used for decoding graphs. + * @param propMap A map of properties for resolving dynamic values (e.g., URIs). + * @param msName The name of the microservice or module being analyzed. + * @return A set of `WebsocketMessageType` objects containing extracted message type details. + */ public static Set extractClassWebsocketMessageTypes(Class clazz, AnalysisMetaAccess metaAccess, Inflation bb, Map propMap, String msName) { Set websocketMessageTypes = new HashSet<>(); AnalysisType analysisType = metaAccess.lookupJavaType(clazz); @@ -383,7 +481,6 @@ public static Set extractClassWebsocketMessageTypes(Class< } - // Loop through all nodes in the graph for (Node node : decodedGraph.getNodes()) { // Detect URI.create invocations @@ -450,61 +547,21 @@ public static Set extractClassWebsocketMessageTypes(Class< return websocketMessageTypes; } - - public static Set extractClassStompMessageTypes(Class clazz, AnalysisMetaAccess metaAccess, Inflation bb, Map propMap, String msName, String handlerClass) { - Set websocketMessageTypes = new HashSet<>(); - AnalysisType analysisType = metaAccess.lookupJavaType(clazz); - - try { - for (AnalysisMethod method : ((AnalysisMethod[]) analysisType.getDeclaredMethods())) { - if (method.isAbstract()) { - continue; - } - - try { - StructuredGraph decodedGraph = ReachabilityAnalysisMethod.getDecodedGraph(bb, method); - - String substring = handlerClass + ".getPayloadType"; - - if (method.getQualifiedName().contains(substring)) { - - - for (Node node : decodedGraph.getNodes()) { - - if (node instanceof ReturnNode) { - ReturnNode returnNode = (ReturnNode) node; - ValueNode returnVal = returnNode.result(); - if (returnVal instanceof ConstantNode) { - ConstantNode constantNode = (ConstantNode) returnVal; - Object payloadObject = constantNode.getValue(); - - if (payloadObject instanceof DirectSubstrateObjectConstant) { - DirectSubstrateObjectConstant dsoc = (DirectSubstrateObjectConstant) payloadObject; - Object actualMessage = dsoc.getObject(); - - if (actualMessage instanceof com.oracle.svm.core.hub.DynamicHub) { - Class actualClass = extractHostedClass(actualMessage); - websocketMessageTypes.add(actualClass.getSimpleName()); - } - } - } - - } - } - } - } catch (Exception | LinkageError ex) { - ex.printStackTrace(); - } - } - } catch (Exception | LinkageError ex) { - ex.printStackTrace(); - } - return websocketMessageTypes; - } - - - - private static WebsocketParameter setIfBodyAndType(WebsocketParameter param, CallTargetNode node) { + /** + * Recursively processes the arguments of a `CallTargetNode` to determine if a WebSocket parameter + * has a body and its type. This method traverses through `PiNode` and `Invoke` nodes to extract + * relevant information and updates the provided `WebsocketParameter` object. + * + * Key Features: + * - Handles `PiNode` arguments by iterating over their inputs and recursively processing them. + * - Handles `Invoke` arguments by delegating to the `handleIfInvokeInWebsocketParam` method. + * - Returns the updated `WebsocketParameter` object after processing all arguments. + * + * @param param The `WebsocketParameter` object to update with extracted details. + * @param node The `CallTargetNode` whose arguments are being analyzed. + * @return The updated `WebsocketParameter` object. + */ + private static WebsocketParameter setIfBodyAndType(WebsocketParameter param, CallTargetNode node) { for (ValueNode arg : node.arguments()) { if (arg instanceof PiNode) { for (Node inputNode : ((PiNode) arg).inputs()) { @@ -536,7 +593,7 @@ private static WebsocketParameter handleIfInvokeInWebsocketParam(WebsocketParame // System.out.println("\t\t\tpredecessor of BeginNode = " + predecessor.predecessor()); Node bNodePredecessor = predecessor.predecessor(); - if (((Invoke) predecessor.predecessor()).callTarget().targetMethod().toString().contains(WebsocketCallExtraction.WEBSOCKET_TEXT_MESSAGE)) { + if (((Invoke) predecessor.predecessor()).callTarget().targetMethod().toString().contains(WebsocketConnectionExtraction.WEBSOCKET_TEXT_MESSAGE)) { // System.out.println("callTarget = " + ((Invoke)predecessor.predecessor()).callTarget()); int inputAmnt = 0; for (Node ctIn : ((Invoke) predecessor.predecessor()).callTarget().inputs()) { @@ -559,7 +616,24 @@ private static WebsocketParameter handleIfInvokeInWebsocketParam(WebsocketParame return param; } - private static String extractURI(CallTargetNode node, Map propMap){ + /** + * Extracts a URI portion from a `CallTargetNode` by analyzing its arguments and traversing + * through various node types such as `LoadFieldNode`, `PiNode`, `ConstantNode`, and others. + * The method resolves dynamic values using a property map (`propMap`) and recursively processes + * nested nodes to construct the full URI. + * + * Key Features: + * - Handles `LoadFieldNode` to extract annotations and resolve values using `propMap`. + * - Processes `PiNode` and its inputs recursively to extract URI components. + * - Handles `ConstantNode` and `AllocatedObjectNode` to extract constant values. + * - Recursively processes `Invoke` nodes to extract nested URI portions. + * - Skips null or unsupported node types gracefully. + * + * @param node The `CallTargetNode` to analyze for URI extraction. + * @param propMap A map of properties for resolving dynamic values (e.g., placeholders in URIs). + * @return A string representing the extracted URI portion. + */ + private static String extractURI(CallTargetNode node, Map propMap) { // System.out.println("NODE CALL TARGET: " + node); // System.out.println("NODE CALL TARGET ARGS: " + node.arguments()); String uriPortion = ""; @@ -570,9 +644,9 @@ private static String extractURI(CallTargetNode node, Map propMa * else if node is a loadfieldnode, go over annotations and get 'value' annotation * get value based off prop map */ - for (ValueNode arg : node.arguments()){ + for (ValueNode arg : node.arguments()) { NodeIterable inputsList = arg.inputs(); - if (arg instanceof LoadFieldNode){ + if (arg instanceof LoadFieldNode) { // System.out.println("arg is a LOAD_FIELD_NODE, arg = " + arg); LoadFieldNode loadfieldNode = (LoadFieldNode) arg; AnalysisField field = (AnalysisField) loadfieldNode.field(); @@ -581,33 +655,31 @@ private static String extractURI(CallTargetNode node, Map propMa if (annotation.annotationType().getName().contains("Value")) { // System.out.println("Load field with value annotation"); // System.out.println("methods = " + annotation.annotationType().getMethods()); - try{ + try { Method valueMethod = annotation.annotationType().getMethod("value"); valueMethod.setAccessible(true); String res = ""; - if (propMap != null){ - res = tryResolve(((String)valueMethod.invoke(annotation)), propMap); + if (propMap != null) { + res = tryResolve(((String) valueMethod.invoke(annotation)), propMap); } uriPortion = uriPortion + res; - }catch(Exception ex){ + } catch (Exception ex) { System.err.println("ERROR = " + ex); } } } - } - else if (arg instanceof PiNode){ + } else if (arg instanceof PiNode) { // System.out.println(arg + " is a PiNode"); // System.out.println("pi node inputs: " + ((PiNode)arg).inputs()); - for (Node inputNode : ((PiNode)arg).inputs()){ - if (inputNode instanceof Invoke){ + for (Node inputNode : ((PiNode) arg).inputs()) { + if (inputNode instanceof Invoke) { // System.out.println(inputNode + " is Invoke"); - uriPortion = uriPortion + extractURI(((Invoke)inputNode).callTarget(), propMap); + uriPortion = uriPortion + extractURI(((Invoke) inputNode).callTarget(), propMap); } } - } - else if (arg instanceof ConstantNode){ - ConstantNode cn = (ConstantNode)arg; + } else if (arg instanceof ConstantNode) { + ConstantNode cn = (ConstantNode) arg; //PrimitiveConstants can not be converted to DirectSubstrateObjectConstant if (cn.asJavaConstant() != null && cn.asJavaConstant().isNull()) { // Skip, it's null @@ -617,8 +689,7 @@ else if (arg instanceof ConstantNode){ uriPortion += dsoc.getObject().toString(); } } - } - else if (arg instanceof AllocatedObjectNode) { + } else if (arg instanceof AllocatedObjectNode) { // Handle allocated objects, which may contain constant values AllocatedObjectNode allocatedObject = (AllocatedObjectNode) arg; for (Node input : allocatedObject.inputs()) { @@ -641,15 +712,14 @@ else if (arg instanceof AllocatedObjectNode) { } } } - } - else if (arg instanceof Invoke){ + } else if (arg instanceof Invoke) { // System.out.println("arg = " + arg + " && is an instance of invoke"); - uriPortion = uriPortion + extractURI(((Invoke)arg).callTarget(), propMap); - } - else{ - for (Node n : inputsList){ - if (n instanceof Invoke){; - uriPortion = uriPortion + extractURI(((Invoke)n).callTarget(), propMap); + uriPortion = uriPortion + extractURI(((Invoke) arg).callTarget(), propMap); + } else { + for (Node n : inputsList) { + if (n instanceof Invoke) { + ; + uriPortion = uriPortion + extractURI(((Invoke) n).callTarget(), propMap); } } } From 3b08bfb37fa29ceb93776150f68ca3eab04eadde Mon Sep 17 00:00:00 2001 From: Vsevolod Pokhvalenko <119959398+xpokhv00@users.noreply.github.com> Date: Sun, 11 May 2025 13:57:49 +0200 Subject: [PATCH 6/6] add authors to modified files --- .../src/com/oracle/svm/hosted/prophet/ProphetPlugin.java | 6 ++++++ .../com/oracle/svm/hosted/prophet/RestCallExtraction.java | 6 ++++++ .../src/com/oracle/svm/hosted/prophet/RestDump.java | 6 ++++++ .../svm/hosted/prophet/WebsocketConnectionExtraction.java | 5 +++++ .../src/com/oracle/svm/hosted/prophet/model/Module.java | 6 ++++++ .../svm/hosted/prophet/model/WebsocketConnection.java | 4 ++++ .../oracle/svm/hosted/prophet/model/WebsocketEndpoint.java | 5 +++++ .../svm/hosted/prophet/model/WebsocketMessageType.java | 5 +++++ .../oracle/svm/hosted/prophet/model/WebsocketParameter.java | 4 ++++ 9 files changed, 47 insertions(+) diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/ProphetPlugin.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/ProphetPlugin.java index 7f07b60f78e3..a7547d62ac86 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/ProphetPlugin.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/ProphetPlugin.java @@ -1,3 +1,9 @@ +/** + * Authors: + * - Original Authors + * - Vsevolod Pokhvalenko + */ + package com.oracle.svm.hosted.prophet; import java.io.BufferedReader; diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/RestCallExtraction.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/RestCallExtraction.java index f7d837faffae..9ce54614fa91 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/RestCallExtraction.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/RestCallExtraction.java @@ -1,3 +1,9 @@ +/** + * Authors: + * - Original Authors + * - Vsevolod Pokhvalenko + */ + package com.oracle.svm.hosted.prophet; import java.lang.reflect.Method; diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/RestDump.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/RestDump.java index 0549630ff956..29b31d83034a 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/RestDump.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/RestDump.java @@ -1,3 +1,9 @@ +/** + * Authors: + * - Original Authors + * - Vsevolod Pokhvalenko + */ + package com.oracle.svm.hosted.prophet; import com.oracle.svm.hosted.prophet.model.Endpoint; import com.oracle.svm.hosted.prophet.model.GraphQLCall; diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/WebsocketConnectionExtraction.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/WebsocketConnectionExtraction.java index 2b5bde463a84..07254bb77a1b 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/WebsocketConnectionExtraction.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/WebsocketConnectionExtraction.java @@ -1,3 +1,8 @@ +/** + * Authors: + * - Vsevolod Pokhvalenko + */ + package com.oracle.svm.hosted.prophet; import com.oracle.graal.pointsto.meta.AnalysisField; diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/Module.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/Module.java index 22bbb81cf944..f2e1dab5c279 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/Module.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/Module.java @@ -1,3 +1,9 @@ +/** + * Authors: + * - Original Authors + * - Vsevolod Pokhvalenko + */ + package com.oracle.svm.hosted.prophet.model; import java.util.Set; diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketConnection.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketConnection.java index 4393a1a8fec4..de0681ea1996 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketConnection.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketConnection.java @@ -1,3 +1,7 @@ +/** + * Authors: + * - Vsevolod Pokhvalenko + */ package com.oracle.svm.hosted.prophet.model; public class WebsocketConnection { diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketEndpoint.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketEndpoint.java index 960b808540ad..d1f7311aabdc 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketEndpoint.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketEndpoint.java @@ -1,3 +1,8 @@ +/** + * Authors: + * - Vsevolod Pokhvalenko + */ + package com.oracle.svm.hosted.prophet.model; public class WebsocketEndpoint { diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketMessageType.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketMessageType.java index 44b58843872e..33e2c9f52b5d 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketMessageType.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketMessageType.java @@ -1,3 +1,8 @@ +/** + * Authors: + * - Vsevolod Pokhvalenko + */ + package com.oracle.svm.hosted.prophet.model; public class WebsocketMessageType { diff --git a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketParameter.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketParameter.java index bcbc922e52e8..62985d54b6a2 100644 --- a/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketParameter.java +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketParameter.java @@ -1,3 +1,7 @@ +/** + * Authors: + * - Vsevolod Pokhvalenko + */ package com.oracle.svm.hosted.prophet.model; public class WebsocketParameter {