/******************************************************************************* * Copyright (c) 2006-2010 eBay Inc. All Rights Reserved. * 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 *******************************************************************************/ package org.ebayopensource.turmeric.plugins.maven; import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Properties; import java.util.logging.Handler; import java.util.logging.Logger; import java.util.regex.Pattern; import org.apache.commons.io.FilenameUtils; import org.apache.maven.model.Resource; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.project.MavenProject; import org.codehaus.plexus.interpolation.EnvarBasedValueSource; import org.codehaus.plexus.interpolation.InterpolationException; import org.codehaus.plexus.interpolation.Interpolator; import org.codehaus.plexus.interpolation.PrefixedObjectValueSource; import org.codehaus.plexus.interpolation.PropertiesBasedValueSource; import org.codehaus.plexus.interpolation.RegexBasedInterpolator; import org.codehaus.plexus.resource.ResourceManager; import org.codehaus.plexus.resource.loader.FileResourceLoader; import org.codehaus.plexus.resource.loader.ThreadContextClasspathResourceLoader; import org.codehaus.plexus.resource.loader.URLResourceLoader; import org.codehaus.plexus.util.IOUtil; import org.codehaus.plexus.util.StringUtils; import org.ebayopensource.turmeric.plugins.maven.resources.ResourceLocator.Location; import org.ebayopensource.turmeric.plugins.maven.utils.AddMatchingFilesFilter; import org.ebayopensource.turmeric.plugins.maven.utils.GenTimestamp; import org.ebayopensource.turmeric.plugins.maven.utils.LogDelegateHandler; import org.ebayopensource.turmeric.tools.codegen.NonInteractiveCodeGen; import org.jdom.Document; import org.jdom.Element; import org.jdom.JDOMException; import org.jdom.input.SAXBuilder; /** * Base level mojo for the most common turmeric parameters, and methods. */ public abstract class AbstractTurmericMojo extends AbstractMojo { private static final String GROUPID_SELF = "org.ebayopensource.turmeric.maven"; private static final String ARTIFACTID_SELF = "turmeric-maven-plugin"; /** * The default maven project object * * @parameter expression="${project}" * @required * @readonly */ protected MavenProject project; /** * * @parameter expression="${turmeric.verbose}" default-value="true" * @required */ protected boolean verbose; /** * Plexus component used to lookup resources from a variety of search locations (including Thread context * classloader, a file resource from the project base, a file resource from the project resources directories, a * fully qualified file path, or even a url) * * @component role="org.codehaus.plexus.resource.ResourceManager" role-hint="default" * @required * @readonly */ private ResourceManager locator; /** * Timestamp file used for tracking last generation and preventing a loop of generation seen in m2eclipse. * * @parameter expression="${project.build.directory}/turmeric-maven-plugin-gen-timestamp" * @required */ private File timestampFile; private GenTimestamp timestamps; private boolean locatorAlreadyConfigured = false; protected final void ensureDirectoryExists(String id, File dir) throws MojoExecutionException { if (!dir.exists()) { if (!dir.mkdirs()) { throw new MojoExecutionException("Unable to create " + id + ": " + dir); } } } /** * Wraps the mojo execute with some behavioral lifecycle. */ @Override public final void execute() throws MojoExecutionException, MojoFailureException { getLog().debug("[execute]"); getLog().info("Using turmeric-maven-plugin version " + getTurmericMavenPluginVersion()); if (executeSkip()) { getLog().warn("Skipping execution"); return; } if (verbose) { logDependencyDetails("Turmeric Maven Plugin", AbstractTurmericMojo.class, GROUPID_SELF, ARTIFACTID_SELF); logDependencyDetails("Codegen Tools", NonInteractiveCodeGen.class, "org.ebayopensource.turmeric.runtime", "soa-client"); getLog().info("Verbose Mode: enabling java.util.logging"); // Initialize java.util.logging (which is present in CodeGen classes) Logger root = Logger.getLogger(""); // Remove old delegates for (Handler handler : root.getHandlers()) { getLog().info("Removing existing logging handler: " + handler.getClass().getName()); root.removeHandler(handler); } // Add our delegate root.addHandler(new LogDelegateHandler(getLog())); } getLog().debug("[onValidateParameters]"); onValidateParameters(); // Test for need to generate getLog().debug("[needsGeneration]"); if (needsGeneration() == false) { getLog().warn("No need to generate. skipping turmeric plugin execution."); return; } try { getLog().debug("[onRunSetup]"); onRunSetup(); // Attach directories to project even if skipping servicegen. // This is to be a good maven and m2eclipse citizen. getLog().debug("[onAttachGeneratedDirectories]"); onAttachGeneratedDirectories(); getLog().debug("[onRun]"); onRun(); } finally { getLog().debug("[onRunTearDown]"); onRunTearDown(); } } /** * Look into the ${project.basedir}/.project file and see if the turmeric eclipse plugin's nature for codegen is * defined. * * @return */ public boolean isTurmericEclipsePluginEnabled() { File projectFile = new File(project.getBasedir(), ".project"); if (!projectFile.exists()) { // No .project file, not using turmeric eclipse plugin return false; } try { Document doc = parseXml(projectFile); Pattern legacyNaturePattern = Pattern .compile("^org\\.ebay\\.soaframework\\.eclipse\\.SOA[A-Za-z0-9]*ProjectNature$"); Pattern naturePattern = Pattern .compile("^org\\.ebayopensource\\.turmeric\\.eclipse\\.build\\.SOA[A-Za-z0-9]*ProjectNature$"); Pattern typeLibNaturePattern = Pattern .compile("^org\\.ebayopensource\\.turmeric\\.eclipse\\.typelibrary\\.Type[A-Za-z0-9]*ProjectNature$"); Pattern errorLibNaturePattern = Pattern .compile("^org\\.ebayopensource\\.turmeric\\.eclipse\\.errorlibrary\\.properties\\.TurmericError[A-Za-z0-9]*ProjectNature$"); Element root = doc.getRootElement(); if (!"projectDescription".equals(root.getName())) { getLog().error("Not an eclipse .project file" + " (format mismatch): " + projectFile); return false; } @SuppressWarnings("unchecked") List<Element> naturesList = root.getChildren("natures"); for (Element natureRoot : naturesList) { @SuppressWarnings("unchecked") List<Element> natures = natureRoot.getChildren("nature"); for (Element nature : natures) { String natureSpec = nature.getTextTrim(); if (naturePattern.matcher(natureSpec).matches() || legacyNaturePattern.matcher(natureSpec).matches() || typeLibNaturePattern.matcher(natureSpec).matches() || errorLibNaturePattern.matcher(natureSpec).matches()) { return true; } } } } catch (MojoExecutionException e) { getLog().error("Unable to parse Eclipse .project file: " + projectFile, e); } return false; } /** * Obtains the as-built version of the turmeric maven plugin for reporting to the user. */ private String getTurmericMavenPluginVersion() { String pompath = String.format("META-INF/maven/%s/%s/pom.properties", GROUPID_SELF, ARTIFACTID_SELF); URL url = this.getClass().getClassLoader().getResource(pompath); if (url == null) { return "(unknown/dev)"; } Properties props = new Properties(); InputStream stream = null; try { stream = url.openStream(); props.load(stream); String version = props.getProperty("version"); if (StringUtils.isBlank(version)) { return "(unknown)"; } return version; } catch (IOException e) { getLog().debug("Unable to read version from: " + pompath, e); return "(unknown/io)"; } finally { IOUtil.close(stream); } } public void logDependencyDetails(String header, Class<?> clazz, String groupId, String artifactId) { StringBuilder msg = new StringBuilder(); msg.append(header); msg.append("\n Version: ").append(getClassVersion(clazz, groupId, artifactId)); msg.append("\n Location: ").append(getJarLocationOfClass(clazz)); getLog().info(msg.toString()); } public void logClassLocation(Class<?> clazz) { getLog().info("Location of class: " + clazz.getName()); getLog().info(" is: " + getJarLocationOfClass(clazz)); } public String getClassVersion(Class<?> clazz, String groupId, String artifactId) { try { String ver; ver = getPackageVersion(clazz); if (ver != null) { return ver; } ver = getMavenVersion(groupId, artifactId); if (ver != null) { return ver; } } catch (Exception ignore) { /* ignore */ } return "<unknown>"; } private String getMavenVersion(String groupId, String artifactId) { try { String resource = String.format("META-INF/maven/%s/%s/pom.properties", groupId, artifactId); URL url = this.getClass().getClassLoader().getResource(resource); if (url == null) { return null; } InputStream in = null; try { in = url.openStream(); Properties props = new Properties(); props.load(in); return props.getProperty("version"); } finally { IOUtil.close(in); } } catch (Exception ignore) { /* ignore */ } return null; } private static String getPackageVersion(Class<?> clazz) { Package p = clazz.getPackage(); if (p == null) { return null; } String ver = p.getImplementationVersion(); if (ver == null) { return null; } return ver; } public static String getJarLocationOfClass(Class<?> clazz) { try { String classpath = clazz.getName().replace('.', '/') + ".class"; URL resource = clazz.getClassLoader().getResource(classpath); if (resource != null) { String uri = resource.toExternalForm(); int idx = uri.lastIndexOf("!"); if (idx > 0) { uri = uri.substring(0, idx); return uri; } else if (uri.endsWith(classpath)) { return uri.substring(0, uri.length() - classpath.length()); } else { return uri; } } } catch (Exception ignore) { /* ignore */ } return "<unknown>"; } /** * Attempt to determine if we are running within the Eclipse environment. */ public boolean isMojoRunningInEclipse() { try { getLog().debug("Eclipse Check: is org.eclipse.core.launcher.Main in classpath?"); // Simple check: is launcher main in classpath Class<?> c = Class.forName("org.eclipse.core.launcher.Main"); if (c != null) { return true; } } catch (ClassNotFoundException e) { /* * Launcher not found in classpath. Perform other tests in an attempt to verify. */ } String FS = File.separator; // Are we using a plugin embedded maven (1/2) // Test the "java.class.path" property String jClassPath = System.getProperty("java.class.path"); if (StringUtils.isNotBlank(jClassPath)) { String expected = FS + "plugins" + FS + "org.maven.ide.eclipse"; getLog().debug("Eclipse Check: \"java.class.path\" = \"" + jClassPath + "\".contains(\"" + expected + "\")"); if (jClassPath.contains(expected)) { // We are using a plugin embedded maven return true; } } // Are we using a plugin embedded maven (2/2) // Test the "classworlds.conf" property String cworldConf = System.getProperty("classworlds.conf"); if (StringUtils.isNotBlank(cworldConf)) { String expected = FS + ".metadata" + FS + ".plugins" + FS + "org.maven.ide.eclipse"; getLog().debug("Eclipse Check: \"classworlds.conf\" = \"" + cworldConf + "\".contains(\"" + expected + "\")"); if (cworldConf.contains(expected)) { // We are using a plugin embedded maven. return true; } } getLog().debug("Eclipse Check: Not running in Eclipse."); return false; } /** * Convenience method for using {@link #expandParameter(String)} with File objects. * * @param rawfile * the raw file object * @return null if rawfile is null, or the expanded File object. * @throws MojoExecutionException */ public File expandFile(File rawfile) throws MojoExecutionException { if (rawfile == null) { return null; } String rawpath = rawfile.getPath(); return new File(expandParameter(rawpath)); } /** * Take a raw parameter value, and expand any found properties within it. * * @param parameter * @return the expanded parameter * @throws MojoExecutionException * if unable to interpolate */ public String expandParameter(String rawparameter) throws MojoExecutionException { if (StringUtils.isBlank(rawparameter)) { return rawparameter; } try { Interpolator interpolator = new RegexBasedInterpolator(); interpolator.addValueSource(new PrefixedObjectValueSource("project", project)); interpolator.addValueSource(new PrefixedObjectValueSource("mojo", this)); interpolator.addValueSource(new PropertiesBasedValueSource(System.getProperties())); interpolator.addValueSource(new EnvarBasedValueSource()); String result = interpolator.interpolate(rawparameter); if (getLog().isDebugEnabled()) { getLog().debug("Expand Parameter: " + "\n Raw: " + rawparameter + "\n Expanded: " + result); } return result; } catch (IOException e) { throw new MojoExecutionException("Unable to use Environment for Parameter Interpolation", e); } catch (InterpolationException e) { throw new MojoExecutionException("Unable to use interpolatate: " + rawparameter, e); } } /** * Look through all project.resources trees and find the pathref specified. * <p> * Since this mojo executes in the 'generate-sources' phase, we can't rely on the resources being (yet) present in * the output directories, as that occurs later in the maven lifecycle in the 'process-resources' phase. See <a * href="http://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html#Lifecycle_Reference" >Maven * Lifecycle</a> for details of phase order. * * @param pathref * the pathref within the resource directory to look for * @return the File path pointing to the resources directory content, or null if not found. */ protected final File findResourceFile(String pathref) { if (StringUtils.isBlank(pathref)) { getLog().warn("Unable to lookup resource for null pathref: " + pathref); return null; } List<?> resources = project.getBuild().getResources(); getLog().debug("Looking for resource in [" + resources.size() + "] directories: " + pathref); Iterator<?> iter = resources.iterator(); while (iter.hasNext()) { Resource resource = (Resource) iter.next(); String resDir = resource.getDirectory(); if (StringUtils.isBlank(resDir)) { getLog().warn("Blank resources directory: " + resource); continue; } File dir = new File(resDir); getLog().debug("Testing for resource in dir: " + dir); File possiblePath = new File(dir, pathref); if (possiblePath.exists()) { getLog().debug("Found resource: " + possiblePath); return possiblePath; } } return null; } /** * Get a configured ResourceManager for location lookups. * <p> * Note: be sure you set the {@link ResourceManager#setOutputDirectory(File)}! * * @return the resource locator to use. */ protected ResourceManager getResourceLocator() { if (locatorAlreadyConfigured) { return locator; } locatorAlreadyConfigured = true; locator.addSearchPath(ThreadContextClasspathResourceLoader.ID, ""); locator.addSearchPath(FileResourceLoader.ID, project.getBasedir().getAbsolutePath()); List<?> resources = project.getBuild().getResources(); Iterator<?> iter = resources.iterator(); while (iter.hasNext()) { Resource resource = (Resource) iter.next(); locator.addSearchPath(FileResourceLoader.ID, resource.getDirectory()); } locator.addSearchPath(URLResourceLoader.ID, ""); return locator; } public MavenProject getProject() { return project; } public boolean isVerbose() { return verbose; } /** * Internal check to see if execution is needed. * <p> * This differs from {@link #needsGeneration()} in so far that the variables on the mojo are not validated or * expanded at this point in the mojo lifecycle. * <p> * This is really only useful in mitigating the conflict that appears when the same functionality exists between a * specific Mojo goal and an Eclipse Plugin. * * @return true to indicate that execution can be skipped. * @throws MojoExecutionException * when wrapping other exceptions. Will cause build to go into error, resulting in a "BUILD ERROR" * message. The build will stop. * @throws MojoFailureException * to throw new exceptions. Will cause build to go into error, resulting in a "BUILD FAILURE" message. * The build will stop. */ public boolean executeSkip() throws MojoExecutionException, MojoFailureException { // Default is to not skip execution. return false; } /** * Internal check to see if generation is needed. * * @return true to indicate that generation is required, false to skip generation * @throws MojoExecutionException * when wrapping other exceptions. Will cause build to go into error, resulting in a "BUILD ERROR" * message. The build will stop. * @throws MojoFailureException * to throw new exceptions. Will cause build to go into error, resulting in a "BUILD FAILURE" message. * The build will stop. */ public abstract boolean needsGeneration() throws MojoExecutionException, MojoFailureException; /** * Attach any of the generated directories. */ protected abstract void onAttachGeneratedDirectories(); /** * The name of the goal for the mojo. */ protected abstract String getGoalName(); /** * Event to perform mojo operations on. * * @throws MojoExecutionException * when wrapping other exceptions. Will cause build to go into error, resulting in a "BUILD ERROR" * message. The build will stop. * @throws MojoFailureException * to throw new exceptions. Will cause build to go into error, resulting in a "BUILD FAILURE" message. * The build will stop. */ public void onRun() throws MojoExecutionException, MojoFailureException { /* perform actual mojo steps */ } /** * Event to perform setup for a run of the mojo. * * @throws MojoExecutionException * when wrapping other exceptions. Will cause build to go into error, resulting in a "BUILD ERROR" * message. The build will stop. * @throws MojoFailureException * to throw new exceptions. Will cause build to go into error, resulting in a "BUILD FAILURE" message. * The build will stop. */ protected void onRunSetup() throws MojoExecutionException, MojoFailureException { /* setup directory output, copy files around, download files, etc ... */ } /** * Event to perform teardown for a run of the mojo. * * @throws MojoExecutionException * when wrapping other exceptions. Will cause build to go into error, resulting in a "BUILD ERROR" * message. The build will stop. * @throws MojoFailureException * to throw new exceptions. Will cause build to go into error, resulting in a "BUILD FAILURE" message. * The build will stop. */ protected void onRunTearDown() throws MojoExecutionException, MojoFailureException { /* cleanup temporary files, etc ... */ ensureDirectoryExists("Timestamp Home Dir", timestampFile.getParentFile()); timestamps.write(); } /** * Event to perform parameter validation on. * * @throws MojoExecutionException * when wrapping other exceptions. Will cause build to go into error, resulting in a "BUILD ERROR" * message. The build will stop. * @throws MojoFailureException * to throw new exceptions. Will cause build to go into error, resulting in a "BUILD FAILURE" message. * The build will stop. */ protected void onValidateParameters() throws MojoExecutionException, MojoFailureException { if (project == null) { throw new MojoFailureException("No project present!"); } timestampFile = expandFile(timestampFile); timestamps = new GenTimestamp(getLog(), timestampFile); } /** * Tests the provided paths against the last timestamp persisted to know if any of the paths have changed. * * @param paths * the paths to verify against. * @return true if a timestamp is newer than the persisted one. * @see GenTimestamp#isNewerThan(String, File...) */ protected boolean isNewerThanLastTimestamp(File... paths) { return timestamps.isNewerThan(getGoalName(), paths); } /** * Tests the provided live locations against the last timestamp persisted to know if any of the locations have * changed. * * @param locations * the locations to verify against. (non-live locations are ignored) * @return true if a timestamp is newer than the persisted one. * @see GenTimestamp#isNewerThan(String, File...) */ protected boolean isNewerThanLastTimestamp(Location... locations) { List<File> paths = new ArrayList<File>(); for (Location location : locations) { if (location == null) { continue; // skip null entries } if (location.isLiveFile()) { paths.add(location.getFile()); } } if (paths.isEmpty()) { // No paths, no change. return false; } return timestamps.isNewerThan(getGoalName(), paths.toArray(new File[0])); } /** * Get the project's pom.xml file. * * @return the project's pom.xml file (could be named something other than pom.xml) */ protected File getProjectPomFile() { return project.getFile(); } /** * Ensure that the path has appropriate separators for the system in use. * * @param path * the raw input path. * @return the corrected system separators */ protected String toOS(String path) { return FilenameUtils.separatorsToSystem(path); } /** * Validates that a resources exists in the project's resources. * * @param msg * the type of resource * @param pathref * the path reference to the resource. * @throws MojoFailureException */ protected void validateResourceExists(String msg, String pathref) throws MojoFailureException { File path = findResourceFile(pathref); if ((path == null) || (path.exists() == false)) { throw new MojoFailureException("Missing Required resource (" + msg + "): " + pathref); } } protected Document parseXml(File xmlfile) throws MojoExecutionException { try { SAXBuilder builder = new SAXBuilder(false); return builder.build(xmlfile); } catch (JDOMException e) { throw new MojoExecutionException("Unable to parse: " + xmlfile, e); } catch (IOException e) { throw new MojoExecutionException("Unable to parse: " + xmlfile, e); } } /** * Search through the project resource directories, and attempt to find a set of {@link File}s that match the * provided regex pattern. * * @param regex * the regex pattern to look for * @return the list of Files found (may be empty) */ protected List<File> findProjectResourceFiles(String regex) { List<File> hits = new ArrayList<File>(); Pattern pattern = Pattern.compile(regex); File dir; @SuppressWarnings("unchecked") List<Resource> resources = project.getBuild().getResources(); for (Resource resource : resources) { dir = new File(resource.getDirectory()); recursiveAddMatchingFiles(dir, new AddMatchingFilesFilter(getLog(), dir, pattern, hits)); } return hits; } private void recursiveAddMatchingFiles(File dir, FileFilter filter) { for (File path : dir.listFiles()) { if (path.isFile()) { filter.accept(path); continue; } if (path.isDirectory()) { recursiveAddMatchingFiles(path, filter); } } } }