com.google.gwt.emul.java.lang.String Maven / Gradle / Ivy
/*
* Copyright 2008 Google Inc.
*
* 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 java.lang;
import static javaemul.internal.InternalPreconditions.checkCriticalStringBounds;
import static javaemul.internal.InternalPreconditions.checkNotNull;
import static javaemul.internal.InternalPreconditions.checkStringBounds;
import static javaemul.internal.InternalPreconditions.checkStringElementIndex;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.nio.charset.UnsupportedCharsetException;
import java.util.Comparator;
import java.util.Locale;
import java.util.StringJoiner;
import javaemul.internal.ArrayHelper;
import javaemul.internal.Coercions;
import javaemul.internal.EmulatedCharset;
import javaemul.internal.JsUtils;
import javaemul.internal.NativeRegExp;
import javaemul.internal.annotations.DoNotInline;
import jsinterop.annotations.JsMethod;
import jsinterop.annotations.JsNonNull;
import jsinterop.annotations.JsPackage;
import jsinterop.annotations.JsProperty;
import jsinterop.annotations.JsType;
/**
* Intrinsic string class.
*/
// Needed to have constructors not fail compilation internally at Google
@SuppressWarnings({ "ReturnValueIgnored", "unusable-by-js" })
public final class String implements Comparable, CharSequence,
Serializable {
/* TODO(jat): consider whether we want to support the following methods;
*
*
* - deprecated methods dealing with bytes (I assume not since I can't see
* much use for them)
*
* - String(byte[] ascii, int hibyte)
*
- String(byte[] ascii, int hibyte, int offset, int count)
*
- getBytes(int srcBegin, int srcEnd, byte[] dst, int dstBegin)
*
* - methods which in JS will essentially do nothing or be the same as other
* methods
*
* - copyValueOf(char[] data)
*
- copyValueOf(char[] data, int offset, int count)
*
* - methods added in Java 1.6 (the issue is how will it impact users
* building against Java 1.5)
*
* - isEmpty()
*
* - other methods which are not straightforward in JS
*
* - format(String format, Object... args)
*
*
*
* Also, in general, we need to improve our support of non-ASCII characters. The
* problem is that correct support requires large tables, and we don't want to
* make users who aren't going to use that pay for it. There are two ways to do
* that:
*
* - construct the tables in such a way that if the corresponding method is
* not called the table will be elided from the output.
*
- provide a deferred binding target selecting the level of compatibility
* required. Those that only need ASCII (or perhaps a different relatively small
* subset such as Latin1-5) will not pay for large tables, even if they do call
* toLowercase(), for example.
*
*
* Also, if we ever add multi-locale support, there are a number of other
* methods such as toLowercase(Locale) we will want to consider supporting. This
* is probably rare, but there will be some apps (such as a translation tool)
* which cannot be written without this support.
*
* Another category of incomplete support is that we currently just use the JS
* regex support, which is not exactly the same as Java. We should support Java
* syntax by mapping it into equivalent JS patterns, or emulating them.
*
* IMPORTANT NOTE: if newer JREs add new interfaces to String, please update
* {@link Devirtualizer} and {@link JavaResourceBase}
*/
public static final Comparator CASE_INSENSITIVE_ORDER = new Comparator() {
@Override
public int compare(String a, String b) {
return a.compareToIgnoreCase(b);
}
};
public static String copyValueOf(char[] v) {
return valueOf(v);
}
public static String copyValueOf(char[] v, int offset, int count) {
return valueOf(v, offset, count);
}
public static String join(CharSequence delimiter, CharSequence... elements) {
StringJoiner joiner = new StringJoiner(delimiter);
for (CharSequence e : elements) {
joiner.add(e);
}
return joiner.toString();
}
public static String join(CharSequence delimiter, Iterable extends CharSequence> elements) {
StringJoiner joiner = new StringJoiner(delimiter);
for (CharSequence e : elements) {
joiner.add(e);
}
return joiner.toString();
}
public static String valueOf(boolean x) {
return "" + x;
}
public static String valueOf(char x) {
return NativeString.fromCharCode(x);
}
public static String valueOf(char x[], int offset, int count) {
int end = offset + count;
checkCriticalStringBounds(offset, end, x.length);
// Work around function.prototype.apply call stack size limits:
// https://code.google.com/p/v8/issues/detail?id=2896
// Performance: http://jsperf.com/string-fromcharcode-test/13
int batchSize = ArrayHelper.ARRAY_PROCESS_BATCH_SIZE;
String s = "";
for (int batchStart = offset; batchStart < end;) {
int batchEnd = Math.min(batchStart + batchSize, end);
s += fromCharCode(ArrayHelper.unsafeClone(x, batchStart, batchEnd));
batchStart = batchEnd;
}
return s;
}
private static String fromCharCode(Object[] array) {
return getFromCharCodeFunction().apply(null, array);
}
@JsType(isNative = true, name = "Function", namespace = JsPackage.GLOBAL)
private static class NativeFunction {
public native String apply(String thisContext, Object[] argsArray);
}
@JsProperty(name = "String.fromCharCode", namespace = "")
private static native NativeFunction getFromCharCodeFunction();
public static String valueOf(char[] x) {
return valueOf(x, 0, x.length);
}
public static String valueOf(double x) {
return "" + x;
}
public static String valueOf(float x) {
return "" + x;
}
public static String valueOf(int x) {
return "" + x;
}
public static String valueOf(long x) {
return "" + x;
}
// valueOf needs to be treated special:
// J2cl uses it for String concat and thus it can not use string concatenation itself.
public static @JsNonNull String valueOf(Object x) {
return x == null ? "null" : x.toString();
}
/**
* This method converts Java-escaped dollar signs "\$" into JavaScript-escaped
* dollar signs "$$", and removes all other lone backslashes, which serve as
* escapes in Java but are passed through literally in JavaScript.
*
* @skip
*/
private static String translateReplaceString(String replaceStr) {
int pos = 0;
while (0 <= (pos = replaceStr.indexOf("\\", pos))) {
if (replaceStr.charAt(pos + 1) == '$') {
replaceStr = replaceStr.substring(0, pos) + "$"
+ replaceStr.substring(++pos);
} else {
replaceStr = replaceStr.substring(0, pos) + replaceStr.substring(++pos);
}
}
return replaceStr;
}
private static Charset getCharset(String charsetName) throws UnsupportedEncodingException {
try {
return Charset.forName(charsetName);
} catch (UnsupportedCharsetException e) {
throw new UnsupportedEncodingException(charsetName);
}
}
static String fromCodePoint(int codePoint) {
if (codePoint >= Character.MIN_SUPPLEMENTARY_CODE_POINT) {
char hiSurrogate = Character.getHighSurrogate(codePoint);
char loSurrogate = Character.getLowSurrogate(codePoint);
return String.valueOf(hiSurrogate)
+ String.valueOf(loSurrogate);
} else {
return String.valueOf((char) codePoint);
}
}
public String() {
/*
* Call to $create(args) must be here so that the method is referenced and not
* pruned before new String(args) is replaced by $create(args) by
* RewriteConstructorCallsForUnboxedTypes.
*/
$create();
}
public String(byte[] bytes) {
/*
* Call to $create(args) must be here so that the method is referenced and not
* pruned before new String(args) is replaced by $create(args) by
* RewriteConstructorCallsForUnboxedTypes.
*/
$create(bytes);
}
public String(byte[] bytes, int ofs, int len) {
/*
* Call to $create(args) must be here so that the method is referenced and not
* pruned before new String(args) is replaced by $create(args) by
* RewriteConstructorCallsForUnboxedTypes.
*/
$create(bytes, ofs, len);
}
public String(byte[] bytes, int ofs, int len, String charsetName)
throws UnsupportedEncodingException {
/*
* Call to $create(args) must be here so that the method is referenced and not
* pruned before new String(args) is replaced by $create(args) by
* RewriteConstructorCallsForUnboxedTypes.
*/
$create(bytes, ofs, len, charsetName);
}
public String(byte[] bytes, int ofs, int len, Charset charset) {
/*
* Call to $create(args) must be here so that the method is referenced and not
* pruned before new String(args) is replaced by $create(args) by
* RewriteConstructorCallsForUnboxedTypes.
*/
$create(bytes, ofs, len, charset);
}
public String(byte[] bytes, String charsetName)
throws UnsupportedEncodingException {
/*
* Call to $create(args) must be here so that the method is referenced and not
* pruned before new String(args) is replaced by $create(args) by
* RewriteConstructorCallsForUnboxedTypes.
*/
$create(bytes, charsetName);
}
public String(byte[] bytes, Charset charset) {
/*
* Call to $create(args) must be here so that the method is referenced and not
* pruned before new String(args) is replaced by $create(args) by
* RewriteConstructorCallsForUnboxedTypes.
*/
$create(bytes, charset);
}
public String(char value[]) {
/*
* Call to $create(args) must be here so that the method is referenced and not
* pruned before new String(args) is replaced by $create(args) by
* RewriteConstructorCallsForUnboxedTypes.
*/
$create(value);
}
public String(char value[], int offset, int count) {
/*
* Call to $create(args) must be here so that the method is referenced and not
* pruned before new String(args) is replaced by $create(args) by
* RewriteConstructorCallsForUnboxedTypes.
*/
$create(value, offset, count);
}
public String(int codePoints[], int offset, int count) {
/*
* Call to $create(args) must be here so that the method is referenced and not
* pruned before new String(args) is replaced by $create(args) by
* RewriteConstructorCallsForUnboxedTypes.
*/
$create(codePoints, offset, count);
}
public String(String other) {
/*
* Call to $create(args) must be here so that the method is referenced and not
* pruned before new String(args) is replaced by $create(args) by
* RewriteConstructorCallsForUnboxedTypes.
*/
$create(other);
}
public String(StringBuffer sb) {
/*
* Call to $create(args) must be here so that the method is referenced and not
* pruned before new String(args) is replaced by $create(args) by
* RewriteConstructorCallsForUnboxedTypes.
*/
$create(sb);
}
public String(StringBuilder sb) {
/*
* Call to $create(args) must be here so that the method is referenced and not
* pruned before new String(args) is replaced by $create(args) by
* RewriteConstructorCallsForUnboxedTypes.
*/
$create(sb);
}
private NativeString asNativeString() {
return JsUtils.uncheckedCast(this);
}
@Override
public char charAt(int index) {
checkStringElementIndex(index, length());
return asNativeString().charCodeAt(index);
}
public int codePointAt(int index) {
return Character.codePointAt(this, index, length());
}
public int codePointBefore(int index) {
return Character.codePointBefore(this, index, 0);
}
public int codePointCount(int beginIndex, int endIndex) {
return Character.codePointCount(this, beginIndex, endIndex);
}
@Override
public int compareTo(String other) {
// Trick compiler into thinking that these are double so what we could do arithmetic comparison
// which is supported on underlying JavaScript strings.
double a = JsUtils.uncheckedCast(this);
double b = JsUtils.uncheckedCast(other);
return a == b ? 0 : (a < b ? -1 : 1);
}
public int compareToIgnoreCase(String other) {
return toLowerCase().compareTo(other.toLowerCase());
}
public String concat(String str) {
return checkNotNull(this) + checkNotNull(str);
}
public boolean contains(CharSequence s) {
return indexOf(s.toString()) != -1;
}
public boolean contentEquals(CharSequence cs) {
return equals(cs.toString());
}
public boolean contentEquals(StringBuffer sb) {
return equals(sb.toString());
}
public boolean endsWith(String suffix) {
// If IE8 supported negative start index, we could have just used "-suffixlength".
int suffixlength = suffix.length();
return asNativeString().substr(length() - suffixlength, suffixlength).equals(suffix);
}
// Marked with @DoNotInline because we don't have static eval for "==" yet.
@DoNotInline
@Override
public boolean equals(Object other) {
// Java equality is translated into triple equality which is a quick to compare strings for
// equality without any instanceOf checks.
return checkNotNull(this) == other;
}
public boolean equalsIgnoreCase(String other) {
checkNotNull(this);
if (other == null) {
return false;
}
if (equals(other)) {
return true;
}
return length() == other.length() && toLowerCase().equals(other.toLowerCase());
}
public byte[] getBytes() {
// default character set for GWT is UTF-8
return getBytes(EmulatedCharset.UTF_8);
}
public byte[] getBytes(String charsetName) throws UnsupportedEncodingException {
return getBytes(getCharset(charsetName));
}
public byte[] getBytes(Charset charset) {
return ((EmulatedCharset) charset).getBytes(this);
}
public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin) {
checkCriticalStringBounds(srcBegin, srcEnd, length());
checkCriticalStringBounds(dstBegin, dstBegin + (srcEnd - srcBegin), dst.length);
getChars0(srcBegin, srcEnd, dst, dstBegin);
}
private void getChars0(int srcBegin, int srcEnd, char[] dst, int dstBegin) {
while (srcBegin < srcEnd) {
dst[dstBegin++] = charAt(srcBegin++);
}
}
@Override
public int hashCode() {
int h = 0;
for (int i = 0; i < length(); i++) {
// Following is the common hash function '(31 * h + x)' as '(x << 5) - x' equal to '31 * x'.
h = Coercions.ensureInt((h << 5) - h + charAt(i));
}
return h;
}
public int indexOf(int codePoint) {
return indexOf(fromCodePoint(codePoint));
}
public int indexOf(int codePoint, int startIndex) {
return indexOf(fromCodePoint(codePoint), startIndex);
}
public int indexOf(String str) {
return asNativeString().indexOf(str);
}
public int indexOf(String str, int startIndex) {
return asNativeString().indexOf(str, startIndex);
}
public String intern() {
return checkNotNull(this);
}
public boolean isEmpty() {
return length() == 0;
}
public int lastIndexOf(int codePoint) {
return lastIndexOf(fromCodePoint(codePoint));
}
public int lastIndexOf(int codePoint, int startIndex) {
return lastIndexOf(fromCodePoint(codePoint), startIndex);
}
public int lastIndexOf(String str) {
return asNativeString().lastIndexOf(str);
}
public int lastIndexOf(String str, int start) {
return asNativeString().lastIndexOf(str, start);
}
@Override
public int length() {
return asNativeString().length;
}
/**
* Regular expressions vary from the standard implementation. The
* regex
parameter is interpreted by JavaScript as a JavaScript
* regular expression. For consistency, use only the subset of regular
* expression syntax common to both Java and JavaScript.
*
* TODO(jat): properly handle Java regex syntax
*/
public boolean matches(String regex) {
// We surround the regex with '^' and '$' because it must match the entire string.
return new NativeRegExp("^(" + regex + ")$").test(this);
}
public int offsetByCodePoints(int index, int codePointOffset) {
return Character.offsetByCodePoints(this, index, codePointOffset);
}
public boolean regionMatches(boolean ignoreCase, int toffset, String other,
int ooffset, int len) {
checkNotNull(other);
if (toffset < 0 || ooffset < 0) {
return false;
}
if (toffset + len > length() || ooffset + len > other.length()) {
return false;
}
if (len <= 0) {
return true;
}
String left = asNativeString().substr(toffset, len);
String right = other.asNativeString().substr(ooffset, len);
return ignoreCase ? left.equalsIgnoreCase(right) : left.equals(right);
}
public boolean regionMatches(int toffset, String other, int ooffset, int len) {
return regionMatches(false, toffset, other, ooffset, len);
}
public String replace(char from, char to) {
// Translate 'from' into unicode escape sequence (\\u and a four-digit hexadecimal number).
// Escape sequence replacement is used instead of a string literal replacement
// in order to escape regexp special characters (e.g. '.').
String hex = Integer.toHexString(from);
String regex = "\\u" + "0000".substring(hex.length()) + hex;
String replace = NativeString.fromCharCode(to);
return nativeReplaceAll(regex, replace);
}
public String replace(CharSequence from, CharSequence to) {
// Implementation note: This uses a regex replacement instead of
// a string literal replacement because Safari does not
// follow the spec for "$$" in the replacement string: it
// will insert a literal "$$". IE and Firefox, meanwhile,
// treat "$$" as "$".
// Escape regex special characters from literal replacement string.
String regex =
from.toString().replaceAll("([/\\\\\\.\\*\\+\\?\\|\\(\\)\\[\\]\\{\\}$^])", "\\\\$1");
// Escape $ since it is for match backrefs and \ since it is used to escape
// $.
String replacement = to.toString().replaceAll("\\\\", "\\\\\\\\").replaceAll("\\$", "\\\\$");
return replaceAll(regex, replacement);
}
/**
* Regular expressions vary from the standard implementation. The
* regex
parameter is interpreted by JavaScript as a JavaScript
* regular expression. For consistency, use only the subset of regular
* expression syntax common to both Java and JavaScript.
*
* TODO(jat): properly handle Java regex syntax
*/
public String replaceAll(String regex, String replace) {
replace = translateReplaceString(replace);
return nativeReplaceAll(regex, replace);
}
String nativeReplaceAll(String regex, String replace) {
return asNativeString().replace(new NativeRegExp(regex, "g"), replace);
}
/**
* Regular expressions vary from the standard implementation. The
* regex
parameter is interpreted by JavaScript as a JavaScript
* regular expression. For consistency, use only the subset of regular
* expression syntax common to both Java and JavaScript.
*
* TODO(jat): properly handle Java regex syntax
*/
public String replaceFirst(String regex, String replace) {
replace = translateReplaceString(replace);
NativeRegExp jsRegEx = new NativeRegExp(regex);
return asNativeString().replace(jsRegEx, replace);
}
/**
* Regular expressions vary from the standard implementation. The
* regex
parameter is interpreted by JavaScript as a JavaScript
* regular expression. For consistency, use only the subset of regular
* expression syntax common to both Java and JavaScript.
*/
public String[] split(String regex) {
return split(regex, 0);
}
/**
* Regular expressions vary from the standard implementation. The
* regex
parameter is interpreted by JavaScript as a JavaScript
* regular expression. For consistency, use only the subset of regular
* expression syntax common to both Java and JavaScript.
*
* TODO(jat): properly handle Java regex syntax
*/
public String[] split(String regex, int maxMatch) {
// The compiled regular expression created from the string
NativeRegExp compiled = new NativeRegExp(regex, "g");
// the Javascipt array to hold the matches prior to conversion
String[] out = new String[0];
// how many matches performed so far
int count = 0;
// The current string that is being matched; trimmed as each piece matches
String trail = this;
// used to detect repeated zero length matches
// Must be null to start with because the first match of "" makes no
// progress by intention
String lastTrail = null;
// We do the split manually to avoid Javascript incompatibility
while (true) {
// None of the information in the match returned are useful as we have no
// subgroup handling
NativeRegExp.Match matchObj = compiled.exec(trail);
if (matchObj == null || trail == "" || (count == (maxMatch - 1) && maxMatch > 0)) {
out[count] = trail;
break;
} else {
int matchIndex = matchObj.getIndex();
out[count] = trail.substring(0, matchIndex);
trail = trail.substring(matchIndex + matchObj.asArray()[0].length(), trail.length());
// Force the compiled pattern to reset internal state
compiled.lastIndex = 0;
// Only one zero length match per character to ensure termination
if (lastTrail == trail) {
out[count] = trail.substring(0, 1);
trail = trail.substring(1);
}
lastTrail = trail;
count++;
}
}
// all blank delimiters at the end are supposed to disappear if maxMatch == 0;
// however, if the input string is empty, the output should consist of a
// single empty string
if (maxMatch == 0 && this.length() > 0) {
int lastNonEmpty = out.length;
while (lastNonEmpty > 0 && out[lastNonEmpty - 1] == "") {
--lastNonEmpty;
}
if (lastNonEmpty < out.length) {
ArrayHelper.setLength(out, lastNonEmpty);
}
}
return out;
}
public boolean startsWith(String prefix) {
return startsWith(prefix, 0);
}
public boolean startsWith(String prefix, int toffset) {
return toffset >= 0 && asNativeString().substr(toffset, prefix.length()).equals(prefix);
}
@Override
public CharSequence subSequence(int beginIndex, int endIndex) {
return substring(beginIndex, endIndex);
}
public String substring(int beginIndex) {
checkStringElementIndex(beginIndex, length() + 1);
return asNativeString().substr(beginIndex);
}
public String substring(int beginIndex, int endIndex) {
checkStringBounds(beginIndex, endIndex, length());
return asNativeString().substr(beginIndex, endIndex - beginIndex);
}
public char[] toCharArray() {
int n = length();
char[] charArr = new char[n];
getChars0(0, n, charArr, 0);
return charArr;
}
/**
* Transforms the String to lower-case in a locale insensitive way.
*
* Unlike JRE, we don't do locale specific transformation by default. That is backward compatible
* for GWT and in most of the cases that is what the developer actually wants. If you want to make
* a transformation based on native locale of the browser, you can do
* {@code toLowerCase(Locale.getDefault())} instead.
*/
public String toLowerCase() {
return asNativeString().toLowerCase();
}
/**
* If provided {@code locale} is {@link Locale#getDefault()}, uses javascript's
* {@code toLocaleLowerCase} to do a locale specific transformation. Otherwise, it will fallback
* to {@code toLowerCase} which performs the right thing for the limited set of Locale's
* predefined in GWT Locale emulation.
*/
public String toLowerCase(Locale locale) {
return locale == Locale.getDefault()
? asNativeString().toLocaleLowerCase() : asNativeString().toLowerCase();
}
// See the notes in lowerCase pair.
public String toUpperCase() {
return asNativeString().toUpperCase();
}
// See the notes in lowerCase pair.
public String toUpperCase(Locale locale) {
return locale == Locale.getDefault()
? asNativeString().toLocaleUpperCase() : asNativeString().toUpperCase();
}
@Override
public String toString() {
return checkNotNull(this);
}
public String trim() {
int length = length();
int start = 0;
while (start < length && charAt(start) <= ' ') {
start++;
}
int end = length;
while (end > start && charAt(end - 1) <= ' ') {
end--;
}
return start > 0 || end < length ? substring(start, end) : this;
}
@JsType(isNative = true, name = "String", namespace = "")
private static class NativeString {
public static native String fromCharCode(char x);
public int length;
public native char charCodeAt(int index);
public native int indexOf(String str);
public native int indexOf(String str, int startIndex);
public native int lastIndexOf(String str);
public native int lastIndexOf(String str, int start);
public native String replace(NativeRegExp regex, String replace);
public native String substr(int beginIndex);
public native String substr(int beginIndex, int len);
public native String toLocaleLowerCase();
public native String toLocaleUpperCase();
public native String toLowerCase();
public native String toUpperCase();
}
// CHECKSTYLE_OFF: Utility Methods for unboxed String.
protected static String $create() {
return "";
}
protected static String $create(byte[] bytes) {
return $create(bytes, 0, bytes.length);
}
protected static String $create(byte[] bytes, int ofs, int len) {
return $create(bytes, ofs, len, EmulatedCharset.UTF_8);
}
protected static String $create(byte[] bytes, int ofs, int len, String charsetName)
throws UnsupportedEncodingException {
return $create(bytes, ofs, len, String.getCharset(charsetName));
}
protected static String $create(byte[] bytes, int ofs, int len, Charset charset) {
return String.valueOf(((EmulatedCharset) charset).decodeString(bytes, ofs, len));
}
protected static String $create(byte[] bytes, String charsetName)
throws UnsupportedEncodingException {
return $create(bytes, 0, bytes.length, charsetName);
}
protected static String $create(byte[] bytes, Charset charset) {
return $create(bytes, 0, bytes.length, charset);
}
protected static String $create(char value[]) {
return String.valueOf(value);
}
protected static String $create(char value[], int offset, int count) {
return String.valueOf(value, offset, count);
}
protected static String $create(int[] codePoints, int offset, int count) {
char[] chars = new char[count * 2];
int charIdx = 0;
while (count-- > 0) {
charIdx += Character.toChars(codePoints[offset++], chars, charIdx);
}
return String.valueOf(chars, 0, charIdx);
}
protected static String $create(String other) {
return checkNotNull(other);
}
protected static String $create(StringBuffer sb) {
return sb.toString();
}
protected static String $create(StringBuilder sb) {
return sb.toString();
}
@JsMethod
protected static boolean $isInstance(Object instance) {
return "string".equals(JsUtils.typeOf(instance));
}
// CHECKSTYLE_ON: end utility methods
}