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..6713f6196cbe --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/GraphQLCallExtraction.java @@ -0,0 +1,323 @@ +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 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.WebsocketConnectionExtraction.extractHostedClass; + +public class GraphQLCallExtraction { + + // 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 { + 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 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 input.replace("\"", "").replace("\n", ""); + } + + /** + * 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) { + String uriPortion = ""; + + for (ValueNode arg : node.arguments()) { + NodeIterable inputsList = arg.inputs(); + if (arg instanceof LoadFieldNode) { + LoadFieldNode loadfieldNode = (LoadFieldNode) arg; + AnalysisField field = (AnalysisField) loadfieldNode.field(); + + for (java.lang.annotation.Annotation annotation : field.getWrapped().getAnnotations()) { + if (annotation.annotationType().getName().contains("Value")) { + 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) { + for (Node inputNode : ((PiNode) arg).inputs()) { + if (inputNode instanceof Invoke) { + uriPortion = uriPortion + extractURI(((Invoke) inputNode).callTarget(), propMap); + } + } + } else if (arg instanceof ConstantNode) { + ConstantNode cn = (ConstantNode) arg; + if (!(cn.getValue() instanceof PrimitiveConstant)) { + DirectSubstrateObjectConstant dsoc = (DirectSubstrateObjectConstant) cn.getValue(); + uriPortion = uriPortion + dsoc.getObject().toString(); + } + + } else if (arg instanceof 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; + } + + /** + * 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 fully qualified method signature. + * @return The method name without the parameter list. + */ + private static String cleanParentMethod(String input) { + String parentMethod = null; + + parentMethod = input.substring(0, input.indexOf("(")); + return parentMethod; + } + + /** + * 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) { + + 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..056a6f03202e --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/GraphQLEndpointExtraction.java @@ -0,0 +1,181 @@ +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 { + + /** + * 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"; + 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(); + try { + + for (AnalysisMethod method : ((AnalysisMethod[]) analysisType.getDeclaredMethods())) { + try { + Annotation[] annotations = method.getWrapped().getAnnotations(); + for (Annotation annotation : annotations) { + + ArrayList parameterAnnotationsList = new ArrayList<>(); + String graphqlMethod = null, parentMethod = null, returnTypeResult = null, path = ""; + boolean returnTypeCollection = false, isEndpoint = false; + if (controllerAnnotationNames.contains(annotation.annotationType().getSimpleName())) { + isEndpoint = true; + parentMethod = method.getQualifiedName().substring(0, method.getQualifiedName().indexOf("(")); + path = method.getName(); + if (annotation.annotationType().getName().startsWith(QUERY_MAPPING)) { + graphqlMethod = "QUERY"; + } else if (annotation.annotationType().getName().startsWith(MUTATION_MAPPING)) { + graphqlMethod = "MUTATION"; + } + + parameterAnnotationsList = extractArguments(method); + returnTypeResult = extractReturnType(method); + if (returnTypeResult.startsWith("[L") && isCollection(returnTypeResult)) { + returnTypeCollection = true; + returnTypeResult = returnTypeResult.substring(2); + } else { + returnTypeCollection = isCollection(returnTypeResult); + } + } + + if (isEndpoint) { + endpoints.add(new GraphQLEndpoint(graphqlMethod, parentMethod, parameterAnnotationsList, returnTypeResult, path, returnTypeCollection, clazz.getCanonicalName(), msName)); + } + } + + } catch (Exception | LinkageError ex) { + ex.printStackTrace(); + } + } + } catch (Exception | LinkageError ex) { + ex.printStackTrace(); + } + + 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; // Not a collection if null or "null" + } + // Check for array or generic collection types + return returnType.startsWith("[L") || returnType.matches(".*[<].*[>]"); + } + + + /** + * 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. + * + * 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(); + 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(); + } + + } + + /** + * 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) { + + ArrayList parameterAnnotationsList = new ArrayList<>(); + Parameter[] params = method.getParameters(); + Annotation[][] annotations1 = method.getParameterAnnotations(); + + for (int i = 0; i < params.length; i++) { + Annotation[] annotations2 = annotations1[i]; + 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 6ef92d1ee70f..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,5 +1,12 @@ +/** + * Authors: + * - Original Authors + * - Vsevolod Pokhvalenko + */ + package com.oracle.svm.hosted.prophet; +import java.io.BufferedReader; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.FileWriter; @@ -7,12 +14,18 @@ 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.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; import org.graalvm.compiler.options.Option; import com.oracle.graal.pointsto.meta.AnalysisMetaAccess; @@ -73,6 +86,18 @@ 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 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); @@ -92,11 +117,25 @@ 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(); 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()); + restDump.writeOutGraphQLCalls(module.getGraphQLCalls(), Options.ProphetGraphQLCallOutputFile.getValue()); + restDump.writeOutGraphQLEndpoints(module.getGraphQLEndpoints(), Options.ProphetGraphQLEndpointOutputFile.getValue()); + dumpModule(module); logger.info("Final summary: " + module.shortSummary()); @@ -123,10 +162,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,10 +189,115 @@ 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; // 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()); // 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()); // 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()); + } + } + } + // 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; // 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()); // 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()); // 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(); + 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) { @@ -146,12 +306,28 @@ private Module processClasses(List> classes) { 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 = 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); + 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); - } - return new Module(new Name(msName), entities, restCallList, endpointList); + + websocketEndpointsList = linkWebsocketEndpointsAndMessageTypes(websocketEndpointsList, websocketMessageTypesList); + websocketConnectionsList = linkWebsocketConnectionsAndMessageTypes(websocketConnectionsList, websocketMessageTypesList); + + + 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/RestCallExtraction.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/RestCallExtraction.java index 290176a7b4f3..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; @@ -50,6 +56,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 +122,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..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,6 +1,16 @@ +/** + * 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; +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; import java.io.IOException; import java.util.Set; @@ -11,27 +21,54 @@ 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 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"); @@ -44,4 +81,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/WebsocketConnectionExtraction.java b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/WebsocketConnectionExtraction.java new file mode 100644 index 000000000000..07254bb77a1b --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/WebsocketConnectionExtraction.java @@ -0,0 +1,776 @@ +/** + * Authors: + * - Vsevolod Pokhvalenko + */ + +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 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.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; +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; + +public class WebsocketConnectionExtraction { + + /* + 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 = ""; + + /** + * 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"); + + // 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 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; + + public ExtractionResult(StringBuilder uriBuilder, StringBuilder handlerBuilder) { + this.uriBuilder = uriBuilder; + this.handlerBuilder = 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)) { + 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); + } + + /** + * 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; + } + + 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; + } + + /** + * Extracts WebSocket connection details from a given class by analyzing its methods and their + * associated graphs. This method identifies specific WebSocket-related invocations, such as + * `WebSocketStompClient.connect` and `WebSocketClient.doHandshake`, 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 `WebsocketConnection` objects and + * returned as a set. + * + * @param clazz The class to analyze for WebSocket connection 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 `WebsocketConnection` objects containing extracted connection details. + */ + public static 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())) { + + 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(); + + // 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(); + + 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 || returnType != null) { + String parentMethod = cleanParentMethod(method.getQualifiedName()); + websocketConnections.add(new WebsocketConnection(parentMethod, returnType, uri, isCollection, clazz.getCanonicalName(), msName, param, wsHandler)); + + // 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; + } + + /** + * 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); + + 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(); + } + + // **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"; + } + } + } + + // 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; + } + + /** + * 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); + + 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; + + 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 + 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; + } + + /** + * 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()) { + 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(WebsocketConnectionExtraction.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); + } + } + + param = setIfBodyAndType(param, ((Invoke) predecessor.predecessor()).callTarget()); + } else { + param = setIfBodyAndType(param, ((Invoke) node).callTarget()); + } + return param; + } + + /** + * 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 = ""; + + /* + * 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.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"); + 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/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 81d8c69f39c6..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; @@ -8,33 +14,48 @@ public class Module { private Set entities; private Set restCalls; + private Set websocketConnections; + private Set websocketEndpoints; private Set endpoints; + private Set graphQLCalls; + private Set graphQLEndpoints; - public Module(Name name, Set entities, Set restCalls, 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() { return "Module(name=" + - name + - ",entities=" + - entities.size() + - ",restcalls=" + - restCalls.size() + - ",endpoints=" + - endpoints.size() + - ')'; + name + + ",entities=" + + entities.size() + + ",restcalls=" + + restCalls.size() + + ",websocketConnections=" + + websocketConnections.size() + + ",websocketEndpoints=" + + websocketEndpoints.size() + + ",endpoints=" + + endpoints.size() + + ",graphQLCalls=" + + graphQLCalls.size() + + ",graphQLEndpoints=" + + graphQLEndpoints.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("\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(); } @@ -67,15 +88,39 @@ public Set getRestCalls() { return restCalls; } + public Set getWebsocketConnections() { + return websocketConnections; + } + + public Set getWebsocketEndpoints() { + return websocketEndpoints; + } + 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; } + 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 new file mode 100644 index 000000000000..de0681ea1996 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketConnection.java @@ -0,0 +1,101 @@ +/** + * Authors: + * - Vsevolod Pokhvalenko + */ +package com.oracle.svm.hosted.prophet.model; + +public class WebsocketConnection { + + private String parentMethod; + private String returnType; + private String wsHandler; + 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, String wsHandler) { + + this.msName = msName; + this.connectionInClassName = connectionInClassName; + this.parentMethod = parentMethod; + this.uri = uri; + this.returnType = returnType; + this.param = param; + this.isCollection = isCollection; + 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 : "").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; + } + + 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 new file mode 100644 index 000000000000..d1f7311aabdc --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketEndpoint.java @@ -0,0 +1,93 @@ +/** + * Authors: + * - Vsevolod Pokhvalenko + */ + +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.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 setReturnType(String returnType) { + this.returnType = returnType; + } + + 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..33e2c9f52b5d --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketMessageType.java @@ -0,0 +1,98 @@ +/** + * Authors: + * - Vsevolod Pokhvalenko + */ + +package com.oracle.svm.hosted.prophet.model; + +public class WebsocketMessageType { + + private String wsDataType; + private String msName; + private String wsHandler; + private String parentMethod; + 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 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 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..62985d54b6a2 --- /dev/null +++ b/substratevm/src/com.oracle.svm.hosted/src/com/oracle/svm/hosted/prophet/model/WebsocketParameter.java @@ -0,0 +1,52 @@ +/** + * Authors: + * - Vsevolod Pokhvalenko + */ +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; + } + +}