initial commit

This commit is contained in:
markfisher
2016-09-21 22:33:06 -04:00
commit bdd39b74ff
36 changed files with 2715 additions and 0 deletions

View File

@@ -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<Object>, Flux<Object>> getFunction() {\n" +
" %s\n" +
" }\n" +
"}\n";
private final RuntimeJavaCompiler compiler = new RuntimeJavaCompiler();
/**
* Produce a Function instance by:<ul>
* <li>Decoding the code String to process any newlines/double-double-quotes
* <li>Insert the code into the source code template for a class
* <li>Compiling the class using the JDK provided Java Compiler
* <li>Loading the compiled class
* <li>Invoking a well known method on the factory class to produce a Function instance
* <li>Returning that instance.
* </ul>
*
* @return a Function instance
*/
public <T, R> Function<T, R> 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<Class<?>> 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<CompilationMessage> 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 <tt>Function&lt;Flux&lt;Object&gt;,Flux&lt;Object&gt;&gt;</tt>.
* This method can return more than one class if the method body includes local class
* declarations. An example methodBody would be <tt>return input -> input.buffer(5).map(list->list.get(0));</tt>.
*
* @param methodBody the source code for a method that should return a
* <tt>Function&lt;Flux&lt;Object&gt;,Flux&lt;Object&gt;&gt;</tt>
* @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);
}
}

View File

@@ -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 {
<T, R> Function<T, R> getFunction();
}

View File

@@ -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<JavaFileObject> {
// 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();
}

View File

@@ -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<CompilationMessage> messages) {
super(consolidateMessages(messages));
}
private static String consolidateMessages(List<CompilationMessage> messages) {
if (messages == null || messages.isEmpty()) {
return "";
}
StringBuilder sb = new StringBuilder();
for (CompilationMessage message : messages) {
sb.append(message.toString());
}
return sb.toString();
}
}

View File

@@ -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 <tt>toString()</tt> 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
}

View File

@@ -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<InMemoryJavaFileObject> 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<CompiledClassDefinition> getCompiledClasses() {
List<CompiledClassDefinition> 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;
}
}

View File

@@ -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<CompilationMessage> compilationMessages = new ArrayList<>();
List<Class<?>> compiledClasses = new ArrayList<>();
public CompilationResult(boolean successfulCompilation) {
this.successfulCompilation = successfulCompilation;
}
public boolean wasSuccessful() {
return successfulCompilation;
}
public List<Class<?>> getCompiledClasses() {
return compiledClasses;
}
public List<CompilationMessage> getCompilationMessages() {
return Collections.unmodifiableList(compilationMessages);
}
public void recordCompilationMessage(CompilationMessage compilationMessage) {
this.compilationMessages.add(compilationMessage);
}
public void setCompiledClasses(List<Class<?>> 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();
}
}

View File

@@ -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;
}
}

View File

@@ -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()));
}
}

View File

@@ -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<File> {
// private final static Logger logger = LoggerFactory.getLogger(DirEnumeration.class);
// The starting point
private File basedir;
// Candidates collected so far
private List<File> filesToReturn;
// Places still to explore for candidates
private List<File> 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);
}
}

View File

@@ -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<JavaFileObject> iterator() {
return IteratorUtils.emptyIterator();
}
}

View File

@@ -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;
}
}

View File

@@ -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<File> classpathEntries = new ArrayList<>();
private List<ZipFile> 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<JavaFileObject> iterator() {
return new ClasspathEntriesIterator();
}
class ClasspathEntriesIterator implements Iterator<JavaFileObject> {
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<Enumeration<? extends ZipEntry>> 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<Enumeration<? extends ZipEntry>>();
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<? extends ZipEntry> 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<ZipEntry>{
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;
}
}
}

View File

@@ -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<CloseableFilterableJavaFileObjectIterable> 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<JavaFileObject> list(Location location, String packageName, Set<Kind> 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<String> 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<CompiledClassDefinition> getCompiledClasses() {
return outputCollector.getCompiledClasses();
}
}

View File

@@ -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()));
}
}

View File

@@ -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<JavaFileObject> diagnosticCollector = new DiagnosticCollector<JavaFileObject>();
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<? extends JavaFileObject> 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<? extends JavaFileObject> 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<CompiledClassDefinition> ccds = fileManager.getCompiledClasses();
List<Class<?>> 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;
}
}

View File

@@ -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);
}
}

View File

@@ -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()));
}
}