/*
* 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 com.streamsets.pipeline.maven.rbgen;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.io.IOUtils;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProject;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
@Mojo(name="rbgen", defaultPhase = LifecyclePhase.PACKAGE, requiresDependencyResolution = ResolutionScope.COMPILE)
public class RBGenMojo extends AbstractMojo {
private static final String BUNDLES_TO_GEN_FILE = "datacollector-resource-bundles.json";
private static final String STAGE_CLASS_NAME = "com.streamsets.pipeline.api.Stage";
private static final String ERROR_CODE_CLASS_NAME = "com.streamsets.pipeline.api.ErrorCode";
private static final String LABEL_CLASS_NAME = "com.streamsets.pipeline.api.Label";
private static final String STAGE_DEF_CLASS_NAME = "com.streamsets.pipeline.api.StageDef";
private static final String ERROR_STAGE_CLASS_NAME = "com.streamsets.pipeline.api.ErrorStage";
private static final String CONFIG_DEF_CLASS_NAME = "com.streamsets.pipeline.api.ConfigDef";
@Parameter(defaultValue="${project}", readonly=true)
private MavenProject project;
@Parameter(defaultValue="${project.build.directory}/resource-bundles")
private File output;
private Class stageClass;
private Class errorCodeClass;
private Class labelClass;
private Class stageDefClass;
private Class errorStageClass;
private Class configDefClass;
private boolean usesDataCollectorAPI(ClassLoader classLoader) {
try {
stageClass = classLoader.loadClass(STAGE_CLASS_NAME);
errorCodeClass = classLoader.loadClass(ERROR_CODE_CLASS_NAME);
labelClass = classLoader.loadClass(LABEL_CLASS_NAME);
stageDefClass = classLoader.loadClass(STAGE_DEF_CLASS_NAME);
errorStageClass = classLoader.loadClass(ERROR_STAGE_CLASS_NAME);
configDefClass = classLoader.loadClass(CONFIG_DEF_CLASS_NAME);
return true;
} catch (ClassNotFoundException ex) {
return false;
}
}
@Override
@SuppressWarnings("unchecked")
public void execute() throws MojoExecutionException {
try {
ClassLoader projectCL = getProjectClassLoader();
if (usesDataCollectorAPI(projectCL)) {
File file = new File(project.getBuild().getOutputDirectory(), BUNDLES_TO_GEN_FILE);
if (file.exists() && file.isFile()) {
Map<String, File> bundles = new HashMap<>();
List<String> classNames = new ObjectMapper().readValue(file, List.class);
if (!classNames.isEmpty()) {
for (String e : classNames) {
Class klass = projectCL.loadClass(e);
String bundleName = klass.getName().replace(".", "/") + "-bundle.properties";
File bundleFile = generateDefaultBundleForClass(klass, bundleName);
bundles.put(bundleName, bundleFile);
}
File jarFile = new File(project.getBuild().getDirectory(),
project.getArtifactId() + "-" + project.getVersion() + "-bundles.jar");
getLog().info("Building bundles jar: " + jarFile.getAbsolutePath());
createBundlesJar(jarFile, bundles);
} else {
getLog().debug(BUNDLES_TO_GEN_FILE + "' file does not have any class, no bundles jar will be generated");
}
} else {
getLog().debug("Project does not have '" + BUNDLES_TO_GEN_FILE + "' file, no bundles jar will be generated");
}
} else {
getLog().debug("Project does not use DataCollector API, no bundles jar will be generated");
}
} catch (Throwable ex) {
throw new MojoExecutionException(ex.toString(), ex);
}
}
private ClassLoader getProjectClassLoader() throws Exception {
List<String> cp = project.getCompileClasspathElements();
URL[] urls = new URL[cp.size()];
for (int i = 0; i < cp.size(); i++) {
urls[i] = new File(cp.get(i)).toURI().toURL();
}
return new URLClassLoader(urls, this.getClass().getClassLoader());
}
private File generateDefaultBundleForClass(Class klass, String bundleName) throws Exception {
File bundleFile = new File(output, bundleName);
if (!bundleFile.getParentFile().exists()) {
if (!bundleFile.getParentFile().mkdirs()) {
throw new IOException("Could not create directory: " + bundleFile.getParentFile());
}
}
// we want to preserve the order, thus we cannot use JDK Properties for this.
try (Writer writer = new FileWriter(bundleFile)) {
for (Map.Entry<String, String> entry : extractResources(klass).entrySet()) {
writer.write(escapeValue(entry.getKey(), true) + "=" + escapeValue(entry.getValue(), false) + "\n");
}
getLog().debug("Generated bundle: " + bundleName);
}
return bundleFile;
}
@SuppressWarnings("unchecked")
private String invokeMessageMethod(Class klass, String methodName, Object obj) throws Exception {
Method method = klass.getMethod(methodName);
return (String) method.invoke(obj);
}
@SuppressWarnings("unchecked")
private LinkedHashMap<String, String> extractResources(Class klass) throws Exception {
LinkedHashMap<String, String> map = new LinkedHashMap<>();
if (klass.isEnum()) {
Object[] enums = klass.getEnumConstants();
for (Object e : enums) {
String name = ((Enum)e).name();
String text = name;
if (errorCodeClass.isAssignableFrom(klass)) {
text = invokeMessageMethod(errorCodeClass, "getMessage", e);
}
if (labelClass.isAssignableFrom(klass)) {
text = invokeMessageMethod(labelClass, "getLabel", e);
}
map.put(name, text);
}
} else if (stageClass.isAssignableFrom(klass)) {
Annotation stageDef = klass.getAnnotation(stageDefClass);
if (stageDef != null) {
String labelText = invokeMessageMethod(stageDefClass, "label", stageDef);
map.put("stageLabel", labelText);
String descriptionText = invokeMessageMethod(stageDefClass, "description", stageDef);
map.put("stageDescription", descriptionText);
Annotation errorStage = klass.getAnnotation(errorStageClass);
if (errorStage != null) {
String errorLabelText = invokeMessageMethod(errorStageClass, "label", errorStage);
if (errorLabelText.isEmpty()) {
errorLabelText = labelText;
}
map.put("errorStageLabel", errorLabelText);
String errorDescriptionText = invokeMessageMethod(errorStageClass, "description", errorStage);
if (!errorDescriptionText.isEmpty()) {
errorDescriptionText = descriptionText;
}
map.put("errorStageDescription", errorDescriptionText);
}
}
for (Field field : klass.getFields()) {
Annotation configDef = field.getAnnotation(configDefClass);
if (configDef != null) {
String labelText = invokeMessageMethod(configDefClass, "label", configDef);
map.put("configLabel." + field.getName(), labelText);
String descriptionText = invokeMessageMethod(configDefClass, "description", configDef);
map.put("configDescription." + field.getName(), descriptionText);
}
}
}
return map;
}
// same escaping done by java.util.Properties for keys/values
/*
* Escapes special characters with a preceding slash
*/
private String escapeValue(String str, boolean escapeSpace) {
int len = str.length();
int bufLen = len * 2;
if (bufLen < 0) {
bufLen = Integer.MAX_VALUE;
}
StringBuilder outBuffer = new StringBuilder(bufLen);
for(int x=0; x<len; x++) {
char aChar = str.charAt(x);
// Handle common case first, selecting largest block that
// avoids the specials below
if ((aChar > 61) && (aChar < 127)) {
if (aChar == '\\') {
outBuffer.append('\\'); outBuffer.append('\\');
continue;
}
outBuffer.append(aChar);
continue;
}
switch(aChar) {
case ' ':
if (x == 0 || escapeSpace)
outBuffer.append('\\');
outBuffer.append(' ');
break;
case '\t':outBuffer.append('\\'); outBuffer.append('t');
break;
case '\n':outBuffer.append('\\'); outBuffer.append('n');
break;
case '\r':outBuffer.append('\\'); outBuffer.append('r');
break;
case '\f':outBuffer.append('\\'); outBuffer.append('f');
break;
case '=': // Fall through
case ':': // Fall through
case '#': // Fall through
case '!':
outBuffer.append('\\'); outBuffer.append(aChar);
break;
default:
outBuffer.append(aChar);
}
}
return outBuffer.toString();
}
private void createBundlesJar(File jarFile, Map<String, File> bundles) throws IOException {
try (JarOutputStream jar = new JarOutputStream(new FileOutputStream(jarFile))) {
for (Map.Entry<String, File> entry : bundles.entrySet()) {
addFile(entry.getKey(), entry.getValue(), jar);
}
}
}
private void addFile(String path, File file, JarOutputStream jar) throws IOException {
try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(file))) {
JarEntry entry = new JarEntry(path);
entry.setTime(file.lastModified());
jar.putNextEntry(entry);
IOUtils.copy(in, jar);
jar.closeEntry();
}
}
}