diff --git a/README.adoc b/README.adoc index 81e15d4f..bd8cfdc1 100644 --- a/README.adoc +++ b/README.adoc @@ -1371,3 +1371,50 @@ Just set extension property `tomitribe.crest.useInPlaceRegistrations` to `true`: ---- This enables to use the same scanning for both tasks and therefore to have a common and unified scanning for java and native runs. + +== Run commands from source + +There is an experimental mode in crest enabling you to write commands in a standalone `.java` and run it with `tomitribe-crest.jar` fatjar directly. +The trick is to pass as first parameter `--crest.source=/path/to/file/with/commands.java`. + +[source,java] +.commands.java +---- +import org.tomitribe.crest.api.Command; +import java.time.LocalDate; + +class Command1 { + @Command + String ok() { + return "ok"; + } +} + +@Command +class Command2 { + @Command + String time() { + return LocalDate.now().toString(); + } +} +---- + +IMPORTANT: this mode has some limitation like not supporting (yet) a dynamic classpath so it just uses the script and the launching classpath. + +TIP: to ease writing scripts with completion the lines starting with `//-- ` will drop this prefix. +It avoids compilations errors in general on classes no in the classpath (crest there). + +Usage example: + +[source,bash] +---- +java \ + -jar tomitribe-crest-0.20-fatjar.jar \ + --crest.source=mycommands.java \ + my-command \ + --foo=bar +---- + +IMPORTANT: this requires to run with a *JDK* to work. + +TIP: you can also use it in a script easily. diff --git a/tomitribe-crest-arthur-extension/src/main/java/org/tomitribe/crest/arthur/svm/MainSubstitute.java b/tomitribe-crest-arthur-extension/src/main/java/org/tomitribe/crest/arthur/svm/MainSubstitute.java new file mode 100644 index 00000000..e5c75a86 --- /dev/null +++ b/tomitribe-crest-arthur-extension/src/main/java/org/tomitribe/crest/arthur/svm/MainSubstitute.java @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.tomitribe.crest.arthur.svm; + +import com.oracle.svm.core.annotate.Substitute; +import com.oracle.svm.core.annotate.TargetClass; +import org.tomitribe.crest.Main; + +@TargetClass(Main.class) +public final class MainSubstitute { + @Substitute + private void handleSource(final String command) { + throw new UnsupportedOperationException("In native mode, compiler feature is disabled"); + } +} diff --git a/tomitribe-crest-arthur-extension/src/main/java/org/tomitribe/crest/arthur/svm/SimpleCompilerDelete.java b/tomitribe-crest-arthur-extension/src/main/java/org/tomitribe/crest/arthur/svm/SimpleCompilerDelete.java new file mode 100644 index 00000000..f5ce1aa0 --- /dev/null +++ b/tomitribe-crest-arthur-extension/src/main/java/org/tomitribe/crest/arthur/svm/SimpleCompilerDelete.java @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.tomitribe.crest.arthur.svm; + +import com.oracle.svm.core.annotate.Delete; +import com.oracle.svm.core.annotate.TargetClass; +import org.tomitribe.crest.compiler.SimpleCompiler; + +@Delete("native mode will not support compilation on the fly") +@TargetClass(SimpleCompiler.class) +public final class SimpleCompilerDelete { +} diff --git a/tomitribe-crest/pom.xml b/tomitribe-crest/pom.xml index 6df01930..2399b9f0 100644 --- a/tomitribe-crest/pom.xml +++ b/tomitribe-crest/pom.xml @@ -81,16 +81,22 @@ ${project.version} test - - com.google.auto.service - auto-service - 1.0-rc2 - true - + + org.apache.maven.plugins + maven-compiler-plugin + + + default-compile + + none + + + + org.apache.maven.plugins maven-surefire-plugin @@ -103,6 +109,42 @@ + + org.apache.maven.plugins + maven-shade-plugin + 3.3.0 + + + package + + shade + + + true + fatjar + + + org.tomitribe.crest.Main + + + + + org.apache.xbean:* + + + + + *:* + + META-INF/LICENSE.txt + META-INF/NOTICE.txt + + + + + + + diff --git a/tomitribe-crest/src/main/java/org/tomitribe/crest/Main.java b/tomitribe-crest/src/main/java/org/tomitribe/crest/Main.java index 664ec1ac..9b13ab30 100644 --- a/tomitribe-crest/src/main/java/org/tomitribe/crest/Main.java +++ b/tomitribe-crest/src/main/java/org/tomitribe/crest/Main.java @@ -26,10 +26,12 @@ import org.tomitribe.crest.cmds.HelpPrintedException; import org.tomitribe.crest.cmds.processors.Commands; import org.tomitribe.crest.cmds.processors.Help; +import org.tomitribe.crest.compiler.SimpleCompiler; import org.tomitribe.crest.contexts.DefaultsContext; import org.tomitribe.crest.contexts.SystemPropertiesDefaultsContext; import org.tomitribe.crest.environments.Environment; import org.tomitribe.crest.environments.SystemEnvironment; +import org.tomitribe.crest.interceptor.InterceptorAnnotationNotFoundException; import org.tomitribe.crest.interceptor.internal.InternalInterceptor; import org.tomitribe.crest.table.Formatting; import org.tomitribe.crest.table.TableInterceptor; @@ -50,10 +52,14 @@ import java.util.function.Consumer; import java.util.stream.Stream; -public class Main implements Completer { +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toMap; - protected final Map commands = new ConcurrentHashMap<>(); - protected final Map, InternalInterceptor> interceptors = new HashMap<>(); +public class Main implements Completer { + // this is "exposed" so we don't type them as ContextualizableMap for children + protected final Map commands = new ContextualizableMap<>(); + protected final Map, InternalInterceptor> interceptors = new ContextualizableMap<>(); + protected final DefaultsContext defaultsContext; public Main() { this(new SystemPropertiesDefaultsContext(), Commands.load()); @@ -68,6 +74,8 @@ public Main(final DefaultsContext defaultsContext, final Class... classes) { } public Main(final DefaultsContext defaultsContext, final Iterable> classes) { + this.defaultsContext = defaultsContext; + for (final Class clazz : classes) { processClass(defaultsContext, clazz); } @@ -84,29 +92,14 @@ public void processClass(final DefaultsContext defaultsContext, final Class c if (!m.isEmpty()) { this.commands.putAll(m); } else { - - final InternalInterceptor internalInterceptor = InternalInterceptor.from(clazz); - if (interceptors.put(clazz, internalInterceptor) != null) { - throw new IllegalArgumentException(clazz + " interceptor is conflicting"); - } - - for (final Annotation annotation : clazz.getDeclaredAnnotations()) { - if (isCustomInterceptorAnnotation(annotation)) { - if (interceptors.put(annotation.annotationType(), internalInterceptor) != null) { - throw new IllegalArgumentException(clazz + " interceptor is conflicting"); - } - } - } + storeInterceptors(interceptors, clazz); } } private static boolean isCustomInterceptorAnnotation(final Annotation annotation) { - for (final Annotation declaredAnnotation : annotation.annotationType().getDeclaredAnnotations()) { - if (declaredAnnotation instanceof CrestInterceptor) { - return true; - } - } - return false; + return Stream.of(annotation.annotationType().getDeclaredAnnotations()) + .map(Annotation::annotationType) + .anyMatch(it -> it == CrestInterceptor.class); } public Main(final Iterable> classes) { @@ -238,25 +231,118 @@ public void main(final Environment env, final String... args) throws Exception { public Object exec(String... args) throws Exception { final List list = processSystemProperties(args); - final String command = (list.isEmpty()) ? "help" : list.remove(0); - args = list.toArray(new String[list.size()]); - + String command = (list.isEmpty()) ? "help" : list.remove(0); if (command.equals("_completion")) { return BashCompletion.generate(this, args); } - final Cmd cmd = commands.get(command); + final Runnable cleanup; + if (command.startsWith("--crest.source=")) { + // todo: support multiple? for now all commands can be in a single file + cleanup = handleSource(command); + command = (list.isEmpty()) ? "help" : list.remove(0); + args = list.toArray(new String[0]); + } else { + args = list.toArray(new String[0]); + cleanup = null; + } + + try { + final Cmd cmd = commands.get(command); + + if (cmd == null) { + + final PrintStream err = Environment.ENVIRONMENT_THREAD_LOCAL.get().getError(); + err.println("Unknown command: " + command); + err.println(); + commands.get("help").exec(interceptors); + throw new IllegalArgumentException(); + } + + return cmd.exec(interceptors, args); + } finally { + if (cleanup != null) { + cleanup.run(); + } + } + } + + // DON'T MODIFY WITHOUT FIXING org.tomitribe.crest.arthur.svm.DropCompilerSupport + private Runnable handleSource(final String command) { + final List> additionalClasses = SimpleCompiler + .compile(command.substring("--crest.source=".length())) + .ownedClasses() + .collect(toList()); + + final Map tempCommands = new HashMap<>(); + final Map, InternalInterceptor> tempInterceptors = new HashMap<>(); + final ThreadLocal> commandTL = ((ContextualizableMap) commands).current; + final ThreadLocal, InternalInterceptor>> interceptorTL = + ((ContextualizableMap, InternalInterceptor>) interceptors).current; + final Map oldCommands = commandTL.get(); + final Map, InternalInterceptor> oldInterceptors = interceptorTL.get(); + commandTL.set(tempCommands); + interceptorTL.set(tempInterceptors); + + additionalClasses.forEach(clazz -> { + final Map m = Commands.get(clazz, defaultsContext); + if (!m.isEmpty()) { + tempCommands.putAll(m); + } else if (!interceptors.containsKey(clazz)) { // we don't want to override global config + try { + storeInterceptors(tempInterceptors, clazz); + } catch (final InterceptorAnnotationNotFoundException ignore) { + // no-op + } + } + }); + + if (!tempCommands.isEmpty()) { + // override help to reflect actual state + final Map commandsWithHelp = Stream.concat( + commands.entrySet().stream() + .filter(it -> !"help".equals(it.getKey())), + tempCommands.entrySet().stream()) + .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); + Commands.get(new Help(commandsWithHelp), defaultsContext) + .values() + .forEach(c -> { + tempCommands.put(c.getName(), c); + commandsWithHelp.put(c.getName(), c); + }); + } + + return () -> { + if (oldCommands == null) { + commandTL.remove(); + } else { + commandTL.set(oldCommands); + } + if (oldInterceptors == null) { + interceptorTL.remove(); + } else { + interceptorTL.set(oldInterceptors); + } + }; + } - if (cmd == null) { + private boolean isCrestAnnotation(final Annotation a) { + return a.annotationType().getName().startsWith("org.tomitribe.crest.api"); + } - final PrintStream err = Environment.ENVIRONMENT_THREAD_LOCAL.get().getError(); - err.println("Unknown command: " + command); - err.println(); - commands.get("help").exec(interceptors); - throw new IllegalArgumentException(); + private void storeInterceptors(final Map, InternalInterceptor> tempInterceptors, final Class clazz) { + final InternalInterceptor internalInterceptor = InternalInterceptor.from(clazz); + if (tempInterceptors.put(clazz, internalInterceptor) != null) { + throw new IllegalArgumentException(clazz + " interceptor is conflicting"); } - return cmd.exec(interceptors, args); + for (final Annotation annotation : clazz.getDeclaredAnnotations()) { + if (isCustomInterceptorAnnotation(annotation)) { + if (tempInterceptors.put(annotation.annotationType(), internalInterceptor) != null) { + throw new IllegalArgumentException(clazz + " interceptor is conflicting"); + } + } + } } public static List processSystemProperties(final String[] args) { @@ -325,4 +411,22 @@ private Cmd getCmd(String buffer) { return null; } + + private static class ContextualizableMap extends ConcurrentHashMap { + private final ThreadLocal> current = new ThreadLocal<>(); + + @Override + public B get(final Object key) { + final Map local = current.get(); + if (local == null) { + current.remove(); + } else { + final B value = local.get(key); + if (value != null) { + return value; + } + } + return super.get(key); + } + } } diff --git a/tomitribe-crest/src/main/java/org/tomitribe/crest/cmds/processors/Commands.java b/tomitribe-crest/src/main/java/org/tomitribe/crest/cmds/processors/Commands.java index 749eca82..cd99de4b 100644 --- a/tomitribe-crest/src/main/java/org/tomitribe/crest/cmds/processors/Commands.java +++ b/tomitribe-crest/src/main/java/org/tomitribe/crest/cmds/processors/Commands.java @@ -30,8 +30,6 @@ import org.tomitribe.crest.val.BeanValidationImpl; import org.tomitribe.util.Strings; import org.tomitribe.util.collect.FilteredIterable; -import org.tomitribe.util.collect.FilteredIterator; -import org.tomitribe.util.reflect.Reflection; import java.io.BufferedReader; import java.io.IOException; @@ -46,9 +44,11 @@ import java.util.LinkedHashSet; import java.util.Map; import java.util.ServiceLoader; +import java.util.stream.Stream; import static java.util.Arrays.asList; import static java.util.Optional.ofNullable; +import static java.util.stream.Collectors.toList; public class Commands { @@ -57,12 +57,15 @@ private Commands() { } public static Iterable commands(final Class clazz) { - return new FilteredIterable<>(Reflection.methods(clazz), - new FilteredIterator.Filter() { - @Override - public boolean accept(final Method method) { - return method.isAnnotationPresent(Command.class); + return new FilteredIterable<>( + new FilteredIterable<>( + methods(clazz).collect(toList()), + method -> method.isAnnotationPresent(Command.class)), + method -> { + if (!method.isAccessible()) { + method.setAccessible(true); } + return true; } ); } @@ -241,4 +244,12 @@ private static void onClass(final Collection> classes, final Class c classes.add(clazz); } } + + // not only public since we want to ease code encapsulation and script like style + private static Stream methods(final Classclazz) { + if (clazz == null || clazz == Object.class) { + return Stream.empty(); + } + return Stream.concat(Stream.of(clazz.getDeclaredMethods()), methods(clazz.getSuperclass())); + } } diff --git a/tomitribe-crest/src/main/java/org/tomitribe/crest/cmds/targets/SimpleBean.java b/tomitribe-crest/src/main/java/org/tomitribe/crest/cmds/targets/SimpleBean.java index bbc89ed6..5591ff63 100644 --- a/tomitribe-crest/src/main/java/org/tomitribe/crest/cmds/targets/SimpleBean.java +++ b/tomitribe-crest/src/main/java/org/tomitribe/crest/cmds/targets/SimpleBean.java @@ -44,15 +44,26 @@ private Object getBean(final Method method) { return bean; } if (Modifier.isStatic(method.getModifiers())) { - return bean; + return null; } try { final Class declaringClass = method.getDeclaringClass(); final Constructor constructor = declaringClass.getConstructor(); return constructor.newInstance(); - } catch (final NoSuchMethodException e) { - return null; + } catch (final NoSuchMethodException | IllegalAccessException e) { + try { + final Class declaringClass = method.getDeclaringClass(); + final Constructor constructor = declaringClass.getDeclaredConstructor(); + if (!constructor.isAccessible()) { + constructor.setAccessible(true); + } + return constructor.newInstance(); + } catch (final NoSuchMethodException e2) { + return null; + } catch (final Throwable e2) { + throw new IllegalStateException(e); + } } catch (final InvocationTargetException e) { throw new IllegalStateException(e.getCause()); } catch (final Throwable e) { diff --git a/tomitribe-crest/src/main/java/org/tomitribe/crest/compiler/SimpleCompiler.java b/tomitribe-crest/src/main/java/org/tomitribe/crest/compiler/SimpleCompiler.java new file mode 100644 index 00000000..8d16a1c2 --- /dev/null +++ b/tomitribe-crest/src/main/java/org/tomitribe/crest/compiler/SimpleCompiler.java @@ -0,0 +1,169 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.tomitribe.crest.compiler; + +import javax.tools.Diagnostic; +import javax.tools.DiagnosticCollector; +import javax.tools.FileObject; +import javax.tools.ForwardingJavaFileManager; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileManager; +import javax.tools.JavaFileObject; +import javax.tools.SimpleJavaFileObject; +import javax.tools.ToolProvider; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.function.Supplier; +import java.util.logging.Logger; +import java.util.stream.Stream; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Collections.singleton; + +public final class SimpleCompiler { + private SimpleCompiler() { + // no-op + } + + public static MemoryLoader compile(final String file) { + final Path path = Paths.get(file); + if (!Files.exists(path)) { + throw new IllegalArgumentException("No source file at '" + path.toAbsolutePath().normalize() + "'"); + } + + final Locale locale = Locale.getDefault(); + final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + final DiagnosticCollector collector = new DiagnosticCollector<>(); + final MemoryLoader loader = new MemoryLoader(); + try { + final JavaFileObject from = new StringInputJavaFileObject( + path.toAbsolutePath().normalize().toUri(), + new String(Files.readAllBytes(path)).replace("//-- ", "")); + final JavaCompiler.CompilationTask task = compiler.getTask( + null, + new ForwardingJavaFileManager(compiler.getStandardFileManager(null, locale, UTF_8)) { + @Override + public JavaFileObject getJavaFileForOutput(final Location location, final String className, + final JavaFileObject.Kind kind, final FileObject sibling) { + final ClassOutputJavaFileObject clazz = new ClassOutputJavaFileObject(URI.create("class://" + className + ".class"), className); + loader.registry.put(className, clazz.bytecode::toByteArray); + return clazz; + } + }, + collector, null, null, + singleton(from)); + + boolean success = task.call(); + final List> diagnostics = collector.getDiagnostics(); + if (!diagnostics.isEmpty()) { + final Logger logger = Logger.getLogger(SimpleCompiler.class.getName()); + success = diagnostics.stream() + .peek(d -> logMessage(locale, logger, d)) + .reduce(success, (c, it) -> it.getKind() != Diagnostic.Kind.ERROR && c, (a, b) -> a && b); + } + + if (!success) { + throw new IllegalStateException("Invalid compilation of '" + path.toAbsolutePath().normalize() + "'"); + } + return loader; + } catch (final IOException ioe) { + throw new IllegalStateException(ioe); + } + } + + private static void logMessage(final Locale locale, final Logger logger, Diagnostic d) { + final String message = "" + + "[" + (d.getSource() == null ? "-" : d.getSource().toUri()) + "]" + + "[" + d.getLineNumber() + ", " + d.getColumnNumber() + "] " + + d.getMessage(locale); + switch (d.getKind()) { + case ERROR: + logger.severe(message); + break; + + // keep in mind it stays a cli so we don't want these errors generally + case WARNING: + case MANDATORY_WARNING: // it is generally ok to ignore them + logger.fine(message); + break; + default: // more than ok to ignore + logger.finest(message); + } + } + + public static class MemoryLoader extends ClassLoader { + private final Map> registry = new HashMap<>(); + + @Override + protected Class findClass(final String name) throws ClassNotFoundException { + final Supplier bytes = registry.get(name); + if (bytes != null) { + final byte[] content = bytes.get(); + return defineClass(name, content, 0, content.length); + } + return super.findClass(name); + } + + public Stream> ownedClasses() { + return registry.keySet().stream().map(name -> { + try { + return loadClass(name); + } catch (final ClassNotFoundException e) { + throw new IllegalArgumentException(e); + } + }); + } + } + + private static class ClassOutputJavaFileObject extends SimpleJavaFileObject { + private final ByteArrayOutputStream bytecode = new ByteArrayOutputStream(); + private final String className; + + private ClassOutputJavaFileObject(final URI uri, final String className) { + super(uri, Kind.CLASS); + this.className = className; + } + + @Override + public OutputStream openOutputStream() { + return bytecode; + } + } + + private static class StringInputJavaFileObject extends SimpleJavaFileObject { + private final String source; + + private StringInputJavaFileObject(final URI uri, String content) { + super(uri, Kind.SOURCE); + this.source = content; + } + + @Override + public CharSequence getCharContent(final boolean ignoreEncodingErrors) { + return source; + } + } +} diff --git a/tomitribe-crest/src/main/java/org/tomitribe/crest/help/HelpProcessor.java b/tomitribe-crest/src/main/java/org/tomitribe/crest/help/HelpProcessor.java index 7d347c4a..e0aeccca 100644 --- a/tomitribe-crest/src/main/java/org/tomitribe/crest/help/HelpProcessor.java +++ b/tomitribe-crest/src/main/java/org/tomitribe/crest/help/HelpProcessor.java @@ -17,13 +17,11 @@ package org.tomitribe.crest.help; -import com.google.auto.service.AutoService; import org.tomitribe.crest.api.Command; import org.tomitribe.crest.api.Option; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.Filer; -import javax.annotation.processing.Processor; import javax.annotation.processing.RoundEnvironment; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; @@ -44,9 +42,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -@AutoService(Processor.class) public class HelpProcessor extends AbstractProcessor { - @Override public Set getSupportedAnnotationTypes() { final Set annotations = new LinkedHashSet(); diff --git a/tomitribe-crest/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/tomitribe-crest/src/main/resources/META-INF/services/javax.annotation.processing.Processor new file mode 100644 index 00000000..46592215 --- /dev/null +++ b/tomitribe-crest/src/main/resources/META-INF/services/javax.annotation.processing.Processor @@ -0,0 +1 @@ +org.tomitribe.crest.help.HelpProcessor diff --git a/tomitribe-crest/src/test/java/org/tomitribe/crest/MainCompilerTest.java b/tomitribe-crest/src/test/java/org/tomitribe/crest/MainCompilerTest.java new file mode 100644 index 00000000..5510ea97 --- /dev/null +++ b/tomitribe-crest/src/test/java/org/tomitribe/crest/MainCompilerTest.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.tomitribe.crest; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tomitribe.crest.api.Command; +import org.tomitribe.crest.compiler.SimpleCompiler; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.stream.Collectors.toList; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class MainCompilerTest { + @Rule + public final TemporaryFolder folder = new TemporaryFolder(); + + @Test + public void compile() throws Exception { + final Path source = folder.getRoot().toPath().resolve("Source.java"); + Files.write(source, ("" + + "//-- import org.tomitribe.crest.api.Command;\n" + + "\n" + + "//-- @Command\n" + + "class MyCommand2 {\n" + + " //-- @Command\n" + + " public static String test() { return \"ok\"; }\n" + + "}\n" + + "\n" + + "").getBytes(StandardCharsets.UTF_8)); + assertEquals("ok", new Main(emptyList()).exec("--crest.source=" + source, "myCommand2", "test")); + } +} diff --git a/tomitribe-crest/src/test/java/org/tomitribe/crest/compiler/SimpleCompilerTest.java b/tomitribe-crest/src/test/java/org/tomitribe/crest/compiler/SimpleCompilerTest.java new file mode 100644 index 00000000..2aa4b6bc --- /dev/null +++ b/tomitribe-crest/src/test/java/org/tomitribe/crest/compiler/SimpleCompilerTest.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.tomitribe.crest.compiler; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.tomitribe.crest.Main; +import org.tomitribe.crest.api.Command; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import static java.util.Arrays.asList; +import static java.util.stream.Collectors.toList; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class SimpleCompilerTest { + @Rule + public final TemporaryFolder folder = new TemporaryFolder(); + + @Test + public void compile() throws Exception { + final Path source = folder.getRoot().toPath().resolve("Source.java"); + Files.write(source, ("" + + "@org.tomitribe.crest.api.Command\n" + + "class MyCommand2 {\n" + + " @org.tomitribe.crest.api.Command\n" + + " public static String test() { return \"ok\"; }\n" + + "}\n" + + "public class Source {\n" + + "}\n" + + "\n" + + "").getBytes(StandardCharsets.UTF_8)); + + final SimpleCompiler.MemoryLoader loader = SimpleCompiler.compile(source.toString()); + assertEquals( + asList("MyCommand2", "Source"), + loader.ownedClasses().map(Class::getName).sorted().collect(toList())); + + final Class myCommand2 = loader.loadClass("MyCommand2"); + assertTrue(myCommand2.isAnnotationPresent(Command.class)); + + final Main main = new Main(myCommand2); + assertEquals("ok", main.exec("myCommand2", "test")); + } +}