Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions CodenameOne/src/com/codename1/impl/JdkApiRewriteHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,44 @@ public final class JdkApiRewriteHelper {
private JdkApiRewriteHelper() {
}

public static String replaceAll(String source, String regex, String replacement) {
if (source == null) {
throw new NullPointerException("source is null");
}
if (regex == null) {
throw new NullPointerException("regex is null");
}
if (replacement == null) {
throw new NullPointerException("replacement is null");
}
try {
return new RE(regex).subst(source, replacement, RE.REPLACE_ALL | RE.REPLACE_BACKREFERENCES);
} catch (RESyntaxException ex) {
return com.codename1.util.StringUtil.replaceAll(source, regex, replacement);
}
}

public static String replaceFirst(String source, String regex, String replacement) {
if (source == null) {
throw new NullPointerException("source is null");
}
if (regex == null) {
throw new NullPointerException("regex is null");
}
if (replacement == null) {
throw new NullPointerException("replacement is null");
}
try {
return new RE(regex).subst(source, replacement, RE.REPLACE_FIRSTONLY | RE.REPLACE_BACKREFERENCES);
} catch (RESyntaxException ex) {
int idx = source.indexOf(regex);
if (idx < 0) {
return source;
}
return source.substring(0, idx) + replacement + source.substring(idx + regex.length());
}
}

public static String[] split(String source, String regex) {
return split(source, regex, 0);
}
Expand Down
6 changes: 6 additions & 0 deletions Ports/CLDC11/src/java/lang/String.java
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,12 @@ public java.lang.String replace(char oldChar, char newChar){
return null; //TODO codavaj!!
}

/// Replaces each substring of this string that matches the literal target sequence with the specified literal replacement sequence.
/// The replacement proceeds from the beginning of the string to the end, for example, replacing "aa" with "b" in the string "aaa" will result in "ba" rather than "ab".
public java.lang.String replace(java.lang.CharSequence target, java.lang.CharSequence replacement){
return null; //TODO codavaj!!
}

/// Tests if this string starts with the specified prefix.
public boolean startsWith(java.lang.String prefix){
return false; //TODO codavaj!!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,14 @@ private static Map<MethodRef, MethodRef> createInvocationRewriteRules() {
MethodRef.virtual("java/lang/String", "split", "(Ljava/lang/String;I)[Ljava/lang/String;"),
MethodRef.staticRef(JDK_API_REWRITE_HELPER_INTERNAL_NAME, "split", "(Ljava/lang/String;Ljava/lang/String;I)[Ljava/lang/String;")
);
rules.put(
MethodRef.virtual("java/lang/String", "replaceAll", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;"),
MethodRef.staticRef(JDK_API_REWRITE_HELPER_INTERNAL_NAME, "replaceAll", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;")
);
rules.put(
MethodRef.virtual("java/lang/String", "replaceFirst", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;"),
MethodRef.staticRef(JDK_API_REWRITE_HELPER_INTERNAL_NAME, "replaceFirst", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;")
);
return Collections.unmodifiableMap(rules);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,32 @@ void rewritesStringSplitInvocations(@TempDir Path tempDir) throws Exception {
assertTrue(containsMethodInsn(rewritten, "com/codename1/impl/JdkApiRewriteHelper", "split", "(Ljava/lang/String;Ljava/lang/String;)[Ljava/lang/String;", Opcodes.INVOKESTATIC));
}

@Test
void rewritesStringReplaceAllInvocations(@TempDir Path tempDir) throws Exception {
Path outputDir = tempDir.resolve("classes");
Files.createDirectories(outputDir);
Path classFile = writeStringRegexInvocationClass(outputDir, "app/StringReplaceAllUser", "replaceAll");

BytecodeComplianceMojo mojo = new BytecodeComplianceMojo();
applyInvocationRewrites(mojo, outputDir.toFile());

byte[] rewritten = Files.readAllBytes(classFile);
assertTrue(containsMethodInsn(rewritten, "com/codename1/impl/JdkApiRewriteHelper", "replaceAll", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", Opcodes.INVOKESTATIC));
}

@Test
void rewritesStringReplaceFirstInvocations(@TempDir Path tempDir) throws Exception {
Path outputDir = tempDir.resolve("classes");
Files.createDirectories(outputDir);
Path classFile = writeStringRegexInvocationClass(outputDir, "app/StringReplaceFirstUser", "replaceFirst");

BytecodeComplianceMojo mojo = new BytecodeComplianceMojo();
applyInvocationRewrites(mojo, outputDir.toFile());

byte[] rewritten = Files.readAllBytes(classFile);
assertTrue(containsMethodInsn(rewritten, "com/codename1/impl/JdkApiRewriteHelper", "replaceFirst", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", Opcodes.INVOKESTATIC));
}

@Test
void allowsRewriteHelperCallsAfterSplitRewrite(@TempDir Path tempDir) throws Exception {
Path outputDir = tempDir.resolve("classes");
Expand Down Expand Up @@ -442,6 +468,36 @@ private Path writeStringApiUsageClass(Path root, String className) throws Except
return classFile;
}

private Path writeStringRegexInvocationClass(Path root, String className, String methodName) throws Exception {
ClassWriter writer = new ClassWriter(0);
writer.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, className, null, "java/lang/Object", null);

MethodVisitor init = writer.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
init.visitCode();
init.visitVarInsn(Opcodes.ALOAD, 0);
init.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
init.visitInsn(Opcodes.RETURN);
init.visitMaxs(1, 1);
init.visitEnd();

MethodVisitor run = writer.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "run", "()V", null, null);
run.visitCode();
run.visitLdcInsn("aaa");
run.visitLdcInsn("a");
run.visitLdcInsn("b");
run.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/String", methodName, "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", false);
run.visitInsn(Opcodes.POP);
run.visitInsn(Opcodes.RETURN);
run.visitMaxs(3, 0);
run.visitEnd();

writer.visitEnd();
Path classFile = root.resolve(className + ".class");
Files.createDirectories(classFile.getParent());
Files.write(classFile, writer.toByteArray());
return classFile;
}

private boolean containsMethodInsn(byte[] classBytes, final String owner, final String name, final String descriptor, final int opcode) {
final boolean[] found = new boolean[]{false};
ClassReader reader = new ClassReader(classBytes);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ private static int testTimeoutMs() {
new SimdApiTest(),
new SimdLargeAllocaTest(),
new StreamApiTest(),
new StringApiTest(),
new TimeApiTest(),
new Java17Tests(),
new BackgroundThreadUiAccessTest(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.codenameone.examples.hellocodenameone.tests;

/**
* End-to-end coverage for the String regex/literal helpers that Codename One
* routes through the bytecode rewriter (replaceAll/replaceFirst) and the
* literal CharSequence overload that lives directly on the String API
* (replace(CharSequence, CharSequence)). Exercises the call sites on the
* device so we catch any platform-specific divergence on iOS, Android and
* JavaScript.
*/
public class StringApiTest extends BaseTest {

@Override
public boolean runTest() {
try {
// String.replace(CharSequence, CharSequence) - literal substitution
assertEqual("ba", "aaa".replace("aa", "b"), "replace(aa,b) on aaa should be ba (left-to-right)");
assertEqual("hello world", "hello there".replace("there", "world"), "single token replacement failed");
assertEqual("X.X.X", "a.a.a".replace("a", "X"), "all-occurrence char-sequence replace failed");
assertEqual("abc", "abc".replace("z", "Q"), "replace with no match should return original");
CharSequence target = new StringBuilder("a");
CharSequence repl = new StringBuilder("XY");
assertEqual("XYbXY", "aba".replace(target, repl), "non-String CharSequence overload failed");

// String.replaceAll - regex-driven substitution via JdkApiRewriteHelper -> RE
assertEqual("XbXcX", "aabacaa".replaceAll("a+", "X"), "replaceAll greedy + quantifier failed");
assertEqual("xBxB", "aBaB".replaceAll("a", "x"), "replaceAll literal token failed");
assertEqual("ABC", "abc".replaceAll("[a-z]", "$0").toUpperCase(),
"replaceAll with $0 backreference / toUpperCase pipeline failed");
assertEqual("--", "ab".replaceAll(".", "-"), "replaceAll '.' should match every character");
assertEqual("nochange", "nochange".replaceAll("zzz", "X"),
"replaceAll with no matches should return original");

// String.replaceFirst - regex-driven first-only substitution
assertEqual("XbacaaB", "aabacaaB".replaceFirst("a+", "X"),
"replaceFirst should only replace the first regex match");
assertEqual("xbab", "abab".replaceFirst("a", "x"),
"replaceFirst literal token failed");
assertEqual("nochange", "nochange".replaceFirst("zzz", "X"),
"replaceFirst with no match should return original");
} catch (Throwable t) {
fail("String API test failed: " + t);
return false;
}
done();
return true;
}

@Override
public boolean shouldTakeScreenshot() {
return false;
}
}
40 changes: 40 additions & 0 deletions vm/JavaAPI/src/java/lang/String.java
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,46 @@ public java.lang.String replace(char oldChar, char newChar){
return copied ? new String(buffer) : this;
}

/**
* Replaces each substring of this string that matches the literal target sequence with the specified literal replacement sequence.
* The replacement proceeds from the beginning of the string to the end, for example, replacing "aa" with "b" in the string "aaa" will result in "ba" rather than "ab".
*/
public java.lang.String replace(java.lang.CharSequence target, java.lang.CharSequence replacement) {
if (target == null) {
throw new NullPointerException("target");
}
if (replacement == null) {
throw new NullPointerException("replacement");
}
java.lang.String targetStr = target.toString();
java.lang.String replacementStr = replacement.toString();
int targetLen = targetStr.length();
if (targetLen == 0) {
int len = count;
StringBuilder sb = new StringBuilder(len + (len + 1) * replacementStr.length());
sb.append(replacementStr);
for (int i = 0; i < len; i++) {
sb.append(value[offset + i]);
sb.append(replacementStr);
}
return sb.toString();
}
int idx = indexOf(targetStr);
if (idx < 0) {
return this;
}
StringBuilder sb = new StringBuilder(count);
int prev = 0;
while (idx >= 0) {
sb.append(value, offset + prev, idx - prev);
sb.append(replacementStr);
prev = idx + targetLen;
idx = indexOf(targetStr, prev);
}
sb.append(value, offset + prev, count - prev);
return sb.toString();
}

/**
* Tests if this string starts with the specified prefix.
*/
Expand Down
Loading