mirror of
https://github.com/openjdk/jdk.git
synced 2025-08-27 14:54:52 +02:00
8285932: Implementation of JEP 430 String Templates (Preview)
Reviewed-by: mcimadamore, rriggs, darcy
This commit is contained in:
parent
da2c930262
commit
4aa65cbeef
74 changed files with 9309 additions and 99 deletions
283
src/java.base/share/classes/java/util/Digits.java
Normal file
283
src/java.base/share/classes/java/util/Digits.java
Normal file
|
@ -0,0 +1,283 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License version 2 only, as
|
||||
* published by the Free Software Foundation. Oracle designates this
|
||||
* particular file as subject to the "Classpath" exception as provided
|
||||
* by Oracle in the LICENSE file that accompanied this code.
|
||||
*
|
||||
* This code is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* version 2 for more details (a copy is included in the LICENSE file that
|
||||
* accompanied this code).
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License version
|
||||
* 2 along with this work; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
||||
* or visit www.oracle.com if you need additional information or have any
|
||||
* questions.
|
||||
*/
|
||||
|
||||
package java.util;
|
||||
|
||||
import java.lang.invoke.MethodHandle;
|
||||
|
||||
import jdk.internal.vm.annotation.Stable;
|
||||
|
||||
/**
|
||||
* Digits provides a fast methodology for converting integers and longs to
|
||||
* ASCII strings.
|
||||
*
|
||||
* @since 21
|
||||
*/
|
||||
sealed interface Digits permits Digits.DecimalDigits, Digits.HexDigits, Digits.OctalDigits {
|
||||
/**
|
||||
* Insert digits for long value in buffer from high index to low index.
|
||||
*
|
||||
* @param value value to convert
|
||||
* @param buffer byte buffer to copy into
|
||||
* @param index insert point + 1
|
||||
* @param putCharMH method to put character
|
||||
*
|
||||
* @return the last index used
|
||||
*
|
||||
* @throws Throwable if putCharMH fails (unusual).
|
||||
*/
|
||||
int digits(long value, byte[] buffer, int index,
|
||||
MethodHandle putCharMH) throws Throwable;
|
||||
|
||||
/**
|
||||
* Calculate the number of digits required to represent the long.
|
||||
*
|
||||
* @param value value to convert
|
||||
*
|
||||
* @return number of digits
|
||||
*/
|
||||
int size(long value);
|
||||
|
||||
/**
|
||||
* Digits class for decimal digits.
|
||||
*/
|
||||
final class DecimalDigits implements Digits {
|
||||
@Stable
|
||||
private static final short[] DIGITS;
|
||||
|
||||
/**
|
||||
* Singleton instance of DecimalDigits.
|
||||
*/
|
||||
static final Digits INSTANCE = new DecimalDigits();
|
||||
|
||||
static {
|
||||
short[] digits = new short[10 * 10];
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
short hi = (short) ((i + '0') << 8);
|
||||
|
||||
for (int j = 0; j < 10; j++) {
|
||||
short lo = (short) (j + '0');
|
||||
digits[i * 10 + j] = (short) (hi | lo);
|
||||
}
|
||||
}
|
||||
|
||||
DIGITS = digits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
private DecimalDigits() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int digits(long value, byte[] buffer, int index,
|
||||
MethodHandle putCharMH) throws Throwable {
|
||||
boolean negative = value < 0;
|
||||
if (!negative) {
|
||||
value = -value;
|
||||
}
|
||||
|
||||
long q;
|
||||
int r;
|
||||
while (value <= Integer.MIN_VALUE) {
|
||||
q = value / 100;
|
||||
r = (int)((q * 100) - value);
|
||||
value = q;
|
||||
int digits = DIGITS[r];
|
||||
|
||||
putCharMH.invokeExact(buffer, --index, digits & 0xFF);
|
||||
putCharMH.invokeExact(buffer, --index, digits >> 8);
|
||||
}
|
||||
|
||||
int iq, ivalue = (int)value;
|
||||
while (ivalue <= -100) {
|
||||
iq = ivalue / 100;
|
||||
r = (iq * 100) - ivalue;
|
||||
ivalue = iq;
|
||||
int digits = DIGITS[r];
|
||||
putCharMH.invokeExact(buffer, --index, digits & 0xFF);
|
||||
putCharMH.invokeExact(buffer, --index, digits >> 8);
|
||||
}
|
||||
|
||||
if (ivalue < 0) {
|
||||
ivalue = -ivalue;
|
||||
}
|
||||
|
||||
int digits = DIGITS[ivalue];
|
||||
putCharMH.invokeExact(buffer, --index, digits & 0xFF);
|
||||
|
||||
if (9 < ivalue) {
|
||||
putCharMH.invokeExact(buffer, --index, digits >> 8);
|
||||
}
|
||||
|
||||
if (negative) {
|
||||
putCharMH.invokeExact(buffer, --index, (int)'-');
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size(long value) {
|
||||
boolean negative = value < 0;
|
||||
int sign = negative ? 1 : 0;
|
||||
|
||||
if (!negative) {
|
||||
value = -value;
|
||||
}
|
||||
|
||||
long precision = -10;
|
||||
for (int i = 1; i < 19; i++) {
|
||||
if (value > precision)
|
||||
return i + sign;
|
||||
|
||||
precision = 10 * precision;
|
||||
}
|
||||
|
||||
return 19 + sign;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Digits class for hexadecimal digits.
|
||||
*/
|
||||
final class HexDigits implements Digits {
|
||||
@Stable
|
||||
private static final short[] DIGITS;
|
||||
|
||||
/**
|
||||
* Singleton instance of HexDigits.
|
||||
*/
|
||||
static final Digits INSTANCE = new HexDigits();
|
||||
|
||||
static {
|
||||
short[] digits = new short[16 * 16];
|
||||
|
||||
for (int i = 0; i < 16; i++) {
|
||||
short hi = (short) ((i < 10 ? i + '0' : i - 10 + 'a') << 8);
|
||||
|
||||
for (int j = 0; j < 16; j++) {
|
||||
short lo = (short) (j < 10 ? j + '0' : j - 10 + 'a');
|
||||
digits[(i << 4) + j] = (short) (hi | lo);
|
||||
}
|
||||
}
|
||||
|
||||
DIGITS = digits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
private HexDigits() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int digits(long value, byte[] buffer, int index,
|
||||
MethodHandle putCharMH) throws Throwable {
|
||||
while ((value & ~0xFF) != 0) {
|
||||
int digits = DIGITS[(int) (value & 0xFF)];
|
||||
value >>>= 8;
|
||||
putCharMH.invokeExact(buffer, --index, digits & 0xFF);
|
||||
putCharMH.invokeExact(buffer, --index, digits >> 8);
|
||||
}
|
||||
|
||||
int digits = DIGITS[(int) (value & 0xFF)];
|
||||
putCharMH.invokeExact(buffer, --index, digits & 0xFF);
|
||||
|
||||
if (0xF < value) {
|
||||
putCharMH.invokeExact(buffer, --index, digits >> 8);
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size(long value) {
|
||||
return value == 0 ? 1 :
|
||||
67 - Long.numberOfLeadingZeros(value) >> 2;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Digits class for octal digits.
|
||||
*/
|
||||
final class OctalDigits implements Digits {
|
||||
@Stable
|
||||
private static final short[] DIGITS;
|
||||
|
||||
/**
|
||||
* Singleton instance of OctalDigits.
|
||||
*/
|
||||
static final Digits INSTANCE = new OctalDigits();
|
||||
|
||||
static {
|
||||
short[] digits = new short[8 * 8];
|
||||
|
||||
for (int i = 0; i < 8; i++) {
|
||||
short hi = (short) ((i + '0') << 8);
|
||||
|
||||
for (int j = 0; j < 8; j++) {
|
||||
short lo = (short) (j + '0');
|
||||
digits[(i << 3) + j] = (short) (hi | lo);
|
||||
}
|
||||
}
|
||||
|
||||
DIGITS = digits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
private OctalDigits() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int digits(long value, byte[] buffer, int index,
|
||||
MethodHandle putCharMH) throws Throwable {
|
||||
while ((value & ~0x3F) != 0) {
|
||||
int digits = DIGITS[(int) (value & 0x3F)];
|
||||
value >>>= 6;
|
||||
putCharMH.invokeExact(buffer, --index, digits & 0xFF);
|
||||
putCharMH.invokeExact(buffer, --index, digits >> 8);
|
||||
}
|
||||
|
||||
int digits = DIGITS[(int) (value & 0x3F)];
|
||||
putCharMH.invokeExact(buffer, --index, digits & 0xFF);
|
||||
|
||||
if (7 < value) {
|
||||
putCharMH.invokeExact(buffer, --index, digits >> 8);
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size(long value) {
|
||||
return (66 - Long.numberOfLeadingZeros(value)) / 3;
|
||||
}
|
||||
}
|
||||
}
|
539
src/java.base/share/classes/java/util/FormatItem.java
Normal file
539
src/java.base/share/classes/java/util/FormatItem.java
Normal file
|
@ -0,0 +1,539 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License version 2 only, as
|
||||
* published by the Free Software Foundation. Oracle designates this
|
||||
* particular file as subject to the "Classpath" exception as provided
|
||||
* by Oracle in the LICENSE file that accompanied this code.
|
||||
*
|
||||
* This code is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* version 2 for more details (a copy is included in the LICENSE file that
|
||||
* accompanied this code).
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License version
|
||||
* 2 along with this work; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
||||
* or visit www.oracle.com if you need additional information or have any
|
||||
* questions.
|
||||
*/
|
||||
|
||||
package java.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.invoke.*;
|
||||
import java.lang.invoke.MethodHandles.Lookup;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.DecimalFormatSymbols;
|
||||
import java.util.Digits.*;
|
||||
import java.util.Formatter.FormatSpecifier;
|
||||
|
||||
import jdk.internal.access.JavaLangAccess;
|
||||
import jdk.internal.access.SharedSecrets;
|
||||
import jdk.internal.util.FormatConcatItem;
|
||||
|
||||
import static java.lang.invoke.MethodType.methodType;
|
||||
|
||||
/**
|
||||
* A specialized objects used by FormatterBuilder that knows how to insert
|
||||
* themselves into a concatenation performed by StringConcatFactory.
|
||||
*
|
||||
* @since 21
|
||||
*
|
||||
* Warning: This class is part of PreviewFeature.Feature.STRING_TEMPLATES.
|
||||
* Do not rely on its availability.
|
||||
*/
|
||||
class FormatItem {
|
||||
private static final JavaLangAccess JLA = SharedSecrets.getJavaLangAccess();
|
||||
|
||||
private static final MethodHandle CHAR_MIX =
|
||||
JLA.stringConcatHelper("mix",
|
||||
MethodType.methodType(long.class, long.class,char.class));
|
||||
|
||||
private static final MethodHandle STRING_PREPEND =
|
||||
JLA.stringConcatHelper("prepend",
|
||||
MethodType.methodType(long.class, long.class, byte[].class,
|
||||
String.class, String.class));
|
||||
|
||||
private static final MethodHandle SELECT_GETCHAR_MH =
|
||||
JLA.stringConcatHelper("selectGetChar",
|
||||
MethodType.methodType(MethodHandle.class, long.class));
|
||||
|
||||
private static final MethodHandle SELECT_PUTCHAR_MH =
|
||||
JLA.stringConcatHelper("selectPutChar",
|
||||
MethodType.methodType(MethodHandle.class, long.class));
|
||||
|
||||
private static long charMix(long lengthCoder, char value) {
|
||||
try {
|
||||
return (long)CHAR_MIX.invokeExact(lengthCoder, value);
|
||||
} catch (Error | RuntimeException ex) {
|
||||
throw ex;
|
||||
} catch (Throwable ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static long stringMix(long lengthCoder, String value) {
|
||||
return JLA.stringConcatMix(lengthCoder, value);
|
||||
}
|
||||
|
||||
private static long stringPrepend(long lengthCoder, byte[] buffer,
|
||||
String value) throws Throwable {
|
||||
return (long)STRING_PREPEND.invokeExact(lengthCoder, buffer, value,
|
||||
(String)null);
|
||||
}
|
||||
|
||||
private static MethodHandle selectGetChar(long indexCoder) throws Throwable {
|
||||
return (MethodHandle)SELECT_GETCHAR_MH.invokeExact(indexCoder);
|
||||
}
|
||||
|
||||
private static MethodHandle selectPutChar(long indexCoder) throws Throwable {
|
||||
return (MethodHandle)SELECT_PUTCHAR_MH.invokeExact(indexCoder);
|
||||
}
|
||||
|
||||
private static final MethodHandle PUT_CHAR_DIGIT;
|
||||
|
||||
static {
|
||||
try {
|
||||
Lookup lookup = MethodHandles.lookup();
|
||||
PUT_CHAR_DIGIT = lookup.findStatic(FormatItem.class, "putByte",
|
||||
MethodType.methodType(void.class,
|
||||
byte[].class, int.class, int.class));
|
||||
} catch (ReflectiveOperationException ex) {
|
||||
throw new AssertionError("putByte lookup failed", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void putByte(byte[] buffer, int index, int ch) {
|
||||
buffer[index] = (byte)ch;
|
||||
}
|
||||
|
||||
private FormatItem() {
|
||||
throw new AssertionError("private constructor");
|
||||
}
|
||||
|
||||
/**
|
||||
* Decimal value format item.
|
||||
*/
|
||||
static final class FormatItemDecimal implements FormatConcatItem {
|
||||
private final char groupingSeparator;
|
||||
private final char zeroDigit;
|
||||
private final char minusSign;
|
||||
private final int digitOffset;
|
||||
private final byte[] digits;
|
||||
private final int length;
|
||||
private final boolean isNegative;
|
||||
private final int width;
|
||||
private final byte prefixSign;
|
||||
private final int groupSize;
|
||||
private final long value;
|
||||
private final boolean parentheses;
|
||||
|
||||
FormatItemDecimal(DecimalFormatSymbols dfs, int width, char sign,
|
||||
boolean parentheses, int groupSize, long value) throws Throwable {
|
||||
this.groupingSeparator = dfs.getGroupingSeparator();
|
||||
this.zeroDigit = dfs.getZeroDigit();
|
||||
this.minusSign = dfs.getMinusSign();
|
||||
this.digitOffset = this.zeroDigit - '0';
|
||||
int length = DecimalDigits.INSTANCE.size(value);
|
||||
this.digits = new byte[length];
|
||||
DecimalDigits.INSTANCE.digits(value, this.digits, length, PUT_CHAR_DIGIT);
|
||||
this.isNegative = value < 0L;
|
||||
this.length = this.isNegative ? length - 1 : length;
|
||||
this.width = width;
|
||||
this.groupSize = groupSize;
|
||||
this.value = value;
|
||||
this.parentheses = parentheses && isNegative;
|
||||
this.prefixSign = (byte)(isNegative ? (parentheses ? '\0' : minusSign) : sign);
|
||||
}
|
||||
|
||||
private int signLength() {
|
||||
return (prefixSign != '\0' ? 1 : 0) + (parentheses ? 2 : 0);
|
||||
}
|
||||
|
||||
private int groupLength() {
|
||||
return 0 < groupSize ? (length - 1) / groupSize : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long mix(long lengthCoder) {
|
||||
return JLA.stringConcatCoder(zeroDigit) |
|
||||
(lengthCoder +
|
||||
Integer.max(length + signLength() + groupLength(), width));
|
||||
}
|
||||
|
||||
@Override
|
||||
public long prepend(long lengthCoder, byte[] buffer) throws Throwable {
|
||||
MethodHandle putCharMH = selectPutChar(lengthCoder);
|
||||
|
||||
if (parentheses) {
|
||||
putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)')');
|
||||
}
|
||||
|
||||
if (0 < groupSize) {
|
||||
int groupIndex = groupSize;
|
||||
|
||||
for (int i = 1; i <= length; i++) {
|
||||
if (groupIndex-- == 0) {
|
||||
putCharMH.invokeExact(buffer, (int)--lengthCoder,
|
||||
(int)groupingSeparator);
|
||||
groupIndex = groupSize - 1;
|
||||
}
|
||||
|
||||
putCharMH.invokeExact(buffer, (int)--lengthCoder,
|
||||
digits[digits.length - i] + digitOffset);
|
||||
}
|
||||
} else {
|
||||
for (int i = 1; i <= length; i++) {
|
||||
putCharMH.invokeExact(buffer, (int)--lengthCoder,
|
||||
digits[digits.length - i] + digitOffset);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = length + signLength() + groupLength(); i < width; i++) {
|
||||
putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)'0');
|
||||
}
|
||||
|
||||
if (parentheses) {
|
||||
putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)'(');
|
||||
}
|
||||
if (prefixSign != '\0') {
|
||||
putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)prefixSign);
|
||||
}
|
||||
|
||||
return lengthCoder;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hexadecimal format item.
|
||||
*/
|
||||
static final class FormatItemHexadecimal implements FormatConcatItem {
|
||||
private final int width;
|
||||
private final boolean hasPrefix;
|
||||
private final long value;
|
||||
private final int length;
|
||||
|
||||
FormatItemHexadecimal(int width, boolean hasPrefix, long value) {
|
||||
this.width = width;
|
||||
this.hasPrefix = hasPrefix;
|
||||
this.value = value;
|
||||
this.length = HexDigits.INSTANCE.size(value);
|
||||
}
|
||||
|
||||
private int prefixLength() {
|
||||
return hasPrefix ? 2 : 0;
|
||||
}
|
||||
|
||||
private int zeroesLength() {
|
||||
return Integer.max(0, width - length - prefixLength());
|
||||
}
|
||||
|
||||
@Override
|
||||
public long mix(long lengthCoder) {
|
||||
return lengthCoder + length + prefixLength() + zeroesLength();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long prepend(long lengthCoder, byte[] buffer) throws Throwable {
|
||||
MethodHandle putCharMH = selectPutChar(lengthCoder);
|
||||
HexDigits.INSTANCE.digits(value, buffer, (int)lengthCoder, putCharMH);
|
||||
lengthCoder -= length;
|
||||
|
||||
for (int i = 0; i < zeroesLength(); i++) {
|
||||
putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)'0');
|
||||
}
|
||||
|
||||
if (hasPrefix) {
|
||||
putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)'x');
|
||||
putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)'0');
|
||||
}
|
||||
|
||||
return lengthCoder;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hexadecimal format item.
|
||||
*/
|
||||
static final class FormatItemOctal implements FormatConcatItem {
|
||||
private final int width;
|
||||
private final boolean hasPrefix;
|
||||
private final long value;
|
||||
private final int length;
|
||||
|
||||
FormatItemOctal(int width, boolean hasPrefix, long value) {
|
||||
this.width = width;
|
||||
this.hasPrefix = hasPrefix;
|
||||
this.value = value;
|
||||
this.length = OctalDigits.INSTANCE.size(value);
|
||||
}
|
||||
|
||||
private int prefixLength() {
|
||||
return hasPrefix && value != 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
private int zeroesLength() {
|
||||
return Integer.max(0, width - length - prefixLength());
|
||||
}
|
||||
|
||||
@Override
|
||||
public long mix(long lengthCoder) {
|
||||
return lengthCoder + length + prefixLength() + zeroesLength();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long prepend(long lengthCoder, byte[] buffer) throws Throwable {
|
||||
MethodHandle putCharMH = selectPutChar(lengthCoder);
|
||||
OctalDigits.INSTANCE.digits(value, buffer, (int)lengthCoder, putCharMH);
|
||||
lengthCoder -= length;
|
||||
|
||||
for (int i = 0; i < zeroesLength(); i++) {
|
||||
putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)'0');
|
||||
}
|
||||
|
||||
if (hasPrefix && value != 0) {
|
||||
putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)'0');
|
||||
}
|
||||
|
||||
return lengthCoder;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Boolean format item.
|
||||
*/
|
||||
static final class FormatItemBoolean implements FormatConcatItem {
|
||||
private final boolean value;
|
||||
|
||||
FormatItemBoolean(boolean value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long mix(long lengthCoder) {
|
||||
return lengthCoder + (value ? "true".length() : "false".length());
|
||||
}
|
||||
|
||||
@Override
|
||||
public long prepend(long lengthCoder, byte[] buffer) throws Throwable {
|
||||
MethodHandle putCharMH = selectPutChar(lengthCoder);
|
||||
|
||||
if (value) {
|
||||
putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)'e');
|
||||
putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)'u');
|
||||
putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)'r');
|
||||
putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)'t');
|
||||
} else {
|
||||
putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)'e');
|
||||
putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)'s');
|
||||
putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)'l');
|
||||
putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)'a');
|
||||
putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)'f');
|
||||
}
|
||||
|
||||
return lengthCoder;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Character format item.
|
||||
*/
|
||||
static final class FormatItemCharacter implements FormatConcatItem {
|
||||
private final char value;
|
||||
|
||||
FormatItemCharacter(char value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long mix(long lengthCoder) {
|
||||
return charMix(lengthCoder, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long prepend(long lengthCoder, byte[] buffer) throws Throwable {
|
||||
MethodHandle putCharMH = selectPutChar(lengthCoder);
|
||||
putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)value);
|
||||
|
||||
return lengthCoder;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* String format item.
|
||||
*/
|
||||
static final class FormatItemString implements FormatConcatItem {
|
||||
private String value;
|
||||
|
||||
FormatItemString(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long mix(long lengthCoder) {
|
||||
return stringMix(lengthCoder, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long prepend(long lengthCoder, byte[] buffer) throws Throwable {
|
||||
return stringPrepend(lengthCoder, buffer, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* FormatSpecifier format item.
|
||||
*/
|
||||
static final class FormatItemFormatSpecifier implements FormatConcatItem {
|
||||
private StringBuilder sb;
|
||||
|
||||
FormatItemFormatSpecifier(FormatSpecifier fs, Locale locale, Object value) {
|
||||
this.sb = new StringBuilder(64);
|
||||
Formatter formatter = new Formatter(this.sb, locale);
|
||||
|
||||
try {
|
||||
fs.print(formatter, value, locale);
|
||||
} catch (IOException ex) {
|
||||
throw new AssertionError("FormatItemFormatSpecifier IOException", ex);
|
||||
}
|
||||
}
|
||||
|
||||
FormatItemFormatSpecifier(Locale locale,
|
||||
int flags, int width, int precision,
|
||||
Formattable formattable) {
|
||||
this.sb = new StringBuilder(64);
|
||||
Formatter formatter = new Formatter(this.sb, locale);
|
||||
formattable.formatTo(formatter, flags, width, precision);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long mix(long lengthCoder) {
|
||||
return JLA.stringBuilderConcatMix(lengthCoder, sb);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long prepend(long lengthCoder, byte[] buffer) throws Throwable {
|
||||
return JLA.stringBuilderConcatPrepend(lengthCoder, buffer, sb);
|
||||
}
|
||||
}
|
||||
|
||||
protected static abstract sealed class FormatItemModifier implements FormatConcatItem
|
||||
permits FormatItemFillLeft,
|
||||
FormatItemFillRight
|
||||
{
|
||||
private final long itemLengthCoder;
|
||||
protected final FormatConcatItem item;
|
||||
|
||||
FormatItemModifier(FormatConcatItem item) {
|
||||
this.itemLengthCoder = item.mix(0L);
|
||||
this.item = item;
|
||||
}
|
||||
|
||||
int length() {
|
||||
return (int)itemLengthCoder;
|
||||
}
|
||||
|
||||
long coder() {
|
||||
return itemLengthCoder & ~Integer.MAX_VALUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public abstract long mix(long lengthCoder);
|
||||
|
||||
@Override
|
||||
public abstract long prepend(long lengthCoder, byte[] buffer) throws Throwable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill left format item.
|
||||
*/
|
||||
static final class FormatItemFillLeft extends FormatItemModifier
|
||||
implements FormatConcatItem {
|
||||
private final int width;
|
||||
|
||||
FormatItemFillLeft(int width, FormatConcatItem item) {
|
||||
super(item);
|
||||
this.width = Integer.max(length(), width);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long mix(long lengthCoder) {
|
||||
return (lengthCoder | coder()) + width;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long prepend(long lengthCoder, byte[] buffer) throws Throwable {
|
||||
MethodHandle putCharMH = selectPutChar(lengthCoder);
|
||||
lengthCoder = item.prepend(lengthCoder, buffer);
|
||||
|
||||
for (int i = length(); i < width; i++) {
|
||||
putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)' ');
|
||||
}
|
||||
|
||||
return lengthCoder;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill right format item.
|
||||
*/
|
||||
static final class FormatItemFillRight extends FormatItemModifier
|
||||
implements FormatConcatItem {
|
||||
private final int width;
|
||||
|
||||
FormatItemFillRight(int width, FormatConcatItem item) {
|
||||
super(item);
|
||||
this.width = Integer.max(length(), width);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long mix(long lengthCoder) {
|
||||
return (lengthCoder | coder()) + width;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long prepend(long lengthCoder, byte[] buffer) throws Throwable {
|
||||
MethodHandle putCharMH = selectPutChar(lengthCoder);
|
||||
|
||||
for (int i = length(); i < width; i++) {
|
||||
putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)' ');
|
||||
}
|
||||
|
||||
lengthCoder = item.prepend(lengthCoder, buffer);
|
||||
|
||||
return lengthCoder;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Null format item.
|
||||
*/
|
||||
static final class FormatItemNull implements FormatConcatItem {
|
||||
FormatItemNull() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public long mix(long lengthCoder) {
|
||||
return lengthCoder + "null".length();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long prepend(long lengthCoder, byte[] buffer) throws Throwable {
|
||||
MethodHandle putCharMH = selectPutChar(lengthCoder);
|
||||
|
||||
putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)'l');
|
||||
putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)'l');
|
||||
putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)'u');
|
||||
putCharMH.invokeExact(buffer, (int)--lengthCoder, (int)'n');
|
||||
|
||||
return lengthCoder;
|
||||
}
|
||||
}
|
||||
}
|
286
src/java.base/share/classes/java/util/FormatProcessor.java
Normal file
286
src/java.base/share/classes/java/util/FormatProcessor.java
Normal file
|
@ -0,0 +1,286 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License version 2 only, as
|
||||
* published by the Free Software Foundation. Oracle designates this
|
||||
* particular file as subject to the "Classpath" exception as provided
|
||||
* by Oracle in the LICENSE file that accompanied this code.
|
||||
*
|
||||
* This code is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* version 2 for more details (a copy is included in the LICENSE file that
|
||||
* accompanied this code).
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License version
|
||||
* 2 along with this work; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
||||
* or visit www.oracle.com if you need additional information or have any
|
||||
* questions.
|
||||
*/
|
||||
|
||||
package java.util;
|
||||
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.lang.StringTemplate.Processor;
|
||||
import java.lang.StringTemplate.Processor.Linkage;
|
||||
import java.util.regex.Matcher;
|
||||
|
||||
import jdk.internal.javac.PreviewFeature;
|
||||
|
||||
/**
|
||||
* This {@link Processor} constructs a {@link String} result using
|
||||
* {@link Formatter} specifications and values found in the {@link StringTemplate}.
|
||||
* Unlike {@link Formatter}, {@link FormatProcessor} uses the value from the
|
||||
* embedded expression that immediately follows, without whitespace, the
|
||||
* <a href="../util/Formatter.html#syntax">format specifier</a>.
|
||||
* For example:
|
||||
* {@snippet :
|
||||
* FormatProcessor fmt = FormatProcessor.create(Locale.ROOT);
|
||||
* int x = 10;
|
||||
* int y = 20;
|
||||
* String result = fmt."%05d\{x} + %05d\{y} = %05d\{x + y}";
|
||||
* }
|
||||
* In the above example, the value of {@code result} will be {@code "00010 + 00020 = 00030"}.
|
||||
* <p>
|
||||
* Embedded expressions without a preceeding format specifier, use {@code %s}
|
||||
* by default.
|
||||
* {@snippet :
|
||||
* FormatProcessor fmt = FormatProcessor.create(Locale.ROOT);
|
||||
* int x = 10;
|
||||
* int y = 20;
|
||||
* String result1 = fmt."\{x} + \{y} = \{x + y}";
|
||||
* String result2 = fmt."%s\{x} + %s\{y} = %s\{x + y}";
|
||||
* }
|
||||
* In the above example, the value of {@code result1} and {@code result2} will
|
||||
* both be {@code "10 + 20 = 30"}.
|
||||
* <p>
|
||||
* The {@link FormatProcessor} format specification used and exceptions thrown are the
|
||||
* same as those of {@link Formatter}.
|
||||
* <p>
|
||||
* However, there are two significant differences related to the position of arguments.
|
||||
* An explict {@code n$} and relative {@code <} index will cause an exception due to
|
||||
* a missing argument list.
|
||||
* Whitespace appearing between the specification and the embedded expression will
|
||||
* also cause an exception.
|
||||
* <p>
|
||||
* {@link FormatProcessor} allows the use of different locales. For example:
|
||||
* {@snippet :
|
||||
* Locale locale = Locale.forLanguageTag("th-TH-u-nu-thai");
|
||||
* FormatProcessor thaiFMT = FormatProcessor.create(locale);
|
||||
* int x = 10;
|
||||
* int y = 20;
|
||||
* String result = thaiFMT."%4d\{x} + %4d\{y} = %5d\{x + y}";
|
||||
* }
|
||||
* In the above example, the value of {@code result} will be
|
||||
* {@code " \u0E51\u0E50 + \u0E52\u0E50 = \u0E53\u0E50"}.
|
||||
* <p>
|
||||
* For day to day use, the predefined {@link FormatProcessor#FMT} {@link FormatProcessor}
|
||||
* is available. {@link FormatProcessor#FMT} is defined using the {@link Locale#ROOT}.
|
||||
* Example: {@snippet :
|
||||
* int x = 10;
|
||||
* int y = 20;
|
||||
* String result = FMT."0x%04x\{x} + 0x%04x\{y} = 0x%04x\{x + y}"; // @highlight substring="FMT"
|
||||
* }
|
||||
* In the above example, the value of {@code result} will be {@code "0x000a + 0x0014 = 0x001E"}.
|
||||
*
|
||||
* @since 21
|
||||
*
|
||||
* @see Processor
|
||||
*/
|
||||
@PreviewFeature(feature=PreviewFeature.Feature.STRING_TEMPLATES)
|
||||
public final class FormatProcessor implements Processor<String, RuntimeException>, Linkage {
|
||||
/**
|
||||
* {@link Locale} used to format
|
||||
*/
|
||||
private final Locale locale;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param locale {@link Locale} used to format
|
||||
*/
|
||||
private FormatProcessor(Locale locale) {
|
||||
this.locale = locale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link FormatProcessor} using the specified locale.
|
||||
*
|
||||
* @param locale {@link Locale} used to format
|
||||
*
|
||||
* @return a new instance of {@link FormatProcessor}
|
||||
*
|
||||
* @throws java.lang.NullPointerException if locale is null
|
||||
*/
|
||||
public static FormatProcessor create(Locale locale) {
|
||||
Objects.requireNonNull(locale);
|
||||
return new FormatProcessor(locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a {@link String} based on the fragments, format
|
||||
* specifications found in the fragments and values in the
|
||||
* supplied {@link StringTemplate} object. This method constructs a
|
||||
* format string from the fragments, gathers up the values and
|
||||
* evaluates the expression asif evaulating
|
||||
* {@code new Formatter(locale).format(format, values).toString()}.
|
||||
* <p>
|
||||
* If an embedded expression is not immediately preceded by a
|
||||
* specifier then a {@code %s} is inserted in the format.
|
||||
*
|
||||
* @param stringTemplate a {@link StringTemplate} instance
|
||||
*
|
||||
* @return constructed {@link String}
|
||||
|
||||
* @throws IllegalFormatException
|
||||
* If a format specifier contains an illegal syntax, a format
|
||||
* specifier that is incompatible with the given arguments,
|
||||
* a specifier not followed immediately by an embedded expression or
|
||||
* other illegal conditions. For specification of all possible
|
||||
* formatting errors, see the
|
||||
* <a href="../util/Formatter.html#detail">details</a>
|
||||
* section of the formatter class specification.
|
||||
* @throws NullPointerException if stringTemplate is null
|
||||
*
|
||||
* @see java.util.Formatter
|
||||
*/
|
||||
@Override
|
||||
public final String process(StringTemplate stringTemplate) {
|
||||
Objects.requireNonNull(stringTemplate);
|
||||
String format = stringTemplateFormat(stringTemplate.fragments());
|
||||
Object[] values = stringTemplate.values().toArray();
|
||||
|
||||
return new Formatter(locale).format(format, values).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a {@link MethodHandle} that when supplied with the values from
|
||||
* a {@link StringTemplate} will produce a result equivalent to that provided by
|
||||
* {@link FormatProcessor#process(StringTemplate)}. This {@link MethodHandle}
|
||||
* is used by {@link FormatProcessor#FMT} and the ilk to perform a more
|
||||
* specialized composition of a result. This specialization is done by
|
||||
* prescanning the fragments and value types of a {@link StringTemplate}.
|
||||
* <p>
|
||||
* Process template expressions can be specialized when the processor is
|
||||
* of type {@link Linkage} and fetched from a static constant as is
|
||||
* {@link FormatProcessor#FMT} ({@code static final FormatProcessor}).
|
||||
* <p>
|
||||
* Other {@link FormatProcessor FormatProcessors} can be specialized when stored in a static
|
||||
* final.
|
||||
* For example:
|
||||
* {@snippet :
|
||||
* FormatProcessor THAI_FMT = FormatProcessor.create(Locale.forLanguageTag("th-TH-u-nu-thai"));
|
||||
* }
|
||||
* {@code THAI_FMT} will now produce specialized {@link MethodHandle MethodHandles} by way
|
||||
* of {@link FormatProcessor#linkage(List, MethodType)}.
|
||||
*
|
||||
* See {@link FormatProcessor#process(StringTemplate)} for more information.
|
||||
*
|
||||
* @throws IllegalFormatException
|
||||
* If a format specifier contains an illegal syntax, a format
|
||||
* specifier that is incompatible with the given arguments,
|
||||
* a specifier not followed immediately by an embedded expression or
|
||||
* other illegal conditions. For specification of all possible
|
||||
* formatting errors, see the
|
||||
* <a href="../util/Formatter.html#detail">details</a>
|
||||
* section of the formatter class specification.
|
||||
* @throws NullPointerException if fragments or type is null
|
||||
*
|
||||
* @see java.util.Formatter
|
||||
*/
|
||||
@Override
|
||||
public MethodHandle linkage(List<String> fragments, MethodType type) {
|
||||
Objects.requireNonNull(fragments);
|
||||
Objects.requireNonNull(type);
|
||||
String format = stringTemplateFormat(fragments);
|
||||
Class<?>[] ptypes = type.dropParameterTypes(0, 1).parameterArray();
|
||||
MethodHandle mh = new FormatterBuilder(format, locale, ptypes).build();
|
||||
mh = MethodHandles.dropArguments(mh, 0, type.parameterType(0));
|
||||
|
||||
return mh;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a format specification at the end of a fragment.
|
||||
*
|
||||
* @param fragment fragment to check
|
||||
* @param needed if the specification is needed
|
||||
*
|
||||
* @return true if the specification is found and needed
|
||||
*
|
||||
* @throws MissingFormatArgumentException if not at end or found and not needed
|
||||
*/
|
||||
private static boolean findFormat(String fragment, boolean needed) {
|
||||
Matcher matcher = Formatter.FORMAT_SPECIFIER_PATTERN.matcher(fragment);
|
||||
String group;
|
||||
|
||||
while (matcher.find()) {
|
||||
group = matcher.group();
|
||||
|
||||
if (!group.equals("%%") && !group.equals("%n")) {
|
||||
if (matcher.end() == fragment.length() && needed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new MissingFormatArgumentException(group +
|
||||
" is not immediately followed by an embedded expression");
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert {@link StringTemplate} fragments, containing format specifications,
|
||||
* to a form that can be passed on to {@link Formatter}. The method scans each fragment,
|
||||
* matching up formatter specifications with the following expression. If no
|
||||
* specification is found, the method inserts "%s".
|
||||
*
|
||||
* @param fragments string template fragments
|
||||
*
|
||||
* @return format string
|
||||
*/
|
||||
private static String stringTemplateFormat(List<String> fragments) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
int lastIndex = fragments.size() - 1;
|
||||
List<String> formats = fragments.subList(0, lastIndex);
|
||||
String last = fragments.get(lastIndex);
|
||||
|
||||
for (String format : formats) {
|
||||
if (findFormat(format, true)) {
|
||||
sb.append(format);
|
||||
} else {
|
||||
sb.append(format);
|
||||
sb.append("%s");
|
||||
}
|
||||
}
|
||||
|
||||
if (!findFormat(last, false)) {
|
||||
sb.append(last);
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* This predefined {@link FormatProcessor} instance constructs a {@link String} result using
|
||||
* the Locale.ROOT {@link Locale}. See {@link FormatProcessor} for more details.
|
||||
* Example: {@snippet :
|
||||
* int x = 10;
|
||||
* int y = 20;
|
||||
* String result = FMT."0x%04x\{x} + 0x%04x\{y} = 0x%04x\{x + y}"; // @highlight substring="FMT"
|
||||
* }
|
||||
* In the above example, the value of {@code result} will be {@code "0x000a + 0x0014 = 0x001E"}.
|
||||
*
|
||||
* @see java.util.FormatProcessor
|
||||
*/
|
||||
public static final FormatProcessor FMT = FormatProcessor.create(Locale.ROOT);
|
||||
|
||||
}
|
|
@ -36,6 +36,7 @@ import java.io.OutputStream;
|
|||
import java.io.OutputStreamWriter;
|
||||
import java.io.PrintStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.math.MathContext;
|
||||
|
@ -60,6 +61,7 @@ import java.time.temporal.TemporalAccessor;
|
|||
import java.time.temporal.TemporalQueries;
|
||||
import java.time.temporal.UnsupportedTemporalTypeException;
|
||||
|
||||
import jdk.internal.javac.PreviewFeature;
|
||||
import jdk.internal.math.DoubleConsts;
|
||||
import jdk.internal.math.FormattedFPDecimal;
|
||||
import sun.util.locale.provider.LocaleProviderAdapter;
|
||||
|
@ -2770,8 +2772,7 @@ public final class Formatter implements Closeable, Flushable {
|
|||
int lasto = -1;
|
||||
|
||||
List<FormatString> fsa = parse(format);
|
||||
for (int i = 0; i < fsa.size(); i++) {
|
||||
var fs = fsa.get(i);
|
||||
for (FormatString fs : fsa) {
|
||||
int index = fs.index();
|
||||
try {
|
||||
switch (index) {
|
||||
|
@ -2789,7 +2790,7 @@ public final class Formatter implements Closeable, Flushable {
|
|||
throw new MissingFormatArgumentException(fs.toString());
|
||||
fs.print(this, (args == null ? null : args[lasto]), l);
|
||||
}
|
||||
default -> { // explicit index
|
||||
default -> { // explicit index
|
||||
last = index - 1;
|
||||
if (args != null && last > args.length - 1)
|
||||
throw new MissingFormatArgumentException(fs.toString());
|
||||
|
@ -2804,15 +2805,15 @@ public final class Formatter implements Closeable, Flushable {
|
|||
}
|
||||
|
||||
// %[argument_index$][flags][width][.precision][t]conversion
|
||||
private static final String formatSpecifier
|
||||
static final String FORMAT_SPECIFIER
|
||||
= "%(\\d+\\$)?([-#+ 0,(\\<]*)?(\\d+)?(\\.\\d+)?([tT])?([a-zA-Z%])";
|
||||
|
||||
private static final Pattern fsPattern = Pattern.compile(formatSpecifier);
|
||||
static final Pattern FORMAT_SPECIFIER_PATTERN = Pattern.compile(FORMAT_SPECIFIER);
|
||||
|
||||
/**
|
||||
* Finds format specifiers in the format string.
|
||||
*/
|
||||
private List<FormatString> parse(String s) {
|
||||
static List<FormatString> parse(String s) {
|
||||
ArrayList<FormatString> al = new ArrayList<>();
|
||||
int i = 0;
|
||||
int max = s.length();
|
||||
|
@ -2840,7 +2841,7 @@ public final class Formatter implements Closeable, Flushable {
|
|||
i++;
|
||||
} else {
|
||||
if (m == null) {
|
||||
m = fsPattern.matcher(s);
|
||||
m = FORMAT_SPECIFIER_PATTERN.matcher(s);
|
||||
}
|
||||
// We have already parsed a '%' at n, so we either have a
|
||||
// match or the specifier at n is invalid
|
||||
|
@ -2855,7 +2856,7 @@ public final class Formatter implements Closeable, Flushable {
|
|||
return al;
|
||||
}
|
||||
|
||||
private interface FormatString {
|
||||
interface FormatString {
|
||||
int index();
|
||||
void print(Formatter fmt, Object arg, Locale l) throws IOException;
|
||||
String toString();
|
||||
|
@ -2891,14 +2892,15 @@ public final class Formatter implements Closeable, Flushable {
|
|||
DECIMAL_FLOAT
|
||||
};
|
||||
|
||||
private static class FormatSpecifier implements FormatString {
|
||||
static class FormatSpecifier implements FormatString {
|
||||
private static final double SCALEUP = Math.scalb(1.0, 54);
|
||||
|
||||
private int index = 0;
|
||||
private int flags = Flags.NONE;
|
||||
private int width = -1;
|
||||
private int precision = -1;
|
||||
private boolean dt = false;
|
||||
private char c;
|
||||
int index = 0;
|
||||
int flags = Flags.NONE;
|
||||
int width = -1;
|
||||
int precision = -1;
|
||||
boolean dt = false;
|
||||
char c;
|
||||
|
||||
private void index(String s, int start, int end) {
|
||||
if (start >= 0) {
|
||||
|
@ -3548,8 +3550,8 @@ public final class Formatter implements Closeable, Flushable {
|
|||
if (width != -1) {
|
||||
newW = adjustWidth(width - exp.length - 1, flags, neg);
|
||||
}
|
||||
localizedMagnitude(fmt, sb, mant, 0, flags, newW, l);
|
||||
|
||||
localizedMagnitude(fmt, sb, mant, 0, flags, newW, l);
|
||||
sb.append(Flags.contains(flags, Flags.UPPERCASE) ? 'E' : 'e');
|
||||
|
||||
char sign = exp[0];
|
||||
|
@ -3719,8 +3721,7 @@ public final class Formatter implements Closeable, Flushable {
|
|||
// If this is subnormal input so normalize (could be faster to
|
||||
// do as integer operation).
|
||||
if (subnormal) {
|
||||
double scaleUp = Math.scalb(1.0, 54);
|
||||
d *= scaleUp;
|
||||
d *= SCALEUP;
|
||||
// Calculate the exponent. This is not just exponent + 54
|
||||
// since the former is not the normalized exponent.
|
||||
exponent = Math.getExponent(d);
|
||||
|
@ -4623,7 +4624,7 @@ public final class Formatter implements Closeable, Flushable {
|
|||
}
|
||||
}
|
||||
|
||||
private static class Flags {
|
||||
static class Flags {
|
||||
|
||||
static final int NONE = 0; // ''
|
||||
|
||||
|
@ -4701,7 +4702,7 @@ public final class Formatter implements Closeable, Flushable {
|
|||
}
|
||||
}
|
||||
|
||||
private static class Conversion {
|
||||
static class Conversion {
|
||||
// Byte, Short, Integer, Long, BigInteger
|
||||
// (and associated primitives due to autoboxing)
|
||||
static final char DECIMAL_INTEGER = 'd';
|
||||
|
@ -4826,7 +4827,7 @@ public final class Formatter implements Closeable, Flushable {
|
|||
}
|
||||
}
|
||||
|
||||
private static class DateTime {
|
||||
static class DateTime {
|
||||
static final char HOUR_OF_DAY_0 = 'H'; // (00 - 23)
|
||||
static final char HOUR_0 = 'I'; // (01 - 12)
|
||||
static final char HOUR_OF_DAY = 'k'; // (0 - 23) -- like H
|
||||
|
@ -4877,4 +4878,5 @@ public final class Formatter implements Closeable, Flushable {
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
489
src/java.base/share/classes/java/util/FormatterBuilder.java
Normal file
489
src/java.base/share/classes/java/util/FormatterBuilder.java
Normal file
|
@ -0,0 +1,489 @@
|
|||
/*
|
||||
* Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License version 2 only, as
|
||||
* published by the Free Software Foundation. Oracle designates this
|
||||
* particular file as subject to the "Classpath" exception as provided
|
||||
* by Oracle in the LICENSE file that accompanied this code.
|
||||
*
|
||||
* This code is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* version 2 for more details (a copy is included in the LICENSE file that
|
||||
* accompanied this code).
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License version
|
||||
* 2 along with this work; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
||||
* or visit www.oracle.com if you need additional information or have any
|
||||
* questions.
|
||||
*/
|
||||
|
||||
package java.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.invoke.*;
|
||||
import java.lang.invoke.MethodHandles.Lookup;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.DecimalFormatSymbols;
|
||||
import java.text.NumberFormat;
|
||||
import java.text.spi.NumberFormatProvider;
|
||||
import java.util.FormatItem.*;
|
||||
import java.util.Formatter.*;
|
||||
|
||||
import jdk.internal.util.FormatConcatItem;
|
||||
|
||||
import sun.invoke.util.Wrapper;
|
||||
import sun.util.locale.provider.LocaleProviderAdapter;
|
||||
import sun.util.locale.provider.ResourceBundleBasedAdapter;
|
||||
|
||||
import static java.util.Formatter.Conversion.*;
|
||||
import static java.util.Formatter.Flags.*;
|
||||
import static java.lang.invoke.MethodHandles.*;
|
||||
import static java.lang.invoke.MethodType.*;
|
||||
|
||||
/**
|
||||
* This package private class supports the construction of the {@link MethodHandle}
|
||||
* used by {@link FormatProcessor}.
|
||||
*
|
||||
* @since 21
|
||||
*
|
||||
* Warning: This class is part of PreviewFeature.Feature.STRING_TEMPLATES.
|
||||
* Do not rely on its availability.
|
||||
*/
|
||||
final class FormatterBuilder {
|
||||
private static final Lookup LOOKUP = lookup();
|
||||
|
||||
private final String format;
|
||||
private final Locale locale;
|
||||
private final Class<?>[] ptypes;
|
||||
private final DecimalFormatSymbols dfs;
|
||||
private final boolean isGenericDFS;
|
||||
|
||||
FormatterBuilder(String format, Locale locale, Class<?>[] ptypes) {
|
||||
this.format = format;
|
||||
this.locale = locale;
|
||||
this.ptypes = ptypes;
|
||||
this.dfs = DecimalFormatSymbols.getInstance(locale);
|
||||
this.isGenericDFS = isGenericDFS(this.dfs);
|
||||
}
|
||||
|
||||
private static boolean isGenericDFS(DecimalFormatSymbols dfs) {
|
||||
return dfs.getZeroDigit() == '0' &&
|
||||
dfs.getDecimalSeparator() == '.' &&
|
||||
dfs.getGroupingSeparator() == ',' &&
|
||||
dfs.getMinusSign() == '-';
|
||||
}
|
||||
|
||||
private static Class<?> mapType(Class<?> type) {
|
||||
return type.isPrimitive() || type == String.class ? type : Object.class;
|
||||
}
|
||||
|
||||
private static MethodHandle findStringConcatItemConstructor(Class<?> cls,
|
||||
Class<?>... ptypes) {
|
||||
MethodType methodType = methodType(void.class, ptypes);
|
||||
|
||||
try {
|
||||
MethodHandle mh = LOOKUP.findConstructor(cls, methodType);
|
||||
|
||||
return mh.asType(mh.type().changeReturnType(FormatConcatItem.class));
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new AssertionError("Missing constructor in " +
|
||||
cls + ": " + methodType);
|
||||
}
|
||||
}
|
||||
|
||||
private static MethodHandle findMethod(Class<?> cls, String name,
|
||||
Class<?> rType, Class<?>... ptypes) {
|
||||
MethodType methodType = methodType(rType, ptypes);
|
||||
|
||||
try {
|
||||
return LOOKUP.findVirtual(cls, name, methodType);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new AssertionError("Missing method in " +
|
||||
cls + ": " + name + " " + methodType);
|
||||
}
|
||||
}
|
||||
|
||||
private static MethodHandle findStaticMethod(Class<?> cls, String name,
|
||||
Class<?> rType, Class<?>... ptypes) {
|
||||
MethodType methodType = methodType(rType, ptypes);
|
||||
|
||||
try {
|
||||
return LOOKUP.findStatic(cls, name, methodType);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new AssertionError("Missing static method in " +
|
||||
cls + ": " + name + " " + methodType);
|
||||
}
|
||||
}
|
||||
|
||||
private static final MethodHandle FIDecimal_MH =
|
||||
findStringConcatItemConstructor(FormatItemDecimal.class,
|
||||
DecimalFormatSymbols.class, int.class, char.class, boolean.class,
|
||||
int.class, long.class);
|
||||
|
||||
private static final MethodHandle FIHexadecimal_MH =
|
||||
findStringConcatItemConstructor(FormatItemHexadecimal.class,
|
||||
int.class, boolean.class, long.class);
|
||||
|
||||
private static final MethodHandle FIOctal_MH =
|
||||
findStringConcatItemConstructor(FormatItemOctal.class,
|
||||
int.class, boolean.class, long.class);
|
||||
|
||||
private static final MethodHandle FIBoolean_MH =
|
||||
findStringConcatItemConstructor(FormatItemBoolean.class,
|
||||
boolean.class);
|
||||
|
||||
private static final MethodHandle FICharacter_MH =
|
||||
findStringConcatItemConstructor(FormatItemCharacter.class,
|
||||
char.class);
|
||||
|
||||
private static final MethodHandle FIString_MH =
|
||||
findStringConcatItemConstructor(FormatItemString.class,
|
||||
String.class);
|
||||
|
||||
private static final MethodHandle FIFormatSpecifier_MH =
|
||||
findStringConcatItemConstructor(FormatItemFormatSpecifier.class,
|
||||
FormatSpecifier.class, Locale.class, Object.class);
|
||||
|
||||
private static final MethodHandle FIFormattable_MH =
|
||||
findStringConcatItemConstructor(FormatItemFormatSpecifier.class,
|
||||
Locale.class, int.class, int.class, int.class,
|
||||
Formattable.class);
|
||||
|
||||
private static final MethodHandle FIFillLeft_MH =
|
||||
findStringConcatItemConstructor(FormatItemFillLeft.class,
|
||||
int.class, FormatConcatItem.class);
|
||||
|
||||
private static final MethodHandle FIFillRight_MH =
|
||||
findStringConcatItemConstructor(FormatItemFillRight.class,
|
||||
int.class, FormatConcatItem.class);
|
||||
|
||||
private static final MethodHandle FINull_MH =
|
||||
findStringConcatItemConstructor(FormatItemNull.class);
|
||||
|
||||
private static final MethodHandle NullCheck_MH =
|
||||
findStaticMethod(FormatterBuilder.class, "nullCheck", boolean.class,
|
||||
Object.class);
|
||||
|
||||
private static final MethodHandle FormattableCheck_MH =
|
||||
findStaticMethod(FormatterBuilder.class, "formattableCheck", boolean.class,
|
||||
Object.class);
|
||||
|
||||
private static final MethodHandle ToLong_MH =
|
||||
findStaticMethod(java.util.FormatterBuilder.class, "toLong", long.class,
|
||||
int.class);
|
||||
|
||||
private static final MethodHandle ToString_MH =
|
||||
findStaticMethod(String.class, "valueOf", String.class,
|
||||
Object.class);
|
||||
|
||||
private static final MethodHandle HashCode_MH =
|
||||
findStaticMethod(Objects.class, "hashCode", int.class,
|
||||
Object.class);
|
||||
|
||||
private static boolean nullCheck(Object object) {
|
||||
return object == null;
|
||||
}
|
||||
|
||||
private static boolean formattableCheck(Object object) {
|
||||
return Formattable.class.isAssignableFrom(object.getClass());
|
||||
}
|
||||
|
||||
private static long toLong(int value) {
|
||||
return (long)value & 0xFFFFFFFFL;
|
||||
}
|
||||
|
||||
private static boolean isFlag(int value, int flags) {
|
||||
return (value & flags) != 0;
|
||||
}
|
||||
|
||||
private static boolean validFlags(int value, int flags) {
|
||||
return (value & ~flags) == 0;
|
||||
}
|
||||
|
||||
private static int groupSize(Locale locale, DecimalFormatSymbols dfs) {
|
||||
if (isGenericDFS(dfs)) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
DecimalFormat df;
|
||||
NumberFormat nf = NumberFormat.getNumberInstance(locale);
|
||||
|
||||
if (nf instanceof DecimalFormat) {
|
||||
df = (DecimalFormat)nf;
|
||||
} else {
|
||||
LocaleProviderAdapter adapter = LocaleProviderAdapter
|
||||
.getAdapter(NumberFormatProvider.class, locale);
|
||||
|
||||
if (!(adapter instanceof ResourceBundleBasedAdapter)) {
|
||||
adapter = LocaleProviderAdapter.getResourceBundleBased();
|
||||
}
|
||||
|
||||
String[] all = adapter.getLocaleResources(locale)
|
||||
.getNumberPatterns();
|
||||
|
||||
df = new DecimalFormat(all[0], dfs);
|
||||
}
|
||||
|
||||
return df.isGroupingUsed() ? df.getGroupingSize() : 0;
|
||||
}
|
||||
|
||||
private MethodHandle formatSpecifier(FormatSpecifier fs, Class<?> ptype) {
|
||||
boolean isPrimitive = ptype.isPrimitive();
|
||||
MethodHandle mh = identity(ptype);
|
||||
MethodType mt = mh.type();
|
||||
|
||||
//cannot cast to primitive types as it breaks null values formatting
|
||||
// if (ptype == byte.class || ptype == short.class ||
|
||||
// ptype == Byte.class || ptype == Short.class ||
|
||||
// ptype == Integer.class) {
|
||||
// mt = mt.changeReturnType(int.class);
|
||||
// } else if (ptype == Long.class) {
|
||||
// mt = mt.changeReturnType(long.class);
|
||||
// } else if (ptype == float.class || ptype == Float.class ||
|
||||
// ptype == Double.class) {
|
||||
// mt = mt.changeReturnType(double.class);
|
||||
// } else if (ptype == Boolean.class) {
|
||||
// mt = mt.changeReturnType(boolean.class);
|
||||
// } else if (ptype == Character.class) {
|
||||
// mt = mt.changeReturnType(char.class);
|
||||
// }
|
||||
|
||||
Class<?> itype = mt.returnType();
|
||||
|
||||
if (itype != ptype) {
|
||||
mh = explicitCastArguments(mh, mt);
|
||||
}
|
||||
|
||||
boolean handled = false;
|
||||
int flags = fs.flags;
|
||||
int width = fs.width;
|
||||
int precision = fs.precision;
|
||||
Character conv = fs.dt ? 't' : fs.c;
|
||||
|
||||
switch (Character.toLowerCase(conv)) {
|
||||
case BOOLEAN -> {
|
||||
if (itype == boolean.class && precision == -1) {
|
||||
if (flags == 0 && width == -1 && isPrimitive) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (validFlags(flags, LEFT_JUSTIFY)) {
|
||||
handled = true;
|
||||
mh = filterReturnValue(mh, FIBoolean_MH);
|
||||
}
|
||||
}
|
||||
}
|
||||
case STRING -> {
|
||||
if (flags == 0 && width == -1 && precision == -1) {
|
||||
if (isPrimitive || ptype == String.class) {
|
||||
return null;
|
||||
} else if (itype.isPrimitive()) {
|
||||
return mh;
|
||||
}
|
||||
}
|
||||
|
||||
if (validFlags(flags, LEFT_JUSTIFY) && precision == -1) {
|
||||
if (itype == String.class) {
|
||||
handled = true;
|
||||
mh = filterReturnValue(mh, FIString_MH);
|
||||
} else if (!itype.isPrimitive()) {
|
||||
handled = true;
|
||||
MethodHandle test = FormattableCheck_MH;
|
||||
test = test.asType(test.type().changeParameterType(0, ptype));
|
||||
MethodHandle pass = insertArguments(FIFormattable_MH,
|
||||
0, locale, flags, width, precision);
|
||||
pass = pass.asType(pass.type().changeParameterType(0, ptype));
|
||||
MethodHandle fail = ToString_MH;
|
||||
fail = filterReturnValue(fail, FIString_MH);
|
||||
fail = fail.asType(fail.type().changeParameterType(0, ptype));
|
||||
mh = guardWithTest(test, pass, fail);
|
||||
}
|
||||
}
|
||||
}
|
||||
case CHARACTER -> {
|
||||
if (itype == char.class && precision == -1) {
|
||||
if (flags == 0 && width == -1) {
|
||||
return isPrimitive ? null : mh;
|
||||
}
|
||||
|
||||
if (validFlags(flags, LEFT_JUSTIFY)) {
|
||||
handled = true;
|
||||
mh = filterReturnValue(mh, FICharacter_MH);
|
||||
}
|
||||
}
|
||||
}
|
||||
case DECIMAL_INTEGER -> {
|
||||
if ((itype == int.class || itype == long.class) && precision == -1) {
|
||||
if (itype == int.class) {
|
||||
mh = explicitCastArguments(mh,
|
||||
mh.type().changeReturnType(long.class));
|
||||
}
|
||||
|
||||
if (flags == 0 && isGenericDFS && width == -1) {
|
||||
return mh;
|
||||
} else if (validFlags(flags, PLUS | LEADING_SPACE |
|
||||
ZERO_PAD | GROUP |
|
||||
PARENTHESES)) {
|
||||
handled = true;
|
||||
int zeroPad = isFlag(flags, ZERO_PAD) ? width : -1;
|
||||
char sign = isFlag(flags, PLUS) ? '+' :
|
||||
isFlag(flags, LEADING_SPACE) ? ' ' : '\0';
|
||||
boolean parentheses = isFlag(flags, PARENTHESES);
|
||||
int groupSize = isFlag(flags, GROUP) ?
|
||||
groupSize(locale, dfs) : 0;
|
||||
mh = filterReturnValue(mh,
|
||||
insertArguments(FIDecimal_MH, 0, dfs, zeroPad,
|
||||
sign, parentheses, groupSize));
|
||||
}
|
||||
}
|
||||
}
|
||||
case OCTAL_INTEGER -> {
|
||||
if ((itype == int.class || itype == long.class) &&
|
||||
precision == -1 &&
|
||||
validFlags(flags, ZERO_PAD | ALTERNATE)) {
|
||||
handled = true;
|
||||
|
||||
if (itype == int.class) {
|
||||
mh = filterReturnValue(mh, ToLong_MH);
|
||||
}
|
||||
|
||||
int zeroPad = isFlag(flags, ZERO_PAD) ? width : -1;
|
||||
boolean hasPrefix = isFlag(flags, ALTERNATE);
|
||||
mh = filterReturnValue(mh,
|
||||
insertArguments(FIOctal_MH, 0, zeroPad, hasPrefix));
|
||||
}
|
||||
}
|
||||
case HEXADECIMAL_INTEGER -> {
|
||||
if ((itype == int.class || itype == long.class) &&
|
||||
precision == -1 &&
|
||||
validFlags(flags, ZERO_PAD | ALTERNATE)) {
|
||||
handled = true;
|
||||
|
||||
if (itype == int.class) {
|
||||
mh = filterReturnValue(mh, ToLong_MH);
|
||||
}
|
||||
|
||||
int zeroPad = isFlag(flags, ZERO_PAD) ? width : -1;
|
||||
boolean hasPrefix = isFlag(flags, ALTERNATE);
|
||||
mh = filterReturnValue(mh,
|
||||
insertArguments(FIHexadecimal_MH, 0, zeroPad, hasPrefix));
|
||||
}
|
||||
}
|
||||
default -> {
|
||||
// pass thru
|
||||
}
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
if (!isPrimitive) {
|
||||
MethodHandle test = NullCheck_MH.asType(
|
||||
NullCheck_MH.type().changeParameterType(0, ptype));
|
||||
MethodHandle pass = dropArguments(FINull_MH, 0, ptype);
|
||||
mh = guardWithTest(test, pass, mh);
|
||||
}
|
||||
|
||||
if (0 < width) {
|
||||
if (isFlag(flags, LEFT_JUSTIFY)) {
|
||||
mh = filterReturnValue(mh,
|
||||
insertArguments(FIFillRight_MH, 0, width));
|
||||
} else {
|
||||
mh = filterReturnValue(mh,
|
||||
insertArguments(FIFillLeft_MH, 0, width));
|
||||
}
|
||||
}
|
||||
|
||||
if (!isFlag(flags, UPPERCASE)) {
|
||||
return mh;
|
||||
}
|
||||
}
|
||||
|
||||
mh = insertArguments(FIFormatSpecifier_MH, 0, fs, locale);
|
||||
mh = mh.asType(mh.type().changeParameterType(0, ptype));
|
||||
|
||||
return mh;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct concat {@link MethodHandle} for based on format.
|
||||
*
|
||||
* @param fsa list of specifiers
|
||||
*
|
||||
* @return concat {@link MethodHandle} for based on format
|
||||
*/
|
||||
private MethodHandle buildFilters(List<FormatString> fsa,
|
||||
List<String> segments,
|
||||
MethodHandle[] filters) {
|
||||
MethodHandle mh = null;
|
||||
int iParam = 0;
|
||||
StringBuilder segment = new StringBuilder();
|
||||
|
||||
for (FormatString fs : fsa) {
|
||||
int index = fs.index();
|
||||
|
||||
switch (index) {
|
||||
case -2: // fixed string, "%n", or "%%"
|
||||
String string = fs.toString();
|
||||
|
||||
if ("%%".equals(string)) {
|
||||
segment.append('%');
|
||||
} else if ("%n".equals(string)) {
|
||||
segment.append(System.lineSeparator());
|
||||
} else {
|
||||
segment.append(string);
|
||||
}
|
||||
break;
|
||||
case 0: // ordinary index
|
||||
segments.add(segment.toString());
|
||||
segment.setLength(0);
|
||||
|
||||
if (iParam < ptypes.length) {
|
||||
Class<?> ptype = ptypes[iParam];
|
||||
filters[iParam++] = formatSpecifier((FormatSpecifier)fs, ptype);
|
||||
} else {
|
||||
throw new MissingFormatArgumentException(fs.toString());
|
||||
}
|
||||
break;
|
||||
case -1: // relative index
|
||||
default: // explicit index
|
||||
throw new IllegalFormatFlagsException("Indexing not allowed: " + fs.toString());
|
||||
}
|
||||
}
|
||||
|
||||
segments.add(segment.toString());
|
||||
|
||||
return mh;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a {@link MethodHandle} to format arguments.
|
||||
*
|
||||
* @return new {@link MethodHandle} to format arguments
|
||||
*/
|
||||
MethodHandle build() {
|
||||
List<String> segments = new ArrayList<>();
|
||||
MethodHandle[] filters = new MethodHandle[ptypes.length];
|
||||
buildFilters(Formatter.parse(format), segments, filters);
|
||||
Class<?>[] ftypes = new Class<?>[filters.length];
|
||||
|
||||
for (int i = 0; i < filters.length; i++) {
|
||||
MethodHandle filter = filters[i];
|
||||
ftypes[i] = filter == null ? ptypes[i] : filter.type().returnType();
|
||||
}
|
||||
|
||||
try {
|
||||
MethodHandle mh = StringConcatFactory.makeConcatWithTemplate(segments,
|
||||
List.of(ftypes));
|
||||
mh = filterArguments(mh, 0, filters);
|
||||
|
||||
return mh;
|
||||
} catch (StringConcatException ex) {
|
||||
throw new AssertionError("concat fail", ex);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue