diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 60f3c67c2..938259be1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -47,11 +47,43 @@ jobs: name: ${{ steps.ls.outputs.fileName }}.zip - name: Create a release in repo run: | - createResponse=`curl -X POST -H "Accept: application/vnd.github.v3+json" \ - -H "Authorization:token ${{ secrets.GIT_BOT_TOKEN }}" -d '{"tag_name":"v${{ steps.ls.outputs.version }}", \ - "draft":false, "name": "Release v${{ steps.ls.outputs.version }}", "prerelease":true}' \ - https://api.github.com/repos/${{ github.repository }}/releases` \ - && id=`echo "$createResponse" | sed -n -e 's/"id":\ \([0-9]\+\),/\1/p' | head -n 1 | sed 's/[[:blank:]]//g'` && \ - uploadResponse=`curl -X POST -H "Authorization:token ${{ secrets.GIT_BOT_TOKEN }}" -H "Content-Type:application/octet-stream" \ + set -euo pipefail + createResponse=$(curl --fail-with-body -X POST \ + -H "Accept: application/vnd.github.v3+json" \ + -H "Authorization:token ${{ secrets.GIT_BOT_TOKEN }}" \ + -d '{"tag_name":"v${{ steps.ls.outputs.version }}", "draft":false, "name": "Release v${{ steps.ls.outputs.version }}", "prerelease":true}' \ + https://api.github.com/repos/${{ github.repository }}/releases) + id=$(echo "$createResponse" | jq -r '.id // empty') + if [ -z "$id" ]; then + echo "Failed to parse release id from response:" + echo "$createResponse" + exit 1 + fi + curl --fail-with-body -X POST \ + -H "Authorization:token ${{ secrets.GIT_BOT_TOKEN }}" \ + -H "Content-Type:application/octet-stream" \ --data-binary @${{ steps.ls.outputs.fileName }}.zip \ - https://uploads.github.com/repos/${{ github.repository }}/releases/$id/assets?name=${{ steps.ls.outputs.fileName }}.zip` + "https://uploads.github.com/repos/${{ github.repository }}/releases/$id/assets?name=${{ steps.ls.outputs.fileName }}.zip" + + - name: Create sync PR from stable/mi to main + if: github.ref == 'refs/heads/stable/mi' + env: + GH_TOKEN: ${{ secrets.GIT_BOT_TOKEN }} + run: | + git fetch --unshallow origin main stable/mi 2>/dev/null || git fetch origin main stable/mi + commits=$(git rev-list --count origin/main..origin/stable/mi) + if [ "$commits" -eq 0 ]; then + echo "No new commits on stable/mi compared to main — skipping PR creation." + exit 0 + fi + existing=$(gh pr list --repo ${{ github.repository }} --base main --head stable/mi --state open --json number --jq '.[0].number // empty') + if [ -n "$existing" ]; then + echo "An open PR from stable/mi to main already exists: #$existing" + exit 0 + fi + gh pr create \ + --repo ${{ github.repository }} \ + --base main \ + --head stable/mi \ + --title "Sync stable/mi to main after release v${{ steps.ls.outputs.version }}" \ + --body "Automated PR to sync \`stable/mi\` into \`main\` following the successful release of v${{ steps.ls.outputs.version }}." diff --git a/org.eclipse.lemminx/pom.xml b/org.eclipse.lemminx/pom.xml index 4c0922b97..77dbeba12 100644 --- a/org.eclipse.lemminx/pom.xml +++ b/org.eclipse.lemminx/pom.xml @@ -3,7 +3,7 @@ org.wso2.language.server mi-language-server-parent - 0.24.0-wso2v90 + 0.24.0-wso2v95 ../pom.xml MI Language Server @@ -15,7 +15,7 @@ ${maven.build.timestamp} true 0.9.16 - 0.9.10 + 0.9.11 diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/SynapseLanguageService.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/SynapseLanguageService.java index 1da9fb4fd..328ba14ae 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/SynapseLanguageService.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/SynapseLanguageService.java @@ -150,6 +150,7 @@ import org.eclipse.lemminx.customservice.synapse.idp.PdfToImagesRequest; import org.eclipse.lemminx.dom.DOMDocument; import org.eclipse.lemminx.extensions.contentmodel.settings.XMLValidationSettings; +import org.eclipse.lemminx.extensions.synapse.SynapseDiagnosticsParticipant; import org.eclipse.lemminx.services.extensions.completion.ICompletionResponse; import org.eclipse.lemminx.settings.SharedSettings; import org.eclipse.lemminx.uriresolver.URIResolverExtensionManager; @@ -346,8 +347,21 @@ private PublishDiagnosticsParams doDiagnostics(DOMDocument xmlDocument, CancelCh public CompletableFuture codeDiagnostic(CodeDiagnosticRequest param) { return CompletableFuture.supplyAsync(() -> { - DOMDocument xmlDocument = Utils.getDOMDocument(param.getCode(), uriResolverExtensionManager); - return doDiagnostics(xmlDocument, NULL_CANCEL_CHECKER); + // Use the real file path (when supplied) as the document URI. Several diagnostics are + // gated on the document path — e.g. SynapseExpressionValidator only runs for files under + // src/main/wso2mi/artifacts — so the literal "temp" fallback would silently drop them. + // Treat a blank fileName as missing, otherwise an unusable URI would skip those checks. + String uri = StringUtils.isBlank(param.getFileName()) ? "temp" : param.getFileName(); + // Opt-out (default off) for cross-file reference checks: the agent validates a file + // before its referenced siblings are written, so those checks would fire spuriously. + // Set/clear around doDiagnostics on this thread; the editor never sets it. + try { + SynapseDiagnosticsParticipant.setSkipCrossFileValidation(param.isSkipCrossFileValidation()); + DOMDocument xmlDocument = Utils.getDOMDocument(param.getCode(), uri, uriResolverExtensionManager); + return doDiagnostics(xmlDocument, NULL_CANCEL_CHECKER); + } finally { + SynapseDiagnosticsParticipant.clearSkipCrossFileValidation(); + } }); } @@ -1044,7 +1058,10 @@ public CompletableFuture generateConnector(Connector connectorGenReq.connectorProjectPath, projectServerVersion, projectUri); } } catch (Exception e) { - log.log(Level.SEVERE, "Error occurred while generating the connector", e); + String errorMsg = "Error occurred while generating the connector: " + e.getMessage(); + log.log(Level.SEVERE, errorMsg, e); + ConnectorGeneratorResponse errorResponse = new ConnectorGeneratorResponse(false, null, errorMsg); + return CompletableFuture.supplyAsync(() -> errorResponse); } ConnectorGeneratorResponse response = new ConnectorGeneratorResponse(filePath != null, filePath); return CompletableFuture.supplyAsync(() -> response); @@ -1202,6 +1219,12 @@ public CompletableFuture> resolveConnector }); } + @Override + public CompletableFuture fetchInboundConnectors() { + + return CompletableFuture.supplyAsync(() -> inboundConnectorHolder.getCustomInboundConnectors()); + } + public String getProjectUri() { return projectUri; } @@ -1233,7 +1256,8 @@ public void dispose() { private void packHttpConnector() { - if (Utils.compareVersions(projectServerVersion, Constant.MI_440_VERSION) >= 0) { + if (Utils.compareVersions(projectServerVersion, Constant.MI_440_VERSION) >= 0 + && Utils.hasDependency(projectUri, Constant.HTTP_CONNECTOR_ARTIFACT_ID)) { String projectId = new File(projectUri).getName() + "_" + Utils.getHash(projectUri); String connectorDownloadPath = Path.of(System.getProperty(Constant.USER_HOME), Constant.WSO2_MI, Constant.CONNECTORS, projectId, Constant.DOWNLOADED).toString(); diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/XMLTextDocumentService.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/XMLTextDocumentService.java index 4c742b6ab..380748726 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/XMLTextDocumentService.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/XMLTextDocumentService.java @@ -39,6 +39,7 @@ import org.eclipse.lemminx.dom.DOMDocument; import org.eclipse.lemminx.dom.DOMParser; import org.eclipse.lemminx.extensions.contentmodel.settings.XMLValidationRootSettings; +import org.eclipse.lemminx.extensions.synapse.SynapseDiagnosticsParticipant; import org.eclipse.lemminx.services.DocumentSymbolsResult; import org.eclipse.lemminx.services.SymbolInformationResult; import org.eclipse.lemminx.services.XMLLanguageService; @@ -599,7 +600,14 @@ public CompletableFuture> colorPresentation(ColorPresent public void didSave(DidSaveTextDocumentParams params) { computeAsync((monitor) -> { // A document was saved, collect documents to revalidate - SaveContext context = new SaveContext(params.getTextDocument().getUri()); + String savedUri = params.getTextDocument().getUri(); + // LSP document URIs use '/', but normalize defensively so a backslash path also matches on Windows. + if (savedUri != null && savedUri.replace('\\', '/').contains("src/main/wso2mi")) { + // An artifact/resource file was saved — drop the cached cross-file index so the + // revalidation below (and sibling files) sees the updated set instead of stale data. + SynapseDiagnosticsParticipant.invalidateArtifactIndexCache(); + } + SaveContext context = new SaveContext(savedUri); doSave(context); // Manage didSave document lifecycle participants diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/XMLWorkspaceService.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/XMLWorkspaceService.java index f866168c8..284c0fbea 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/XMLWorkspaceService.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/XMLWorkspaceService.java @@ -21,6 +21,7 @@ import org.eclipse.lemminx.commons.WorkspaceFolders; import org.eclipse.lemminx.customservice.synapse.utils.Constant; +import org.eclipse.lemminx.extensions.synapse.SynapseDiagnosticsParticipant; import org.eclipse.lemminx.services.extensions.commands.IXMLCommandService; import org.eclipse.lsp4j.DidChangeConfigurationParams; import org.eclipse.lsp4j.DidChangeWatchedFilesParams; @@ -94,11 +95,19 @@ public void didChangeWatchedFiles(DidChangeWatchedFilesParams params) { .getTextDocumentService(); List changes = params.getChanges(); for (FileEvent change : changes) { - if (change.getUri().contains(Constant.INBOUND_CONNECTORS_DIR) && change.getUri().contains(".zip")) { + if ((change.getUri().contains(Constant.INBOUND_ENDPOINTS) + || change.getUri().contains(Constant.INBOUND_CONNECTORS_DIR)) && change.getUri().contains(".zip")) { ((SynapseLanguageService) xmlLanguageServer.getSynapseLanguageService()).updateInboundConnectors(); } else if (change.getUri().contains(Constant.CONNECTORS) && change.getUri().contains(".zip")) { ((SynapseLanguageService) xmlLanguageServer.getSynapseLanguageService()).updateConnectors(); } else { + // LSP URIs use '/', but normalize defensively so a backslash path also matches on Windows. + if (change.getUri().replace('\\', '/').contains("src/main/wso2mi")) { + // An artifact/resource file changed on disk — drop the cached cross-file index so + // the next diagnostics run rebuilds it (otherwise a just-written sibling stays + // "unresolved" for up to the cache TTL). + SynapseDiagnosticsParticipant.invalidateArtifactIndexCache(); + } if (!xmlTextDocumentService.documentIsOpen(change.getUri())) { xmlTextDocumentService.doSave(change.getUri()); } diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/ISynapseLanguageService.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/ISynapseLanguageService.java index c8c8da214..72933ee1f 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/ISynapseLanguageService.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/ISynapseLanguageService.java @@ -359,4 +359,7 @@ CompletableFuture getDriverMavenCoordinates( @JsonRequest CompletableFuture> getInboundInfo(InboundInfoRequest request); + + @JsonRequest + CompletableFuture fetchInboundConnectors(); } diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/CodeDiagnosticRequest.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/CodeDiagnosticRequest.java index 029d7027a..189eddd65 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/CodeDiagnosticRequest.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/CodeDiagnosticRequest.java @@ -17,6 +17,8 @@ public class CodeDiagnosticRequest { private String code; + private String fileName; + private boolean skipCrossFileValidation; public String getCode() { @@ -27,4 +29,30 @@ public void setCode(String code) { this.code = code; } + + public String getFileName() { + + return fileName; + } + + public void setFileName(String fileName) { + + this.fileName = fileName; + } + + /** + * When true, cross-file reference checks (which depend on other artifact files existing) are + * skipped for this request. Defaults to false, so the editor and the explicit "validate all" + * path are unaffected. The MI Copilot agent sets it for per-file auto-validation after a write, + * where a referenced sibling artifact may not exist on disk yet. + */ + public boolean isSkipCrossFileValidation() { + + return skipCrossFileValidation; + } + + public void setSkipCrossFileValidation(boolean skipCrossFileValidation) { + + this.skipCrossFileValidation = skipCrossFileValidation; + } } diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/api/generator/APIGenerator.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/api/generator/APIGenerator.java index 4308d341e..2205cee4d 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/api/generator/APIGenerator.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/api/generator/APIGenerator.java @@ -16,6 +16,7 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import org.apache.commons.lang3.StringUtils; import org.eclipse.lemminx.customservice.synapse.syntaxTree.pojo.CommentMediator; import org.eclipse.lemminx.customservice.synapse.syntaxTree.pojo.api.API; import org.eclipse.lemminx.customservice.synapse.syntaxTree.pojo.api.APIResource; @@ -51,12 +52,99 @@ public class APIGenerator { private JsonObject swaggerJson; private String publishSwaggerPath; + private String context; + private String version; + private String versionType; private static final Logger log = Logger.getLogger(APIGenerator.class.getName()); public APIGenerator(JsonObject swaggerJson, String publishSwaggerPath) { + this(swaggerJson, publishSwaggerPath, null, null); + } + + public APIGenerator(JsonObject swaggerJson, String publishSwaggerPath, String context) { + + this(swaggerJson, publishSwaggerPath, context, null); + } + + public APIGenerator(JsonObject swaggerJson, String publishSwaggerPath, String context, String version) { + + this(swaggerJson, publishSwaggerPath, context, version, null); + } + + public APIGenerator(JsonObject swaggerJson, String publishSwaggerPath, String context, String version, + String versionType) { + this.swaggerJson = swaggerJson; this.publishSwaggerPath = publishSwaggerPath; + this.context = context; + this.version = version; + this.versionType = versionType; + } + + /** + * Normalize the optionally provided context so that it starts with '/' and has no trailing '/'. + * + * @return the normalized context, or null when no usable context was provided + */ + private String getNormalizedProvidedContext() { + + if (StringUtils.isBlank(context)) { + return null; + } + String resolved = context.trim(); + // remove trailing '/' + if (resolved.length() > 1 && resolved.endsWith("/")) { + resolved = resolved.substring(0, resolved.length() - 1); + } + // add leading '/' if not present + if (!resolved.startsWith("/")) { + resolved = "/" + resolved; + } + if (StringUtils.isEmpty(resolved) || "/".equals(resolved)) { + return null; + } + return resolved; + } + + /** + * Normalize the optionally provided version. + * + * @return the trimmed version, or null when no usable version was provided + */ + private String getNormalizedProvidedVersion() { + + if (StringUtils.isBlank(version)) { + return null; + } + return version.trim(); + } + + /** + * Normalize the optionally provided version type. + * + * @return the matching {@link ApiVersionType}, or null when no valid version type was provided + */ + private ApiVersionType getNormalizedProvidedVersionType() { + + if (StringUtils.isBlank(versionType)) { + return null; + } + try { + return ApiVersionType.valueOf(versionType.trim().toLowerCase()); + } catch (IllegalArgumentException e) { + return null; + } + } + + /** + * Check whether the caller explicitly asked for the API to be unversioned. + * + * @return true when the provided version type is "none" + */ + private boolean isProvidedVersionTypeNone() { + + return StringUtils.equalsIgnoreCase(SwaggerConstants.VERSION_TYPE_NONE, StringUtils.trim(versionType)); } public String generateSynapseAPIXml() { @@ -81,40 +169,43 @@ public String generateSynapseAPIXml() { */ public API generateSynapseAPI() throws APIGenException { - String apiContext; - if (swaggerJson.get(SwaggerConstants.SERVERS) == null || - swaggerJson.get(SwaggerConstants.SERVERS).getAsJsonArray().size() == 0) { - apiContext = SwaggerConstants.DEFAULT_CONTEXT; - } else { - JsonObject firstServer = swaggerJson.getAsJsonArray(SwaggerConstants.SERVERS).get(0).getAsJsonObject(); - // get the first path in the servers section - String serversString = firstServer.get(SwaggerConstants.URL).getAsString(); - if (serversString.contains("{") && serversString.contains("}")) { - // url is templated, need to resolve - if (firstServer.has(SwaggerConstants.VARIABLES)) { - JsonObject variables = firstServer.get(SwaggerConstants.VARIABLES).getAsJsonObject(); - serversString = replaceTemplates(serversString, variables); - } else { - throw new APIGenException("Server url is templated, but variables cannot be found"); - } - } - try { - URL url = new URL(serversString); - apiContext = url.getPath(); - } catch (MalformedURLException e) { - // url can be relative the place where the swagger is hosted. - apiContext = serversString; - } - if (apiContext.isEmpty() || "/".equals(apiContext)) { + // A provided context always takes priority over the one derived from the swagger servers section + String apiContext = getNormalizedProvidedContext(); + if (apiContext == null) { + if (swaggerJson.get(SwaggerConstants.SERVERS) == null || + swaggerJson.get(SwaggerConstants.SERVERS).getAsJsonArray().size() == 0) { apiContext = SwaggerConstants.DEFAULT_CONTEXT; - } - //cleanup context : remove ending '/' - if (apiContext.lastIndexOf('/') == (apiContext.length() - 1)) { - apiContext = apiContext.substring(0, apiContext.length() - 1); - } - // add leading / if not exists - if (!apiContext.startsWith("/")) { - apiContext = "/" + apiContext; + } else { + JsonObject firstServer = swaggerJson.getAsJsonArray(SwaggerConstants.SERVERS).get(0).getAsJsonObject(); + // get the first path in the servers section + String serversString = firstServer.get(SwaggerConstants.URL).getAsString(); + if (serversString.contains("{") && serversString.contains("}")) { + // url is templated, need to resolve + if (firstServer.has(SwaggerConstants.VARIABLES)) { + JsonObject variables = firstServer.get(SwaggerConstants.VARIABLES).getAsJsonObject(); + serversString = replaceTemplates(serversString, variables); + } else { + throw new APIGenException("Server url is templated, but variables cannot be found"); + } + } + try { + URL url = new URL(serversString); + apiContext = url.getPath(); + } catch (MalformedURLException e) { + // url can be relative the place where the swagger is hosted. + apiContext = serversString; + } + if (StringUtils.isEmpty(apiContext) || "/".equals(apiContext)) { + apiContext = SwaggerConstants.DEFAULT_CONTEXT; + } + //cleanup context : remove ending '/' + if (apiContext.lastIndexOf('/') == (apiContext.length() - 1)) { + apiContext = apiContext.substring(0, apiContext.length() - 1); + } + // add leading / if not exists + if (!apiContext.startsWith("/")) { + apiContext = "/" + apiContext; + } } } @@ -129,23 +220,29 @@ public API generateSynapseAPI() throws APIGenException { String apiName = swaggerInfo.get(SwaggerConstants.TITLE).getAsString(); - // Extract version information - ApiVersionType versionType = null; - String version = ""; - JsonElement swaggerVersionElement = swaggerInfo.get(SwaggerConstants.VERSION); - if (swaggerVersionElement != null && swaggerVersionElement.isJsonPrimitive() && - swaggerVersionElement.getAsJsonPrimitive().isString()) { - version = swaggerVersionElement.getAsString(); - if (apiContext.endsWith(version)) { - // If the base path ends with the version, then it will be considered as version-type=url - versionType = ApiVersionType.url; - //cleanup api context path : remove version from base path - apiContext = apiContext.substring(0, apiContext.length() - version.length() - 1); - } else { - // otherwise context based version strategy - versionType = ApiVersionType.context; + // Extract version information. A user provided version always takes priority over the values derived from the swagger info section. + String providedVersion = getNormalizedProvidedVersion(); + ApiVersionType versionType = getNormalizedProvidedVersionType(); + boolean versionTypeNone = isProvidedVersionTypeNone(); + String swaggerVersion = null; + if (!versionTypeNone) { + JsonElement swaggerVersionElement = swaggerInfo.get(SwaggerConstants.VERSION); + if (swaggerVersionElement != null && swaggerVersionElement.isJsonPrimitive() && + swaggerVersionElement.getAsJsonPrimitive().isString()) { + swaggerVersion = swaggerVersionElement.getAsString(); } } + String version = versionTypeNone ? StringUtils.EMPTY : + (StringUtils.isNotBlank(providedVersion) ? providedVersion : + (StringUtils.isNotBlank(swaggerVersion) ? swaggerVersion : StringUtils.EMPTY)); + + if (ApiVersionType.url.equals(versionType)) { + String versionInPath = StringUtils.isNotBlank(swaggerVersion) ? swaggerVersion : version; + if (StringUtils.isNotEmpty(versionInPath) && apiContext.endsWith(versionInPath)) { + // remove version from base path + apiContext = apiContext.substring(0, apiContext.length() - versionInPath.length() - 1); + } + } // Create API API genAPI = new API(); diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/api/generator/GenericApiObjectDefinition.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/api/generator/GenericApiObjectDefinition.java index e722a2b23..a1d624fdd 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/api/generator/GenericApiObjectDefinition.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/api/generator/GenericApiObjectDefinition.java @@ -116,11 +116,10 @@ private static Object[] getResourceParameters(APIResource resource, String metho if (resource.getUrlMapping() != null) { uri = StringEscapeUtils.unescapeHtml4(resource.getUrlMapping()); - generateParameterList(parameterList, uri, false); } else { uri = StringEscapeUtils.unescapeHtml4(resource.getUriTemplate()); - generateParameterList(parameterList, uri, true); } + generateParameterList(parameterList, uri); if (log.isLoggable(Level.FINE)) { log.info("Parameters processed for the URI + " + uri + " size " + parameterList.size()); } @@ -131,31 +130,16 @@ private static Object[] getResourceParameters(APIResource resource, String metho } /** - * Generate URI and Path parameters for the given URI. + * Generate Path parameters for the given URI. * - * @param parameterList List of maps to be populated with parameters - * @param uriString URI string to be used to extract parameters - * @param generateBothTypes Indicates whether to consider both query and uri parameters. True if both to be - * considered. + * @param parameterList List of maps to be populated with parameters + * @param uriString URI string to be used to extract parameters */ - private static void generateParameterList(ArrayList> parameterList, String uriString, boolean - generateBothTypes) { + private static void generateParameterList(ArrayList> parameterList, String uriString) { if (uriString == null) { return; } - if (generateBothTypes) { - String[] params = getQueryStringFromUrl(uriString).split("&"); - for (String parameter : params) { - if (parameter != null) { - int pos = parameter.indexOf('='); - if (pos > 0) { - parameterList.add(getParametersMap(parameter.substring(0, pos), - SwaggerConstants.PARAMETER_IN_QUERY)); - } - } - } - } Matcher matcher = SwaggerConstants.PATH_PARAMETER_PATTERN.matcher(getPathFromUrl(uriString)); while (matcher.find()) { parameterList.add(getParametersMap(matcher.group(1), SwaggerConstants.PARAMETER_IN_PATH)); @@ -237,21 +221,6 @@ private static String getPathFromUrl(String uri) { return uri; } - /** - * Get query parameter portion from the URI. - * - * @param uri String URI to be analysed - * @return String containing the URI parameter portion of the URI - */ - private static String getQueryStringFromUrl(String uri) { - - int pos = uri.indexOf("?"); - if (pos > 0) { - return uri.substring(pos + 1); - } - return ""; - } - /** * A util method to convert from YAML to JSON. * diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/api/generator/RestApiAdmin.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/api/generator/RestApiAdmin.java index 805fd5980..4528d945d 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/api/generator/RestApiAdmin.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/api/generator/RestApiAdmin.java @@ -103,8 +103,11 @@ public GenerateAPIResponse createAPI(GenerateAPIParam param) { String endpoint = param.wsdlEndpointName; String publishSwaggerPath = param.publishSwaggerPath; String mode = param.mode; + String context = param.context; + String version = param.version; + String versionType = param.versionType; - return createAPI(apiName, sourcePath, endpoint, publishSwaggerPath, mode); + return createAPI(apiName, sourcePath, endpoint, publishSwaggerPath, mode, context, version, versionType); } /** @@ -115,52 +118,62 @@ public GenerateAPIResponse createAPI(GenerateAPIParam param) { * @param endpoint WSDL endpoint * @param publishSwaggerPath Swagger publish path * @param mode Mode of the API creation (Swagger / WSDL) + * @param context API context, takes priority over the context derived from the swagger + * @param version API version, takes priority over the version derived from the swagger + * @param versionType API version type (url / context), takes priority over the version type + * derived from the swagger * @return */ public GenerateAPIResponse createAPI(String apiName, String sourcePath, String endpoint, String publishSwaggerPath - , String mode) { + , String mode, String context, String version, String versionType) { if (CREATE_FROM_SWAGGER.equalsIgnoreCase(mode)) { try { - return createAPIFromSwagger(apiName, sourcePath, publishSwaggerPath); + return createAPIFromSwagger(apiName, sourcePath, publishSwaggerPath, context, version, versionType); } catch (JsonProcessingException e) { LOGGER.log(Level.SEVERE, "Exception occurred while creating API from Swagger", e); - return null; + return new GenerateAPIResponse(null, null, + "Exception occurred while creating API from Swagger: " + e.getMessage()); } } else if (CREATE_FROM_WSDL.equalsIgnoreCase(mode)) { try { - return createAPIFromWSDL(apiName, endpoint, sourcePath); + return createAPIFromWSDL(apiName, endpoint, sourcePath, context, version, versionType); } catch (SOAPToRESTException e) { LOGGER.log(Level.SEVERE, "Exception occurred while converting SOAP to REST", e); + return new GenerateAPIResponse(null, null, + "Exception occurred while converting SOAP to REST: " + e.getMessage()); } catch (MalformedURLException e) { LOGGER.log(Level.SEVERE, "Invalid WSDL URL", e); + return new GenerateAPIResponse(null, null, "Invalid WSDL URL: " + e.getMessage()); } catch (Exception e) { LOGGER.log(Level.SEVERE, "Exception occurred while creating API from WSDL", e); + return new GenerateAPIResponse(null, null, + "Exception occurred while creating API from WSDL: " + e.getMessage()); } } return null; } - private GenerateAPIResponse createAPIFromSwagger(String apiName, String swaggerPath, String publishSwaggerPath) throws JsonProcessingException { + private GenerateAPIResponse createAPIFromSwagger(String apiName, String swaggerPath, String publishSwaggerPath, String context, String version, String versionType) throws JsonProcessingException { File swaggerFile = new File(swaggerPath); String swaggerYaml = getSwaggerFileAsYAML(swaggerFile, apiName); - String api = getSynapseAPIFromSwagger(swaggerYaml, publishSwaggerPath); + String api = getSynapseAPIFromSwagger(swaggerYaml, publishSwaggerPath, context, version, versionType); return new GenerateAPIResponse(api); } - private String getSynapseAPIFromSwagger(String swaggerYaml, String publishSwaggerPath) throws JsonProcessingException { + private String getSynapseAPIFromSwagger(String swaggerYaml, String publishSwaggerPath, String context, String version, String versionType) throws JsonProcessingException { String swaggerString = GenericApiObjectDefinition.convertYamlToJson(swaggerYaml); JsonParser jsonParser = new JsonParser(); JsonElement swaggerJson = jsonParser.parse(swaggerString); - APIGenerator apiGenerator = new APIGenerator(swaggerJson.getAsJsonObject(), publishSwaggerPath); + APIGenerator apiGenerator = new APIGenerator(swaggerJson.getAsJsonObject(), publishSwaggerPath, context, version, versionType); String apiXml = apiGenerator.generateSynapseAPIXml(); return apiXml; } - private GenerateAPIResponse createAPIFromWSDL(String apiName, String endpoint, String sourcePath) throws SOAPToRESTException, MalformedURLException, JsonProcessingException, TransformerException { + private GenerateAPIResponse createAPIFromWSDL(String apiName, String endpoint, String sourcePath, String context, String version, String versionType) throws SOAPToRESTException, MalformedURLException, JsonProcessingException, TransformerException { URL url = new URL(sourcePath); SOAPtoRESTConversionData soaPtoRESTConversionData = SOAPToRESTConverter.getSOAPtoRESTConversionData(url, @@ -185,7 +198,7 @@ private GenerateAPIResponse createAPIFromWSDL(String apiName, String endpoint, S wsdlEndpoint.setWsdl(wsdlEndpointData); String swaggerYaml = soaPtoRESTConversionData.getOASString(); - String apiXml = getSynapseAPIFromSwagger(swaggerYaml, null); + String apiXml = getSynapseAPIFromSwagger(swaggerYaml, null, context, version, versionType); APIFactory apiFactory = new APIFactory(); API api = (API) apiFactory.create(Utils.getDOMDocument(apiXml).getDocumentElement()); diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/api/generator/SwaggerConstants.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/api/generator/SwaggerConstants.java index 0dbf90d0e..d80bad128 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/api/generator/SwaggerConstants.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/api/generator/SwaggerConstants.java @@ -190,4 +190,9 @@ public class SwaggerConstants { * Path param normaized placeholder */ public static final String NORMALIZED_PLACEHOLDER = "{}"; + + /** + * Version type value indicating that the API should be unversioned + */ + static final String VERSION_TYPE_NONE = "none"; } diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/api/generator/pojo/GenerateAPIParam.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/api/generator/pojo/GenerateAPIParam.java index 010659a5a..fc153bde7 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/api/generator/pojo/GenerateAPIParam.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/api/generator/pojo/GenerateAPIParam.java @@ -21,4 +21,7 @@ public class GenerateAPIParam { public String wsdlEndpointName; public String publishSwaggerPath; public String mode; + public String context; + public String version; + public String versionType; } diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/api/generator/pojo/GenerateAPIResponse.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/api/generator/pojo/GenerateAPIResponse.java index 1e95d1590..721dccd18 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/api/generator/pojo/GenerateAPIResponse.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/api/generator/pojo/GenerateAPIResponse.java @@ -18,6 +18,7 @@ public class GenerateAPIResponse { public String apiXml; public String endpointXml; + public String error; private final String XML_TAG = "\n"; public GenerateAPIResponse(String apiXml) { @@ -30,4 +31,11 @@ public GenerateAPIResponse(String apiXml, String endpointXml) { this.apiXml = XML_TAG + apiXml; this.endpointXml = XML_TAG + endpointXml; } + + public GenerateAPIResponse(String apiXml, String endpointXml, String error) { + + this.apiXml = apiXml != null ? XML_TAG + apiXml : null; + this.endpointXml = endpointXml != null ? XML_TAG + endpointXml : null; + this.error = error; + } } diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/connectors/AbstractConnectorLoader.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/connectors/AbstractConnectorLoader.java index 811c3190b..07c648405 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/connectors/AbstractConnectorLoader.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/connectors/AbstractConnectorLoader.java @@ -14,6 +14,7 @@ package org.eclipse.lemminx.customservice.synapse.connectors; +import com.google.gson.JsonObject; import org.eclipse.lemminx.customservice.SynapseLanguageClientAPI; import org.eclipse.lemminx.customservice.synapse.ConnectorStatusNotification; import org.eclipse.lemminx.customservice.synapse.connectors.entity.Connector; @@ -160,8 +161,15 @@ private void extractZips(List connectorZips, File extractFolder) { if (zipName.contains(INBOUND_CONNECTOR_PREFIX)) { String schema = Utils.readFile(extractToFolder.toPath().resolve(Constant.RESOURCES) .resolve(Constant.UI_SCHEMA_JSON).toFile()); - inboundConnectorHolder.saveInboundConnector(Utils.getJsonObject(schema) - .get(Constant.NAME).getAsString(), schema); + JsonObject uiSchemaJson = Utils.getJsonObject(schema); + String connectorName = uiSchemaJson.get(Constant.NAME).getAsString(); + inboundConnectorHolder.saveInboundConnector(connectorName, schema); + File inputSchemaFile = extractToFolder.toPath().resolve(Constant.RESOURCES) + .resolve(Constant.INPUT_SCHEMA_JSON).toFile(); + if (inputSchemaFile.exists() && uiSchemaJson.has(Constant.ID)) { + inboundConnectorHolder.saveInboundConnectorInputSchema(connectorName, + uiSchemaJson.get(Constant.ID).getAsString(), Utils.readFile(inputSchemaFile)); + } } } catch (IOException e) { log.log(Level.WARNING, "Failed to extract connector zip:" + zipName, e); diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/connectors/NewProjectConnectorLoader.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/connectors/NewProjectConnectorLoader.java index 6a169eb59..97f0fa526 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/connectors/NewProjectConnectorLoader.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/connectors/NewProjectConnectorLoader.java @@ -119,11 +119,19 @@ protected void cleanOldConnectors(File connectorExtractFolder, List connec if (connectorFolder.getName().contains(Constant.INBOUND_CONNECTOR_PREFIX) ) { String schema = Utils.readFile(connectorFolder.toPath().resolve(Constant.RESOURCES) .resolve(Constant.UI_SCHEMA_JSON).toFile()); - String fileName = Utils.getJsonObject(schema).get(Constant.NAME).getAsString() + Constant.JSON_FILE_EXT; + String inboundName = Utils.getJsonObject(schema).get(Constant.NAME).getAsString(); String projectFolderName = connectorExtractFolder.getParentFile().getName(); - File schemaToRemove = Path.of(getUserHome(), Constant.WSO2_MI, - Constant.INBOUND_CONNECTORS).resolve(projectFolderName).resolve(fileName).toFile(); - FileUtils.delete(schemaToRemove); + Path inboundCacheDir = Path.of(getUserHome(), Constant.WSO2_MI, + Constant.INBOUND_CONNECTORS).resolve(projectFolderName); + File schemaToRemove = inboundCacheDir.resolve(inboundName + Constant.JSON_FILE_EXT).toFile(); + if (schemaToRemove.exists()) { + FileUtils.delete(schemaToRemove); + } + File inputSchemaToRemove = inboundCacheDir + .resolve(inboundName + InboundConnectorHolder.INPUT_SCHEMA_FILE_SUFFIX).toFile(); + if (inputSchemaToRemove.exists()) { + FileUtils.delete(inputSchemaToRemove); + } } FileUtils.deleteDirectory(connectorFolder); notifyRemoveConnector(connectorName, true, "Connector deleted successfully"); diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/connectors/SchemaGenerate.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/connectors/SchemaGenerate.java index 06d32ca88..4cf2b5ed4 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/connectors/SchemaGenerate.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/connectors/SchemaGenerate.java @@ -108,6 +108,7 @@ private static String getConnectorSchema(ConnectorHolder holder) { } sb.append(" \n"); sb.append(" \n"); + sb.append(" \n"); sb.append(" \n" + " \n"); } diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/connectors/entity/ConnectorAction.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/connectors/entity/ConnectorAction.java index c9246be1d..858f988fa 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/connectors/entity/ConnectorAction.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/connectors/entity/ConnectorAction.java @@ -14,21 +14,15 @@ package org.eclipse.lemminx.customservice.synapse.connectors.entity; -import com.google.gson.JsonElement; import com.google.gson.JsonObject; -import org.apache.commons.lang3.StringUtils; import org.eclipse.lemminx.customservice.synapse.mediator.tryout.pojo.Property; -import org.eclipse.lemminx.customservice.synapse.utils.Constant; import org.eclipse.lemminx.customservice.synapse.utils.Utils; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; -import java.util.HashSet; import java.util.List; -import java.util.Map; -import java.util.Set; public class ConnectorAction { @@ -151,91 +145,10 @@ private void loadOutputSchema() throws IOException { String outputSchemaString = Utils.readFile(new File(outputSchemaPath)); JsonObject outputSchemaJson = Utils.getJsonObject(outputSchemaString); if (outputSchemaJson != null) { - outputSchema = createSchemaObject(outputSchemaJson); + outputSchema = ConnectorVariableSchemaUtils.buildSchemaProperty(outputSchemaJson); } } - private Property createSchemaObject(JsonObject outputSchemaJson) { - JsonObject properties = outputSchemaJson.getAsJsonObject(Constant.PROPERTIES); - if (properties == null) { - return null; - } - Property outputSchemaObject = new Property("root", StringUtils.EMPTY); - // Store definitions for reference resolution - JsonObject definitions = outputSchemaJson.getAsJsonObject(Constant.DEFINITIONS); - List propertiesList = extractProperties(properties, definitions, new HashSet<>()); - outputSchemaObject.setProperties(propertiesList); - return outputSchemaObject; - } - - private List extractProperties(JsonObject propertiesObject, JsonObject definitions, Set processedRefs) { - List propertiesList = new ArrayList<>(); - for (Map.Entry entry : propertiesObject.entrySet()) { - String key = entry.getKey(); - JsonElement value = entry.getValue(); - if (value.isJsonObject()) { - JsonObject propertyObject = value.getAsJsonObject(); - - // Check if this is a reference to a definition - if (propertyObject.has(Constant.REF)) { - String ref = propertyObject.get(Constant.REF).getAsString(); - // Handle only definitions references (#/definitions/...) - if (ref.startsWith(Constant.SCHEMA_DEFINITION) && definitions != null) { - String definitionKey = ref.substring(Constant.SCHEMA_DEFINITION.length()); - - // Prevent circular references - if (!processedRefs.contains(definitionKey)) { - processedRefs.add(definitionKey); - - JsonObject definitionObj = definitions.getAsJsonObject(definitionKey); - if (definitionObj != null) { - // Create property with the key from the property name - Property property = new Property(key, StringUtils.EMPTY); - - // Get description from the definition if available - if (definitionObj.has(Constant.DESCRIPTION)) { - property.setDescription(definitionObj.get(Constant.DESCRIPTION).getAsString()); - } - - // Extract nested properties from the definition - if (definitionObj.has(Constant.PROPERTIES)) { - List nestedProps = extractProperties( - definitionObj.getAsJsonObject(Constant.PROPERTIES), - definitions, - new HashSet<>(processedRefs) - ); - property.setProperties(nestedProps); - } - - propertiesList.add(property); - } - } - continue; - } - } - - // Process regular properties (non-reference) - JsonElement propDescriptionObj = propertyObject.get(Constant.DESCRIPTION); - String propDescription = propDescriptionObj != null ? - propDescriptionObj.getAsString() : StringUtils.EMPTY; - - Property property = new Property(key, StringUtils.EMPTY, propDescription); - - if (propertyObject.has(Constant.PROPERTIES)) { - List properties = extractProperties( - propertyObject.getAsJsonObject(Constant.PROPERTIES), - definitions, - new HashSet<>(processedRefs) - ); - property.setProperties(properties); - } - - propertiesList.add(property); - } - } - return propertiesList; - } - public Property getOutputSchema() { if (outputSchema == null) { diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/connectors/entity/ConnectorVariableSchemaUtils.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/connectors/entity/ConnectorVariableSchemaUtils.java new file mode 100644 index 000000000..d507c95a2 --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/connectors/entity/ConnectorVariableSchemaUtils.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com). + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * WSO2 LLC - support for WSO2 Micro Integrator Configuration + */ + +package org.eclipse.lemminx.customservice.synapse.connectors.entity; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import org.apache.commons.lang3.StringUtils; +import org.eclipse.lemminx.customservice.synapse.mediator.tryout.pojo.Property; +import org.eclipse.lemminx.customservice.synapse.utils.Constant; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Utility for converting a JSON schema (the {@code outputschema.json} shipped by + * connectors and inbound connectors) into the {@link Property} tree consumed by the + * mediator tryout system. + */ +public class ConnectorVariableSchemaUtils { + + private ConnectorVariableSchemaUtils() { + + } + + /** + * Builds a {@link Property} tree rooted at "root" from the given output schema + * JSON object. Returns {@code null} if the schema has no {@code properties}. + */ + public static Property buildSchemaProperty(JsonObject outputSchemaJson) { + + if (outputSchemaJson == null) { + return null; + } + JsonObject properties = outputSchemaJson.getAsJsonObject(Constant.PROPERTIES); + if (properties == null) { + return null; + } + Property outputSchemaObject = new Property("root", StringUtils.EMPTY); + // Store definitions for reference resolution + JsonObject definitions = outputSchemaJson.getAsJsonObject(Constant.DEFINITIONS); + List propertiesList = extractProperties(properties, definitions, new HashSet<>()); + outputSchemaObject.setProperties(propertiesList); + return outputSchemaObject; + } + + private static List extractProperties(JsonObject propertiesObject, JsonObject definitions, + Set processedRefs) { + List propertiesList = new ArrayList<>(); + for (Map.Entry entry : propertiesObject.entrySet()) { + String key = entry.getKey(); + JsonElement value = entry.getValue(); + if (value.isJsonObject()) { + JsonObject propertyObject = value.getAsJsonObject(); + + // Check if this is a reference to a definition + if (propertyObject.has(Constant.REF)) { + String ref = propertyObject.get(Constant.REF).getAsString(); + // Handle only definitions references (#/definitions/...) + if (ref.startsWith(Constant.SCHEMA_DEFINITION) && definitions != null) { + String definitionKey = ref.substring(Constant.SCHEMA_DEFINITION.length()); + + // Prevent circular references. Keep processedRefs path-scoped: do not mutate the + // shared set, so sibling properties can reuse the same definition. + if (!processedRefs.contains(definitionKey)) { + JsonObject definitionObj = definitions.getAsJsonObject(definitionKey); + if (definitionObj != null) { + // Create property with the key from the property name + Property property = new Property(key, StringUtils.EMPTY); + + // Get description from the definition if available + if (definitionObj.has(Constant.DESCRIPTION)) { + property.setDescription(definitionObj.get(Constant.DESCRIPTION).getAsString()); + } + + // Extract nested properties from the definition + if (definitionObj.has(Constant.PROPERTIES)) { + Set nestedRefs = new HashSet<>(processedRefs); + nestedRefs.add(definitionKey); + List nestedProps = extractProperties( + definitionObj.getAsJsonObject(Constant.PROPERTIES), + definitions, + nestedRefs + ); + property.setProperties(nestedProps); + } + + propertiesList.add(property); + } + } + continue; + } + } + + // Process regular properties (non-reference) + JsonElement propDescriptionObj = propertyObject.get(Constant.DESCRIPTION); + String propDescription = propDescriptionObj != null ? + propDescriptionObj.getAsString() : StringUtils.EMPTY; + + Property property = new Property(key, StringUtils.EMPTY, propDescription); + + if (propertyObject.has(Constant.PROPERTIES)) { + List properties = extractProperties( + propertyObject.getAsJsonObject(Constant.PROPERTIES), + definitions, + new HashSet<>(processedRefs) + ); + property.setProperties(properties); + } + + propertiesList.add(property); + } + } + return propertiesList; + } +} diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/connectors/generate/ConnectorGeneratorResponse.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/connectors/generate/ConnectorGeneratorResponse.java index ac8e63754..67af55bd3 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/connectors/generate/ConnectorGeneratorResponse.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/connectors/generate/ConnectorGeneratorResponse.java @@ -18,10 +18,18 @@ public class ConnectorGeneratorResponse { public boolean buildStatus; public String connectorPath; + public String errorMessage; public ConnectorGeneratorResponse(boolean buildStatus, String connectorPath) { this.buildStatus = buildStatus; this.connectorPath = connectorPath; } + + public ConnectorGeneratorResponse(boolean buildStatus, String connectorPath, String errorMessage) { + + this.buildStatus = buildStatus; + this.connectorPath = connectorPath; + this.errorMessage = errorMessage; + } } diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/dependency/tree/OverviewModelGenerator.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/dependency/tree/OverviewModelGenerator.java index 406ab1eb1..19f575c37 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/dependency/tree/OverviewModelGenerator.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/dependency/tree/OverviewModelGenerator.java @@ -55,8 +55,12 @@ public static OverviewModel getOverviewModel(String projectPath) { NewProjectResourceFinder newProjectResourceFinder = new NewProjectResourceFinder(); ResourceResponse response = newProjectResourceFinder.getAvailableResources(projectPath, Either.forRight(requiredResources)); for (Resource resource : response.getResources()) { + if (((ArtifactResource) resource).isMcpInbound()) { + continue; + } + String absolutePath = ((ArtifactResource) resource).getAbsolutePath(); DependencyScanner dependencyScanner = new DependencyScanner(projectPath); - DependencyTree dependencyTree = dependencyScanner.analyzeArtifact(((ArtifactResource) resource).getAbsolutePath()); + DependencyTree dependencyTree = dependencyScanner.analyzeArtifact(absolutePath); dependencyTreeList.add(dependencyTree); } return convertDataToOverviewModel(Paths.get(projectPath).getFileName().toString(), dependencyTreeList); diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/directoryTree/DirectoryMapResponse.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/directoryTree/DirectoryMapResponse.java index 8e707d589..ccaae0846 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/directoryTree/DirectoryMapResponse.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/directoryTree/DirectoryMapResponse.java @@ -70,7 +70,7 @@ private JsonElement convertFormat(JsonElement jsonTree) { String[] artifactNames = {"apis", "endpoints", "sequences", "proxyServices", "inboundEndpoints", "messageStores", "messageProcessors", "tasks", "localEntries", "connections", "templates", - "dataServices", "dataSources"}; + "dataServices", "dataSources", "mcpServers"}; processLocalEntries(jsonObject); for (String element : artifactNames) { artifacts.add(element, jsonObject.get(element)); diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/directoryTree/DirectoryTreeBuilder.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/directoryTree/DirectoryTreeBuilder.java index ab3ea82c4..04b23b0a6 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/directoryTree/DirectoryTreeBuilder.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/directoryTree/DirectoryTreeBuilder.java @@ -19,6 +19,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import com.google.gson.JsonParser; import org.eclipse.lemminx.customservice.synapse.directoryTree.node.APINode; import org.eclipse.lemminx.customservice.synapse.directoryTree.node.APIResource; @@ -55,7 +58,9 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.function.Consumer; import java.util.logging.Level; import java.util.logging.Logger; @@ -69,6 +74,8 @@ public class DirectoryTreeBuilder { private static final String WSO2MI = "wso2mi"; private static final String RESOURCES = "resources"; private static final String JAVA = "java"; + private static final String MCP_SERVERS_SECTION = "MCP Servers"; + private static final String MCP_SERVERS_KEY = "mcpServers"; private static String projectPath; private static String mainSequence; private static List artifactResourcePaths = new ArrayList<>(); @@ -104,9 +111,37 @@ public static DirectoryMapResponse buildDirectoryTree(WorkspaceFolder projectFol } DirectoryMapResponse directoryMapResponse = new DirectoryMapResponse(directoryTree); + if (directoryTree instanceof IntegrationDirectoryTree) { + applyMcpClassification(directoryMapResponse); + } return directoryMapResponse; } + /** + * Classifies MCP server artifacts in the raw directory tree response. + */ + private static void applyMcpClassification(DirectoryMapResponse response) { + + if (response.getDirectoryMap() == null) return; + JsonObject root = response.getDirectoryMap().getAsJsonObject(); + JsonObject src = root.getAsJsonObject(Constant.SRC); + if (src == null) return; + + JsonObject main = src.getAsJsonObject(MAIN); + if (main == null) return; + + JsonObject wso2mi = main.getAsJsonObject(WSO2MI); + if (wso2mi == null) return; + + JsonObject artifacts = wso2mi.getAsJsonObject(Constant.ARTIFACTS); + if (artifacts == null) return; + + JsonArray[] result = extractMcpServers(artifacts); + artifacts.add(Constant.INBOUNDENDPOINTS, result[1]); + artifacts.add(Constant.LOCALENTRIES, result[2]); + artifacts.add(MCP_SERVERS_KEY, result[0]); + } + /** * Generate model for the project explorer * @@ -128,10 +163,18 @@ public static DirectoryMapResponse getProjectExplorerModel(WorkspaceFolder proje JsonNode resources = root.path(Constant.SRC).path(MAIN).path(WSO2MI).path(Constant.RESOURCES); ObjectNode newArtifacts = mapper.createObjectNode(); + ArrayNode mcpServersArray = artifacts.path(MCP_SERVERS_KEY).isArray() + ? (ArrayNode) artifacts.path(MCP_SERVERS_KEY) : mapper.createArrayNode(); + ArrayNode filteredInboundEndpoints = artifacts.path(Constant.INBOUNDENDPOINTS).isArray() + ? (ArrayNode) artifacts.path(Constant.INBOUNDENDPOINTS) : mapper.createArrayNode(); + ArrayNode filteredLocalEntries = artifacts.path(Constant.LOCALENTRIES).isArray() + ? (ArrayNode) artifacts.path(Constant.LOCALENTRIES) : mapper.createArrayNode(); + newArtifacts.set("APIs", artifacts.path(Constant.APIS)); - newArtifacts.set("Event Integrations", artifacts.path(Constant.INBOUNDENDPOINTS)); + newArtifacts.set("Event Integrations", filteredInboundEndpoints); newArtifacts.set("Automations", artifacts.path(Constant.TASKS)); newArtifacts.set("Data Services", artifacts.path(Constant.DATA_SERVICES)); + newArtifacts.set(MCP_SERVERS_SECTION, mcpServersArray); ObjectNode otherArtifacts = newArtifacts.putObject("Other Artifacts"); otherArtifacts.set("Sequences", artifacts.path(Constant.SEQUENCES)); @@ -152,7 +195,7 @@ public static DirectoryMapResponse getProjectExplorerModel(WorkspaceFolder proje otherArtifacts.set("Proxy Services", artifacts.path(Constant.PROXYSERVICES)); otherArtifacts.set("Message Stores", artifacts.path(Constant.MESSAGE_STORES)); otherArtifacts.set("Message Processors", artifacts.path(Constant.MESSAGE_PROCESSORS)); - otherArtifacts.set("Local Entries", artifacts.path(Constant.LOCALENTRIES)); + otherArtifacts.set("Local Entries", filteredLocalEntries); otherArtifacts.set("Templates", artifacts.path(Constant.TEMPLATES)); JsonNode registryFolders = root.path(Constant.SRC).path(MAIN).path(WSO2MI).path(Constant.RESOURCES) @@ -387,6 +430,7 @@ private static void analyzeResources(IntegrationDirectoryTree directoryTree) { analyzeRegistryResources(directoryTree); analyzeConnectorResources(directoryTree); + analyzeInboundConnectorResources(directoryTree); analyzeMetadataResources(directoryTree); analyzeNewResources(directoryTree); } @@ -450,6 +494,26 @@ private static void analyzeConnectorResources(IntegrationDirectoryTree directory } } + private static void analyzeInboundConnectorResources(IntegrationDirectoryTree directoryTree) { + + String resourcesPath = projectPath + File.separator + Constant.SRC + File.separator + MAIN + + File.separator + WSO2MI + File.separator + RESOURCES + File.separator; + for (String dirName : new String[]{Constant.INBOUND_ENDPOINTS, Constant.INBOUND_CONNECTORS_DIR}) { + File folder = new File(resourcesPath + dirName); + File[] listOfFiles = folder.listFiles(); + if (listOfFiles != null) { + for (File file : listOfFiles) { + if (Utils.isZipFile(file) && !file.isHidden()) { + String name = file.getName(); + String path = file.getAbsolutePath(); + Node resource = new Node("inboundConnector", name, path); + directoryTree.getResources().addInboundConnector(resource); + } + } + } + } + } + private static void analyzeMetadataResources(IntegrationDirectoryTree directoryTree) { String metadataPath = projectPath + File.separator + Constant.SRC + File.separator + MAIN + @@ -690,6 +754,12 @@ private static AdvancedNode createAdvancedEsbComponent(Node component, String ty if (Constant.API.equalsIgnoreCase(type)) { addResources(rootElement, advancedNode); } + if (Constant.INBOUND_ENDPOINT.equalsIgnoreCase(type) && Utils.isMcpInboundEndpoint(rootElement)) { + String mcpConfigRef = getMcpConfigReference(rootElement); + if (mcpConfigRef != null) { + advancedNode.setMcpConfigReference(mcpConfigRef); + } + } } return advancedNode; } @@ -702,6 +772,12 @@ private static Node createLocalEntry(Node component, String path) { if (domDocument != null) { DOMElement rootElement = domDocument.getDocumentElement(); String key = rootElement.getAttribute(Constant.KEY); + + if (isMcpConfig(rootElement)) { + component.setIsMcpConfig(true); + return component; + } + DOMElement childElement = Utils.getFirstElement(rootElement); if (childElement != null) { String entryTag = childElement.getNodeName(); @@ -736,6 +812,39 @@ private static String getConnectionType(DOMElement element) { return null; } + private static boolean isMcpConfig(DOMElement rootElement) { + + List children = rootElement.getChildren(); + if (children != null) { + for (DOMNode child : children) { + if ("mcptools".equals(child.getNodeName())) { + return true; + } + } + } + return false; + } + + private static String getMcpConfigReference(DOMElement inboundElement) { + + DOMNode parametersNode = Utils.getChildNodeByName(inboundElement, "parameters"); + if (parametersNode != null) { + List children = parametersNode.getChildren(); + if (children != null) { + for (DOMNode child : children) { + if ("parameter".equals(child.getNodeName())) { + String paramName = ((DOMElement) child).getAttribute("name"); + if ("mcp.tools.localentry".equals(paramName)) { + String paramValue = Utils.getInlineString(child.getFirstChild()); + return paramValue; + } + } + } + } + } + return null; + } + private static String getApiContext(String path) { File file = new File(path); @@ -822,6 +931,85 @@ private static void addResources(DOMElement rootElement, AdvancedNode advancedNo } } + /** + * Separates MCP server artifacts from the regular inbound endpoints and local entries. + */ + private static JsonArray[] extractMcpServers(JsonObject artifacts) { + + JsonElement inboundEndpointsElem = artifacts.get(Constant.INBOUNDENDPOINTS); + JsonElement localEntriesElem = artifacts.get(Constant.LOCALENTRIES); + + JsonArray inboundEndpointsNode = (inboundEndpointsElem != null && inboundEndpointsElem.isJsonArray()) + ? inboundEndpointsElem.getAsJsonArray() : new JsonArray(); + JsonArray localEntriesNode = (localEntriesElem != null && localEntriesElem.isJsonArray()) + ? localEntriesElem.getAsJsonArray() : new JsonArray(); + + Map mcpLocalEntries = new LinkedHashMap<>(); + JsonArray filteredLocalEntries = new JsonArray(); + for (JsonElement localEntry : localEntriesNode) { + if (!localEntry.isJsonObject() || !localEntry.getAsJsonObject().has(Constant.NAME)) { + filteredLocalEntries.add(localEntry); + continue; + } + JsonObject entryObj = localEntry.getAsJsonObject(); + String entryName = entryObj.get(Constant.NAME).getAsString(); + + if (entryObj.has("isMcpConfig") && entryObj.get("isMcpConfig").getAsBoolean()) { + mcpLocalEntries.put(entryName, localEntry); + } else { + filteredLocalEntries.add(localEntry); + } + } + + Map mcpInboundEndpoints = new LinkedHashMap<>(); + JsonArray filteredInboundEndpoints = new JsonArray(); + for (JsonElement inboundEndpoint : inboundEndpointsNode) { + if (!inboundEndpoint.isJsonObject()) { + filteredInboundEndpoints.add(inboundEndpoint); + continue; + } + JsonObject endpointObj = inboundEndpoint.getAsJsonObject(); + String mcpConfigRef = null; + + if (endpointObj.has("mcpConfigReference") && !endpointObj.get("mcpConfigReference").isJsonNull()) { + mcpConfigRef = endpointObj.get("mcpConfigReference").getAsString(); + } + + if (mcpConfigRef != null && mcpLocalEntries.containsKey(mcpConfigRef)) { + mcpInboundEndpoints.put(mcpConfigRef, inboundEndpoint); + } else { + filteredInboundEndpoints.add(inboundEndpoint); + } + } + + JsonArray mcpServersArray = new JsonArray(); + for (String mcpConfigKey : mcpInboundEndpoints.keySet()) { + JsonElement localEntry = mcpLocalEntries.get(mcpConfigKey); + if (localEntry == null) { + filteredInboundEndpoints.add(mcpInboundEndpoints.get(mcpConfigKey)); + continue; + } + JsonObject mcpServer = new JsonObject(); + JsonObject inboundEndpointObj = mcpInboundEndpoints.get(mcpConfigKey).getAsJsonObject(); + String serverName = (inboundEndpointObj.has(Constant.NAME) && !inboundEndpointObj.get(Constant.NAME).isJsonNull()) + ? inboundEndpointObj.get(Constant.NAME).getAsString() + : mcpConfigKey; + mcpServer.addProperty(Constant.NAME, serverName); + mcpServer.add(Constant.LOCAL_ENTRY, localEntry); + mcpServer.add(Constant.INBOUND_ENDPOINT, mcpInboundEndpoints.get(mcpConfigKey)); + mcpServersArray.add(mcpServer); + } + + // Restore MCP local entries (with ) that have no matching endpoint + for (Map.Entry entry : mcpLocalEntries.entrySet()) { + if (!mcpInboundEndpoints.containsKey(entry.getKey())) { + filteredLocalEntries.add(entry.getValue()); + } + } + + return new JsonArray[]{mcpServersArray, filteredInboundEndpoints, filteredLocalEntries}; + } + private static void extractClassMediators(JsonNode mediatorFolders, ArrayNode classMediatorArray) { for (JsonNode classMediatorFolder : mediatorFolders) { if (classMediatorFolder.has(Constant.FILES)) { diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/directoryTree/node/Node.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/directoryTree/node/Node.java index 4947867b3..4ebe9aa39 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/directoryTree/node/Node.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/directoryTree/node/Node.java @@ -21,6 +21,8 @@ public class Node { String name; String path; Boolean isFaulty = false; + String mcpConfigReference; + Boolean isMcpConfig = false; public Node(String type, String name, String path) { @@ -87,6 +89,26 @@ public void setFaulty(Boolean faulty) { isFaulty = faulty; } + public String getMcpConfigReference() { + + return mcpConfigReference; + } + + public void setMcpConfigReference(String mcpConfigReference) { + + this.mcpConfigReference = mcpConfigReference; + } + + public Boolean getIsMcpConfig() { + + return isMcpConfig; + } + + public void setIsMcpConfig(Boolean isMcpConfig) { + + this.isMcpConfig = isMcpConfig; + } + protected Boolean equals(Node component) { return this.type.equals(component.type) && this.name.equals(component.name) && this.path.equals(component.path); diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/directoryTree/node/Resource.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/directoryTree/node/Resource.java index f1c364379..edd682360 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/directoryTree/node/Resource.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/directoryTree/node/Resource.java @@ -21,6 +21,7 @@ public class Resource { private RegistryResource registry; private List connectors; + private List inboundConnectors; private List metadata; private FolderNode newResources; @@ -28,6 +29,7 @@ public Resource() { registry = new RegistryResource(); connectors = new ArrayList<>(); + inboundConnectors = new ArrayList<>(); metadata = new ArrayList<>(); } @@ -46,6 +48,11 @@ public void addConnector(Node connector) { connectors.add(connector); } + public void addInboundConnector(Node inboundConnector) { + + inboundConnectors.add(inboundConnector); + } + public void addMetadata(Node meta) { metadata.add(meta); diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/expression/SemanticExpressionValidator.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/expression/SemanticExpressionValidator.java index fc8a729d4..11f46a5dc 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/expression/SemanticExpressionValidator.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/expression/SemanticExpressionValidator.java @@ -198,6 +198,64 @@ private boolean isOverloadCompatible(FunctionSignature sig, String[] literalType return true; } + /** + * Detects the operator-precedence pitfall where a logical operator ('and'/'or') is used as a + * direct, unparenthesized operand of a comparison operator. In the Synapse expression grammar, + * 'and'/'or' bind TIGHTER than the comparison operators (<, >, <=, >=, ==, !=), so an + * expression such as {@code a <= 0 or b > 10} parses as {@code a <= (0 or b) > 10} instead of + * the usually-intended {@code (a <= 0) or (b > 10)}. A warning is emitted recommending explicit + * parentheses. The correctly-parenthesized form parses as a single top-level logical expression + * with no comparison operator at this node, so it is never flagged (no false positives). + */ + @Override + public Void visitComparisonExpression(ExpressionParser.ComparisonExpressionContext ctx) { + if (hasComparisonOperator(ctx)) { + for (ExpressionParser.LogicalExpressionContext operand : ctx.logicalExpression()) { + TerminalNode logicalOp = operand.AND() != null ? operand.AND() : operand.OR(); + if (logicalOp != null) { + Token opToken = logicalOp.getSymbol(); + String op = opToken.getText(); + String message = "Operator precedence: '" + op + "' binds tighter than the comparison " + + "operators (<, >, <=, >=, ==, !=) in Synapse expressions, so '" + op + + "' is evaluated before the comparison. This is likely not the intended " + + "behavior. Add parentheses around each comparison to make the intent " + + "explicit, e.g. (a <= 0) " + op + " (b > 10)."; + ExpressionError error = new ExpressionError(opToken.getLine(), + opToken.getCharPositionInLine(), message, opToken, null); + error.setWarning(true); + errors.add(error); + break; // One warning per comparison expression is sufficient. + } + } + } + return visitChildren(ctx); + } + + /** + * Returns true if this comparison expression actually applies a comparison operator, as opposed + * to being a pass-through to a single logical expression. Checks the direct children for a + * comparison operator token rather than relying on a specific generated accessor. + */ + private boolean hasComparisonOperator(ExpressionParser.ComparisonExpressionContext ctx) { + for (int i = 0; i < ctx.getChildCount(); i++) { + if (ctx.getChild(i) instanceof TerminalNode) { + int type = ((TerminalNode) ctx.getChild(i)).getSymbol().getType(); + switch (type) { + case ExpressionLexer.GT: + case ExpressionLexer.LT: + case ExpressionLexer.GTE: + case ExpressionLexer.LTE: + case ExpressionLexer.EQ: + case ExpressionLexer.NEQ: + return true; + default: + break; + } + } + } + return false; + } + @Override public Void visitArithmeticExpression(ExpressionParser.ArithmeticExpressionContext ctx) { List terms = ctx.term(); diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/inbound/conector/InboundConnectorHolder.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/inbound/conector/InboundConnectorHolder.java index 38801b406..860d70580 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/inbound/conector/InboundConnectorHolder.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/inbound/conector/InboundConnectorHolder.java @@ -22,10 +22,13 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.github.fge.jackson.JsonLoader; import com.google.gson.JsonArray; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import org.apache.commons.lang3.StringUtils; import org.eclipse.lemminx.customservice.synapse.connectors.UiSchemaFlattener; +import org.eclipse.lemminx.customservice.synapse.connectors.entity.ConnectorVariableSchemaUtils; +import org.eclipse.lemminx.customservice.synapse.mediator.tryout.pojo.Property; import org.eclipse.lemminx.customservice.synapse.parser.Node; import org.eclipse.lemminx.customservice.synapse.parser.OverviewPageDetailsResponse; import org.eclipse.lemminx.customservice.synapse.syntaxTree.SyntaxTreeGenerator; @@ -56,13 +59,17 @@ public class InboundConnectorHolder { private static final Logger LOGGER = Logger.getLogger(InboundConnectorHolder.class.getName()); + public static final String INPUT_SCHEMA_FILE_SUFFIX = "_inputschema.json"; + private static InboundConnectorHolder instance; private String projectId; private String projectPath; private String tempFolderPath; // map - private HashMap connectorIdMap; + private final HashMap connectorIdMap; // map - private HashMap inboundConnectors; + private final HashMap inboundConnectors; + // map + private final HashMap inboundConnectorInputSchemas; private Map localInboundConnectors; private JsonObject inboundConnectorListJson; private String projectRuntimeVersion; @@ -77,6 +84,15 @@ public InboundConnectorHolder() { this.inboundConnectors = new HashMap<>(); this.connectorIdMap = new HashMap<>(); + this.inboundConnectorInputSchemas = new HashMap<>(); + } + + public static InboundConnectorHolder getInstance() { + + if (instance == null) { + throw new IllegalStateException("InboundConnectorHolder has not yet been initialized"); + } + return instance; } public void init(String projectPath, String projectRuntimeVersion) { @@ -106,6 +122,7 @@ public void init(String projectPath, String projectRuntimeVersion) { getCustomInboundConnectors(); loadInboundConnectors(); this.localInboundEndpointsListForCopilot = generateInboundConnectorArray(); + instance = this; } private void loadInboundConnectors() { @@ -123,15 +140,28 @@ private void loadInboundConnectors() { } } - public void getCustomInboundConnectors() { + public synchronized String getCustomInboundConnectors() { - File extractFolder = new File(Path.of(this.projectPath, Constant.SRC, Constant.MAIN, Constant.WSO2MI, - Constant.RESOURCES, Constant.INBOUND_CONNECTORS_DIR).toString()); + boolean isInboundConnectorAdded = false; InputStream inputStream = JsonLoader.class .getResourceAsStream("/org/eclipse/lemminx/inbound-endpoints/inbound_endpoints_" + this.projectRuntimeVersion.replace(".", StringUtils.EMPTY) + Constant.JSON_FILE_EXT); BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); this.inboundConnectorListJson = JsonParser.parseReader(reader).getAsJsonObject(); + Path resourcesPath = Path.of(this.projectPath, Constant.SRC, Constant.MAIN, Constant.WSO2MI, + Constant.RESOURCES); + for (String dirName : new String[]{Constant.INBOUND_ENDPOINTS, Constant.INBOUND_CONNECTORS_DIR}) { + File extractFolder = new File(resourcesPath.resolve(dirName).toString()); + if (importInboundConnectorsFromDirectory(extractFolder)) { + isInboundConnectorAdded = true; + } + } + return isInboundConnectorAdded ? "success" : "Failed to import the inbound-connector"; + } + + private boolean importInboundConnectorsFromDirectory(File extractFolder) { + + boolean isInboundConnectorAdded = false; List inboundConnectorZips = getInboundConnectorZips(extractFolder); for (File zip : inboundConnectorZips) { String zipName = zip.getName().replace(Constant.DOT + "zip", StringUtils.EMPTY); @@ -140,18 +170,30 @@ public void getCustomInboundConnectors() { Utils.extractZip(zip, extractToFolder); String schema = Utils.readFile(extractToFolder.toPath().resolve(Constant.RESOURCES) .resolve(Constant.UI_SCHEMA_JSON).toFile()); - saveInboundConnector(Utils.getJsonObject(schema).get(Constant.NAME).getAsString(), schema); - JsonObject newConnector = new JsonObject(); JsonObject connectorSchema = Utils.getJsonObject(schema); - newConnector.addProperty(Constant.NAME, connectorSchema.get(Constant.TITLE) != null ? - connectorSchema.get(Constant.TITLE).getAsString() : StringUtils.EMPTY); - newConnector.addProperty(Constant.ID, connectorSchema.get(Constant.ID) != null ? - connectorSchema.get(Constant.ID).getAsString() : StringUtils.EMPTY); - newConnector.addProperty(Constant.DESCRIPTION, connectorSchema.get(Constant.DESCRIPTION) != null ? - connectorSchema.get(Constant.DESCRIPTION).getAsString() : StringUtils.EMPTY); - newConnector.addProperty(Constant.TYPE, Constant.INBOUND_DASH_ENDPOINT); - JsonArray connectorArray = this.inboundConnectorListJson.getAsJsonArray(Constant.INBOUND_CONNECTOR_DATA); - connectorArray.add(newConnector); + if (saveInboundConnector(connectorSchema.get(Constant.NAME).getAsString(), schema)) { + File inputSchemaFile = extractToFolder.toPath().resolve(Constant.RESOURCES) + .resolve(Constant.INPUT_SCHEMA_JSON).toFile(); + if (inputSchemaFile.exists()) { + saveInboundConnectorInputSchema(connectorSchema.get(Constant.NAME).getAsString(), + connectorSchema.has(Constant.ID) ? connectorSchema.get(Constant.ID).getAsString() : null, + Utils.readFile(inputSchemaFile)); + } + JsonArray connectorArray = this.inboundConnectorListJson.getAsJsonArray(Constant.INBOUND_CONNECTOR_DATA); + String connectorId = connectorSchema.get(Constant.ID) != null ? + connectorSchema.get(Constant.ID).getAsString() : StringUtils.EMPTY; + if (!isConnectorAlreadyListed(connectorArray, connectorId)) { + JsonObject newConnector = new JsonObject(); + newConnector.addProperty(Constant.NAME, connectorSchema.get(Constant.TITLE) != null ? + connectorSchema.get(Constant.TITLE).getAsString() : StringUtils.EMPTY); + newConnector.addProperty(Constant.ID, connectorId); + newConnector.addProperty(Constant.DESCRIPTION, connectorSchema.get(Constant.DESCRIPTION) != null ? + connectorSchema.get(Constant.DESCRIPTION).getAsString() : StringUtils.EMPTY); + newConnector.addProperty(Constant.TYPE, Constant.INBOUND_DASH_ENDPOINT); + connectorArray.add(newConnector); + } + isInboundConnectorAdded = true; + } } catch (Exception e) { LOGGER.log(Level.SEVERE, "Failed to import custom inbound-connector:" + zipName, e); } @@ -163,6 +205,21 @@ public void getCustomInboundConnectors() { } } } + return isInboundConnectorAdded; + } + + private boolean isConnectorAlreadyListed(JsonArray connectorArray, String connectorId) { + + if (connectorId == null || connectorId.isEmpty()) { + return false; + } + for (JsonElement element : connectorArray) { + JsonObject connector = element.getAsJsonObject(); + if (connector.has(Constant.ID) && connectorId.equals(connector.get(Constant.ID).getAsString())) { + return true; + } + } + return false; } private List getInboundConnectorZips(File extractFolder) { @@ -183,8 +240,13 @@ private List getInboundConnectorZips(File extractFolder) { private void loadInboundConnector(File file) { + String fileName = file.getName(); + // Input schema files are loaded together with their uischema, skip them here. + if (fileName.endsWith(INPUT_SCHEMA_FILE_SUFFIX)) { + return; + } try { - String connectorName = file.getName().replace(".json", ""); + String connectorName = fileName.replace(Constant.JSON_FILE_EXT, StringUtils.EMPTY); String uiSchema = Utils.readFile(file); JsonObject inboundConnector = Utils.getJsonObject(uiSchema); if (inboundConnector == null || !inboundConnector.has(Constant.ID)) { @@ -193,6 +255,10 @@ private void loadInboundConnector(File file) { String id = inboundConnector.get(Constant.ID).getAsString(); connectorIdMap.put(connectorName, id); inboundConnectors.put(id, file.getAbsolutePath()); + File inputSchemaFile = Path.of(tempFolderPath, connectorName + INPUT_SCHEMA_FILE_SUFFIX).toFile(); + if (inputSchemaFile.exists()) { + inboundConnectorInputSchemas.put(id, inputSchemaFile.getAbsolutePath()); + } } catch (IOException e) { LOGGER.log(Level.SEVERE, "Error occurred while loading inbound connector schema from file", e); } @@ -215,6 +281,47 @@ public Boolean saveInboundConnector(String connectorName, String uiSchema) { return false; } + /** + * Persists the input schema shipped by an inbound connector (its + * {@code resources/inputschema.json}) next to the uischema in the per-project + * temp folder and registers it against the connector id. + * + * @param connectorName the connector name (uischema {@code name}) + * @param id the connector id (uischema {@code id}) + * @param inputSchema the raw input schema JSON + */ + public void saveInboundConnectorInputSchema(String connectorName, String id, String inputSchema) { + + if (StringUtils.isEmpty(id) || StringUtils.isEmpty(inputSchema)) { + return; + } + Path filePath = Path.of(tempFolderPath, connectorName + INPUT_SCHEMA_FILE_SUFFIX); + if (saveToFile(filePath.toFile(), inputSchema)) { + inboundConnectorInputSchemas.put(id, filePath.toString()); + } + } + + /** + * Returns the input schema of the inbound connector with the given id as a + * {@link Property} tree (built the same way as regular connector output schemas), + * or {@code null} if no input schema is registered. + */ + public Property getInboundConnectorInputSchema(String id) { + + String inputSchemaPath = inboundConnectorInputSchemas.get(id); + if (StringUtils.isEmpty(inputSchemaPath)) { + return null; + } + try { + String inputSchema = Utils.readFile(new File(inputSchemaPath)); + JsonObject inputSchemaJson = Utils.getJsonObject(inputSchema); + return ConnectorVariableSchemaUtils.buildSchemaProperty(inputSchemaJson); + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Error occurred while reading inbound connector input schema from file", e); + } + return null; + } + public InboundConnectorResponse getInboundConnectorSchema(File inboundEPFile) { try { diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/mediator/schema/generate/ServerLessTryoutHandler.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/mediator/schema/generate/ServerLessTryoutHandler.java index be5a30302..470007ab7 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/mediator/schema/generate/ServerLessTryoutHandler.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/mediator/schema/generate/ServerLessTryoutHandler.java @@ -15,6 +15,8 @@ package org.eclipse.lemminx.customservice.synapse.mediator.schema.generate; import com.google.gson.JsonPrimitive; +import org.apache.commons.lang3.StringUtils; +import org.eclipse.lemminx.customservice.synapse.InvalidConfigurationException; import org.eclipse.lemminx.customservice.synapse.mediator.TryOutUtils; import org.eclipse.lemminx.customservice.synapse.mediator.schema.generate.visitor.SchemaVisitor; import org.eclipse.lemminx.customservice.synapse.mediator.schema.generate.visitor.SchemaVisitorFactory; @@ -23,6 +25,9 @@ import org.eclipse.lemminx.customservice.synapse.mediator.tryout.pojo.MediatorTryoutRequest; import org.eclipse.lemminx.customservice.synapse.syntaxTree.SyntaxTreeGenerator; import org.eclipse.lemminx.customservice.synapse.syntaxTree.pojo.STNode; +import org.eclipse.lemminx.customservice.synapse.syntaxTree.pojo.inbound.InboundEndpoint; +import org.eclipse.lemminx.customservice.synapse.utils.ConfigFinder; +import org.eclipse.lemminx.customservice.synapse.utils.Constant; import org.eclipse.lemminx.customservice.synapse.utils.Utils; import org.eclipse.lemminx.dom.DOMDocument; @@ -44,27 +49,48 @@ public ServerLessTryoutHandler(String projectUri) { public MediatorTryoutInfo handle(MediatorTryoutRequest request) { try { - String filePath = request.getFile(); + String visitFilePath = request.getFile(); if (request.getEdits() != null) { + STNode node = getSTNode(request.getFile()); String documentUri = request.getFile(); + String editFilePath = TEMP_FOLDER.resolve(TEMP_FILE_NAME).toString(); + if (node instanceof InboundEndpoint) { + String sequence = ((InboundEndpoint) node).getSequence(); + if (StringUtils.isNotEmpty(sequence)) { + String seqPath = ConfigFinder.findEsbComponentPath(sequence, Constant.SEQUENCES, projectUri); + if (StringUtils.isNotEmpty(seqPath)) { + documentUri = seqPath; + } + } + } else { + visitFilePath = editFilePath; + } Utils.copyFile(documentUri, TEMP_FOLDER.toString(), TEMP_FILE_NAME); - filePath = TEMP_FOLDER.resolve(TEMP_FILE_NAME).toString(); - TryOutUtils.doEdits(request.getEdits(), Path.of(filePath)); - request = new MediatorTryoutRequest(filePath, request.getLine(), request.getColumn() + 1, + TryOutUtils.doEdits(request.getEdits(), Path.of(editFilePath)); + request = new MediatorTryoutRequest(editFilePath, request.getLine(), request.getColumn() + 1, request.getInputPayload(), null); } - DOMDocument domDocument = Utils.getDOMDocument(new File(filePath)); + DOMDocument domDocument = Utils.getDOMDocument(new File(visitFilePath)); STNode node = SyntaxTreeGenerator.buildTree(domDocument.getDocumentElement()); MediatorTryoutInfo mediatorTryoutInfo = createInitialMediatorTryoutInfo(request); if (node != null) { visitNode(node, request, mediatorTryoutInfo); } return mediatorTryoutInfo; - } catch (IOException e) { + } catch (IOException | InvalidConfigurationException e) { return new MediatorTryoutInfo(e.getMessage()); } } + private STNode getSTNode(String filePath) throws IOException, InvalidConfigurationException { + + if (StringUtils.isEmpty(filePath)) { + throw new IllegalArgumentException("FilePath is null"); + } + DOMDocument domDocument = Utils.getDOMDocument(new File(filePath)); + return SyntaxTreeGenerator.buildTree(domDocument.getDocumentElement()); + } + private MediatorTryoutInfo createInitialMediatorTryoutInfo(MediatorTryoutRequest request) { MediatorInfo mediatorInfo = new MediatorInfo(); diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/mediator/schema/generate/visitor/InboundEndpointVisitor.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/mediator/schema/generate/visitor/InboundEndpointVisitor.java index f38835174..69dff96f6 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/mediator/schema/generate/visitor/InboundEndpointVisitor.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/mediator/schema/generate/visitor/InboundEndpointVisitor.java @@ -15,10 +15,15 @@ package org.eclipse.lemminx.customservice.synapse.mediator.schema.generate.visitor; import org.apache.commons.lang3.StringUtils; +import org.eclipse.lemminx.customservice.synapse.inbound.conector.InboundConnectorHolder; import org.eclipse.lemminx.customservice.synapse.mediator.tryout.pojo.MediatorTryoutInfo; import org.eclipse.lemminx.customservice.synapse.mediator.tryout.pojo.MediatorTryoutRequest; +import org.eclipse.lemminx.customservice.synapse.mediator.tryout.pojo.Property; import org.eclipse.lemminx.customservice.synapse.syntaxTree.pojo.STNode; import org.eclipse.lemminx.customservice.synapse.syntaxTree.pojo.inbound.InboundEndpoint; +import org.eclipse.lemminx.customservice.synapse.syntaxTree.pojo.inbound.InboundEndpointParameters; +import org.eclipse.lemminx.customservice.synapse.syntaxTree.pojo.misc.common.Parameter; +import org.eclipse.lemminx.customservice.synapse.utils.Constant; import java.io.IOException; import java.util.logging.Level; @@ -37,14 +42,69 @@ public InboundEndpointVisitor(String projectPath) { @Override public void visit(STNode node, MediatorTryoutInfo info, MediatorTryoutRequest request) { - String sequence = ((InboundEndpoint) node).getSequence(); + InboundEndpoint inboundEndpoint = (InboundEndpoint) node; + String sequence = inboundEndpoint.getSequence(); if (StringUtils.isEmpty(sequence)) { return; } + + loadInboundVariable(inboundEndpoint, info); + try { Utils.visitSequenceByKey(sequence, projectPath, info, request); } catch (IOException e) { LOGGER.log(Level.SEVERE, String.format("Error occurred while visiting the sequence: %s", sequence), e); } } + + /** + * If the inbound endpoint declares an {@code inboundVariableName} parameter, load the + * input schema of the corresponding inbound connector and seed a variable with that + * name into the tryout info. This mirrors how connector mediators seed their response + * variable, making the incoming message structure available to the dispatched sequence. + */ + private void loadInboundVariable(InboundEndpoint inboundEndpoint, MediatorTryoutInfo info) { + + String inboundVariableName = getParameterValue(inboundEndpoint, Constant.INBOUND_VARIABLE_NAME); + if (StringUtils.isEmpty(inboundVariableName)) { + return; + } + InboundConnectorHolder holder; + try { + holder = InboundConnectorHolder.getInstance(); + } catch (IllegalStateException e) { + LOGGER.severe("Inbound connector holder is not initialized"); + return; + } + String id = inboundEndpoint.getProtocol() != null ? inboundEndpoint.getProtocol() + : inboundEndpoint.getClazz(); + if (StringUtils.isEmpty(id)) { + return; + } + Property inputSchema = holder.getInboundConnectorInputSchema(id); + if (inputSchema == null) { + return; + } + inputSchema.setKey(inboundVariableName); + info.addOutputVariable(inputSchema); + } + + private String getParameterValue(InboundEndpoint inboundEndpoint, String parameterName) { + + InboundEndpointParameters[] parametersList = inboundEndpoint.getParameters(); + if (parametersList == null) { + return null; + } + for (InboundEndpointParameters parameters : parametersList) { + if (parameters == null || parameters.getParameter() == null) { + continue; + } + for (Parameter parameter : parameters.getParameter()) { + if (parameter != null && parameterName.equals(parameter.getName())) { + return parameter.getContent(); + } + } + } + return null; + } } diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/mediatorService/AIConnectorHandler.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/mediatorService/AIConnectorHandler.java index 46aece6d1..ec30f49fe 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/mediatorService/AIConnectorHandler.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/mediatorService/AIConnectorHandler.java @@ -555,6 +555,14 @@ private Map processToolData(Map data, String seq return toolData; } + private static String unwrapExpression(String value) { + + if (StringUtils.isNotBlank(value) && value.startsWith("${") && value.endsWith("}")) { + return value.substring(2, value.length() - 1); + } + return value; + } + /** * Generates a unique sequence template name for the newly added tool. */ @@ -931,7 +939,7 @@ private void addToolConfigurations(JsonObject schema, DOMNode node, Mediator med JsonObject expression = new JsonObject(); expression.addProperty(Constant.IS_EXPRESSION, true); - expression.addProperty(Constant.VALUE, node.getAttribute(RESULT_EXPRESSION)); + expression.addProperty(Constant.VALUE, unwrapExpression(node.getAttribute(RESULT_EXPRESSION))); toolData.put(TOOL_RESULT_EXPRESSION, expression); toolData.put(TOOL_DESCRIPTION, node.getAttribute(Constant.DESCRIPTION)); @@ -1408,19 +1416,37 @@ public MCPToolResponse fetchMcpTools(String documentUri, Range range, List 0) { + break; + } + continue; + } + if (line.startsWith("data:")) { + dataBuffer.append(line.substring(5).trim()); + } } + responseJson = dataBuffer.toString(); + } else { + responseJson = responseBody; + } + + if (StringUtils.isBlank(responseJson)) { + response.error = "Empty MCP response"; + return response; } - String responseJson = dataBuffer.toString(); ObjectMapper mapper = new ObjectMapper(); JsonNode dataJson = mapper.readTree(responseJson); diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/mediatorService/MediatorHandler.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/mediatorService/MediatorHandler.java index 6094bfdd9..9a94631c1 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/mediatorService/MediatorHandler.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/mediatorService/MediatorHandler.java @@ -176,6 +176,11 @@ private SynapseConfigResponse generateConnectorSynapseConfig(STNode node, String Map connectorData = new HashMap<>(); connectorData.put(Constant.TAG, operation.getTag()); connectorData.put(Constant.CONFIG_KEY, data.get(Constant.CONFIG_KEY)); + Object description = data.get(Constant.DESCRIPTION); + if (description == null && node instanceof Connector) { + description = ((Connector) node).getDescription(); + } + connectorData.put(Constant.DESCRIPTION, description); List parameterData = new ArrayList<>(); for (OperationParameter parameter : parameters) { if (data.containsKey(parameter.getName())) { @@ -197,6 +202,7 @@ private SynapseConfigResponse generateConnectorSynapseConfig(STNode node, String } connectorData.put(Constant.PARAMETERS, parameterData); + connectorData.put(Constant.HAS_PARAMETERS, !parameterData.isEmpty()); StringWriter writer = new StringWriter(); String edit = templateMap.get(Constant.CONNECTOR).execute(writer, connectorData).toString(); TextEdit textEdit = new TextEdit(range, edit); diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/resourceFinder/AbstractResourceFinder.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/resourceFinder/AbstractResourceFinder.java index 3696540f2..5e35441e9 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/resourceFinder/AbstractResourceFinder.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/resourceFinder/AbstractResourceFinder.java @@ -672,6 +672,34 @@ private void traverseRegistryFolder(File folder, Map a */ public abstract Map findAllResources(String projectPath); + /** + * For each requested resource that declares a protocols filter, drops resources of that + * type whose protocol attribute is not in the list. Unfiltered types are left untouched. + * + * @param response the response whose resource list is filtered in place + * @param requestedResources the requested resources and protocol filters if any + */ + protected void applyProtocolFilters(ResourceResponse response, List requestedResources) { + + if (response.getResources() == null) { + return; + } + for (RequestedResource requested : requestedResources) { + List protocols = requested.getProtocols(); + if (protocols == null || protocols.isEmpty()) { + continue; + } + response.getResources().removeIf(resource -> { + if (!requested.getType().equals(resource.getType())) { + return false; + } + String protocol = resource instanceof ArtifactResource + ? ((ArtifactResource) resource).getProtocol() : null; + return protocol == null || protocols.stream().noneMatch(p -> p.equalsIgnoreCase(protocol.trim())); + }); + } + } + protected List findResourceInArtifacts(Path artifactsPath, List types) { List resources = new ArrayList<>(); @@ -1024,6 +1052,8 @@ private Resource createArtifactResource(File file, DOMElement rootElement, Strin artifact.setType(type); artifact.setFrom(ARTIFACTS); ((ArtifactResource) artifact).setLocalEntry(isLocalEntry); + ((ArtifactResource) artifact).setMcpInbound(Utils.isMcpInboundEndpoint(rootElement)); + ((ArtifactResource) artifact).setProtocol(rootElement.getAttribute(Constant.PROTOCOL)); ((ArtifactResource) artifact).setArtifactPath(file.getName()); ((ArtifactResource) artifact).setAbsolutePath(file.getAbsolutePath()); return artifact; diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/resourceFinder/NewProjectResourceFinder.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/resourceFinder/NewProjectResourceFinder.java index 07c74dfc2..03e8c09bc 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/resourceFinder/NewProjectResourceFinder.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/resourceFinder/NewProjectResourceFinder.java @@ -54,6 +54,7 @@ protected ResourceResponse findResources(String projectPath, List protocols; public RequestedResource() { @@ -48,4 +51,14 @@ public void setNeedRegistry(boolean needRegistry) { this.needRegistry = needRegistry; } + + public List getProtocols() { + + return protocols; + } + + public void setProtocols(List protocols) { + + this.protocols = protocols; + } } diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/syntaxTree/factory/APIFactory.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/syntaxTree/factory/APIFactory.java index 8be5470bc..392869a71 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/syntaxTree/factory/APIFactory.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/syntaxTree/factory/APIFactory.java @@ -103,6 +103,10 @@ public void populateAttributes(STNode node, DOMElement element) { if (Objects.nonNull(traceEnum)) { api.setTrace(traceEnum); } + String bindsTo = element.getAttribute(Constant.BINDS_TO); + if (Objects.nonNull(bindsTo)) { + api.setBindsTo(bindsTo); + } } public STNode createAPIResource(DOMNode node, String apiName) { diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/syntaxTree/factory/ResourceFactory.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/syntaxTree/factory/ResourceFactory.java index 19f87f3e5..3a91d9d41 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/syntaxTree/factory/ResourceFactory.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/syntaxTree/factory/ResourceFactory.java @@ -95,6 +95,10 @@ public void populateAttributes(STNode node, DOMElement element) { if (Objects.nonNull(faultSequence)) { apiResource.setFaultSequenceAttribute(faultSequence); } + String bindsTo = element.getAttribute(Constant.BINDS_TO); + if (Objects.nonNull(bindsTo)) { + apiResource.setBindsTo(bindsTo); + } } private STNode createSequence(DOMNode node) { diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/syntaxTree/factory/mediators/ConnectorFactory.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/syntaxTree/factory/mediators/ConnectorFactory.java index a1d9849d5..73ff0606b 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/syntaxTree/factory/mediators/ConnectorFactory.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/syntaxTree/factory/mediators/ConnectorFactory.java @@ -38,6 +38,10 @@ public void populateAttributes(STNode node, DOMElement element) { if (configKey != null) { ((Connector) node).setConfigKey(configKey); } + String description = element.getAttribute(Constant.DESCRIPTION); + if (description != null) { + ((Connector) node).setDescription(description); + } addConnectorParameters((Connector) node, element); } diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/syntaxTree/pojo/api/API.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/syntaxTree/pojo/api/API.java index eb35fb30e..fff7a90b1 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/syntaxTree/pojo/api/API.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/syntaxTree/pojo/api/API.java @@ -30,6 +30,7 @@ public class API extends STNode { String description; EnableDisable statistics; EnableDisable trace; + String bindsTo; public APIResource[] getResource() { @@ -150,4 +151,14 @@ public void setTrace(EnableDisable trace) { this.trace = trace; } + + public String getBindsTo() { + + return bindsTo; + } + + public void setBindsTo(String bindsTo) { + + this.bindsTo = bindsTo; + } } diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/syntaxTree/pojo/api/APIResource.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/syntaxTree/pojo/api/APIResource.java index afeeea14a..5ca39019e 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/syntaxTree/pojo/api/APIResource.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/syntaxTree/pojo/api/APIResource.java @@ -30,6 +30,7 @@ public class APIResource extends STNode { String faultSequenceAttribute; String uriTemplate; String urlMapping; + String bindsTo; public String getApi() { @@ -141,6 +142,16 @@ public void setUrlMapping(String urlMapping) { this.urlMapping = urlMapping; } + public String getBindsTo() { + + return bindsTo; + } + + public void setBindsTo(String bindsTo) { + + this.bindsTo = bindsTo; + } + public void addMethod(String method) { if (this.methods == null) { diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/syntaxTree/pojo/connector/Connector.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/syntaxTree/pojo/connector/Connector.java index 1de889081..9565749eb 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/syntaxTree/pojo/connector/Connector.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/syntaxTree/pojo/connector/Connector.java @@ -24,6 +24,7 @@ public class Connector extends Mediator { String connectorName; String method; String configKey; + String description; List parameters; public Connector() { @@ -86,6 +87,16 @@ public void setConfigKey(String configKey) { this.configKey = configKey; } + public String getDescription() { + + return description; + } + + public void setDescription(String description) { + + this.description = description; + } + public void removeParameter(String name) { for (ConnectorParameter parameter : parameters) { diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/syntaxTree/serializer/api/APISerializer.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/syntaxTree/serializer/api/APISerializer.java index 2f70276dd..8aa59a4e5 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/syntaxTree/serializer/api/APISerializer.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/syntaxTree/serializer/api/APISerializer.java @@ -59,6 +59,9 @@ private static void addAttributes(API api, OMElement apiElt) { if (api.getTrace() != null) { apiElt.addAttribute("trace", api.getTrace().name(), null); } + if (api.getBindsTo() != null) { + apiElt.addAttribute(Constant.BINDS_TO, api.getBindsTo(), null); + } } public static OMElement serializeVersioningStrategy(API api, OMElement apiElement) { diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/syntaxTree/serializer/api/ResourceSerializer.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/syntaxTree/serializer/api/ResourceSerializer.java index 919d005a4..18ebe0e8f 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/syntaxTree/serializer/api/ResourceSerializer.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/syntaxTree/serializer/api/ResourceSerializer.java @@ -51,6 +51,10 @@ public static OMElement serializeResource(APIResource resource) { resourceElt.addAttribute("url-mapping", resource.getUrlMapping(), null); } + if (resource.getBindsTo() != null) { + resourceElt.addAttribute(Constant.BINDS_TO, resource.getBindsTo(), null); + } + if (resource.getInSequenceAttribute() != null) { resourceElt.addAttribute("inSequence", resource.getInSequenceAttribute(), null); } else { diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/utils/Constant.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/utils/Constant.java index 020618ba9..05f3a10ed 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/utils/Constant.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/utils/Constant.java @@ -173,6 +173,7 @@ public class Constant { public static final String STATISTICS = "statistics"; public static final String TRACE = "trace"; public static final String PARAMETERS = "parameters"; + public static final String HAS_PARAMETERS = "hasParameters"; public static final String PARAMETER = "parameter"; public static final String LOCKED = "locked"; public static final String SRC = "src"; @@ -292,6 +293,7 @@ public class Constant { public static final String HOSTNAME = "hostname"; public static final String VERSION_TYPE = "version-type"; public static final String PUBLISH_SWAGGER = "publishSwagger"; + public static final String BINDS_TO = "binds-to"; public static final String URL_MAPPING = "url-mapping"; public static final String SPACE = " "; public static final String WSDL_IMPORT = "wsdl:import"; @@ -496,6 +498,7 @@ public class Constant { public static final String TASK_UPPERCASE = "TASK"; public static final String INBOUND_UPPERCASE = "INBOUND_ENDPOINT"; public static final String INBOUND_DASH_ENDPOINT = "inbound-endpoint"; + public static final String MCP_INBOUND_LISTENER_CLASS = "org.wso2.carbon.inbound.sse.McpInboundListener"; public static final String CONNECTION_UPPERCASE = "CONNECTION"; public static final String OTHER = "other"; public static final String SCHEDULED_TASK = "schedule-task"; @@ -549,6 +552,7 @@ public class Constant { public static final String WSO2_MI = ".wso2-mi"; public static final String M2 = ".m2"; public static final String DOWNLOADED = "Downloaded"; + public static final String HTTP_CONNECTOR_ARTIFACT_ID = "mi-connector-http"; public static final String EXTRACTED = "Extracted"; public static final String PARALLEL_EXECUTION = "parallelExecution"; public static final String CONTINUE_WITHOUT_AGGREGATION = "continueWithoutAggregation"; @@ -592,6 +596,8 @@ public class Constant { public static final String BALLERINA_MODULE_PATH = "ballerinaModulePath"; public static final String BALLERINA = "ballerina"; public static final String UI_SCHEMA_JSON = "uischema.json"; + public static final String INPUT_SCHEMA_JSON = "inputschema.json"; + public static final String INBOUND_VARIABLE_NAME = "inboundVariableName"; public static final String JSON_FILE_EXT = ".json"; public static final String INBOUND_CONNECTOR_PREFIX = "mi-inbound-"; public static final String INBOUND_CONNECTORS = "inbound.connectors"; diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/utils/UISchemaMapper.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/utils/UISchemaMapper.java index 8d051aaa1..3f85aaf9d 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/utils/UISchemaMapper.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/utils/UISchemaMapper.java @@ -198,6 +198,9 @@ public static JsonObject mapInputToUISchemaForConnector(Connector connector, Jso } else { data.addProperty(Constant.CONFIG_REF, connector.getConfigKey()); } + if (StringUtils.isNotEmpty(connector.getDescription())) { + data.addProperty(Constant.DESCRIPTION, connector.getDescription()); + } return mapInputToUISchema(data, uiSchema); } diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/utils/Utils.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/utils/Utils.java index ae34d043e..bafb8e2d3 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/utils/Utils.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/customservice/synapse/utils/Utils.java @@ -270,7 +270,24 @@ public static DOMDocument getDOMDocument(String content) { public static DOMDocument getDOMDocument(String content, URIResolverExtensionManager resolverExtensionManager) { - TextDocument document = new TextDocument(content, "temp"); + return getDOMDocument(content, "temp", resolverExtensionManager); + } + + /** + * Get the DOM document from the given xml content, using the provided URI as the document's + * system id. The URI matters for diagnostics that are gated on the document path (e.g. + * SynapseExpressionValidator only runs for files under src/main/wso2mi/artifacts), so callers + * that have a real file path should pass it instead of relying on the "temp" fallback. + * + * @param content the xml content + * @param uri the URI to assign to the parsed document + * @param resolverExtensionManager the URI resolver extension manager + * @return the DOM document for the given xml content + */ + public static DOMDocument getDOMDocument(String content, String uri, + URIResolverExtensionManager resolverExtensionManager) { + + TextDocument document = new TextDocument(content, uri); return DOMParser.getInstance().parse(document, resolverExtensionManager); } @@ -575,6 +592,10 @@ public static DOMNode getChildNodeByName(DOMNode node, String name) { return foundNode; } + public static boolean isMcpInboundEndpoint(DOMElement element) { + return Constant.MCP_INBOUND_LISTENER_CLASS.equals(element.getAttribute("class")); + } + public static String addUnderscoreBetweenWords(String input) { StringBuilder result = new StringBuilder(); @@ -1062,6 +1083,36 @@ public static String getServerVersion(String projectPath, String defaultVersion) return mapped != null ? mapped : Constant.DEFAULT_MI_VERSION; } + public static boolean hasDependency(String projectPath, String artifactId) { + try { + Path pomPath = Path.of(projectPath, "pom.xml"); + File pomFile = pomPath.toFile(); + if (!pomFile.exists()) { + return false; + } + DOMDocument document = getDOMDocument(pomFile); + DOMNode dependencies = getChildNodeByName(document.getDocumentElement(), Constant.DEPENDENCIES); + if (dependencies == null) { + return false; + } + for (DOMNode dependency : dependencies.getChildren()) { + if (!Constant.DEPENDENCY.equalsIgnoreCase(dependency.getNodeName())) { + continue; + } + DOMNode artifactNode = getChildNodeByName(dependency, Constant.ARTIFACT_ID); + if (artifactNode != null) { + String pomArtifactId = getInlineString(artifactNode.getFirstChild()); + if (pomArtifactId != null && artifactId.equals(pomArtifactId.trim())) { + return true; + } + } + } + } catch (Exception e) { + logger.log(Level.SEVERE, "Error occurred while checking pom dependency: " + artifactId, e); + } + return false; + } + public static Map getTemplateMap(String resourceFolderName) { Map templateMap = new HashMap<>(); try { diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/synapse/SynapseDiagnosticsParticipant.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/synapse/SynapseDiagnosticsParticipant.java index 4202f22a0..fd2fd9afa 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/synapse/SynapseDiagnosticsParticipant.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/synapse/SynapseDiagnosticsParticipant.java @@ -47,6 +47,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; @@ -72,6 +73,20 @@ public class SynapseDiagnosticsParticipant implements IDiagnosticsParticipant { private static final long ARTIFACT_CACHE_TTL_MS = 5000; // 5 seconds private final Map artifactIndexCache = new ConcurrentHashMap<>(); + /** + * Invalidation signal for {@link #artifactIndexCache}. The cache is keyed per project with a + * short TTL; when an artifact/resource file under {@code src/main/wso2mi} changes, the language + * server bumps this epoch so the next diagnostics run rebuilds the index instead of serving a + * stale entry (which would wrongly flag a just-written sibling as unresolved for up to the TTL). + * A cache entry is only honored while its stored epoch matches the current one. + */ + private static final AtomicLong artifactCacheEpoch = new AtomicLong(); + + /** Invalidate the cross-file artifact index cache (call when project artifact files change). */ + public static void invalidateArtifactIndexCache() { + artifactCacheEpoch.incrementAndGet(); + } + /** Template name -> absolute file path, populated during artifact index building. */ private volatile Map templateFilePaths = java.util.Collections.emptyMap(); /** Artifact names that appear in multiple files (duplicates). */ @@ -79,6 +94,28 @@ public class SynapseDiagnosticsParticipant implements IDiagnosticsParticipant { /** Artifact names that participate in direct circular references (A->B->A). */ private volatile Set cyclicArtifacts = java.util.Collections.emptySet(); + /** + * Request-scoped opt-out for cross-file (other-artifact-dependent) checks. The MI Copilot agent + * sets this for per-file auto-validation after a write, when sibling artifacts it references may + * not exist on disk yet, to suppress transient false positives. It is thread-confined and set by + * {@code SynapseLanguageService.codeDiagnostic()} around the {@code doDiagnostics} call; the + * editor/manual flows never set it, so cross-file validation stays on by default. + */ + private static final ThreadLocal SKIP_CROSS_FILE_VALIDATION = + ThreadLocal.withInitial(() -> Boolean.FALSE); + + public static void setSkipCrossFileValidation(boolean skip) { + SKIP_CROSS_FILE_VALIDATION.set(skip); + } + + public static void clearSkipCrossFileValidation() { + SKIP_CROSS_FILE_VALIDATION.remove(); + } + + private static boolean isSkipCrossFileValidation() { + return Boolean.TRUE.equals(SKIP_CROSS_FILE_VALIDATION.get()); + } + private static final Set SYNAPSE_ROOT_ELEMENTS = new HashSet<>(Arrays.asList( "api", "proxy", "endpoint", "sequence", "inboundEndpoint", "template", "task", "localEntry", "messageStore", "messageProcessor", "registry" @@ -130,6 +167,15 @@ public class SynapseDiagnosticsParticipant implements IDiagnosticsParticipant { "|\\{\\$\\{(.+?)\\}\\}" // {${...}} — wrapped form ); + /** + * Leaf elements whose text content is raw code (not a Synapse expression) and must not be + * scanned for vars.X references — e.g. a JS/Groovy script body could legitimately contain a + * ${...} template literal that is not a Synapse variable reference. + */ + private static final Set RAW_TEXT_ELEMENTS = new HashSet<>(Arrays.asList( + "script" + )); + /** * Regex to find $N placeholders in PayloadFactory format strings. */ @@ -149,8 +195,13 @@ public class SynapseDiagnosticsParticipant implements IDiagnosticsParticipant { "api", "proxy", "sequence", "inboundEndpoint", "resource" )); + // Synchronized because a single participant instance is registered in SynapsePlugin and + // diagnostics can run concurrently (editor validation and codeDiagnostic both execute async). + // The cross-file index is derived into shared instance fields per run, so serializing here keeps + // one request from clearing/overwriting that state while another is still validating. Runs are + // short (the project scan is cached), so the contention cost is minimal. @Override - public void doDiagnostics(DOMDocument xmlDocument, List diagnostics, + public synchronized void doDiagnostics(DOMDocument xmlDocument, List diagnostics, XMLValidationSettings validationSettings, CancelChecker cancelChecker) { DOMElement root = xmlDocument.getDocumentElement(); if (root == null) { @@ -164,7 +215,18 @@ public void doDiagnostics(DOMDocument xmlDocument, List diagnostics, if (SYNAPSE_NS.equals(namespace)) { // Valid Synapse file — run all validations Set definedVariables = new HashSet<>(); - Set knownArtifacts = buildArtifactNameIndex(xmlDocument, cancelChecker); + // Cross-file reference checks depend on the project-wide artifact index. When the caller + // opts out (agent per-file validation) the index is not built, avoiding the filesystem + // scan; it is also null when the project path is not derivable. In either case the + // index is unavailable, so clear the derived cross-file state — otherwise a stale index + // from a prior request could surface template/duplicate/cycle diagnostics here. + boolean skipCrossFile = isSkipCrossFileValidation(); + Set knownArtifacts = skipCrossFile ? null : buildArtifactNameIndex(xmlDocument, cancelChecker); + if (knownArtifacts == null) { + this.templateFilePaths = java.util.Collections.emptyMap(); + this.duplicateArtifactNames = java.util.Collections.emptySet(); + this.cyclicArtifacts = java.util.Collections.emptySet(); + } // Detect MI runtime version for new-pattern hints String projectPath = deriveProjectPath(xmlDocument); @@ -227,6 +289,10 @@ private void validateElement(DOMNode node, List diagnostics, DOMDocu // Validate variable references in expression attributes validateVariableReferences(element, diagnostics, document, definedVariables); + // Flag an opening "${" with no matching "}" (the malformed expression is otherwise + // treated as a plain string and reaches runtime with no feedback) + validateUnclosedExpressions(element, diagnostics, document); + // Cross-file reference validation if (knownArtifacts != null) { validateCrossReferences(element, diagnostics, knownArtifacts); @@ -952,6 +1018,8 @@ private void validateScriptMediator(DOMElement element, List diagnos * P1-14: Validate call-template with-param names against template parameter declarations. */ private void validateCallTemplateParams(DOMElement element, List diagnostics) { + // Cross-file check: depends on the referenced template file (cycles + parameter declarations) + if (isSkipCrossFileValidation()) return; String target = element.getAttribute("target"); if (StringUtils.isEmpty(target) || isExpression(target)) return; @@ -1058,6 +1126,7 @@ private Map parseTemplateParameters(String filePath) { * P1-19: Warn if the current document's root artifact name is duplicated in the project. */ private void validateDuplicateArtifactName(DOMElement root, List diagnostics) { + if (isSkipCrossFileValidation()) return; // cross-file check: needs the project-wide index if (duplicateArtifactNames.isEmpty()) return; String name = root.getAttribute("name"); if (name != null && duplicateArtifactNames.contains(name)) { @@ -1146,56 +1215,199 @@ private void collectVariableDefinition(DOMElement element, Set definedVa */ private void validateVariableReferences(DOMElement element, List diagnostics, DOMDocument document, Set definedVariables) { - // Check all attributes for ${...} expressions containing vars.X + // Check all attribute values for ${...} expressions containing vars.X List attrs = element.getAttributeNodes(); - if (attrs == null) { + if (attrs != null) { + for (DOMAttr attr : attrs) { + String attrValue = attr.getValue(); + if (attrValue == null || (!attrValue.contains("vars.") && !attrValue.contains("vars["))) { + continue; + } + checkContentForUndefinedVariables(attrValue, XMLPositionUtility.selectAttributeValue(attr), + diagnostics, definedVariables); + } + } + + // Check element text content too — e.g. connector operation parameters such as + // {${vars.x}} reference variables in text, not attributes. + validateVariableReferencesInText(element, diagnostics, document, definedVariables); + } + + /** + * Scans the text content of a leaf element for ${...} expressions referencing undefined vars.X. + * Only leaf elements are inspected (containers expose their text via their leaf children), and + * raw-code elements (see {@link #RAW_TEXT_ELEMENTS}) are skipped to avoid false positives. + */ + private void validateVariableReferencesInText(DOMElement element, List diagnostics, + DOMDocument document, Set definedVariables) { + if (hasChildElements(element)) { + return; + } + String localName = element.getLocalName(); + if (localName != null && RAW_TEXT_ELEMENTS.contains(localName.toLowerCase())) { + return; + } + List children = element.getChildren(); + if (children == null) { return; } - for (DOMAttr attr : attrs) { - String attrValue = attr.getValue(); - if (attrValue == null || (!attrValue.contains("vars.") && !attrValue.contains("vars["))) { + for (DOMNode child : children) { + // Include CDATA: inline JSON/XML payloads (e.g. payloadFactory ) commonly wrap + // ${vars.x} references in . + if (!child.isText() && !child.isCDATA()) { continue; } + String text = child.getTextContent(); + if (text == null || (!text.contains("vars.") && !text.contains("vars["))) { + continue; + } + Range range = XMLPositionUtility.createRange(child.getStart(), child.getEnd(), document); + checkContentForUndefinedVariables(text, range, diagnostics, definedVariables); + } + } - // Extract ${...} or {${...}} expressions from the attribute value - Matcher exprMatcher = EXPRESSION_PATTERN.matcher(attrValue); - while (exprMatcher.find()) { - String exprContent = exprMatcher.group(1); - if (exprContent == null) { - exprContent = exprMatcher.group(2); // {${...}} form + /** + * Extracts every ${...}/{${...}} expression from {@code content}, and for each vars.X reference + * to a name not in {@code definedVariables}, adds an UndefinedVariable warning anchored at + * {@code range}. Shared by the attribute-value and text-content reference checks. + */ + private void checkContentForUndefinedVariables(String content, Range range, + List diagnostics, Set definedVariables) { + if (range == null) { + return; + } + Matcher exprMatcher = EXPRESSION_PATTERN.matcher(content); + while (exprMatcher.find()) { + String exprContent = exprMatcher.group(1); + if (exprContent == null) { + exprContent = exprMatcher.group(2); // {${...}} form + } + if (exprContent == null) { + continue; + } + + // Find vars.X references within the expression + Matcher varsMatcher = VARS_REF_PATTERN.matcher(exprContent); + while (varsMatcher.find()) { + // Get the variable name from whichever group matched + String varName = varsMatcher.group(1); + if (varName == null) varName = varsMatcher.group(2); + if (varName == null) varName = varsMatcher.group(3); + + if (varName != null && !definedVariables.contains(varName)) { + Diagnostic d = new Diagnostic(); + d.setRange(range); + d.setMessage( + "Variable '" + varName + "' is referenced but not defined in this file. " + + "If it is defined in a calling sequence, this warning can be ignored. " + + "Otherwise, define it using before this point."); + d.setSeverity(DiagnosticSeverity.Warning); + d.setSource(SOURCE); + d.setCode("UndefinedVariable"); + d.setData(varName); + diagnostics.add(d); } - if (exprContent == null) { - continue; + } + } + } + + /** + * Flags an opening "${" that is never closed by a matching "}". Such a value is not recognized + * as a Synapse expression (it is treated as a plain string), so the malformed expression would + * otherwise reach runtime with no feedback. Scans attribute values and leaf element text, + * mirroring the variable-reference checks; raw-code elements (see {@link #RAW_TEXT_ELEMENTS}) + * are skipped to avoid false positives. + */ + private void validateUnclosedExpressions(DOMElement element, List diagnostics, + DOMDocument document) { + List attrs = element.getAttributeNodes(); + if (attrs != null) { + for (DOMAttr attr : attrs) { + String attrValue = attr.getValue(); + if (attrValue != null && attrValue.contains("${") && hasUnclosedExpression(attrValue)) { + reportUnclosedExpression(XMLPositionUtility.selectAttributeValue(attr), diagnostics); } + } + } + + if (hasChildElements(element)) { + return; + } + String localName = element.getLocalName(); + if (localName != null && RAW_TEXT_ELEMENTS.contains(localName.toLowerCase())) { + return; + } + List children = element.getChildren(); + if (children == null) { + return; + } + for (DOMNode child : children) { + // Include CDATA: ${...} can appear inside payloads too. + if (!child.isText() && !child.isCDATA()) { + continue; + } + String text = child.getTextContent(); + if (text != null && text.contains("${") && hasUnclosedExpression(text)) { + reportUnclosedExpression( + XMLPositionUtility.createRange(child.getStart(), child.getEnd(), document), diagnostics); + } + } + } - // Find vars.X references within the expression - Matcher varsMatcher = VARS_REF_PATTERN.matcher(exprContent); - while (varsMatcher.find()) { - // Get the variable name from whichever group matched - String varName = varsMatcher.group(1); - if (varName == null) varName = varsMatcher.group(2); - if (varName == null) varName = varsMatcher.group(3); + private void reportUnclosedExpression(Range range, List diagnostics) { + if (range == null) { + return; + } + Diagnostic d = new Diagnostic(); + d.setRange(range); + d.setMessage("Unclosed expression: '${' is not terminated by a matching '}'. " + + "Synapse expressions must be written as ${...} — add the missing '}'."); + d.setSeverity(DiagnosticSeverity.Warning); + d.setSource(SOURCE); + d.setCode("UnclosedExpression"); + diagnostics.add(d); + } - if (varName != null && !definedVariables.contains(varName)) { - Range range = XMLPositionUtility.selectAttributeValue(attr); - if (range != null) { - Diagnostic d = new Diagnostic(); - d.setRange(range); - d.setMessage( - "Variable '" + varName + "' is referenced but not defined in this file. " + - "If it is defined in a calling sequence, this warning can be ignored. " + - "Otherwise, define it using before this point."); - d.setSeverity(DiagnosticSeverity.Warning); - d.setSource(SOURCE); - d.setCode("UndefinedVariable"); - d.setData(varName); - diagnostics.add(d); - } - } + /** + * Returns true if {@code value} contains an opening "${" with no matching "}" closing it. + * Synapse expressions have no "{"/"}" tokens of their own (indexing uses "[" "]"), so the only + * braces that can appear inside ${...} are within string literals — which are skipped here, so a + * valid expression such as {@code ${concat('{', x)}} is not mistaken for unclosed. + */ + private boolean hasUnclosedExpression(String value) { + int open = value.indexOf("${"); + while (open >= 0) { + if (!hasClosingBrace(value, open + 2)) { + return true; + } + open = value.indexOf("${", open + 2); + } + return false; + } + + /** + * Returns true if there is a '}' at or after {@code from} that lies outside any string literal. + */ + private boolean hasClosingBrace(String value, int from) { + boolean inString = false; + char quote = 0; + for (int i = from; i < value.length(); i++) { + char c = value.charAt(i); + if (inString) { + if (c == '\\') { + i++; // skip the escaped character + } else if (c == quote) { + inString = false; } + } else if (c == '"' || c == '\'') { + inString = true; + quote = c; + } else if (c == '}') { + return true; } } + return false; } /** @@ -1368,9 +1580,14 @@ private Set buildArtifactNameIndex(DOMDocument document, CancelChecker c return null; } + // Read the invalidation epoch up front: a cache entry built before the latest file change + // is treated as a miss even within its TTL, so a just-written sibling is picked up at once. + long epoch = artifactCacheEpoch.get(); + // Check cache first CachedArtifactIndex cached = artifactIndexCache.get(projectPath); - if (cached != null && (System.currentTimeMillis() - cached.timestamp) < ARTIFACT_CACHE_TTL_MS) { + if (cached != null && cached.epoch == epoch + && (System.currentTimeMillis() - cached.timestamp) < ARTIFACT_CACHE_TTL_MS) { // Restore all derived state so cross-reference checks see the same // template paths, duplicates, and cycles as a fresh build would. this.templateFilePaths = cached.templateFilePaths; @@ -1415,7 +1632,7 @@ private Set buildArtifactNameIndex(DOMDocument document, CancelChecker c this.duplicateArtifactNames = duplicates; this.cyclicArtifacts = cycles; artifactIndexCache.put(projectPath, new CachedArtifactIndex( - artifactNames, templatePaths, duplicates, cycles, System.currentTimeMillis())); + artifactNames, templatePaths, duplicates, cycles, System.currentTimeMillis(), epoch)); return artifactNames; } @@ -2219,17 +2436,20 @@ private static class CachedArtifactIndex { final Set duplicateArtifactNames; final Set cyclicArtifacts; final long timestamp; + final long epoch; CachedArtifactIndex(Set artifactNames, Map templateFilePaths, Set duplicateArtifactNames, Set cyclicArtifacts, - long timestamp) { + long timestamp, + long epoch) { this.artifactNames = artifactNames; this.templateFilePaths = templateFilePaths; this.duplicateArtifactNames = duplicateArtifactNames; this.cyclicArtifacts = cyclicArtifacts; this.timestamp = timestamp; + this.epoch = epoch; } } } diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/synapse/validator/SynapseExpressionValidator.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/synapse/validator/SynapseExpressionValidator.java index 6f6af2573..1800df10a 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/synapse/validator/SynapseExpressionValidator.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/extensions/synapse/validator/SynapseExpressionValidator.java @@ -64,7 +64,16 @@ public void startDocument(XMLLocator locator, String encoding, NamespaceContext private boolean isFileInArtifacts(String baseSystemId) { - return baseSystemId.contains(TryOutConstants.PROJECT_ARTIFACT_PATH.toString()); + if (baseSystemId == null) { + return false; + } + // Compare with forward slashes so the check holds on every OS: the document system id is + // typically a file:// URI (always '/'), while PROJECT_ARTIFACT_PATH.toString() uses the + // platform separator ('\' on Windows) — without normalizing, the gate would never match on + // Windows and expression validation would silently not run there. + String normalizedId = baseSystemId.replace('\\', '/'); + String artifactsPath = TryOutConstants.PROJECT_ARTIFACT_PATH.toString().replace('\\', '/'); + return normalizedId.contains(artifactsPath); } @Override diff --git a/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/inbound-endpoints/inbound_endpoints_440.json b/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/inbound-endpoints/inbound_endpoints_440.json index c128089ec..1580524b6 100644 --- a/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/inbound-endpoints/inbound_endpoints_440.json +++ b/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/inbound-endpoints/inbound_endpoints_440.json @@ -143,6 +143,11 @@ "name": "Google PubSub (Inbound)", "id": "org.wso2.carbon.inbound.googlepubsub.GooglePubSubMessageConsumer", "type": "inbound-endpoint" + }, + { + "name": "Azure Service Bus (Inbound)", + "id": "org.wso2.carbon.inbound.asb.ASBEventConsumer", + "type": "inbound-endpoint" } ] } diff --git a/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/inbound-endpoints/inbound_endpoints_450.json b/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/inbound-endpoints/inbound_endpoints_450.json index 71f52eace..bdbf13668 100644 --- a/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/inbound-endpoints/inbound_endpoints_450.json +++ b/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/inbound-endpoints/inbound_endpoints_450.json @@ -137,6 +137,11 @@ "name": "Google PubSub (Inbound)", "id": "org.wso2.carbon.inbound.googlepubsub.GooglePubSubMessageConsumer", "type": "inbound-endpoint" + }, + { + "name": "Azure Service Bus (Inbound)", + "id": "org.wso2.carbon.inbound.asb.ASBEventConsumer", + "type": "inbound-endpoint" } ] } diff --git a/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/inbound-endpoints/inbound_endpoints_460.json b/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/inbound-endpoints/inbound_endpoints_460.json index 7a40ee83a..645d4f6e5 100644 --- a/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/inbound-endpoints/inbound_endpoints_460.json +++ b/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/inbound-endpoints/inbound_endpoints_460.json @@ -131,6 +131,16 @@ "name": "RabbitMQ (Inbound)", "id": "org.wso2.carbon.inbound.rabbitmq.RabbitMQListener", "type": "inbound-endpoint" + }, + { + "name": "Solace (Inbound)", + "id": "org.wso2.carbon.inbound.solace.SolaceEventListener", + "type": "inbound-endpoint" + }, + { + "name": "Azure Service Bus (Inbound)", + "id": "org.wso2.carbon.inbound.asb.ASBEventConsumer", + "type": "inbound-endpoint" } ] } diff --git a/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/mediators/430/templates/connector.mustache b/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/mediators/430/templates/connector.mustache index 5afa03a56..fbb9f685e 100644 --- a/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/mediators/430/templates/connector.mustache +++ b/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/mediators/430/templates/connector.mustache @@ -1,5 +1,5 @@ -<{{tag}} {{#configKey}}configKey="{{{configKey}}}"{{/configKey}}> +<{{tag}}{{#configKey}} configKey="{{{configKey}}}"{{/configKey}}{{#description}} description="{{description}}"{{/description}}{{^hasParameters}}/>{{/hasParameters}}{{#hasParameters}}> {{#parameters}} <{{{name}}} {{#value}}{{#namespaces}} xmlns:{{{prefix}}}="{{{uri}}}"{{/namespaces}}>{{#isCDATA}}{{/isCDATA}}{{^isCDATA}}{{#isExpression}}{{value}}{{/isExpression}}{{^isExpression}}{{{value}}}{{/isExpression}}{{/isCDATA}}{{/value}} {{/parameters}} - +{{/hasParameters}} diff --git a/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/mediators/440/templates/connector.mustache b/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/mediators/440/templates/connector.mustache index 5afa03a56..fbb9f685e 100644 --- a/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/mediators/440/templates/connector.mustache +++ b/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/mediators/440/templates/connector.mustache @@ -1,5 +1,5 @@ -<{{tag}} {{#configKey}}configKey="{{{configKey}}}"{{/configKey}}> +<{{tag}}{{#configKey}} configKey="{{{configKey}}}"{{/configKey}}{{#description}} description="{{description}}"{{/description}}{{^hasParameters}}/>{{/hasParameters}}{{#hasParameters}}> {{#parameters}} <{{{name}}} {{#value}}{{#namespaces}} xmlns:{{{prefix}}}="{{{uri}}}"{{/namespaces}}>{{#isCDATA}}{{/isCDATA}}{{^isCDATA}}{{#isExpression}}{{value}}{{/isExpression}}{{^isExpression}}{{{value}}}{{/isExpression}}{{/isCDATA}}{{/value}} {{/parameters}} - +{{/hasParameters}} diff --git a/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/schemas/430/api.xsd b/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/schemas/430/api.xsd index bf49c6330..93d56092b 100644 --- a/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/schemas/430/api.xsd +++ b/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/schemas/430/api.xsd @@ -74,6 +74,7 @@ + diff --git a/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/schemas/430/misc/resource.xsd b/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/schemas/430/misc/resource.xsd index 2313b5360..5a10574af 100644 --- a/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/schemas/430/misc/resource.xsd +++ b/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/schemas/430/misc/resource.xsd @@ -75,6 +75,8 @@ + + diff --git a/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/schemas/430/task.xsd b/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/schemas/430/task.xsd index 4c5482d16..830cc1f11 100644 --- a/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/schemas/430/task.xsd +++ b/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/schemas/430/task.xsd @@ -49,6 +49,7 @@ + diff --git a/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/schemas/440/api.xsd b/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/schemas/440/api.xsd index bf49c6330..93d56092b 100644 --- a/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/schemas/440/api.xsd +++ b/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/schemas/440/api.xsd @@ -74,6 +74,7 @@ + diff --git a/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/schemas/440/misc/resource.xsd b/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/schemas/440/misc/resource.xsd index 2313b5360..5a10574af 100644 --- a/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/schemas/440/misc/resource.xsd +++ b/org.eclipse.lemminx/src/main/resources/org/eclipse/lemminx/schemas/440/misc/resource.xsd @@ -75,6 +75,8 @@ + + diff --git a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/synapse/CodeDiagnosticFileNameTest.java b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/synapse/CodeDiagnosticFileNameTest.java new file mode 100644 index 000000000..51315537e --- /dev/null +++ b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/synapse/CodeDiagnosticFileNameTest.java @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com). + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * WSO2 LLC - support for WSO2 Micro Integrator Configuration + */ + +package org.eclipse.lemminx.extensions.synapse; + +import org.eclipse.lemminx.AbstractCacheBasedTest; +import org.eclipse.lemminx.XMLAssert; +import org.eclipse.lemminx.customservice.synapse.utils.Utils; +import org.eclipse.lemminx.dom.DOMDocument; +import org.eclipse.lemminx.extensions.contentmodel.settings.ContentModelSettings; +import org.eclipse.lemminx.extensions.contentmodel.settings.XMLValidationRootSettings; +import org.eclipse.lemminx.services.XMLLanguageService; +import org.eclipse.lsp4j.Diagnostic; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Tests for the {@code synapse/codeDiagnostic} document-URI plumbing. + * + *

The MI Copilot agent validates in-memory (unsaved) code via {@code synapse/codeDiagnostic}. + * Before the fix this path parsed the code with the literal URI {@code "temp"}, which made the + * URI-gated {@code SynapseExpressionValidator} (it only runs for files under + * {@code src/main/wso2mi/artifacts}) silently skip every expression diagnostic. The fix lets the + * caller supply the real file name as the document URI (with {@code "temp"} as the backward + * compatible fallback). + * + *

These tests exercise the exact pipeline {@code SynapseLanguageService.codeDiagnostic()} runs + * internally — {@code Utils.getDOMDocument(code, uri, resolver)} followed by + * {@code XMLLanguageService.doDiagnostics(...)} — and assert the operator-precedence warning is + * surfaced only when the URI is under the artifacts path. + */ +public class CodeDiagnosticFileNameTest extends AbstractCacheBasedTest { + + private static final String SYNAPSE_NS = "http://ws.apache.org/ns/synapse"; + private static final String SYNAPSE_CATALOG_440 = + "src/main/resources/org/eclipse/lemminx/schemas/440/catalog.xml"; + // An absolute path under the project artifacts directory, as the MI extension sends. The + // SynapseExpressionValidator gate is separator-agnostic, so this forward-slash URI works on all OSes. + private static final String ARTIFACT_URI = + "/home/proj/src/main/wso2mi/artifacts/sequences/Test.xml"; + + // with an unparenthesized comparison/logical mix — the precedence pitfall. + private static final String UNPARENTHESIZED = "" + + "" + + ""; + // The corrected, explicitly parenthesized form. + private static final String PARENTHESIZED = "" + + "" + + ""; + + /** + * Reproduces the codeDiagnostic pipeline: build the DOM document with the given URI (via the + * fileName-aware overload the fix added), then run the full XML diagnostics pipeline. + */ + private List codeDiagnostic(String code, String fileName) { + XMLLanguageService ls = new XMLLanguageService(); + String uri = fileName != null ? fileName : "temp"; + DOMDocument document = Utils.getDOMDocument(code, uri, ls.getResolverExtensionManager()); + ls.setDocumentProvider(u -> document); + + ContentModelSettings settings = new ContentModelSettings(); + settings.setUseCache(false); + XMLValidationRootSettings validation = new XMLValidationRootSettings(); + validation.setNoGrammar("ignore"); + settings.setValidation(validation); + settings.setCatalogs(new String[]{SYNAPSE_CATALOG_440}); + ls.doSave(new XMLAssert.SettingsSaveContext(settings)); + + return ls.doDiagnostics(document, settings.getValidation(), Collections.emptyMap(), () -> {}); + } + + private List precedenceWarnings(List diagnostics) { + return diagnostics.stream() + .filter(d -> d.getMessage() != null && d.getMessage().contains("Operator precedence")) + .collect(Collectors.toList()); + } + + @Test + public void testPrecedenceWarningReturnedForArtifactsPath() { + // With a real artifacts file name, the expression validator runs and reports the warning. + List diags = codeDiagnostic(UNPARENTHESIZED, ARTIFACT_URI); + assertFalse(precedenceWarnings(diags).isEmpty(), + "codeDiagnostic with an artifacts file name should surface the operator-precedence warning"); + } + + @Test + public void testNoPrecedenceWarningForParenthesizedExpression() { + // The corrected, parenthesized form must not be flagged even on the artifacts path. + List diags = codeDiagnostic(PARENTHESIZED, ARTIFACT_URI); + assertTrue(precedenceWarnings(diags).isEmpty(), + "Parenthesized comparisons should produce no operator-precedence warning"); + } + + @Test + public void testTempFallbackDocumentsUnchangedBehavior() { + // Backward compatibility: a null file name falls back to "temp", which is not under the + // artifacts path, so the expression validator stays disabled (unchanged pre-fix behavior). + List diags = codeDiagnostic(UNPARENTHESIZED, null); + assertTrue(precedenceWarnings(diags).isEmpty(), + "The \"temp\" fallback keeps the expression validator disabled, as before the fix"); + } + + // ===== URI plumbing unit checks (deterministic, no validation pipeline) ===== + + @Test + public void testGetDOMDocumentUsesProvidedUri() { + DOMDocument document = Utils.getDOMDocument(UNPARENTHESIZED, ARTIFACT_URI, null); + assertEquals(ARTIFACT_URI, document.getDocumentURI(), + "The 3-arg overload should assign the supplied URI to the document"); + } + + @Test + public void testGetDOMDocumentDefaultStillUsesTemp() { + // The legacy overloads (which delegate with no URI) must keep the "temp" URI unchanged. + DOMDocument document = Utils.getDOMDocument(UNPARENTHESIZED); + assertEquals("temp", document.getDocumentURI(), + "The legacy overloads should keep the \"temp\" URI for backward compatibility"); + } + + @Test + public void testCodeDiagnosticRequestCarriesFileName() { + org.eclipse.lemminx.customservice.synapse.CodeDiagnosticRequest request = + new org.eclipse.lemminx.customservice.synapse.CodeDiagnosticRequest(); + request.setCode(UNPARENTHESIZED); + request.setFileName(ARTIFACT_URI); + assertEquals(UNPARENTHESIZED, request.getCode()); + assertEquals(ARTIFACT_URI, request.getFileName(), + "CodeDiagnosticRequest must carry the fileName sent by the extension"); + } + + @Test + public void testCodeDiagnosticRequestSkipCrossFileDefaultsFalse() { + org.eclipse.lemminx.customservice.synapse.CodeDiagnosticRequest request = + new org.eclipse.lemminx.customservice.synapse.CodeDiagnosticRequest(); + assertFalse(request.isSkipCrossFileValidation(), + "skipCrossFileValidation must default to false so the editor/validate-all paths are unchanged"); + request.setSkipCrossFileValidation(true); + assertTrue(request.isSkipCrossFileValidation()); + } +} diff --git a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/synapse/SynapseDiagnosticsParticipantTest.java b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/synapse/SynapseDiagnosticsParticipantTest.java index 6ba9e5610..585b37d47 100644 --- a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/synapse/SynapseDiagnosticsParticipantTest.java +++ b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/extensions/synapse/SynapseDiagnosticsParticipantTest.java @@ -468,6 +468,171 @@ public void testResponseVariableDefinesVariable() { assertTrue(diags.isEmpty(), "responseVariable child element should define the variable"); } + // ===== Variable references in element text content (Issue #4) ===== + + @Test + public void testUndefinedVariableInElementText() { + // Connector operation parameter references vars.X in element text, not in an attribute. + // 'soqlQuery1' is a typo for the defined 'soqlQuery' — this is the exact MI Copilot case. + String xml = "" + + "" + + "" + + "{${vars.soqlQuery1}}" + + "sfResponse" + + "false" + + "" + + ""; + List diags = diagnosticsWithCode(diagnose(xml), "UndefinedVariable"); + assertEquals(1, diags.size(), "Undefined variable referenced in element text should warn"); + assertEquals(DiagnosticSeverity.Warning, diags.get(0).getSeverity()); + assertTrue(diags.get(0).getMessage().contains("soqlQuery1")); + } + + @Test + public void testDefinedVariableInElementTextNoWarning() { + // The correctly-spelled variable referenced in element text should not warn. + String xml = "" + + "" + + "" + + "{${vars.soqlQuery}}" + + "sfResponse" + + "" + + ""; + List diags = diagnosticsWithCode(diagnose(xml), "UndefinedVariable"); + assertTrue(diags.isEmpty(), "Defined variable referenced in element text should not warn"); + } + + @Test + public void testUndefinedVariableInElementTextPlainExpressionForm() { + // The plain ${...} form (not {${...}}) inside element text should also be detected. + String xml = "" + + "" + + "{\"id\": \"${vars.missingId}\"}" + + "" + + ""; + List diags = diagnosticsWithCode(diagnose(xml), "UndefinedVariable"); + assertEquals(1, diags.size(), "Undefined variable in ${...} text form should warn"); + assertTrue(diags.get(0).getMessage().contains("missingId")); + } + + @Test + public void testScriptBodyNotScannedForVariables() { + // Raw-code (script) bodies must not be treated as Synapse expressions (no false positives). + String xml = "" + + "" + + ""; + List diags = diagnosticsWithCode(diagnose(xml), "UndefinedVariable"); + assertTrue(diags.isEmpty(), "Script body should not be scanned for variable references"); + } + + @Test + public void testPlainTextWithoutExpressionNoWarning() { + // Element text that merely contains the literal 'vars.' but no ${...} expression must not warn. + String xml = "" + + "" + + "SELECT vars.field FROM Account" + + "sfResponse" + + "" + + ""; + List diags = diagnosticsWithCode(diagnose(xml), "UndefinedVariable"); + assertTrue(diags.isEmpty(), "Plain text without a ${...} expression should not warn"); + } + + // ===== Unclosed expression delimiters ===== + + @Test + public void testUnclosedExpressionInAttributeWarns() { + // '${' opened but never closed — previously treated as a plain string with no feedback. + String xml = "" + + " 0\"/>" + + ""; + List diags = diagnosticsWithCode(diagnose(xml), "UnclosedExpression"); + assertEquals(1, diags.size(), "An unclosed ${ in an attribute should warn"); + assertEquals(DiagnosticSeverity.Warning, diags.get(0).getSeverity()); + assertTrue(diags.get(0).getMessage().contains("Unclosed expression")); + } + + @Test + public void testClosedExpressionInAttributeNoWarning() { + String xml = "" + + " 0}\"/>" + + ""; + List diags = diagnosticsWithCode(diagnose(xml), "UnclosedExpression"); + assertTrue(diags.isEmpty(), "A properly closed ${...} must not be flagged"); + } + + @Test + public void testUnclosedExpressionInElementTextWarns() { + // Same gap in element text (e.g. a connector operation parameter). + String xml = "" + + "" + + "${payload.count" + + "" + + ""; + List diags = diagnosticsWithCode(diagnose(xml), "UnclosedExpression"); + assertEquals(1, diags.size(), "An unclosed ${ in element text should warn"); + assertTrue(diags.get(0).getMessage().contains("Unclosed expression")); + } + + @Test + public void testWrappedClosedExpressionNoWarning() { + // The {${...}} form, properly closed, must not be flagged as unclosed. + String xml = "" + + "" + + "{${vars.q}}" + + ""; + List diags = diagnosticsWithCode(diagnose(xml), "UnclosedExpression"); + assertTrue(diags.isEmpty(), "A closed {${...}} expression must not be flagged"); + } + + @Test + public void testExpressionWithBraceInStringLiteralNoWarning() { + // A '}' inside a string literal must not be mistaken for the closing delimiter, and the + // real closing '}' must still be recognized — so this valid expression is not flagged. + String xml = "" + + "" + + ""; + List diags = diagnosticsWithCode(diagnose(xml), "UnclosedExpression"); + assertTrue(diags.isEmpty(), "A brace inside a string literal must not cause a false positive"); + } + + @Test + public void testScriptBodyUnclosedExpressionNotFlagged() { + // Raw-code (script) bodies are excluded — a ${ in JS is not a Synapse expression. + String xml = "" + + "" + + ""; + List diags = diagnosticsWithCode(diagnose(xml), "UnclosedExpression"); + assertTrue(diags.isEmpty(), "Script bodies must not be scanned for unclosed expressions"); + } + + // ===== CDATA payloads are scanned too ===== + + @Test + public void testUndefinedVariableInCdataWarns() { + // ${vars.x} inside a CDATA payload (e.g. payloadFactory format) must still be validated. + String xml = "" + + "" + + "" + + "" + + ""; + List diags = diagnosticsWithCode(diagnose(xml), "UndefinedVariable"); + assertEquals(1, diags.size(), "Undefined variable referenced inside CDATA should warn"); + assertTrue(diags.get(0).getMessage().contains("missingCdata")); + } + + @Test + public void testUnclosedExpressionInCdataWarns() { + // An unclosed ${ inside a CDATA payload must still be flagged. + String xml = "" + + "" + + "" + + "" + + ""; + List diags = diagnosticsWithCode(diagnose(xml), "UnclosedExpression"); + assertEquals(1, diags.size(), "Unclosed ${ inside CDATA should warn"); + } + // ===== Non-Synapse document skipping ===== @Test @@ -1518,6 +1683,7 @@ public void restoreUserHome() { originalUserHome = null; } SynapseLanguageService.setLoadedResourceFinder(null); + SynapseDiagnosticsParticipant.clearSkipCrossFileValidation(); } /** @@ -1606,4 +1772,139 @@ public void testUnknownSequenceStillFlaggedWithDependencies(@TempDir Path tempDi assertEquals(1, unresolved.size()); assertTrue(unresolved.get(0).getMessage().contains("reallyDoesNotExist")); } + + // ===== skipCrossFileValidation opt-out (Change 1) ===== + + /** As {@link #diagnoseAtPath(String, Path)} but with the request-scoped cross-file opt-out set. */ + private List diagnoseAtPath(String xml, Path xmlFilePath, boolean skipCrossFile) throws Exception { + try { + SynapseDiagnosticsParticipant.setSkipCrossFileValidation(skipCrossFile); + return diagnoseAtPath(xml, xmlFilePath); + } finally { + SynapseDiagnosticsParticipant.clearSkipCrossFileValidation(); + } + } + + @Test + public void testSkipCrossFileValidationSuppressesUnresolvedButKeepsWithinFileChecks(@TempDir Path tempDir) + throws Exception { + originalUserHome = System.getProperty("user.home"); + System.setProperty("user.home", tempDir.toString()); + + Path consumer = tempDir.resolve("consumer"); + Path apiXml = consumer.resolve("src/main/wso2mi/artifacts/apis/cvh.xml"); + // References a sequence that does not exist (cross-file) AND an undefined variable (within-file). + String xml = "" + + "" + + "" + + "" + + ""; + + List diags = diagnoseAtPath(xml, apiXml, true); + assertTrue(diagnosticsWithCode(diags, "UnresolvedArtifactReference").isEmpty(), + "skipCrossFileValidation must suppress the cross-file UnresolvedArtifactReference"); + assertEquals(1, diagnosticsWithCode(diags, "UndefinedVariable").size(), + "Within-file UndefinedVariable must still be reported when cross-file checks are skipped"); + } + + @Test + public void testCrossFileValidationDefaultStillFlagsUnresolved(@TempDir Path tempDir) throws Exception { + originalUserHome = System.getProperty("user.home"); + System.setProperty("user.home", tempDir.toString()); + + Path consumer = tempDir.resolve("consumer"); + Path apiXml = consumer.resolve("src/main/wso2mi/artifacts/apis/cvh.xml"); + String xml = "" + + "" + + "" + + ""; + + // Default (flag false) — cross-file validation runs and flags the unresolved reference. + List diags = diagnoseAtPath(xml, apiXml, false); + assertEquals(1, diagnosticsWithCode(diags, "UnresolvedArtifactReference").size(), + "With cross-file validation on (default), an unresolved reference must still be flagged"); + } + + // ===== Cached artifact index invalidation (Change 2) ===== + + /** Runs diagnostics with a caller-supplied participant so its artifact-index cache persists across calls. */ + private List diagnoseAtPathWith(SynapseDiagnosticsParticipant participant, String xml, + Path xmlFilePath) throws Exception { + Files.createDirectories(xmlFilePath.getParent()); + Files.writeString(xmlFilePath, xml); + TextDocument textDocument = new TextDocument(xml, xmlFilePath.toUri().toString()); + DOMDocument document = DOMParser.getInstance().parse(textDocument, null); + List diagnostics = new ArrayList<>(); + participant.doDiagnostics(document, diagnostics, null, () -> {}); + return diagnostics; + } + + @Test + public void testInvalidateArtifactIndexCacheRebuildsAfterFileChange(@TempDir Path tempDir) throws Exception { + originalUserHome = System.getProperty("user.home"); + System.setProperty("user.home", tempDir.toString()); + + Path consumer = tempDir.resolve("consumer"); + Path apiXml = consumer.resolve("src/main/wso2mi/artifacts/apis/cvh.xml"); + String api = "" + + "" + + ""; + + // Reuse one participant so its cross-file index cache survives across calls (as in production). + SynapseDiagnosticsParticipant participant = new SynapseDiagnosticsParticipant(); + + // 1. Sibling does not exist yet -> unresolved, and the index is now cached for this project. + List first = diagnoseAtPathWith(participant, api, apiXml); + assertEquals(1, diagnosticsWithCode(first, "UnresolvedArtifactReference").size(), + "Sibling 'sibling' does not exist yet -> should be flagged unresolved"); + + // 2. Write the sibling on disk. Within the TTL and without invalidation the cache is stale. + Path siblingXml = consumer.resolve("src/main/wso2mi/artifacts/sequences/sibling.xml"); + Files.createDirectories(siblingXml.getParent()); + Files.writeString(siblingXml, ""); + + List stale = diagnoseAtPathWith(participant, api, apiXml); + assertEquals(1, diagnosticsWithCode(stale, "UnresolvedArtifactReference").size(), + "Within the TTL and without invalidation, the stale cached index still flags it unresolved"); + + // 3. Invalidate -> the next run rebuilds the index and resolves the now-present sibling. + SynapseDiagnosticsParticipant.invalidateArtifactIndexCache(); + List fresh = diagnoseAtPathWith(participant, api, apiXml); + assertTrue(diagnosticsWithCode(fresh, "UnresolvedArtifactReference").isEmpty(), + "After invalidation the rebuilt index includes the new sibling -> no longer unresolved"); + } + + @Test + public void testStaleCrossFileStateNotLeakedWhenIndexUnavailable(@TempDir Path tempDir) throws Exception { + originalUserHome = System.getProperty("user.home"); + System.setProperty("user.home", tempDir.toString()); + + Path project = tempDir.resolve("proj"); + // Two artifacts sharing a name -> "DupSeq" becomes a known duplicate for this project. + Path seqA = project.resolve("src/main/wso2mi/artifacts/sequences/a.xml"); + Path seqB = project.resolve("src/main/wso2mi/artifacts/sequences/b.xml"); + Files.createDirectories(seqA.getParent()); + Files.writeString(seqA, ""); + Files.writeString(seqB, ""); + + // Reuse one participant so its instance-level cross-file state persists across requests. + SynapseDiagnosticsParticipant participant = new SynapseDiagnosticsParticipant(); + + // Request A: validate a doc inside the project so the duplicate index is built into the + // participant's instance state (duplicateArtifactNames = { "DupSeq" }). + Path apiXml = project.resolve("src/main/wso2mi/artifacts/apis/cvh.xml"); + diagnoseAtPathWith(participant, "" + + "", + apiXml); + + // Request B (same participant): a doc named "DupSeq" whose project path is not derivable, so + // the cross-file index is unavailable. The stale duplicate state must be cleared, not reused. + TextDocument textB = new TextDocument( + "", "test.xml"); + DOMDocument docB = DOMParser.getInstance().parse(textB, null); + List diagsB = new ArrayList<>(); + participant.doDiagnostics(docB, diagsB, null, () -> {}); + assertTrue(diagnosticsWithCode(diagsB, "DuplicateArtifactName").isEmpty(), + "Stale cross-file duplicate state must not leak to a request with no project index"); + } } diff --git a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/synapse/directorytree/builder/DirectoryTreeBuilderTest.java b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/synapse/directorytree/builder/DirectoryTreeBuilderTest.java index 7927b3cff..2babf94f4 100644 --- a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/synapse/directorytree/builder/DirectoryTreeBuilderTest.java +++ b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/synapse/directorytree/builder/DirectoryTreeBuilderTest.java @@ -129,7 +129,7 @@ void getProjectIdentifiersWithEmptyArtifactList() { private static JsonObject sanitizeJson(JsonObject jsonObject) { JsonObject sanitizedJson = new JsonObject(); for (String key : jsonObject.keySet()) { - if (!(key.equals("path") || key.equals("registryPath"))) { + if (!(key.equals("path") || key.equals("registryPath") || key.equals("mcpConfigReference") || key.equals("isMcpConfig"))) { JsonElement value = jsonObject.get(key); if (value.isJsonObject()) { sanitizedJson.add(key, sanitizeJson(value.getAsJsonObject())); diff --git a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/synapse/expression/ExpressionValidatorTest.java b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/synapse/expression/ExpressionValidatorTest.java index fe49955d7..d97c1dc59 100644 --- a/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/synapse/expression/ExpressionValidatorTest.java +++ b/org.eclipse.lemminx/src/test/java/org/eclipse/lemminx/synapse/expression/ExpressionValidatorTest.java @@ -441,4 +441,68 @@ public void testZeroArrayIndexNoWarning() { List errors = ExpressionValidator.validate("payload[0]"); assertTrue(errors.isEmpty(), "Zero array index should produce no warnings"); } + + // ===== Operator precedence: logical operator (and/or) as a comparison operand ===== + // In the Synapse expression grammar 'and'/'or' bind TIGHTER than the comparison operators, + // so 'a <= 0 or b > 10' parses as 'a <= (0 or b) > 10' rather than '(a <= 0) or (b > 10)'. + + @Test + public void testOrBindsTighterThanComparisonWarns() { + // Parses as payload.id <= (0 or payload.id) > 10 — almost certainly not intended. + List errors = ExpressionValidator.validate("payload.id <= 0 or payload.id > 10"); + assertEquals(1, errors.size(), "Mixing 'or' with comparisons without parentheses should warn once"); + assertTrue(errors.get(0).getMessage().contains("Operator precedence"), + "Should warn about operator precedence: " + errors.get(0).getMessage()); + assertTrue(errors.get(0).isWarning(), "Precedence issue should be a warning, not an error"); + } + + @Test + public void testCopilotPrecedenceExampleWarns() { + // The exact expression MI Copilot generated (Issue #1). + List errors = ExpressionValidator.validate( + "integer(params.queryParams.id) <= 0 or integer(params.queryParams.id) > 10"); + assertTrue(errors.stream().anyMatch(e -> e.getMessage().contains("Operator precedence")), + "Copilot precedence example should produce a precedence warning"); + assertTrue(errors.stream().filter(e -> e.getMessage().contains("Operator precedence")) + .allMatch(ExpressionError::isWarning), + "Precedence diagnostic should be a warning"); + } + + @Test + public void testAndBindsTighterThanComparisonWarns() { + // Parses as (payload.a and payload.b) <= 5. + List errors = ExpressionValidator.validate("payload.a and payload.b <= 5"); + assertEquals(1, errors.size(), "Mixing 'and' with a comparison without parentheses should warn once"); + assertTrue(errors.get(0).getMessage().contains("Operator precedence"), + "Should warn about operator precedence: " + errors.get(0).getMessage()); + assertTrue(errors.get(0).isWarning(), "Precedence issue should be a warning, not an error"); + } + + @Test + public void testParenthesizedComparisonsNoWarning() { + // The corrected form: each comparison wrapped in parentheses. No precedence ambiguity. + List errors = ExpressionValidator.validate( + "(payload.id <= 0) or (payload.id > 10)"); + assertTrue(errors.isEmpty(), "Parenthesized comparisons should produce no precedence warning"); + } + + @Test + public void testPureLogicalExpressionNoWarning() { + // No comparison operator present — pure logical expression, nothing to flag. + List errors = ExpressionValidator.validate("payload.a and payload.b"); + assertTrue(errors.isEmpty(), "Pure logical expression without a comparison should not warn"); + } + + @Test + public void testPureComparisonNoWarning() { + // No logical operator present — pure comparison, nothing to flag. + List errors = ExpressionValidator.validate("payload.x <= 10"); + assertTrue(errors.isEmpty(), "Pure comparison without a logical operator should not warn"); + } + + @Test + public void testComparisonBetweenTwoAccessesNoWarning() { + List errors = ExpressionValidator.validate("payload.x < payload.y"); + assertTrue(errors.isEmpty(), "Comparison between two accesses should not warn"); + } } diff --git a/org.eclipse.lemminx/src/test/resources/synapse/directorytree.builder/generated-directory-tree.json b/org.eclipse.lemminx/src/test/resources/synapse/directorytree.builder/generated-directory-tree.json index a792654f1..d343531c1 100644 --- a/org.eclipse.lemminx/src/test/resources/synapse/directorytree.builder/generated-directory-tree.json +++ b/org.eclipse.lemminx/src/test/resources/synapse/directorytree.builder/generated-directory-tree.json @@ -1 +1 @@ -{"src":{"main":{"wso2mi":{"artifacts":{"apis":[{"context":"/test","resources":[{"methods":"GET","uriTemplate":"/","urlMapping":null}],"sequences":[],"endpoints":[],"type":"API","subType":null,"name":"testApi","isFaulty":false}],"endpoints":[{"type":"ENDPOINT","subType":"DEFAULT_ENDPOINT","name":"testEndpoint1","isFaulty":false},{"isRegistryResource":true,"type":"ENDPOINT","subType":"DEFAULT_ENDPOINT","name":"testEndpoint1","isFaulty":false},{"type":"ENDPOINT","subType":"HTTP_ENDPOINT","name":"testEndpoint2","isFaulty":false},{"isRegistryResource":true,"type":"ENDPOINT","subType":"HTTP_ENDPOINT","name":"testEndpoint2","isFaulty":false},{"isRegistryResource":true,"type":"ENDPOINT","subType":"DEFAULT_ENDPOINT","name":"testEndpoint3","isFaulty":false},{"isRegistryResource":true,"type":"ENDPOINT","subType":"HTTP_ENDPOINT","name":"testEndpoint4","isFaulty":false}],"sequences":[{"isMainSequence":true,"sequences":[],"endpoints":[],"type":"SEQUENCE","subType":null,"name":"testSequence1","isFaulty":false},{"isRegistryResource":true,"type":"SEQUENCE","subType":null,"name":"testSequence1","isFaulty":false},{"isRegistryResource":true,"type":"SEQUENCE","subType":null,"name":"testSequence2","isFaulty":false}],"proxyServices":[{"sequences":[],"endpoints":[],"type":"PROXY_SERVICE","subType":null,"name":"testProxy1","isFaulty":false}],"inboundEndpoints":[{"sequences":[],"endpoints":[],"type":"INBOUND_ENDPOINT","subType":"HTTPS","name":"HttpsInboundEndpoint","isFaulty":false}],"messageStores":[{"type":"MESSAGE_STORE","subType":"IN_MEMORY","name":"testMessageStore","isFaulty":false}],"messageProcessors":[{"type":"MESSAGE_PROCESSOR","subType":"MESSAGE_SAMPLING","name":"testMessageProcessor","isFaulty":false}],"tasks":[{"type":"TASK","subType":null,"name":"testTask","isFaulty":false}],"localEntries":[{"type":"LOCAL_ENTRY","subType":null,"name":"testLocalEntry","isFaulty":false}],"connections":[{"connectorName":"http","connectionType":"HTTPS","type":"localEntry","subType":null,"name":"HttpsCon","isFaulty":false}],"templates":[{"type":"TEMPLATE","subType":"DEFAULT_ENDPOINT","name":"testEndpointTemplate","isFaulty":false},{"isRegistryResource":true,"type":"TEMPLATE","subType":"DEFAULT_ENDPOINT","name":"testEndpointTemplate1","isFaulty":false},{"isRegistryResource":true,"type":"TEMPLATE","subType":"DEFAULT_ENDPOINT","name":"testEndpointTemplate2","isFaulty":false},{"type":"TEMPLATE","subType":"SEQUENCE","name":"testSequenceTemplate","isFaulty":false},{"isRegistryResource":true,"type":"TEMPLATE","subType":"SEQUENCE","name":"testSequenceTemplate1","isFaulty":false},{"isRegistryResource":true,"type":"TEMPLATE","subType":"SEQUENCE","name":"testSequenceTemplate2","isFaulty":false}],"dataServices":[{"type":"DATA_SERVICE","subType":null,"name":"RDBMSDataservice","isFaulty":false}],"dataSources":[{"type":"DATA_SOURCE","subType":null,"name":"RDBMSDatasource","isFaulty":false}]},"resources":{"registry":{"conf":null,"gov":{"name":"gov","files":[],"folders":[{"name":"datamapper","files":[{"name":"sample.dmc"}],"folders":[]},{"name":"endpoints","files":[{"name":"testEndpoint1.xml"},{"name":"testEndpoint2.xml"}],"folders":[]},{"name":"js","files":[{"name":"test.js"}],"folders":[]},{"name":"json","files":[{"name":"test.json"}],"folders":[]},{"name":"sequences","files":[{"name":"testSequence1.xml"}],"folders":[]},{"name":"smooks","files":[{"name":"test_smooks_config.xml"}],"folders":[]},{"name":"swagger","files":[{"name":"swagger.json"},{"name":"swagger.yaml"}],"folders":[]},{"name":"templates","files":[{"name":"testEndpointTemplate1.xml"},{"name":"testSequenceTemplate1.xml"}],"folders":[]},{"name":"ws_policy","files":[{"name":"ws_policy.xml"}],"folders":[]},{"name":"wsdl","files":[{"name":"wsdlfile.wsdl"}],"folders":[]},{"name":"xsd","files":[{"name":"sample.xsd"}],"folders":[]},{"name":"xsl","files":[{"name":"sample.xsl"}],"folders":[]},{"name":"xslt","files":[{"name":"sample.xslt"}],"folders":[]},{"name":"yaml","files":[{"name":"sample.yaml"}],"folders":[]}]}},"connectors":[],"metadata":[],"newResources":{"name":"resources","files":[],"folders":[{"name":"conf","files":[{"name":"config.properties"}],"folders":[]},{"name":"endpoints","files":[{"name":"testEndpoint3.xml"},{"name":"testEndpoint4.xml"}],"folders":[]},{"name":"js","files":[{"name":"test1.js"}],"folders":[]},{"name":"json","files":[{"name":"test1.json"}],"folders":[]},{"name":"registry","files":[],"folders":[{"name":"gov","files":[],"folders":[{"name":"datamapper","files":[{"name":"sample.dmc"}],"folders":[]},{"name":"endpoints","files":[{"name":"testEndpoint1.xml"},{"name":"testEndpoint2.xml"}],"folders":[]},{"name":"js","files":[{"name":"test.js"}],"folders":[]},{"name":"json","files":[{"name":"test.json"}],"folders":[]},{"name":"sequences","files":[{"name":"testSequence1.xml"}],"folders":[]},{"name":"smooks","files":[{"name":"test_smooks_config.xml"}],"folders":[]},{"name":"swagger","files":[{"name":"swagger.json"},{"name":"swagger.yaml"}],"folders":[]},{"name":"templates","files":[{"name":"testEndpointTemplate1.xml"},{"name":"testSequenceTemplate1.xml"}],"folders":[]},{"name":"ws_policy","files":[{"name":"ws_policy.xml"}],"folders":[]},{"name":"wsdl","files":[{"name":"wsdlfile.wsdl"}],"folders":[]},{"name":"xsd","files":[{"name":"sample.xsd"}],"folders":[]},{"name":"xsl","files":[{"name":"sample.xsl"}],"folders":[]},{"name":"xslt","files":[{"name":"sample.xslt"}],"folders":[]},{"name":"yaml","files":[{"name":"sample.yaml"}],"folders":[]}]}]},{"name":"sequences","files":[{"name":"testSequence2.xml"}],"folders":[]},{"name":"smooks","files":[{"name":"test_smooks_config1.xml"}],"folders":[]},{"name":"swagger","files":[{"name":"swagger1.json"},{"name":"swagger1.yaml"}],"folders":[]},{"name":"templates","files":[{"name":"testEndpointTemplate2.xml"},{"name":"testSequenceTemplate2.xml"}],"folders":[]},{"name":"ws_policy","files":[{"name":"ws_policy1.xml"}],"folders":[]},{"name":"wsdl","files":[{"name":"wsdlfile1.wsdl"}],"folders":[]},{"name":"xsd","files":[{"name":"sample1.xsd"}],"folders":[]},{"name":"xsl","files":[{"name":"sample1.xsl"}],"folders":[]},{"name":"xslt","files":[{"name":"sample1.xslt"}],"folders":[]},{"name":"yaml","files":[{"name":"sample1.yaml"}],"folders":[]}]}}},"java":null,"ballerina":null},"tests":{"wso2mi":null,"java":null}}} \ No newline at end of file +{"src":{"main":{"wso2mi":{"artifacts":{"apis":[{"context":"/test","resources":[{"methods":"GET","uriTemplate":"/","urlMapping":null}],"sequences":[],"endpoints":[],"type":"API","subType":null,"name":"testApi","isFaulty":false}],"endpoints":[{"type":"ENDPOINT","subType":"DEFAULT_ENDPOINT","name":"testEndpoint1","isFaulty":false},{"isRegistryResource":true,"type":"ENDPOINT","subType":"DEFAULT_ENDPOINT","name":"testEndpoint1","isFaulty":false},{"type":"ENDPOINT","subType":"HTTP_ENDPOINT","name":"testEndpoint2","isFaulty":false},{"isRegistryResource":true,"type":"ENDPOINT","subType":"HTTP_ENDPOINT","name":"testEndpoint2","isFaulty":false},{"isRegistryResource":true,"type":"ENDPOINT","subType":"DEFAULT_ENDPOINT","name":"testEndpoint3","isFaulty":false},{"isRegistryResource":true,"type":"ENDPOINT","subType":"HTTP_ENDPOINT","name":"testEndpoint4","isFaulty":false}],"sequences":[{"isMainSequence":true,"sequences":[],"endpoints":[],"type":"SEQUENCE","subType":null,"name":"testSequence1","isFaulty":false},{"isRegistryResource":true,"type":"SEQUENCE","subType":null,"name":"testSequence1","isFaulty":false},{"isRegistryResource":true,"type":"SEQUENCE","subType":null,"name":"testSequence2","isFaulty":false}],"proxyServices":[{"sequences":[],"endpoints":[],"type":"PROXY_SERVICE","subType":null,"name":"testProxy1","isFaulty":false}],"inboundEndpoints":[{"sequences":[],"endpoints":[],"type":"INBOUND_ENDPOINT","subType":"HTTPS","name":"HttpsInboundEndpoint","isFaulty":false}],"messageStores":[{"type":"MESSAGE_STORE","subType":"IN_MEMORY","name":"testMessageStore","isFaulty":false}],"messageProcessors":[{"type":"MESSAGE_PROCESSOR","subType":"MESSAGE_SAMPLING","name":"testMessageProcessor","isFaulty":false}],"tasks":[{"type":"TASK","subType":null,"name":"testTask","isFaulty":false}],"localEntries":[{"type":"LOCAL_ENTRY","subType":null,"name":"testLocalEntry","isFaulty":false}],"connections":[{"connectorName":"http","connectionType":"HTTPS","type":"localEntry","subType":null,"name":"HttpsCon","isFaulty":false}],"templates":[{"type":"TEMPLATE","subType":"DEFAULT_ENDPOINT","name":"testEndpointTemplate","isFaulty":false},{"isRegistryResource":true,"type":"TEMPLATE","subType":"DEFAULT_ENDPOINT","name":"testEndpointTemplate1","isFaulty":false},{"isRegistryResource":true,"type":"TEMPLATE","subType":"DEFAULT_ENDPOINT","name":"testEndpointTemplate2","isFaulty":false},{"type":"TEMPLATE","subType":"SEQUENCE","name":"testSequenceTemplate","isFaulty":false},{"isRegistryResource":true,"type":"TEMPLATE","subType":"SEQUENCE","name":"testSequenceTemplate1","isFaulty":false},{"isRegistryResource":true,"type":"TEMPLATE","subType":"SEQUENCE","name":"testSequenceTemplate2","isFaulty":false}],"dataServices":[{"type":"DATA_SERVICE","subType":null,"name":"RDBMSDataservice","isFaulty":false}],"dataSources":[{"type":"DATA_SOURCE","subType":null,"name":"RDBMSDatasource","isFaulty":false}],"mcpServers":[]},"resources":{"registry":{"conf":null,"gov":{"name":"gov","files":[],"folders":[{"name":"datamapper","files":[{"name":"sample.dmc"}],"folders":[]},{"name":"endpoints","files":[{"name":"testEndpoint1.xml"},{"name":"testEndpoint2.xml"}],"folders":[]},{"name":"js","files":[{"name":"test.js"}],"folders":[]},{"name":"json","files":[{"name":"test.json"}],"folders":[]},{"name":"sequences","files":[{"name":"testSequence1.xml"}],"folders":[]},{"name":"smooks","files":[{"name":"test_smooks_config.xml"}],"folders":[]},{"name":"swagger","files":[{"name":"swagger.json"},{"name":"swagger.yaml"}],"folders":[]},{"name":"templates","files":[{"name":"testEndpointTemplate1.xml"},{"name":"testSequenceTemplate1.xml"}],"folders":[]},{"name":"ws_policy","files":[{"name":"ws_policy.xml"}],"folders":[]},{"name":"wsdl","files":[{"name":"wsdlfile.wsdl"}],"folders":[]},{"name":"xsd","files":[{"name":"sample.xsd"}],"folders":[]},{"name":"xsl","files":[{"name":"sample.xsl"}],"folders":[]},{"name":"xslt","files":[{"name":"sample.xslt"}],"folders":[]},{"name":"yaml","files":[{"name":"sample.yaml"}],"folders":[]}]}},"connectors":[],"inboundConnectors":[],"metadata":[],"newResources":{"name":"resources","files":[],"folders":[{"name":"conf","files":[{"name":"config.properties"}],"folders":[]},{"name":"endpoints","files":[{"name":"testEndpoint3.xml"},{"name":"testEndpoint4.xml"}],"folders":[]},{"name":"js","files":[{"name":"test1.js"}],"folders":[]},{"name":"json","files":[{"name":"test1.json"}],"folders":[]},{"name":"registry","files":[],"folders":[{"name":"gov","files":[],"folders":[{"name":"datamapper","files":[{"name":"sample.dmc"}],"folders":[]},{"name":"endpoints","files":[{"name":"testEndpoint1.xml"},{"name":"testEndpoint2.xml"}],"folders":[]},{"name":"js","files":[{"name":"test.js"}],"folders":[]},{"name":"json","files":[{"name":"test.json"}],"folders":[]},{"name":"sequences","files":[{"name":"testSequence1.xml"}],"folders":[]},{"name":"smooks","files":[{"name":"test_smooks_config.xml"}],"folders":[]},{"name":"swagger","files":[{"name":"swagger.json"},{"name":"swagger.yaml"}],"folders":[]},{"name":"templates","files":[{"name":"testEndpointTemplate1.xml"},{"name":"testSequenceTemplate1.xml"}],"folders":[]},{"name":"ws_policy","files":[{"name":"ws_policy.xml"}],"folders":[]},{"name":"wsdl","files":[{"name":"wsdlfile.wsdl"}],"folders":[]},{"name":"xsd","files":[{"name":"sample.xsd"}],"folders":[]},{"name":"xsl","files":[{"name":"sample.xsl"}],"folders":[]},{"name":"xslt","files":[{"name":"sample.xslt"}],"folders":[]},{"name":"yaml","files":[{"name":"sample.yaml"}],"folders":[]}]}]},{"name":"sequences","files":[{"name":"testSequence2.xml"}],"folders":[]},{"name":"smooks","files":[{"name":"test_smooks_config1.xml"}],"folders":[]},{"name":"swagger","files":[{"name":"swagger1.json"},{"name":"swagger1.yaml"}],"folders":[]},{"name":"templates","files":[{"name":"testEndpointTemplate2.xml"},{"name":"testSequenceTemplate2.xml"}],"folders":[]},{"name":"ws_policy","files":[{"name":"ws_policy1.xml"}],"folders":[]},{"name":"wsdl","files":[{"name":"wsdlfile1.wsdl"}],"folders":[]},{"name":"xsd","files":[{"name":"sample1.xsd"}],"folders":[]},{"name":"xsl","files":[{"name":"sample1.xsl"}],"folders":[]},{"name":"xslt","files":[{"name":"sample1.xslt"}],"folders":[]},{"name":"yaml","files":[{"name":"sample1.yaml"}],"folders":[]}]}}},"java":null,"ballerina":null},"tests":{"wso2mi":null,"java":null}}} \ No newline at end of file diff --git a/org.eclipse.lemminx/src/test/resources/synapse/directorytree.builder/generated-project-explorer.json b/org.eclipse.lemminx/src/test/resources/synapse/directorytree.builder/generated-project-explorer.json index 8e94951bb..c7b8a4f6f 100644 --- a/org.eclipse.lemminx/src/test/resources/synapse/directorytree.builder/generated-project-explorer.json +++ b/org.eclipse.lemminx/src/test/resources/synapse/directorytree.builder/generated-project-explorer.json @@ -1 +1 @@ -{"src":{"main":{"wso2mi":{"artifacts":{"APIs":[{"context":"/test","resources":[{"methods":"GET","uriTemplate":"/","urlMapping":null}],"sequences":[],"endpoints":[],"type":"API","subType":null,"name":"testApi","isFaulty":false}],"Event Integrations":[{"sequences":[],"endpoints":[],"type":"INBOUND_ENDPOINT","subType":"HTTPS","name":"HttpsInboundEndpoint","isFaulty":false}],"Automations":[{"type":"TASK","subType":null,"name":"testTask","isFaulty":false}],"Data Services":[{"type":"DATA_SERVICE","subType":null,"name":"RDBMSDataservice","isFaulty":false}],"Other Artifacts":{"Sequences":[{"isMainSequence":true,"sequences":[],"endpoints":[],"type":"SEQUENCE","subType":null,"name":"testSequence1","isFaulty":false},{"isRegistryResource":true,"type":"SEQUENCE","subType":null,"name":"testSequence1","isFaulty":false},{"isRegistryResource":true,"type":"SEQUENCE","subType":null,"name":"testSequence2","isFaulty":false}],"Connections":[{"connectorName":"http","connectionType":"HTTPS","type":"localEntry","subType":null,"name":"HttpsCon","isFaulty":false}],"Data Sources":[{"type":"DATA_SOURCE","subType":null,"name":"RDBMSDatasource","isFaulty":false}],"Class Mediators":[],"Ballerina Modules":[],"Endpoints":[{"type":"ENDPOINT","subType":"DEFAULT_ENDPOINT","name":"testEndpoint1","isFaulty":false},{"isRegistryResource":true,"type":"ENDPOINT","subType":"DEFAULT_ENDPOINT","name":"testEndpoint1","isFaulty":false},{"type":"ENDPOINT","subType":"HTTP_ENDPOINT","name":"testEndpoint2","isFaulty":false},{"isRegistryResource":true,"type":"ENDPOINT","subType":"HTTP_ENDPOINT","name":"testEndpoint2","isFaulty":false},{"isRegistryResource":true,"type":"ENDPOINT","subType":"DEFAULT_ENDPOINT","name":"testEndpoint3","isFaulty":false},{"isRegistryResource":true,"type":"ENDPOINT","subType":"HTTP_ENDPOINT","name":"testEndpoint4","isFaulty":false}],"Proxy Services":[{"sequences":[],"endpoints":[],"type":"PROXY_SERVICE","subType":null,"name":"testProxy1","isFaulty":false}],"Message Stores":[{"type":"MESSAGE_STORE","subType":"IN_MEMORY","name":"testMessageStore","isFaulty":false}],"Message Processors":[{"type":"MESSAGE_PROCESSOR","subType":"MESSAGE_SAMPLING","name":"testMessageProcessor","isFaulty":false}],"Local Entries":[{"type":"LOCAL_ENTRY","subType":null,"name":"testLocalEntry","isFaulty":false}],"Templates":[{"type":"TEMPLATE","subType":"DEFAULT_ENDPOINT","name":"testEndpointTemplate","isFaulty":false},{"isRegistryResource":true,"type":"TEMPLATE","subType":"DEFAULT_ENDPOINT","name":"testEndpointTemplate1","isFaulty":false},{"isRegistryResource":true,"type":"TEMPLATE","subType":"DEFAULT_ENDPOINT","name":"testEndpointTemplate2","isFaulty":false},{"type":"TEMPLATE","subType":"SEQUENCE","name":"testSequenceTemplate","isFaulty":false},{"isRegistryResource":true,"type":"TEMPLATE","subType":"SEQUENCE","name":"testSequenceTemplate1","isFaulty":false},{"isRegistryResource":true,"type":"TEMPLATE","subType":"SEQUENCE","name":"testSequenceTemplate2","isFaulty":false}],"Data Mappers":[]},"Resources":{"name":"resources","files":[],"folders":[{"name":"conf","files":[{"name":"config.properties"}],"folders":[]},{"name":"endpoints","files":[{"name":"testEndpoint3.xml"},{"name":"testEndpoint4.xml"}],"folders":[]},{"name":"js","files":[{"name":"test1.js"}],"folders":[]},{"name":"json","files":[{"name":"test1.json"}],"folders":[]},{"name":"registry","files":[],"folders":[{"name":"gov","files":[],"folders":[{"name":"datamapper","files":[{"name":"sample.dmc"}],"folders":[]},{"name":"endpoints","files":[{"name":"testEndpoint1.xml"},{"name":"testEndpoint2.xml"}],"folders":[]},{"name":"js","files":[{"name":"test.js"}],"folders":[]},{"name":"json","files":[{"name":"test.json"}],"folders":[]},{"name":"sequences","files":[{"name":"testSequence1.xml"}],"folders":[]},{"name":"smooks","files":[{"name":"test_smooks_config.xml"}],"folders":[]},{"name":"swagger","files":[{"name":"swagger.json"},{"name":"swagger.yaml"}],"folders":[]},{"name":"templates","files":[{"name":"testEndpointTemplate1.xml"},{"name":"testSequenceTemplate1.xml"}],"folders":[]},{"name":"ws_policy","files":[{"name":"ws_policy.xml"}],"folders":[]},{"name":"wsdl","files":[{"name":"wsdlfile.wsdl"}],"folders":[]},{"name":"xsd","files":[{"name":"sample.xsd"}],"folders":[]},{"name":"xsl","files":[{"name":"sample.xsl"}],"folders":[]},{"name":"xslt","files":[{"name":"sample.xslt"}],"folders":[]},{"name":"yaml","files":[{"name":"sample.yaml"}],"folders":[]}]}]},{"name":"sequences","files":[{"name":"testSequence2.xml"}],"folders":[]},{"name":"smooks","files":[{"name":"test_smooks_config1.xml"}],"folders":[]},{"name":"swagger","files":[{"name":"swagger1.json"},{"name":"swagger1.yaml"}],"folders":[]},{"name":"templates","files":[{"name":"testEndpointTemplate2.xml"},{"name":"testSequenceTemplate2.xml"}],"folders":[]},{"name":"ws_policy","files":[{"name":"ws_policy1.xml"}],"folders":[]},{"name":"wsdl","files":[{"name":"wsdlfile1.wsdl"}],"folders":[]},{"name":"xsd","files":[{"name":"sample1.xsd"}],"folders":[]},{"name":"xsl","files":[{"name":"sample1.xsl"}],"folders":[]},{"name":"xslt","files":[{"name":"sample1.xslt"}],"folders":[]},{"name":"yaml","files":[{"name":"sample1.yaml"}],"folders":[]}]}},"resources":{"registry":{"conf":null,"gov":{"name":"gov","files":[],"folders":[{"name":"datamapper","files":[{"name":"sample.dmc"}],"folders":[]},{"name":"endpoints","files":[{"name":"testEndpoint1.xml"},{"name":"testEndpoint2.xml"}],"folders":[]},{"name":"js","files":[{"name":"test.js"}],"folders":[]},{"name":"json","files":[{"name":"test.json"}],"folders":[]},{"name":"sequences","files":[{"name":"testSequence1.xml"}],"folders":[]},{"name":"smooks","files":[{"name":"test_smooks_config.xml"}],"folders":[]},{"name":"swagger","files":[{"name":"swagger.json"},{"name":"swagger.yaml"}],"folders":[]},{"name":"templates","files":[{"name":"testEndpointTemplate1.xml"},{"name":"testSequenceTemplate1.xml"}],"folders":[]},{"name":"ws_policy","files":[{"name":"ws_policy.xml"}],"folders":[]},{"name":"wsdl","files":[{"name":"wsdlfile.wsdl"}],"folders":[]},{"name":"xsd","files":[{"name":"sample.xsd"}],"folders":[]},{"name":"xsl","files":[{"name":"sample.xsl"}],"folders":[]},{"name":"xslt","files":[{"name":"sample.xslt"}],"folders":[]},{"name":"yaml","files":[{"name":"sample.yaml"}],"folders":[]}]}},"connectors":[],"metadata":[],"newResources":{"name":"resources","files":[],"folders":[{"name":"conf","files":[{"name":"config.properties"}],"folders":[]},{"name":"endpoints","files":[{"name":"testEndpoint3.xml"},{"name":"testEndpoint4.xml"}],"folders":[]},{"name":"js","files":[{"name":"test1.js"}],"folders":[]},{"name":"json","files":[{"name":"test1.json"}],"folders":[]},{"name":"registry","files":[],"folders":[{"name":"gov","files":[],"folders":[{"name":"datamapper","files":[{"name":"sample.dmc"}],"folders":[]},{"name":"endpoints","files":[{"name":"testEndpoint1.xml"},{"name":"testEndpoint2.xml"}],"folders":[]},{"name":"js","files":[{"name":"test.js"}],"folders":[]},{"name":"json","files":[{"name":"test.json"}],"folders":[]},{"name":"sequences","files":[{"name":"testSequence1.xml"}],"folders":[]},{"name":"smooks","files":[{"name":"test_smooks_config.xml"}],"folders":[]},{"name":"swagger","files":[{"name":"swagger.json"},{"name":"swagger.yaml"}],"folders":[]},{"name":"templates","files":[{"name":"testEndpointTemplate1.xml"},{"name":"testSequenceTemplate1.xml"}],"folders":[]},{"name":"ws_policy","files":[{"name":"ws_policy.xml"}],"folders":[]},{"name":"wsdl","files":[{"name":"wsdlfile.wsdl"}],"folders":[]},{"name":"xsd","files":[{"name":"sample.xsd"}],"folders":[]},{"name":"xsl","files":[{"name":"sample.xsl"}],"folders":[]},{"name":"xslt","files":[{"name":"sample.xslt"}],"folders":[]},{"name":"yaml","files":[{"name":"sample.yaml"}],"folders":[]}]}]},{"name":"sequences","files":[{"name":"testSequence2.xml"}],"folders":[]},{"name":"smooks","files":[{"name":"test_smooks_config1.xml"}],"folders":[]},{"name":"swagger","files":[{"name":"swagger1.json"},{"name":"swagger1.yaml"}],"folders":[]},{"name":"templates","files":[{"name":"testEndpointTemplate2.xml"},{"name":"testSequenceTemplate2.xml"}],"folders":[]},{"name":"ws_policy","files":[{"name":"ws_policy1.xml"}],"folders":[]},{"name":"wsdl","files":[{"name":"wsdlfile1.wsdl"}],"folders":[]},{"name":"xsd","files":[{"name":"sample1.xsd"}],"folders":[]},{"name":"xsl","files":[{"name":"sample1.xsl"}],"folders":[]},{"name":"xslt","files":[{"name":"sample1.xslt"}],"folders":[]},{"name":"yaml","files":[{"name":"sample1.yaml"}],"folders":[]}]}}},"java":null,"ballerina":null},"tests":{"wso2mi":null,"java":null}}} \ No newline at end of file +{"src":{"main":{"wso2mi":{"artifacts":{"APIs":[{"context":"/test","resources":[{"methods":"GET","uriTemplate":"/","urlMapping":null}],"sequences":[],"endpoints":[],"type":"API","subType":null,"name":"testApi","isFaulty":false}],"Event Integrations":[{"sequences":[],"endpoints":[],"type":"INBOUND_ENDPOINT","subType":"HTTPS","name":"HttpsInboundEndpoint","isFaulty":false}],"Automations":[{"type":"TASK","subType":null,"name":"testTask","isFaulty":false}],"Data Services":[{"type":"DATA_SERVICE","subType":null,"name":"RDBMSDataservice","isFaulty":false}],"MCP Servers":[],"Other Artifacts":{"Sequences":[{"isMainSequence":true,"sequences":[],"endpoints":[],"type":"SEQUENCE","subType":null,"name":"testSequence1","isFaulty":false},{"isRegistryResource":true,"type":"SEQUENCE","subType":null,"name":"testSequence1","isFaulty":false},{"isRegistryResource":true,"type":"SEQUENCE","subType":null,"name":"testSequence2","isFaulty":false}],"Connections":[{"connectorName":"http","connectionType":"HTTPS","type":"localEntry","subType":null,"name":"HttpsCon","isFaulty":false}],"Data Sources":[{"type":"DATA_SOURCE","subType":null,"name":"RDBMSDatasource","isFaulty":false}],"Class Mediators":[],"Ballerina Modules":[],"Endpoints":[{"type":"ENDPOINT","subType":"DEFAULT_ENDPOINT","name":"testEndpoint1","isFaulty":false},{"isRegistryResource":true,"type":"ENDPOINT","subType":"DEFAULT_ENDPOINT","name":"testEndpoint1","isFaulty":false},{"type":"ENDPOINT","subType":"HTTP_ENDPOINT","name":"testEndpoint2","isFaulty":false},{"isRegistryResource":true,"type":"ENDPOINT","subType":"HTTP_ENDPOINT","name":"testEndpoint2","isFaulty":false},{"isRegistryResource":true,"type":"ENDPOINT","subType":"DEFAULT_ENDPOINT","name":"testEndpoint3","isFaulty":false},{"isRegistryResource":true,"type":"ENDPOINT","subType":"HTTP_ENDPOINT","name":"testEndpoint4","isFaulty":false}],"Proxy Services":[{"sequences":[],"endpoints":[],"type":"PROXY_SERVICE","subType":null,"name":"testProxy1","isFaulty":false}],"Message Stores":[{"type":"MESSAGE_STORE","subType":"IN_MEMORY","name":"testMessageStore","isFaulty":false}],"Message Processors":[{"type":"MESSAGE_PROCESSOR","subType":"MESSAGE_SAMPLING","name":"testMessageProcessor","isFaulty":false}],"Local Entries":[{"type":"LOCAL_ENTRY","subType":null,"name":"testLocalEntry","isFaulty":false}],"Templates":[{"type":"TEMPLATE","subType":"DEFAULT_ENDPOINT","name":"testEndpointTemplate","isFaulty":false},{"isRegistryResource":true,"type":"TEMPLATE","subType":"DEFAULT_ENDPOINT","name":"testEndpointTemplate1","isFaulty":false},{"isRegistryResource":true,"type":"TEMPLATE","subType":"DEFAULT_ENDPOINT","name":"testEndpointTemplate2","isFaulty":false},{"type":"TEMPLATE","subType":"SEQUENCE","name":"testSequenceTemplate","isFaulty":false},{"isRegistryResource":true,"type":"TEMPLATE","subType":"SEQUENCE","name":"testSequenceTemplate1","isFaulty":false},{"isRegistryResource":true,"type":"TEMPLATE","subType":"SEQUENCE","name":"testSequenceTemplate2","isFaulty":false}],"Data Mappers":[]},"Resources":{"name":"resources","files":[],"folders":[{"name":"conf","files":[{"name":"config.properties"}],"folders":[]},{"name":"endpoints","files":[{"name":"testEndpoint3.xml"},{"name":"testEndpoint4.xml"}],"folders":[]},{"name":"js","files":[{"name":"test1.js"}],"folders":[]},{"name":"json","files":[{"name":"test1.json"}],"folders":[]},{"name":"registry","files":[],"folders":[{"name":"gov","files":[],"folders":[{"name":"datamapper","files":[{"name":"sample.dmc"}],"folders":[]},{"name":"endpoints","files":[{"name":"testEndpoint1.xml"},{"name":"testEndpoint2.xml"}],"folders":[]},{"name":"js","files":[{"name":"test.js"}],"folders":[]},{"name":"json","files":[{"name":"test.json"}],"folders":[]},{"name":"sequences","files":[{"name":"testSequence1.xml"}],"folders":[]},{"name":"smooks","files":[{"name":"test_smooks_config.xml"}],"folders":[]},{"name":"swagger","files":[{"name":"swagger.json"},{"name":"swagger.yaml"}],"folders":[]},{"name":"templates","files":[{"name":"testEndpointTemplate1.xml"},{"name":"testSequenceTemplate1.xml"}],"folders":[]},{"name":"ws_policy","files":[{"name":"ws_policy.xml"}],"folders":[]},{"name":"wsdl","files":[{"name":"wsdlfile.wsdl"}],"folders":[]},{"name":"xsd","files":[{"name":"sample.xsd"}],"folders":[]},{"name":"xsl","files":[{"name":"sample.xsl"}],"folders":[]},{"name":"xslt","files":[{"name":"sample.xslt"}],"folders":[]},{"name":"yaml","files":[{"name":"sample.yaml"}],"folders":[]}]}]},{"name":"sequences","files":[{"name":"testSequence2.xml"}],"folders":[]},{"name":"smooks","files":[{"name":"test_smooks_config1.xml"}],"folders":[]},{"name":"swagger","files":[{"name":"swagger1.json"},{"name":"swagger1.yaml"}],"folders":[]},{"name":"templates","files":[{"name":"testEndpointTemplate2.xml"},{"name":"testSequenceTemplate2.xml"}],"folders":[]},{"name":"ws_policy","files":[{"name":"ws_policy1.xml"}],"folders":[]},{"name":"wsdl","files":[{"name":"wsdlfile1.wsdl"}],"folders":[]},{"name":"xsd","files":[{"name":"sample1.xsd"}],"folders":[]},{"name":"xsl","files":[{"name":"sample1.xsl"}],"folders":[]},{"name":"xslt","files":[{"name":"sample1.xslt"}],"folders":[]},{"name":"yaml","files":[{"name":"sample1.yaml"}],"folders":[]}]}},"resources":{"registry":{"conf":null,"gov":{"name":"gov","files":[],"folders":[{"name":"datamapper","files":[{"name":"sample.dmc"}],"folders":[]},{"name":"endpoints","files":[{"name":"testEndpoint1.xml"},{"name":"testEndpoint2.xml"}],"folders":[]},{"name":"js","files":[{"name":"test.js"}],"folders":[]},{"name":"json","files":[{"name":"test.json"}],"folders":[]},{"name":"sequences","files":[{"name":"testSequence1.xml"}],"folders":[]},{"name":"smooks","files":[{"name":"test_smooks_config.xml"}],"folders":[]},{"name":"swagger","files":[{"name":"swagger.json"},{"name":"swagger.yaml"}],"folders":[]},{"name":"templates","files":[{"name":"testEndpointTemplate1.xml"},{"name":"testSequenceTemplate1.xml"}],"folders":[]},{"name":"ws_policy","files":[{"name":"ws_policy.xml"}],"folders":[]},{"name":"wsdl","files":[{"name":"wsdlfile.wsdl"}],"folders":[]},{"name":"xsd","files":[{"name":"sample.xsd"}],"folders":[]},{"name":"xsl","files":[{"name":"sample.xsl"}],"folders":[]},{"name":"xslt","files":[{"name":"sample.xslt"}],"folders":[]},{"name":"yaml","files":[{"name":"sample.yaml"}],"folders":[]}]}},"connectors":[],"inboundConnectors":[],"metadata":[],"newResources":{"name":"resources","files":[],"folders":[{"name":"conf","files":[{"name":"config.properties"}],"folders":[]},{"name":"endpoints","files":[{"name":"testEndpoint3.xml"},{"name":"testEndpoint4.xml"}],"folders":[]},{"name":"js","files":[{"name":"test1.js"}],"folders":[]},{"name":"json","files":[{"name":"test1.json"}],"folders":[]},{"name":"registry","files":[],"folders":[{"name":"gov","files":[],"folders":[{"name":"datamapper","files":[{"name":"sample.dmc"}],"folders":[]},{"name":"endpoints","files":[{"name":"testEndpoint1.xml"},{"name":"testEndpoint2.xml"}],"folders":[]},{"name":"js","files":[{"name":"test.js"}],"folders":[]},{"name":"json","files":[{"name":"test.json"}],"folders":[]},{"name":"sequences","files":[{"name":"testSequence1.xml"}],"folders":[]},{"name":"smooks","files":[{"name":"test_smooks_config.xml"}],"folders":[]},{"name":"swagger","files":[{"name":"swagger.json"},{"name":"swagger.yaml"}],"folders":[]},{"name":"templates","files":[{"name":"testEndpointTemplate1.xml"},{"name":"testSequenceTemplate1.xml"}],"folders":[]},{"name":"ws_policy","files":[{"name":"ws_policy.xml"}],"folders":[]},{"name":"wsdl","files":[{"name":"wsdlfile.wsdl"}],"folders":[]},{"name":"xsd","files":[{"name":"sample.xsd"}],"folders":[]},{"name":"xsl","files":[{"name":"sample.xsl"}],"folders":[]},{"name":"xslt","files":[{"name":"sample.xslt"}],"folders":[]},{"name":"yaml","files":[{"name":"sample.yaml"}],"folders":[]}]}]},{"name":"sequences","files":[{"name":"testSequence2.xml"}],"folders":[]},{"name":"smooks","files":[{"name":"test_smooks_config1.xml"}],"folders":[]},{"name":"swagger","files":[{"name":"swagger1.json"},{"name":"swagger1.yaml"}],"folders":[]},{"name":"templates","files":[{"name":"testEndpointTemplate2.xml"},{"name":"testSequenceTemplate2.xml"}],"folders":[]},{"name":"ws_policy","files":[{"name":"ws_policy1.xml"}],"folders":[]},{"name":"wsdl","files":[{"name":"wsdlfile1.wsdl"}],"folders":[]},{"name":"xsd","files":[{"name":"sample1.xsd"}],"folders":[]},{"name":"xsl","files":[{"name":"sample1.xsl"}],"folders":[]},{"name":"xslt","files":[{"name":"sample1.xslt"}],"folders":[]},{"name":"yaml","files":[{"name":"sample1.yaml"}],"folders":[]}]}}},"java":null,"ballerina":null},"tests":{"wso2mi":null,"java":null}}} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 44c33afda..5c22fc0db 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 org.wso2.language.server mi-language-server-parent - 0.24.0-wso2v90 + 0.24.0-wso2v95 pom MI Language Server - Parent LemMinX is a XML Language Server Protocol (LSP), and can be used with any editor that supports LSP, to offer an outstanding XML editing experience