-
Notifications
You must be signed in to change notification settings - Fork 2
Added Language Server and its functionality to backend #180
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
uiysg
wants to merge
37
commits into
develop
Choose a base branch
from
language-server-integration
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
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 c482896
basline metamodel resolving works next Step: refactoring
uiysg 4e9c43c
refactored and added LSP jat to git
uiysg b9e9507
deleted comment lines from 102 until 113
uiysg 69a9cde
worked on checkstyle errors
uiysg 2c41037
returning to last commit to avoid ne errors
uiysg 421c3e2
Fix JaCoCo coverage by excluding LSP directory
uiysg 731b757
did some checkstyle refacotrings for github-action bot
uiysg 50631ca
corrected checkstyle
uiysg 9ad487c
corrected more checkstyle errors
uiysg c594eee
corrected some more javadoc warnings
uiysg 523cf5c
corrected some more javadoc warnings
uiysg 91a395f
added endpoint to javadoc to resolve checkstyle errors
uiysg be942a4
Merge branch 'develop' into language-server-integration
ma-mirzaei 493ca5c
Now only the metamodels that are also in VSUM are loaded
uiysg dced2ba
rmeoved the comments from the Builder in MetaModelService
uiysg e498c51
removed checkstyle errors
uiysg 7d2a141
removed unnecessary model.ecore file
uiysg fd90c0a
added tests to the lsp features
uiysg 4c825c0
solved checkstyle errors in test classes
uiysg 9e1029d
added the google checktslye plugin to the pom after accidentally dele…
uiysg 6ade5c0
fixed spotless checkstyle errors
uiysg 45604f1
changed LSPWebSocketHandler to conform to SOnarCloud
uiysg f62bc22
refactored some code in WebSocketHandler and in MetaModelServiceTest
uiysg 614f935
refactored code to tackle Security Hotspots
uiysg 75826d9
refactored code to solve some sonarIssues
uiysg 837c32c
refactored more Code to solve SonarIssues
uiysg a2f8f57
removed the @Autowired
uiysg dec18e9
added javadoc
uiysg 94c5877
refactored too long lines
uiysg cb8643f
solving SonartIssue reltaed to too long string
uiysg c7aa27c
try to fix sonar security Issue
uiysg 775c8f5
try to fix sonar security Issue
uiysg 3f7a249
removed unecessary logs
uiysg 757d75f
removed unecessary logs
uiysg 23ae43c
used configuraiton path for jar file
uiysg b49f392
fixed LspWebHandlerTest
uiysg File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
308 changes: 308 additions & 0 deletions
308
app/src/main/java/tools/vitruv/methodologist/config/LspWebSocketHandler.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
24 changes: 24 additions & 0 deletions
24
app/src/main/java/tools/vitruv/methodologist/config/WebSocketConfig.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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("*"); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.