diff --git a/gradle.properties b/gradle.properties index 7035ec4f6..e074f34de 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ org.gradle.caching=true group=org.ballerinalang -version=2.0.0-SNAPSHOT +version=2.0.0 ballerinaLangVersion=2201.12.0-20250228-201300-8d411a0f ballerinaGradlePluginVersion=2.3.0 diff --git a/model-generator-commons/src/main/java/io/ballerina/modelgenerator/commons/FunctionData.java b/model-generator-commons/src/main/java/io/ballerina/modelgenerator/commons/FunctionData.java index aef487b92..8badcf5a7 100644 --- a/model-generator-commons/src/main/java/io/ballerina/modelgenerator/commons/FunctionData.java +++ b/model-generator-commons/src/main/java/io/ballerina/modelgenerator/commons/FunctionData.java @@ -39,6 +39,7 @@ public class FunctionData { private final boolean returnError; private final boolean inferredReturnType; private Map parameters; + private String packageId; public FunctionData(int functionId, String name, String description, String returnType, String packageName, String org, String version, String resourcePath, @@ -60,6 +61,10 @@ public void setParameters(Map parameters) { this.parameters = parameters; } + public void setPackageId(String attachmentId) { + this.packageId = attachmentId; + } + // Getters public int functionId() { return functionId; @@ -109,11 +114,16 @@ public Map parameters() { return parameters; } + public String packageId() { + return packageId; + } + public enum Kind { FUNCTION, CONNECTOR, REMOTE, - RESOURCE + RESOURCE, + LISTENER_INIT, } } diff --git a/model-generator-commons/src/main/java/io/ballerina/modelgenerator/commons/PackageUtil.java b/model-generator-commons/src/main/java/io/ballerina/modelgenerator/commons/PackageUtil.java index c0410dea0..ffe98f8b6 100644 --- a/model-generator-commons/src/main/java/io/ballerina/modelgenerator/commons/PackageUtil.java +++ b/model-generator-commons/src/main/java/io/ballerina/modelgenerator/commons/PackageUtil.java @@ -60,10 +60,11 @@ public static BuildProject getSampleProject() { // Obtain the Ballerina distribution path String ballerinaHome = System.getProperty(BALLERINA_HOME_PROPERTY); if (ballerinaHome == null || ballerinaHome.isEmpty()) { - Path currentPath = getPath(Paths.get( - PackageUtil.class.getProtectionDomain().getCodeSource().getLocation().getPath())); - Path distributionPath = getParentPath(getParentPath(getParentPath(currentPath))); - System.setProperty(BALLERINA_HOME_PROPERTY, distributionPath.toString()); +// Path currentPath = getPath(Paths.get( +// PackageUtil.class.getProtectionDomain().getCodeSource().getLocation().getPath())); +// Path distributionPath = getParentPath(getParentPath(getParentPath(currentPath))); +// System.setProperty(BALLERINA_HOME_PROPERTY, distributionPath.toString()); + System.setProperty(BALLERINA_HOME_PROPERTY, "/Users/lakshanweerasinghe/.ballerina/ballerina-home/distributions/ballerina-2201.12.0"); } try { @@ -80,7 +81,7 @@ public static BuildProject getSampleProject() { "org = \"wso2\"\n" + "name = \"sample\"\n" + "version = \"0.1.0\"\n" + - "distribution = \"2201.11.0\""; + "distribution = \"2201.12.0\""; Files.writeString(ballerinaTomlFile, tomlContent, StandardOpenOption.CREATE); return BuildProject.load(tempDir); } catch (IOException e) { diff --git a/model-generator-commons/src/main/java/io/ballerina/modelgenerator/commons/ServiceDatabaseManager.java b/model-generator-commons/src/main/java/io/ballerina/modelgenerator/commons/ServiceDatabaseManager.java new file mode 100644 index 000000000..4acc317b8 --- /dev/null +++ b/model-generator-commons/src/main/java/io/ballerina/modelgenerator/commons/ServiceDatabaseManager.java @@ -0,0 +1,230 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com) + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.modelgenerator.commons; + +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Optional; +import java.util.logging.Logger; + +/** + * Manages database operations for retrieving information about external connectors and functions. + * + * @since 2.0.0 + */ +public class ServiceDatabaseManager { + + private static final String INDEX_FILE_NAME = "service-index.sqlite"; + private static final Logger LOGGER = Logger.getLogger(ServiceDatabaseManager.class.getName()); + private final String dbPath; + + private static class Holder { + private static final ServiceDatabaseManager INSTANCE = new ServiceDatabaseManager(); + } + + public static ServiceDatabaseManager getInstance() { + return Holder.INSTANCE; + } + + private ServiceDatabaseManager() { + try { + Class.forName("org.sqlite.JDBC"); + } catch (ClassNotFoundException e) { + throw new RuntimeException("Failed to load SQLite JDBC driver", e); + } + + Path tempDir; + try { + tempDir = Files.createTempDirectory("service-index"); + } catch (IOException e) { + throw new RuntimeException("Failed to create a temporary directory", e); + } + + URL dbUrl = getClass().getClassLoader().getResource(INDEX_FILE_NAME); + if (dbUrl == null) { + throw new RuntimeException("Database resource not found: " + INDEX_FILE_NAME); + } + Path tempFile = tempDir.resolve(INDEX_FILE_NAME); + try { + Files.copy(dbUrl.openStream(), tempFile); + } catch (IOException e) { + throw new RuntimeException("Failed to copy the database file to the temporary directory", e); + } + + dbPath = "jdbc:sqlite:" + tempFile.toString(); + } + + public Optional getListener(String org, String module) { + StringBuilder sql = new StringBuilder("SELECT "); + sql.append("l.listener_id, "); + sql.append("l.name AS listener_name, "); + sql.append("l.description AS listener_description, "); + sql.append("l.return_error, "); + sql.append("p.package_id, "); + sql.append("p.name AS package_name, "); + sql.append("p.org, "); + sql.append("p.version "); + sql.append("FROM Listener l "); + sql.append("JOIN Package p ON l.package_id = p.package_id "); + sql.append("WHERE p.org = ? "); + sql.append("AND p.name = ? "); + + try (Connection conn = DriverManager.getConnection(dbPath); + PreparedStatement stmt = conn.prepareStatement(sql.toString())) { + stmt.setString(1, org); + stmt.setString(2, module); + + ResultSet rs = stmt.executeQuery(); + if (rs.next()) { + FunctionData functionData = new FunctionData( + rs.getInt("listener_id"), + rs.getString("listener_name"), + rs.getString("listener_description"), + null, + rs.getString("package_name"), + rs.getString("org"), + rs.getString("version"), + null, + null, + rs.getBoolean("return_error"), + false); + functionData.setPackageId(rs.getString("package_id")); + return Optional.of(functionData); + } + return Optional.empty(); + } catch (SQLException e) { + Logger.getGlobal().severe("Error executing query: " + e.getMessage()); + return Optional.empty(); + } + } + + public LinkedHashMap getFunctionParametersAsMap(int listenerId) { + String sql = "SELECT " + + "p.parameter_id, " + + "p.name, " + + "p.type, " + + "p.kind, " + + "p.optional, " + + "p.default_value, " + + "p.description, " + + "p.import_statements, " + + "pmt.type AS member_type, " + + "pmt.kind AS member_kind, " + + "pmt.package AS member_package " + + "FROM Parameter p " + + "LEFT JOIN ParameterMemberType pmt ON p.parameter_id = pmt.parameter_id " + + "WHERE p.listener_id = ?;"; + + try (Connection conn = DriverManager.getConnection(dbPath); + PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setInt(1, listenerId); + ResultSet rs = stmt.executeQuery(); + + // Use a builder to accumulate parameter data and member types + LinkedHashMap builders = new LinkedHashMap<>(); + + while (rs.next()) { + String paramName = rs.getString("name"); + int parameterId = rs.getInt("parameter_id"); + String type = rs.getString("type"); + ParameterData.Kind kind = ParameterData.Kind.valueOf(rs.getString("kind")); + String defaultValue = rs.getString("default_value"); + String description = rs.getString("description"); + boolean optional = rs.getBoolean("optional"); + String importStatements = rs.getString("import_statements"); + + // Member type data + String memberType = rs.getString("member_type"); + String memberKind = rs.getString("member_kind"); + String memberPackage = rs.getString("member_package"); + + // Get or create the builder for this parameter + ParameterDataBuilder builder = builders.get(paramName); + if (builder == null) { + builder = new ParameterDataBuilder(); + builder.parameterId = parameterId; + builder.name = paramName; + builder.type = type; + builder.kind = kind; + builder.defaultValue = defaultValue; + builder.description = description; + builder.optional = optional; + builder.importStatements = importStatements; + builders.put(paramName, builder); + } + + // Add member type if present + if (memberType != null) { + ParameterMemberTypeData memberData = new ParameterMemberTypeData( + memberType, memberKind, memberPackage); + builder.typeMembers.add(memberData); + } + } + + // Convert builders to ParameterData + LinkedHashMap parameterResults = new LinkedHashMap<>(); + for (ParameterDataBuilder builder : builders.values()) { + parameterResults.put(builder.name, builder.build()); + } + return parameterResults; + + } catch (SQLException e) { + Logger.getGlobal().severe("Error executing query: " + e.getMessage()); + return new LinkedHashMap<>(); + } + } + + // Helper builder class + private static class ParameterDataBuilder { + int parameterId; + String name; + String type; + ParameterData.Kind kind; + String defaultValue; + String description; + boolean optional; + String importStatements; + List typeMembers = new ArrayList<>(); + + ParameterData build() { + return new ParameterData( + parameterId, + name, + type, + kind, + defaultValue, + description, + optional, + importStatements, + typeMembers + ); + } + } + +} diff --git a/service-model-generator/modules/service-model-generator-ls-extension/build.gradle b/service-model-generator/modules/service-model-generator-ls-extension/build.gradle index ffc6c0b20..897215c81 100644 --- a/service-model-generator/modules/service-model-generator-ls-extension/build.gradle +++ b/service-model-generator/modules/service-model-generator-ls-extension/build.gradle @@ -58,6 +58,7 @@ dependencies { exclude group: "javax.validation", module: "validation-api" } implementation "io.swagger.core.v3:swagger-models" + implementation "org.xerial:sqlite-jdbc:${sqliteJdbcVersion}" } def balDistribution = file("$project.buildDir/extracted-distribution/jballerina-tools-${ballerinaLangVersion}") diff --git a/service-model-generator/modules/service-model-generator-ls-extension/src/main/java/io/ballerina/servicemodelgenerator/extension/ServiceModelGeneratorService.java b/service-model-generator/modules/service-model-generator-ls-extension/src/main/java/io/ballerina/servicemodelgenerator/extension/ServiceModelGeneratorService.java index 7f9d8b0b4..0a4af9729 100644 --- a/service-model-generator/modules/service-model-generator-ls-extension/src/main/java/io/ballerina/servicemodelgenerator/extension/ServiceModelGeneratorService.java +++ b/service-model-generator/modules/service-model-generator-ls-extension/src/main/java/io/ballerina/servicemodelgenerator/extension/ServiceModelGeneratorService.java @@ -46,6 +46,11 @@ import io.ballerina.compiler.syntax.tree.SyntaxTree; import io.ballerina.compiler.syntax.tree.TypeDefinitionNode; import io.ballerina.compiler.syntax.tree.TypeDescriptorNode; +import io.ballerina.modelgenerator.commons.DatabaseManager; +import io.ballerina.modelgenerator.commons.FunctionData; +import io.ballerina.modelgenerator.commons.FunctionDataBuilder; +import io.ballerina.modelgenerator.commons.ModuleInfo; +import io.ballerina.modelgenerator.commons.ServiceDatabaseManager; import io.ballerina.projects.Document; import io.ballerina.projects.Module; import io.ballerina.projects.ModuleId; @@ -204,7 +209,7 @@ public CompletableFuture getListeners(ListenerDiscove public CompletableFuture getListenerModel(ListenerModelRequest request) { return CompletableFuture.supplyAsync(() -> { try { - return getListenerByName(request.moduleName()) + return ListenerUtil.getListenerByName(request) .map(ListenerModelResponse::new) .orElseGet(ListenerModelResponse::new); } catch (Throwable e) { @@ -619,7 +624,8 @@ public CompletableFuture getListenerFromSource(Commo if (listenerName.isEmpty()) { return new ListenerFromSourceResponse(); } - Optional listener = getListenerByName(listenerName.get()); + Optional listener = ListenerUtil.getListenerByName(new + ListenerModelRequest("", "", "")); if (listener.isEmpty()) { return new ListenerFromSourceResponse(); } @@ -1143,26 +1149,6 @@ private Optional getTriggerBasicInfoByName(String name) { } } - private Optional getListenerByName(String name) { - if (!name.equals(ServiceModelGeneratorConstants.HTTP) && - !name.equals(ServiceModelGeneratorConstants.GRAPHQL) && - !name.equals(ServiceModelGeneratorConstants.TCP) && - triggerProperties.values().stream().noneMatch(trigger -> trigger.name().equals(name))) { - return Optional.empty(); - } - InputStream resourceStream = getClass().getClassLoader() - .getResourceAsStream(String.format("listeners/%s.json", name)); - if (resourceStream == null) { - return Optional.empty(); - } - - try (JsonReader reader = new JsonReader(new InputStreamReader(resourceStream, StandardCharsets.UTF_8))) { - return Optional.of(new Gson().fromJson(reader, Listener.class)); - } catch (IOException e) { - return Optional.empty(); - } - } - private Optional getServiceByName(String name) { if (!name.equals(ServiceModelGeneratorConstants.HTTP) && !name.equals(ServiceModelGeneratorConstants.GRAPHQL) && diff --git a/service-model-generator/modules/service-model-generator-ls-extension/src/main/java/io/ballerina/servicemodelgenerator/extension/model/Codedata.java b/service-model-generator/modules/service-model-generator-ls-extension/src/main/java/io/ballerina/servicemodelgenerator/extension/model/Codedata.java index bc0614283..d041fae0e 100644 --- a/service-model-generator/modules/service-model-generator-ls-extension/src/main/java/io/ballerina/servicemodelgenerator/extension/model/Codedata.java +++ b/service-model-generator/modules/service-model-generator-ls-extension/src/main/java/io/ballerina/servicemodelgenerator/extension/model/Codedata.java @@ -27,6 +27,10 @@ public class Codedata { private boolean inDisplayAnnotation; private String type; private String argType; + private String originalName; + + public Codedata() { + } public Codedata(String type) { this.type = type; @@ -95,4 +99,12 @@ public String getArgType() { public void setArgType(String argType) { this.argType = argType; } + + public String getOriginalName() { + return originalName; + } + + public void setOriginalName(String originalName) { + this.originalName = originalName; + } } diff --git a/service-model-generator/modules/service-model-generator-ls-extension/src/main/java/io/ballerina/servicemodelgenerator/extension/util/ListenerUtil.java b/service-model-generator/modules/service-model-generator-ls-extension/src/main/java/io/ballerina/servicemodelgenerator/extension/util/ListenerUtil.java index 766736fca..f209ad9cf 100644 --- a/service-model-generator/modules/service-model-generator-ls-extension/src/main/java/io/ballerina/servicemodelgenerator/extension/util/ListenerUtil.java +++ b/service-model-generator/modules/service-model-generator-ls-extension/src/main/java/io/ballerina/servicemodelgenerator/extension/util/ListenerUtil.java @@ -26,22 +26,30 @@ import io.ballerina.compiler.syntax.tree.ListenerDeclarationNode; import io.ballerina.compiler.syntax.tree.ModulePartNode; import io.ballerina.compiler.syntax.tree.NonTerminalNode; +import io.ballerina.modelgenerator.commons.CommonUtils; +import io.ballerina.modelgenerator.commons.FunctionData; +import io.ballerina.modelgenerator.commons.ParameterData; +import io.ballerina.modelgenerator.commons.ServiceDatabaseManager; import io.ballerina.projects.Document; import io.ballerina.projects.DocumentId; import io.ballerina.projects.Project; import io.ballerina.servicemodelgenerator.extension.ServiceModelGeneratorConstants; import io.ballerina.servicemodelgenerator.extension.model.Codedata; +import io.ballerina.servicemodelgenerator.extension.model.DisplayAnnotation; import io.ballerina.servicemodelgenerator.extension.model.Listener; import io.ballerina.servicemodelgenerator.extension.model.MetaData; import io.ballerina.servicemodelgenerator.extension.model.Value; +import io.ballerina.servicemodelgenerator.extension.request.ListenerModelRequest; import io.ballerina.tools.diagnostics.Location; import io.ballerina.tools.text.LinePosition; import io.ballerina.tools.text.TextRange; import java.nio.file.Path; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -160,4 +168,82 @@ public static Value getHttpDefaultListenerValue() { value.setValue(ServiceModelGeneratorConstants.HTTP_DEFAULT_LISTENER_EXPR); return value; } + + public static Optional getListenerByName(ListenerModelRequest request) { + ServiceDatabaseManager dbManager = ServiceDatabaseManager.getInstance(); + Optional optFunctionResult = dbManager.getListener(request.orgName(), request.moduleName()); + if (optFunctionResult.isEmpty()) { + return Optional.empty(); + } + FunctionData functionData = optFunctionResult.get(); + LinkedHashMap parameters = dbManager + .getFunctionParametersAsMap(functionData.functionId()); + functionData.setParameters(parameters); + + Map properties = new HashMap<>(); + String formattedModuleName = upperCaseFirstLetter(functionData.packageName()); + String icon = CommonUtils.generateIcon(functionData.org(), functionData.packageName(), + functionData.version()); + + Listener.ListenerBuilder listenerBuilder = new Listener.ListenerBuilder(); + listenerBuilder + .setId(functionData.packageId()) + .setName(formattedModuleName + " Listener") + .setType(functionData.packageName()) + .setDisplayName(formattedModuleName) + .setDescription(functionData.description()) + .setListenerProtocol(functionData.packageName().toLowerCase(Locale.ROOT)) + .setModuleName(functionData.packageName()) + .setOrgName(functionData.org()) + .setPackageName(functionData.packageName()) + .setVersion(functionData.version()) + .setIcon(icon) + .setDisplayAnnotation(new DisplayAnnotation(formattedModuleName, icon)) + .setProperties(properties); + + setParameterProperties(functionData, properties); + return Optional.of(listenerBuilder.build()); + } + + public static String upperCaseFirstLetter(String value) { + return value.substring(0, 1).toUpperCase(Locale.ROOT) + value.substring(1).toLowerCase(Locale.ROOT); + } + + protected static void setParameterProperties(FunctionData function, Map properties) { + for (ParameterData paramResult : function.parameters().values()) { + if (paramResult.kind().equals(ParameterData.Kind.PARAM_FOR_TYPE_INFER) + || paramResult.kind().equals(ParameterData.Kind.INCLUDED_RECORD)) { + continue; + } + + String unescapedParamName = removeLeadingSingleQuote(paramResult.name()); + + Codedata codedata = new Codedata(); + codedata.setOriginalName(paramResult.name()); + + Value.ValueBuilder valueBuilder = new Value.ValueBuilder(); + valueBuilder + .setMetadata(new MetaData(unescapedParamName, paramResult.description())) + .setCodedata(codedata) + .setValue("") + .setValueType("EXPRESSION") + .setPlaceholder(paramResult.defaultValue()) + .setValueTypeConstraint(paramResult.type()) + .setEditable(true) + .setType(false) + .setOptional(paramResult.optional()) + .setAdvanced(paramResult.optional()); + + properties.put(unescapedParamName, valueBuilder.build()); + } + } + + public static String removeLeadingSingleQuote(String input) { + if (input != null && input.startsWith("'")) { + return input.substring(1); + } + return input; + } + + } diff --git a/service-model-generator/modules/service-model-generator-ls-extension/src/main/java/module-info.java b/service-model-generator/modules/service-model-generator-ls-extension/src/main/java/module-info.java index cf95592b8..df394a0a5 100644 --- a/service-model-generator/modules/service-model-generator-ls-extension/src/main/java/module-info.java +++ b/service-model-generator/modules/service-model-generator-ls-extension/src/main/java/module-info.java @@ -31,4 +31,5 @@ requires io.ballerina.parser; requires io.ballerina.tools.api; requires java.logging; + requires org.xerial.sqlitejdbc; } diff --git a/service-model-generator/modules/service-model-generator-ls-extension/src/main/resources/service-index.sqlite b/service-model-generator/modules/service-model-generator-ls-extension/src/main/resources/service-index.sqlite new file mode 100644 index 000000000..dd09600cb Binary files /dev/null and b/service-model-generator/modules/service-model-generator-ls-extension/src/main/resources/service-index.sqlite differ diff --git a/service-model-generator/modules/service-model-generator-ls-extension/src/test/java/io/ballerina/servicemodelgenerator/extension/GetListenerModelTest.java b/service-model-generator/modules/service-model-generator-ls-extension/src/test/java/io/ballerina/servicemodelgenerator/extension/GetListenerModelTest.java new file mode 100644 index 000000000..1b2d7591f --- /dev/null +++ b/service-model-generator/modules/service-model-generator-ls-extension/src/test/java/io/ballerina/servicemodelgenerator/extension/GetListenerModelTest.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com) + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.servicemodelgenerator.extension; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import io.ballerina.modelgenerator.commons.AbstractLSTest; +import io.ballerina.servicemodelgenerator.extension.model.Function; +import io.ballerina.servicemodelgenerator.extension.request.FunctionModelRequest; +import io.ballerina.servicemodelgenerator.extension.request.ListenerModelRequest; +import io.ballerina.servicemodelgenerator.extension.response.FunctionModelResponse; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * Assert the response returned by the getFunctionModel. + * + * @since 2.0.0 + */ +public class GetListenerModelTest extends AbstractLSTest { + + @Override + @Test(dataProvider = "data-provider") + public void test(Path config) throws IOException { + Path configJsonPath = configDir.resolve(config); + GetListenerModelTest.TestConfig testConfig = gson.fromJson(Files.newBufferedReader(configJsonPath), + GetListenerModelTest.TestConfig.class); + + ListenerModelRequest request = new ListenerModelRequest(testConfig.orgName(), testConfig.pkgName(), + testConfig.moduleName()); + JsonObject jsonMap = getResponse(request); + + boolean assertTrue = testConfig.response().getAsJsonObject().equals(jsonMap); + if (!assertTrue) { + GetListenerModelTest.TestConfig updatedConfig = + new GetListenerModelTest.TestConfig(testConfig.description(), testConfig.orgName(), + testConfig.pkgName(), testConfig.moduleName(), jsonMap); + updateConfig(configJsonPath, updatedConfig); + Assert.fail(String.format("Failed test: '%s' (%s)", testConfig.description(), configJsonPath)); + } + } + + + @Override + protected String getResourceDir() { + return "get_listener_model"; + } + + @Override + protected Class clazz() { + return GetListenerModelTest.class; + } + + @Override + protected String getServiceName() { + return "serviceDesign"; + } + + @Override + protected String getApiName() { + return "getListenerModel"; + } + + /** + * Represents the test configuration. + * + * @param description description of the test + * @param orgName organization name + * @param pkgName package name + * @param moduleName module name + * @since 2.0.0 + */ + private record TestConfig(String description, String orgName, String pkgName, String moduleName, + JsonElement response) { + public String description() { + return description == null ? "" : description; + } + } +} diff --git a/service-model-generator/modules/service-model-generator-ls-extension/src/test/resources/get_listener_model/config/http_listener_model.json b/service-model-generator/modules/service-model-generator-ls-extension/src/test/resources/get_listener_model/config/http_listener_model.json new file mode 100644 index 000000000..b83001110 --- /dev/null +++ b/service-model-generator/modules/service-model-generator-ls-extension/src/test/resources/get_listener_model/config/http_listener_model.json @@ -0,0 +1,313 @@ +{ + "description": "Test getting http listener model", + "orgName": "ballerina", + "pkgName": "http", + "moduleName": "http", + "response": { + "listener": { + "id": "1", + "name": "Http Listener", + "type": "http", + "displayName": "Http", + "description": "This is used for creating HTTP server endpoints. An HTTP server endpoint is capable of responding to\nremote callers. The `Listener` is responsible for initializing the endpoint using the provided configurations.", + "displayAnnotation": { + "label": "Http", + "iconPath": "https://bcentral-packageicons.azureedge.net/images/ballerina_http_2.12.2.png" + }, + "moduleName": "http", + "orgName": "ballerina", + "version": "2.12.2", + "packageName": "http", + "listenerProtocol": "http", + "icon": "https://bcentral-packageicons.azureedge.net/images/ballerina_http_2.12.2.png", + "properties": { + "server": { + "metadata": { + "label": "server", + "description": "" + }, + "enabled": false, + "editable": true, + "value": "", + "valueType": "EXPRESSION", + "valueTypeConstraint": "string|()", + "isType": false, + "placeholder": "()", + "optional": true, + "advanced": true, + "codedata": { + "inListenerInit": false, + "isBasePath": false, + "inDisplayAnnotation": false, + "originalName": "server" + }, + "addNewButton": false + }, + "gracefulStopTimeout": { + "metadata": { + "label": "gracefulStopTimeout", + "description": "" + }, + "enabled": false, + "editable": true, + "value": "", + "valueType": "EXPRESSION", + "valueTypeConstraint": "decimal", + "isType": false, + "placeholder": "http:DEFAULT_GRACEFULSTOP_TIMEOUT", + "optional": true, + "advanced": true, + "codedata": { + "inListenerInit": false, + "isBasePath": false, + "inDisplayAnnotation": false, + "originalName": "gracefulStopTimeout" + }, + "addNewButton": false + }, + "timeout": { + "metadata": { + "label": "timeout", + "description": "" + }, + "enabled": false, + "editable": true, + "value": "", + "valueType": "EXPRESSION", + "valueTypeConstraint": "decimal", + "isType": false, + "placeholder": "http:DEFAULT_LISTENER_TIMEOUT", + "optional": true, + "advanced": true, + "codedata": { + "inListenerInit": false, + "isBasePath": false, + "inDisplayAnnotation": false, + "originalName": "timeout" + }, + "addNewButton": false + }, + "requestLimits": { + "metadata": { + "label": "requestLimits", + "description": "" + }, + "enabled": false, + "editable": true, + "value": "", + "valueType": "EXPRESSION", + "valueTypeConstraint": "http:RequestLimitConfigs", + "isType": false, + "placeholder": "{}", + "optional": true, + "advanced": true, + "codedata": { + "inListenerInit": false, + "isBasePath": false, + "inDisplayAnnotation": false, + "originalName": "requestLimits" + }, + "addNewButton": false + }, + "socketConfig": { + "metadata": { + "label": "socketConfig", + "description": "" + }, + "enabled": false, + "editable": true, + "value": "", + "valueType": "EXPRESSION", + "valueTypeConstraint": "http:ServerSocketConfig", + "isType": false, + "placeholder": "{}", + "optional": true, + "advanced": true, + "codedata": { + "inListenerInit": false, + "isBasePath": false, + "inDisplayAnnotation": false, + "originalName": "socketConfig" + }, + "addNewButton": false + }, + "http2InitialWindowSize": { + "metadata": { + "label": "http2InitialWindowSize", + "description": "" + }, + "enabled": false, + "editable": true, + "value": "", + "valueType": "EXPRESSION", + "valueTypeConstraint": "int", + "isType": false, + "placeholder": "65535", + "optional": true, + "advanced": true, + "codedata": { + "inListenerInit": false, + "isBasePath": false, + "inDisplayAnnotation": false, + "originalName": "http2InitialWindowSize" + }, + "addNewButton": false + }, + "httpVersion": { + "metadata": { + "label": "httpVersion", + "description": "" + }, + "enabled": false, + "editable": true, + "value": "", + "valueType": "EXPRESSION", + "valueTypeConstraint": "http:HttpVersion", + "isType": false, + "placeholder": "http:HTTP_2_0", + "optional": true, + "advanced": true, + "codedata": { + "inListenerInit": false, + "isBasePath": false, + "inDisplayAnnotation": false, + "originalName": "httpVersion" + }, + "addNewButton": false + }, + "minIdleTimeInStaleState": { + "metadata": { + "label": "minIdleTimeInStaleState", + "description": "" + }, + "enabled": false, + "editable": true, + "value": "", + "valueType": "EXPRESSION", + "valueTypeConstraint": "decimal", + "isType": false, + "placeholder": "300", + "optional": true, + "advanced": true, + "codedata": { + "inListenerInit": false, + "isBasePath": false, + "inDisplayAnnotation": false, + "originalName": "minIdleTimeInStaleState" + }, + "addNewButton": false + }, + "port": { + "metadata": { + "label": "port", + "description": "Listening port of the HTTP service listener" + }, + "enabled": false, + "editable": true, + "value": "", + "valueType": "EXPRESSION", + "valueTypeConstraint": "int", + "isType": false, + "placeholder": "0", + "optional": false, + "advanced": false, + "codedata": { + "inListenerInit": false, + "isBasePath": false, + "inDisplayAnnotation": false, + "originalName": "port" + }, + "addNewButton": false + }, + "secureSocket": { + "metadata": { + "label": "secureSocket", + "description": "" + }, + "enabled": false, + "editable": true, + "value": "", + "valueType": "EXPRESSION", + "valueTypeConstraint": "http:ListenerSecureSocket|()", + "isType": false, + "placeholder": "()", + "optional": true, + "advanced": true, + "codedata": { + "inListenerInit": false, + "isBasePath": false, + "inDisplayAnnotation": false, + "originalName": "secureSocket" + }, + "addNewButton": false + }, + "host": { + "metadata": { + "label": "host", + "description": "" + }, + "enabled": false, + "editable": true, + "value": "", + "valueType": "EXPRESSION", + "valueTypeConstraint": "string", + "isType": false, + "placeholder": "\"0.0.0.0\"", + "optional": true, + "advanced": true, + "codedata": { + "inListenerInit": false, + "isBasePath": false, + "inDisplayAnnotation": false, + "originalName": "host" + }, + "addNewButton": false + }, + "timeBetweenStaleEviction": { + "metadata": { + "label": "timeBetweenStaleEviction", + "description": "" + }, + "enabled": false, + "editable": true, + "value": "", + "valueType": "EXPRESSION", + "valueTypeConstraint": "decimal", + "isType": false, + "placeholder": "30", + "optional": true, + "advanced": true, + "codedata": { + "inListenerInit": false, + "isBasePath": false, + "inDisplayAnnotation": false, + "originalName": "timeBetweenStaleEviction" + }, + "addNewButton": false + }, + "http1Settings": { + "metadata": { + "label": "http1Settings", + "description": "" + }, + "enabled": false, + "editable": true, + "value": "", + "valueType": "EXPRESSION", + "valueTypeConstraint": "http:ListenerHttp1Settings", + "isType": false, + "placeholder": "{}", + "optional": true, + "advanced": true, + "codedata": { + "inListenerInit": false, + "isBasePath": false, + "inDisplayAnnotation": false, + "originalName": "http1Settings" + }, + "addNewButton": false + } + } + } + } +} diff --git a/service-model-generator/modules/service-model-generator-ls-extension/src/test/resources/testng.xml b/service-model-generator/modules/service-model-generator-ls-extension/src/test/resources/testng.xml index 98710806c..926b45c2a 100644 --- a/service-model-generator/modules/service-model-generator-ls-extension/src/test/resources/testng.xml +++ b/service-model-generator/modules/service-model-generator-ls-extension/src/test/resources/testng.xml @@ -32,6 +32,7 @@ under the License. + diff --git a/service-model-generator/modules/service-model-index-generator/build.gradle b/service-model-generator/modules/service-model-index-generator/build.gradle new file mode 100644 index 000000000..ea6768ccc --- /dev/null +++ b/service-model-generator/modules/service-model-index-generator/build.gradle @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com) + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +apply from: "$rootDir/gradle/javaProject.gradle" +apply plugin: "com.github.johnrengelman.shadow" +apply plugin: "java" + +description = 'Index generator for the flow model extension' + +configurations.configureEach { + resolutionStrategy.preferProjectModules() +} + +configurations { + dist { + transitive true + } +} + +dependencies { + implementation project(':flow-model-generator:flow-model-central-client') + implementation(project(':flow-model-generator:flow-model-generator-core')) { + exclude group: 'io.ballerina.flowmodelgenerator.core', module: 'flow-model-generator-core' + } + implementation project(":model-generator-commons") + + implementation "org.ballerinalang:ballerina-lang:${ballerinaLangVersion}" + implementation "org.ballerinalang:ballerina-parser:${ballerinaLangVersion}" + implementation "org.ballerinalang:ballerina-tools-api:${ballerinaLangVersion}" + implementation "org.ballerinalang:diagram-util:${ballerinaLangVersion}" + implementation "org.ballerinalang:toml-parser:${ballerinaLangVersion}" + implementation "org.ballerinalang:language-server-core:${ballerinaLangVersion}" + implementation "org.xerial:sqlite-jdbc:${sqliteJdbcVersion}" + implementation "com.google.code.gson:gson:${gsonVersion}" +} + + +ext.moduleName = 'io.ballerina.indexgenerator' + +compileJava { + doFirst { + options.compilerArgs = [ + '--module-path', classpath.asPath, + ] + classpath = files() + } +} diff --git a/service-model-generator/modules/service-model-index-generator/src/main/java/io/ballerina/indexgenerator/DatabaseManager.java b/service-model-generator/modules/service-model-index-generator/src/main/java/io/ballerina/indexgenerator/DatabaseManager.java new file mode 100644 index 000000000..6774bc269 --- /dev/null +++ b/service-model-generator/modules/service-model-index-generator/src/main/java/io/ballerina/indexgenerator/DatabaseManager.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com) + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.indexgenerator; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.List; +import java.util.logging.Logger; + +/** + * Database Manager use to create the database and insert data. + * + * @since 2.0.0 + */ +class DatabaseManager { + + private static final Logger LOGGER = Logger.getLogger(DatabaseManager.class.getName()); + private static final String INDEX_FILE_NAME = "service-index.sqlite"; + private static final String SERVICE_INDEX_SQL = "service-index.sql"; + private static final String dbPath = getDatabasePath(); + + private static String getDatabasePath() { + String destinationPath = + Path.of("service-model-generator/modules/service-model-generator-ls-extension/src/main/resources") + .resolve(INDEX_FILE_NAME) + .toString(); + return "jdbc:sqlite:" + destinationPath; + } + + private static int insertEntry(String sql, Object[] params) { + try (Connection conn = DriverManager.getConnection(dbPath); + PreparedStatement stmt = conn.prepareStatement(sql)) { + for (int i = 0; i < params.length; i++) { + stmt.setObject(i + 1, params[i]); + } + stmt.executeUpdate(); + try (ResultSet generatedKeys = stmt.getGeneratedKeys()) { + if (generatedKeys.next()) { + return generatedKeys.getInt(1); + } else { + throw new SQLException("Creating package failed, no ID obtained."); + } + } + } catch (SQLException e) { + LOGGER.severe("Error executing query: " + e.getMessage()); + return -1; + } + } + + public static void createDatabase() { + Path destinationPath = + Path.of("service-model-generator/modules/service-model-index-generator/src/main/resources") + .resolve(SERVICE_INDEX_SQL); + try { + String sql = Files.readString(destinationPath); + executeQuery(sql); + } catch (IOException e) { + LOGGER.severe("Error reading SQL file: " + e.getMessage()); + } + } + + private static void executeQuery(String sql) { + try (Connection conn = DriverManager.getConnection(dbPath); + Statement stmt = conn.createStatement()) { // Use Statement instead + stmt.executeUpdate(sql); + LOGGER.info("Database created successfully"); + } catch (SQLException e) { + LOGGER.severe("Error executing query: " + e.getMessage()); + } + } + + public static int insertPackage(String org, String name, String version, List keywords) { + String sql = "INSERT INTO Package (org, name, version, keywords) VALUES (?, ?, ?, ?)"; + return insertEntry(sql, new Object[]{org, name, version, keywords == null ? "" : String.join(",", keywords)}); + } + + public static int insertListener(int packageId, String name, String description, int returnError) { + String sql = "INSERT INTO Listener (package_id, name, description, return_error) VALUES (?, ?, ?, ?)"; + return insertEntry(sql, new Object[]{packageId, name, description, returnError}); + } + + public static int insertListenerParameter(int listenerId, String paramName, String paramDescription, + String paramType, String defaultValue, + ServiceIndexGenerator.FunctionParameterKind parameterKind, + int optional, String importStatements) { + String sql = + "INSERT INTO Parameter (listener_id, name, description, type, default_value, kind, optional, " + + "import_statements) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; + return insertEntry(sql, + new Object[]{listenerId, paramName, paramDescription, paramType, defaultValue, + parameterKind.name(), optional, importStatements}); + } + + public static void insertParameterMemberType(int parameterId, String type, String kind, String packageIdentifier) { + String sql = "INSERT INTO ParameterMemberType (parameter_id, type, kind, package) " + + "VALUES (?, ?, ?, ?)"; + insertEntry(sql, new Object[]{parameterId, type, kind, packageIdentifier}); + } + +} diff --git a/service-model-generator/modules/service-model-index-generator/src/main/java/io/ballerina/indexgenerator/ServiceIndexGenerator.java b/service-model-generator/modules/service-model-index-generator/src/main/java/io/ballerina/indexgenerator/ServiceIndexGenerator.java new file mode 100644 index 000000000..941f002e3 --- /dev/null +++ b/service-model-generator/modules/service-model-index-generator/src/main/java/io/ballerina/indexgenerator/ServiceIndexGenerator.java @@ -0,0 +1,477 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com) + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.indexgenerator; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import io.ballerina.compiler.api.ModuleID; +import io.ballerina.compiler.api.SemanticModel; +import io.ballerina.compiler.api.TypeBuilder; +import io.ballerina.compiler.api.Types; +import io.ballerina.compiler.api.symbols.ArrayTypeSymbol; +import io.ballerina.compiler.api.symbols.ClassSymbol; +import io.ballerina.compiler.api.symbols.Documentable; +import io.ballerina.compiler.api.symbols.Documentation; +import io.ballerina.compiler.api.symbols.ErrorTypeSymbol; +import io.ballerina.compiler.api.symbols.FunctionSymbol; +import io.ballerina.compiler.api.symbols.FunctionTypeSymbol; +import io.ballerina.compiler.api.symbols.MapTypeSymbol; +import io.ballerina.compiler.api.symbols.MethodSymbol; +import io.ballerina.compiler.api.symbols.ObjectTypeSymbol; +import io.ballerina.compiler.api.symbols.ParameterSymbol; +import io.ballerina.compiler.api.symbols.RecordFieldSymbol; +import io.ballerina.compiler.api.symbols.RecordTypeSymbol; +import io.ballerina.compiler.api.symbols.StreamTypeSymbol; +import io.ballerina.compiler.api.symbols.Symbol; +import io.ballerina.compiler.api.symbols.SymbolKind; +import io.ballerina.compiler.api.symbols.TableTypeSymbol; +import io.ballerina.compiler.api.symbols.TupleTypeSymbol; +import io.ballerina.compiler.api.symbols.TypeDescKind; +import io.ballerina.compiler.api.symbols.TypeSymbol; +import io.ballerina.compiler.api.symbols.UnionTypeSymbol; +import io.ballerina.compiler.syntax.tree.DefaultableParameterNode; +import io.ballerina.compiler.syntax.tree.ExpressionNode; +import io.ballerina.compiler.syntax.tree.ModulePartNode; +import io.ballerina.compiler.syntax.tree.NonTerminalNode; +import io.ballerina.compiler.syntax.tree.QualifiedNameReferenceNode; +import io.ballerina.compiler.syntax.tree.RecordFieldWithDefaultValueNode; +import io.ballerina.compiler.syntax.tree.SimpleNameReferenceNode; +import io.ballerina.compiler.syntax.tree.SyntaxKind; +import io.ballerina.modelgenerator.commons.CommonUtils; +import io.ballerina.modelgenerator.commons.DefaultValueGeneratorUtil; +import io.ballerina.modelgenerator.commons.ModuleInfo; +import io.ballerina.modelgenerator.commons.PackageUtil; +import io.ballerina.projects.Document; +import io.ballerina.projects.DocumentId; +import io.ballerina.projects.Module; +import io.ballerina.projects.Package; +import io.ballerina.projects.PackageDescriptor; +import io.ballerina.projects.Project; +import io.ballerina.projects.directory.BuildProject; +import io.ballerina.tools.diagnostics.Location; +import io.ballerina.tools.text.TextRange; +import org.ballerinalang.langserver.common.utils.CommonUtil; + +import java.io.FileReader; +import java.io.IOException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ForkJoinPool; +import java.util.logging.Logger; + +/** + * Index generator to cache functions and connectors. + * + * @since 2.0.0 + */ +class ServiceIndexGenerator { + + private static final java.lang.reflect.Type typeToken = new TypeToken>>() {}.getType(); + private static final Logger LOGGER = Logger.getLogger(ServiceIndexGenerator.class.getName()); + private static final String PACKAGE_JSON_FILE = "packages.json"; + + public static void main(String[] args) { + DatabaseManager.createDatabase(); + BuildProject buildProject = PackageUtil.getSampleProject(); + + Gson gson = new Gson(); + URL resource = ServiceIndexGenerator.class.getClassLoader().getResource(PACKAGE_JSON_FILE); + try (FileReader reader = new FileReader(Objects.requireNonNull(resource).getFile(), StandardCharsets.UTF_8)) { + Map> packagesMap = gson.fromJson(reader, + typeToken); + ForkJoinPool forkJoinPool = new ForkJoinPool(Runtime.getRuntime().availableProcessors()); + forkJoinPool.submit(() -> packagesMap.forEach((key, value) -> value.forEach( + packageMetadataInfo -> resolvePackage(buildProject, key, packageMetadataInfo)))).join(); + } catch (IOException e) { + LOGGER.severe("Error reading packages JSON file: " + e.getMessage()); + } + } + + private static void resolvePackage(BuildProject buildProject, String org, + PackageMetadataInfo packageMetadataInfo) { + Package resolvedPackage; + try { + resolvedPackage = Objects.requireNonNull(PackageUtil.getModulePackage(buildProject, org, + packageMetadataInfo.name(), packageMetadataInfo.version())).orElseThrow(); + } catch (Throwable e) { + LOGGER.severe("Error resolving package: " + packageMetadataInfo.name() + e.getMessage()); + return; + } + PackageDescriptor descriptor = resolvedPackage.descriptor(); + + LOGGER.info("Processing package: " + descriptor.name().value()); + int packageId = DatabaseManager.insertPackage(descriptor.org().value(), descriptor.name().value(), + descriptor.version().value().toString(), resolvedPackage.manifest().keywords()); + if (packageId == -1) { + LOGGER.severe("Error inserting package to database: " + descriptor.name().value()); + return; + } + + SemanticModel semanticModel; + try { + semanticModel = resolvedPackage.getCompilation() + .getSemanticModel(resolvedPackage.getDefaultModule().moduleId()); + } catch (Exception e) { + LOGGER.severe("Error reading semantic model: " + e.getMessage()); + return; + } + + TypeSymbol errorTypeSymbol = semanticModel.types().ERROR; + + for (Symbol symbol : semanticModel.moduleSymbols()) { + + if (symbol.kind() == SymbolKind.CLASS) { + ClassSymbol classSymbol = (ClassSymbol) symbol; + + if (classSymbol.nameEquals("Listener")) { + + Optional initMethodSymbol = classSymbol.initMethod(); + if (initMethodSymbol.isEmpty()) { + continue; + } + processListenerInit(semanticModel, initMethodSymbol.get(), classSymbol, packageId, + errorTypeSymbol, resolvedPackage); + } + continue; + } + + // TODO: handle service types + + // TODO: process the annotation attachments + } + } + + private static void processListenerInit(SemanticModel semanticModel, FunctionSymbol functionSymbol, + Documentable documentable, int packageId, + TypeSymbol errorTypeSymbol, Package resolvedPackage) { + // Capture the name of the function + Optional name = functionSymbol.getName(); + if (name.isEmpty()) { + return; + } + + // Obtain the description of the function + String description = getDescription(documentable); + Map documentationMap = functionSymbol.documentation().map(Documentation::parameterMap) + .orElse(Map.of()); + + // Obtain the return type of the function + FunctionTypeSymbol functionTypeSymbol = functionSymbol.typeDescriptor(); + + int returnError = functionTypeSymbol.returnTypeDescriptor().map(returnTypeDesc -> + CommonUtils.subTypeOf(returnTypeDesc, errorTypeSymbol) ? 1 : 0).orElse(0); + + int functionId = DatabaseManager.insertListener(packageId, name.get(), description, returnError); + + ModuleInfo defaultModuleInfo = ModuleInfo.from(resolvedPackage.getDefaultModule().descriptor()); + functionTypeSymbol.params().ifPresent( + paramList -> paramList.forEach(paramSymbol -> processParameterSymbol(paramSymbol, + documentationMap, functionId, resolvedPackage, null, + defaultModuleInfo, semanticModel))); + functionTypeSymbol.restParam() + .ifPresent(paramSymbol -> processParameterSymbol(paramSymbol, + documentationMap, functionId, resolvedPackage, null, + defaultModuleInfo, semanticModel)); + } + + private static void processParameterSymbol(ParameterSymbol paramSymbol, Map documentationMap, + int functionId, Package resolvedPackage, + ParamForTypeInfer paramForTypeInfer, + ModuleInfo defaultModuleInfo, SemanticModel semanticModel) { + String paramName = paramSymbol.getName().orElse(""); + String paramDescription = documentationMap.get(paramName); + FunctionParameterKind parameterKind = FunctionParameterKind.fromString(paramSymbol.paramKind().toString()); + String paramType; + int optional = 1; + String defaultValue; + TypeSymbol typeSymbol = paramSymbol.typeDescriptor(); + String importStatements = CommonUtils.getImportStatements(typeSymbol, defaultModuleInfo).orElse(null); + if (parameterKind == FunctionParameterKind.REST_PARAMETER) { + defaultValue = DefaultValueGeneratorUtil.getDefaultValueForType( + ((ArrayTypeSymbol) typeSymbol).memberTypeDescriptor()); + paramType = CommonUtils.getTypeSignature(semanticModel, + ((ArrayTypeSymbol) typeSymbol).memberTypeDescriptor(), false); + } else if (parameterKind == FunctionParameterKind.INCLUDED_RECORD) { + paramType = CommonUtils.getTypeSignature(semanticModel, typeSymbol, false); + addIncludedRecordParamsToDb((RecordTypeSymbol) CommonUtils.getRawType(typeSymbol), + functionId, resolvedPackage, defaultModuleInfo, semanticModel, true, new HashMap<>()); + defaultValue = DefaultValueGeneratorUtil.getDefaultValueForType(typeSymbol); + } else if (parameterKind == FunctionParameterKind.REQUIRED) { + paramType = CommonUtils.getTypeSignature(semanticModel, typeSymbol, false); + defaultValue = DefaultValueGeneratorUtil.getDefaultValueForType(typeSymbol); + optional = 0; + } else { + if (paramForTypeInfer != null) { + if (paramForTypeInfer.paramName().equals(paramName)) { + defaultValue = paramForTypeInfer.type(); + paramType = paramForTypeInfer.type(); + DatabaseManager.insertListenerParameter(functionId, paramName, paramDescription, + paramType, defaultValue, FunctionParameterKind.PARAM_FOR_TYPE_INFER, optional, + importStatements); + return; + } + } + Location symbolLocation = paramSymbol.getLocation().get(); + Document document = findDocument(resolvedPackage, symbolLocation.lineRange().fileName()); + defaultValue = DefaultValueGeneratorUtil.getDefaultValueForType(typeSymbol); + if (document != null) { + defaultValue = getParamDefaultValue(document.syntaxTree().rootNode(), + symbolLocation, resolvedPackage.packageName().value()); + } + paramType = CommonUtils.getTypeSignature(semanticModel, typeSymbol, false); + } + int paramId = DatabaseManager.insertListenerParameter(functionId, paramName, paramDescription, paramType, + defaultValue, parameterKind, optional, importStatements); + insertParameterMemberTypes(paramId, typeSymbol, semanticModel); + } + + protected static void addIncludedRecordParamsToDb(RecordTypeSymbol recordTypeSymbol, int functionId, + Package resolvedPackage, ModuleInfo defaultModuleInfo, + SemanticModel semanticModel, boolean insert, + Map documentationMap) { + recordTypeSymbol.typeInclusions().forEach(includedType -> addIncludedRecordParamsToDb( + ((RecordTypeSymbol) CommonUtils.getRawType(includedType)), functionId, resolvedPackage, + defaultModuleInfo, semanticModel, false, documentationMap) + ); + for (Map.Entry entry : recordTypeSymbol.fieldDescriptors().entrySet()) { + RecordFieldSymbol recordFieldSymbol = entry.getValue(); + TypeSymbol typeSymbol = recordFieldSymbol.typeDescriptor(); + TypeSymbol fieldType = CommonUtil.getRawType(typeSymbol); + if (fieldType.typeKind() == TypeDescKind.NEVER) { + continue; + } + String paramName = entry.getKey(); + String paramDescription = entry.getValue().documentation() + .flatMap(Documentation::description).orElse(""); + if (documentationMap.containsKey(paramName) && !paramDescription.isEmpty()) { + documentationMap.put(paramName, paramDescription); + } else if (!documentationMap.containsKey(paramName)) { + documentationMap.put(paramName, paramDescription); + } + if (!insert) { + continue; + } + + Location symbolLocation = recordFieldSymbol.getLocation().get(); + Document document = findDocument(resolvedPackage, symbolLocation.lineRange().fileName()); + String defaultValue; + if (document != null) { + defaultValue = getAttributeDefaultValue(document.syntaxTree().rootNode(), + symbolLocation, resolvedPackage.packageName().value()); + if (defaultValue == null) { + defaultValue = DefaultValueGeneratorUtil.getDefaultValueForType(fieldType); + } + } else { + defaultValue = DefaultValueGeneratorUtil.getDefaultValueForType(fieldType); + } + String paramType = CommonUtils.getTypeSignature(semanticModel, typeSymbol, false); + int optional = 0; + if (recordFieldSymbol.isOptional() || recordFieldSymbol.hasDefaultValue()) { + optional = 1; + } + int paramId = DatabaseManager.insertListenerParameter(functionId, paramName, + documentationMap.get(paramName), paramType, defaultValue, + FunctionParameterKind.INCLUDED_FIELD, optional, + CommonUtils.getImportStatements(typeSymbol, defaultModuleInfo).orElse(null)); + insertParameterMemberTypes(paramId, typeSymbol, semanticModel); + } + recordTypeSymbol.restTypeDescriptor().ifPresent(typeSymbol -> { + String paramType = CommonUtils.getTypeSignature(semanticModel, typeSymbol, false); + String defaultValue = DefaultValueGeneratorUtil.getDefaultValueForType(typeSymbol); + DatabaseManager.insertListenerParameter(functionId, "Additional Values", + "Capture key value pairs", paramType, defaultValue, + FunctionParameterKind.INCLUDED_RECORD_REST, 1, + CommonUtils.getImportStatements(typeSymbol, defaultModuleInfo).orElse(null)); + }); + } + + private static void insertParameterMemberTypes(int parameterId, TypeSymbol typeSymbol, + SemanticModel semanticModel) { + Types types = semanticModel.types(); + TypeBuilder builder = semanticModel.types().builder(); + UnionTypeSymbol union = builder.UNION_TYPE.withMemberTypes( + types.BOOLEAN, types.NIL, types.STRING, types.INT, types.FLOAT, + types.DECIMAL, types.BYTE, types.REGEX, types.XML).build(); + + if (typeSymbol instanceof UnionTypeSymbol unionTypeSymbol) { + unionTypeSymbol.memberTypeDescriptors().forEach( + memberType -> insertParameterMemberTypes(parameterId, memberType, semanticModel)); + return; + } + + String packageIdentifier = ""; + ModuleInfo moduleInfo = null; + if (typeSymbol.getModule().isPresent()) { + ModuleID id = typeSymbol.getModule().get().id(); + packageIdentifier = "%s:%s:%s".formatted(id.orgName(), id.moduleName(), id.version()); + moduleInfo = ModuleInfo.from(id); + } + String type = CommonUtils.getTypeSignature(typeSymbol, moduleInfo); + String kind = "OTHER"; + TypeSymbol rawType = CommonUtils.getRawType(typeSymbol); + if (typeSymbol.subtypeOf(union)) { + kind = "BASIC_TYPE"; + } else if (rawType instanceof TupleTypeSymbol) { + kind = "TUPLE_TYPE"; + } else if (rawType instanceof ArrayTypeSymbol arrayTypeSymbol) { + kind = "ARRAY_TYPE"; + TypeSymbol memberType = arrayTypeSymbol.memberTypeDescriptor(); + if (memberType.getModule().isPresent()) { + ModuleID id = memberType.getModule().get().id(); + packageIdentifier = "%s:%s:%s".formatted(id.orgName(), id.moduleName(), id.version()); + moduleInfo = ModuleInfo.from(id); + } + type = CommonUtils.getTypeSignature(memberType, moduleInfo); + } else if (rawType instanceof RecordTypeSymbol) { + if (typeSymbol instanceof RecordTypeSymbol) { + kind = "ANON_RECORD_TYPE"; + } else { + kind = "RECORD_TYPE"; + } + } else if (rawType instanceof MapTypeSymbol mapTypeSymbol) { + kind = "MAP_TYPE"; + TypeSymbol typeParam = mapTypeSymbol.typeParam(); + if (typeParam.getModule().isPresent()) { + ModuleID id = typeParam.getModule().get().id(); + packageIdentifier = "%s:%s:%s".formatted(id.orgName(), id.moduleName(), id.version()); + moduleInfo = ModuleInfo.from(id); + } + type = CommonUtils.getTypeSignature(typeSymbol, moduleInfo); + } else if (rawType instanceof TableTypeSymbol tableTypeSymbol) { + kind = "TABLE_TYPE"; + TypeSymbol rowTypeParameter = tableTypeSymbol.rowTypeParameter(); + if (rowTypeParameter.getModule().isPresent()) { + ModuleID id = rowTypeParameter.getModule().get().id(); + packageIdentifier = "%s:%s:%s".formatted(id.orgName(), id.moduleName(), id.version()); + moduleInfo = ModuleInfo.from(id); + } + type = CommonUtils.getTypeSignature(typeSymbol, moduleInfo); + } else if (rawType instanceof StreamTypeSymbol) { + kind = "STREAM_TYPE"; + } else if (rawType instanceof ObjectTypeSymbol) { + kind = "OBJECT_TYPE"; + } else if (rawType instanceof FunctionTypeSymbol) { + kind = "FUNCTION_TYPE"; + } else if (rawType instanceof ErrorTypeSymbol) { + kind = "ERROR_TYPE"; + } + + DatabaseManager.insertParameterMemberType(parameterId, type, kind, packageIdentifier); + } + + private static String getDescription(Documentable documentable) { + return documentable.documentation().flatMap(Documentation::description).orElse(""); + } + + enum FunctionType { + FUNCTION, + REMOTE, + LISTENER_INIT, + RESOURCE + } + + enum AttachToServiceKind { + SERVICE, + LISTENER + } + + enum FunctionParameterKind { + REQUIRED, + DEFAULTABLE, + INCLUDED_RECORD, + REST_PARAMETER, + INCLUDED_FIELD, + PARAM_FOR_TYPE_INFER, + INCLUDED_RECORD_REST, + PATH_PARAM, + PATH_REST_PARAM; + + // need to have a fromString logic here + public static FunctionParameterKind fromString(String value) { + if (value.equals("REST")) { + return REST_PARAMETER; + } + return FunctionParameterKind.valueOf(value); + } + + private FunctionParameterKind() { + } + } + + private static String getParamDefaultValue(ModulePartNode rootNode, Location location, String module) { + NonTerminalNode node = rootNode.findNode(TextRange.from(location.textRange().startOffset(), + location.textRange().length())); + if (node.kind() == SyntaxKind.DEFAULTABLE_PARAM) { + DefaultableParameterNode valueNode = (DefaultableParameterNode) node; + ExpressionNode expression = (ExpressionNode) valueNode.expression(); + if (expression instanceof SimpleNameReferenceNode simpleNameReferenceNode) { + return module + ":" + simpleNameReferenceNode.name().text(); + } else if (expression instanceof QualifiedNameReferenceNode qualifiedNameReferenceNode) { + return qualifiedNameReferenceNode.modulePrefix().text() + ":" + qualifiedNameReferenceNode.identifier() + .text(); + } else { + return expression.toSourceCode(); + } + } + return null; + } + + private static String getAttributeDefaultValue(ModulePartNode rootNode, Location location, String module) { + NonTerminalNode node = rootNode.findNode(TextRange.from(location.textRange().startOffset(), + location.textRange().length())); + if (node.kind() == SyntaxKind.RECORD_FIELD_WITH_DEFAULT_VALUE) { + RecordFieldWithDefaultValueNode valueNode = (RecordFieldWithDefaultValueNode) node; + ExpressionNode expression = valueNode.expression(); + if (expression instanceof SimpleNameReferenceNode simpleNameReferenceNode) { + return module + ":" + simpleNameReferenceNode.name().text(); + } else if (expression instanceof QualifiedNameReferenceNode qualifiedNameReferenceNode) { + return qualifiedNameReferenceNode.modulePrefix().text() + ":" + qualifiedNameReferenceNode.identifier() + .text(); + } else { + return expression.toSourceCode(); + } + } + return null; + } + + public static Document findDocument(Package pkg, String path) { + Project project = pkg.project(); + Module defaultModule = pkg.getDefaultModule(); + String module = pkg.packageName().value(); + Path docPath = project.sourceRoot().resolve("modules").resolve(module).resolve(path); + try { + DocumentId documentId = project.documentId(docPath); + return defaultModule.document(documentId); + } catch (RuntimeException ex) { + return null; + } + } + + private record ParamForTypeInfer(String paramName, String defaultValue, String type) { + } + + private record PackageMetadataInfo(String name, String version, List serviceTypeSkipList) { } +} diff --git a/service-model-generator/modules/service-model-index-generator/src/main/java/module-info.java b/service-model-generator/modules/service-model-index-generator/src/main/java/module-info.java new file mode 100644 index 000000000..575f6750e --- /dev/null +++ b/service-model-generator/modules/service-model-index-generator/src/main/java/module-info.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.com) + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module io.ballerina.flowmodel.indexgenerator { + requires io.ballerina.lang; + requires io.ballerina.tools.api; + requires io.ballerina.parser; + requires io.ballerina.centralconnector; + requires com.google.gson; + requires io.ballerina.diagram.util; + requires java.sql; + requires io.ballerina.toml; + requires io.ballerina.flow.model.generator; + requires io.ballerina.language.server.core; + requires io.ballerina.model.generator.commons; + + exports io.ballerina.indexgenerator; +} diff --git a/service-model-generator/modules/service-model-index-generator/src/main/resources/packages.json b/service-model-generator/modules/service-model-index-generator/src/main/resources/packages.json new file mode 100644 index 000000000..e59d3b308 --- /dev/null +++ b/service-model-generator/modules/service-model-index-generator/src/main/resources/packages.json @@ -0,0 +1,62 @@ +{ + "ballerina": [ + { + "name": "http", + "version": "2.12.2", + "serviceTypeSkipList": ["Service", "ServiceContract", "RequestInterceptor", "ResponseInterceptor", + "RequestErrorInterceptor", "ResponseErrorInterceptor", "InterceptableService"] + }, + { + "name": "graphql", + "version": "1.15.0", + "serviceTypeSkipList": ["Interceptor", "Service"] + }, + { + "name": "tcp", + "version": "1.12.1", + "serviceTypeSkipList": ["ConnectionService"] + }, + { + "name": "file", + "version": "1.10.0", + "serviceTypeSkipList": ["Service"] + }, + { + "name": "ftp", + "version": "2.11.0", + "serviceTypeSkipList": ["Service"] + }, + { + "name": "mqtt", + "version": "1.3.0", + "serviceTypeSkipList": [] + } + ], + "ballerinax": [ + { + "name": "asb", + "version": "3.9.0", + "serviceTypeSkipList": [] + }, + { + "name": "rabbitmq", + "version": "3.1.0", + "serviceTypeSkipList": [] + }, + { + "name": "kafka", + "version": "4.2.0", + "serviceTypeSkipList": [] + }, + { + "name": "salesforce", + "version": "8.1.0", + "serviceTypeSkipList": [] + }, + { + "name": "trigger.github", + "version": "0.9.2", + "serviceTypeSkipList": [] + } + ] +} diff --git a/service-model-generator/modules/service-model-index-generator/src/main/resources/service-index.sql b/service-model-generator/modules/service-model-index-generator/src/main/resources/service-index.sql new file mode 100644 index 000000000..27d03aeec --- /dev/null +++ b/service-model-generator/modules/service-model-index-generator/src/main/resources/service-index.sql @@ -0,0 +1,85 @@ +-- Drop tables if they already exist to prevent conflicts +DROP TABLE IF EXISTS Package; +DROP TABLE IF EXISTS Listener; +DROP TABLE IF EXISTS Parameter; +DROP TABLE IF EXISTS ParameterMemberType; +DROP TABLE IF EXISTS Annotation; +DROP TABLE IF EXISTS AnnotationField; +DROP TABLE IF EXISTS AnnotationFieldMemberType; + +-- Create Package table +CREATE TABLE Package ( + package_id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + org TEXT NOT NULL, + version TEXT, + keywords TEXT +); + +-- Create Function table +CREATE TABLE Listener ( + listener_id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + description TEXT, + return_error INTEGER CHECK(return_error IN (0, 1)), + package_id INTEGER, + FOREIGN KEY (package_id) REFERENCES Package(package_id) ON DELETE CASCADE +); + +-- Create Parameter table +CREATE TABLE Parameter ( + parameter_id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + description TEXT, + kind TEXT CHECK(kind IN ('REQUIRED', 'DEFAULTABLE', 'INCLUDED_RECORD', 'REST_PARAMETER', + 'INCLUDED_FIELD', 'INCLUDED_RECORD_REST', 'PARAM_FOR_TYPE_INFER', 'PATH_PARAM', 'PATH_REST_PARAM')), + type JSON, -- JSON type for parameter type information + default_value TEXT, + optional INTEGER CHECK(optional IN (0, 1)), + import_statements TEXT, + listener_id INTEGER, + FOREIGN KEY (listener_id) REFERENCES Function(listener_id) ON DELETE CASCADE +); + +-- Create Parameter Member Type table +CREATE TABLE ParameterMemberType ( + member_id INTEGER PRIMARY KEY AUTOINCREMENT, + type JSON, -- JSON type for parameter type information + kind TEXT, + parameter_id INTEGER, + package TEXT, -- format of the package is org:name:version + FOREIGN KEY (parameter_id) REFERENCES Parameter(parameter_id) ON DELETE CASCADE +); + +-- Create Annotation table +CREATE TABLE Annotation ( + annotation_id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + attachment_points TEXT NOT NULL, + package_id INTEGER, + FOREIGN KEY (package_id) REFERENCES Package(package_id) ON DELETE CASCADE +); + +-- Create Annotation Attachment table +CREATE TABLE AnnotationField ( + annotation_field_id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + description TEXT, + kind TEXT CHECK(kind IN ('REQUIRED', 'DEFAULTABLE', 'INCLUDED_RECORD','INCLUDED_FIELD', 'INCLUDED_RECORD_REST')), + type JSON, -- JSON type for parameter type information + default_value TEXT, + optional INTEGER CHECK(optional IN (0, 1)), + import_statements TEXT, + annotation_id INTEGER, + FOREIGN KEY (annotation_id) REFERENCES Annotation(annotation_id) ON DELETE CASCADE +); + +-- Create Annotation Field Member Type table +CREATE TABLE AnnotationFieldMemberType ( + member_id INTEGER PRIMARY KEY AUTOINCREMENT, + type JSON, -- JSON type for parameter type information + kind TEXT, + annotation_field_id INTEGER, + package TEXT, -- format of the package is org:name:version + FOREIGN KEY (annotation_field_id) REFERENCES AnnotationField(annotation_field_id) ON DELETE CASCADE +); diff --git a/settings.gradle b/settings.gradle index d06f48bf3..f5ec1b964 100644 --- a/settings.gradle +++ b/settings.gradle @@ -36,6 +36,7 @@ include(':flow-model-generator:flow-model-generator-ls-extension') include(':flow-model-generator:flow-model-index-generator') include(':flow-model-generator:flow-model-central-client') include(':service-model-generator:service-model-generator-ls-extension') +include(':service-model-generator:service-model-index-generator') include(':test-manager-service:test-manager-service-ls-extension') project(':checkstyle').projectDir = file("build-config${File.separator}checkstyle") @@ -53,6 +54,7 @@ project(':flow-model-generator:flow-model-generator-ls-extension').projectDir = project(':flow-model-generator:flow-model-index-generator').projectDir = file('flow-model-generator/modules/flow-model-index-generator') project(':flow-model-generator:flow-model-central-client').projectDir = file('flow-model-generator/modules/flow-model-central-client') project(':service-model-generator:service-model-generator-ls-extension').projectDir = file('service-model-generator/modules/service-model-generator-ls-extension') +project(':service-model-generator:service-model-index-generator').projectDir = file('service-model-generator/modules/service-model-index-generator') project(':test-manager-service:test-manager-service-ls-extension').projectDir = file('test-manager-service/modules/test-manager-service-ls-extension') gradleEnterprise {