From 5421ff541e03ad1c7e6be16a5d7252ad7af92bae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20L=C3=A4ubrich?= Date: Tue, 12 Nov 2024 10:49:30 +0100 Subject: [PATCH] Remove xtend.lib dependency (#1630) Signed-off-by: Sebastian Zarnekow Co-authored-by: Sebastian Zarnekow --- .../client/CodeLensKindCapabilities.java | 7 +- .../client/ExtendedCodeLensCapabilities.java | 7 +- .../lemminx/utils/ToStringBuilder.java | 415 ++++++++++++++++++ 3 files changed, 417 insertions(+), 12 deletions(-) create mode 100644 org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/utils/ToStringBuilder.java diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/client/CodeLensKindCapabilities.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/client/CodeLensKindCapabilities.java index defd1b2d5..15baf6f89 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/client/CodeLensKindCapabilities.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/client/CodeLensKindCapabilities.java @@ -13,8 +13,7 @@ import java.util.List; -import org.eclipse.xtext.xbase.lib.Pure; -import org.eclipse.xtext.xbase.lib.util.ToStringBuilder; +import org.eclipse.lemminx.utils.ToStringBuilder; /** * Specific capabilities for the `CodeLensKind`. @@ -48,7 +47,6 @@ public CodeLensKindCapabilities(final List valueSet) { * If this property is not present the client only supports the codeLens kinds * from `File` to `Array` as defined in the initial version of the protocol. */ - @Pure public List getValueSet() { return this.valueSet; } @@ -66,7 +64,6 @@ public void setValueSet(final List valueSet) { } @Override - @Pure public String toString() { ToStringBuilder b = new ToStringBuilder(this); b.add("valueSet", this.valueSet); @@ -74,7 +71,6 @@ public String toString() { } @Override - @Pure public boolean equals(final Object obj) { if (this == obj) { return true; @@ -97,7 +93,6 @@ public boolean equals(final Object obj) { } @Override - @Pure public int hashCode() { return 31 * 1 + ((this.valueSet == null) ? 0 : this.valueSet.hashCode()); } diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/client/ExtendedCodeLensCapabilities.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/client/ExtendedCodeLensCapabilities.java index 4993ba915..ef0bd6146 100644 --- a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/client/ExtendedCodeLensCapabilities.java +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/client/ExtendedCodeLensCapabilities.java @@ -11,9 +11,8 @@ *******************************************************************************/ package org.eclipse.lemminx.client; +import org.eclipse.lemminx.utils.ToStringBuilder; import org.eclipse.lsp4j.DynamicRegistrationCapabilities; -import org.eclipse.xtext.xbase.lib.Pure; -import org.eclipse.xtext.xbase.lib.util.ToStringBuilder; /** * Extended capabilities specific to the `textDocument/codeLens` request. This capability doesn't belong to LSP specification. See proposal at @@ -52,7 +51,6 @@ public ExtendedCodeLensCapabilities(final CodeLensKindCapabilities codeLensKind, * Specific capabilities for the `CodeLensKind` in the `textDocument/codeLens` * request. */ - @Pure public CodeLensKindCapabilities getCodeLensKind() { return this.codeLensKind; } @@ -66,7 +64,6 @@ public void setCodeLensKind(final CodeLensKindCapabilities codeLensKind) { } @Override - @Pure public String toString() { ToStringBuilder b = new ToStringBuilder(this); b.add("codeLensKind", this.codeLensKind); @@ -75,7 +72,6 @@ public String toString() { } @Override - @Pure public boolean equals(final Object obj) { if (this == obj) { return true; @@ -101,7 +97,6 @@ public boolean equals(final Object obj) { } @Override - @Pure public int hashCode() { return 31 * super.hashCode() + ((this.codeLensKind == null) ? 0 : this.codeLensKind.hashCode()); } diff --git a/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/utils/ToStringBuilder.java b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/utils/ToStringBuilder.java new file mode 100644 index 000000000..80c3b8e2c --- /dev/null +++ b/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/utils/ToStringBuilder.java @@ -0,0 +1,415 @@ +/** + * Copyright (c) 2014, 2024 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.lemminx.utils; +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; +import java.util.function.Predicate; + + +/** + * 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. + * + * @implNote This class originally came from xbase.lib and has been extended for LSP4J purposes with some additional functionality. + * It then was copied due to java 1.8 compatibility requirements into lemminx. + */ +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 directly in the object's class + * that satisfy the given condition to the output + * @param condition + * @return this + */ + public ToStringBuilder addDeclaredFields(Predicate condition) { + Field[] fields = instance.getClass().getDeclaredFields(); + for(Field field : fields) { + if (condition.test(field)) { + 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; + } + + /** + * Adds all fields declared in the object's class and its superclasses + * that satisfy the given condition to the output. + * @param condition + * @return this + */ + public ToStringBuilder addAllFields(Predicate condition) { + List fields = getAllDeclaredFields(instance.getClass()); + for(Field field : fields) { + if (condition.test(field)) { + 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 List 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) { + final StringBuilder result = new StringBuilder(); + for(int i=0; i < count; i++) { + result.append(string); + } + return result.toString(); + } + } +} \ No newline at end of file