Skip to content
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

[FUSETOOLS2-1673] Choice EIP implementation #861

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,14 @@
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;

import com.github.cameltooling.lsp.internal.completion.modeline.CamelKModelineInsertionProcessor;
import com.github.cameltooling.lsp.internal.parser.CamelEIPParser;
import com.github.cameltooling.lsp.internal.parser.CamelKModelineInsertionParser;
import org.apache.camel.catalog.CamelCatalog;
import org.apache.camel.catalog.DefaultCamelCatalog;
Expand Down Expand Up @@ -161,15 +164,34 @@ public CompletableFuture<Either<List<CompletionItem>, CompletionList>> completio
TextDocumentItem textDocumentItem = openedDocuments.get(uri);

if (textDocumentItem != null) {
if (uri.endsWith(".properties")){
if (uri.endsWith(".properties")) {
return new CamelPropertiesCompletionProcessor(textDocumentItem, getCamelCatalog(), getCamelKafkaConnectorManager()).getCompletions(completionParams.getPosition(), getSettingsManager(), getKameletsCatalogManager()).thenApply(Either::forLeft);
} else if (new CamelKModelineInsertionParser(textDocumentItem).canPutCamelKModeline(completionParams.getPosition())){
return new CamelKModelineInsertionProcessor(textDocumentItem).getCompletions().thenApply(Either::forLeft);
}

List<CompletableFuture<List<CompletionItem>>> completions = new LinkedList<>();

if (new CamelEIPParser(textDocumentItem).canPutEIP(completionParams.getPosition())){
completions.add(new CamelEIPParser(textDocumentItem).getCompletions());
}

if (new CamelKModelineInsertionParser(textDocumentItem).canPutCamelKModeline(completionParams.getPosition())){
completions.add(new CamelKModelineInsertionProcessor(textDocumentItem).getCompletions());
} else if (new CamelKModelineParser().isOnCamelKModeline(completionParams.getPosition().getLine(), textDocumentItem)){
return new CamelKModelineCompletionprocessor(textDocumentItem, getCamelCatalog()).getCompletions(completionParams.getPosition()).thenApply(Either::forLeft);
completions.add(new CamelKModelineCompletionprocessor(textDocumentItem, getCamelCatalog()).getCompletions(completionParams.getPosition()));
} else {
return new CamelEndpointCompletionProcessor(textDocumentItem, getCamelCatalog(), getKameletsCatalogManager()).getCompletions(completionParams.getPosition(), getSettingsManager()).thenApply(Either::forLeft);
completions.add(new CamelEndpointCompletionProcessor(textDocumentItem, getCamelCatalog(), getKameletsCatalogManager()).getCompletions(completionParams.getPosition(), getSettingsManager()));
}

//Combining all completable futures completion lists
//Source: https://nurkiewicz.com/2013/05/java-8-completablefuture-in-action.html
return CompletableFuture
.allOf(completions.toArray(new CompletableFuture[completions.size()]))
.thenApply(
v -> completions.stream()
.map(future -> future.join())
.flatMap(x -> x.stream())
.collect(Collectors.toList())
).thenApply(Either::forLeft);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is needed because we want to be able to add different completions that have different triggers

} else {
LOGGER.warn("The document with uri {} has not been found in opened documents. Cannot provide completion.", uri);
return CompletableFuture.completedFuture(Either.forLeft(Collections.emptyList()));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.github.cameltooling.lsp.internal.parser;

import org.eclipse.lsp4j.CompletionItem;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.TextDocumentItem;

import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.regex.Pattern;

public class CamelEIPParser {

private static final String LATEST_LTS_URL = "https://camel.apache.org/components/3.20.x";
private final TextDocumentItem document;

public CamelEIPParser(TextDocumentItem textDocumentItem) {
this.document = textDocumentItem;
}

public boolean canPutEIP(Position position) {
Pattern choiceEipPattern = Pattern.compile("\\)\\s*[.[c[h[o[i[c[e]?]?]?]?]?]?]?\\(?$", Pattern.MULTILINE);
ParserFileHelperUtil util = new ParserFileHelperUtil();
String textUntilPosition = util.getTextUntilPosition(document, position);

return choiceEipPattern.matcher(textUntilPosition).find();
}

public CompletableFuture<List<CompletionItem>> getCompletions() {
return CompletableFuture.completedFuture(
List.of(choiceEIPcompletion())
);
}

private CompletionItem choiceEIPcompletion() {
String newLine = "\n";
CompletionItem completion = new CompletionItem("Content Based Router");
completion.setDocumentation(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can be done in another iteration.
it is possibel to use Markdown for documentation which allows to provide clickable link

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving it for other iteration. I know what you refer, i'll pushing a first iteration with most of the comment changes and iterate it further later today

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think providing a bit more details on what is the Content based router with a single sentence would help users. This can be done in another iteration.

"Read more: "+LATEST_LTS_URL+"/eips/choice-eip.html"
);
completion.setInsertText(
".choice()" + newLine +
".when()" + newLine +
".to()" + newLine +
".when()" + newLine +
".to()" + newLine +
".otherwise()" + newLine +
".to()"
);

return completion;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.TextDocumentItem;

import java.util.regex.Pattern;

public class ParserFileHelperUtil {

public String getLine(TextDocumentItem textDocumentItem, Position position) {
Expand All @@ -36,5 +38,23 @@ public String getLine(String text, int line) {
}
return null;
}

public String getTextUntilPosition(TextDocumentItem document, Position position) {
String newLine = "\n";
String[] lines = document.getText().split(newLine);
StringBuilder textUntilPosition = new StringBuilder("");

for(int i = 0; i<position.getLine(); i++) {
textUntilPosition.append(lines[i] + newLine);
}

if(position.getLine() != lines.length) {
//Edge case: end of file with a character return
textUntilPosition.append(lines[position.getLine()].substring(0, position.getCharacter()));
}

return textUntilPosition.toString();

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package com.github.cameltooling.lsp.internal.completion.eip;

import com.github.cameltooling.lsp.internal.AbstractCamelLanguageServerTest;
import com.github.cameltooling.lsp.internal.CamelLanguageServer;
import com.github.cameltooling.lsp.internal.util.RouteTextBuilder;
import org.eclipse.lsp4j.CompletionItem;
import org.eclipse.lsp4j.CompletionList;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Test;

import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;

import static org.assertj.core.api.Assertions.assertThat;


class EIPChoiceCompletionTest extends AbstractCamelLanguageServerTest {

private static final String FROM_ROUTE = "from(\"timer:foo?period={{timer.period}}\")";
private static final String FROM_ROUTE_WITH_LINE_BREAKS_AND_TABS
= "from(\"timer:foo?period={{timer.period}}\")\n\t.bean()\n";

private static final String FROM_ROUTE_WITH_CHOICE_EIP_MID_WRITTEN
= "from(\"timer:foo?period={{timer.period}}\")\n\t.ch\n";
public static final String CONTENT_BASED_ROUTER_LABEL = "Content Based Router";


@Test
void testDontProvideInsertionOnEmptyJavaFile() throws Exception {
String contents = "";
Position position = new Position(0,0);

List<CompletionItem> completionItems = getCompletionsFor(contents, position);
completionItems = getContentBasedRouterCompletionItems(completionItems);

assertThat(completionItems).isEmpty();
}

@Test
void testProvideInsertionOnEmptyJavaClass() throws Exception {
RouteTextBuilder.DocumentContentWithPosition document =
RouteTextBuilder.createJavaDocumentClass("");

List<CompletionItem> completionItems = getCompletionsFor(document.getContent(), document.getPosition());
completionItems = getContentBasedRouterCompletionItems(completionItems);


assertThat(completionItems).isEmpty();
}

@Test
void testProvideInsertionOnEmptyJavaCamelRoute() throws Exception {
RouteTextBuilder.DocumentContentWithPosition document =
RouteTextBuilder.createJavaDocumentCamelRoute("");

List<CompletionItem> completionItems = getCompletionsFor(document.getContent(), document.getPosition());
completionItems = getContentBasedRouterCompletionItems(completionItems);

assertThat(completionItems).hasSize(0);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AssertJ is providing an isEmpty() method. It will provide a better error message, simplifying the debugging process.

}

@Test
void testProvideInsertionAfterFromOnCamelRoute() throws Exception {
RouteTextBuilder.DocumentContentWithPosition document =
RouteTextBuilder.createJavaDocumentCamelRoute(FROM_ROUTE);

List<CompletionItem> completionItems = getCompletionsFor(document.getContent(), document.getPosition());
completionItems = getContentBasedRouterCompletionItems(completionItems);


assertThat(completionItems).hasSize(1);
}

@Test
void testProvideInsertionAfterFromOnCamelRouteWithLineBreaks() throws Exception {
RouteTextBuilder.DocumentContentWithPosition document =
RouteTextBuilder.createJavaDocumentCamelRoute(FROM_ROUTE_WITH_LINE_BREAKS_AND_TABS);

List<CompletionItem> completionItems = getCompletionsFor(document.getContent(), document.getPosition());
completionItems = getContentBasedRouterCompletionItems(completionItems);


assertThat(completionItems).hasSize(1);
}

@Test
void testProvideInsertionMidWritingChoice() throws Exception {
RouteTextBuilder.DocumentContentWithPosition document =
RouteTextBuilder.createJavaDocumentCamelRoute(FROM_ROUTE_WITH_CHOICE_EIP_MID_WRITTEN);

List<CompletionItem> completionItems = getCompletionsFor(document.getContent(), document.getPosition());
completionItems = getContentBasedRouterCompletionItems(completionItems);


assertThat(completionItems).hasSize(1);
}


private List<CompletionItem> getCompletionsFor(String contents, Position position) throws Exception {
CamelLanguageServer camelLanguageServer = initializeLanguageServer(contents, ".java");

CompletableFuture<Either<List<CompletionItem>, CompletionList>> completions = getCompletionFor(
camelLanguageServer, position);

return completions.get().getLeft();
}

@NotNull
private List<CompletionItem> getContentBasedRouterCompletionItems(List<CompletionItem> completionItems) {

return completionItems.stream().filter(
completionItem -> completionItem.getLabel().startsWith(CONTENT_BASED_ROUTER_LABEL)
).collect(Collectors.toList());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,23 @@
*/
package com.github.cameltooling.lsp.internal.util;

import org.eclipse.lsp4j.Position;

public class RouteTextBuilder {

public static final String XML_PREFIX_FROM = "<from uri=\"";
private static final String XML_SUFFIX_FROM_SPRING = "\" xmlns=\"http://camel.apache.org/schema/spring\"/>\n";
private static final String XML_SUFFIX_FROM_BLUEPRINT = "\" xmlns=\"http://camel.apache.org/schema/blueprint\"/>\n";

private static final String CAMEL_ROUTEBUILDER_IMPORT
= "import org.apache.camel.builder.RouteBuilder;";

private static final String CLASS_DECLARATION = "public class TestRoute";

private static final String CAMEL_ROUTEBUILDER_EXTEMD = "extends RouteBuilder";

private static final String CONFIGURE_METHOD_DECLARATION = "public void configure()";


/**
* @param camelUri
Expand All @@ -38,4 +50,81 @@ public static String createXMLBlueprintRoute(String camelUri) {
return XML_PREFIX_FROM + camelUri + XML_SUFFIX_FROM_BLUEPRINT;
}

/**
* @param javaClassContent
* @return builds an empty Java class with the specified content and the cursor position inside and after
* contents
*/
public static DocumentContentWithPosition createJavaDocumentClass(String javaClassContent) {
String newLine = "\n";
String[] contentSplit = javaClassContent.split(newLine);
int lineOffset = contentSplit.length;
int characterOffset = contentSplit[contentSplit.length-1].length();

if (javaClassContent.startsWith(newLine)) {
lineOffset += 1;
}

if (javaClassContent.endsWith(newLine)) {
lineOffset +=1;
characterOffset = 0;
}

return new DocumentContentWithPosition(
CLASS_DECLARATION + newLine +
"{" + newLine
+ javaClassContent + newLine
+ "}" + newLine
, 2+lineOffset, characterOffset);
}

/**
* @param camelRoute
* @return builds an empty Java class with the specified content and the cursor position placed inside configure
* method and after content.
*/
public static DocumentContentWithPosition createJavaDocumentCamelRoute(String camelRoute) {
String newLine = "\n";
String[] contentSplit = camelRoute.split(newLine);
int lineOffset = contentSplit.length - 1;
int characterOffset = contentSplit[contentSplit.length-1].length();

if (camelRoute.startsWith(newLine)) {
lineOffset += 1;
}

if (camelRoute.endsWith(newLine)) {
lineOffset +=1;
characterOffset = 0;
}

return new DocumentContentWithPosition(
CAMEL_ROUTEBUILDER_IMPORT + newLine +
CLASS_DECLARATION + newLine +
CAMEL_ROUTEBUILDER_EXTEMD + newLine +
"{" + newLine +
CONFIGURE_METHOD_DECLARATION + "{" + newLine +
camelRoute + newLine +
"}" + newLine +
"}" + newLine
, 5 + lineOffset,characterOffset);
}

public static class DocumentContentWithPosition {
public String content;
public Position position;

public DocumentContentWithPosition(String content, int line, int character) {
this.content = content;
this.position = new Position(line, character);
}

public String getContent() {
return content;
}

public Position getPosition() {
return position;
}
}
}