| /******************************************************************************* |
| * Copyright (c) 2011, 2016 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.xtend2.lib; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.List; |
| |
| import org.eclipse.xtend2.lib.StringConcatenationClient.TargetStringConcatenation; |
| |
| import com.google.common.annotations.GwtCompatible; |
| |
| /** |
| * <p> |
| * A {@link StringConcatenation} allows for efficient, indentation aware concatenation of character sequences. |
| * </p> |
| * <p> |
| * In addition to the methods that are specified by the implemented interface {@link CharSequence}, there are some other |
| * public operations that allow to modify the contents of this sequence. The string representation of arbitrary objects |
| * can be appended to an instance of {@link StringConcatenation}. There are overloaded variants of |
| * {@link #append(Object, String)} that allow to pass an indentation that should be applied to each line of the appended |
| * content. Each line break that is part of the new content will be replaced by the line delimiter that was configured |
| * for this {@link StringConcatenation}. |
| * </p> |
| * <p> |
| * The {@link #append(Object) append}-operation ignores <code>null</code> arguments. This is different to what |
| * {@link StringBuffer} or {@link StringBuilder} do and that's the reason why the {@link Appendable} interface is not |
| * fulfilled by a {@link StringConcatenation}. |
| * <p> |
| * <p> |
| * The object uses an internal {@link List} of {@link String Strings} that is concatenated on demand to a complete |
| * sequence. Use {@link #toString()} to get the joined version of a {@link StringConcatenation}. |
| * </p> |
| * <p> |
| * {@link #equals(Object)} and {@link #hashCode()} are not specialized for a {@link StringConcatenation}, that is, the |
| * semantics are based on identity similar to what {@link StringBuffer} and {@link StringBuilder} do. |
| * </p> |
| * |
| * @author Sebastian Zarnekow - Initial contribution and API |
| * @since 2.3 |
| */ |
| @GwtCompatible |
| public class StringConcatenation implements CharSequence { |
| |
| /** |
| * The default line delimiter that is used by instances of {@link StringConcatenation}. It uses |
| * <code>System.getProperty("line.separator")</code>. |
| * @since 2.3 |
| */ |
| public static final String DEFAULT_LINE_DELIMITER = DefaultLineDelimiter.get(); |
| |
| /** |
| * Number of elements to add to {@link #segments} array when we need to grow it. This should be a power-of-two |
| * so we can perform efficient modulo operation. |
| */ |
| private static final int SEGMENTS_SIZE_INCREMENT = 16; |
| |
| /** |
| * Initial allocation size of the {@link #segments} array. |
| */ |
| private static final int SEGMENTS_INITIAL_SIZE = 3 * SEGMENTS_SIZE_INCREMENT; |
| |
| /** |
| * The complete content of this sequence. It may contain insignificant trailing parts that are not part of the final |
| * string representation that can be obtained by {@link #toString()}. Insignificant parts will not be considered by |
| * {@link #length()}, {@link #charAt(int)} or {@link #subSequence(int, int)}. |
| */ |
| private final ArrayList<String> segments = new ArrayList<String>(SEGMENTS_INITIAL_SIZE); |
| |
| /** |
| * Internal hint for the size of segments. We cache this value so we do not have to perform costly calculations. |
| */ |
| private int lastSegmentsSize = SEGMENTS_INITIAL_SIZE; |
| |
| /** |
| * A cached string representation. |
| */ |
| private String cachedToString; |
| |
| /** |
| * The configured delimiter. It will be used to replace possibly existing delimiters of to-be-appended content. |
| */ |
| private final String lineDelimiter; |
| |
| /** |
| * Create a new concatenation that uses the system line delimiter. |
| * |
| * @see System#getProperties() |
| * @see System#getProperty(String) |
| */ |
| public StringConcatenation() { |
| this(DEFAULT_LINE_DELIMITER); |
| } |
| |
| /** |
| * Create a new concatenation with the specified delimiter. |
| * |
| * @param lineDelimiter |
| * the used delimiter. |
| * @throws IllegalArgumentException |
| * if the lineDelimiter is <code>null</code> or the empty String. |
| * @since 2.3 |
| */ |
| public StringConcatenation(String lineDelimiter) { |
| if (lineDelimiter == null || lineDelimiter.length() == 0) |
| throw new IllegalArgumentException("lineDelimiter must not be null or empty"); |
| this.lineDelimiter = lineDelimiter; |
| } |
| |
| private void growSegments(int increment) { |
| int targetSize = segments.size() + increment; |
| if (targetSize <= lastSegmentsSize) { |
| return; |
| } |
| |
| int mod = targetSize % SEGMENTS_SIZE_INCREMENT; |
| if (mod != 0) { |
| targetSize += SEGMENTS_SIZE_INCREMENT - mod; |
| } |
| |
| segments.ensureCapacity(targetSize); |
| lastSegmentsSize = targetSize; |
| } |
| |
| /** |
| * Append the string representation of the given object to this sequence. Does nothing if the object is |
| * <code>null</code>. |
| * |
| * @param object |
| * the to-be-appended object. |
| */ |
| public void append(Object object) { |
| append(object, segments.size()); |
| } |
| |
| /** |
| * Append the given string to this sequence. Does nothing if the string is <code>null</code>. |
| * |
| * @param str |
| * the to-be-appended string. |
| * @since 2.11 |
| */ |
| public void append(String str) { |
| if (str != null) |
| append(str, segments.size()); |
| } |
| |
| /** |
| * Append the contents of a given StringConcatenation to this sequence. Does nothing |
| * if the concatenation is <code>null</code>. |
| * |
| * @param concat |
| * the to-be-appended StringConcatenation. |
| * @since 2.11 |
| */ |
| public void append(StringConcatenation concat) { |
| if (concat != null) |
| appendSegments(segments.size(), concat.getSignificantContent(), concat.lineDelimiter); |
| } |
| |
| /** |
| * Append the contents of a given StringConcatenationClient to this sequence. Does nothing |
| * if the argument is <code>null</code>. |
| * |
| * @param client |
| * the to-be-appended StringConcatenationClient. |
| * @since 2.11 |
| */ |
| public void append(StringConcatenationClient client) { |
| if (client != null) |
| client.appendTo(new SimpleTarget(this, segments.size())); |
| } |
| |
| /* |
| * Add the string representation of the given object to this sequence at the given index. Does nothing if the object |
| * is <code>null</code>. |
| * |
| * @param object |
| * the to-be-appended object. |
| * @param index |
| * the index in the list of segments. |
| */ |
| protected void append(Object object, int index) { |
| if (object == null) |
| return; |
| if (object instanceof String) { |
| append((String) object, index); |
| } else if (object instanceof StringConcatenation) { |
| StringConcatenation other = (StringConcatenation) object; |
| appendSegments(index, other.getSignificantContent(), other.lineDelimiter); |
| } else if (object instanceof StringConcatenationClient) { |
| StringConcatenationClient other = (StringConcatenationClient) object; |
| other.appendTo(new SimpleTarget(this, index)); |
| } else { |
| final String text = getStringRepresentation(object); |
| if (text != null) { |
| append(text, index); |
| } |
| } |
| } |
| |
| private void append(String text, int index) { |
| final int initial = initialSegmentSize(text); |
| if (initial == text.length()) { |
| appendSegment(index, text); |
| } else { |
| appendSegments(index, continueSplitting(text, initial)); |
| } |
| } |
| |
| /** |
| * Add the string representation of the given object to this sequence. The given indentation will be prepended to |
| * each line except the first one if the object has a multi-line string representation. |
| * |
| * @param object |
| * the appended object. |
| * @param indentation |
| * the indentation string that should be prepended. May not be <code>null</code>. |
| */ |
| public void append(Object object, String indentation) { |
| append(object, indentation, segments.size()); |
| } |
| |
| /** |
| * Add the given string to this sequence. The given indentation will be prepended to |
| * each line except the first one if the object has a multi-line string representation. |
| * |
| * @param str |
| * the appended string. |
| * @param indentation |
| * the indentation string that should be prepended. May not be <code>null</code>. |
| * @since 2.11 |
| */ |
| public void append(String str, String indentation) { |
| if (indentation.isEmpty()) { |
| append(str); |
| } else if (str != null) |
| append(indentation, str, segments.size()); |
| } |
| |
| /** |
| * Append the contents of a given StringConcatenation to this sequence. Does nothing |
| * if the concatenation is <code>null</code>. The given indentation will be prepended to each line except |
| * the first one. |
| * |
| * @param concat |
| * the to-be-appended StringConcatenation. |
| * @param indentation |
| * the indentation string that should be prepended. May not be <code>null</code>. |
| * @since 2.11 |
| */ |
| public void append(StringConcatenation concat, String indentation) { |
| if (indentation.isEmpty()) { |
| append(concat); |
| } else if (concat != null) |
| appendSegments(indentation, segments.size(), concat.getSignificantContent(), concat.lineDelimiter); |
| } |
| |
| /** |
| * Append the contents of a given StringConcatenationClient to this sequence. Does nothing |
| * if that argument is <code>null</code>. The given indentation will be prepended to each line except |
| * the first one. |
| * |
| * @param client |
| * the to-be-appended StringConcatenationClient. |
| * @param indentation |
| * the indentation string that should be prepended. May not be <code>null</code>. |
| * @since 2.11 |
| */ |
| public void append(StringConcatenationClient client, String indentation) { |
| if (indentation.isEmpty()) { |
| append(client); |
| } else if (client != null) |
| client.appendTo(new IndentedTarget(this, indentation, segments.size())); |
| } |
| |
| /** |
| * Add the string representation of the given object to this sequence at the given index. The given indentation will |
| * be prepended to each line except the first one if the object has a multi-line string representation. |
| * |
| * @param object |
| * the to-be-appended object. |
| * @param indentation |
| * the indentation string that should be prepended. May not be <code>null</code>. |
| * @param index |
| * the index in the list of segments. |
| */ |
| protected void append(Object object, String indentation, int index) { |
| if (indentation.length() == 0) { |
| append(object, index); |
| return; |
| } |
| if (object == null) |
| return; |
| if (object instanceof String) { |
| append(indentation, (String)object, index); |
| } else if (object instanceof StringConcatenation) { |
| StringConcatenation other = (StringConcatenation) object; |
| List<String> otherSegments = other.getSignificantContent(); |
| appendSegments(indentation, index, otherSegments, other.lineDelimiter); |
| } else if (object instanceof StringConcatenationClient) { |
| StringConcatenationClient other = (StringConcatenationClient) object; |
| other.appendTo(new IndentedTarget(this, indentation, index)); |
| } else { |
| final String text = getStringRepresentation(object); |
| if (text != null) { |
| append(indentation, text, index); |
| } |
| } |
| } |
| |
| private void append(String indentation, String text, int index) { |
| final int initial = initialSegmentSize(text); |
| if (initial == text.length()) { |
| appendSegment(index, text); |
| } else { |
| appendSegments(indentation, index, continueSplitting(text, initial), lineDelimiter); |
| } |
| } |
| |
| /** |
| * Computes the string representation of the given object. The default implementation |
| * will just invoke {@link Object#toString()} but clients may override and specialize |
| * the logic. |
| * |
| * @param object the object that shall be appended. Never <code>null</code>. |
| * @return the string representation. May not be <code>null</code>. |
| * @since 2.5 |
| */ |
| protected String getStringRepresentation(Object object) { |
| return object.toString(); |
| } |
| |
| /** |
| * Add the string representation of the given object to this sequence immediately. That is, all the trailing |
| * whitespace of this sequence will be ignored and the string is appended directly after the last segment that |
| * contains something besides whitespace. The given indentation will be prepended to each line except the first one |
| * if the object has a multi-line string representation. |
| * |
| * @param object |
| * the to-be-appended object. |
| * @param indentation |
| * the indentation string that should be prepended. May not be <code>null</code>. |
| */ |
| public void appendImmediate(Object object, String indentation) { |
| for (int i = segments.size() - 1; i >= 0; i--) { |
| String segment = segments.get(i); |
| for (int j = 0; j < segment.length(); j++) { |
| if (!WhitespaceMatcher.isWhitespace(segment.charAt(j))) { |
| append(object, indentation, i + 1); |
| return; |
| } |
| } |
| } |
| append(object, indentation, 0); |
| } |
| |
| /** |
| * Add the list of segments to this sequence at the given index. The given indentation will be prepended to each |
| * line except the first one if the object has a multi-line string representation. |
| * |
| * @param indentation |
| * the indentation string that should be prepended. May not be <code>null</code>. |
| * @param index |
| * the index in this instance's list of segments. |
| * @param otherSegments |
| * the to-be-appended segments. May not be <code>null</code>. |
| * @param otherDelimiter |
| * the line delimiter that was used in the otherSegments list. |
| */ |
| protected void appendSegments(String indentation, int index, List<String> otherSegments, String otherDelimiter) { |
| if (otherSegments.isEmpty()) { |
| return; |
| } |
| |
| // This may not be accurate, but it's better than nothing |
| growSegments(otherSegments.size()); |
| for (String otherSegment : otherSegments) { |
| if (otherDelimiter.equals(otherSegment)) { |
| segments.add(index++, lineDelimiter); |
| segments.add(index++, indentation); |
| } else { |
| segments.add(index++, otherSegment); |
| } |
| } |
| cachedToString = null; |
| } |
| |
| /** |
| * Add the list of segments to this sequence at the given index. The given indentation will be prepended to each |
| * line except the first one if the object has a multi-line string representation. |
| * |
| * @param index |
| * the index in this instance's list of segments. |
| * @param otherSegments |
| * the to-be-appended segments. May not be <code>null</code>. |
| * @param otherDelimiter |
| * the line delimiter that was used in the otherSegments list. |
| * @since 2.5 |
| */ |
| protected void appendSegments(int index, List<String> otherSegments, String otherDelimiter) { |
| if (otherDelimiter.equals(lineDelimiter)) { |
| appendSegments(index, otherSegments); |
| } else { |
| if (otherSegments.isEmpty()) { |
| return; |
| } |
| growSegments(otherSegments.size()); |
| for (String otherSegment : otherSegments) { |
| if (otherDelimiter.equals(otherSegment)) { |
| segments.add(index++, lineDelimiter); |
| } else { |
| segments.add(index++, otherSegment); |
| } |
| } |
| cachedToString = null; |
| } |
| } |
| |
| /** |
| * Add the list of segments to this sequence at the given index. The given indentation will be prepended to each |
| * line except the first one if the object has a multi-line string representation. |
| * |
| * @param index |
| * the index in this instance's list of segments. |
| * @param otherSegments |
| * the to-be-appended segments. May not be <code>null</code>. |
| * @since 2.5 |
| */ |
| protected void appendSegments(int index, List<String> otherSegments) { |
| growSegments(otherSegments.size()); |
| if (segments.addAll(index, otherSegments)) { |
| cachedToString = null; |
| } |
| } |
| |
| private void appendSegment(int index, String segment) { |
| growSegments(1); |
| segments.add(index, segment); |
| cachedToString = null; |
| } |
| |
| /** |
| * Add a newline to this sequence according to the configured lineDelimiter. |
| */ |
| public void newLine() { |
| growSegments(1); |
| segments.add(lineDelimiter); |
| cachedToString = null; |
| } |
| |
| /** |
| * Add a newline to this sequence according to the configured lineDelimiter if the last line contains |
| * something besides whitespace. |
| */ |
| public void newLineIfNotEmpty() { |
| for (int i = segments.size() - 1; i >= 0; i--) { |
| String segment = segments.get(i); |
| if (lineDelimiter.equals(segment)) { |
| segments.subList(i + 1, segments.size()).clear(); |
| cachedToString = null; |
| return; |
| } |
| for (int j = 0; j < segment.length(); j++) { |
| if (!WhitespaceMatcher.isWhitespace(segment.charAt(j))) { |
| newLine(); |
| return; |
| } |
| } |
| } |
| segments.clear(); |
| cachedToString = null; |
| } |
| |
| @Override |
| public String toString() { |
| if (cachedToString != null) { |
| return cachedToString; |
| } |
| List<String> significantContent = getSignificantContent(); |
| StringBuilder builder = new StringBuilder(significantContent.size() * 4); |
| for (String segment : significantContent) |
| builder.append(segment); |
| cachedToString = builder.toString(); |
| return cachedToString; |
| } |
| |
| /** |
| * Return the actual content of this sequence, including all trailing whitespace. The return value is unsafe, |
| * that is modification to this {@link StringConcatenation} will cause changes in a previously obtained |
| * result and vice versa. |
| * |
| * @return the actual content of this instance. Never <code>null</code>. |
| * @since 2.8 |
| */ |
| protected final List<String> getContent() { |
| return segments; |
| } |
| |
| /** |
| * Compute the significant content of this sequence. That is, trailing whitespace after the last line-break will be |
| * ignored if the last line contains only whitespace. The return value is unsafe, that is modification to this |
| * {@link StringConcatenation} will cause changes in a previously obtained result and vice versa. |
| * |
| * @return the significant content of this instance. Never <code>null</code>. |
| */ |
| protected List<String> getSignificantContent() { |
| for (int i = segments.size() - 1; i >= 0; i--) { |
| String segment = segments.get(i); |
| if (lineDelimiter.equals(segment)) { |
| return segments.subList(0, i + 1); |
| } |
| for (int j = 0; j < segment.length(); j++) { |
| if (!WhitespaceMatcher.isWhitespace(segment.charAt(j))) { |
| return segments; |
| } |
| } |
| } |
| return segments; |
| } |
| |
| /** |
| * Allows subtypes to access the configured line delimiter. |
| |
| * @return the line delimiter |
| * @since 2.5 |
| */ |
| protected String getLineDelimiter() { |
| return lineDelimiter; |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * <p> |
| * Only the significant content of this sequence is considered. |
| * </p> |
| */ |
| @Override |
| public int length() { |
| return toString().length(); |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * <p> |
| * Only the significant content of this sequence is considered. |
| * </p> |
| */ |
| @Override |
| public char charAt(int index) { |
| return toString().charAt(index); |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * <p> |
| * Only the significant content of this sequence is considered. |
| * </p> |
| */ |
| @Override |
| public CharSequence subSequence(int start, int end) { |
| return toString().subSequence(start, end); |
| } |
| |
| /** |
| * Return a list of segments where each segment is either the content of a line in the given text or a line-break |
| * according to the configured delimiter. Existing line-breaks in the text will be replaced by this's |
| * instances delimiter. |
| * |
| * @param text |
| * the to-be-splitted text. May be <code>null</code>. |
| * @return a list of segments. Is never <code>null</code>. |
| */ |
| protected List<String> splitLinesAndNewLines(String text) { |
| if (text == null) |
| return Collections.emptyList(); |
| int idx = initialSegmentSize(text); |
| if (idx == text.length()) { |
| return Collections.singletonList(text); |
| } |
| |
| return continueSplitting(text, idx); |
| } |
| |
| private static int initialSegmentSize(String text) { |
| final int length = text.length(); |
| int idx = 0; |
| while (idx < length) { |
| char currentChar = text.charAt(idx); |
| if (currentChar == '\r' || currentChar == '\n') { |
| break; |
| } |
| idx++; |
| } |
| |
| return idx; |
| } |
| |
| private List<String> continueSplitting(String text, int idx) { |
| final int length = text.length(); |
| int nextLineOffset = 0; |
| |
| final List<String> result = new ArrayList<String>(5); |
| while (idx < length) { |
| char currentChar = text.charAt(idx); |
| // check for \r or \r\n |
| if (currentChar == '\r') { |
| int delimiterLength = 1; |
| if (idx + 1 < length && text.charAt(idx + 1) == '\n') { |
| delimiterLength++; |
| idx++; |
| } |
| int lineLength = idx - delimiterLength - nextLineOffset + 1; |
| result.add(text.substring(nextLineOffset, nextLineOffset + lineLength)); |
| result.add(lineDelimiter); |
| nextLineOffset = idx + 1; |
| } else if (currentChar == '\n') { |
| int lineLength = idx - nextLineOffset; |
| result.add(text.substring(nextLineOffset, nextLineOffset + lineLength)); |
| result.add(lineDelimiter); |
| nextLineOffset = idx + 1; |
| } |
| idx++; |
| } |
| if (nextLineOffset != length) { |
| int lineLength = length - nextLineOffset; |
| result.add(text.substring(nextLineOffset, nextLineOffset + lineLength)); |
| } |
| return result; |
| } |
| |
| /** |
| * Decorates an existing {@link StringConcatenation} as a {@link TargetStringConcatenation}. |
| * It maintains the index at which new elements should inserted into the existing concatenation. |
| * |
| * @noextend This class is not intended to be subclassed by clients. |
| * @noinstantiate This class is not intended to be instantiated by clients. |
| */ |
| private static class SimpleTarget implements StringConcatenationClient.TargetStringConcatenation { |
| |
| private final StringConcatenation target; |
| private final int offsetFixup; |
| |
| private SimpleTarget(StringConcatenation target, int index) { |
| this.target = target; |
| this.offsetFixup = target.segments.size() - index; |
| } |
| |
| @Override |
| public int length() { |
| return target.length(); |
| } |
| |
| @Override |
| public char charAt(int index) { |
| return target.charAt(index); |
| } |
| |
| @Override |
| public CharSequence subSequence(int start, int end) { |
| return target.subSequence(start, end); |
| } |
| |
| @Override |
| public void newLineIfNotEmpty() { |
| target.newLineIfNotEmpty(); |
| } |
| |
| @Override |
| public void newLine() { |
| target.newLine(); |
| } |
| |
| @Override |
| public void appendImmediate(Object object, String indentation) { |
| target.appendImmediate(object, indentation); |
| } |
| |
| @Override |
| public void append(Object object, String indentation) { |
| if (offsetFixup == 0) |
| target.append(object, indentation); |
| else |
| target.append(object, indentation, target.segments.size() - offsetFixup); |
| } |
| |
| @Override |
| public void append(Object object) { |
| target.append(object, target.segments.size() - offsetFixup); |
| } |
| |
| } |
| |
| /** |
| * Decorates an existing {@link StringConcatenation} as a {@link TargetStringConcatenation}. |
| * This implementation keeps track of the current indentation at the position where |
| * the clients should write into the decorated {@link StringConcatenation}. |
| * |
| * @noextend This class is not intended to be subclassed by clients. |
| * @noinstantiate This class is not intended to be instantiated by clients. |
| */ |
| private static class IndentedTarget extends SimpleTarget { |
| |
| private final String indentation; |
| |
| private IndentedTarget(StringConcatenation target, String indentation, int index) { |
| super(target, index); |
| this.indentation = indentation; |
| } |
| |
| @Override |
| public void newLineIfNotEmpty() { |
| super.newLineIfNotEmpty(); |
| super.append(indentation); |
| } |
| |
| @Override |
| public void newLine() { |
| super.newLine(); |
| super.append(indentation); |
| } |
| |
| @Override |
| public void appendImmediate(Object object, String indentation) { |
| super.appendImmediate(object, this.indentation + indentation); |
| } |
| |
| @Override |
| public void append(Object object, String indentation) { |
| super.append(object, this.indentation + indentation); |
| } |
| |
| @Override |
| public void append(Object object) { |
| super.append(object, indentation); |
| } |
| } |
| } |