Skip to content

Commit

Permalink
Merge pull request #151 from quarkusio/#quarkus-32507
Browse files Browse the repository at this point in the history
Add support for loading String's larger than 65535
  • Loading branch information
stuartwdouglas committed Apr 12, 2023
2 parents c8aa40d + d4d982b commit 7ff6379
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 4 deletions.
23 changes: 19 additions & 4 deletions src/main/java/io/quarkus/gizmo/BytecodeCreatorImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ class BytecodeCreatorImpl implements BytecodeCreator {
ArrayList.class.getName(),
Map.class.getName(),
HashMap.class.getName());
static final int MAX_STRING_LENGTH = 65535;

protected final List<Operation> operations = new ArrayList<>();

Expand Down Expand Up @@ -311,11 +312,25 @@ ResultHandle getOutgoingResultHandle() {
@Override
public ResultHandle load(String val) {
Objects.requireNonNull(val);
if (val.length() > 65535) {
//TODO: we could auto split this, but I don't think we really want strings this long
throw new IllegalArgumentException("Cannot load strings larger than " + 65535 + " bytes");
int length = val.length();
if (length > MAX_STRING_LENGTH) {
int div = length / MAX_STRING_LENGTH;
int mod = length % MAX_STRING_LENGTH;

Gizmo.StringBuilderGenerator sb = Gizmo.newStringBuilder(this, length);
for (int i = 0; i < div; i++) {
int beginIndex = i * MAX_STRING_LENGTH;
int endIndex = beginIndex + MAX_STRING_LENGTH;
sb.append(val.substring(beginIndex, endIndex));
}
if (mod > 0) {
sb.append(val.substring(div * MAX_STRING_LENGTH));
}
return sb.callToString();

} else {
return new ResultHandle("Ljava/lang/String;", this, val);
}
return new ResultHandle("Ljava/lang/String;", this, val);
}

@Override
Expand Down
43 changes: 43 additions & 0 deletions src/main/java/io/quarkus/gizmo/Gizmo.java
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,43 @@ public static StringBuilderGenerator newStringBuilder(BytecodeCreator target) {
return new StringBuilderGenerator(target);
}

/**
* Creates a {@code StringBuilder} generator that helps to generate a chain of
* {@code append} calls and a final {@code toString} call.
*
* <pre>
* StringBuilderGenerator str = Gizmo.newStringBuilder(bytecode, capacity);
* str.append("constant");
* str.append(someResultHandle);
* ResultHandle result = str.callToString();
* </pre>
*
* The {@code append} method mimics the regular {@code StringBuilder.append}, so
* it accepts {@code ResultHandle}s of all types for which {@code StringBuilder}
* has an overload:
* <ul>
* <li>primitive types</li>
* <li>{@code char[]}</li>
* <li>{@code java.lang.String}</li>
* <li>{@code java.lang.Object}</li>
* </ul>
*
* Notably, arrays except of {@code char[]} are appended using {@code Object.toString}
* and if {@code Arrays.toString} should be used, it must be generated manually.
* <p>
* Methods for appending only a part of {@code char[]} or {@code CharSequence} are not
* provided. Other {@code StringBuilder} methods are not provided either. This is just
* a simple utility for generating code that concatenates strings, e.g. for implementing
* the {@code toString} method.
*
* @param target
* @param capacity
* @return the generator
*/
public static StringBuilderGenerator newStringBuilder(BytecodeCreator target, int capacity) {
return new StringBuilderGenerator(target, capacity);
}

/**
* Generates a structural {@code equals} method in given {@code clazz} that compares
* given {@code fields}. The generated code is similar to what IDEs would typically
Expand Down Expand Up @@ -947,6 +984,7 @@ public ResultHandle containsKey(ResultHandle key) {

public static class StringBuilderGenerator {
private static final MethodDescriptor CONSTRUCTOR = MethodDescriptor.ofConstructor(StringBuilder.class);
private static final MethodDescriptor CONSTRUCTOR_WITH_CAPACITY = MethodDescriptor.ofConstructor(StringBuilder.class, int.class);
private static final MethodDescriptor APPEND_BOOLEAN = MethodDescriptor.ofMethod(StringBuilder.class, "append", StringBuilder.class, boolean.class);
private static final MethodDescriptor APPEND_INT = MethodDescriptor.ofMethod(StringBuilder.class, "append", StringBuilder.class, int.class);
private static final MethodDescriptor APPEND_LONG = MethodDescriptor.ofMethod(StringBuilder.class, "append", StringBuilder.class, long.class);
Expand All @@ -967,6 +1005,11 @@ private StringBuilderGenerator(BytecodeCreator bytecode) {
this.instance = bytecode.newInstance(CONSTRUCTOR);
}

private StringBuilderGenerator(BytecodeCreator bytecode, int capacity) {
this.bytecode = bytecode;
this.instance = bytecode.newInstance(CONSTRUCTOR_WITH_CAPACITY, bytecode.load(capacity));
}

public StringBuilderGenerator append(ResultHandle value) {
switch (value.getType()) {
case "Z": // boolean
Expand Down
62 changes: 62 additions & 0 deletions src/test/java/io/quarkus/gizmo/GiganticStringTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package io.quarkus.gizmo;

import static io.quarkus.gizmo.BytecodeCreatorImpl.MAX_STRING_LENGTH;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.function.Supplier;
import org.junit.Assert;
import org.junit.Test;

public class GiganticStringTest {

@Test
public void exactCutoffSize() throws Exception {
doTest(MAX_STRING_LENGTH);
}

@Test
public void cutoffSizePlus1() throws Exception {
doTest(MAX_STRING_LENGTH + 1);
}

@Test
public void cutoffSizePlusSeven() throws Exception {
doTest(MAX_STRING_LENGTH + 7);
}

@Test
public void tenTimesTheCutoffSize() throws Exception {
doTest(MAX_STRING_LENGTH * 10);
}

@Test
public void thirteenTimesTheCutoffSizePlus13() throws Exception {
doTest((MAX_STRING_LENGTH * 10) + 13);
}

private void doTest(int size)
throws ClassNotFoundException, InstantiationException, IllegalAccessException, InvocationTargetException,
NoSuchMethodException {
TestClassLoader cl = new TestClassLoader(getClass().getClassLoader());
try (ClassCreator creator = ClassCreator.builder().classOutput(cl).className("com.MyTest").interfaces(Supplier.class).build()) {
MethodCreator method = creator.getMethodCreator("get", Object.class);
char[] chars = new char[size];
Arrays.fill(chars, 'a');
chars[0] = 'f';
chars[size - 1] = 'l';

String str = new String(chars);

method.returnValue(method.load(str));
}
Class<?> clazz = cl.loadClass("com.MyTest");
Supplier myInterface = (Supplier) clazz.getDeclaredConstructor().newInstance();
Object o = myInterface.get();
Assert.assertEquals(String.class, o.getClass());
String s = o.toString();
Assert.assertEquals(size, s.length());
Assert.assertEquals('f', s.charAt(0));
Assert.assertEquals('l', s.charAt(size - 1));
Assert.assertEquals('a', s.charAt(size / 2));
}
}

0 comments on commit 7ff6379

Please sign in to comment.