blob: a0f47bf489b9d23d3cfe2ba9fd2725d5aa6df68c [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.drill.exec.expr.fn;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.util.List;
import java.util.Map;
import com.google.common.annotations.VisibleForTesting;
import org.apache.drill.common.exceptions.UserException;
import org.apache.drill.common.util.DrillFileUtils;
import org.codehaus.commons.compiler.CompileException;
import org.codehaus.janino.Java.CompilationUnit;
import org.codehaus.janino.Parser;
import org.codehaus.janino.Scanner;
import org.eclipse.jetty.util.IO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* To avoid the cost of initializing all functions up front, this class contains
* all information required to initializing a function when it is used.
*/
public class FunctionInitializer {
private static final Logger logger = LoggerFactory.getLogger(FunctionInitializer.class);
private final String className;
private final ClassLoader classLoader;
private Map<String, String> methods;
private List<String> imports;
private volatile boolean isLoaded;
/**
* @param className
* the fully qualified name of the class implementing the function
* @param classLoader
* class loader associated with the function, is unique for each jar
* that holds function to prevent classpath collisions during loading
* an unloading jars
*/
public FunctionInitializer(String className, ClassLoader classLoader) {
this.className = className;
this.classLoader = classLoader;
}
/**
* @return returns class loader
*/
public ClassLoader getClassLoader() { return classLoader; }
/**
* @return the fully qualified name of the class implementing the function
*/
public String getClassName() {
return className;
}
/**
* @return the imports of this class (for java code gen)
*/
public List<String> getImports() {
loadFunctionBody();
return imports;
}
/**
* @param methodName method name
* @return the content of the method (for java code gen inlining)
*/
public String getMethod(String methodName) {
loadFunctionBody();
return methods.get(methodName);
}
/**
* Loads function body: methods (for instance, eval, setup, reset) and imports.
* Loading is done once per class instance upon first function invocation.
* Double-checked locking is used to avoid concurrency issues
* when two threads are trying to load the function body at the same time.
*/
private void loadFunctionBody() {
if (isLoaded) {
return;
}
synchronized (this) {
if (isLoaded) {
return;
}
logger.trace("Getting function body for the {}", className);
try {
final Class<?> clazz = Class.forName(className, true, classLoader);
final CompilationUnit cu = convertToCompilationUnit(clazz);
methods = MethodGrabbingVisitor.getMethods(cu, clazz);
imports = ImportGrabber.getImports(cu);
isLoaded = true;
} catch (IOException | ClassNotFoundException e) {
throw UserException.functionError(e)
.message("Failure reading Function class.")
.addContext("Function Class", className)
.build(logger);
}
}
}
/**
* Using class name generates path to class source code (*.java),
* reads its content as string and parses it into {@link org.codehaus.janino.Java.CompilationUnit}.
*
* @param clazz function class
* @return compilation unit
* @throws IOException if did not find class or could not load it
*/
@VisibleForTesting
CompilationUnit convertToCompilationUnit(Class<?> clazz) throws IOException {
String path = clazz.getName();
path = path.replaceFirst("\\$.*", "");
path = path.replace(".", DrillFileUtils.SEPARATOR);
path = "/" + path + ".java";
logger.trace("Loading function code from the {}", path);
try (InputStream is = clazz.getResourceAsStream(path)) {
if (is == null) {
throw new IOException(String.format(
"Failure trying to locate source code for class %s, tried to read on classpath location %s", clazz.getName(),
path));
}
String body = IO.toString(is);
// TODO: Hack to remove annotations so Janino doesn't choke. Need to reconsider this problem...
body = body.replaceAll("@\\w+(?:\\([^\\\\]*?\\))?", "");
try {
return (CompilationUnit) new Parser(new Scanner(null, new StringReader(body))).parseAbstractCompilationUnit();
} catch (CompileException e) {
throw new IOException(String.format("Failure while loading class %s.", clazz.getName()), e);
}
}
}
}