Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
bbfcd82
added language server to backend
uiysg Jan 13, 2026
c482896
basline metamodel resolving works next Step: refactoring
uiysg Jan 19, 2026
4e9c43c
refactored and added LSP jat to git
uiysg Jan 19, 2026
b9e9507
deleted comment lines from 102 until 113
uiysg Jan 19, 2026
69a9cde
worked on checkstyle errors
uiysg Jan 19, 2026
2c41037
returning to last commit to avoid ne errors
uiysg Jan 19, 2026
421c3e2
Fix JaCoCo coverage by excluding LSP directory
uiysg Jan 19, 2026
731b757
did some checkstyle refacotrings for github-action bot
uiysg Jan 19, 2026
50631ca
corrected checkstyle
uiysg Jan 20, 2026
9ad487c
corrected more checkstyle errors
uiysg Jan 20, 2026
c594eee
corrected some more javadoc warnings
uiysg Jan 20, 2026
523cf5c
corrected some more javadoc warnings
uiysg Jan 20, 2026
91a395f
added endpoint to javadoc to resolve checkstyle errors
uiysg Jan 22, 2026
be942a4
Merge branch 'develop' into language-server-integration
ma-mirzaei Jan 23, 2026
493ca5c
Now only the metamodels that are also in VSUM are loaded
uiysg Jan 27, 2026
dced2ba
rmeoved the comments from the Builder in MetaModelService
uiysg Jan 27, 2026
e498c51
removed checkstyle errors
uiysg Jan 27, 2026
7d2a141
removed unnecessary model.ecore file
uiysg Jan 27, 2026
fd90c0a
added tests to the lsp features
uiysg Jan 27, 2026
4c825c0
solved checkstyle errors in test classes
uiysg Jan 27, 2026
9e1029d
added the google checktslye plugin to the pom after accidentally dele…
uiysg Jan 28, 2026
6ade5c0
fixed spotless checkstyle errors
uiysg Jan 28, 2026
45604f1
changed LSPWebSocketHandler to conform to SOnarCloud
uiysg Jan 28, 2026
f62bc22
refactored some code in WebSocketHandler and in MetaModelServiceTest
uiysg Jan 28, 2026
614f935
refactored code to tackle Security Hotspots
uiysg Jan 28, 2026
75826d9
refactored code to solve some sonarIssues
uiysg Jan 28, 2026
837c32c
refactored more Code to solve SonarIssues
uiysg Jan 28, 2026
a2f8f57
removed the @Autowired
uiysg Jan 28, 2026
dec18e9
added javadoc
uiysg Jan 28, 2026
94c5877
refactored too long lines
uiysg Jan 28, 2026
cb8643f
solving SonartIssue reltaed to too long string
uiysg Jan 28, 2026
c7aa27c
try to fix sonar security Issue
uiysg Jan 28, 2026
775c8f5
try to fix sonar security Issue
uiysg Jan 28, 2026
3f7a249
removed unecessary logs
uiysg Jan 30, 2026
757d75f
removed unecessary logs
uiysg Jan 30, 2026
23ae43c
used configuraiton path for jar file
uiysg Jan 30, 2026
b49f392
fixed LspWebHandlerTest
uiysg Jan 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 4 additions & 18 deletions app/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,10 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
</dependencies>

<build>
Expand Down Expand Up @@ -228,24 +232,6 @@
</systemPropertyVariables>
</configuration>
</plugin>
<plugin>
<groupId>com.diffplug.spotless</groupId>
<artifactId>spotless-maven-plugin</artifactId>
<version>2.43.0</version>
<configuration>
<java>
<googleJavaFormat/>
</java>
</configuration>
<executions>
<execution>
<goals>
<goal>apply</goal>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,308 @@
package tools.vitruv.methodologist.config;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.concurrent.ConcurrentHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import tools.vitruv.methodologist.vsum.model.MetaModel;
import tools.vitruv.methodologist.vsum.service.MetaModelService;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;

@Component
public class LspWebSocketHandler extends TextWebSocketHandler {

@Autowired
private MetaModelService metaModelService;

private static final Logger logger = LoggerFactory.getLogger(LspWebSocketHandler.class);
private final ConcurrentHashMap<String, LspServerProcess> sessions = new ConcurrentHashMap<>();

@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
Long userId = extractUserId(session);
Long projectId = extractProjectId(session);

if (userId == null) {
session.close(CloseStatus.POLICY_VIOLATION.withReason("userId required"));
return;
}

Path sessionDir = Files.createTempDirectory("lsp-session-" + session.getId());
Path userProject = sessionDir.resolve("UserProject");
Path modelDir = userProject.resolve("model");
Files.createDirectories(modelDir);
List<MetaModel> metamodels = metaModelService
.findAccessibleByUserOrProject(userId, projectId);

for (MetaModel mm : metamodels) {
byte[] ecoreData = mm.getEcoreFile().getData();
String fileName = mm.getEcoreFile().getFilename();
Path ecoreFile = modelDir.resolve(fileName);
Files.write(ecoreFile, ecoreData);
}

String jarPath = new File("src/main/resources/lsp/tools.vitruv.dsls.reactions.ide.jar")
.getAbsolutePath();

ProcessBuilder pb = new ProcessBuilder(
"java",
"-jar", jarPath,
"-log", "-trace");
pb.directory(userProject.toFile());
pb.redirectErrorStream(true);

Process process = pb.start();

BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8));
BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8));

LspServerProcess lspProcess = new LspServerProcess(
session, process, writer, reader, sessionDir, userProject);
sessions.put(session.getId(), lspProcess);

new Thread(() -> lspProcess.readFromLsp()).start();

new Thread(() -> {
try {
Thread.sleep(500);

String rootUriMessage = String.format(
"{\"type\":\"workspaceReady\",\"rootUri\":\"%s\"}",
userProject.toUri().toString());
session.sendMessage(new TextMessage(rootUriMessage));
} catch (Exception e) {
System.err.println("💥 Failed to send workspaceReady: " + e.getMessage());
e.printStackTrace();
}
}).start();
}

@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
LspServerProcess serverProcess = sessions.get(session.getId());
if (serverProcess != null) {
try {
serverProcess.sendToLsp(message.getPayload());
} catch (IOException e) {
logger.error("Failed to send message to LSP", e);
}
}
}

@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
LspServerProcess serverProcess = sessions.remove(session.getId());
if (serverProcess != null) {
serverProcess.destroy();

if (serverProcess.tempDir != null && Files.exists(serverProcess.tempDir)) {
Files.walk(serverProcess.tempDir)
.sorted(Comparator.reverseOrder())
.forEach(path -> {
try {
Files.delete(path);
} catch (IOException e) {
logger.warn("Cleanup failed: {}", path);
}
});
}
}
}

@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
logger.error("WebSocket transport error for session: {}", session.getId(), exception);
LspServerProcess serverProcess = sessions.remove(session.getId());
if (serverProcess != null) {
serverProcess.destroy();
}
}

private class LspServerProcess {
final WebSocketSession session;
final Process process;
final BufferedWriter writer;
final BufferedReader reader;
private final Path tempDir;
private final Path userProject;

LspServerProcess(
WebSocketSession session,
Process process,
BufferedWriter writer,
BufferedReader reader,
Path tempDir,
Path userProject) {
this.session = session;
this.process = process;
this.writer = writer;
this.reader = reader;
this.tempDir = tempDir;
this.userProject = userProject;
}

void readFromLsp() {
try {
String line;
while ((line = reader.readLine()) != null) {
if (line.startsWith("Content-Length:")) {
int contentLength = Integer.parseInt(line.split(":")[1].trim());

reader.readLine();

char[] content = new char[contentLength];
int read = reader.read(content, 0, contentLength);

String message = new String(content);

session.sendMessage(new TextMessage(message));
}
}
} catch (IOException e) {
System.err.println("💥 LSP reader error: " + e.getMessage());
e.printStackTrace();
}
}

void sendToLsp(String jsonMessage) throws IOException {
String lspMessage = "Content-Length: " + jsonMessage.getBytes(StandardCharsets.UTF_8).length + "\r\n\r\n"
+ jsonMessage;
writer.write(lspMessage);
writer.flush();
}

void destroy() {
try {
writer.close();
} catch (IOException e) {
logger.error("Error closing LSP writer", e);
}
process.destroy();
}
}

private Long extractUserId(WebSocketSession session) {
try {
String query = session.getUri().getQuery();
if (query != null && query.contains("userId=")) {
String userIdStr = extractQueryParam(query, "userId");
if (userIdStr != null) {
Long userId = Long.parseLong(userIdStr);
logger.debug("Extracted userId from query parameter: {}", userId);
return userId;
}
}

Object principal = session.getPrincipal();
if (principal != null) {
logger.debug("Principal type: {}", principal.getClass().getName());

if (principal instanceof org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken) {
org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken jwt = (org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken) principal;

String sub = jwt.getToken().getClaim("sub");
if (sub != null) {
try {
Long userId = Long.parseLong(sub);
logger.debug("Extracted userId from JWT 'sub' claim: {}", userId);
return userId;
} catch (NumberFormatException e) {
logger.warn("JWT 'sub' claim is not a number: {}", sub);
}
}

Object userIdClaim = jwt.getToken().getClaim("userId");
if (userIdClaim != null) {
Long userId = Long.parseLong(userIdClaim.toString());
logger.debug("Extracted userId from JWT 'userId' claim: {}", userId);
return userId;
}

String email = jwt.getToken().getClaim("preferred_username");
if (email == null) {
email = jwt.getToken().getClaim("email");
}
if (email != null) {
logger.debug("Found email in JWT: {}, need to lookup userId", email);
}
}

logger.debug("Principal toString: {}", principal);
}

Object userIdAttr = session.getAttributes().get("userId");
if (userIdAttr != null) {
Long userId = Long.parseLong(userIdAttr.toString());
logger.debug("Extracted userId from session attributes: {}", userId);
return userId;
}

logger.warn("Could not extract userId from WebSocket session. URI: {}", session.getUri());
return null;

} catch (Exception e) {
logger.error("Error extracting userId from WebSocket session", e);
return null;
}
}

private Long extractProjectId(WebSocketSession session) {
try {
String query = session.getUri().getQuery();
if (query != null && query.contains("projectId=")) {
String projectIdStr = extractQueryParam(query, "projectId");
if (projectIdStr != null) {
Long projectId = Long.parseLong(projectIdStr);
logger.debug("Extracted projectId from query parameter: {}", projectId);
return projectId;
}
}

Object projectIdAttr = session.getAttributes().get("projectId");
if (projectIdAttr != null) {
Long projectId = Long.parseLong(projectIdAttr.toString());
logger.debug("Extracted projectId from session attributes: {}", projectId);
return projectId;
}

logger.debug("No projectId found in WebSocket session (this is optional)");
return null;

} catch (Exception e) {
logger.error("Error extracting projectId from WebSocket session", e);
return null;
}
}

private String extractQueryParam(String query, String paramName) {
if (query == null || paramName == null) {
return null;
}

String[] params = query.split("&");
for (String param : params) {
String[] keyValue = param.split("=", 2);
if (keyValue.length == 2 && keyValue[0].equals(paramName)) {
return keyValue[1];
}
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,4 @@ public CorsConfigurationSource corsConfigurationSource() {
source.registerCorsConfiguration("/**", cfg);
return source;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package tools.vitruv.methodologist.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

private final LspWebSocketHandler lspWebSocketHandler;

public WebSocketConfig(LspWebSocketHandler lspWebSocketHandler) {
this.lspWebSocketHandler = lspWebSocketHandler;
}

@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry
.addHandler(lspWebSocketHandler, "/lsp")
.setAllowedOrigins("*");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.springframework.stereotype.Repository;
import tools.vitruv.methodologist.user.model.User;
import tools.vitruv.methodologist.vsum.model.MetaModel;
import tools.vitruv.methodologist.vsum.model.VsumMetaModel;

/**
* Repository interface for performing CRUD operations and complex queries on {@link MetaModel}
Expand Down Expand Up @@ -54,4 +55,9 @@ public interface MetaModelRepository extends CrudRepository<MetaModel, Long> {
*/
@SuppressWarnings("checkstyle:MethodName")
Optional<MetaModel> findByIdAndUser_Email(Long id, String callerEmail);


List<MetaModel> findByUserId(Long userId);

List<MetaModel> findByUserIdAndRemovedAtIsNull(Long userId);
}
Loading