Package org.apache.uima.fit.maven

Source Code of org.apache.uima.fit.maven.EnhanceMojo

/*
* 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);
    }
  }
}
TOP

Related Classes of org.apache.uima.fit.maven.EnhanceMojo

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.