From bdd39b74ffbdfeec281261bb5a79330eda0ccc6c Mon Sep 17 00:00:00 2001 From: markfisher Date: Wed, 21 Sep 2016 22:33:06 -0400 Subject: [PATCH] initial commit --- .gitignore | 23 ++ README.md | 7 + pom.xml | 108 +++++++++ spring-cloud-function-compiler/pom.xml | 45 ++++ .../function/compiler/FunctionCompiler.java | 140 ++++++++++++ .../function/compiler/FunctionFactory.java | 28 +++ ...eableFilterableJavaFileObjectIterable.java | 86 +++++++ .../java/CompilationFailedException.java | 40 ++++ .../compiler/java/CompilationMessage.java | 128 +++++++++++ .../java/CompilationOutputCollector.java | 69 ++++++ .../compiler/java/CompilationResult.java | 72 ++++++ .../java/CompiledClassDefinition.java | 58 +++++ .../compiler/java/DirEntryJavaFileObject.java | 138 +++++++++++ .../compiler/java/DirEnumeration.java | 112 +++++++++ .../function/compiler/java/EmptyIterable.java | 47 ++++ .../compiler/java/InMemoryJavaFileObject.java | 216 ++++++++++++++++++ .../compiler/java/IterableClasspath.java | 214 +++++++++++++++++ .../java/MemoryBasedJavaFileManager.java | 168 ++++++++++++++ .../java/NestedZipEntryJavaFileObject.java | 172 ++++++++++++++ .../compiler/java/RuntimeJavaCompiler.java | 105 +++++++++ .../compiler/java/SimpleClassLoader.java | 38 +++ .../compiler/java/ZipEntryJavaFileObject.java | 146 ++++++++++++ spring-cloud-function-core/.jdk8 | 0 spring-cloud-function-core/pom.xml | 36 +++ .../function/gateway/FunctionGateway.java | 107 +++++++++ .../invoker/AbstractFunctionInvoker.java | 41 ++++ .../invoker/FunctionInvokingRunnable.java | 47 ++++ .../function/registry/FunctionRegistry.java | 32 +++ .../registry/FunctionRegistrySupport.java | 35 +++ .../registry/InMemoryFunctionRegistry.java | 38 +++ spring-cloud-function-stream/.jdk8 | 0 spring-cloud-function-stream/pom.xml | 50 ++++ .../function/stream/StreamApplication.java | 31 +++ .../function/stream/StreamConfiguration.java | 49 ++++ .../stream/StreamConfigurationProperties.java | 46 ++++ .../StreamListeningFunctionInvoker.java | 43 ++++ 36 files changed, 2715 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 pom.xml create mode 100644 spring-cloud-function-compiler/pom.xml create mode 100644 spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/FunctionCompiler.java create mode 100644 spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/FunctionFactory.java create mode 100644 spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CloseableFilterableJavaFileObjectIterable.java create mode 100644 spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompilationFailedException.java create mode 100644 spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompilationMessage.java create mode 100644 spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompilationOutputCollector.java create mode 100644 spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompilationResult.java create mode 100644 spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompiledClassDefinition.java create mode 100644 spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/DirEntryJavaFileObject.java create mode 100644 spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/DirEnumeration.java create mode 100644 spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/EmptyIterable.java create mode 100644 spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/InMemoryJavaFileObject.java create mode 100644 spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/IterableClasspath.java create mode 100644 spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/MemoryBasedJavaFileManager.java create mode 100644 spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/NestedZipEntryJavaFileObject.java create mode 100644 spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/RuntimeJavaCompiler.java create mode 100644 spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/SimpleClassLoader.java create mode 100644 spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/ZipEntryJavaFileObject.java create mode 100644 spring-cloud-function-core/.jdk8 create mode 100644 spring-cloud-function-core/pom.xml create mode 100644 spring-cloud-function-core/src/main/java/org/springframework/cloud/function/gateway/FunctionGateway.java create mode 100644 spring-cloud-function-core/src/main/java/org/springframework/cloud/function/invoker/AbstractFunctionInvoker.java create mode 100644 spring-cloud-function-core/src/main/java/org/springframework/cloud/function/invoker/FunctionInvokingRunnable.java create mode 100644 spring-cloud-function-core/src/main/java/org/springframework/cloud/function/registry/FunctionRegistry.java create mode 100644 spring-cloud-function-core/src/main/java/org/springframework/cloud/function/registry/FunctionRegistrySupport.java create mode 100644 spring-cloud-function-core/src/main/java/org/springframework/cloud/function/registry/InMemoryFunctionRegistry.java create mode 100644 spring-cloud-function-stream/.jdk8 create mode 100644 spring-cloud-function-stream/pom.xml create mode 100644 spring-cloud-function-stream/src/main/java/org/springframework/cloud/function/stream/StreamApplication.java create mode 100644 spring-cloud-function-stream/src/main/java/org/springframework/cloud/function/stream/StreamConfiguration.java create mode 100644 spring-cloud-function-stream/src/main/java/org/springframework/cloud/function/stream/StreamConfigurationProperties.java create mode 100644 spring-cloud-function-stream/src/main/java/org/springframework/cloud/function/stream/StreamListeningFunctionInvoker.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..e1e4dd3e5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +/application.yml +/application.properties +asciidoctor.css +*~ +.#* +*# +target/ +build/ +bin/ +_site/ +.classpath +.project +.settings/ +.springBeans +.DS_Store +*.sw* +*.iml +*.ipr +*.iws +.idea/ +.factorypath +coverage-error.log +.apt_generated diff --git a/README.md b/README.md new file mode 100644 index 000000000..0b856efad --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +Example: + +``` +java -jar spring-cloud-function-stream-1.0.0.BUILD-SNAPSHOT.jar --server.port=8081 --spring.cloud.stream.bindings.input.destination=words --spring.cloud.stream.bindings.output.destination=uppercaseWords --function.name=uppercase --function.code="f -> f.map(s -> s.toString().toUpperCase())" +``` + +(more docs soon) diff --git a/pom.xml b/pom.xml new file mode 100644 index 000000000..037f4c649 --- /dev/null +++ b/pom.xml @@ -0,0 +1,108 @@ + + + 4.0.0 + + spring-cloud-function-parent + Spring Cloud Function Parent + 1.0.0.BUILD-SNAPSHOT + pom + + + org.springframework.cloud + spring-cloud-build + 1.2.0.RELEASE + + + + + 1.8 + 3.0.0.RELEASE + 1.1.0.BUILD-SNAPSHOT + + + + + + org.springframework.cloud + spring-cloud-stream + ${spring-cloud-stream.version} + + + io.projectreactor + reactor-core + ${reactor.version} + + + + + + spring-cloud-function-compiler + spring-cloud-function-core + spring-cloud-function-stream + + + + + + spring + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/libs-snapshot-local + + true + + + false + + + + spring-milestones + Spring Milestones + https://repo.spring.io/libs-milestone-local + + false + + + + spring-releases + Spring Releases + https://repo.spring.io/release + + false + + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/libs-snapshot-local + + true + + + false + + + + spring-milestones + Spring Milestones + https://repo.spring.io/libs-milestone-local + + false + + + + spring-releases + Spring Releases + https://repo.spring.io/libs-release-local + + false + + + + + + diff --git a/spring-cloud-function-compiler/pom.xml b/spring-cloud-function-compiler/pom.xml new file mode 100644 index 000000000..4e6d0fd9d --- /dev/null +++ b/spring-cloud-function-compiler/pom.xml @@ -0,0 +1,45 @@ + + + 4.0.0 + + spring-cloud-function-compiler + jar + Spring Cloud Function Compiler + Spring Cloud Function Compiler + + + org.springframework.cloud + spring-cloud-function-parent + 1.0.0.BUILD-SNAPSHOT + + + + 1.8 + + + + + io.projectreactor + reactor-core + + + org.eclipse.jdt.core.compiler + ecj + 4.4.2 + + + commons-io + commons-io + 2.3 + + + commons-collections + commons-collections + + + org.springframework.boot + spring-boot-starter-logging + + + + diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/FunctionCompiler.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/FunctionCompiler.java new file mode 100644 index 000000000..3268f2b20 --- /dev/null +++ b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/FunctionCompiler.java @@ -0,0 +1,140 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed 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.springframework.cloud.function.compiler; + +import java.util.List; +import java.util.function.Function; +import java.util.regex.Matcher; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.cloud.function.compiler.java.CompilationFailedException; +import org.springframework.cloud.function.compiler.java.CompilationMessage; +import org.springframework.cloud.function.compiler.java.CompilationResult; +import org.springframework.cloud.function.compiler.java.RuntimeJavaCompiler; + +/** + * @author Andy Clement + * @author Mark Fisher + */ +public class FunctionCompiler { + + private static Logger logger = LoggerFactory.getLogger(FunctionCompiler.class); + + // Newlines in the property are escaped + private static final String NEWLINE_ESCAPE = Matcher.quoteReplacement("\\n"); + + // Individual double-quote characters are represented by two double quotes in the DSL + private static final String DOUBLE_DOUBLE_QUOTE = Matcher.quoteReplacement("\"\""); + + private final static String PACKAGE = "org.springframework.cloud.function.compiler"; + + private final static String MAIN_COMPILED_CLASS_NAME = PACKAGE + ".GeneratedFunctionFactory"; + + /** + * The user supplied code snippet is inserted into the template and then the result is compiled + */ + private static String SOURCE_CODE_TEMPLATE = + "package " + PACKAGE + ";\n" + + "import java.util.*;\n" + // Helpful to include this + "import java.util.function.*;\n" + + "import reactor.core.publisher.Flux;\n" + + "public class GeneratedFunctionFactory implements FunctionFactory {\n" + + " public Function, Flux> getFunction() {\n" + + " %s\n" + + " }\n" + + "}\n"; + + private final RuntimeJavaCompiler compiler = new RuntimeJavaCompiler(); + + /** + * Produce a Function instance by:
    + *
  • Decoding the code String to process any newlines/double-double-quotes + *
  • Insert the code into the source code template for a class + *
  • Compiling the class using the JDK provided Java Compiler + *
  • Loading the compiled class + *
  • Invoking a well known method on the factory class to produce a Function instance + *
  • Returning that instance. + *
+ * + * @return a Function instance + */ + public Function compile(String code) { + logger.info("Initial code property value :'{}'", code); + code = decode(code); + if (code.startsWith("\"") && code.endsWith("\"")) { + code = code.substring(1,code.length()-1); + } + if (!code.startsWith("return ") && !code.endsWith(";")) { + code = "return " + code + ";"; + } + logger.info("Processed code property value :\n{}\n", code); + CompilationResult compilationResult = buildAndCompileSourceCode(code); + if (compilationResult.wasSuccessful()) { + List> clazzes = compilationResult.getCompiledClasses(); + logger.info("Compilation resulted in #{} classes", clazzes.size()); + for (Class clazz: clazzes) { + if (clazz.getName().equals(MAIN_COMPILED_CLASS_NAME)) { + try { + FunctionFactory functionFactory = (FunctionFactory) clazz.newInstance(); + return functionFactory.getFunction(); + } + catch (Exception e) { + logger.error("Unexpected problem during retrieval of Function from compiled class", e); + } + } + System.out.println(clazz.getName()); + } + logger.error("Failed to find the expected compiled class"); + } + List compilationMessages = compilationResult.getCompilationMessages(); + throw new CompilationFailedException(compilationMessages); + } + + /** + * Create the source for and then compile and load a class that embodies + * the supplied methodBody. The methodBody is inserted into a class template that + * returns a Function<Flux<Object>,Flux<Object>>. + * This method can return more than one class if the method body includes local class + * declarations. An example methodBody would be return input -> input.buffer(5).map(list->list.get(0));. + * + * @param methodBody the source code for a method that should return a + * Function<Flux<Object>,Flux<Object>> + * @return the list of Classes produced by compiling and then loading the snippet of code + */ + private CompilationResult buildAndCompileSourceCode(String methodBody) { + String sourceCode = makeSourceClassDefinition(methodBody); + return compiler.compile(MAIN_COMPILED_CLASS_NAME, sourceCode); + } + + private static String decode(String input) { + return input.replaceAll(NEWLINE_ESCAPE, "\n").replaceAll(DOUBLE_DOUBLE_QUOTE, "\""); + } + + /** + * Make a full source code definition for a class by applying the specified method body + * to the Reactive template. + * + * @param methodBody the code to insert into the Reactive source class template + * @return a complete Java Class definition + */ + public static String makeSourceClassDefinition(String methodBody) { + return String.format(SOURCE_CODE_TEMPLATE, methodBody); + } + +} diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/FunctionFactory.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/FunctionFactory.java new file mode 100644 index 000000000..559d8f949 --- /dev/null +++ b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/FunctionFactory.java @@ -0,0 +1,28 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed 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.springframework.cloud.function.compiler; + +import java.util.function.Function; + +/** + * @author Mark Fisher + */ +public interface FunctionFactory { + + Function getFunction(); + +} diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CloseableFilterableJavaFileObjectIterable.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CloseableFilterableJavaFileObjectIterable.java new file mode 100644 index 000000000..a087b7dc7 --- /dev/null +++ b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CloseableFilterableJavaFileObjectIterable.java @@ -0,0 +1,86 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed 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.springframework.cloud.function.compiler.java; + +import java.io.File; + +import javax.tools.JavaFileObject; + +/** + * Common superclass for iterables that need to handle closing when finished + * with and that need to handle possible constraints on the values that + * are iterated over. + * + * @author Andy Clement + */ +public abstract class CloseableFilterableJavaFileObjectIterable implements Iterable { + +// private final static Logger logger = LoggerFactory.getLogger(CloseableFilterableJavaFileObjectIterable.class); + + private final static boolean BOOT_PACKAGING_AWARE = true; + private final static String BOOT_PACKAGING_PREFIX_FOR_CLASSES = "BOOT-INF/classes/"; + + // If set specifies the package the iterator consumer is interested in. Only + // return results in this package. + private String packageNameFilter; + + // Indicates whether the consumer of the iterator wants to see classes + // that are in subpackages of those matching the filter. + private boolean includeSubpackages; + + public CloseableFilterableJavaFileObjectIterable(String packageNameFilter, boolean includeSubpackages) { + if (packageNameFilter!=null && packageNameFilter.contains(File.separator)) { + throw new IllegalArgumentException("Package name filters should use dots to separate components: "+packageNameFilter); + } + this.packageNameFilter = packageNameFilter==null?null:packageNameFilter.replace('.', File.separatorChar) + "/"; + this.includeSubpackages = includeSubpackages; + } + + /** + * Used by subclasses to check values against any specified constraints. + * + * @param name the name to check against the criteria + * @return true if the name is a valid iterator result based on the specified criteria + */ + protected boolean accept(String name) { +// logger.debug("checking {} against constraints packageNameFilter={} includeSubpackages={}",name,packageNameFilter,includeSubpackages); + if (!name.endsWith(".class")) { + return false; + } + if (packageNameFilter == null) { + return true; + } + boolean accept; + if (includeSubpackages == true) { + accept = name.startsWith(packageNameFilter); + if (!accept && BOOT_PACKAGING_AWARE) { + accept = name.startsWith(BOOT_PACKAGING_PREFIX_FOR_CLASSES) && + name.indexOf(packageNameFilter)==BOOT_PACKAGING_PREFIX_FOR_CLASSES.length(); + } + } else { + accept = name.startsWith(packageNameFilter) && name.indexOf("/",packageNameFilter.length())==-1; + if (!accept && BOOT_PACKAGING_AWARE) { + accept = name.startsWith(BOOT_PACKAGING_PREFIX_FOR_CLASSES) && + name.indexOf(packageNameFilter)==BOOT_PACKAGING_PREFIX_FOR_CLASSES.length() && + name.indexOf("/",BOOT_PACKAGING_PREFIX_FOR_CLASSES.length()+packageNameFilter.length())==-1; + } + } + return accept; + } + + abstract void close(); +} \ No newline at end of file diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompilationFailedException.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompilationFailedException.java new file mode 100644 index 000000000..d5e3f784d --- /dev/null +++ b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompilationFailedException.java @@ -0,0 +1,40 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed 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.springframework.cloud.function.compiler.java; + +import java.util.List; + +/** + * @author Mark Fisher + */ +public class CompilationFailedException extends RuntimeException { + + public CompilationFailedException(List messages) { + super(consolidateMessages(messages)); + } + + private static String consolidateMessages(List messages) { + if (messages == null || messages.isEmpty()) { + return ""; + } + StringBuilder sb = new StringBuilder(); + for (CompilationMessage message : messages) { + sb.append(message.toString()); + } + return sb.toString(); + } +} diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompilationMessage.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompilationMessage.java new file mode 100644 index 000000000..f8aa1c6e0 --- /dev/null +++ b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompilationMessage.java @@ -0,0 +1,128 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed 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.springframework.cloud.function.compiler.java; + +/** + * Encapsulate information produced during compilation. A message may be an error + * or something less serious (warning/informational). The toString() method + * will produce a formatted error include source context indicating the precise + * location of the problem. + * + * @author Andy Clement + */ +public class CompilationMessage { + + private Kind kind; + private String message; + private String sourceCode; + private int startPosition; + private int endPosition; + + enum Kind { + ERROR, OTHER + }; + + public CompilationMessage(Kind kind, String message, String sourceCode, int startPosition, int endPosition) { + this.kind = kind; + this.message = message; + this.sourceCode = sourceCode; + this.startPosition = startPosition; + this.endPosition = endPosition; + } + + /** + * @return the type of message + */ + public Kind getKind() { + return this.kind; + } + + /** + * @return the message text + */ + public String getMessage() { + return this.message; + } + + /** + * @return the source code for the file associated with the message + */ + public String getSourceCode() { + return this.sourceCode; + } + + /** + * @return offset from start of source file where the error begins + */ + public int getStartPosition() { + return this.startPosition; + } + + /** + * @return offset from start of source file where the error ends + */ + public int getEndPosition() { + return this.endPosition; + } + + public String toString() { + StringBuilder s = new StringBuilder(); + s.append("==========\n"); + if (sourceCode != null) { // Cannot include source context if no source available + int[] lineStartEnd = getLineStartEnd(startPosition); + s.append(sourceCode.substring(lineStartEnd[0], lineStartEnd[1])).append("\n"); + int col = lineStartEnd[0]; + // When inserting the whitespace, ensure tabs in the source line are respected + while ((col) < startPosition) { + s.append(sourceCode.charAt(col++)=='\t'?"\t":" "); + } + // Want at least one ^ + s.append("^"); + col++; + while ((col++) < endPosition) { + s.append("^"); + } + s.append("\n"); + } + s.append(kind).append(":").append(message).append("\n"); + s.append("==========\n"); + return s.toString(); + } + + /** + * For a given position in the source code this method returns a pair of int + * that indicate the start and end of the line within the source code that + * contain the position. + * + * @param searchPos the position of interest in the source code + * @return an int array of length 2 containing the start and end positions of the line + */ + private int[] getLineStartEnd(int searchPos) { + int previousPos = -1; + int pos = 0; + do { + pos = sourceCode.indexOf('\n', previousPos + 1); + if (searchPos < pos) { + return new int[] { previousPos + 1, pos }; + } + previousPos = pos; + } while (pos != -1); + return new int[] { previousPos + 1, sourceCode.length() }; + } + // TODO test coverage for first line/last line situations + +} diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompilationOutputCollector.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompilationOutputCollector.java new file mode 100644 index 000000000..6b8398a22 --- /dev/null +++ b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompilationOutputCollector.java @@ -0,0 +1,69 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed 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.springframework.cloud.function.compiler.java; + +import java.util.ArrayList; +import java.util.List; + +import javax.tools.FileObject; +import javax.tools.JavaFileManager.Location; +import javax.tools.JavaFileObject.Kind; + +/** + * During compilation instances of this class will collect up the output files from the compilation process. + * Any kind of file is collected upon but access is only currently provided to retrieve classes produced + * during compilation. Annotation processors that run may create other kinds of artifact. + * + * @author Andy Clement + */ +public class CompilationOutputCollector { + + private List outputFiles = new ArrayList<>(); + + /** + * Retrieve compiled classes that have been collected since this collector + * was built. Due to annotation processing it is possible other source files + * or metadata files may be produced during compilation - those are not included + * in the returned list. + * + * @return list of compiled classes + */ + public List getCompiledClasses() { + List compiledClassDefinitions = new ArrayList<>(); + for (InMemoryJavaFileObject outputFile : outputFiles) { + if (outputFile.getKind() == Kind.CLASS) { + CompiledClassDefinition compiledClassDefinition = new CompiledClassDefinition(outputFile.getName(), + outputFile.getBytes()); + compiledClassDefinitions.add(compiledClassDefinition); + } + } + return compiledClassDefinitions; + } + + public InMemoryJavaFileObject getJavaFileForOutput(Location location, String className, Kind kind, FileObject sibling) { + InMemoryJavaFileObject jfo = InMemoryJavaFileObject.getJavaFileObject(location, className, kind, sibling); + outputFiles.add(jfo); + return jfo; + } + + public InMemoryJavaFileObject getFileForOutput(Location location, String packageName, String relativeName, FileObject sibling) { + InMemoryJavaFileObject ojfo = InMemoryJavaFileObject.getFileObject(location, packageName, relativeName, sibling); + outputFiles.add(ojfo); + return ojfo; + } + +} \ No newline at end of file diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompilationResult.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompilationResult.java new file mode 100644 index 000000000..8a2967c21 --- /dev/null +++ b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompilationResult.java @@ -0,0 +1,72 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed 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.springframework.cloud.function.compiler.java; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Holder for the results of compilation. If compilation was successful the set + * of classes that resulted from compilation will be available. If compilation + * was not successful the error messages should provide information about why. + * Note that compilation may succeed and yet there will still be informational or + * warning messages collected. + * + * @author Andy Clement + */ +public class CompilationResult { + + private boolean successfulCompilation; + + List compilationMessages = new ArrayList<>(); + + List> compiledClasses = new ArrayList<>(); + + public CompilationResult(boolean successfulCompilation) { + this.successfulCompilation = successfulCompilation; + } + + public boolean wasSuccessful() { + return successfulCompilation; + } + + public List> getCompiledClasses() { + return compiledClasses; + } + + public List getCompilationMessages() { + return Collections.unmodifiableList(compilationMessages); + } + + public void recordCompilationMessage(CompilationMessage compilationMessage) { + this.compilationMessages.add(compilationMessage); + } + + public void setCompiledClasses(List> compiledClasses) { + this.compiledClasses = compiledClasses; + } + + public String toString() { + StringBuilder s = new StringBuilder(); + s.append("Compilation result: #classes="+compiledClasses.size()+" #messages="+compilationMessages.size()+"\n"); + s.append("Compiled classes:\n").append(compiledClasses).append("\n"); + s.append("Compilation messages:\n").append(compilationMessages).append("\n"); + return s.toString(); + } + +} diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompiledClassDefinition.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompiledClassDefinition.java new file mode 100644 index 000000000..fcfb7ef60 --- /dev/null +++ b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompiledClassDefinition.java @@ -0,0 +1,58 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed 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.springframework.cloud.function.compiler.java; + +import java.io.File; + +/** + * Encapsulates a name with the bytes for its class definition. + * + * @author Andy Clement + */ +public class CompiledClassDefinition { + + private byte[] bytes; + private String filename; + private String classname; + + public CompiledClassDefinition(String filename, byte[] bytes) { + this.filename = filename; + this.bytes = bytes; + this.classname = filename; + if (classname.startsWith(File.separator)) { + classname = classname.substring(1); + } + classname = classname.replace(File.separatorChar, '.').substring(0, classname.length()-6);//strip off .class + } + + public String getName() { + return filename; + } + + public byte[] getBytes() { + return bytes; + } + + public String toString() { + return "CompiledClassDefinition(name=" + getName() + ",#bytes=" + getBytes().length + ")"; + } + + public String getClassName() { + return this.classname; + } + +} \ No newline at end of file diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/DirEntryJavaFileObject.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/DirEntryJavaFileObject.java new file mode 100644 index 000000000..949927778 --- /dev/null +++ b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/DirEntryJavaFileObject.java @@ -0,0 +1,138 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed 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.springframework.cloud.function.compiler.java; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.Writer; +import java.net.URI; + +import javax.lang.model.element.Modifier; +import javax.lang.model.element.NestingKind; +import javax.tools.JavaFileObject; + +/** + * A JavaFileObject that represents a file in a directory. + * + * @author Andy Clement + */ +public class DirEntryJavaFileObject implements JavaFileObject { + + private File file; + private File basedir; + + public DirEntryJavaFileObject(File basedir, File file) { + this.basedir = basedir; + this.file = file; + } + + @Override + public URI toUri() { + return file.toURI(); + } + + /** + * @return the path of the file relative to the base directory, for example: a/b/c/D.class + */ + @Override + public String getName() { + String basedirPath = basedir.getPath(); + String filePath = file.getPath(); + return filePath.substring(basedirPath.length()+1); + } + + @Override + public InputStream openInputStream() throws IOException { + return new FileInputStream(file); + } + + @Override + public OutputStream openOutputStream() throws IOException { + throw new IllegalStateException("Only expected to be used for input"); + } + + @Override + public Reader openReader(boolean ignoreEncodingErrors) throws IOException { + // It is bytecode + throw new UnsupportedOperationException("openReader() not supported on class file: " + getName()); + } + + @Override + public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { + // It is bytecode + throw new UnsupportedOperationException("getCharContent() not supported on class file: " + getName()); + } + + @Override + public Writer openWriter() throws IOException { + throw new IllegalStateException("only expected to be used for input"); + } + + @Override + public long getLastModified() { + return file.lastModified(); + } + + @Override + public boolean delete() { + return false; // This object is for read only access to a class + } + + @Override + public Kind getKind() { + return Kind.CLASS; + } + + @Override + public boolean isNameCompatible(String simpleName, Kind kind) { + if (kind != Kind.CLASS) { + return false; + } + String name = getName(); + int lastSlash = name.lastIndexOf('/'); + return name.substring(lastSlash + 1).equals(simpleName + ".class"); + } + + @Override + public NestingKind getNestingKind() { + return null; + } + + @Override + public Modifier getAccessLevel() { + return null; + } + + @Override + public int hashCode() { + return file.getName().hashCode()*37+basedir.getName().hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof DirEntryJavaFileObject)) { + return false; + } + DirEntryJavaFileObject that = (DirEntryJavaFileObject)obj; + return (basedir.getName().equals(that.basedir.getName())) && (file.getName().equals(that.file.getName())); + } + +} \ No newline at end of file diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/DirEnumeration.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/DirEnumeration.java new file mode 100644 index 000000000..59777bf6a --- /dev/null +++ b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/DirEnumeration.java @@ -0,0 +1,112 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed 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.springframework.cloud.function.compiler.java; + +import java.io.File; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.NoSuchElementException; + +/** + * Walks a directory hierarchy from some base directory discovering files. + * + * @author Andy Clement + */ +public class DirEnumeration implements Enumeration { + +// private final static Logger logger = LoggerFactory.getLogger(DirEnumeration.class); + + // The starting point + private File basedir; + + // Candidates collected so far + private List filesToReturn; + + // Places still to explore for candidates + private List directoriesToExplore; + + public DirEnumeration(File basedir) { + this.basedir = basedir; + } + + private void computeValue() { + if (filesToReturn == null) { // Indicates we haven't started yet + filesToReturn = new ArrayList<>(); + directoriesToExplore = new ArrayList<>(); + visitDirectory(basedir); + } + if (filesToReturn.size() == 0) { + while (filesToReturn.size() == 0 && directoriesToExplore.size() != 0) { + File nextDir = directoriesToExplore.get(0); + directoriesToExplore.remove(0); + visitDirectory(nextDir); + } + } + } + + @Override + public boolean hasMoreElements() { + computeValue(); + return filesToReturn.size() != 0; + } + + @Override + public File nextElement() { + computeValue(); + if (filesToReturn.size()==0) { + throw new NoSuchElementException(); + } + File toReturn = filesToReturn.get(0); + filesToReturn.remove(0); + return toReturn; + } + + private void visitDirectory(File dir) { + File[] files = dir.listFiles(); + if (files != null) { + for (File file: files) { + if (file.isDirectory()) { + directoriesToExplore.add(file); + } else { + filesToReturn.add(file); + } + } + } +// logger.debug("after visiting {} filesToReturn=#{} dirsToExplore=#{}",dir,filesToReturn.size(), directoriesToExplore.size()); + } + + public File getDirectory() { + return basedir; + } + + /** + * Return the relative path of this file to the base directory that the directory enumeration was + * started for. + * @param file a file discovered returned by this enumeration + * @return the relative path of the file (for example: a/b/c/D.class) + */ + public String getName(File file) { + String basedirPath = basedir.getPath(); + String filePath = file.getPath(); + if (!filePath.startsWith(basedirPath)) { + throw new IllegalStateException("The file '"+filePath+"' is not nested below the base directory '"+basedirPath+"'"); + } + return filePath.substring(basedirPath.length()+1); + } + +} diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/EmptyIterable.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/EmptyIterable.java new file mode 100644 index 000000000..4741a87b5 --- /dev/null +++ b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/EmptyIterable.java @@ -0,0 +1,47 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed 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.springframework.cloud.function.compiler.java; + +import java.util.Iterator; + +import javax.tools.JavaFileObject; + +import org.apache.commons.collections.IteratorUtils; + +/** + * Simple iterable that can be used to return an iterator over no values. + * + * @author Andy Clement + */ +class EmptyIterable extends CloseableFilterableJavaFileObjectIterable { + + static EmptyIterable instance = new EmptyIterable(); + + private EmptyIterable() { + super(null,false); + } + + public void close() { + } + + @SuppressWarnings("unchecked") + @Override + public Iterator iterator() { + return IteratorUtils.emptyIterator(); + } + +} \ No newline at end of file diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/InMemoryJavaFileObject.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/InMemoryJavaFileObject.java new file mode 100644 index 000000000..ef012dac0 --- /dev/null +++ b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/InMemoryJavaFileObject.java @@ -0,0 +1,216 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed 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.springframework.cloud.function.compiler.java; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.CharArrayWriter; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.Reader; +import java.io.Writer; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.Charset; + +import javax.lang.model.element.Modifier; +import javax.lang.model.element.NestingKind; +import javax.tools.FileObject; +import javax.tools.JavaFileManager.Location; +import javax.tools.JavaFileObject; +import javax.tools.StandardLocation; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A JavaFileObject that represents a source artifact created for compilation or an output + * artifact producing during compilation (a .class file or some other thing if an annotation + * processor has run). In order to be clear what it is being used for there are static factory + * methods that ask for specific types of file. + * + * @author Andy Clement + */ +public class InMemoryJavaFileObject implements JavaFileObject { + + private final static Logger logger = LoggerFactory.getLogger(InMemoryJavaFileObject.class); + + private Location location; + private String packageName; + private String relativeName; + private FileObject sibling; + private String className; + private Kind kind; + + private byte[] content = null; + private long lastModifiedTime = 0; + private URI uri = null; + + private InMemoryJavaFileObject() {} + + public static InMemoryJavaFileObject getFileObject(Location location, String packageName, String relativeName, FileObject sibling) { + InMemoryJavaFileObject retval = new InMemoryJavaFileObject(); + retval.kind = Kind.OTHER; + retval.location = location; + retval.packageName = packageName; + retval.relativeName = relativeName; + retval.sibling = sibling; + return retval; + } + + public static InMemoryJavaFileObject getJavaFileObject(Location location, String className, Kind kind, FileObject sibling) { + InMemoryJavaFileObject retval = new InMemoryJavaFileObject(); + retval.location = location; + retval.className = className; + retval.kind = kind; + retval.sibling = sibling; + return retval; + } + + public static InMemoryJavaFileObject getSourceJavaFileObject(String className, String content) { + InMemoryJavaFileObject retval = new InMemoryJavaFileObject(); + retval.location = StandardLocation.SOURCE_PATH; + retval.className = className; + retval.kind = Kind.SOURCE; + retval.content = content.getBytes(); + return retval; + } + + public byte[] getBytes() { + return content; + } + + public String toString() { + return "OutputJavaFileObject: Location="+location+",className="+className+",kind="+kind+",relativeName="+relativeName+",sibling="+sibling+",packageName="+packageName; + } + + @Override + public URI toUri() { + // These memory based output files 'pretend' to be relative to the file system root + if (uri == null) { + String name = null; + if (className != null) { + name = className.replace('.', '/'); + } else if (packageName !=null && packageName.length()!=0) { + name = packageName.replace('.', '/')+'/'+relativeName; + } else { + name = relativeName; + } + + String uriString = null; + try { + uriString = "file:/"+name+kind.extension; + uri = new URI(uriString); + } catch (URISyntaxException e) { + throw new IllegalStateException("Unexpected URISyntaxException for string '" + uriString + "'", e); + } + } + return uri; + } + + @Override + public String getName() { + return toUri().getPath(); + } + + @Override + public InputStream openInputStream() throws IOException { + if (content == null) { + throw new FileNotFoundException(); + } + logger.debug("opening input stream for {}",getName()); + return new ByteArrayInputStream(content); + } + + @Override + public OutputStream openOutputStream() throws IOException { + logger.debug("opening output stream for {}",getName()); + return new ByteArrayOutputStream() { + @Override + public void close() throws IOException { + super.close(); + lastModifiedTime = System.currentTimeMillis(); + content = this.toByteArray(); + } + }; + } + + @Override + public Reader openReader(boolean ignoreEncodingErrors) throws IOException { + return new InputStreamReader(openInputStream(), Charset.defaultCharset()); + } + + @Override + public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { + if (kind!=Kind.SOURCE) { + throw new UnsupportedOperationException("getCharContent() not supported on file object: " + getName()); + } + // Not yet supporting encodings + return (content==null?null:new String(content)); + } + + @Override + public Writer openWriter() throws IOException { + // Let's not enforce this restriction right now +// if (kind == Kind.CLASS) { +// throw new UnsupportedOperationException("openWriter() not supported on file object: " + getName()); +// } + return new CharArrayWriter() { + @Override + public void close() { + lastModifiedTime = System.currentTimeMillis(); + content = new String(toCharArray()).getBytes(); // Ignoring encoding... + }; + }; + } + + @Override + public long getLastModified() { + return lastModifiedTime; + } + + @Override + public boolean delete() { + return false; + } + + @Override + public Kind getKind() { + return kind; + } + + public boolean isNameCompatible(String simpleName, Kind kind) { + String baseName = simpleName + kind.extension; + return kind.equals(getKind()) + && (baseName.equals(toUri().getPath()) + || toUri().getPath().endsWith("/" + baseName)); + } + + @Override + public NestingKind getNestingKind() { + return null; + } + + @Override + public Modifier getAccessLevel() { + return null; + } + +} \ No newline at end of file diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/IterableClasspath.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/IterableClasspath.java new file mode 100644 index 000000000..7bd55b688 --- /dev/null +++ b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/IterableClasspath.java @@ -0,0 +1,214 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed 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.springframework.cloud.function.compiler.java; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Stack; +import java.util.StringTokenizer; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipInputStream; + +import javax.tools.JavaFileObject; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Iterable that will produce an iterator that returns classes found + * on a specified classpath that meet specified criteria. For jars it finds, the + * iterator will go into nested jars - this handles the situation with a + * spring boot uberjar. + * + * @author Andy Clement + */ +public class IterableClasspath extends CloseableFilterableJavaFileObjectIterable { + + private static Logger logger = LoggerFactory.getLogger(IterableClasspath.class); + + private static final String BOOT_PACKAGING_PREFIX_FOR_LIBRARIES = "BOOT-INF/lib/"; + + private List classpathEntries = new ArrayList<>(); + + private List openArchives = new ArrayList<>(); + + /** + * @param classpath a classpath of jars/directories + * @param packageNameFilter an optional package name if choosing to filter (e.g. com.example) + * @param includeSubpackages if true, include results in subpackages of the specified package filter + */ + IterableClasspath(String classpath, String packageNameFilter, boolean includeSubpackages) { + super(packageNameFilter, includeSubpackages); + StringTokenizer tokenizer = new StringTokenizer(classpath, File.pathSeparator); + while (tokenizer.hasMoreElements()) { + String nextEntry = tokenizer.nextToken(); + File f = new File(nextEntry); + if (f.exists()) { + classpathEntries.add(f); + } else { + logger.debug("path element does not exist {}",f); + } + } + } + + public void close() { + for (ZipFile openArchive : openArchives) { + try { + openArchive.close(); + } catch (IOException ioe) { + logger.debug("Unexpected error closing archive {}",openArchive,ioe); + } + } + openArchives.clear(); + } + + public Iterator iterator() { + return new ClasspathEntriesIterator(); + } + + class ClasspathEntriesIterator implements Iterator { + private int currentClasspathEntriesIndex = 0; + + // Either a directory or an archive will be open at any one time + private File openDirectory = null; + private DirEnumeration openDirectoryEnumeration = null; + + private ZipFile openArchive = null; + private File openFile = null; + private ZipEntry nestedZip = null; + private Stack> openArchiveEnumeration = null; + + private JavaFileObject nextEntry = null; + + private void findNext() { + if (nextEntry == null) { + try { + while (openArchive!=null || openDirectory!=null || currentClasspathEntriesIndex < classpathEntries.size()) { + if (openArchive == null && openDirectory == null) { + // Open the next item + File nextFile = classpathEntries.get(currentClasspathEntriesIndex); + if (nextFile.isDirectory()) { + openDirectory = nextFile; + openDirectoryEnumeration = new DirEnumeration(nextFile); + } else { + openFile = nextFile; + openArchive = new ZipFile(nextFile); + openArchives.add(openArchive); + openArchiveEnumeration = new Stack>(); + openArchiveEnumeration.push(openArchive.entries()); + } + currentClasspathEntriesIndex++; + } + if (openArchiveEnumeration != null) { + while (!openArchiveEnumeration.isEmpty()) { + while (openArchiveEnumeration.peek().hasMoreElements()) { + ZipEntry entry = openArchiveEnumeration.peek().nextElement(); + String entryName = entry.getName(); + if (accept(entryName)) { + if (nestedZip!=null) { + nextEntry = new NestedZipEntryJavaFileObject(openFile, openArchive,nestedZip, entry); + } else { + nextEntry = new ZipEntryJavaFileObject(openFile, openArchive, entry); + } + return; + } else if (nestedZip == null && entryName.startsWith(BOOT_PACKAGING_PREFIX_FOR_LIBRARIES) && entryName.endsWith(".jar")) { + // nested jar in uber jar + logger.debug("opening nested archive {}",entry.getName()); + ZipInputStream zis = new ZipInputStream(openArchive.getInputStream(entry)); + // nextEntry = new NestedZipEntryJavaFileObject(openArchive.firstElement(),openArchive.peek(),entry); + Enumeration nestedZipEnumerator = new ZipEnumerator(zis); + nestedZip = entry; + openArchiveEnumeration.push(nestedZipEnumerator); + } + } + openArchiveEnumeration.pop(); + if (nestedZip ==null) { openArchive = null; openFile = null; } + else nestedZip = null; + } + openArchiveEnumeration = null; + openArchive = null; + openFile = null; + } else if (openDirectoryEnumeration != null) { + while (openDirectoryEnumeration.hasMoreElements()) { + File entry = openDirectoryEnumeration.nextElement(); + String name = openDirectoryEnumeration.getName(entry); + if (accept(name)) { + nextEntry = new DirEntryJavaFileObject(openDirectoryEnumeration.getDirectory(), entry); + return; + } + } + openDirectoryEnumeration = null; + openDirectory = null; + } + } + } catch (IOException ioe) { + logger.debug("Unexpected error whilst processing classpath entries",ioe); + } + } + } + + public boolean hasNext() { + findNext(); + return nextEntry != null; + } + + public JavaFileObject next() { + findNext(); + if (nextEntry == null) { + throw new NoSuchElementException(); + } + JavaFileObject retval = nextEntry; + nextEntry = null; + return retval; + } + + } + + static class ZipEnumerator implements Enumeration{ + + private ZipInputStream zis; + private ZipEntry nextEntry = null; + + public ZipEnumerator(ZipInputStream zis) { + this.zis = zis; + } + + @Override + public boolean hasMoreElements() { + try { + nextEntry = zis.getNextEntry(); + } catch (IOException ioe) { + nextEntry=null; + } + return nextEntry!=null; + } + + @Override + public ZipEntry nextElement() { + ZipEntry retval = nextEntry; + nextEntry = null; + return retval; + } + + } +} \ No newline at end of file diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/MemoryBasedJavaFileManager.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/MemoryBasedJavaFileManager.java new file mode 100644 index 000000000..1d04b7697 --- /dev/null +++ b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/MemoryBasedJavaFileManager.java @@ -0,0 +1,168 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed 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.springframework.cloud.function.compiler.java; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import javax.tools.FileObject; +import javax.tools.JavaFileManager; +import javax.tools.JavaFileObject; +import javax.tools.JavaFileObject.Kind; +import javax.tools.StandardLocation; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A file manager that serves source code from in memory and ensures output results are kept in memory + * rather than being flushed out to disk. The JavaFileManager is also used as a lookup mechanism + * for resolving types. + * + * @author Andy Clement + */ +public class MemoryBasedJavaFileManager implements JavaFileManager { + + private static Logger logger = LoggerFactory.getLogger(MemoryBasedJavaFileManager.class); + + private CompilationOutputCollector outputCollector; + + private List toClose = new ArrayList<>(); + + public MemoryBasedJavaFileManager() { + outputCollector = new CompilationOutputCollector(); + } + + @Override + public int isSupportedOption(String option) { + logger.debug("isSupportedOption({})",option); + return -1; // Not yet supporting options + } + + @Override + public ClassLoader getClassLoader(Location location) { + // Do not simply return the context classloader as it may get closed and then + // be unusable for loading any further classes + logger.debug("getClassLoader({})",location); + return null; // Do not currently need to load plugins + } + + @Override + public Iterable list(Location location, String packageName, Set kinds, boolean recurse) + throws IOException { + logger.debug("list({},{},{},{})",location,packageName,kinds,recurse); + CloseableFilterableJavaFileObjectIterable resultIterable = null; + if (location == StandardLocation.PLATFORM_CLASS_PATH && (kinds==null || kinds.contains(Kind.CLASS))) { + String sunBootClassPath = System.getProperty("sun.boot.class.path"); + logger.debug("Creating iterable for boot class path: {}",sunBootClassPath); + resultIterable = new IterableClasspath(sunBootClassPath, packageName, recurse); + toClose.add(resultIterable); + } else if (location == StandardLocation.CLASS_PATH && (kinds==null || kinds.contains(Kind.CLASS))) { + String javaClassPath = System.getProperty("java.class.path"); + logger.debug("Creating iterable for class path: {}",javaClassPath); + resultIterable = new IterableClasspath(javaClassPath, packageName, recurse); + toClose.add(resultIterable); + } else if (location == StandardLocation.SOURCE_PATH) { + // There are no 'extra sources' + resultIterable = EmptyIterable.instance; + } else { + // Nothing to list + resultIterable = EmptyIterable.instance; + } + return resultIterable; + } + + @Override + public boolean hasLocation(Location location) { + logger.debug("hasLocation({})",location); + return (location == StandardLocation.SOURCE_PATH || + location == StandardLocation.CLASS_PATH || + location == StandardLocation.PLATFORM_CLASS_PATH); + } + + @Override + public String inferBinaryName(Location location, JavaFileObject file) { + if (location == StandardLocation.SOURCE_PATH) { + return null; + } + // Kind of ignoring location here... assuming we want basically the FQ type name + // Example value from getName(): javax/validation/bootstrap/GenericBootstrap.class + String classname = file.getName().replace('/', '.'); + return classname.substring(0, classname.lastIndexOf(".class")); + } + + @Override + public boolean isSameFile(FileObject a, FileObject b) { + logger.debug("isSameFile({},{})",a,b); + return a.equals(b); + } + + @Override + public boolean handleOption(String current, Iterator remaining) { + logger.debug("handleOption({},{})",current,remaining); + return false; // This file manager does not manage any options + } + + + @Override + public JavaFileObject getJavaFileForInput(Location location, String className, Kind kind) throws IOException { + logger.debug("getJavaFileForInput({},{},{})",location,className,kind); + throw new IllegalStateException("Not expected to be used in this context"); + } + + @Override + public JavaFileObject getJavaFileForOutput(Location location, String className, Kind kind, FileObject sibling) + throws IOException { + logger.debug("getJavaFileForOutput({},{},{},{})",location,className,kind,sibling); + // Example parameters: CLASS_OUTPUT, Foo, CLASS, StringBasedJavaSourceFileObject[string:///a/b/c/Foo.java] + return outputCollector.getJavaFileForOutput(location, className, kind, sibling); + } + + @Override + public FileObject getFileForInput(Location location, String packageName, String relativeName) throws IOException { + logger.debug("getFileForInput({},{},{})",location,packageName,relativeName); + throw new IllegalStateException("Not expected to be used in this context"); + } + + @Override + public FileObject getFileForOutput(Location location, String packageName, String relativeName, FileObject sibling) + throws IOException { + logger.debug("getFileForOutput({},{},{},{})",location,packageName,relativeName,sibling); + // This can be called when the annotation config processor runs + // Example parameters: CLASS_OUTPUT, , META-INF/spring-configuration-metadata.json, null + return outputCollector.getFileForOutput(location, packageName, relativeName, sibling); + } + + @Override + public void flush() throws IOException { + } + + @Override + public void close() throws IOException { + for (CloseableFilterableJavaFileObjectIterable closeable: toClose) { + closeable.close(); + } + } + + public List getCompiledClasses() { + return outputCollector.getCompiledClasses(); + } + +} \ No newline at end of file diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/NestedZipEntryJavaFileObject.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/NestedZipEntryJavaFileObject.java new file mode 100644 index 000000000..ea4521215 --- /dev/null +++ b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/NestedZipEntryJavaFileObject.java @@ -0,0 +1,172 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed 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.springframework.cloud.function.compiler.java; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.Writer; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipInputStream; + +import javax.lang.model.element.Modifier; +import javax.lang.model.element.NestingKind; +import javax.tools.JavaFileObject; + +/** + * Represents an element inside in zip which is itself inside a zip. These objects are + * not initially created with the content of the file they represent, + * only enough information to find that content because many will + * typically be created but only few will be opened. + * + * @author Andy Clement + */ +public class NestedZipEntryJavaFileObject implements JavaFileObject { + + private File outerFile; + private ZipFile outerZipFile; + private ZipEntry innerZipFile; + private ZipEntry innerZipFileEntry; + + private URI uri; + + public NestedZipEntryJavaFileObject(File outerFile, ZipFile outerZipFile, ZipEntry innerZipFile, ZipEntry innerZipFileEntry) { + this.outerFile = outerFile; + this.outerZipFile = outerZipFile; + this.innerZipFile = innerZipFile; + this.innerZipFileEntry = innerZipFileEntry; + } + + @Override + public String getName() { + return innerZipFileEntry.getName(); // Example: a/b/C.class + } + + @Override + public URI toUri() { + if (uri == null) { + String uriString = null; + try { + uriString = "zip:"+outerFile.getAbsolutePath()+"!"+innerZipFile.getName()+"!"+innerZipFileEntry.getName(); + uri = new URI(uriString); + } catch (URISyntaxException e) { + throw new IllegalStateException("Unexpected URISyntaxException for string '"+uriString+"'",e); + } + } + return uri; + } + + @Override + public InputStream openInputStream() throws IOException { + // Find the inner zip file inside the outer zip file, then + // find the relevant entry, then return the stream. + InputStream innerZipFileInputStream = this.outerZipFile.getInputStream(innerZipFile); + ZipInputStream innerZipInputStream = new ZipInputStream(innerZipFileInputStream); + ZipEntry nextEntry = innerZipInputStream.getNextEntry(); + while (nextEntry != null) { + if (nextEntry.getName().equals(innerZipFileEntry.getName())) { + return innerZipInputStream; + } + nextEntry = innerZipInputStream.getNextEntry(); + } + throw new IllegalStateException("Unable to locate nested zip entry "+innerZipFileEntry.getName()+" in zip "+innerZipFile.getName()+" inside zip "+outerZipFile.getName()); + } + + @Override + public Reader openReader(boolean ignoreEncodingErrors) throws IOException { + // It is bytecode + throw new UnsupportedOperationException("getCharContent() not supported on class file: " + getName()); + } + + @Override + public long getLastModified() { + return innerZipFileEntry.getTime(); + } + + @Override + public Kind getKind() { + // The filtering before this object was created ensure it is only used for classes + return Kind.CLASS; + } + + @Override + public boolean delete() { + return false; // Cannot delete entries inside nested zips + } + + @Override + public OutputStream openOutputStream() throws IOException { + throw new IllegalStateException("cannot write to nested zip entry: "+toUri()); + } + + @Override + public Writer openWriter() throws IOException { + throw new IllegalStateException("cannot write to nested zip entry: "+toUri()); + } + + @Override + public boolean isNameCompatible(String simpleName, Kind kind) { + if (kind != Kind.CLASS) { + return false; + } + String name = getName(); + int lastSlash = name.lastIndexOf('/'); + return name.substring(lastSlash+1).equals(simpleName+".class"); + } + + @Override + public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { + // It is bytecode + throw new UnsupportedOperationException("getCharContent() not supported on class file: " + getName()); + } + + @Override + public NestingKind getNestingKind() { + return null; // nesting level not known + } + + @Override + public Modifier getAccessLevel() { + return null; // access level not known + } + + @Override + public int hashCode() { + int hc = outerFile.getName().hashCode(); + hc = hc * 37 + innerZipFile.getName().hashCode(); + hc = hc * 37 + innerZipFileEntry.getName().hashCode(); + return hc; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof NestedZipEntryJavaFileObject)) { + return false; + } + NestedZipEntryJavaFileObject that = (NestedZipEntryJavaFileObject)obj; + return (outerFile.getName().equals(that.outerFile.getName())) && + (innerZipFile.getName().equals(that.innerZipFile.getName())) && + (innerZipFileEntry.getName().equals(that.innerZipFileEntry.getName())); + } + + +} \ No newline at end of file diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/RuntimeJavaCompiler.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/RuntimeJavaCompiler.java new file mode 100644 index 000000000..e064c6481 --- /dev/null +++ b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/RuntimeJavaCompiler.java @@ -0,0 +1,105 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed 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.springframework.cloud.function.compiler.java; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.tools.Diagnostic; +import javax.tools.Diagnostic.Kind; +import javax.tools.DiagnosticCollector; +import javax.tools.JavaCompiler; +import javax.tools.JavaCompiler.CompilationTask; +import javax.tools.JavaFileObject; +import javax.tools.ToolProvider; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Compile Java source at runtime and load it. + * + * @author Andy Clement + */ +public class RuntimeJavaCompiler { + + private JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + + private static Logger logger = LoggerFactory.getLogger(RuntimeJavaCompiler.class); + + /** + * Compile the named class consisting of the supplied source code. If successful load the class + * and return it. Multiple classes may get loaded if the source code included anonymous/inner/local + * classes. + * @param className the name of the class (dotted form, e.g. com.foo.bar.Goo) + * @param classSourceCode the full source code for the class + * @return a CompilationResult that encapsulates what happened during compilation (classes/messages produced) + */ + public CompilationResult compile(String className, String classSourceCode) { + logger.info("Compiling source for class {} using compiler {}",className,compiler.getClass().getName()); + + DiagnosticCollector diagnosticCollector = new DiagnosticCollector(); + MemoryBasedJavaFileManager fileManager = new MemoryBasedJavaFileManager(); +// JavaFileObject sourceFile = new StringBasedJavaSourceFileObject(className, classSourceCode); + JavaFileObject sourceFile = InMemoryJavaFileObject.getSourceJavaFileObject(className, classSourceCode); +// new InMemoryJavaFileObject(StandardLocation.SOURCE_PATH, className, javax.tools.JavaFileObject.Kind.SOURCE, null); +// try (Writer w = sourceFile.openWriter()) { +// w.write(classSourceCode); +// } catch (IOException ioe) { +// ioe.printStackTrace(); +// } + Iterable compilationUnits = Arrays.asList(sourceFile); + CompilationTask task = compiler.getTask(null, fileManager , diagnosticCollector, null, null, compilationUnits); + + boolean success = task.call(); + CompilationResult compilationResult = new CompilationResult(success); + + // If successful there may be no errors but there might be info/warnings + for (Diagnostic diagnostic : diagnosticCollector.getDiagnostics()) { + CompilationMessage.Kind kind = (diagnostic.getKind()==Kind.ERROR?CompilationMessage.Kind.ERROR:CompilationMessage.Kind.OTHER); +// String sourceCode = ((StringBasedJavaSourceFileObject)diagnostic.getSource()).getSourceCode(); + String sourceCode =null; + try { + sourceCode = (String)diagnostic.getSource().getCharContent(true); + } catch (IOException ioe) { + // Unexpected, but leave sourceCode null to indicate it was not retrievable + } + int startPosition = (int)diagnostic.getPosition(); + if (startPosition == Diagnostic.NOPOS) { + startPosition = (int)diagnostic.getStartPosition(); + } + CompilationMessage compilationMessage = new CompilationMessage(kind,diagnostic.getMessage(null),sourceCode,startPosition,(int)diagnostic.getEndPosition()); + compilationResult.recordCompilationMessage(compilationMessage); + } + if (success) { + List ccds = fileManager.getCompiledClasses(); + List> classes = new ArrayList<>(); + try (SimpleClassLoader ccl = new SimpleClassLoader(this.getClass().getClassLoader())) { + for (CompiledClassDefinition ccd: ccds) { + Class clazz = ccl.defineClass(ccd.getClassName(), ccd.getBytes()); + classes.add(clazz); + } + } catch (IOException ioe) { + logger.debug("Unexpected exception defining classes",ioe); + } + compilationResult.setCompiledClasses(classes); + } + return compilationResult; + } +} \ No newline at end of file diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/SimpleClassLoader.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/SimpleClassLoader.java new file mode 100644 index 000000000..221c1c6fb --- /dev/null +++ b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/SimpleClassLoader.java @@ -0,0 +1,38 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed 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.springframework.cloud.function.compiler.java; + +import java.net.URL; +import java.net.URLClassLoader; + +/** + * Very simple classloader that can be used to load the compiled types. + * + * @author Andy Clement + */ +public class SimpleClassLoader extends URLClassLoader { + + private static final URL[] NO_URLS = new URL[0]; + + public SimpleClassLoader(ClassLoader classLoader) { + super(NO_URLS, classLoader); + } + + public Class defineClass(String name, byte[] bytes) { + return super.defineClass(name, bytes, 0, bytes.length); + } +} \ No newline at end of file diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/ZipEntryJavaFileObject.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/ZipEntryJavaFileObject.java new file mode 100644 index 000000000..60ce4d7cd --- /dev/null +++ b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/ZipEntryJavaFileObject.java @@ -0,0 +1,146 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed 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.springframework.cloud.function.compiler.java; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.Writer; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import javax.lang.model.element.Modifier; +import javax.lang.model.element.NestingKind; +import javax.tools.JavaFileObject; + +public class ZipEntryJavaFileObject implements JavaFileObject { + + private File containingFile; + private ZipFile zf; + private ZipEntry ze; + + private URI uri; + + public ZipEntryJavaFileObject(File containingFile, ZipFile zipFile, ZipEntry entry) { + this.containingFile = containingFile; + this.zf = zipFile; + this.ze = entry; + } + + @Override + public URI toUri() { + if (uri == null) { + String uriString = null; + try { + uriString = "zip:" + containingFile.getAbsolutePath() + "!" + ze.getName(); + uri = new URI(uriString); + } catch (URISyntaxException e) { + throw new IllegalStateException("Unexpected URISyntaxException for string '" + uriString + "'", e); + } + } + return uri; + } + + @Override + public String getName() { + return ze.getName(); // a/b/C.class + } + + @Override + public InputStream openInputStream() throws IOException { + return zf.getInputStream(ze); + } + + @Override + public OutputStream openOutputStream() throws IOException { + throw new IllegalStateException("only expected to be used for input"); + } + + @Override + public Reader openReader(boolean ignoreEncodingErrors) throws IOException { + // It is bytecode + throw new UnsupportedOperationException("openReader() not supported on class file: " + getName()); + } + + @Override + public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { + // It is bytecode + throw new UnsupportedOperationException("getCharContent() not supported on class file: " + getName()); + } + + @Override + public Writer openWriter() throws IOException { + throw new IllegalStateException("only expected to be used for input"); + } + + @Override + public long getLastModified() { + return ze.getTime(); + } + + @Override + public boolean delete() { + return false; // Cannot delete entries inside zips + } + + @Override + public Kind getKind() { + return Kind.CLASS; + } + + @Override + public boolean isNameCompatible(String simpleName, Kind kind) { + if (kind != Kind.CLASS) { + return false; + } + String name = getName(); + int lastSlash = name.lastIndexOf('/'); + return name.substring(lastSlash + 1).equals(simpleName + ".class"); + } + + @Override + public NestingKind getNestingKind() { + return null; + } + + @Override + public Modifier getAccessLevel() { + return null; + } + + @Override + public int hashCode() { + int hc = containingFile.getName().hashCode(); + hc = hc * 37 + ze.getName().hashCode(); + return hc; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ZipEntryJavaFileObject)) { + return false; + } + ZipEntryJavaFileObject that = (ZipEntryJavaFileObject)obj; + return (containingFile.getName().equals(that.containingFile.getName())) && + (ze.getName().equals(that.ze.getName())); + } + +} \ No newline at end of file diff --git a/spring-cloud-function-core/.jdk8 b/spring-cloud-function-core/.jdk8 new file mode 100644 index 000000000..e69de29bb diff --git a/spring-cloud-function-core/pom.xml b/spring-cloud-function-core/pom.xml new file mode 100644 index 000000000..9d8574a57 --- /dev/null +++ b/spring-cloud-function-core/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + + spring-cloud-function-core + jar + Spring Cloud Function Core + Spring Cloud Function Core + + + org.springframework.cloud + spring-cloud-function-parent + 1.0.0.BUILD-SNAPSHOT + + + + 1.8 + + + + + org.springframework.cloud + spring-cloud-function-compiler + ${project.version} + + + org.springframework + spring-context + + + org.springframework.boot + spring-boot-starter-logging + + + + diff --git a/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/gateway/FunctionGateway.java b/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/gateway/FunctionGateway.java new file mode 100644 index 000000000..43a1bb221 --- /dev/null +++ b/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/gateway/FunctionGateway.java @@ -0,0 +1,107 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed 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.springframework.cloud.function.gateway; + +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import org.springframework.cloud.function.invoker.FunctionInvokingRunnable; +import org.springframework.cloud.function.registry.FunctionRegistry; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.Trigger; +import org.springframework.util.Assert; + +/** + * @author Mark Fisher + */ +public class FunctionGateway { + + private final FunctionRegistry registry; + + private final TaskScheduler scheduler; + + public FunctionGateway(FunctionRegistry registry, TaskScheduler scheduler) { + Assert.notNull(registry, "FunctionRegistry must not be null"); + Assert.notNull(scheduler, "TaskScheduler must not be null"); + this.registry = registry; + this.scheduler = scheduler; + } + + @SuppressWarnings("unchecked") + public void compose(String name, Function... functions) { + Assert.isTrue(functions != null && functions.length > 1, "more than one Function is required"); + @SuppressWarnings("rawtypes") + Function function = functions[0]; + for (int i = 1; i < functions.length; i++) { + function = function.andThen(functions[i]); + } + this.registry.register(name, function); + } + + @SuppressWarnings("unchecked") + public void compose(String composedFunctionName, String... functionNames) { + Assert.isTrue(functionNames != null && functionNames.length > 1, "more than one Function is required"); + @SuppressWarnings("rawtypes") + Function function = this.registry.lookup(functionNames[0]); + for (int i = 1; i < functionNames.length; i++) { + function = function.andThen(this.registry.lookup(functionNames[i])); + } + this.registry.register(composedFunctionName, function); + } + + public R invoke(String functionName, T request) { + Function function = this.registry.lookup(functionName); + return function.apply(request); + } + + public void schedule(String functionName, Trigger trigger, Supplier supplier, Consumer consumer) { + Function function = this.registry.lookup(functionName); + this.scheduler.schedule(new FunctionInvokingRunnable(supplier, function, consumer), trigger); + } + + public void subscribe(Publisher publisher, String functionName, final Consumer consumer) { + final Function function = this.registry.lookup(functionName); + publisher.subscribe(new Subscriber() { + + @Override + public void onComplete() {} + + @Override + public void onError(Throwable error) {} + + @Override + public void onNext(T next) { + if (consumer != null) { + consumer.accept(function.apply(next)); + } + else { + function.apply(next); + } + } + + @Override + public void onSubscribe(Subscription subscription) { + subscription.request(Long.MAX_VALUE); + } + }); + } +} diff --git a/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/invoker/AbstractFunctionInvoker.java b/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/invoker/AbstractFunctionInvoker.java new file mode 100644 index 000000000..f82ab6103 --- /dev/null +++ b/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/invoker/AbstractFunctionInvoker.java @@ -0,0 +1,41 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed 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.springframework.cloud.function.invoker; + +import java.util.function.Function; + +import org.springframework.util.Assert; + +/** + * @author Mark Fisher + * + * @param function parameter type + * @param function return type + */ +public abstract class AbstractFunctionInvoker { + + private final Function function; + + protected AbstractFunctionInvoker(Function function) { + Assert.notNull(function, "Function must not be null"); + this.function = function; + } + + protected R doInvoke(T input) { + return this.function.apply(input); + } +} diff --git a/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/invoker/FunctionInvokingRunnable.java b/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/invoker/FunctionInvokingRunnable.java new file mode 100644 index 000000000..b7ee58dc4 --- /dev/null +++ b/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/invoker/FunctionInvokingRunnable.java @@ -0,0 +1,47 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed 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.springframework.cloud.function.invoker; + +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * @author Mark Fisher + * + * @param output of supplier, input to function + * @param output of function, input to consumer + */ +public class FunctionInvokingRunnable implements Runnable { + + private final Supplier supplier; + + private final Function function; + + private final Consumer consumer; + + public FunctionInvokingRunnable(Supplier supplier, Function function, Consumer consumer) { + this.supplier = supplier; + this.function = function; + this.consumer = consumer; + } + + @Override + public void run() { + this.consumer.accept(this.function.apply(this.supplier.get())); + } +} diff --git a/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/registry/FunctionRegistry.java b/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/registry/FunctionRegistry.java new file mode 100644 index 000000000..f9220bcf6 --- /dev/null +++ b/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/registry/FunctionRegistry.java @@ -0,0 +1,32 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed 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.springframework.cloud.function.registry; + +import java.util.function.Function; + +/** + * @author Mark Fisher + */ +public interface FunctionRegistry { + + Function lookup(String name); + + void register(String name, Function function); + + void register(String name, String function); + +} diff --git a/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/registry/FunctionRegistrySupport.java b/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/registry/FunctionRegistrySupport.java new file mode 100644 index 000000000..e8cf45904 --- /dev/null +++ b/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/registry/FunctionRegistrySupport.java @@ -0,0 +1,35 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed 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.springframework.cloud.function.registry; + +import java.util.function.Function; + +import org.springframework.cloud.function.compiler.FunctionCompiler; + +/** + * @author Mark Fisher + */ +public abstract class FunctionRegistrySupport implements FunctionRegistry { + + private final FunctionCompiler compiler = new FunctionCompiler(); + + @Override + public void register(String name, String code) { + Function function = compiler.compile(code); + this.register(name, function); + } +} diff --git a/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/registry/InMemoryFunctionRegistry.java b/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/registry/InMemoryFunctionRegistry.java new file mode 100644 index 000000000..8ead1dde1 --- /dev/null +++ b/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/registry/InMemoryFunctionRegistry.java @@ -0,0 +1,38 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed 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.springframework.cloud.function.registry; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +/** + * @author Mark Fisher + */ +public class InMemoryFunctionRegistry extends FunctionRegistrySupport { + + private final ConcurrentHashMap> map = new ConcurrentHashMap<>(); + + @Override + public Function lookup(String name) { + return this.map.get(name); + } + + @Override + public void register(String name, Function function) { + this.map.put(name, function); + } +} diff --git a/spring-cloud-function-stream/.jdk8 b/spring-cloud-function-stream/.jdk8 new file mode 100644 index 000000000..e69de29bb diff --git a/spring-cloud-function-stream/pom.xml b/spring-cloud-function-stream/pom.xml new file mode 100644 index 000000000..51b6da3f0 --- /dev/null +++ b/spring-cloud-function-stream/pom.xml @@ -0,0 +1,50 @@ + + + 4.0.0 + + spring-cloud-function-stream + jar + Spring Cloud Function Stream Support + Spring Cloud Function Stream Support + + + org.springframework.cloud + spring-cloud-function-parent + 1.0.0.BUILD-SNAPSHOT + + + + 1.8 + + + + + org.springframework.cloud + spring-cloud-stream + + + org.springframework.cloud + spring-cloud-stream-reactive + 1.1.0.BUILD-SNAPSHOT + + + org.springframework.cloud + spring-cloud-function-core + ${project.version} + + + io.projectreactor + reactor-core + + + org.springframework.cloud + spring-cloud-stream-binder-kafka + ${spring-cloud-stream.version} + + + org.springframework.boot + spring-boot-starter-logging + + + + diff --git a/spring-cloud-function-stream/src/main/java/org/springframework/cloud/function/stream/StreamApplication.java b/spring-cloud-function-stream/src/main/java/org/springframework/cloud/function/stream/StreamApplication.java new file mode 100644 index 000000000..a623e2aad --- /dev/null +++ b/spring-cloud-function-stream/src/main/java/org/springframework/cloud/function/stream/StreamApplication.java @@ -0,0 +1,31 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed 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.springframework.cloud.function.stream; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author Mark Fisher + */ +@SpringBootApplication +public class StreamApplication { + + public static void main(String[] args) { + SpringApplication.run(StreamApplication.class, args); + } +} diff --git a/spring-cloud-function-stream/src/main/java/org/springframework/cloud/function/stream/StreamConfiguration.java b/spring-cloud-function-stream/src/main/java/org/springframework/cloud/function/stream/StreamConfiguration.java new file mode 100644 index 000000000..8f3e8c1a8 --- /dev/null +++ b/spring-cloud-function-stream/src/main/java/org/springframework/cloud/function/stream/StreamConfiguration.java @@ -0,0 +1,49 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed 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.springframework.cloud.function.stream; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.function.invoker.AbstractFunctionInvoker; +import org.springframework.cloud.function.registry.FunctionRegistry; +import org.springframework.cloud.function.registry.InMemoryFunctionRegistry; +import org.springframework.cloud.stream.annotation.EnableBinding; +import org.springframework.cloud.stream.messaging.Processor; +import org.springframework.context.annotation.Bean; + +/** + * @author Mark Fisher + */ +@EnableBinding(Processor.class) +@EnableConfigurationProperties(StreamConfigurationProperties.class) +public class StreamConfiguration { + + @Autowired + private StreamConfigurationProperties properties; + + @Bean + public FunctionRegistry registry() { + FunctionRegistry registry = new InMemoryFunctionRegistry(); + registry.register(properties.getName(), properties.getCode()); + return registry; + } + + @Bean + public AbstractFunctionInvoker invoker() { + return new StreamListeningFunctionInvoker(registry().lookup(properties.getName())); + } +} diff --git a/spring-cloud-function-stream/src/main/java/org/springframework/cloud/function/stream/StreamConfigurationProperties.java b/spring-cloud-function-stream/src/main/java/org/springframework/cloud/function/stream/StreamConfigurationProperties.java new file mode 100644 index 000000000..7e4c6dce7 --- /dev/null +++ b/spring-cloud-function-stream/src/main/java/org/springframework/cloud/function/stream/StreamConfigurationProperties.java @@ -0,0 +1,46 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed 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.springframework.cloud.function.stream; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @author Mark Fisher + */ +@ConfigurationProperties(prefix = "function") +public class StreamConfigurationProperties { + + private String name; + + private String code; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } +} diff --git a/spring-cloud-function-stream/src/main/java/org/springframework/cloud/function/stream/StreamListeningFunctionInvoker.java b/spring-cloud-function-stream/src/main/java/org/springframework/cloud/function/stream/StreamListeningFunctionInvoker.java new file mode 100644 index 000000000..926a65df6 --- /dev/null +++ b/spring-cloud-function-stream/src/main/java/org/springframework/cloud/function/stream/StreamListeningFunctionInvoker.java @@ -0,0 +1,43 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed 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.springframework.cloud.function.stream; + +import java.util.function.Function; + +import org.springframework.cloud.function.invoker.AbstractFunctionInvoker; +import org.springframework.cloud.stream.annotation.Input; +import org.springframework.cloud.stream.annotation.Output; +import org.springframework.cloud.stream.annotation.StreamListener; +import org.springframework.cloud.stream.messaging.Processor; + +import reactor.core.publisher.Flux; + +/** + * @author Mark Fisher + */ +public class StreamListeningFunctionInvoker extends AbstractFunctionInvoker, Flux> { + + public StreamListeningFunctionInvoker(Function, Flux> function) { + super(function); + } + + @StreamListener + @Output(Processor.OUTPUT) + public Flux handle(@Input(Processor.INPUT) Flux input) { + return this.doInvoke(input); + } +}