Skip to content

Commit

Permalink
0001647: Create a Java router and transform that uses compiled Java code
Browse files Browse the repository at this point in the history
  • Loading branch information
erilong committed Mar 16, 2014
1 parent 9ed5d5e commit 601cc2b
Show file tree
Hide file tree
Showing 2 changed files with 252 additions and 0 deletions.
@@ -0,0 +1,82 @@
/**
* Licensed to JumpMind Inc under one or more contributor
* license agreements. See the NOTICE file distributed
* with this work for additional information regarding
* copyright ownership. JumpMind Inc licenses this file
* to you under the GNU General Public License, version 3.0 (GPLv3)
* (the "License"); you may not use this file except in compliance
* with the License.
*
* You should have received a copy of the GNU General Public License,
* version 3.0 (GPLv3) along with this library; if not, see
* <http://www.gnu.org/licenses/>.
*
* 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.jumpmind.symmetric.io.data.transform;

import java.util.Map;

import org.jumpmind.db.platform.IDatabasePlatform;
import org.jumpmind.extension.IBuiltInExtensionPoint;
import org.jumpmind.symmetric.io.data.DataContext;
import org.jumpmind.util.SimpleClassCompiler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class JavaColumnTransform implements ISingleValueColumnTransform, IBuiltInExtensionPoint {

public final static String CODE_START = "import org.jumpmind.symmetric.io.data.transform.*;\n"
+ "import org.jumpmind.symmetric.io.data.*;\n"
+ "import org.jumpmind.db.platform.*;\n"
+ "import java.util.*;\n"
+ "public class " + SimpleClassCompiler.CLASSNAME_TOKEN + " extends JavaColumnTransform { \n"
+ " public String transform(IDatabasePlatform platform, DataContext context, TransformColumn column, TransformedData data,\n"
+ " Map<String, String> sourceValues, String newValue, String oldValue) throws IgnoreColumnException, IgnoreRowException {\n\n";

public final static String CODE_END = "\n\n }\n}\n";

public static final String NAME = "java";

protected final Logger log = LoggerFactory.getLogger(getClass());

protected final String TRANSFORM_KEY = String.format("%d.JavaRouter", hashCode());

public String getName() {
return NAME;
}

public boolean isExtractColumnTransform() {
return true;
}

public boolean isLoadColumnTransform() {
return true;
}

public String transform(IDatabasePlatform platform, DataContext context, TransformColumn column, TransformedData data,
Map<String, String> sourceValues, String newValue, String oldValue) throws IgnoreColumnException, IgnoreRowException {
try {
ISingleValueColumnTransform colTransform = getCompiledClass(context, column);
return colTransform.transform(platform, context, column, data, sourceValues, newValue, oldValue);
} catch (Exception e) {
throw new RuntimeException(e);
}
}

protected ISingleValueColumnTransform getCompiledClass(DataContext context, TransformColumn column) throws Exception {
ISingleValueColumnTransform colTransform = (ISingleValueColumnTransform) context.get(TRANSFORM_KEY);
if (colTransform == null) {
String javaCode = CODE_START + column.getTransformExpression() + CODE_END;
colTransform = (ISingleValueColumnTransform) SimpleClassCompiler.getInstance().getCompiledClass(javaCode);
context.put(TRANSFORM_KEY, colTransform);
}
return colTransform;
}

}
@@ -0,0 +1,170 @@
/**
* Licensed to JumpMind Inc under one or more contributor
* license agreements. See the NOTICE file distributed
* with this work for additional information regarding
* copyright ownership. JumpMind Inc licenses this file
* to you under the GNU General Public License, version 3.0 (GPLv3)
* (the "License"); you may not use this file except in compliance
* with the License.
*
* You should have received a copy of the GNU General Public License,
* version 3.0 (GPLv3) along with this library; if not, see
* <http://www.gnu.org/licenses/>.
*
* 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.jumpmind.util;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.security.SecureClassLoader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.FileObject;
import javax.tools.ForwardingJavaFileManager;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.JavaFileObject.Kind;
import javax.tools.SimpleJavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Compile Java code in memory, load it, and return a new instance. The class name is changed to make it unique
* and the caller should cast to a parent class or interface. On subsequent calls, if the same Java code
* is passed again, the cached object is returned. Otherwise, a new class is compiled, loaded, and an instance returned.
*
*/
@SuppressWarnings({ "rawtypes", "restriction" })
public class SimpleClassCompiler {

public static final String CLASSNAME_TOKEN = "$(CLASSNAME)";

protected static final String CLASSNAME_TOKEN_REGEX = "\\$\\(CLASSNAME\\)";

protected static SimpleClassCompiler instance;

protected Map<Integer, Object> objectMap = new HashMap<Integer, Object>();

protected int classSuffix;

private static Logger log = LoggerFactory.getLogger(SimpleClassCompiler.class);

public static SimpleClassCompiler getInstance() {
if (instance == null) {
instance = new SimpleClassCompiler();
}
return instance;
}

public Object getCompiledClass(String javaCode) throws Exception {

Integer id = javaCode.hashCode();
Object javaObject = objectMap.get(id);

if (javaObject == null ) {
String className = getNextClassName();
javaCode = javaCode.replaceAll(CLASSNAME_TOKEN_REGEX, className);
if (log.isDebugEnabled()) {
log.debug("Compiling code: \n" + javaCode);
}

JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
JavaFileManager fileManager = new ClassFileManager(compiler.getStandardFileManager(null, null, null));
DiagnosticCollector<JavaFileObject> diag = new DiagnosticCollector<JavaFileObject>();
List<JavaFileObject> javaFiles = new ArrayList<JavaFileObject>();
javaFiles.add(new JavaObjectFromString(className, javaCode));
Boolean success = compiler.getTask(null, fileManager, diag, null, null, javaFiles).call();

if (success) {
log.debug("Compilation has succeeded");
javaObject = fileManager.getClassLoader(null).loadClass(className).newInstance();
objectMap.put(id, javaObject);
} else {
log.error("Compilation failed:\n" + javaCode);
for (Diagnostic d : diag.getDiagnostics()) {
log.error("Error at line " + d.getLineNumber() + ": " + d.getMessage(null));
}
}
}

return javaObject;
}

protected synchronized String getNextClassName() {
return getClass().getSimpleName() + (classSuffix++);
}

class JavaObjectFromString extends SimpleJavaFileObject {
private String data = null;

public JavaObjectFromString(String className, String data) throws Exception {
super(URI.create("string:///" + className.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE);
this.data = data;
}

public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
return data;
}
}

class JavaClassObject extends SimpleJavaFileObject {
protected final ByteArrayOutputStream bos = new ByteArrayOutputStream();

public JavaClassObject(String name, Kind kind) {
super(URI.create("string:///" + name.replace('.', '/') + kind.extension), kind);
}

public byte[] getBytes() {
return bos.toByteArray();
}

@Override
public OutputStream openOutputStream() throws IOException {
return bos;
}
}

public class ClassFileManager extends ForwardingJavaFileManager {
private JavaClassObject jclassObject;

@SuppressWarnings("unchecked")
public ClassFileManager(StandardJavaFileManager standardManager) {
super(standardManager);
}

@Override
public ClassLoader getClassLoader(Location location) {
return new SecureClassLoader() {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] bytes = jclassObject.getBytes();
return super.defineClass(name, bytes, 0, bytes.length);
}
};
}

@Override
public JavaFileObject getJavaFileForOutput(Location location, String className, Kind kind, FileObject sibling)
throws IOException {
jclassObject = new JavaClassObject(className, kind);
return jclassObject;
}
}
}

0 comments on commit 601cc2b

Please sign in to comment.