blob: 8a4acaadbe8448e8425943341a61aefcf8717ddb [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.uima.fit.maven;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtField;
import javassist.LoaderClassPath;
import javassist.NotFoundException;
import javassist.bytecode.AnnotationsAttribute;
import javassist.bytecode.ClassFile;
import javassist.bytecode.ConstPool;
import javassist.bytecode.annotation.Annotation;
import javassist.bytecode.annotation.MemberValue;
import javassist.bytecode.annotation.StringMemberValue;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.LineIterator;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Component;
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 org.apache.uima.fit.descriptor.ResourceMetaData;
import org.apache.uima.fit.factory.ConfigurationParameterFactory;
import org.apache.uima.fit.factory.ExternalResourceFactory;
import org.apache.uima.fit.factory.ResourceMetaDataFactory;
import org.apache.uima.fit.internal.EnhancedClassFile;
import org.apache.uima.fit.maven.util.Util;
import org.codehaus.plexus.util.FileUtils;
import org.sonatype.plexus.build.incremental.BuildContext;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Multimap;
import com.thoughtworks.qdox.model.JavaSource;
/**
* Enhance UIMA components with automatically generated uimaFIT annotations.
*/
@Mojo(name = "enhance", defaultPhase = LifecyclePhase.PROCESS_CLASSES, requiresDependencyResolution = ResolutionScope.COMPILE, requiresDependencyCollection = ResolutionScope.COMPILE)
public class EnhanceMojo extends AbstractMojo {
@Component
private MavenProject project;
@Component
private BuildContext buildContext;
private ClassLoader componentLoader;
/**
* Override component description in generated descriptors.
*/
@Parameter(defaultValue = "false", required = true)
private boolean overrideComponentDescription;
/**
* Override version in generated descriptors.
*
* @see #componentVersion
*/
@Parameter(defaultValue = "false", required = true)
private boolean overrideComponentVersion;
/**
* Version to use in generated descriptors.
*/
@Parameter(defaultValue = "${project.version}", required = false)
private String componentVersion;
/**
* Override vendor in generated descriptors.
*
* @see #componentVendor
*/
@Parameter(defaultValue = "false", required = true)
private boolean overrideComponentVendor;
/**
* Vendor to use in generated descriptors.
*/
@Parameter(defaultValue = "${project.organization.name}", required = false)
private String componentVendor;
/**
* Override copyright in generated descriptors.
*
* @see #componentCopyright
*/
@Parameter(defaultValue = "false", required = true)
private boolean overrideComponentCopyright;
/**
* Copyright to use in generated descriptors.
*/
@Parameter(required = false)
private String componentCopyright;
/**
* Source file encoding.
*/
@Parameter(defaultValue = "${project.build.sourceEncoding}", required = true)
private String encoding;
/**
* Generate a report of missing meta data in {@code $ project.build.directory}
* /uimafit-missing-meta-data-report.txt}
*/
@Parameter(defaultValue = "true", required = true)
private boolean generateMissingMetaDataReport;
/**
* Fail on missing meta data. This setting has no effect unless
* {@code generateMissingMetaDataReport} is enabled.
*/
@Parameter(defaultValue = "false", required = true)
private boolean failOnMissingMetaData;
/**
* Constant name prefixes used for parameters and external resources, e.g. "PARAM_".
*/
@Parameter(required = false)
private String[] parameterNameConstantPrefixes = { "PARAM_" };
/**
* Constant name prefixes used for parameters and external resources, e.g. "KEY_", and "RES_".
*/
@Parameter(required = false)
private String[] externalResourceNameConstantPrefixes = { "KEY_", "RES_" };
/**
* Start of a line containing a class name in the missing meta data report file
*/
private static final String MARK_CLASS = "Class:";
/**
* Marker that no missing meta data report was found.
*/
private static final String MARK_NO_MISSING_META_DATA = "No missing meta data was found.";
public void execute() throws MojoExecutionException, MojoFailureException {
// Get the compiled classes from this project
String[] files = FileUtils.getFilesFromExtension(project.getBuild().getOutputDirectory(),
new String[] { "class" });
componentLoader = Util.getClassloader(project, getLog());
// Set up class pool with all the project dependencies and the project classes themselves
ClassPool classPool = new ClassPool(true);
classPool.appendClassPath(new LoaderClassPath(componentLoader));
// Set up map to keep a report per class.
Multimap<String, String> reportData = LinkedHashMultimap.create();
// Determine where to write the missing meta data report file
File reportFile = new File(project.getBuild().getDirectory(),
"uimafit-missing-meta-data-report.txt");
// Read existing report
if (generateMissingMetaDataReport) {
readMissingMetaDataReport(reportFile, reportData);
}
// Remember the names of all examined components, whether processed or not.
List<String> examinedComponents = new ArrayList<String>();
int countAlreadyEnhanced = 0;
int countEnhanced = 0;
for (String file : files) {
String clazzName = Util.getClassName(project, file);
// Check if this is a UIMA component
Class<?> clazz;
try {
clazz = componentLoader.loadClass(clazzName);
// Do not process a class twice
// UIMA-3853 workaround for IBM Java 8 beta 3
if (clazz.getAnnotation(EnhancedClassFile.class) != null) {
countAlreadyEnhanced++;
getLog().debug("Class [" + clazzName + "] already enhanced");
// Remember that class was examined
examinedComponents.add(clazzName);
continue;
}
// Only process UIMA components
if (!Util.isComponent(componentLoader, clazz)) {
continue;
}
} catch (ClassNotFoundException e) {
getLog().warn("Cannot analyze class [" + clazzName + "]", e);
continue;
}
// Remember that class was examined
examinedComponents.add(clazzName);
// Forget any previous missing meta data report we have on the class
reportData.removeAll(clazzName);
// Get the Javassist class
CtClass ctClazz;
try {
ctClazz = classPool.get(clazzName);
} catch (NotFoundException e) {
throw new MojoExecutionException("Class [" + clazzName + "] not found in class pool: "
+ ExceptionUtils.getRootCauseMessage(e), e);
}
// Get the source file
String sourceFile = getSourceFile(clazzName);
// Try to extract parameter descriptions from JavaDoc in source file
if (sourceFile != null) {
countEnhanced++;
getLog().debug("Enhancing class [" + clazzName + "]");
// Parse source file so we can extract the JavaDoc
JavaSource ast = parseSource(sourceFile);
// Enhance meta data
enhanceResourceMetaData(ast, clazz, ctClazz, reportData);
// Enhance configuration parameters
enhanceConfigurationParameter(ast, clazz, ctClazz, reportData);
// Add the EnhancedClassFile annotation.
markAsEnhanced(ctClazz);
} else {
getLog().warn("No source file found for class [" + clazzName + "]");
}
try {
if (ctClazz.isModified()) {
getLog().debug("Writing enhanced class [" + clazzName + "]");
// Trying to work around UIMA-2611, see
// http://stackoverflow.com/questions/13797919/javassist-add-method-and-invoke
ctClazz.toBytecode();
ctClazz.writeFile(project.getBuild().getOutputDirectory());
} else {
getLog().debug("No changes to class [" + clazzName + "]");
}
} catch (IOException e) {
throw new MojoExecutionException("Enhanced class [" + clazzName + "] cannot be written: "
+ ExceptionUtils.getRootCauseMessage(e), e);
} catch (CannotCompileException e) {
throw new MojoExecutionException("Enhanced class [" + clazzName + "] cannot be compiled: "
+ ExceptionUtils.getRootCauseMessage(e), e);
}
}
getLog().info(
"Enhanced " + countEnhanced + " class" + (countEnhanced != 1 ? "es" : "") + " ("
+ countAlreadyEnhanced + " already enhanced).");
if (generateMissingMetaDataReport) {
// Remove any classes from the report that are no longer part of the build
List<String> deletedClasses = new ArrayList<String>(reportData.keySet());
deletedClasses.removeAll(examinedComponents);
reportData.removeAll(deletedClasses);
// Write updated report
writeMissingMetaDataReport(reportFile, reportData);
if (failOnMissingMetaData && !reportData.isEmpty()) {
throw new MojoFailureException("Component meta data missing. A report of the missing "
+ "meta data can be found in " + reportFile);
}
}
}
/**
* Add the EnhancedClassFile annotation.
*/
private void markAsEnhanced(CtClass aCtClazz) {
ClassFile classFile = aCtClazz.getClassFile();
ConstPool constPool = classFile.getConstPool();
AnnotationsAttribute annoAttr = (AnnotationsAttribute) classFile
.getAttribute(AnnotationsAttribute.visibleTag);
// Create annotation attribute if it does not exist
if (annoAttr == null) {
annoAttr = new AnnotationsAttribute(constPool, AnnotationsAttribute.visibleTag);
}
// Create annotation if it does not exist
Annotation a = new Annotation(EnhancedClassFile.class.getName(), constPool);
// Replace annotation
annoAttr.addAnnotation(a);
// Replace annotation attribute
classFile.addAttribute(annoAttr);
}
/**
* Enhance resource meta data
*/
private void enhanceResourceMetaData(JavaSource aAST, Class<?> aClazz, CtClass aCtClazz,
Multimap<String, String> aReportData) {
ClassFile classFile = aCtClazz.getClassFile();
ConstPool constPool = classFile.getConstPool();
AnnotationsAttribute annoAttr = (AnnotationsAttribute) classFile
.getAttribute(AnnotationsAttribute.visibleTag);
// Create annotation attribute if it does not exist
if (annoAttr == null) {
annoAttr = new AnnotationsAttribute(constPool, AnnotationsAttribute.visibleTag);
}
// Create annotation if it does not exist
Annotation a = annoAttr.getAnnotation(ResourceMetaData.class.getName());
if (a == null) {
a = new Annotation(ResourceMetaData.class.getName(), constPool);
// Add a name, otherwise there will be none in the generated descriptor.
a.addMemberValue("name", new StringMemberValue(
ResourceMetaDataFactory.getDefaultName(aClazz), constPool));
}
// Update description from JavaDoc
String doc = Util.getComponentDocumentation(aAST, aClazz.getName());
enhanceMemberValue(a, "description", doc, overrideComponentDescription,
ResourceMetaDataFactory.getDefaultDescription(aClazz), constPool, aReportData, aClazz);
// Update version
enhanceMemberValue(a, "version", componentVersion, overrideComponentVersion,
ResourceMetaDataFactory.getDefaultVersion(aClazz), constPool, aReportData, aClazz);
// Update vendor
enhanceMemberValue(a, "vendor", componentVendor, overrideComponentVendor,
ResourceMetaDataFactory.getDefaultVendor(aClazz), constPool, aReportData, aClazz);
// Update copyright
enhanceMemberValue(a, "copyright", componentCopyright, overrideComponentCopyright,
ResourceMetaDataFactory.getDefaultCopyright(aClazz), constPool, aReportData, aClazz);
// Replace annotation
annoAttr.addAnnotation(a);
// Replace annotation attribute
classFile.addAttribute(annoAttr);
}
/**
* Set a annotation member value if no value is present, if the present value is the default
* generated by uimaFIT or if a override is active.
*
* @param aAnnotation
* an annotation
* @param aName
* the name of the member value
* @param aNewValue
* the value to set
* @param aOverride
* set value even if it is already set
* @param aDefault
* default value set by uimaFIT - if the member has this value, it is considered unset
*/
private void enhanceMemberValue(Annotation aAnnotation, String aName, String aNewValue,
boolean aOverride, String aDefault, ConstPool aConstPool,
Multimap<String, String> aReportData, Class<?> aClazz) {
String value = getStringMemberValue(aAnnotation, aName);
boolean isEmpty = value.length() == 0;
boolean isDefault = value.equals(aDefault);
if (isEmpty || isDefault || aOverride) {
if (aNewValue != null) {
aAnnotation.addMemberValue(aName, new StringMemberValue(aNewValue, aConstPool));
getLog().debug("Enhanced component meta data [" + aName + "]");
} else {
getLog().debug("No meta data [" + aName + "] found");
aReportData.put(aClazz.getName(), "No meta data [" + aName + "] found");
}
} else {
getLog().debug("Not overwriting component meta data [" + aName + "]");
}
}
private String getStringMemberValue(Annotation aAnnotation, String aValue) {
MemberValue v = aAnnotation.getMemberValue(aValue);
if (v == null) {
return "";
} else {
return ((StringMemberValue) v).getValue();
}
}
/**
* Enhance descriptions in configuration parameters.
*/
private void enhanceConfigurationParameter(JavaSource aAST, Class<?> aClazz, CtClass aCtClazz,
Multimap<String, String> aReportData) throws MojoExecutionException {
// Get the parameter name constants
Map<String, String> parameterNameFields = getParameterConstants(aClazz,
parameterNameConstantPrefixes);
Map<String, String> resourceNameFields = getParameterConstants(aClazz,
externalResourceNameConstantPrefixes);
// Fetch configuration parameters from the @ConfigurationParameter annotations in the
// compiled class. We only need the fields in the class itself. Superclasses should be
// enhanced by themselves.
for (Field field : aClazz.getDeclaredFields()) {
final String pname;
final String type;
final String pdesc;
// Is this a configuration parameter?
if (ConfigurationParameterFactory.isConfigurationParameterField(field)) {
type = "parameter";
// Extract configuration parameter information from the uimaFIT annotation
pname = ConfigurationParameterFactory.createPrimitiveParameter(field).getName();
// Extract JavaDoc for this resource from the source file
pdesc = Util.getParameterDocumentation(aAST, field.getName(),
parameterNameFields.get(pname));
}
// Is this an external resource?
else if (ExternalResourceFactory.isExternalResourceField(field)) {
type = "external resource";
// Extract resource key from the uimaFIT annotation
pname = ExternalResourceFactory.createExternalResourceDependency(field).getKey();
// Extract JavaDoc for this resource from the source file
pdesc = Util.getParameterDocumentation(aAST, field.getName(),
resourceNameFields.get(pname));
} else {
continue;
}
if (pdesc == null) {
String msg = "No description found for " + type + " [" + pname + "]";
getLog().debug(msg);
aReportData.put(aClazz.getName(), msg);
continue;
}
// Update the "description" field of the annotation
try {
CtField ctField = aCtClazz.getField(field.getName());
AnnotationsAttribute annoAttr = (AnnotationsAttribute) ctField.getFieldInfo().getAttribute(
AnnotationsAttribute.visibleTag);
// Locate and update annotation
if (annoAttr != null) {
Annotation[] annotations = annoAttr.getAnnotations();
// Update existing annotation
for (Annotation a : annotations) {
if (a.getTypeName().equals(
org.apache.uima.fit.descriptor.ConfigurationParameter.class.getName())
|| a.getTypeName().equals(
org.apache.uima.fit.descriptor.ExternalResource.class.getName())
|| a.getTypeName().equals("org.uimafit.descriptor.ConfigurationParameter")
|| a.getTypeName().equals("org.uimafit.descriptor.ExternalResource")) {
if (a.getMemberValue("description") == null) {
a.addMemberValue("description", new StringMemberValue(pdesc, aCtClazz
.getClassFile().getConstPool()));
getLog().debug("Enhanced description of " + type + " [" + pname + "]");
// Replace updated annotation
annoAttr.addAnnotation(a);
} else {
// Extract configuration parameter information from the uimaFIT annotation
// We only want to override if the description is not set yet.
getLog().debug("Not overwriting description of " + type + " [" + pname + "] ");
}
}
}
}
// Replace annotations
ctField.getFieldInfo().addAttribute(annoAttr);
} catch (NotFoundException e) {
throw new MojoExecutionException("Field [" + field.getName() + "] not found in byte code: "
+ ExceptionUtils.getRootCauseMessage(e), e);
}
}
}
/**
* Get a map of parameter name to parameter name constant field, e.g. ("value",
* Field("PARAM_VALUE")).
*/
private Map<String, String> getParameterConstants(Class<?> aClazz, String[] aPrefixes) {
Map<String, String> result = new HashMap<String, String>();
for (Field f : aClazz.getFields()) {
boolean hasPrefix = false;
// Check if any of the registered prefixes matches
for (String prefix : aPrefixes) {
if (f.getName().startsWith(prefix)) {
hasPrefix = true;
break;
}
}
// If none matched, continue
if (!hasPrefix) {
continue;
}
// If one matched, record the field
try {
String parameterName = (String) f.get(null);
result.put(parameterName, f.getName());
} catch (IllegalAccessException e) {
getLog().warn(
"Unable to access name constant field [" + f.getName() + "]: "
+ ExceptionUtils.getRootCauseMessage(e), e);
}
}
return result;
}
private JavaSource parseSource(String aSourceFile) throws MojoExecutionException {
try {
return Util.parseSource(aSourceFile, encoding);
} catch (IOException e) {
throw new MojoExecutionException("Unable to parse source file [" + aSourceFile + "]: "
+ ExceptionUtils.getRootCauseMessage(e), e);
}
}
/**
* Get the source file for the given class.
*
* @return The path to the source file or {@code null} if no source file was found.
*/
@SuppressWarnings("unchecked")
private String getSourceFile(String aClassName) {
String sourceName = aClassName.replace('.', '/') + ".java";
for (String root : (List<String>) project.getCompileSourceRoots()) {
File f = new File(root, sourceName);
if (f.exists()) {
return f.getPath();
}
}
return null;
}
/**
* Write a report on any meta data missing from components.
*/
private void writeMissingMetaDataReport(File aReportFile, Multimap<String, String> aReportData)
throws MojoExecutionException {
String[] classes = aReportData.keySet().toArray(new String[aReportData.keySet().size()]);
Arrays.sort(classes);
PrintWriter out = null;
FileUtils.mkdir(aReportFile.getParent());
try {
out = new PrintWriter(new OutputStreamWriter(new FileOutputStream(aReportFile), encoding));
if (classes.length > 0) {
for (String clazz : classes) {
out.printf("%s %s%n", MARK_CLASS, clazz);
Collection<String> messages = aReportData.get(clazz);
if (messages.isEmpty()) {
out.printf(" No problems");
} else {
for (String message : messages) {
out.printf(" %s%n", message);
}
}
out.printf("%n");
}
} else {
out.printf("%s%n", MARK_NO_MISSING_META_DATA);
}
} catch (IOException e) {
throw new MojoExecutionException("Unable to write missing meta data report to ["
+ aReportFile + "]" + ExceptionUtils.getRootCauseMessage(e), e);
} finally {
IOUtils.closeQuietly(out);
}
}
/**
* Read the missing meta data report from a previous run.
*/
private void readMissingMetaDataReport(File aReportFile, Multimap<String, String> aReportData)
throws MojoExecutionException {
if (!aReportFile.exists()) {
// Ignore if the file is missing
return;
}
LineIterator i = null;
try {
String clazz = null;
i = IOUtils.lineIterator(new FileInputStream(aReportFile), encoding);
while (i.hasNext()) {
String line = i.next();
// Report say there is no missing meta data
if (line.startsWith(MARK_NO_MISSING_META_DATA)) {
return;
}
// Line containing class name
if (line.startsWith(MARK_CLASS)) {
clazz = line.substring(MARK_CLASS.length()).trim();
} else if (StringUtils.isBlank(line)) {
// Empty line, ignore
} else {
// Line containing a missing meta data instance
if (clazz == null) {
throw new MojoExecutionException("Missing meta data report has invalid format.");
}
aReportData.put(clazz, line.trim());
}
}
} catch (IOException e) {
throw new MojoExecutionException("Unable to read missing meta data report: "
+ ExceptionUtils.getRootCauseMessage(e), e);
} finally {
LineIterator.closeQuietly(i);
}
}
}