8285932: Implementation of JEP 430 String Templates (Preview)

Reviewed-by: mcimadamore, rriggs, darcy
This commit is contained in:
Jim Laskey 2023-05-10 11:34:01 +00:00
parent da2c930262
commit 4aa65cbeef
74 changed files with 9309 additions and 99 deletions

View 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;
}
}
}

View 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;
}
}
}

View 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);
}

View file

@ -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 {
};
}
}
}

View 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);
}
}
}