diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c1f14c4..f2fa3a0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -44,5 +44,4 @@ jobs: with: body_path: UPGRADE.md files: | - README.md - build/distributions/ijava-1.4.0.zip + build/distributions/ijava-latest.zip diff --git a/README.md b/README.md index b16564f..7130ad1 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,14 @@ features and magics: ![timeout](docs/img/line-magic-list.png) * add `time` cell magic ![timeout](docs/img/cell-magic-time.png) +* add `compile` cellMagic (make sure `--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED` in env * + IJAVA_COMPILER_OPTS*) + ![compile](docs/img/compile-cell-magic.png) +* add `read/write` cell/body magic + ![r-w](docs/img/read-write-line-magic.png) + ![r-w](docs/img/write-cell-magic.png) +* add `cmd` line magic + ![cmd](docs/img/cmd-line-magic.png) [//]: # ([![badge](https://img.shields.io/badge/launch-binder-E66581.svg?logo=data:image/png;base64,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)](https://mybinder.org/v2/gh/SpencerPark/ijava-binder/master) [![badge](https://img.shields.io/badge/launch-binder%20lab-579ACA.svg?logo=data:image/png;base64,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)](https://mybinder.org/v2/gh/SpencerPark/ijava-binder/master?urlpath=lab)) diff --git a/UPGRADE.md b/UPGRADE.md index d1979d5..51b17c3 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,11 +1,13 @@ Upgrade Note: -* Upgrade to jdk 17 and gradle 7.3.3 -* Print with variable name or source - ![timeout](docs/img/print-with-var-name.png) -* add `print` function and `printerPrefix` line magic - ![timeout](docs/img/print-func-line-magic.png) -* add `list` line magic - ![timeout](docs/img/line-magic-list.png) -* add `time` cell magic - ![timeout](docs/img/cell-magic-time.png) +* Fix `print` input parameter extraction error in code blocks that are called multiple times; +* add `RuntimeCompiler` util; +* add `compile` cellMagic (make sure `--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED` in env * + IJAVA_COMPILER_OPTS*) + ![compile](docs/img/compile-cell-magic.png) +* add `read/write` cell/body magic + ![r-w](docs/img/read-write-line-magic.png) + ![r-w](docs/img/write-cell-magic.png) +* add `cmd` line magic + ![cmd](docs/img/cmd-line-magic.png) + diff --git a/build.gradle b/build.gradle index 7764b1d..c6efb14 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ plugins { } group = 'io.github.spencerpark' -version = '1.4.0' +version = '1.4.1' repositories { mavenLocal() @@ -98,6 +98,8 @@ licenseReport { // pack up tasks.register('packDist', Zip) { + archiveFileName = project.name + "-latest.zip" + from(layout.buildDirectory.dir("resources/main")) { include "install.py" } diff --git a/docs/img/cmd-line-magic.png b/docs/img/cmd-line-magic.png new file mode 100644 index 0000000..d472d78 Binary files /dev/null and b/docs/img/cmd-line-magic.png differ diff --git a/docs/img/compile-cell-magic.png b/docs/img/compile-cell-magic.png new file mode 100644 index 0000000..c926afa Binary files /dev/null and b/docs/img/compile-cell-magic.png differ diff --git a/docs/img/read-write-line-magic.png b/docs/img/read-write-line-magic.png new file mode 100644 index 0000000..8569a29 Binary files /dev/null and b/docs/img/read-write-line-magic.png differ diff --git a/docs/img/write-cell-magic.png b/docs/img/write-cell-magic.png new file mode 100644 index 0000000..c98fa67 Binary files /dev/null and b/docs/img/write-cell-magic.png differ diff --git a/src/main/java/io/github/spencerpark/ijava/JavaKernel.java b/src/main/java/io/github/spencerpark/ijava/JavaKernel.java index 0f32171..9634555 100644 --- a/src/main/java/io/github/spencerpark/ijava/JavaKernel.java +++ b/src/main/java/io/github/spencerpark/ijava/JavaKernel.java @@ -112,6 +112,7 @@ public JavaKernel() { magics.registerMagics(new PrinterMagics()); magics.registerMagics(new MagicsTool()); magics.registerMagics(new TimeItMagics()); + magics.registerMagics(new CompilerMagics(this::addToClasspath)); this.languageInfo = new LanguageInfo.Builder("Java") .version(Runtime.version().toString()) diff --git a/src/main/java/io/github/spencerpark/ijava/magics/CompilerMagics.java b/src/main/java/io/github/spencerpark/ijava/magics/CompilerMagics.java new file mode 100644 index 0000000..dad1c1e --- /dev/null +++ b/src/main/java/io/github/spencerpark/ijava/magics/CompilerMagics.java @@ -0,0 +1,119 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2022 ${author} + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package io.github.spencerpark.ijava.magics; + +import io.github.spencerpark.ijava.IJava; +import io.github.spencerpark.ijava.JavaKernel; +import io.github.spencerpark.ijava.execution.CodeEvaluator; +import io.github.spencerpark.ijava.utils.RuntimeCompiler; +import io.github.spencerpark.jupyter.kernel.magic.registry.CellMagic; +import io.github.spencerpark.jupyter.kernel.util.GlobFinder; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.List; +import java.util.function.Consumer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class CompilerMagics { + private static final List COMMENT_PATTERNS = List.of("/\\*(.|\\s)*?\\*/", "//.*\\n*"); + private static final Pattern PACKAGE_PATTERN = Pattern.compile("\\s*package\\s+(?\\w+(\\.\\w+){0,100})\\s*"); + + private final Consumer addToClasspath; + + private CodeEvaluator evaluator; + + public CompilerMagics(Consumer addToClasspath) { + this.addToClasspath = addToClasspath; + } + + @CellMagic(aliases = {"compile"}) + public void compile(List args, String body) { + if (args.isEmpty()) throw new RuntimeException("Please specify *Class Canonical Name* in args!"); + + // 1. autofill package base on class canonical name + String bodyCopy = body; + for (String pattern : COMMENT_PATTERNS) bodyCopy = bodyCopy.replaceAll(pattern, ""); + Matcher matcher = PACKAGE_PATTERN.matcher(bodyCopy); + String clzCanonicalName = args.get(0); + String[] namePart = clzCanonicalName.split("\\."); + if (!matcher.find()) body = String.format("package %s;", namePart[namePart.length - 1]) + body; + + // 2. build + RuntimeCompiler.compile(clzCanonicalName, body, buildCompilerOptions(), true); + + // 3. add to classpath + // todo hot-reload class + GlobFinder resolver = new GlobFinder(namePart[0]); + try { + resolver.computeMatchingPaths().forEach(path -> this.addToClasspath.accept(path.getParent().toAbsolutePath().toString())); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public RuntimeCompiler.CompileOptions buildCompilerOptions() { + RuntimeCompiler.CompileOptions compileOptions = new RuntimeCompiler.CompileOptions(); + try { + if (evaluator == null) getEvaluator(); + Object result = evaluator.eval(""" + import java.lang.invoke.MethodHandles; + + ClassLoader cl = MethodHandles.lookup().lookupClass().getClassLoader(); + + StringBuilder classpath = new StringBuilder(); + String separator = System.getProperty("path.separator"); + String cp = System.getProperty("java.class.path"); + String mp = System.getProperty("jdk.module.path"); + + if (cp != null && !"".equals(cp)) classpath.append(cp); + if (mp != null && !"".equals(mp)) classpath.append(mp); + + if (cl instanceof URLClassLoader) { + for (URL url : ((URLClassLoader) cl).getURLs()) { + if (classpath.length() > 0) classpath.append(separator); + if ("file".equals(url.getProtocol())) classpath.append(new File(url.toURI())); + } + } + classpath.toString() + """); + return compileOptions.options("-classpath", (String) result); + } catch (Exception e) { + System.err.println("get jshell instance class path error. keep default class path."); + } + return compileOptions; + } + + public void getEvaluator() { + try { + JavaKernel kernel = IJava.getKernelInstance(); + Field field = kernel.getClass().getDeclaredField("evaluator"); + field.setAccessible(true); + evaluator = (CodeEvaluator) field.get(kernel); + } catch (Exception e) { + throw new RuntimeException("Compiler get JShell evaluator instance error." + e.getMessage()); + } + } +} diff --git a/src/main/java/io/github/spencerpark/ijava/magics/MagicsTool.java b/src/main/java/io/github/spencerpark/ijava/magics/MagicsTool.java index 9c914f7..5927f0a 100644 --- a/src/main/java/io/github/spencerpark/ijava/magics/MagicsTool.java +++ b/src/main/java/io/github/spencerpark/ijava/magics/MagicsTool.java @@ -23,12 +23,18 @@ */ package io.github.spencerpark.ijava.magics; +import io.github.spencerpark.ijava.IJava; import io.github.spencerpark.ijava.JavaKernel; +import io.github.spencerpark.ijava.execution.CodeEvaluator; +import io.github.spencerpark.jupyter.kernel.magic.registry.CellMagic; import io.github.spencerpark.jupyter.kernel.magic.registry.LineMagic; import io.github.spencerpark.jupyter.kernel.magic.registry.LineMagicFunction; import io.github.spencerpark.jupyter.kernel.magic.registry.Magics; +import java.io.*; import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -36,6 +42,9 @@ import java.util.stream.Collectors; public class MagicsTool { + private static final String HIGHLIGHT_PATTERN = "\u001B[36m%s\u001B[0m"; + + private CodeEvaluator evaluator; @LineMagic public void listLineMagic(List args) { @@ -65,6 +74,80 @@ public void listMagic(List args) { listCellMagic(Collections.emptyList()); } + @LineMagic(value = "cmd") + public void runCommand(List args) throws IOException { + if (args.isEmpty()) return; + Process proc = Runtime.getRuntime().exec(args.toArray(new String[0])); + + String s; + try (InputStreamReader inputStreamReader = new InputStreamReader(proc.getInputStream()); + BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) { + while ((s = bufferedReader.readLine()) != null) { + System.out.println(s); + } + } + try (InputStreamReader inputStreamReader = new InputStreamReader(proc.getErrorStream()); + BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) { + while ((s = bufferedReader.readLine()) != null) { + System.err.println(s); + } + } + } + + @LineMagic(value = "read") + public String readFromFile(List args) throws IOException { + if (args.isEmpty()) { + System.out.println(""" + -h/--help for help. + help: + example: + 1. `String content = %read filename` will read file and return for content. + """); + return null; + } + return String.join("\n", Files.readAllLines(Path.of(args.get(0)))); + } + + @LineMagic(value = "write") + public void writeToFile(List args) throws IOException { + if (args.isEmpty()) { + System.out.println(""" + -h/--help for help. + help: + example: + 1. `%write variable filename` will read variable's write to file. + 2. `%write variable` will read variable's write to temp file. + """); + return; + } + + if (evaluator == null) getEvaluator(); + Object content; + try { + content = evaluator.eval(args.get(0)); + } catch (Exception e) { + throw new RuntimeException("eval variable `" + args.get(0) + "` error, variable not found or illegal express!"); + } + + List argsLast = args.size() > 1 ? Collections.singletonList(args.get(1)) : Collections.emptyList(); + writeToFile(argsLast, content.toString()); + } + + @CellMagic(value = "write") + public void writeToFile(List args, String body) throws IOException { + String fileName = args.isEmpty() + ? Files.createTempFile("jshell-", ".tmp").toAbsolutePath().toString() + : args.get(0); + File file = new File(fileName); + if (file.getParentFile() != null && !file.getParentFile().exists() && !file.getParentFile().mkdirs()) + throw new IOException("Cannot create parent folder: " + file.getParentFile()); + try (FileWriter writer = new FileWriter(file)) { + writer.write(body); + writer.flush(); + } + System.out.printf("Write to %s success.%n", String.format(HIGHLIGHT_PATTERN, file.getAbsolutePath())); + } + @SuppressWarnings("unchecked") private Collection getMagicsName(Magics magics, String fieldName) throws NoSuchFieldException, IllegalAccessException { Field field = magics.getClass().getDeclaredField(fieldName); @@ -75,4 +158,15 @@ private Collection getMagicsName(Magics magics, String fieldName) throws .collect(Collectors.groupingBy(Map.Entry::getValue, Collectors.mapping(Map.Entry::getKey, Collectors.joining(", ")))) .values(); } + + public void getEvaluator() { + try { + JavaKernel kernel = IJava.getKernelInstance(); + Field field = kernel.getClass().getDeclaredField("evaluator"); + field.setAccessible(true); + evaluator = (CodeEvaluator) field.get(kernel); + } catch (Exception e) { + throw new RuntimeException("Compiler get JShell evaluator instance error." + e.getMessage()); + } + } } diff --git a/src/main/java/io/github/spencerpark/ijava/utils/RuntimeCompiler.java b/src/main/java/io/github/spencerpark/ijava/utils/RuntimeCompiler.java new file mode 100644 index 0000000..129179a --- /dev/null +++ b/src/main/java/io/github/spencerpark/ijava/utils/RuntimeCompiler.java @@ -0,0 +1,242 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2022 ${author} + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package io.github.spencerpark.ijava.utils; + +import javax.annotation.processing.Processor; +import javax.tools.*; +import javax.tools.JavaCompiler.CompilationTask; +import java.io.File; +import java.io.FileWriter; +import java.io.StringWriter; +import java.lang.invoke.MethodHandles; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + + +public class RuntimeCompiler { + public static Class compile(String className, String content) { + return compile(className, content, new CompileOptions(), false); + } + + public static Class compile(String className, String content, boolean forceCompile) { + return compile(className, content, new CompileOptions(), forceCompile); + } + + public static Class compile(String className, String content, CompileOptions compileOptions, boolean forceCompile) { + ClassLoader cl = MethodHandles.lookup().lookupClass().getClassLoader(); + + try { + Class clzCompiled = cl.loadClass(className); + System.out.printf("%s already exist! Class: %s%n", className, clzCompiled); + if (!forceCompile) return clzCompiled; + } catch (ClassNotFoundException ignore) { + // ignore + } + + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + if (compiler == null) + throw new RuntimeException("No compiler was provided by ToolProvider.getSystemJavaCompiler(). Make sure the jdk.compiler module is available."); + + // create source file + File sourceFile = new File(className.replace(".", File.separator) + ".java"); + if (!sourceFile.getParentFile().exists() && !sourceFile.getParentFile().mkdirs()) + throw new RuntimeException("Cannot create parent folder: " + sourceFile.getParentFile()); + + try { + // write source file + try (FileWriter writer = new FileWriter(sourceFile)) { + writer.write(content); + writer.flush(); + } + // 1. compiler output, use System.err if null + StringWriter out = new StringWriter(); + // 2. a diagnostic listener; if null use the compiler's default method for reporting diagnostics + DiagnosticCollector diagnostics = new DiagnosticCollector<>(); + // 3. a file manager; if null use the compiler's standard file manager + StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, null); + // 4. compiler options, null means no options + List options = buildCompileOptions(compileOptions); + // 5. the compilation units to compile, null means no compilation units + Iterable compilationUnit = fileManager.getJavaFileObjectsFromFiles(Collections.singletonList(sourceFile)); + + CompilationTask task = compiler.getTask(out, fileManager, diagnostics, options, null, compilationUnit); + if (!compileOptions.processors.isEmpty()) task.setProcessors(compileOptions.processors); + Boolean isCompileSuccess = task.call(); + fileManager.close(); + + if (Boolean.FALSE.equals(isCompileSuccess)) { + diagnostics.getDiagnostics().forEach(System.err::println); + throw new RuntimeException("Error while compiling " + className + ", System.err for more."); + } + + // Load compiled class + URL[] generatedClassUrls = {new File("./").toURI().toURL()}; + try (URLClassLoader classLoader = new URLClassLoader(generatedClassUrls)) { + return classLoader.loadClass(className); + } + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException("Error while compiling " + className, e); + } + } + + private static List buildCompileOptions(CompileOptions compileOptions) throws URISyntaxException { + List options = new ArrayList<>(compileOptions.options); + if (!options.contains("-classpath")) { + options.add("-classpath"); + options.add(getClassPath()); + } + return options; + } + + public static String getClassPath() throws URISyntaxException { + ClassLoader cl = MethodHandles.lookup().lookupClass().getClassLoader(); + + StringBuilder classpath = new StringBuilder(); + String separator = System.getProperty("path.separator"); + String cp = System.getProperty("java.class.path"); + String mp = System.getProperty("jdk.module.path"); + + if (cp != null && !"".equals(cp)) classpath.append(cp); + if (mp != null && !"".equals(mp)) classpath.append(mp); + + /* [java-16] */ + // if (cl instanceof URLClassLoader) { + // for (URL url : ((URLClassLoader) cl).getURLs()) { + /* [/java-16] */ + if (cl instanceof URLClassLoader urlClassLoader) { + for (URL url : urlClassLoader.getURLs()) { + if (classpath.length() > 0) classpath.append(separator); + if ("file".equals(url.getProtocol())) classpath.append(new File(url.toURI())); + } + } + return classpath.toString(); + } + + public static void main(String... args) { + test(); + } + + public static void test() { + String name = "vo.Cat"; + String clzDef = """ + package vo; + + //import lombok.*; + + //@Builder + //@Data + public class Cat { + private String name; + private Integer age; + } + """; + Class clz = compile(name, clzDef); + List methods = Arrays.stream(clz.getDeclaredMethods()) + .map(method -> method.getName() + "(" + Arrays.stream(method.getGenericParameterTypes()) + .map(type -> type.getTypeName().substring(type.getTypeName().lastIndexOf('.') + 1)) + .collect(Collectors.joining(",")) + ")").toList(); + + System.out.printf("compile done, clz: %s, clz's declared methods: %s%n", clz, methods); + } + + public static final class CompileOptions { + + final List processors; + final List options; + + public CompileOptions() { + this( + Collections.emptyList(), + Collections.emptyList() + ); + } + + private CompileOptions( + List processors, + List options + ) { + this.processors = processors; + this.options = options; + } + + public CompileOptions processors(Processor... newProcessors) { + return processors(Arrays.asList(newProcessors)); + } + + public CompileOptions processors(List newProcessors) { + return new CompileOptions(newProcessors, options); + } + + public CompileOptions options(String... newOptions) { + return options(Arrays.asList(newOptions)); + } + + public CompileOptions options(List newOptions) { + return new CompileOptions(processors, newOptions); + } + + boolean hasOption(String opt) { + for (String option : options) + if (option.equalsIgnoreCase(opt)) + return true; + + return false; + } + } + + // get lombok AnnotationProcessor + //public static Processor createLombokAnnotationProcessor() { + // printf("----%ncreate processor%n"); + // Processor annotationProcessor = null; + // ClassLoader classLoader = Lombok.class.getClassLoader(); + // try { + // Class aClass = classLoader.loadClass("lombok.launch.AnnotationProcessorHider"); + // for (Class declaredClass : aClass.getDeclaredClasses()) { + // if ("AnnotationProcessor".equals(declaredClass.getSimpleName())) { + // for (Constructor declaredConstructor : declaredClass.getDeclaredConstructors()) { + // declaredConstructor.setAccessible(true); + // int parameterCount = declaredConstructor.getParameterCount(); + // if (parameterCount == 0) { + // annotationProcessor = (Processor) declaredConstructor.newInstance(); + // break; + // } + // } + // } + // } + // System.out.printf("found lombok annotation processor: %s%n", annotationProcessor.getClass().getCanonicalName()); + // } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | InvocationTargetException e) { + // throw new RuntimeException(e); + // } + // return annotationProcessor; + //} +} + diff --git a/src/main/resources/print.jshell b/src/main/resources/print.jshell index 925875b..31e2af2 100644 --- a/src/main/resources/print.jshell +++ b/src/main/resources/print.jshell @@ -41,14 +41,20 @@ import java.util.regex.Pattern; public class Printer { private static final Pattern VAR_IDX_PATTERN = Pattern.compile("(?i)\\$JShell\\$(\\d+)"); private static final String METHOD_NAME = "print"; + private static final Pattern methodPattern = Pattern.compile(METHOD_NAME + "\\s*\\(\\s*(?(.*)|(\".*\"))\\s*\\)"); + private static final Pattern methodStrictPattern = Pattern.compile(METHOD_NAME + "\\s*\\(\\s*(?\\w+(\\.\\w+(\\(.*\\))?)*)\\s*\\)"); + private static final List COMMENT_PATTERNS = List.of("/\\*(.|\\s)*?\\*/", "//.*\\n*"); - private static final List COMMENT_PATTERNS = List.of("/\\*(.|\\s)*?\\*/", "//.*\\n*", "\\s+"); + // {String: snippet id, int: pos} + private static final Object[] methodSourcePos = {null, 0}; + + // java ansi code demo: https://stackoverflow.com/a/5762502 + private static final String varNameStylePattern = "\u001B[36m%s\u001B[0m"; private static int varIdx = 0; + //private static String prefix = "printer| "; private static String prefix = ""; - // java ansi code demo: https://stackoverflow.com/a/5762502 - private static final String varNameStylePattern = "\u001B[36m%s\u001B[0m"; private static JShell jshell; @@ -64,6 +70,15 @@ public class Printer { } } + public static int countSubStr(String src, String dst, int startIdx) { + int cnt = 0; + while ((startIdx = src.indexOf(dst, startIdx)) >= 0) { + cnt++; + startIdx += dst.length() - 1; + } + return cnt; + } + public static void print(Object obj) { String varName = null; @@ -80,11 +95,18 @@ public class Printer { if (snippetOptional.isPresent()) { Snippet snippet = snippetOptional.get(); String source = snippet.source(); - if (source.contains(METHOD_NAME)) { - for (String pattern : COMMENT_PATTERNS) { - source = source.replaceAll(pattern, ""); + for (String pattern : COMMENT_PATTERNS) source = source.replaceAll(pattern, ""); + int occuCnt = countSubStr(source, METHOD_NAME, 0); + if (occuCnt > 0) { + if (!id.equals(methodSourcePos[0])) { + methodSourcePos[0] = id; + methodSourcePos[1] = 0; + } + Matcher methodMatcher = (occuCnt == 1 ? methodPattern : methodStrictPattern).matcher(source); + if (methodMatcher.find((int) methodSourcePos[1])) { + methodSourcePos[1] = methodMatcher.end(); + varName = methodMatcher.group("content").replaceAll("\\s+", " "); } - varName = source.substring(source.indexOf("(") + 1, source.lastIndexOf(")")); } } } diff --git a/src/test/java/io/github/spencerpark/ijava/TestUtils.java b/src/test/java/io/github/spencerpark/ijava/TestUtils.java index 5ebe91c..89dc5a9 100644 --- a/src/test/java/io/github/spencerpark/ijava/TestUtils.java +++ b/src/test/java/io/github/spencerpark/ijava/TestUtils.java @@ -1,13 +1,16 @@ package io.github.spencerpark.ijava; import io.github.spencerpark.ijava.utils.FileUtils; +import io.github.spencerpark.ijava.utils.RuntimeCompiler; import org.junit.Assert; import org.junit.Test; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; import java.nio.file.Path; -import java.util.Collections; -import java.util.Map; -import java.util.Set; +import java.util.*; +import java.util.stream.Collectors; public class TestUtils { @Test @@ -36,4 +39,47 @@ public void testReadXml() { System.out.println(e.getMessage()); } } + + //@Test + public void testCompile() { + String name = "vo.Cat"; + String clzDef = """ + package vo; + + //import lombok.Data; + + //@Data + public class Cat { + private String name; + private Integer age; + } + """; + Class clz = RuntimeCompiler.compile(name, clzDef, true); + List methods = Arrays.stream(clz.getDeclaredMethods()) + .map(method -> method.getName() + "(" + Arrays.stream(method.getGenericParameterTypes()) + .map(type -> type.getTypeName().substring(type.getTypeName().lastIndexOf('.') + 1)) + .collect(Collectors.joining(",")) + ")").toList(); + + System.out.printf("compile done, clz: %s, clz's declared methods: %s%n", clz, methods); + } + + //@Test + public void testProcess() throws IOException { + String[] commands = {"ping", "localhost"}; + Process proc = Runtime.getRuntime().exec(commands); + + String s = null; + try (InputStreamReader inputStreamReader = new InputStreamReader(proc.getInputStream()); + BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) { + while ((s = bufferedReader.readLine()) != null) { + System.out.println(s); + } + } + try (InputStreamReader inputStreamReader = new InputStreamReader(proc.getErrorStream()); + BufferedReader bufferedReader = new BufferedReader(inputStreamReader)) { + while ((s = bufferedReader.readLine()) != null) { + System.out.println(s); + } + } + } }