/* * #%L * Wisdom-Framework * %% * Copyright (C) 2013 - 2014 Wisdom Framework * %% * 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. * #L% */ package org.wisdom.openjpa.enhancer; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; 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.shared.utils.io.FileUtils; import org.apache.openjpa.enhance.PCEnhancer; import org.apache.openjpa.lib.util.Options; import org.wisdom.maven.WatchingException; import org.wisdom.maven.mojos.AbstractWisdomWatcherMojo; import org.wisdom.maven.osgi.BundlePackager; import org.wisdom.maven.utils.WatcherUtils; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.List; import java.util.Properties; /** * A Mojo enhancing entity classes for OpenJPA. */ @Mojo(name = "enhance-entities", threadSafe = false, requiresDependencyResolution = ResolutionScope.COMPILE, requiresProject = true, defaultPhase = LifecyclePhase.PROCESS_CLASSES) public class OpenJPAEnhancerMojo extends AbstractWisdomWatcherMojo { /** * The working directory for putting persistence.xml and * other stuff into if we need to. */ @Parameter(defaultValue = "${project.build.directory}/openjpa-work", required = true) protected File workDir; /** * Location where <code>persistence-enabled</code> classes are located. */ @Parameter(defaultValue = "${project.build.outputDirectory}", readonly = true) protected File classes; /** * Comma separated list of includes to scan searchDir to pass to the jobs. * This may be used to restrict the OpenJPA tasks to e.g. a single package which * contains all the entities. */ @Parameter(defaultValue = "**/*.class") private String includes; /** * Comma separated list of excludes to scan searchDir to pass to the jobs. * This option may be used to stop OpenJPA tasks from scanning non-JPA classes * (which usually leads to warnings such as "Type xxx has no metadata") */ @Parameter(defaultValue = "") private String excludes; /** * Additional properties passed to the OpenJPA tools. */ @Parameter private Properties toolProperties; /** * Used if a non-default file location for the persistence.xml should be used * If not specified, the default one in META-INF/persistence.xml will be used. */ @Parameter(defaultValue = "target/classes/META-INF/persistence.xml") private String persistenceXmlFile; /** * <p>This setting can be used to override any openjpa.ConnectionDriverName set in the * persistence.xml. It can also be used if the persistence.xml contains no connection * information at all.<P> * <p> * Sample: * <pre> * <connectionDriverName>com.mchange.v2.c3p0.ComboPooledDataSource</connectionDriverName> * </pre> * <p> * This is most times used in conjunction with {@link #connectionProperties}. */ @Parameter private String connectionDriverName; /** * The string used for passing information about the connectionDriverName. */ public static final String OPTION_CONNECTION_DRIVER_NAME = "ConnectionDriverName"; /** * <p>Used to define the credentials or any other connection properties.</p> * <p> * Sample: * <pre> * <connectionProperties> * driverClass=com.mysql.jdbc.Driver, * jdbcUrl=jdbc:mysql://localhost/mydatabase, * user=root, * password=, * minPoolSize=5, * acquireRetryAttempts=3, * maxPoolSize=20 * </connectionProperties> * </pre> * <p> * This is most times used in conjunction with {@link #connectionDriverName}. */ @Parameter private String connectionProperties; /** * the string used for passing information about the connectionProperties. */ public static final String OPTION_CONNECTION_PROPERTIES = "ConnectionProperties"; /** * List of all class path elements that will be searched for the * <code>persistence-enabled</code> classes and resources expected by * PCEnhancer. */ @Parameter(defaultValue = "${project.compileClasspathElements}", required = true, readonly = true) protected List<String> compileClasspathElements; /** * The properties option is used for passing information about the persistence.xml file location. */ public static final String OPTION_PROPERTIES_FILE = "propertiesFile"; /** * The JPA spec requires that all persistent classes define a no-arg constructor. * This flag tells the enhancer whether to add a protected no-arg constructor * to any persistent classes that don't already have one. */ @Parameter(defaultValue = "true") protected boolean addDefaultConstructor; /** * used for passing the addDefaultConstructor parameter to the enhancer tool */ private static final String OPTION_ADD_DEFAULT_CONSTRUCTOR = "addDefaultConstructor"; /** * Whether to throw an exception when it appears that a property access entity * is not obeying the restrictions placed on property access. */ @Parameter(defaultValue = "false") protected boolean enforcePropertyRestrictions; /** * used for passing the enforcePropertyRestrictions parameter to the enhnacer tool */ private static final String OPTION_ENFORCE_PROPERTY_RESTRICTION = "enforcePropertyRestrictions"; /** * Tell the PCEnhancer to use a temporary classloader for enhancement. * If you enable this feature, then no depending artifacts from the classpath will be used! * Please note that you have to disable the tmpClassLoader for some cases in OpenJPA-1.2.1 * due to an extended parsing strategy. */ @Parameter(defaultValue = "false") protected boolean tmpClassLoader; /** * used for passing the tmpClassLoader parameter to the enhnacer tool */ private static final String OPTION_USE_TEMP_CLASSLOADER = "tcl"; private static final String META_PERSISTENCE_HEADER = "Meta-Persistence"; /** * Perform whatever build-process behavior this <code>Mojo</code> implements. * <br/> * This is the main trigger for the <code>Mojo</code> inside the <code>Maven</code> system, and allows * the <code>Mojo</code> to communicate errors. * * @throws org.apache.maven.plugin.MojoExecutionException if an unexpected problem occurs. Throwing this * exception causes a "BUILD ERROR" message to be displayed. * @throws org.apache.maven.plugin.MojoFailureException if an expected problem (such as a compilation * failure) occurs. Throwing this exception causes a "BUILD FAILURE" message to be displayed. */ @Override public void execute() throws MojoExecutionException, MojoFailureException { if (!classes.exists()) { FileUtils.mkdir(classes.getAbsolutePath()); } List<File> entities = findEntityClassFiles(); enhance(entities); File persistence = ensurePersistenceXml(); final String path = getPathInBundle(persistence); if (path != null) { try { BundlePackager.addExtraHeaderToBundleManifest(basedir, META_PERSISTENCE_HEADER, path.replace("\\", "/")); // Invert path separator on windows. } catch (IOException e) { throw new MojoExecutionException("Cannot save extra headers", e); } } else { getLog().warn("Cannot infer the path of the persistence.xml file in the bundle, " + "do not forget to set the `Meta-Persistence` header in the `osgi.bnd` file"); } } private String getPathInBundle(File persistence) { if (WatcherUtils.isInDirectory(persistence, classes)) { return persistence.getAbsolutePath().substring(classes.getAbsolutePath().length()); } return null; } /** * Checks whether the given file is managed by the current watcher. Notice that implementation must not check * for the existence of the file as this method is also called for deleted files. * * @param file is the file. * @return {@literal true} if the watcher is interested in being notified on an event * attached to the given file, * {@literal false} otherwise. */ @Override public boolean accept(File file) { return WatcherUtils.hasExtension(file, "java"); } /** * Notifies the watcher that a new file is created. * * @param file is the file. * @return {@literal false} if the pipeline processing must be interrupted for this event. Most watchers should * return {@literal true} to let other watchers be notified. * @throws org.wisdom.maven.WatchingException if the watcher failed to process the given file. */ @Override public boolean fileCreated(File file) throws WatchingException { try { List<File> entities = findEntityClassFiles(); enhance(entities); return true; } catch (MojoExecutionException e) { throw new WatchingException("OpenJPA Enhancer", "Error while enhancing JPA entities", file, e); } } /** * Notifies the watcher that a file has been modified. * * @param file is the file. * @return {@literal false} if the pipeline processing must be interrupted for this event. Most watchers should * returns {@literal true} to let other watchers to be notified. * @throws org.wisdom.maven.WatchingException if the watcher failed to process the given file. */ @Override public boolean fileUpdated(File file) throws WatchingException { return fileCreated(file); } /** * Notifies the watcher that a file was deleted. * * @param file the file * @return {@literal false} if the pipeline processing must be interrupted for this event. Most watchers should * return {@literal true} to let other watchers be notified. * @throws org.wisdom.maven.WatchingException if the watcher failed to process the given file. */ @Override public boolean fileDeleted(File file) throws WatchingException { return fileCreated(file); } private File ensurePersistenceXml() throws MojoExecutionException { if (persistenceXmlFile != null) { File persistence = new File(project.getBasedir(), persistenceXmlFile); if (!persistence.isFile()) { throw new MojoExecutionException("Cannot find the custom persistence xml file - " + persistence.getAbsolutePath() + " is not a file"); } return persistence; } else { File persistence = new File(buildDirectory, "classes/META-INF/persistence.xml"); if (!persistence.isFile()) { throw new MojoExecutionException("Cannot find the persistence xml file - " + persistence.getAbsolutePath() + " is not a file"); } return persistence; } } /** * This will prepare the current ClassLoader and add all jars and local * classpaths (e.g. target/classes) needed by the OpenJPA task. * * @throws MojoExecutionException on any error inside the mojo */ protected void extendRealmClasspath() throws MojoExecutionException { List<URL> urls = new ArrayList<>(); for (String fileName : compileClasspathElements) { File pathElem = new File(fileName); try { URL url = pathElem.toURI().toURL(); urls.add(url); getLog().debug("Added classpathElement URL " + url); } catch (MalformedURLException e) { throw new MojoExecutionException("Error in adding the classpath " + pathElem, e); } } ClassLoader jpaRealm = new URLClassLoader(urls.toArray(new URL[urls.size()]), getClass().getClassLoader()); // set the new ClassLoader as default for this Thread // will be reverted in the caller method. Thread.currentThread().setContextClassLoader(jpaRealm); } /** * Locates and returns a list of class files found under specified class * directory. * * @return list of class files. * @throws MojoExecutionException if there was an error scanning class file * resources. */ protected List<File> findEntityClassFiles() throws MojoExecutionException { try { return FileUtils.getFiles(classes, includes, excludes); } catch (IOException e) { throw new MojoExecutionException("Error while scanning for '" + includes + "' in " + "'" + classes.getAbsolutePath() + "'.", e); } } /** * @param files List of files * @return the paths of the given files as String[] */ protected String[] getFilePaths(List<File> files) { String[] args = new String[files.size()]; for (int i = 0; i < files.size(); i++) { File file = files.get(i); args[i] = file.getAbsolutePath(); } return args; } /** * Get the options for the OpenJPA enhancer tool. * * @return populated Options */ protected Options getOptions() throws MojoExecutionException { File persistence = ensurePersistenceXml(); Options opts = new Options(); if (toolProperties != null) { opts.putAll(toolProperties); } opts.put(OPTION_PROPERTIES_FILE, persistence.getAbsolutePath()); if (connectionDriverName != null) { opts.put(OPTION_CONNECTION_DRIVER_NAME, connectionDriverName); } if (connectionProperties != null) { opts.put(OPTION_CONNECTION_PROPERTIES, connectionProperties); } // put the standard options into the list also opts.put(OPTION_ADD_DEFAULT_CONSTRUCTOR, Boolean.toString(addDefaultConstructor)); opts.put(OPTION_ENFORCE_PROPERTY_RESTRICTION, Boolean.toString(enforcePropertyRestrictions)); opts.put(OPTION_USE_TEMP_CLASSLOADER, Boolean.toString(tmpClassLoader)); return opts; } /** * Processes a list of class file resources that are to be enhanced. * * @param files class file resources to enhance. * @throws MojoExecutionException if the enhancer encountered a failure */ private void enhance(List<File> files) throws MojoExecutionException { Options opts = getOptions(); // list of input files String[] args = getFilePaths(files); boolean ok; final ClassLoader original = Thread.currentThread().getContextClassLoader(); try { if (!tmpClassLoader) { extendRealmClasspath(); } ok = PCEnhancer.run(args, opts); } finally { // We may have change the TCCL, restore the original one Thread.currentThread().setContextClassLoader(original); } if (!ok) { throw new MojoExecutionException("The OpenJPA Enhancer tool detected an error, check log"); } } }