diff --git a/gradle/versions.gradle b/gradle/versions.gradle
index 76830d37..5cb72add 100644
--- a/gradle/versions.gradle
+++ b/gradle/versions.gradle
@@ -18,5 +18,6 @@ ext.versions = [
'gson': '[2.9.1,2.11)',
'websocket_jakarta': '2.0.0',
'websocket': '1.0',
- 'junit': '4.13.2'
+ 'junit': '4.13.2',
+ 'archunit': '1.0.1'
]
diff --git a/org.eclipse.lsp4j.debug/build.gradle b/org.eclipse.lsp4j.debug/build.gradle
index c93adebb..561561a6 100644
--- a/org.eclipse.lsp4j.debug/build.gradle
+++ b/org.eclipse.lsp4j.debug/build.gradle
@@ -14,9 +14,12 @@ ext.title = 'LSP4J Debug'
description = 'Java bindings for the Debug Server Protocol'
dependencies {
- api project(":org.eclipse.lsp4j.generator")
+ compileOnly project(":org.eclipse.lsp4j.generator")
api project(":org.eclipse.lsp4j.jsonrpc.debug")
testImplementation "junit:junit:$versions.junit"
+ testImplementation "org.eclipse.xtend:org.eclipse.xtend.lib:$versions.xtend_lib"
+ testImplementation project(":org.eclipse.lsp4j.generator")
+ testImplementation "com.tngtech.archunit:archunit:$versions.archunit"
}
jar {
@@ -24,5 +27,5 @@ jar {
}
jar.bnd (
- 'Import-Package': "com.google.common.*;version=\"$versions.guava\",com.google.gson.*;version=\"$versions.gson\",*",
+ 'Import-Package': "com.google.gson.*;version=\"$versions.gson\",*",
)
diff --git a/org.eclipse.lsp4j.debug/src/main/java/org/eclipse/lsp4j/debug/util/ToStringBuilder.java b/org.eclipse.lsp4j.debug/src/main/java/org/eclipse/lsp4j/debug/util/ToStringBuilder.java
new file mode 100644
index 00000000..513b4325
--- /dev/null
+++ b/org.eclipse.lsp4j.debug/src/main/java/org/eclipse/lsp4j/debug/util/ToStringBuilder.java
@@ -0,0 +1,372 @@
+package org.eclipse.lsp4j.debug.util;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.IdentityHashMap;
+import java.util.Iterator;
+import java.util.List;
+
+
+/**
+ * Helps with the construction of good {@link Object#toString()} representations.
+ *
You can customize the output using the builder-style methods {@link ToStringBuilder#singleLine()} {@link ToStringBuilder#skipNulls()} and {@link ToStringBuilder#hideFieldNames()}.
+ * You can either directly list fields to include via {@link ToStringBuilder#add(String, Object)} and {@link ToStringBuilder#add(Object)}
+ * or you can let the builder do it automatically using reflection, either including the fields declared in this class or including all superclasses.
+ * The builder will automatically handle cycles in the object tree. It also pretty prints arrays and Iterables.
+ *
+ * This class is not thread safe.
+ * @since 2.7
+ */
+public final class ToStringBuilder {
+
+ public static class ToStringContext {
+
+ public final static ToStringContext INSTANCE = new ToStringContext();
+
+ private final static ThreadLocal> currentlyProcessed = new ThreadLocal>() {
+ @Override
+ public IdentityHashMap initialValue() {
+ return new IdentityHashMap();
+ }
+ };
+
+ public boolean startProcessing(final Object obj) {
+ return ToStringContext.currentlyProcessed.get().put(obj, Boolean.TRUE) == null;
+ }
+
+ public void endProcessing(final Object obj) {
+ ToStringContext.currentlyProcessed.get().remove(obj);
+ }
+ }
+
+ private static ToStringContext toStringContext = ToStringContext.INSTANCE;
+
+ private final Object instance;
+
+ private final String typeName;
+
+ private boolean multiLine = true;
+
+ private boolean skipNulls = false;
+
+ private boolean showFieldNames = true;
+
+ private boolean prettyPrint = true;
+
+ private final List parts = new ArrayList();
+
+ /**
+ * Creates a new ToStringBuilder for the given object. If you don't use reflection, then this instance
+ * is only used for obtaining its classes' simple name.
+ *
+ * @param instance the object to convert to a String
+ */
+ public ToStringBuilder(final Object instance) {
+ this.instance = instance;
+ this.typeName = instance.getClass().getSimpleName();
+ }
+
+ /**
+ * Fields are printed on a single line, separated by commas instead of newlines
+ * @return this
+ */
+ public ToStringBuilder singleLine() {
+ this.multiLine = false;
+ return this;
+ }
+
+ /**
+ * Fields with null values will be excluded from the output
+ * @return this
+ */
+ public ToStringBuilder skipNulls() {
+ this.skipNulls = true;
+ return this;
+ }
+
+ /**
+ * Field names will not be included in the output. Useful for small classes.
+ * @return this
+ */
+ public ToStringBuilder hideFieldNames() {
+ this.showFieldNames = false;
+ return this;
+ }
+
+ /**
+ * By default, Iterables, Arrays and multiline Strings are pretty-printed.
+ * Switching to their normal representation makes the toString method significantly faster.
+ * @since 2.9
+ * @return this
+ */
+ public ToStringBuilder verbatimValues() {
+ this.prettyPrint = false;
+ return this;
+ }
+
+ /**
+ * Adds all fields declared directly in the object's class to the output
+ * @return this
+ */
+ public ToStringBuilder addDeclaredFields() {
+ Field[] fields = instance.getClass().getDeclaredFields();
+ for(Field field : fields) {
+ addField(field);
+ }
+ return this;
+ }
+
+ /**
+ * Adds all fields declared in the object's class and its superclasses to the output.
+ * @return this
+ */
+ public ToStringBuilder addAllFields() {
+ List fields = getAllDeclaredFields(instance.getClass());
+ for(Field field : fields) {
+ addField(field);
+ }
+ return this;
+ }
+
+ /**
+ * @param fieldName the name of the field to add to the output using reflection
+ * @return this
+ */
+ public ToStringBuilder addField(final String fieldName) {
+ List fields = getAllDeclaredFields(instance.getClass());
+ for(Field field : fields) {
+ if(fieldName.equals(field.getName())) {
+ addField(field);
+ break;
+ }
+ }
+ return this;
+ }
+
+ private ToStringBuilder addField(final Field field) {
+ if (!Modifier.isStatic(field.getModifiers())) {
+ field.setAccessible(true);
+ try {
+ add(field.getName(), field.get(instance));
+ } catch(IllegalAccessException e) {
+ sneakyThrow(e);
+ }
+ }
+ return this;
+ }
+
+ @SuppressWarnings("unchecked")
+ private static void sneakyThrow(Throwable t) throws T {
+ throw (T) t;
+ }
+
+ /**
+ * @param value the value to add to the output
+ * @param fieldName the field name to list the value under
+ * @return this
+ */
+ public ToStringBuilder add(final String fieldName, final Object value) {
+ return addPart(fieldName, value);
+ }
+
+ /**
+ * @param value the value to add to the output without a field name
+ * @return this
+ */
+ public ToStringBuilder add(final Object value) {
+ return addPart(value);
+ }
+
+ private Part addPart() {
+ final Part p = new Part();
+ this.parts.add(p);
+ return p;
+ }
+
+ private ToStringBuilder addPart(final Object value) {
+ final Part p = this.addPart();
+ p.value = value;
+ return this;
+ }
+
+ private ToStringBuilder addPart(final String fieldName, final Object value) {
+ final Part p = this.addPart();
+ p.fieldName = fieldName;
+ p.value = value;
+ return this;
+ }
+
+ /**
+ * @return the String representation of the processed object
+ */
+ @Override
+ public String toString() {
+ boolean startProcessing = ToStringBuilder.toStringContext.startProcessing(this.instance);
+ if (!startProcessing) {
+ return this.toSimpleReferenceString(this.instance);
+ }
+ try {
+ final IndentationAwareStringBuilder builder = new IndentationAwareStringBuilder();
+ builder.append(typeName).append(" ");
+ builder.append("[");
+ String nextSeparator = "";
+ if (multiLine) {
+ builder.increaseIndent();
+ }
+ for (Part part : parts) {
+ if (!skipNulls || part.value != null) {
+ if (multiLine) {
+ builder.newLine();
+ } else {
+ builder.append(nextSeparator);
+ nextSeparator = ", ";
+ }
+ if (part.fieldName != null && this.showFieldNames) {
+ builder.append(part.fieldName).append(" = ");
+ }
+ this.internalToString(part.value, builder);
+ }
+ }
+ if (multiLine) {
+ builder.decreaseIndent().newLine();
+ }
+ builder.append("]");
+ return builder.toString();
+ } finally {
+ ToStringBuilder.toStringContext.endProcessing(this.instance);
+ }
+ }
+
+ private void internalToString(final Object object, final IndentationAwareStringBuilder sb) {
+ if (prettyPrint) {
+ if (object instanceof Iterable>) {
+ serializeIterable((Iterable>)object, sb);
+ } else if (object instanceof Object[]) {
+ sb.append(Arrays.toString((Object[])object));
+ } else if (object instanceof byte[]) {
+ sb.append(Arrays.toString((byte[])object));
+ } else if (object instanceof char[]) {
+ sb.append(Arrays.toString((char[])object));
+ } else if (object instanceof int[]) {
+ sb.append(Arrays.toString((int[])object));
+ } else if (object instanceof boolean[]) {
+ sb.append(Arrays.toString((boolean[])object));
+ } else if (object instanceof long[]) {
+ sb.append(Arrays.toString((long[])object));
+ } else if (object instanceof float[]) {
+ sb.append(Arrays.toString((float[])object));
+ } else if (object instanceof double[]) {
+ sb.append(Arrays.toString((double[])object));
+ } else if (object instanceof CharSequence) {
+ sb.append("\"").append(((CharSequence)object).toString().replace("\n", "\\n").replace("\r", "\\r")).append("\"");
+ } else if (object instanceof Enum>) {
+ sb.append(((Enum>)object).name());
+ } else {
+ sb.append(String.valueOf(object));
+ }
+ } else {
+ sb.append(String.valueOf(object));
+ }
+ }
+
+ private void serializeIterable(final Iterable> object, final IndentationAwareStringBuilder sb) {
+ final Iterator> iterator = object.iterator();
+ sb.append(object.getClass().getSimpleName()).append(" (");
+ if (multiLine) {
+ sb.increaseIndent();
+ }
+ boolean wasEmpty = true;
+ while (iterator.hasNext()) {
+ wasEmpty = false;
+ if (multiLine) {
+ sb.newLine();
+ }
+ this.internalToString(iterator.next(), sb);
+ if (iterator.hasNext()) {
+ sb.append(",");
+ }
+ }
+ if (multiLine) {
+ sb.decreaseIndent();
+ }
+ if (!wasEmpty && this.multiLine) {
+ sb.newLine();
+ }
+ sb.append(")");
+ }
+
+ private String toSimpleReferenceString(final Object obj) {
+ String simpleName = obj.getClass().getSimpleName();
+ int identityHashCode = System.identityHashCode(obj);
+ return simpleName + "@" + Integer.valueOf(identityHashCode);
+ }
+
+ private List getAllDeclaredFields(final Class> clazz) {
+ final ArrayList result = new ArrayList<>();
+
+ for(Class> current = clazz; current != null; current = current.getSuperclass()) {
+ Field[] declaredFields = current.getDeclaredFields();
+ result.addAll(Arrays.asList(declaredFields));
+
+ }
+ return result;
+ }
+
+ private static final class Part {
+ private String fieldName;
+ private Object value;
+ }
+
+ private static class IndentationAwareStringBuilder {
+ private final StringBuilder builder = new StringBuilder();
+
+ private final String indentationString = " ";
+
+ private final String newLineString = "\n";
+
+ private int indentation = 0;
+
+ public IndentationAwareStringBuilder increaseIndent() {
+ indentation++;
+ return this;
+ }
+
+ public IndentationAwareStringBuilder decreaseIndent() {
+ indentation--;
+ return this;
+ }
+
+ public IndentationAwareStringBuilder append(final CharSequence string) {
+ if (indentation > 0) {
+ String indented = string.toString().replace(
+ newLineString,
+ newLineString + repeat(indentationString, indentation)
+ );
+ builder.append(indented);
+ } else {
+ builder.append(string);
+ }
+ return this;
+ }
+
+ public IndentationAwareStringBuilder newLine() {
+ builder.append(newLineString).
+ append(repeat(this.indentationString, this.indentation));
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return this.builder.toString();
+ }
+
+ private String repeat(String string, int count) {
+ StringBuilder result = new StringBuilder();
+ for(int i=0; i < count; i++) {
+ result.append(string);
+ }
+ return result.toString();
+ }
+ }
+}
\ No newline at end of file
diff --git a/org.eclipse.lsp4j.debug/src/test/java/org/eclipse/lsp4j/debug/test/Lsp4jDebugArchitectureTest.java b/org.eclipse.lsp4j.debug/src/test/java/org/eclipse/lsp4j/debug/test/Lsp4jDebugArchitectureTest.java
new file mode 100644
index 00000000..f5e17d30
--- /dev/null
+++ b/org.eclipse.lsp4j.debug/src/test/java/org/eclipse/lsp4j/debug/test/Lsp4jDebugArchitectureTest.java
@@ -0,0 +1,69 @@
+package org.eclipse.lsp4j.debug.test;
+
+import static com.tngtech.archunit.core.domain.JavaClass.Predicates.resideInAPackage;
+import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
+import static com.tngtech.archunit.base.DescribedPredicate.not;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import com.tngtech.archunit.core.domain.JavaClasses;
+import com.tngtech.archunit.core.importer.ClassFileImporter;
+import com.tngtech.archunit.lang.ArchRule;
+
+/**
+ * Test to make sure we don't depend to xbase.lib, xtend.lib and guava at
+ * runtime
+ *
+ * @author Christian Dietrich
+ */
+public class Lsp4jDebugArchitectureTest {
+
+ private static JavaClasses importedClasses;
+
+ @BeforeClass
+ public static void scan() {
+ importedClasses = new ClassFileImporter().importPackages("org.eclipse.lsp4j.debug");
+ }
+
+ @AfterClass
+ public static void cleanup() {
+ importedClasses = null;
+ }
+
+ @Test
+ public void testNoDependenyToXbaseLib() {
+ assertNoDependencyToPackage("org.eclipse.xtext..");
+ }
+
+ @Test
+ public void testNoDependenyToXtendLib() {
+ assertNoDependencyToPackage("org.eclipse.xtend..");
+ }
+
+ @Test
+ public void testNoDependenyToXtend2Lib() {
+ assertNoDependencyToPackage("org.eclipse.xtend2..");
+ }
+
+ @Test
+ public void testNoDependenyToGuava() {
+ assertNoDependencyToPackage("com.google.common..");
+ }
+
+ @Test
+ public void testNoDependenyToGenerator() {
+ assertNoDependencyToPackage("org.eclipse.lsp4j.generator..");
+ }
+
+ private void assertNoDependencyToPackage(String badPackage) {
+ ArchRule rule = noClasses()
+ .that(resideInAPackage("org.eclipse.lsp4j.debug..")
+ .and(not(resideInAPackage("org.eclipse.lsp4j.generator.."))))
+ .and(not(resideInAPackage("org.eclipse.lsp4j.debug.test.."))).should().dependOnClassesThat()
+ .resideInAPackage(badPackage);
+ rule.check(importedClasses);
+ }
+
+}
diff --git a/org.eclipse.lsp4j.generator/src/main/java/org/eclipse/lsp4j/generator/JsonRpcDataProcessor.xtend b/org.eclipse.lsp4j.generator/src/main/java/org/eclipse/lsp4j/generator/JsonRpcDataProcessor.xtend
index ce296a79..fef82ee2 100644
--- a/org.eclipse.lsp4j.generator/src/main/java/org/eclipse/lsp4j/generator/JsonRpcDataProcessor.xtend
+++ b/org.eclipse.lsp4j.generator/src/main/java/org/eclipse/lsp4j/generator/JsonRpcDataProcessor.xtend
@@ -11,6 +11,7 @@
******************************************************************************/
package org.eclipse.lsp4j.generator
+import com.google.gson.annotations.JsonAdapter
import org.eclipse.lsp4j.jsonrpc.validation.NonNull
import org.eclipse.xtend.lib.annotations.AccessorsProcessor
import org.eclipse.xtend.lib.annotations.EqualsHashCodeProcessor
@@ -22,8 +23,6 @@ import org.eclipse.xtend.lib.macro.declaration.MutableClassDeclaration
import org.eclipse.xtend.lib.macro.declaration.MutableFieldDeclaration
import org.eclipse.xtend.lib.macro.declaration.Type
import org.eclipse.xtend.lib.macro.declaration.Visibility
-import org.eclipse.xtext.xbase.lib.util.ToStringBuilder
-import com.google.gson.annotations.JsonAdapter
class JsonRpcDataProcessor extends AbstractClassProcessor {
@@ -44,7 +43,12 @@ class JsonRpcDataProcessor extends AbstractClassProcessor {
val fields = impl.declaredFields.filter[!static]
equalsHashCodeUtil.addEquals(impl, fields, shouldIncludeSuper)
equalsHashCodeUtil.addHashCode(impl, fields, shouldIncludeSuper)
-
+ impl.getDeclaredMethods.forEach [ method |
+ val purified = method.findAnnotation(Pure.findTypeGlobally)
+ if (purified !== null) {
+ method.removeAnnotation(purified)
+ }
+ ]
return impl
}
@@ -154,10 +158,13 @@ class JsonRpcDataProcessor extends AbstractClassProcessor {
impl.addMethod("toString") [
returnType = string
addAnnotation(newAnnotationReference(Override))
- addAnnotation(newAnnotationReference(Pure))
val accessorsUtil = new AccessorsProcessor.Util(context)
+ val fqn = impl.qualifiedName
+ val char dot = '.'
+ val pkg = fqn.substring(0, fqn.lastIndexOf(dot))
+ val toStringBuilderClassName = (pkg+".util.ToStringBuilder")
body = '''
- «ToStringBuilder» b = new «ToStringBuilder»(this);
+ «toStringBuilderClassName.newTypeReference()» b = new «toStringBuilderClassName.newTypeReference()»(this);
«FOR field : toStringFields»
b.add("«field.simpleName»", «IF field.declaringType == impl»this.«field.simpleName»«ELSE»«
accessorsUtil.getGetterName(field)»()«ENDIF»);
diff --git a/org.eclipse.lsp4j.generator/src/main/java/org/eclipse/lsp4j/generator/TypeAdapterImplProcessor.xtend b/org.eclipse.lsp4j.generator/src/main/java/org/eclipse/lsp4j/generator/TypeAdapterImplProcessor.xtend
index 665a7bd5..e0e01888 100644
--- a/org.eclipse.lsp4j.generator/src/main/java/org/eclipse/lsp4j/generator/TypeAdapterImplProcessor.xtend
+++ b/org.eclipse.lsp4j.generator/src/main/java/org/eclipse/lsp4j/generator/TypeAdapterImplProcessor.xtend
@@ -31,6 +31,9 @@ class TypeAdapterImplProcessor extends AbstractClassProcessor {
val targetType = typeAdapterImplAnnotation.getClassValue('value')
generateImpl(annotatedClass, targetType, context)
generateFactory(findClass(annotatedClass.qualifiedName + '.Factory'), annotatedClass, targetType, context)
+ annotatedClass.removeAnnotation(annotatedClass.annotations.findFirst [
+ annotationTypeDeclaration == TypeAdapterImpl.findTypeGlobally
+ ])
}
protected def generateImpl(MutableClassDeclaration impl, TypeReference targetType, extension TransformationContext context) {
diff --git a/org.eclipse.lsp4j/build.gradle b/org.eclipse.lsp4j/build.gradle
index 8c86f8ef..fadb0481 100644
--- a/org.eclipse.lsp4j/build.gradle
+++ b/org.eclipse.lsp4j/build.gradle
@@ -17,9 +17,11 @@ sourceCompatibility = '1.8'
targetCompatibility = '1.8'
dependencies {
- api project(":org.eclipse.lsp4j.generator")
+ compileOnly project(":org.eclipse.lsp4j.generator")
api project(":org.eclipse.lsp4j.jsonrpc")
testImplementation "junit:junit:$versions.junit"
+ testImplementation project(":org.eclipse.lsp4j.generator")
+ testImplementation "com.tngtech.archunit:archunit:$versions.archunit"
}
jar {
@@ -27,5 +29,5 @@ jar {
}
jar.bnd (
- 'Import-Package': "com.google.common.*;version=\"$versions.guava\",com.google.gson.*;version=\"$versions.gson\",*"
+ 'Import-Package': "com.google.gson.*;version=\"$versions.gson\",*"
)
diff --git a/org.eclipse.lsp4j/src/main/java/org/eclipse/lsp4j/Protocol.xtend b/org.eclipse.lsp4j/src/main/java/org/eclipse/lsp4j/Protocol.xtend
index 34891262..91a5bc64 100644
--- a/org.eclipse.lsp4j/src/main/java/org/eclipse/lsp4j/Protocol.xtend
+++ b/org.eclipse.lsp4j/src/main/java/org/eclipse/lsp4j/Protocol.xtend
@@ -4406,7 +4406,7 @@ class FormattingOptions extends LinkedHashMap getProperties() {
- val properties = newLinkedHashMap
+ val properties = new LinkedHashMap
for (entry : entrySet) {
val value = switch it: entry.value {
case isFirst: getFirst
diff --git a/org.eclipse.lsp4j/src/main/java/org/eclipse/lsp4j/adapters/HoverTypeAdapter.xtend b/org.eclipse.lsp4j/src/main/java/org/eclipse/lsp4j/adapters/HoverTypeAdapter.xtend
index 94ddde3b..bd786f3a 100644
--- a/org.eclipse.lsp4j/src/main/java/org/eclipse/lsp4j/adapters/HoverTypeAdapter.xtend
+++ b/org.eclipse.lsp4j/src/main/java/org/eclipse/lsp4j/adapters/HoverTypeAdapter.xtend
@@ -1,5 +1,5 @@
/******************************************************************************
- * Copyright (c) 2018 TypeFox and others.
+ * Copyright (c) 2018, 2023 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
@@ -24,6 +24,7 @@ import org.eclipse.lsp4j.MarkedString
import org.eclipse.lsp4j.MarkupContent
import org.eclipse.lsp4j.generator.TypeAdapterImpl
import org.eclipse.lsp4j.jsonrpc.messages.Either
+import java.util.ArrayList
/**
* A type adapter for the Hover protocol type.
@@ -36,16 +37,18 @@ class HoverTypeAdapter {
protected def readContents(JsonReader in) throws IOException {
val nextToken = in.peek()
- if (nextToken == JsonToken.STRING) {
- val List> value = newArrayList(Either.forLeft(in.nextString))
+ if (nextToken === JsonToken.STRING) {
+ val List> value = new ArrayList>()
+ value.add(Either.forLeft(in.nextString))
return Either.forLeft(value)
- } else if (nextToken == JsonToken.BEGIN_ARRAY) {
+ } else if (nextToken === JsonToken.BEGIN_ARRAY) {
val value = gson.fromJson(in, LIST_STRING_MARKEDSTRING.type)
return Either.forLeft(value)
} else {
val object = JsonParser.parseReader(in) as JsonObject
if (object.has("language")) {
- val List> value = newArrayList(Either.forRight(gson.fromJson(object, MarkedString)))
+ val List> value = new ArrayList>()
+ value.add(Either.forRight(gson.fromJson(object, MarkedString)))
return Either.forLeft(value)
} else {
return Either.forRight(gson.fromJson(object, MarkupContent))
diff --git a/org.eclipse.lsp4j/src/main/java/org/eclipse/lsp4j/util/ToStringBuilder.java b/org.eclipse.lsp4j/src/main/java/org/eclipse/lsp4j/util/ToStringBuilder.java
new file mode 100644
index 00000000..6276f595
--- /dev/null
+++ b/org.eclipse.lsp4j/src/main/java/org/eclipse/lsp4j/util/ToStringBuilder.java
@@ -0,0 +1,380 @@
+/**
+ * Copyright (c) 2014, 2018 itemis AG (http://www.itemis.eu) and others.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.eclipse.lsp4j.util;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.IdentityHashMap;
+import java.util.Iterator;
+import java.util.List;
+
+
+/**
+ * Helps with the construction of good {@link Object#toString()} representations.
+ * You can customize the output using the builder-style methods {@link ToStringBuilder#singleLine()} {@link ToStringBuilder#skipNulls()} and {@link ToStringBuilder#hideFieldNames()}.
+ * You can either directly list fields to include via {@link ToStringBuilder#add(String, Object)} and {@link ToStringBuilder#add(Object)}
+ * or you can let the builder do it automatically using reflection, either including the fields declared in this class or including all superclasses.
+ * The builder will automatically handle cycles in the object tree. It also pretty prints arrays and Iterables.
+ *
+ * This class is not thread safe.
+ * @since 2.7
+ */
+public final class ToStringBuilder {
+
+ public static class ToStringContext {
+
+ public final static ToStringContext INSTANCE = new ToStringContext();
+
+ private final static ThreadLocal> currentlyProcessed = new ThreadLocal>() {
+ @Override
+ public IdentityHashMap initialValue() {
+ return new IdentityHashMap();
+ }
+ };
+
+ public boolean startProcessing(final Object obj) {
+ return ToStringContext.currentlyProcessed.get().put(obj, Boolean.TRUE) == null;
+ }
+
+ public void endProcessing(final Object obj) {
+ ToStringContext.currentlyProcessed.get().remove(obj);
+ }
+ }
+
+ private static ToStringContext toStringContext = ToStringContext.INSTANCE;
+
+ private final Object instance;
+
+ private final String typeName;
+
+ private boolean multiLine = true;
+
+ private boolean skipNulls = false;
+
+ private boolean showFieldNames = true;
+
+ private boolean prettyPrint = true;
+
+ private final List parts = new ArrayList();
+
+ /**
+ * Creates a new ToStringBuilder for the given object. If you don't use reflection, then this instance
+ * is only used for obtaining its classes' simple name.
+ *
+ * @param instance the object to convert to a String
+ */
+ public ToStringBuilder(final Object instance) {
+ this.instance = instance;
+ this.typeName = instance.getClass().getSimpleName();
+ }
+
+ /**
+ * Fields are printed on a single line, separated by commas instead of newlines
+ * @return this
+ */
+ public ToStringBuilder singleLine() {
+ this.multiLine = false;
+ return this;
+ }
+
+ /**
+ * Fields with null values will be excluded from the output
+ * @return this
+ */
+ public ToStringBuilder skipNulls() {
+ this.skipNulls = true;
+ return this;
+ }
+
+ /**
+ * Field names will not be included in the output. Useful for small classes.
+ * @return this
+ */
+ public ToStringBuilder hideFieldNames() {
+ this.showFieldNames = false;
+ return this;
+ }
+
+ /**
+ * By default, Iterables, Arrays and multiline Strings are pretty-printed.
+ * Switching to their normal representation makes the toString method significantly faster.
+ * @since 2.9
+ * @return this
+ */
+ public ToStringBuilder verbatimValues() {
+ this.prettyPrint = false;
+ return this;
+ }
+
+ /**
+ * Adds all fields declared directly in the object's class to the output
+ * @return this
+ */
+ public ToStringBuilder addDeclaredFields() {
+ Field[] fields = instance.getClass().getDeclaredFields();
+ for(Field field : fields) {
+ addField(field);
+ }
+ return this;
+ }
+
+ /**
+ * Adds all fields declared in the object's class and its superclasses to the output.
+ * @return this
+ */
+ public ToStringBuilder addAllFields() {
+ List fields = getAllDeclaredFields(instance.getClass());
+ for(Field field : fields) {
+ addField(field);
+ }
+ return this;
+ }
+
+ /**
+ * @param fieldName the name of the field to add to the output using reflection
+ * @return this
+ */
+ public ToStringBuilder addField(final String fieldName) {
+ List fields = getAllDeclaredFields(instance.getClass());
+ for(Field field : fields) {
+ if(fieldName.equals(field.getName())) {
+ addField(field);
+ break;
+ }
+ }
+ return this;
+ }
+
+ private ToStringBuilder addField(final Field field) {
+ if (!Modifier.isStatic(field.getModifiers())) {
+ field.setAccessible(true);
+ try {
+ add(field.getName(), field.get(instance));
+ } catch(IllegalAccessException e) {
+ sneakyThrow(e);
+ }
+ }
+ return this;
+ }
+
+ @SuppressWarnings("unchecked")
+ private static void sneakyThrow(Throwable t) throws T {
+ throw (T) t;
+ }
+
+ /**
+ * @param value the value to add to the output
+ * @param fieldName the field name to list the value under
+ * @return this
+ */
+ public ToStringBuilder add(final String fieldName, final Object value) {
+ return addPart(fieldName, value);
+ }
+
+ /**
+ * @param value the value to add to the output without a field name
+ * @return this
+ */
+ public ToStringBuilder add(final Object value) {
+ return addPart(value);
+ }
+
+ private Part addPart() {
+ final Part p = new Part();
+ this.parts.add(p);
+ return p;
+ }
+
+ private ToStringBuilder addPart(final Object value) {
+ final Part p = this.addPart();
+ p.value = value;
+ return this;
+ }
+
+ private ToStringBuilder addPart(final String fieldName, final Object value) {
+ final Part p = this.addPart();
+ p.fieldName = fieldName;
+ p.value = value;
+ return this;
+ }
+
+ /**
+ * @return the String representation of the processed object
+ */
+ @Override
+ public String toString() {
+ boolean startProcessing = ToStringBuilder.toStringContext.startProcessing(this.instance);
+ if (!startProcessing) {
+ return this.toSimpleReferenceString(this.instance);
+ }
+ try {
+ final IndentationAwareStringBuilder builder = new IndentationAwareStringBuilder();
+ builder.append(typeName).append(" ");
+ builder.append("[");
+ String nextSeparator = "";
+ if (multiLine) {
+ builder.increaseIndent();
+ }
+ for (Part part : parts) {
+ if (!skipNulls || part.value != null) {
+ if (multiLine) {
+ builder.newLine();
+ } else {
+ builder.append(nextSeparator);
+ nextSeparator = ", ";
+ }
+ if (part.fieldName != null && this.showFieldNames) {
+ builder.append(part.fieldName).append(" = ");
+ }
+ this.internalToString(part.value, builder);
+ }
+ }
+ if (multiLine) {
+ builder.decreaseIndent().newLine();
+ }
+ builder.append("]");
+ return builder.toString();
+ } finally {
+ ToStringBuilder.toStringContext.endProcessing(this.instance);
+ }
+ }
+
+ private void internalToString(final Object object, final IndentationAwareStringBuilder sb) {
+ if (prettyPrint) {
+ if (object instanceof Iterable>) {
+ serializeIterable((Iterable>)object, sb);
+ } else if (object instanceof Object[]) {
+ sb.append(Arrays.toString((Object[])object));
+ } else if (object instanceof byte[]) {
+ sb.append(Arrays.toString((byte[])object));
+ } else if (object instanceof char[]) {
+ sb.append(Arrays.toString((char[])object));
+ } else if (object instanceof int[]) {
+ sb.append(Arrays.toString((int[])object));
+ } else if (object instanceof boolean[]) {
+ sb.append(Arrays.toString((boolean[])object));
+ } else if (object instanceof long[]) {
+ sb.append(Arrays.toString((long[])object));
+ } else if (object instanceof float[]) {
+ sb.append(Arrays.toString((float[])object));
+ } else if (object instanceof double[]) {
+ sb.append(Arrays.toString((double[])object));
+ } else if (object instanceof CharSequence) {
+ sb.append("\"").append(((CharSequence)object).toString().replace("\n", "\\n").replace("\r", "\\r")).append("\"");
+ } else if (object instanceof Enum>) {
+ sb.append(((Enum>)object).name());
+ } else {
+ sb.append(String.valueOf(object));
+ }
+ } else {
+ sb.append(String.valueOf(object));
+ }
+ }
+
+ private void serializeIterable(final Iterable> object, final IndentationAwareStringBuilder sb) {
+ final Iterator> iterator = object.iterator();
+ sb.append(object.getClass().getSimpleName()).append(" (");
+ if (multiLine) {
+ sb.increaseIndent();
+ }
+ boolean wasEmpty = true;
+ while (iterator.hasNext()) {
+ wasEmpty = false;
+ if (multiLine) {
+ sb.newLine();
+ }
+ this.internalToString(iterator.next(), sb);
+ if (iterator.hasNext()) {
+ sb.append(",");
+ }
+ }
+ if (multiLine) {
+ sb.decreaseIndent();
+ }
+ if (!wasEmpty && this.multiLine) {
+ sb.newLine();
+ }
+ sb.append(")");
+ }
+
+ private String toSimpleReferenceString(final Object obj) {
+ String simpleName = obj.getClass().getSimpleName();
+ int identityHashCode = System.identityHashCode(obj);
+ return simpleName + "@" + Integer.valueOf(identityHashCode);
+ }
+
+ private List getAllDeclaredFields(final Class> clazz) {
+ final ArrayList result = new ArrayList<>();
+
+ for(Class> current = clazz; current != null; current = current.getSuperclass()) {
+ Field[] declaredFields = current.getDeclaredFields();
+ result.addAll(Arrays.asList(declaredFields));
+
+ }
+ return result;
+ }
+
+ private static final class Part {
+ private String fieldName;
+ private Object value;
+ }
+
+ private static class IndentationAwareStringBuilder {
+ private final StringBuilder builder = new StringBuilder();
+
+ private final String indentationString = " ";
+
+ private final String newLineString = "\n";
+
+ private int indentation = 0;
+
+ public IndentationAwareStringBuilder increaseIndent() {
+ indentation++;
+ return this;
+ }
+
+ public IndentationAwareStringBuilder decreaseIndent() {
+ indentation--;
+ return this;
+ }
+
+ public IndentationAwareStringBuilder append(final CharSequence string) {
+ if (indentation > 0) {
+ String indented = string.toString().replace(
+ newLineString,
+ newLineString + repeat(indentationString, indentation)
+ );
+ builder.append(indented);
+ } else {
+ builder.append(string);
+ }
+ return this;
+ }
+
+ public IndentationAwareStringBuilder newLine() {
+ builder.append(newLineString).
+ append(repeat(this.indentationString, this.indentation));
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return this.builder.toString();
+ }
+
+ private String repeat(String string, int count) {
+ StringBuilder result = new StringBuilder();
+ for(int i=0; i < count; i++) {
+ result.append(string);
+ }
+ return result.toString();
+ }
+ }
+}
\ No newline at end of file
diff --git a/org.eclipse.lsp4j/src/test/java/org/eclipse/lsp4j/test/Lsp4jArchitectureTest.java b/org.eclipse.lsp4j/src/test/java/org/eclipse/lsp4j/test/Lsp4jArchitectureTest.java
new file mode 100644
index 00000000..18257562
--- /dev/null
+++ b/org.eclipse.lsp4j/src/test/java/org/eclipse/lsp4j/test/Lsp4jArchitectureTest.java
@@ -0,0 +1,80 @@
+/******************************************************************************
+ * Copyright (c) 2023 itemis AG and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0,
+ * or the Eclipse Distribution License v. 1.0 which is available at
+ * http://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
+ ******************************************************************************/
+package org.eclipse.lsp4j.test;
+
+import static com.tngtech.archunit.core.domain.JavaClass.Predicates.resideInAPackage;
+import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
+import static com.tngtech.archunit.base.DescribedPredicate.not;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import com.tngtech.archunit.core.domain.JavaClasses;
+import com.tngtech.archunit.core.importer.ClassFileImporter;
+import com.tngtech.archunit.lang.ArchRule;
+
+/**
+ * Test to make sure we don't depend to xbase.lib, xtend.lib and guava at
+ * runtime
+ *
+ * @author Christian Dietrich
+ */
+public class Lsp4jArchitectureTest {
+
+ private static JavaClasses importedClasses;
+
+ @BeforeClass
+ public static void scan() {
+ importedClasses = new ClassFileImporter().importPackages("org.eclipse.lsp4j");
+ }
+
+ @AfterClass
+ public static void cleanup() {
+ importedClasses = null;
+ }
+
+ @Test
+ public void testNoDependenyToXbaseLib() {
+ assertNoDependencyToPackage("org.eclipse.xtext..");
+ }
+
+ @Test
+ public void testNoDependenyToXtendLib() {
+ assertNoDependencyToPackage("org.eclipse.xtend..");
+ }
+
+ @Test
+ public void testNoDependenyToXtend2Lib() {
+ assertNoDependencyToPackage("org.eclipse.xtend2..");
+ }
+
+ @Test
+ public void testNoDependenyToGuava() {
+ assertNoDependencyToPackage("com.google.common..");
+ }
+
+ @Test
+ public void testNoDependenyToGenerator() {
+ assertNoDependencyToPackage("org.eclipse.lsp4j.generator..");
+ }
+
+ private void assertNoDependencyToPackage(String badPackage) {
+ ArchRule rule = noClasses()
+ .that(resideInAPackage("org.eclipse.lsp4j..")
+ .and(not(resideInAPackage("org.eclipse.lsp4j.generator.."))))
+ .and(not(resideInAPackage("org.eclipse.lsp4j.test.."))).should().dependOnClassesThat()
+ .resideInAPackage(badPackage);
+ rule.check(importedClasses);
+ }
+
+}