package org.springframework.roo.project.packaging;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
import java.util.logging.Logger;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.felix.scr.annotations.Component;
import org.osgi.framework.BundleContext;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;
import org.osgi.service.component.ComponentContext;
import org.springframework.roo.model.JavaPackage;
import org.springframework.roo.process.manager.FileManager;
import org.springframework.roo.project.GAV;
import org.springframework.roo.project.Path;
import org.springframework.roo.project.PathResolver;
import org.springframework.roo.project.ProjectOperations;
import org.springframework.roo.support.logging.HandlerUtils;
import org.springframework.roo.support.util.DomUtils;
import org.springframework.roo.support.util.FileUtils;
import org.springframework.roo.support.util.XmlUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
/**
* Convenient superclass for core or third-party addons to implement a
* {@link PackagingProvider}. Uses the "Template Method" GoF pattern.
*
* @author Andrew Swan
* @author Paula Navarro
* @author Juan Carlos GarcĂa
* @since 1.2.0
*/
@Component(componentAbstract = true)
public abstract class AbstractPackagingProvider implements PackagingProvider {
// ------------ OSGi component attributes ----------------
private BundleContext context;
protected void activate(final ComponentContext cContext) {
context = cContext.getBundleContext();
}
private static final String DEFAULT_VERSION = "0.1.0.BUILD-SNAPSHOT";
private static final String JAVA_PRODUCT_VERSION_PLACEHOLDER = "JAVA_PRODUCT_VERSION";
private static final String ASPECTJ_PLUGIN_VERSION_PLACEHOLDER = "ASPECTJ_PLUGIN_VERSION";
private static final String ASCIIDOCLET_PLUGIN_VERSION_PLACEHOLDER = "ASCIIDOCLET_PLUGIN_VERSION";
protected static final Logger LOGGER = HandlerUtils.getLogger(PackagingProvider.class);
/**
* The name of the POM property that stores the packaging provider's ID.
*/
public static final String ROO_PACKAGING_PROVIDER_PROPERTY = "roo.packaging.provider";
private static final String VERSION_ELEMENT = "version";
protected FileManager fileManager;
protected PathResolver pathResolver;
private final String id;
private final String name;
private final String pomTemplate;
private final String pomModuleTemplate;
/**
* Constructor
*
* @param id the unique ID of this packaging type, see
* {@link PackagingProvider#getId()}
* @param name the name of this type of packaging as used in the POM
* (required)
* @param pomTemplate the path of this packaging type's POM template,
* relative to its own package, as per
* {@link Class#getResourceAsStream(String)}; this template
* should contain a "parent" element with its own groupId,
* artifactId, and version elements; this parent element will be
* removed if not required
*/
protected AbstractPackagingProvider(final String id, final String name, final String pomTemplate) {
this(id, name, pomTemplate, pomTemplate);
}
/**
* Constructor
*
* @param id the unique ID of this packaging type, see
* {@link PackagingProvider#getId()}
* @param name the name of this type of packaging as used in the POM
* (required)
* @param pomTemplate the path of this packaging type's POM template,
* relative to its own package, as per
* {@link Class#getResourceAsStream(String)}; this template
* should contain a "parent" element with its own groupId,
* artifactId, and version elements; this parent element will be
* removed if not required
* @param pomModuleTemplate the path of this packaging type's POM module template,
* relative to its own package, as per
* {@link Class#getResourceAsStream(String)}; this template
* should contain a "parent" element with its own groupId,
* artifactId, and version elements; can be <code>null</code>
*/
protected AbstractPackagingProvider(final String id, final String name, final String pomTemplate,
final String pomModuleTemplate) {
Validate.notBlank(id, "ID is required");
Validate.notBlank(name, "Name is required");
Validate.notBlank(pomTemplate, "POM template path is required");
this.id = id;
this.name = name;
this.pomTemplate = pomTemplate;
this.pomModuleTemplate = pomModuleTemplate;
}
public String createArtifacts(final JavaPackage topLevelPackage,
final String nullableProjectName, final String javaVersion, final GAV parentPom,
final String module, final ProjectOperations projectOperations) {
final String pomPath =
createPom(topLevelPackage, nullableProjectName, javaVersion, parentPom, module,
projectOperations);
// ROO-3687: Log4J is not necessary to install on this new version
// of Spring Roo.
// createOtherArtifacts(topLevelPackage, module, projectOperations);
return pomPath;
}
/**
* Subclasses can override this method to create any other required files or
* directories (apart from the POM, which has previously been generated by
* {@link #createPom}).
* <p>
* This implementation sets up the Log4j configuration file for the root
* module.
*
* @param topLevelPackage
* @param module the unqualified name of the module being created (empty
* means the root or only module)
* @param projectOperations can't be injected as it would create a circular
* dependency
*/
protected void createOtherArtifacts(final JavaPackage topLevelPackage, final String module,
final ProjectOperations projectOperations) {
if (StringUtils.isBlank(module)) {
setUpLog4jConfiguration();
}
}
/**
* Creates the Maven POM using the subclass' POM template as follows:
* <ul>
* <li>sets the parent POM to the given parent (if any)</li>
* <li>sets the groupId to the result of {@link #getGroupId}, omitting this
* element if it's the same as the parent's groupId (as per Maven best
* practice)</li>
* <li>sets the artifactId to the result of {@link #getArtifactId}</li>
* <li>sets the packaging to the result of {@link #getName()}</li>
* <li>sets the project name to the result of {@link #getProjectName}</li>
* <li>replaces all occurrences of {@link #JAVA_PRODUCT_VERSION_PLACEHOLDER} with
* the given Java version</li>
* </ul>
* This method makes as few assumptions about the POM template as possible,
* to make life easier for anyone writing a {@link PackagingProvider}.
*
* @param topLevelPackage the new project or module's top-level Java package
* (required)
* @param projectName the project name provided by the user (can be blank)
* @param javaVersion the Java version to substitute into the POM (required)
* @param parentPom the Maven coordinates of the parent POM (can be
* <code>null</code>)
* @param module the unqualified name of the Maven module to which the new
* POM belongs
* @param projectOperations cannot be injected otherwise it's a circular
* dependency
* @return the path of the newly created POM
*/
protected String createPom(final JavaPackage topLevelPackage, final String projectName,
final String javaVersion, final GAV parentPom, final String module,
final ProjectOperations projectOperations) {
final Document pom;
final String groupId;
final boolean isModule =
StringUtils.isNotBlank(module) && StringUtils.isNotBlank(pomModuleTemplate);
Validate.isTrue(isModule || StringUtils.isNotBlank(javaVersion), "Java version required");
Validate.notNull(topLevelPackage, "Top level package required");
// Read the POM template from the classpath
if (!isModule) {
pom = XmlUtils.readXml(FileUtils.getInputStream(getClass(), pomTemplate));
groupId = getGroupId(topLevelPackage);
} else {
pom = XmlUtils.readXml(FileUtils.getInputStream(getClass(), pomModuleTemplate));
groupId = parentPom.getGroupId();
}
final Element root = pom.getDocumentElement();
// name
final String mavenName = getProjectName(projectName, module, topLevelPackage);
if (StringUtils.isNotBlank(mavenName)) {
// If the user wants this element in the traditional place, ensure
// the template already contains it
DomUtils.createChildIfNotExists("name", root, pom).setTextContent(mavenName.trim());
} else {
DomUtils.removeElements("name", root);
}
// groupId and parent
setGroupIdAndParent(groupId, parentPom, root, pom);
// artifactId
final String artifactId = getArtifactId(projectName, module, topLevelPackage);
Validate.notBlank(artifactId, "Maven artifactIds cannot be blank");
DomUtils.createChildIfNotExists("artifactId", root, pom).setTextContent(artifactId.trim());
if (!isModule) {
// version
final Element existingVersionElement =
DomUtils.getChildElementByTagName(root, VERSION_ELEMENT);
if (existingVersionElement == null) {
DomUtils.createChildElement(VERSION_ELEMENT, root, pom).setTextContent(DEFAULT_VERSION);
}
// Java product version (8, 7 ,6)
final List<Element> javaProductVersionElements =
XmlUtils.findElements("//*[.='" + JAVA_PRODUCT_VERSION_PLACEHOLDER + "']", root);
for (final Element versionElement : javaProductVersionElements) {
versionElement.setTextContent(javaVersion);
}
// AspectJ Plugin Versions
final List<Element> aspectJPluginVersionElements =
XmlUtils.findElements("//*[.='" + ASPECTJ_PLUGIN_VERSION_PLACEHOLDER + "']", root);
for (final Element aspectJPluginVersion : aspectJPluginVersionElements) {
aspectJPluginVersion.setTextContent("1.8");
// if ("1.8".equals(javaVersion)) {
// aspectJPluginVersion.setTextContent("1.8");
// } else if ("1.7".equals(javaVersion)) {
// aspectJPluginVersion.setTextContent("1.8");
// } else if ("1.6".equals(javaVersion)) {
// aspectJPluginVersion.setTextContent("1.8");
// }
}
// Asciidoclet Plugin versions
final List<Element> asciidocletPluginVersionElements =
XmlUtils.findElements("//*[.='" + ASCIIDOCLET_PLUGIN_VERSION_PLACEHOLDER + "']", root);
for (final Element versionElement : asciidocletPluginVersionElements) {
versionElement.setTextContent("1.5.4");
}
}
// packaging
DomUtils.createChildIfNotExists("packaging", root, pom).setTextContent(name);
setPackagingProviderId(pom);
// Write the new POM to disk
final String pomPath =
getPathResolver().getIdentifier(Path.ROOT.getModulePathId(module), "pom.xml");
getFileManager().createOrUpdateTextFileIfRequired(pomPath, XmlUtils.nodeToString(pom), true);
return pomPath;
}
/**
* Returns the text to be inserted into the POM's
* <code><artifactId></code> element. This implementation simply
* delegates to {@link #getProjectName}. Subclasses can override this method
* to use a different strategy.
*
* @param nullableProjectName the project name entered by the user (can be
* blank)
* @param module the name of the module being created (blank for the root
* module)
* @param topLevelPackage the project or module's top level Java package
* (required)
* @return a non-blank artifactId
*/
protected String getArtifactId(final String nullableProjectName, final String module,
final JavaPackage topLevelPackage) {
if (nullableProjectName == null) {
String packageName = StringUtils.replace(module, "-", ".");
return StringUtils.defaultIfEmpty(packageName, topLevelPackage.getLastElement());
} else {
return nullableProjectName.toLowerCase().replaceAll("\\s+", "");
}
}
/**
* Returns the fully-qualified name of the given module, relative to the
* currently focused module.
*
* @param moduleName can be blank for the root or only module
* @param projectOperations
* @return
*/
protected final String getFullyQualifiedModuleName(final String moduleName,
final ProjectOperations projectOperations) {
if (StringUtils.isBlank(moduleName)) {
return "";
}
final String focusedModuleName = projectOperations.getFocusedModuleName();
if (StringUtils.isBlank(focusedModuleName)) {
return moduleName;
}
return focusedModuleName + File.separator + moduleName;
}
/**
* Returns the groupId of the project or module being created. This
* implementation simply uses the fully-qualified name of the given Java
* package. Subclasses can override this method to use a different strategy.
*
* @param topLevelPackage the new project or module's top-level Java package
* (required)
* @return
*/
protected String getGroupId(final JavaPackage topLevelPackage) {
return topLevelPackage.getFullyQualifiedPackageName();
}
public final String getId() {
return id;
}
/**
* Returns the package-relative path to this {@link PackagingProvider}'s POM
* template.
*
* @return a non-blank path
*/
String getPomTemplate() {
return pomTemplate;
}
/**
* Returns the package-relative path to this {@link PackagingProvider}'s POM
* module template.
*
* @return a non-blank path
*/
String getPomModuleTemplate() {
return pomModuleTemplate;
}
/**
* Returns the text to be inserted into the POM's <code><name></code>
* element. This implementation uses the given project name if not blank,
* otherwise the last element of the given Java package. Subclasses can
* override this method to use a different strategy.
*
* @param nullableProjectName the project name entered by the user (can be
* blank)
* @param module the name of the module being created (blank for the root
* module)
* @param topLevelPackage the project or module's top level Java package
* (required)
* @return a blank name if none is required
*/
protected String getProjectName(final String nullableProjectName, final String module,
final JavaPackage topLevelPackage) {
String packageName =
StringUtils.defaultIfEmpty(nullableProjectName, StringUtils.replace(module, "-", "."));
return StringUtils.defaultIfEmpty(packageName, topLevelPackage.getLastElement());
}
/**
* Sets the Maven groupIds of the parent and/or project as necessary
*
* @param projectGroupId the project's groupId (required)
* @param parentPom the Maven coordinates of the parent POM (can be
* <code>null</code>)
* @param root the root element of the POM document (required)
* @param pom the POM document (required)
*/
protected void setGroupIdAndParent(final String projectGroupId, final GAV parentPom,
final Element root, final Document pom) {
final Element parentPomElement = DomUtils.createChildIfNotExists("parent", root, pom);
final Element projectGroupIdElement = DomUtils.createChildIfNotExists("groupId", root, pom);
// ROO-3687: By default, Spring IO Platform will be the parent pom.
// If developer specify new parent pom, update parent with the new one.
if (parentPom != null) {
// Parent's groupId, artifactId, and version
DomUtils.createChildIfNotExists("groupId", parentPomElement, pom).setTextContent(
parentPom.getGroupId());
DomUtils.createChildIfNotExists("artifactId", parentPomElement, pom).setTextContent(
parentPom.getArtifactId());
DomUtils.createChildIfNotExists(VERSION_ELEMENT, parentPomElement, pom).setTextContent(
parentPom.getVersion());
// Project groupId (if necessary)
if (projectGroupId.equals(parentPom.getGroupId())) {
// Maven best practice is to inherit the groupId from the parent
root.removeChild(projectGroupIdElement);
DomUtils.removeTextNodes(root);
} else {
// Project has its own groupId => needs to be explicit
projectGroupIdElement.setTextContent(projectGroupId);
}
} else {
// Project has its own groupId => needs to be explicit
projectGroupIdElement.setTextContent(projectGroupId);
}
}
/**
* Stores the ID of this {@link PackagingProvider} as a POM property called
* {@value #ROO_PACKAGING_PROVIDER_PROPERTY}. Subclasses can override this
* method, but be aware that Roo needs some way of working out from a given
* <code>pom.xml</code> file which {@link PackagingProvider} should be used.
*
* @param pom the DOM document for the POM being created
*/
protected void setPackagingProviderId(final Document pom) {
final Node propertiesElement =
DomUtils.createChildIfNotExists("properties", pom.getDocumentElement(), pom);
DomUtils.createChildIfNotExists(ROO_PACKAGING_PROVIDER_PROPERTY, propertiesElement, pom)
.setTextContent(getId());
}
private void setUpLog4jConfiguration() {
final String log4jConfigFile =
getPathResolver().getFocusedIdentifier(Path.SRC_MAIN_RESOURCES, "log4j.properties");
final InputStream templateInputStream =
FileUtils.getInputStream(getClass(), "log4j.properties-template");
OutputStream outputStream = null;
try {
outputStream = getFileManager().createFile(log4jConfigFile).getOutputStream();
IOUtils.copy(templateInputStream, outputStream);
} catch (final IOException e) {
LOGGER.warning("Unable to install log4j logging configuration");
} finally {
IOUtils.closeQuietly(templateInputStream);
IOUtils.closeQuietly(outputStream);
}
}
public FileManager getFileManager() {
if (fileManager == null) {
// Get all Services implement FileManager interface
try {
ServiceReference<?>[] references =
context.getAllServiceReferences(FileManager.class.getName(), null);
for (ServiceReference<?> ref : references) {
return (FileManager) context.getService(ref);
}
return null;
} catch (InvalidSyntaxException e) {
LOGGER.warning("Cannot load FileManager on AbstractPackagingProvider.");
return null;
}
} else {
return fileManager;
}
}
public PathResolver getPathResolver() {
if (pathResolver == null) {
// Get all Services implement PathResolver interface
try {
ServiceReference<?>[] references =
context.getAllServiceReferences(PathResolver.class.getName(), null);
for (ServiceReference<?> ref : references) {
pathResolver = (PathResolver) context.getService(ref);
return pathResolver;
}
return null;
} catch (InvalidSyntaxException e) {
LOGGER.warning("Cannot load PathResolver on AbstractPackagingProvider.");
return null;
}
} else {
return pathResolver;
}
}
}