/*
* Copyright 2001-2005 The Apache Software Foundation.
*
* 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.
*/
package com.javaforge.bobber.plugin.archetype;
import com.javaforge.bobber.archetype.model.Template;
import com.javaforge.bobber.archetype.model.Variable;
import com.javaforge.bobber.archetype.model.io.xpp3.BobberArchetypeXpp3Reader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.io.Writer;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;
import org.apache.maven.archetype.Archetype;
import org.apache.maven.archetype.ArchetypeDescriptorException;
import org.apache.maven.archetype.ArchetypeNotFoundException;
import org.apache.maven.archetype.ArchetypeTemplateProcessingException;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.factory.ArtifactFactory;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.artifact.resolver.ArtifactResolutionException;
import org.apache.maven.artifact.resolver.ArtifactResolver;
import org.apache.maven.settings.MavenSettingsBuilder;
import org.apache.velocity.VelocityContext;
import org.codehaus.plexus.components.interactivity.InputHandler;
import org.codehaus.plexus.logging.AbstractLogEnabled;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.IOUtil;
import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
import org.codehaus.plexus.velocity.VelocityComponent;
//source blatantly copied from org.apache.maven.archetype.DefaultArchetype. Also stole code from DefaultPluginVersionManager.java
//and maven-model for the mdo
public class BobberArchetype
extends AbstractLogEnabled
implements Archetype
{
private static final String NEW_LINE = System.getProperty("line.separator");
// ----------------------------------------------------------------------
// Components
// ----------------------------------------------------------------------
/** @component */
private VelocityComponent velocity;
/** @component */
private ArtifactResolver artifactResolver;
/** @component */
private InputHandler inputHandler;
/** @component */
private ArtifactFactory artifactFactory;
/** @component */
private MavenSettingsBuilder settingsBuilder;
private static final int MESSAGE_LINE_LENGTH = 80;
public void createArchetype(String archetypeGroupId, String archetypeArtifactId, String archetypeVersion,
ArtifactRepository localRepository, List remoteRepositories, Map parameters)
throws ArchetypeNotFoundException, ArchetypeDescriptorException, ArchetypeTemplateProcessingException
{
// ---------------------------------------------------------------------
//locate the archetype file
// ---------------------------------------------------------------------
Artifact archetypeArtifact = artifactFactory.createArtifact(archetypeGroupId, archetypeArtifactId,
archetypeVersion, Artifact.SCOPE_RUNTIME, "jar");
try
{
artifactResolver.resolve(archetypeArtifact, remoteRepositories, localRepository);
}
catch (ArtifactResolutionException e)
{
throw new ArchetypeDescriptorException("Error attempting to download archetype: " + e.getMessage(), e);
}
// ----------------------------------------------------------------------
// Load the archetype descriptor
// ----------------------------------------------------------------------
BobberArchetypeXpp3Reader builder = new BobberArchetypeXpp3Reader();
com.javaforge.bobber.archetype.model.BobberArchetype archetype;
JarFile archetypeJarFile;
try
{
archetypeJarFile = new JarFile(archetypeArtifact.getFile());
final ZipEntry zipEntry = archetypeJarFile.getEntry(ARCHETYPE_DESCRIPTOR);
InputStream is = archetypeJarFile.getInputStream(zipEntry);
if (is == null)
{
throw new ArchetypeDescriptorException(
"The " + ARCHETYPE_DESCRIPTOR + " descriptor cannot be found.");
}
archetype = builder.read(new InputStreamReader(is));
archetypeJarFile.close();
}
catch (IOException e)
{
throw new ArchetypeDescriptorException("Error reading the " + ARCHETYPE_DESCRIPTOR + " descriptor.", e);
}
catch (XmlPullParserException e)
{
throw new ArchetypeDescriptorException("Error reading the " + ARCHETYPE_DESCRIPTOR + " descriptor.", e);
}
// ----------------------------------------------------------------------
//
// ----------------------------------------------------------------------
String basedir = (String) parameters.get("basedir");
String artifactId = (String) parameters.get("artifactId");
File pomFile = new File(basedir, ARCHETYPE_POM);
File outputDirectoryFile;
if (pomFile.exists() && archetype.isAllowPartial())
{
outputDirectoryFile = new File(basedir);
}
else
{
outputDirectoryFile = new File(basedir, artifactId);
// TODO temporarily allow partial generation, remove it later
if (!archetype.isAllowPartial() &&
outputDirectoryFile.exists() &&
outputDirectoryFile.listFiles().length > 0)
{
throw new ArchetypeTemplateProcessingException(
outputDirectoryFile.getName() + " already exists - please run from a clean directory");
}
outputDirectoryFile.mkdir();
}
String outputDirectory = outputDirectoryFile.getAbsolutePath();
// ----------------------------------------------------------------------
// Set up the Velocity context
// ----------------------------------------------------------------------
VelocityContext context = new VelocityContext();
String packageName = (String) parameters.get("package");
addParamToContext("package", packageName, context);
addParamToContext("packagePath", StringUtils.replace(packageName, ".", "/"), context);
for (Iterator iterator = parameters.keySet().iterator(); iterator.hasNext();)
{
String key = (String) iterator.next();
Object value = parameters.get(key);
addParamToContext(key, value, context);
}
//add in the specified system properties
//if this were a mojo could have set the settings using the ${settings} expression. Since it is not need to get it from the settings builder
boolean inInteractiveMode = false;
try
{
inInteractiveMode = settingsBuilder.buildSettings().getInteractiveMode().booleanValue();
//TODO there must be a cleaner way of doing this
String temp = System.getProperty("interactive", null);
if (temp != null)
{
inInteractiveMode = Boolean.valueOf(temp).booleanValue();
}
getLogger().info("Interactive is: " + inInteractiveMode);
}
catch (Exception ie)
{
throw new ArchetypeTemplateProcessingException("unable to read settings ", ie);
}
if (inInteractiveMode)
{
getLogger().info("Please enter the values for the following archetype variables:");
}
final List variables = archetype.getVariables();
processVariables(variables.iterator(), context, inInteractiveMode);
// ---------------------------------------------------------------------
// Get Logger and display all parameters used
// ---------------------------------------------------------------------
if (getLogger().isInfoEnabled())
{
Object[] keys = context.getKeys();
if (keys.length > 0)
{
getLogger().info("----------------------------------------------------------------------------");
getLogger().info("Using following parameters for creating Archetype: " + archetypeArtifactId + ":" +
archetypeVersion);
getLogger().info("----------------------------------------------------------------------------");
for (int i = 0; i < keys.length; i++)
{
String parameterName = (String) keys[i];
Object parameterValue = context.get(parameterName);
getLogger().info("Parameter: " + parameterName + " = " + parameterValue);
}
}
else
{
getLogger().info("No Parameters found for creating Archetype");
}
}
// ----------------------------------------------------------------------
// Extract the archetype to the chosen directory
// ----------------------------------------------------------------------
try
{
archetypeJarFile = new JarFile(archetypeArtifact.getFile());
Enumeration entries = archetypeJarFile.entries();
while (entries.hasMoreElements())
{
JarEntry entry = (JarEntry) entries.nextElement();
String path = entry.getName();
if (!path.startsWith(ARCHETYPE_RESOURCES) || path.endsWith(".vm"))
{
continue;
}
File t = new File(outputDirectory, path.substring(19));
if (entry.isDirectory())
{
// Assume directories are stored parents first then children.
getLogger().debug("Extracting directory: " + entry.getName() + " to " + t.getAbsolutePath());
t.mkdir();
continue;
}
getLogger().debug("Extracting file: " + entry.getName() + " to " + t.getAbsolutePath());
t.createNewFile();
IOUtil.copy(archetypeJarFile.getInputStream(entry), new FileOutputStream(t));
}
archetypeJarFile.close();
//remove the archetype descriptor
File t = new File(outputDirectory, ARCHETYPE_DESCRIPTOR);
t.delete();
}
catch (IOException ioe)
{
throw new ArchetypeTemplateProcessingException("Error extracting archetype", ioe);
}
// ----------------------------------------------------------------------
// Process the templates
// ----------------------------------------------------------------------
// use the out of the box codehaus velocity component that loads templates
//from the class path
ClassLoader old = Thread.currentThread().getContextClassLoader();
try
{
URL[] urls = new URL[1];
urls[0] = archetypeArtifact.getFile().toURI().toURL();
URLClassLoader archetypeJarLoader = new URLClassLoader(urls);
Thread.currentThread().setContextClassLoader(archetypeJarLoader);
for (Iterator i = archetype.getTemplates().iterator(); i.hasNext();)
{
final Template template = (Template) i.next();
// Check the optional 'condition' property on the template.
// If present and the variable it points to is 'true', then
// continue processing. If it's false, then skip.
// If condition is not specified, assume the template should
// be processed.
boolean shouldProcess = true;
String condition = template.getDependsOnVar();
String requiredValue = null;
List options = new ArrayList();
if (StringUtils.isNotEmpty(condition))
{
//Crappy logic processing -- for now
boolean not = false;
//Allow very simple matching logic to match templates against variable values
int x = condition.indexOf("!=");
getLogger().debug("Processing Condition : " + condition);
if (x > -1)
{
not = true;
requiredValue = condition.substring(x + 2).trim();
options = getListOfValues(requiredValue);
condition = condition.substring(0, x).trim();
}
else
{
x = condition.indexOf("=");
if (x > -1)
{
requiredValue = condition.substring(x + 1);
options = getListOfValues(requiredValue);
condition = condition.substring(0, x);
}
}
getLogger().debug("Not Expr: " + not);
getLogger().debug("Condition Value: '" + condition + "'");
getLogger().debug("Required Value: '" + requiredValue + "'");
final Variable var = (Variable) findVariable(condition, variables);
if (var != null)
{
final String strValue = (String) context.get(var.getName());
getLogger().debug("Variable Value is: '" + strValue + "'");
if (requiredValue == null)
{
if (!Boolean.valueOf(strValue).booleanValue())
{
shouldProcess = false;
}
}
else
{
if (!options.contains(strValue))
{
shouldProcess = false;
}
}
}
else
{
getLogger().debug("Variable Value is: null");
shouldProcess = false;
}
if (not)
{
shouldProcess = !shouldProcess;
}
}
if (shouldProcess)
{
processTemplate(template, outputDirectory, context);
}
else
{
getLogger().debug("Condition not met, skipping " + template.getOutput());
}
}
}
catch (MalformedURLException mfe)
{
throw new ArchetypeTemplateProcessingException("Error loading archetype resources into the classpath", mfe);
}
finally
{
Thread.currentThread().setContextClassLoader(old);
}
// ----------------------------------------------------------------------
// Log message on Archetype creation
// ----------------------------------------------------------------------
if (getLogger().isInfoEnabled())
{
getLogger().info("Archetype created in dir: " + outputDirectory);
}
}
protected void addParamToContext(String key, Object value, VelocityContext context)
{
getLogger().info("Adding Parameter to template Context: " + key + "=" + value);
context.put(key, value);
}
protected void processVariables(Iterator variables, VelocityContext context, final boolean interactiveMode) throws ArchetypeTemplateProcessingException
{
while (variables.hasNext())
{
Variable var = (Variable) variables.next();
String val = System.getProperty(var.getName(), var.getDefvalue());
if (interactiveMode)
{
StringBuffer message = new StringBuffer();
message.append(var.getName()).append(": ")
.append(NEW_LINE)
.append(StringUtils.repeat("*", MESSAGE_LINE_LENGTH))
.append(NEW_LINE)
.append(NEW_LINE)
.append(StringUtils.center(var.getDescription(), MESSAGE_LINE_LENGTH))
.append(NEW_LINE)
.append(StringUtils.leftPad("[default: " + val + "]", MESSAGE_LINE_LENGTH))
.append(NEW_LINE)
.append(StringUtils.repeat("*", MESSAGE_LINE_LENGTH));
getLogger().info(message.toString());
try
{
String answer = inputHandler.readLine();
if (!StringUtils.isEmpty(answer))
{
val = answer;
}
}
catch (IOException ie)
{
throw new ArchetypeTemplateProcessingException(ie);
}
context.put(var.getName(), val);
}
else
{
context.put(var.getName(), val);
}
if (val.toLowerCase().equals("false") || val.toLowerCase().equals("n"))
{
if (var.getVariables() != null)
{
//keep processing the variables picking up the default values
processVariables(var.getVariables().iterator(), context, false);
}
}
else if (var.getVariables() != null)
{
//keep processing the variables in the current interaction mode
processVariables(var.getVariables().iterator(), context, interactiveMode);
}
}
}
protected List getListOfValues(String s)
{
List options = new ArrayList();
for (StringTokenizer stringTokenizer = new StringTokenizer(s, "|"); stringTokenizer.hasMoreTokens();)
{
options.add(stringTokenizer.nextToken());
}
return options;
}
protected void processTemplate(Template template, String outputDirectory, VelocityContext context)
throws ArchetypeTemplateProcessingException
{
File outFile;
try
{
StringWriter wout = new StringWriter();
velocity.getEngine().evaluate(context, wout, "output value", template.getOutput());
outFile = new File(outputDirectory, wout.toString());
getLogger().debug(outFile.getAbsolutePath());
FileUtils.forceMkdir(outFile.getParentFile());
getLogger().debug("Created directory: " + outFile.getParentFile() + ", Dir exists = " + outFile.getParentFile().exists());
}
catch (Exception e)
{
e.printStackTrace();
throw new ArchetypeTemplateProcessingException("error evaluating output file name " + template.getOutput(), e);
}
Writer writer = null;
try
{
getLogger().info("Processing Template: " + template.getFile());
String templateLocation = ARCHETYPE_RESOURCES + "/" + template.getFile();
writer = new FileWriter(outFile);
velocity.getEngine().mergeTemplate(templateLocation, context, writer);
writer.flush();
}
catch (Exception e)
{
throw new ArchetypeTemplateProcessingException("Error merging velocity templates", e);
}
finally
{
IOUtil.close(writer);
getLogger().info("Written Template to: " + outFile + ", file exists = " + outFile.exists());
}
// Delete archetype-originated folders in case the output path is also templated.
// Otherwise, there will be a processed folder AND the original folder.
try
{
final File templateFile = new File(outputDirectory, template.getFile());
final String templateDir = FileUtils.dirname(templateFile.getCanonicalPath());
final String outputDir = FileUtils.dirname(outFile.getCanonicalPath());
if (getLogger().isDebugEnabled())
{
getLogger().debug("TemplateDir=" + templateDir);
getLogger().debug("OutputDir=" + outputDir);
}
if (!outputDir.startsWith(templateDir))
{
getLogger().debug("Deleting Template Dir:" + templateDir);
FileUtils.forceDelete(templateDir);
}
}
catch (IOException e)
{
throw new ArchetypeTemplateProcessingException("Failed to cleanup the working dir.", e);
}
}
/**
* Find the variable.
*
* @param variableName name
* @param variables all variables of the artifact
* @return variable value or null of not found
*/
protected Object findVariable(String variableName, List variables)
{
for (int i = 0; i < variables.size(); i++)
{
Variable var = (Variable) variables.get(i);
if (variableName.equals(var.getName()))
{
return var;
}
else if (var.getVariables() != null)
{
Object o = findVariable(variableName, var.getVariables());
if (o != null)
{
return o;
}
}
}
return null;
}
}