package com.reucon.maven.plugin.openfire;
import org.apache.maven.archiver.MavenArchiveConfiguration;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.resolver.filter.ScopeArtifactFilter;
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.archiver.jar.JarArchiver;
import org.codehaus.plexus.util.*;
import java.io.*;
import java.util.*;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
public abstract class AbstractOpenfireMojo extends AbstractMojo
{
/**
* The maven project.
*
* @parameter expression="${project}"
* @required
* @readonly
*/
private MavenProject project;
/**
* The directory containing generated classes.
*
* @parameter expression="${project.build.outputDirectory}"
* @required
* @readonly
*/
private File classesDirectory;
/**
* The Jar archiver needed for archiving classes directory into jar file under WEB-INF/lib.
*
* @parameter expression="${component.org.codehaus.plexus.archiver.Archiver#jar}"
* @required
*/
protected JarArchiver jarArchiver;
/**
* The directory where the Openfire Plugin is built.
*
* @parameter expression="${project.build.directory}/${project.build.finalName}"
* @required
*/
private File openfirePluginDirectory;
/**
* Single directory for extra files to include in the WAR.
*
* @parameter expression="${basedir}/src/main/webapp"
* @required
*/
private File warSourceDirectory;
/**
* Single directory for Openfire Plugin configuration files like <tt>plugin.xml</tt>,
* <tt>changelog.html</tt> and <tt>readme.html</tt>.
*
* @parameter expression="${basedir}/src/main/openfire"
* @required
*/
private File openfireSourceDirectory;
/**
* Single directory for Openfire Plugin database scripts.
*
* @parameter expression="${basedir}/src/main/database"
* @required
*/
private File databaseSourceDirectory;
/**
* Single directory for Openfire Plugin i18n resources.
*
* @parameter expression="${basedir}/src/main/i18n"
* @required
*/
private File i18nSourceDirectory;
/**
* The list of webResources we want to transfer.
*
* @parameter
*/
private Resource[] webResources;
/**
* Filters (property files) to include during the interpolation of the pom.xml.
*
* @parameter expression="${project.build.filters}"
*/
private List filters;
/**
* The path to the web.xml file to use, original default was ${maven.war.webxml}.
*
* @parameter expression="${basedir}/target/web.xml"
*/
private File webXml;
/**
* The file name mapping to use to copy libraries and tlds. If no file mapping is
* set (default) the file is copied with its standard name.
*
* @parameter
* @since 2.0.3
*/
private String outputFileNameMapping;
private static final String WEB_INF = "WEB-INF";
private static final String META_INF = "META-INF";
private static final String[] DEFAULT_INCLUDES = {"**/**"};
private static final String DEFAULT_FILE_NAME_MAPPING_CLASSIFIER =
"${artifactId}-${version}-${classifier}.${extension}";
private static final String DEFAULT_FILE_NAME_MAPPING = "${artifactId}-${version}.${extension}";
/**
* The comma separated list of tokens to include in the WAR.
* Default is '**'.
*
* @parameter alias="includes"
*/
private String warSourceIncludes = "**";
/**
* The comma separated list of tokens to exclude from the WAR.
*
* @parameter alias="excludes"
*/
private String warSourceExcludes;
/**
* The maven archive configuration to use.
*
* @parameter
*/
protected MavenArchiveConfiguration archive = new MavenArchiveConfiguration();
private static final String[] EMPTY_STRING_ARRAY = {};
public MavenProject getProject()
{
return project;
}
public void setProject(MavenProject project)
{
this.project = project;
}
public File getClassesDirectory()
{
return classesDirectory;
}
public void setClassesDirectory(File classesDirectory)
{
this.classesDirectory = classesDirectory;
}
public File getOpenfirePluginDirectory()
{
return openfirePluginDirectory;
}
public void setOpenfirePluginDirectory(File openfirePluginDirectory)
{
this.openfirePluginDirectory = openfirePluginDirectory;
}
public File getWarSourceDirectory()
{
return warSourceDirectory;
}
public void setWarSourceDirectory(File warSourceDirectory)
{
this.warSourceDirectory = warSourceDirectory;
}
public File getWebXml()
{
return webXml;
}
public void setWebXml(File webXml)
{
this.webXml = webXml;
}
public String getOutputFileNameMapping()
{
return outputFileNameMapping;
}
public void setOutputFileNameMapping(String outputFileNameMapping)
{
this.outputFileNameMapping = outputFileNameMapping;
}
/**
* Returns a string array of the excludes to be used
* when assembling/copying the war.
*
* @return an array of tokens to exclude
*/
protected String[] getExcludes()
{
List<String> excludeList = new ArrayList<String>();
if (StringUtils.isNotEmpty(warSourceExcludes))
{
excludeList.addAll(Arrays.asList(StringUtils.split(warSourceExcludes, ",")));
}
return excludeList.toArray(EMPTY_STRING_ARRAY);
}
/**
* Returns a string array of the includes to be used
* when assembling/copying the war.
*
* @return an array of tokens to include
*/
protected String[] getIncludes()
{
return StringUtils.split(StringUtils.defaultString(warSourceIncludes), ",");
}
public void buildExplodedOpenfirePlugin(File openfireDirectory) throws MojoExecutionException, MojoFailureException
{
getLog().info("Exploding Openfire Plugin...");
openfireDirectory.mkdirs();
try
{
buildWebapp(project, openfireDirectory);
}
catch (IOException e)
{
throw new MojoExecutionException("Could not explode Openfire Plugin...", e);
}
}
private Map getBuildFilterProperties() throws MojoExecutionException
{
Map<Object, Object> filterProperties = new Properties();
// System properties
filterProperties.putAll(System.getProperties());
// Project properties
filterProperties.putAll(project.getProperties());
filterProperties.put("openfire-plugin.build.date", new SimpleDateFormat("MM/dd/yyyy").format(new Date()));
for (String filtersfile : (List<String>) filters)
{
try
{
Properties properties = PropertyUtils.loadPropertyFile(new File(filtersfile), true, true);
filterProperties.putAll(properties);
}
catch (IOException e)
{
throw new MojoExecutionException("Error loading property file '" + filtersfile + "'", e);
}
}
// can't putAll, as ReflectionProperties doesn't enumerate - so we make a composite map with the project variables as dominant
return new CompositeMap(new ReflectionProperties(project), filterProperties);
}
/**
* Copies webapp webResources from the specified directory.
* <p/>
* Note that the <tt>webXml</tt> parameter could be null and may
* specify a file which is not named <tt>web.xml<tt>. If the file
* exists, it will be copied to the <tt>META-INF</tt> directory and
* renamed accordingly.
*
* @param resource the resource to copy
* @param webappDirectory the target directory
* @param filterProperties
* @throws java.io.IOException if an error occured while copying webResources
*/
public void copyResources(Resource resource, File webappDirectory, Map filterProperties) throws IOException
{
if (!resource.getDirectory().equals(webappDirectory.getPath()))
{
getLog().info("Copy webapp webResources to " + webappDirectory.getAbsolutePath());
if (webappDirectory.exists())
{
String[] fileNames = getWarFiles(resource);
String targetPath = (resource.getTargetPath() == null) ? "" : resource.getTargetPath();
File destination = new File(webappDirectory, targetPath);
for (String fileName : fileNames)
{
if (resource.isFiltering())
{
copyFilteredFile(new File(resource.getDirectory(), fileName),
new File(destination, fileName), null, getFilterWrappers(),
filterProperties);
}
else
{
copyFileIfModified(new File(resource.getDirectory(), fileName),
new File(destination, fileName));
}
}
}
}
}
/**
* Copies webapp webResources from the specified directory.
* <p/>
* Note that the <tt>webXml</tt> parameter could be null and may
* specify a file which is not named <tt>web.xml<tt>. If the file
* exists, it will be copied to the <tt>META-INF</tt> directory and
* renamed accordingly.
*
* @param sourceDirectory the source directory
* @param webappDirectory the target directory
* @throws java.io.IOException if an error occured while copying webResources
*/
public void copyResources(File sourceDirectory, File webappDirectory) throws IOException
{
if (!sourceDirectory.equals(webappDirectory))
{
getLog().info("Copying webResources to " + webappDirectory.getAbsolutePath());
if (warSourceDirectory.exists())
{
String[] fileNames = getWarFiles(sourceDirectory);
for (String fileName : fileNames)
{
copyFileIfModified(new File(sourceDirectory, fileName), new File(webappDirectory, fileName));
}
}
}
}
private void copyOpenfirePluginConfiguration(File sourceDirectory, File openfirePluginDirectory,
Map filterProperties) throws IOException
{
if (!sourceDirectory.equals(openfirePluginDirectory))
{
getLog().info("Copying Openfire Plugin configuration to " + openfirePluginDirectory.getAbsolutePath());
if (warSourceDirectory.exists())
{
String[] fileNames = getWarFiles(sourceDirectory);
for (String fileName : fileNames)
{
if (fileName.endsWith(".html") || fileName.endsWith(".xml"))
{
copyFilteredFile(new File(sourceDirectory, fileName),
new File(openfirePluginDirectory, fileName), null, getFilterWrappers(),
filterProperties);
}
else
{
copyFileIfModified(new File(sourceDirectory, fileName),
new File(openfirePluginDirectory, fileName));
}
}
}
}
}
/**
* Builds the Openfire Plugin for the specified project.
* <p/>
* Classes and libraries are copied to
* <tt>openfirePluginDirectory</tt> during this phase.
*
* @param project the maven project
* @param openfirePluginDirectory
* @throws java.io.IOException if an error occured while building the webapp
*/
public void buildWebapp(MavenProject project, File openfirePluginDirectory)
throws MojoExecutionException, IOException, MojoFailureException
{
getLog().info("Assembling webapp " + project.getArtifactId() + " in " + openfirePluginDirectory);
File webinfDir = new File(openfirePluginDirectory, "web" + File.separator + WEB_INF);
webinfDir.mkdirs();
File metainfDir = new File(openfirePluginDirectory, META_INF);
metainfDir.mkdirs();
final Map filterProperties = getBuildFilterProperties();
final List<Resource> webResources = this.webResources != null ? Arrays.asList(this.webResources) : null;
if (webResources != null && webResources.size() > 0)
{
for (Resource resource : webResources)
{
if (!(new File(resource.getDirectory())).isAbsolute())
{
resource.setDirectory(project.getBasedir() + File.separator + resource.getDirectory());
}
copyResources(resource, new File(openfirePluginDirectory, "web"), filterProperties);
}
}
copyResources(warSourceDirectory, new File(openfirePluginDirectory, "web"));
copyOpenfirePluginConfiguration(openfireSourceDirectory, openfirePluginDirectory, filterProperties);
if (databaseSourceDirectory.exists())
{
copyDirectoryStructureIfModified(databaseSourceDirectory, new File(openfirePluginDirectory, "database"));
}
if (i18nSourceDirectory.exists())
{
copyDirectoryStructureIfModified(i18nSourceDirectory, new File(openfirePluginDirectory, "i18n"));
}
if (webXml != null && StringUtils.isNotEmpty(webXml.getName()))
{
if (webXml.exists())
{
//rename to web.xml
copyFileIfModified(webXml, new File(webinfDir, "/web.xml"));
}
else
{
getLog().info("The web.xml file '" + webXml + "' does not exist: creating empty web.xml");
BufferedWriter out = new BufferedWriter(new FileWriter(new File(webinfDir, "/web.xml")));
out.write("<web-app>\n</web-app>");
out.close();
//throw new MojoFailureException("The specified web.xml file '" + webXml + "' does not exist");
}
}
File libDirectory = new File(openfirePluginDirectory, "lib");
File classesDirectory = new File(openfirePluginDirectory, "classes");
if (this.classesDirectory.exists() && !this.classesDirectory.equals(classesDirectory))
{
copyDirectoryStructureIfModified(this.classesDirectory, classesDirectory);
}
Set<Artifact> artifacts = project.getArtifacts();
List<String> duplicates = findDuplicates(artifacts);
for (Artifact artifact : artifacts)
{
String targetFileName = getFinalName(artifact);
getLog().debug("Processing: " + targetFileName);
if (duplicates.contains(targetFileName))
{
getLog().debug("Duplicate found: " + targetFileName);
targetFileName = artifact.getGroupId() + "-" + targetFileName;
getLog().debug("Renamed to: " + targetFileName);
}
// TODO: utilise appropriate methods from project builder
ScopeArtifactFilter filter = new ScopeArtifactFilter(Artifact.SCOPE_RUNTIME);
if (!artifact.isOptional() && filter.include(artifact))
{
String type = artifact.getType();
if ("jar".equals(type) || "test-jar".equals(type))
{
copyFileIfModified(artifact.getFile(), new File(libDirectory, targetFileName));
}
else
{
getLog().debug("Skipping artifact of type " + type + " for WEB-INF/lib");
}
}
}
}
/**
* Searches a set of artifacts for duplicate filenames and returns a list of duplicates.
*
* @param artifacts set of artifacts
* @return List of duplicated artifacts
*/
private List<String> findDuplicates(Set<Artifact> artifacts)
{
List<String> duplicates = new ArrayList<String>();
List<String> identifiers = new ArrayList<String>();
for (Artifact artifact : artifacts)
{
String candidate = getFinalName(artifact);
if (identifiers.contains(candidate))
{
duplicates.add(candidate);
}
else
{
identifiers.add(candidate);
}
}
return duplicates;
}
/**
* Returns a list of filenames that should be copied
* over to the destination directory.
*
* @param sourceDir the directory to be scanned
* @return the array of filenames, relative to the sourceDir
*/
private String[] getWarFiles(File sourceDir)
{
DirectoryScanner scanner = new DirectoryScanner();
scanner.setBasedir(sourceDir);
scanner.setExcludes(getExcludes());
scanner.addDefaultExcludes();
scanner.setIncludes(getIncludes());
scanner.scan();
return scanner.getIncludedFiles();
}
/**
* Returns a list of filenames that should be copied
* over to the destination directory.
*
* @param resource the resource to be scanned
* @return the array of filenames, relative to the sourceDir
*/
private String[] getWarFiles(Resource resource)
{
DirectoryScanner scanner = new DirectoryScanner();
scanner.setBasedir(resource.getDirectory());
if (resource.getIncludes() != null && !resource.getIncludes().isEmpty())
{
scanner.setIncludes((String[]) resource.getIncludes().toArray(EMPTY_STRING_ARRAY));
}
else
{
scanner.setIncludes(DEFAULT_INCLUDES);
}
if (resource.getExcludes() != null && !resource.getExcludes().isEmpty())
{
scanner.setExcludes((String[]) resource.getExcludes().toArray(EMPTY_STRING_ARRAY));
}
scanner.addDefaultExcludes();
scanner.scan();
return scanner.getIncludedFiles();
}
/**
* Copy file from source to destination only if source is newer than the target file.
* If <code>destinationDirectory</code> does not exist, it
* (and any parent directories) will be created. If a file <code>source</code> in
* <code>destinationDirectory</code> exists, it will be overwritten.
*
* @param source An existing <code>File</code> to copy.
* @param destinationDirectory A directory to copy <code>source</code> into.
* @throws java.io.FileNotFoundException if <code>source</code> isn't a normal file.
* @throws IllegalArgumentException if <code>destinationDirectory</code> isn't a directory.
* @throws java.io.IOException if <code>source</code> does not exist, the file in
* <code>destinationDirectory</code> cannot be written to, or an IO error occurs during copying.
* <p/>
* TO DO: Remove this method when Maven moves to plexus-utils version 1.4
*/
private static void copyFileToDirectoryIfModified(File source, File destinationDirectory) throws IOException
{
// TO DO: Remove this method and use the method in WarFileUtils when Maven 2 changes
// to plexus-utils 1.2.
if (destinationDirectory.exists() && !destinationDirectory.isDirectory())
{
throw new IllegalArgumentException("Destination is not a directory");
}
copyFileIfModified(source, new File(destinationDirectory, source.getName()));
}
private FilterWrapper[] getFilterWrappers()
{
return new FilterWrapper[]{
// support ${token}
new FilterWrapper()
{
public Reader getReader(Reader fileReader, Map filterProperties)
{
return new InterpolationFilterReader(fileReader, filterProperties, "${", "}");
}
},
// support @token@
new FilterWrapper()
{
public Reader getReader(Reader fileReader, Map filterProperties)
{
return new InterpolationFilterReader(fileReader, filterProperties, "@", "@");
}
}};
}
/**
* @param from
* @param to
* @param encoding
* @param wrappers
* @param filterProperties
* @throws IOException TO DO: Remove this method when Maven moves to plexus-utils version 1.4
*/
private static void copyFilteredFile(File from, File to, String encoding, FilterWrapper[] wrappers,
Map filterProperties)
throws IOException
{
// buffer so it isn't reading a byte at a time!
Reader fileReader = null;
Writer fileWriter = null;
try
{
// fix for MWAR-36, ensures that the parent dir are created first
to.getParentFile().mkdirs();
if (encoding == null || encoding.length() < 1)
{
fileReader = new BufferedReader(new FileReader(from));
fileWriter = new FileWriter(to);
}
else
{
FileInputStream instream = new FileInputStream(from);
FileOutputStream outstream = new FileOutputStream(to);
fileReader = new BufferedReader(new InputStreamReader(instream, encoding));
fileWriter = new OutputStreamWriter(outstream, encoding);
}
Reader reader = fileReader;
for (FilterWrapper wrapper : wrappers)
{
reader = wrapper.getReader(reader, filterProperties);
}
IOUtil.copy(reader, fileWriter);
}
finally
{
IOUtil.close(fileReader);
IOUtil.close(fileWriter);
}
}
/**
* Copy file from source to destination only if source timestamp is later than the destination timestamp.
* The directories up to <code>destination</code> will be created if they don't already exist.
* <code>destination</code> will be overwritten if it already exists.
*
* @param source An existing non-directory <code>File</code> to copy bytes from.
* @param destination A non-directory <code>File</code> to write bytes to (possibly
* overwriting).
* @throws IOException if <code>source</code> does not exist, <code>destination</code> cannot be
* written to, or an IO error occurs during copying.
* @throws java.io.FileNotFoundException if <code>destination</code> is a directory
* <p/>
* TO DO: Remove this method when Maven moves to plexus-utils version 1.4
*/
private static void copyFileIfModified(File source, File destination)
throws IOException
{
// TO DO: Remove this method and use the method in WarFileUtils when Maven 2 changes
// to plexus-utils 1.2.
if (destination.lastModified() < source.lastModified())
{
FileUtils.copyFile(source.getCanonicalFile(), destination);
// preserve timestamp
destination.setLastModified(source.lastModified());
}
}
/**
* Copies a entire directory structure but only source files with timestamp later than the destinations'.
* <p/>
* Note:
* <ul>
* <li>It will include empty directories.
* <li>The <code>sourceDirectory</code> must exists.
* </ul>
*
* @param sourceDirectory
* @param destinationDirectory
* @throws IOException TO DO: Remove this method when Maven moves to plexus-utils version 1.4
*/
private static void copyDirectoryStructureIfModified(File sourceDirectory, File destinationDirectory)
throws IOException
{
if (!sourceDirectory.exists())
{
throw new IOException("Source directory doesn't exists (" + sourceDirectory.getAbsolutePath() + ").");
}
File[] files = sourceDirectory.listFiles();
String sourcePath = sourceDirectory.getAbsolutePath();
for (File file : files)
{
String dest = file.getAbsolutePath();
dest = dest.substring(sourcePath.length() + 1);
File destination = new File(destinationDirectory, dest);
if (file.isFile())
{
destination = destination.getParentFile();
copyFileToDirectoryIfModified(file, destination);
}
else if (file.isDirectory())
{
if (!destination.exists() && !destination.mkdirs())
{
throw new IOException(
"Could not create destination directory '" + destination.getAbsolutePath() + "'.");
}
copyDirectoryStructureIfModified(file, destination);
}
else
{
throw new IOException("Unknown file type: " + file.getAbsolutePath());
}
}
}
/**
* TO DO: Remove this interface when Maven moves to plexus-utils version 1.4
*/
private interface FilterWrapper
{
Reader getReader(Reader fileReader, Map filterProperties);
}
/**
* Returns the final name of the specified artifact.
* <p/>
* If the <tt>outputFileNameMapping</tt> is set, it is used, otherwise
* the standard naming scheme is used.
*
* @param artifact the artifact
* @return the converted filename of the artifact
*/
private String getFinalName(Artifact artifact)
{
if (outputFileNameMapping != null)
{
return MappingUtils.evaluateFileNameMapping(outputFileNameMapping, artifact);
}
String classifier = artifact.getClassifier();
if ((classifier != null) && !("".equals(classifier.trim())))
{
return MappingUtils.evaluateFileNameMapping(DEFAULT_FILE_NAME_MAPPING_CLASSIFIER, artifact);
}
else
{
return MappingUtils.evaluateFileNameMapping(DEFAULT_FILE_NAME_MAPPING, artifact);
}
}
}