Skip to content

Commit

Permalink
feat: CLI for copybook listing
Browse files Browse the repository at this point in the history
  • Loading branch information
ishche committed Jan 10, 2024
1 parent a6160ce commit ffa6958
Show file tree
Hide file tree
Showing 8 changed files with 574 additions and 31 deletions.
11 changes: 11 additions & 0 deletions server/engine/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
<logback.classic.version>1.3.14</logback.classic.version>
<lombok.version>1.18.22</lombok.version>
<lsp4j.version>0.14.0</lsp4j.version>
<picocli.version>4.7.5</picocli.version>
<maven.antlr4.plugin.version>4.7</maven.antlr4.plugin.version>
<maven.assembly.plugin.version>3.2.0</maven.assembly.plugin.version>
<maven.cobertura.plugin.version>2.7</maven.cobertura.plugin.version>
Expand Down Expand Up @@ -191,6 +192,16 @@
<artifactId>logback-classic</artifactId>
<version>${logback.classic.version}</version>
</dependency>
<dependency>
<groupId>info.picocli</groupId>
<artifactId>picocli</artifactId>
<version>${picocli.version}</version>
</dependency>
<dependency>
<groupId>info.picocli</groupId>
<artifactId>picocli-codegen</artifactId>
<version>${picocli.version}</version>
</dependency>
</dependencies>


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@
import com.google.inject.Guice;
import com.google.inject.Injector;
import lombok.NonNull;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.lsp.cobol.cli.Cli;
import org.eclipse.lsp.cobol.domain.modules.DatabusModule;
import org.eclipse.lsp.cobol.domain.modules.EngineModule;
import org.eclipse.lsp.cobol.domain.modules.ServiceModule;
Expand All @@ -32,6 +31,7 @@
import org.eclipse.lsp4j.services.LanguageServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import picocli.CommandLine;

import java.io.IOException;
import java.io.InputStream;
Expand All @@ -50,34 +50,48 @@
* <p>To run the extension using path, you may specify "pipeEnabled" as a program argument. In other
* case the server will start using socket.
*/
@Slf4j
@UtilityClass
public class LangServerBootstrap {
public static boolean cliMode = false;
private static final Integer LSP_PORT = 1044;
private static final String PIPE_ARG = "pipeEnabled";
// It's need to be static, so it will be initialized after cliMode mode calculated (it is used in logger setup)
private final Logger logger = LoggerFactory.getLogger(LangServerBootstrap.class);

/**
* The entry point for the application.
*
* @param args command line arguments
*/
public static void main(String[] args)
throws ExecutionException, InterruptedException, IOException {
Injector injector = initCtx();
throws ExecutionException, InterruptedException, IOException {
if (isCliMode(args)) {
cliMode = true;
int exitCode = new CommandLine(new Cli()).execute(args);
System.exit(exitCode);
}
LangServerBootstrap langServerBootstrap = new LangServerBootstrap();
Injector injector = LangServerBootstrap.initCtx();
LanguageServer server = injector.getInstance(LanguageServer.class);
ClientProvider provider = injector.getInstance(ClientProvider.class);

start(args, server, provider);
langServerBootstrap.start(args, server, provider);
}

Injector initCtx() {
private static boolean isCliMode(String[] args) {
if (args.length == 0) {
return false;
}
return !isPipeEnabled(args);
}

static Injector initCtx() {
return Guice.createInjector(new ServiceModule(), new EngineModule(), new DatabusModule());
}

private void start(
@NonNull String[] args, @NonNull LanguageServer server, @NonNull ClientProvider provider)
throws IOException, InterruptedException, ExecutionException {
LOG.info("Java version: " + System.getProperty("java.version"));
@NonNull String[] args, @NonNull LanguageServer server, @NonNull ClientProvider provider)
throws IOException, InterruptedException, ExecutionException {
logger.info(String.format("Java version: %s", System.getProperty("java.version")));
try {
if (isPipeEnabled(args)) {
launchServerWithPipes(server, provider);
Expand All @@ -92,22 +106,22 @@ private void start(
launchServerWithSocket(server, provider);
}
} catch (ExecutionException e) {
LOG.error("An error occurred while starting a language server", e);
logger.error("An error occurred while starting a language server", e);
throw e;
} catch (IOException e) {
LOG.error("Unable to start server using socket communication on port [{}]", LSP_PORT);
logger.error("Unable to start server using socket communication on port [{}]", LSP_PORT);
throw e;
}
}

private void launchServerWithSocket(LanguageServer server, ClientProvider provider)
throws IOException, InterruptedException, ExecutionException {
LOG.info("Language server awaiting socket communication on port [{}]", LSP_PORT);
throws IOException, InterruptedException, ExecutionException {
logger.info("Language server awaiting socket communication on port [{}]", LSP_PORT);
try (ServerSocket serverSocket = new ServerSocket(LSP_PORT);
Socket socket = serverSocket.accept();
InputStream input = socket.getInputStream();
OutputStream output = socket.getOutputStream()) {
LOG.info("Connection established successfully");
Socket socket = serverSocket.accept();
InputStream input = socket.getInputStream();
OutputStream output = socket.getOutputStream()) {
logger.info("Connection established successfully");
Launcher<CobolLanguageClient> launcher = createServerLauncher(server, input, output);
provider.setClient(launcher.getRemoteProxy());
// suspend the main thread on listening
Expand All @@ -117,33 +131,34 @@ private void launchServerWithSocket(LanguageServer server, ClientProvider provid

@SuppressWarnings("squid:S106")
private void launchServerWithPipes(
@NonNull LanguageServer server, @NonNull ClientProvider provider)
throws InterruptedException, ExecutionException {
LOG.info("Language server started using pipe communication");
@NonNull LanguageServer server, @NonNull ClientProvider provider)
throws InterruptedException, ExecutionException {
logger.info("Language server started using pipe communication");
Launcher<CobolLanguageClient> launcher = createServerLauncher(server, System.in, System.out);
provider.setClient(launcher.getRemoteProxy());
// suspend the main thread on listening
launcher.startListening().get();
}

boolean isPipeEnabled(@NonNull String[] args) {
static boolean isPipeEnabled(@NonNull String[] args) {
return args.length > 0 && PIPE_ARG.equals(args[0]);
}

Launcher<CobolLanguageClient> createServerLauncher(
@NonNull LanguageServer server, @NonNull InputStream in, @NonNull OutputStream out) {
static Launcher<CobolLanguageClient> createServerLauncher(
@NonNull LanguageServer server, @NonNull InputStream in, @NonNull OutputStream out) {
ThreadFactory tf = new ThreadFactory() {
private int counter = 0;

public Thread newThread(Runnable r) {
return new Thread(r, "LSP" + "-" + counter++);
}
};
return new LSPLauncher.Builder<CobolLanguageClient>()
.setLocalService(server)
.setExecutorService(Executors.newCachedThreadPool(tf))
.setRemoteInterface(CobolLanguageClient.class)
.setInput(in)
.setOutput(out)
.create();
.setLocalService(server)
.setExecutorService(Executors.newCachedThreadPool(tf))
.setRemoteInterface(CobolLanguageClient.class)
.setInput(in)
.setOutput(out)
.create();
}
}
138 changes: 138 additions & 0 deletions server/engine/src/main/java/org/eclipse/lsp/cobol/cli/Cli.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
* Copyright (c) 2023 Broadcom.
* The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Broadcom, Inc. - initial API and implementation
*
*/
package org.eclipse.lsp.cobol.cli;


import com.google.common.collect.Multimap;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.inject.Guice;
import com.google.inject.Injector;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.lsp.cobol.cli.di.CliModule;
import org.eclipse.lsp.cobol.cli.modules.CliClientProvider;
import org.eclipse.lsp.cobol.common.AnalysisConfig;
import org.eclipse.lsp.cobol.common.ResultWithErrors;
import org.eclipse.lsp.cobol.common.copybook.CopybookProcessingMode;
import org.eclipse.lsp.cobol.common.mapping.ExtendedDocument;
import org.eclipse.lsp.cobol.common.mapping.ExtendedText;
import org.eclipse.lsp.cobol.common.message.MessageService;
import org.eclipse.lsp.cobol.core.engine.analysis.AnalysisContext;
import org.eclipse.lsp.cobol.core.engine.dialects.DialectService;
import org.eclipse.lsp.cobol.core.engine.pipeline.Pipeline;
import org.eclipse.lsp.cobol.core.engine.pipeline.PipelineResult;
import org.eclipse.lsp.cobol.core.engine.pipeline.stages.CompilerDirectivesStage;
import org.eclipse.lsp.cobol.core.engine.pipeline.stages.DialectProcessingStage;
import org.eclipse.lsp.cobol.core.engine.pipeline.stages.PreprocessorStage;
import org.eclipse.lsp.cobol.core.preprocessor.TextPreprocessor;
import org.eclipse.lsp.cobol.core.preprocessor.delegates.GrammarPreprocessor;
import org.eclipse.lsp.cobol.core.semantics.CopybooksRepository;
import org.eclipse.lsp4j.Location;
import picocli.CommandLine;

import java.io.File;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.Callable;

/**
* The Cli class represents a Command Line Interface (CLI) for interacting with the application.
*/
@CommandLine.Command(description = "COBOL Analysis CLI tools.")
@Slf4j
public class Cli implements Callable<Integer> {
private enum Action {
list_copybooks
}

@CommandLine.Parameters(description = "Values: ${COMPLETION-CANDIDATES}")
Action action = null;
@CommandLine.Option(names = {"-s", "--source"}, required = true, description = "The COBOL program file.")
private File src;
@CommandLine.Option(names = {"-cf", "--copybook-folder"}, description = "Path to the copybook folder.")
private File[] cpyPaths;

@CommandLine.Option(names = {"-ce", "--copybook-extension"}, description = "List of copybook paths.")
private String[] cpyExt = {"", ".cpy"};

/**
* Prints the file name to the console and returns result code.
*
* @return 0 indicating success.
* @throws Exception if an error occurs during the method execution.
*/
@Override

public Integer call() throws Exception {
Injector diCtx = Guice.createInjector(new CliModule());
Pipeline pipeline = setupPipeline(diCtx);

CliClientProvider cliClientProvider = diCtx.getInstance(CliClientProvider.class);
if (cpyPaths != null) {
cliClientProvider.setCpyPaths(Arrays.asList(cpyPaths));
}
cliClientProvider.setCpyExt(Arrays.asList(cpyExt));


// Cleaning up
TextPreprocessor preprocessor = diCtx.getInstance(TextPreprocessor.class);
if (src == null) {
LOG.error("src must be provided");
return 1;
}
String documentUri = src.toURI().toString();
String text = new String(Files.readAllBytes(src.toPath()));
ResultWithErrors<ExtendedText> resultWithErrors = preprocessor.cleanUpCode(documentUri, text);
AnalysisContext ctx = new AnalysisContext(new ExtendedDocument(resultWithErrors.getResult(), text), createAnalysisConfiguration());
ctx.getAccumulatedErrors().addAll(resultWithErrors.getErrors());

PipelineResult<CopybooksRepository> pipelineResult = (PipelineResult<CopybooksRepository>) pipeline.run(ctx);
Multimap<String, String> definitions = pipelineResult.getData().getDefinitions();
Multimap<String, Location> usages = pipelineResult.getData().getUsages();
Set<String> missing = new HashSet<>(usages.keySet());
missing.removeAll(definitions.keySet());

Gson gson = new GsonBuilder().setPrettyPrinting().create();
JsonObject result = new JsonObject();
JsonArray copybookUris = new JsonArray();
JsonArray missingCopybooks = new JsonArray();
missing.forEach(missingCopybooks::add);
definitions.values().forEach(copybookUris::add);
result.add("copybookUris", copybookUris);
result.add("missingCopybooks", missingCopybooks);
System.out.println(gson.toJson(result));
return 0;
}

private static AnalysisConfig createAnalysisConfiguration() {
return AnalysisConfig.defaultConfig(CopybookProcessingMode.ENABLED);
}

private static Pipeline setupPipeline(Injector diCtx) {
DialectService dialectService = diCtx.getInstance(DialectService.class);
MessageService messageService = diCtx.getInstance(MessageService.class);
GrammarPreprocessor grammarPreprocessor = diCtx.getInstance(GrammarPreprocessor.class);

Pipeline pipeline = new Pipeline();
pipeline.add(new CompilerDirectivesStage(messageService));
pipeline.add(new DialectProcessingStage(dialectService));
pipeline.add(new PreprocessorStage(grammarPreprocessor));
return pipeline;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright (c) 2023 Broadcom.
* The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Broadcom, Inc. - initial API and implementation
*
*/
package org.eclipse.lsp.cobol.cli;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.layout.TTLLLayout;
import ch.qos.logback.classic.spi.Configurator;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.ConsoleAppender;
import ch.qos.logback.core.encoder.LayoutWrappingEncoder;
import ch.qos.logback.core.spi.ContextAwareBase;
import org.eclipse.lsp.cobol.LangServerBootstrap;

import static org.slf4j.Logger.ROOT_LOGGER_NAME;

/**
* The LogConfig class is a {@link Configurator} implementation that configures the logger for different modes (CLI/LSP).
*/
public class LogConfig extends ContextAwareBase implements Configurator {
@Override
public ExecutionStatus configure(LoggerContext loggerContext) {
// Reconfigure logger for CLI mode
if (LangServerBootstrap.cliMode) {
addInfo("Setting up CLI configuration.");
Logger rootLogger = loggerContext.getLogger(ROOT_LOGGER_NAME);
rootLogger.addAppender(createAppender(loggerContext));
rootLogger.setLevel(Level.INFO);
return ExecutionStatus.DO_NOT_INVOKE_NEXT_IF_ANY;
}
return ExecutionStatus.NEUTRAL;
}

private static ConsoleAppender<ILoggingEvent> createAppender(LoggerContext loggerContext) {
ConsoleAppender<ILoggingEvent> console = new ConsoleAppender<>();
console.setContext(loggerContext);
console.setName("console");
console.setTarget("System.err");
console.setEncoder(createEncoder(loggerContext, createLayout(loggerContext)));
console.start();
return console;
}

private static LayoutWrappingEncoder<ILoggingEvent> createEncoder(LoggerContext loggerContext, TTLLLayout layout) {
LayoutWrappingEncoder<ILoggingEvent> encoder = new LayoutWrappingEncoder<>();
encoder.setContext(loggerContext);
encoder.setLayout(layout);
encoder.start();
return encoder;
}

private static TTLLLayout createLayout(LoggerContext loggerContext) {
TTLLLayout layout = new TTLLLayout();
layout.setContext(loggerContext);
layout.start();
return layout;
}
}
Loading

0 comments on commit ffa6958

Please sign in to comment.