/* * ============================================================================= * * Copyright (c) 2011-2016, The THYMELEAF team (http://www.thymeleaf.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * ============================================================================= */ package org.thymeleaf.util; import java.io.IOException; import java.io.Serializable; import java.io.Writer; import java.util.List; /** * <p> * Character sequence that aggregates one or several {@link CharSequence} objects, without the need to clone them * or convert them to String. * </p> * <p> * Special implementation of the {@link CharSequence} interface that can replace {@link String} objects * wherever a specific text literal is composed of several parts and we want to avoid creating new * {@link String} objects for them, using instead objects of this class that simply keep an array * of references to the original CharSequences. * </p> * <p> * Note that any mutable {@link CharSequence} implementations used to build objects of this class should * <strong>never</strong> be modified after the creation of the aggregated object. * </p> * * * <p> * This class is <strong>thread-safe</strong> * </p> * * * @author Daniel Fernández * * @since 3.0.0 * */ public final class AggregateCharSequence implements Serializable, IWritableCharSequence { protected static final long serialVersionUID = 823987612L; private static final int[] UNIQUE_ZERO_OFFSET = new int[] { 0 }; private final CharSequence[] values; private final int[] offsets; private final int length; // This variable will mimic the hashCode cache mechanism in java.lang.String private int hash; // defaults to 0 public AggregateCharSequence(final CharSequence component) { super(); if (component == null) { throw new IllegalArgumentException("Component argument is null, which is forbidden"); } this.values = new CharSequence[] { component }; this.offsets = UNIQUE_ZERO_OFFSET; this.length = component.length(); } public AggregateCharSequence(final CharSequence component0, final CharSequence component1) { super(); if (component0 == null || component1 == null) { throw new IllegalArgumentException("At least one component argument is null, which is forbidden"); } this.values = new CharSequence[] { component0, component1 }; this.offsets = new int[] { 0, component0.length() }; this.length = this.offsets[1] + component1.length(); } public AggregateCharSequence(final CharSequence component0, final CharSequence component1, final CharSequence component2) { super(); if (component0 == null || component1 == null || component2 == null) { throw new IllegalArgumentException("At least one component argument is null, which is forbidden"); } this.values = new CharSequence[] { component0, component1, component2 }; this.offsets = new int[] { 0, component0.length(), component0.length() + component1.length() }; this.length = this.offsets[2] + component2.length(); } public AggregateCharSequence(final CharSequence component0, final CharSequence component1, final CharSequence component2, final CharSequence component3) { super(); if (component0 == null || component1 == null || component2 == null || component3 == null) { throw new IllegalArgumentException("At least one component argument is null, which is forbidden"); } this.values = new CharSequence[] { component0, component1, component2, component3 }; this.offsets = new int[] { 0, component0.length(), component0.length() + component1.length(), component0.length() + component1.length() + component2.length() }; this.length = this.offsets[3] + component3.length(); } public AggregateCharSequence(final CharSequence component0, final CharSequence component1, final CharSequence component2, final CharSequence component3, final CharSequence component4) { super(); if (component0 == null || component1 == null || component2 == null || component3 == null || component4 == null) { throw new IllegalArgumentException("At least one component argument is null, which is forbidden"); } this.values = new CharSequence[] { component0, component1, component2, component3, component4 }; this.offsets = new int[] { 0, component0.length(), component0.length() + component1.length(), component0.length() + component1.length() + component2.length(), component0.length() + component1.length() + component2.length() + component3.length() }; this.length = this.offsets[4] + component3.length(); } public AggregateCharSequence(final CharSequence[] components) { // NOTE: We have this set of constructors instead of only one with a varargs argument in order to // avoid unnecessary creation of String[] objects super(); if (components == null) { throw new IllegalArgumentException("Components argument array cannot be null"); } if (components.length == 0) { // We want always at least one String this.values = new CharSequence[]{""}; this.offsets = UNIQUE_ZERO_OFFSET; this.length = 0; } else { this.values = new CharSequence[components.length]; this.offsets = new int[components.length]; int totalLength = 0; int i = 0; while (i < components.length) { if (components[i] == null) { throw new IllegalArgumentException("Components argument contains at least a null, which is forbidden"); } final int componentLen = components[i].length(); this.values[i] = components[i]; this.offsets[i] = (i == 0 ? 0 : this.offsets[i - 1] + this.values[i - 1].length()); totalLength += componentLen; i++; } this.length = totalLength; } } public AggregateCharSequence(final List<? extends CharSequence> components) { super(); if (components == null) { throw new IllegalArgumentException("Components argument array cannot be null"); } final int componentsSize = components.size(); if (componentsSize == 0) { // We want always at least one String this.values = new CharSequence[]{""}; this.offsets = UNIQUE_ZERO_OFFSET; this.length = 0; } else { this.values = new CharSequence[componentsSize]; this.offsets = new int[componentsSize]; int totalLength = 0; int i = 0; while (i < componentsSize) { final CharSequence element = components.get(i); if (element == null) { throw new IllegalArgumentException("Components argument contains at least a null, which is forbidden"); } final int componentLen = element.length(); this.values[i] = element; this.offsets[i] = (i == 0 ? 0 : this.offsets[i - 1] + this.values[i - 1].length()); totalLength += componentLen; i++; } this.length = totalLength; } } public int length() { return this.length; } public char charAt(final int index) { if ((index < 0) || (index >= this.length)) { throw new StringIndexOutOfBoundsException(index); } int n = this.values.length; while (n-- != 0) { if (this.offsets[n] <= index) { return this.values[n].charAt(index - this.offsets[n]); } } // Should never reach here! throw new IllegalStateException("Bad computing of charAt at AggregatedString"); } public CharSequence subSequence(final int beginIndex, final int endIndex) { if (beginIndex < 0) { throw new StringIndexOutOfBoundsException(beginIndex); } if (endIndex > this.length) { throw new StringIndexOutOfBoundsException(endIndex); } int subLen = endIndex - beginIndex; if (subLen < 0) { throw new StringIndexOutOfBoundsException(subLen); } if (subLen == 0) { return ""; } int n1 = this.values.length; while (n1-- != 0) { if (this.offsets[n1] < endIndex) { // Will always happen eventually, as the first offset is 0 break; } } int n0 = n1 + 1; while (n0-- != 0) { if (this.offsets[n0] <= beginIndex) { // Will always happen eventually, as the first offset is 0 break; } } if (n0 == n1) { // Shortcut: let the CharSequence#subSequence method do the job... return this.values[n0].subSequence((beginIndex - this.offsets[n0]), (endIndex - this.offsets[n0])); } final char[] chars = new char[endIndex - beginIndex]; int charsOffset = 0; int nx = n0; while (nx <= n1) { final int nstart = Math.max(beginIndex, this.offsets[nx]) - this.offsets[nx]; final int nend = Math.min(endIndex, this.offsets[nx] + this.values[nx].length()) - this.offsets[nx]; copyChars(this.values[nx], nstart, nend, chars, charsOffset); charsOffset += (nend - nstart); nx++; } return new String(chars); } public void write(final Writer writer) throws IOException { if (writer == null) { throw new IllegalArgumentException("Writer cannot be null"); } for (int i = 0; i < this.values.length; i++) { writer.write(this.values[i].toString()); } } @Override public boolean equals(final Object o) { if (this == o) { return true; } if (!(o instanceof AggregateCharSequence)) { return false; } final AggregateCharSequence that = (AggregateCharSequence) o; if (this.values.length == 1 && that.values.length == 1) { if (this.values[0] instanceof String && that.values[0] instanceof String) { return this.values[0].equals(that.values[0]); } } if (this.length != that.length) { return false; } if (this.length == 0) { return true; } if (this.hash != 0 && that.hash != 0 && this.hash != that.hash) { return false; } int i = 0; int m1 = 0; int n1 = 0; int len1 = this.values[m1].length(); int m2 = 0; int n2 = 0; int len2 = that.values[m2].length(); while (i < this.length) { // Move to the next value array if needed, including skipping those with len == 0 while (n1 >= len1 && (m1 + 1) < this.values.length) { m1++; n1 = 0; len1 = this.values[m1].length(); } while (n2 >= len2 && (m2 + 1) < that.values.length) { m2++; n2 = 0; len2 = that.values[m2].length(); } // Shortcut, in case we have to identical Strings ready to be compared with one another if (n1 == 0 && n2 == 0 && len1 == len2 && this.values[m1] instanceof String && that.values[m2] instanceof String) { if (!this.values[m1].equals(that.values[m2])) { return false; } n1 = len1; // Force skipping this value position n2 = len2; // Force skipping this value position i += len1; continue; } // Character-by-character matching if (this.values[m1].charAt(n1) != that.values[m2].charAt(n2)) { return false; } n1++; n2++; i++; } return true; } public int hashCode() { // This method mimics the local-variable cache mechanism from java.lang.String // --------------------------------------- // NOTE: Even if relying on the specific implementation of String.hashCode() might seem // a potential issue for cross-platform compatibility, the fact is that the // implementation of String.hashCode() is actually a part of the Java Specification // since Java 1.2, and its internal workings are explained in the JavaDoc for the // String.hashCode() method. // --------------------------------------- int h = this.hash; if (h == 0 && this.length > 0) { if (this.values.length == 1) { h = this.values[0].hashCode(); // Might be cached at the String object, let's benefit from that } else { final CharSequence[] vals = this.values; CharSequence val; int valLen; for (int x = 0; x < vals.length; x++) { val = vals[x]; valLen = val.length(); for (int i = 0; i < valLen; i++) { h = 31 * h + val.charAt(i); } } } this.hash = h; } return h; } public boolean contentEquals(final StringBuffer sb) { synchronized (sb) { return contentEquals((CharSequence) sb); } } public boolean contentEquals(final CharSequence cs) { if (this.length != cs.length()) { return false; } if (this.length == 0) { return true; } // Shortcut in case argument is another AggregatedString if (cs.equals(this)) { return true; } if (cs instanceof String) { if (this.values.length == 1 && this.values[0] instanceof String) { return this.values[0].equals(cs); } if (this.hash != 0 && this.hash != cs.hashCode()) { return false; } } // Deal with argument as a generic CharSequence int i = 0; int m1 = 0; int n1 = 0; int len1 = this.values[m1].length(); while (i < this.length) { // Move to the next value array if needed, including skipping those with len == 0 while (n1 >= len1 && (m1 + 1) < this.values.length) { m1++; n1 = 0; len1 = this.values[m1].length(); } // Character-by-character matching if (this.values[m1].charAt(n1) != cs.charAt(i)) { return false; } n1++; i++; } return true; } @Override public String toString() { if (this.length == 0) { return ""; } if (this.values.length == 1) { return this.values[0].toString(); } final char[] chars = new char[this.length]; for (int i = 0; i < this.values.length; i++) { copyChars(this.values[i], 0, this.values[i].length(), chars, this.offsets[i]); } return new String(chars); } private static void copyChars( final CharSequence src, final int srcBegin, final int srcEnd, final char[] dst, final int dstBegin) { if (src instanceof String) { ((String)src).getChars(srcBegin, srcEnd, dst, dstBegin); return; } int i = srcBegin; while (i < srcEnd) { dst[dstBegin + (i - srcBegin)] = src.charAt(i); i++; } } }