/*
* Copyright (c) 2002-2013 Alibaba Group Holding Limited.
* 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
*
* 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.alibaba.citrus.maven.eclipse.base.eclipse;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import aQute.lib.osgi.Analyzer;
import com.alibaba.citrus.maven.eclipse.base.eclipse.osgiplugin.EclipseOsgiPlugin;
import com.alibaba.citrus.maven.eclipse.base.eclipse.osgiplugin.ExplodedPlugin;
import com.alibaba.citrus.maven.eclipse.base.eclipse.osgiplugin.PackagedPlugin;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.deployer.ArtifactDeployer;
import org.apache.maven.artifact.deployer.ArtifactDeploymentException;
import org.apache.maven.artifact.factory.ArtifactFactory;
import org.apache.maven.artifact.installer.ArtifactInstallationException;
import org.apache.maven.artifact.installer.ArtifactInstaller;
import org.apache.maven.artifact.metadata.ArtifactMetadata;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.artifact.repository.ArtifactRepositoryFactory;
import org.apache.maven.artifact.repository.layout.ArtifactRepositoryLayout;
import org.apache.maven.model.Dependency;
import org.apache.maven.model.License;
import org.apache.maven.model.Model;
import org.apache.maven.model.io.xpp3.MavenXpp3Writer;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.project.artifact.ProjectArtifactMetadata;
import org.codehaus.plexus.PlexusConstants;
import org.codehaus.plexus.PlexusContainer;
import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
import org.codehaus.plexus.components.interactivity.InputHandler;
import org.codehaus.plexus.context.Context;
import org.codehaus.plexus.context.ContextException;
import org.codehaus.plexus.personality.plexus.lifecycle.phase.Contextualizable;
import org.codehaus.plexus.util.IOUtil;
import org.codehaus.plexus.util.StringUtils;
/**
* Add eclipse artifacts from an eclipse installation to the local repo. This mojo automatically analize the eclipse
* directory, copy plugins jars to the local maven repo, and generates appropriate poms. This is the official central
* repository builder for Eclipse plugins, so it has the necessary default values. For customized repositories see
* {@link MakeArtifactsMojo} Typical usage:
* <code>mvn eclipse:to-maven -DdeployTo=maven.org::default::scpexe://repo1.maven.org/home/maven/repository-staging/to-ibiblio/eclipse-staging -DeclipseDir=.</code>
*
* @author Fabrizio Giustina
* @author <a href="mailto:carlos@apache.org">Carlos Sanchez</a>
* @version $Id: EclipseToMavenMojo.java 1154368 2011-08-05 20:13:42Z rfscholte $
* @goal to-maven
* @requiresProject false
*/
public class EclipseToMavenMojo
extends AbstractMojo
implements Contextualizable {
/** A pattern the <code>deployTo</code> param should match. */
private static final Pattern DEPLOYTO_PATTERN = Pattern.compile("(.+)::(.+)::(.+)"); //$NON-NLS-1$
/** A pattern for a 4 digit osgi version number. */
private static final Pattern VERSION_PATTERN = Pattern.compile("(([0-9]+\\.)+[0-9]+)"); //$NON-NLS-1$
/** Plexus container, needed to manually lookup components for deploy of artifacts. */
private PlexusContainer container;
/**
* Local maven repository.
*
* @parameter expression="${localRepository}"
* @required
* @readonly
*/
private ArtifactRepository localRepository;
/**
* ArtifactRepositoryFactory component.
*
* @component
*/
private ArtifactRepositoryFactory artifactRepositoryFactory;
/**
* ArtifactFactory component.
*
* @component
*/
private ArtifactFactory artifactFactory;
/**
* ArtifactInstaller component.
*
* @component
*/
protected ArtifactInstaller installer;
/**
* ArtifactDeployer component.
*
* @component
*/
private ArtifactDeployer deployer;
/**
* Eclipse installation dir. If not set, a value for this parameter will be asked on the command line.
*
* @parameter expression="${eclipseDir}"
*/
private File eclipseDir;
/**
* Input handler, needed for comand line handling.
*
* @component
*/
protected InputHandler inputHandler;
/**
* Strip qualifier (fourth token) from the plugin version. Qualifiers are for eclipse plugin the equivalent of
* timestamped snapshot versions for Maven, but the date is maintained also for released version (e.g. a jar for the
* release <code>3.2</code> can be named <code>org.eclipse.core.filesystem_1.0.0.v20060603.jar</code>. It's usually
* handy to not to include this qualifier when generating maven artifacts for major releases, while it's needed when
* working with eclipse integration/nightly builds.
*
* @parameter expression="${stripQualifier}" default-value="false"
*/
private boolean stripQualifier;
/**
* Specifies a remote repository to which generated artifacts should be deployed to. If this property is specified,
* artifacts are also deployed to the remote repo. The format for this parameter is <code>id::layout::url</code>
*
* @parameter expression="${deployTo}"
*/
private String deployTo;
/** @see org.apache.maven.plugin.Mojo#execute() */
public void execute()
throws MojoExecutionException, MojoFailureException {
if (eclipseDir == null) {
getLog().info(Messages.getString("EclipseToMavenMojo.eclipseDirectoryPrompt")); //$NON-NLS-1$
String eclipseDirString;
try {
eclipseDirString = inputHandler.readLine();
} catch (IOException e) {
throw new MojoFailureException(Messages.getString("EclipseToMavenMojo.errorreadingfromstandardinput")); //$NON-NLS-1$
}
eclipseDir = new File(eclipseDirString);
}
if (!eclipseDir.isDirectory()) {
throw new MojoFailureException(
Messages.getString(
"EclipseToMavenMojo.directoydoesnotexist", eclipseDir.getAbsolutePath())); //$NON-NLS-1$
}
File pluginDir = new File(eclipseDir, "plugins"); //$NON-NLS-1$
if (!pluginDir.isDirectory()) {
throw new MojoFailureException(
Messages.getString(
"EclipseToMavenMojo.plugindirectorydoesnotexist",
pluginDir.getAbsolutePath())); //$NON-NLS-1$
}
File[] files = pluginDir.listFiles();
ArtifactRepository remoteRepo = resolveRemoteRepo();
if (remoteRepo != null) {
getLog().info(Messages.getString("EclipseToMavenMojo.remoterepositorydeployto", deployTo)); //$NON-NLS-1$
}
Map plugins = new HashMap();
Map models = new HashMap();
for (int j = 0; j < files.length; j++) {
File file = files[j];
getLog().info(Messages.getString("EclipseToMavenMojo.processingfile",
file.getAbsolutePath())); //$NON-NLS-1$
processFile(file, plugins, models);
}
int i = 1;
for (Iterator it = plugins.keySet().iterator(); it.hasNext(); ) {
getLog().info(
Messages.getString(
"EclipseToMavenMojo.processingplugin",
new Object[] { new Integer(i++), new Integer(plugins.keySet().size()) })); //$NON-NLS-1$
String key = (String) it.next();
EclipseOsgiPlugin plugin = (EclipseOsgiPlugin) plugins.get(key);
Model model = (Model) models.get(key);
writeArtifact(plugin, model, remoteRepo);
}
}
protected void processFile(File file, Map plugins, Map models)
throws MojoExecutionException, MojoFailureException {
EclipseOsgiPlugin plugin = getEclipsePlugin(file);
if (plugin == null) {
getLog().warn(Messages.getString("EclipseToMavenMojo.skippingfile", file.getAbsolutePath())); //$NON-NLS-1$
return;
}
Model model = createModel(plugin);
if (model == null) {
return;
}
processPlugin(plugin, model, plugins, models);
}
protected void processPlugin(EclipseOsgiPlugin plugin, Model model, Map plugins, Map models)
throws MojoExecutionException, MojoFailureException {
plugins.put(getKey(model), plugin);
models.put(getKey(model), model);
}
protected String getKey(Model model) {
return model.getGroupId() + "." + model.getArtifactId(); //$NON-NLS-1$
}
private String getKey(Dependency dependency) {
return dependency.getGroupId() + "." + dependency.getArtifactId(); //$NON-NLS-1$
}
/**
* Resolve version ranges in the model provided, overriding version ranges with versions from the dependency in the
* provided map of models. TODO doesn't check if the version is in range, it just overwrites it
*
* @param model
* @param models
* @throws MojoFailureException
*/
protected void resolveVersionRanges(Model model, Map models)
throws MojoFailureException {
for (Iterator it = model.getDependencies().iterator(); it.hasNext(); ) {
Dependency dep = (Dependency) it.next();
if (dep.getVersion().indexOf("[") > -1 || dep.getVersion().indexOf("(") > -1) //$NON-NLS-1$ //$NON-NLS-2$
{
String key = getKey(model);
Model dependencyModel = (Model) models.get(getKey(dep));
if (dependencyModel != null) {
dep.setVersion(dependencyModel.getVersion());
} else {
throw new MojoFailureException(
Messages.getString(
"EclipseToMavenMojo.unabletoresolveversionrange", new Object[] { dep //$NON-NLS-1$
, key })); //$NON-NLS-1$
}
}
}
}
/**
* Get a {@link EclipseOsgiPlugin} object from a plugin jar/dir found in the target dir.
*
* @param file plugin jar or dir
* @throws MojoExecutionException if anything bad happens while parsing files
*/
private EclipseOsgiPlugin getEclipsePlugin(File file)
throws MojoExecutionException {
if (file.isDirectory()) {
return new ExplodedPlugin(file);
} else if (file.getName().endsWith(".jar")) //$NON-NLS-1$
{
try {
return new PackagedPlugin(file);
} catch (IOException e) {
throw new MojoExecutionException(
Messages.getString(
"EclipseToMavenMojo.unabletoaccessjar", file.getAbsolutePath()), e); //$NON-NLS-1$
}
}
return null;
}
/**
* Create the {@link Model} from a plugin manifest
*
* @param plugin Eclipse plugin jar or dir
* @throws MojoExecutionException if anything bad happens while parsing files
*/
private Model createModel(EclipseOsgiPlugin plugin)
throws MojoExecutionException {
String name, bundleName, version, groupId, artifactId, requireBundle;
try {
if (!plugin.hasManifest()) {
getLog().warn(Messages.getString("EclipseToMavenMojo.plugindoesnothavemanifest", plugin)); //$NON-NLS-1$
return null;
}
Analyzer analyzer = new Analyzer();
Map bundleSymbolicNameHeader =
analyzer.parseHeader(plugin.getManifestAttribute(Analyzer.BUNDLE_SYMBOLICNAME));
bundleName = (String) bundleSymbolicNameHeader.keySet().iterator().next();
version = plugin.getManifestAttribute(Analyzer.BUNDLE_VERSION);
if (bundleName == null || version == null) {
getLog().error(Messages.getString("EclipseToMavenMojo.unabletoreadbundlefrommanifest")); //$NON-NLS-1$
return null;
}
version = osgiVersionToMavenVersion(version);
name = plugin.getManifestAttribute(Analyzer.BUNDLE_NAME);
requireBundle = plugin.getManifestAttribute(Analyzer.REQUIRE_BUNDLE);
} catch (IOException e) {
throw new MojoExecutionException(
Messages.getString("EclipseToMavenMojo.errorprocessingplugin", plugin), e); //$NON-NLS-1$
}
Dependency[] deps = parseDependencies(requireBundle);
groupId = createGroupId(bundleName);
artifactId = createArtifactId(bundleName);
Model model = new Model();
model.setModelVersion("4.0.0"); //$NON-NLS-1$
model.setGroupId(groupId);
model.setArtifactId(artifactId);
model.setName(name);
model.setVersion(version);
model.setProperties(plugin.getPomProperties());
if (groupId.startsWith("org.eclipse")) //$NON-NLS-1$
{
// why do we need a parent?
// Parent parent = new Parent();
// parent.setGroupId( "org.eclipse" );
// parent.setArtifactId( "eclipse" );
// parent.setVersion( "1" );
// model.setParent( parent );
// infer license for know projects, everything at eclipse is licensed under EPL
// maybe too simplicistic, but better than nothing
License license = new License();
license.setName("Eclipse Public License - v 1.0"); //$NON-NLS-1$
license.setUrl("http://www.eclipse.org/org/documents/epl-v10.html"); //$NON-NLS-1$
model.addLicense(license);
}
if (deps.length > 0) {
for (int k = 0; k < deps.length; k++) {
model.getDependencies().add(deps[k]);
}
}
return model;
}
/**
* Writes the artifact to the repo
*
* @param model
* @param remoteRepo remote repository (if set)
* @throws MojoExecutionException
*/
private void writeArtifact(EclipseOsgiPlugin plugin, Model model, ArtifactRepository remoteRepo)
throws MojoExecutionException {
Writer fw = null;
ArtifactMetadata metadata = null;
File pomFile = null;
Artifact pomArtifact =
artifactFactory.createArtifact(model.getGroupId(),
model.getArtifactId(),
model.getVersion(),
null,
"pom"); //$NON-NLS-1$
Artifact artifact =
artifactFactory.createArtifact(model.getGroupId(), model.getArtifactId(), model.getVersion(), null,
Constants.PROJECT_PACKAGING_JAR);
try {
pomFile = File.createTempFile("pom-", ".xml"); //$NON-NLS-1$ //$NON-NLS-2$
// TODO use WriterFactory.newXmlWriter() when plexus-utils is upgraded to 1.4.5+
fw = new OutputStreamWriter(new FileOutputStream(pomFile), "UTF-8"); //$NON-NLS-1$
model.setModelEncoding("UTF-8"); // to be removed when encoding is detected instead of forced to UTF-8 //$NON-NLS-1$
pomFile.deleteOnExit();
new MavenXpp3Writer().write(fw, model);
metadata = new ProjectArtifactMetadata(pomArtifact, pomFile);
pomArtifact.addMetadata(metadata);
} catch (IOException e) {
throw new MojoExecutionException(
Messages.getString(
"EclipseToMavenMojo.errorwritingtemporarypom", e.getMessage()), e); //$NON-NLS-1$
} finally {
IOUtil.close(fw);
}
try {
File jarFile = plugin.getJarFile();
if (remoteRepo != null) {
deployer.deploy(pomFile, pomArtifact, remoteRepo, localRepository);
deployer.deploy(jarFile, artifact, remoteRepo, localRepository);
} else {
installer.install(pomFile, pomArtifact, localRepository);
installer.install(jarFile, artifact, localRepository);
}
} catch (ArtifactDeploymentException e) {
throw new MojoExecutionException(
Messages.getString("EclipseToMavenMojo.errordeployartifacttorepository"), e); //$NON-NLS-1$
} catch (ArtifactInstallationException e) {
throw new MojoExecutionException(
Messages.getString("EclipseToMavenMojo.errorinstallartifacttorepository"), e); //$NON-NLS-1$
} catch (IOException e) {
throw new MojoExecutionException(
Messages.getString(
"EclipseToMavenMojo.errorgettingjarfileforplugin", plugin), e); //$NON-NLS-1$
} finally {
pomFile.delete();
}
}
protected String osgiVersionToMavenVersion(String version) {
return osgiVersionToMavenVersion(version, null, stripQualifier);
}
/**
* The 4th (build) token MUST be separed with "-" and not with "." in maven. A version with 4 dots is not parsed,
* and the whole string is considered a qualifier. See tests in DefaultArtifactVersion for reference.
*
* @param version initial version
* @param forcedQualifier build number
* @param stripQualifier always remove 4th token in version
* @return converted version
*/
protected String osgiVersionToMavenVersion(String version, String forcedQualifier, boolean stripQualifier) {
if (stripQualifier && StringUtils.countMatches(version, ".") > 2) //$NON-NLS-1$
{
version = StringUtils.substring(version, 0, version.lastIndexOf('.')); //$NON-NLS-1$
} else if (StringUtils.countMatches(version, ".") > 2) //$NON-NLS-1$
{
int lastDot = version.lastIndexOf('.'); //$NON-NLS-1$
if (StringUtils.isNotEmpty(forcedQualifier)) {
version = StringUtils.substring(version, 0, lastDot) + "-" + forcedQualifier; //$NON-NLS-1$
} else {
version = StringUtils.substring(version, 0, lastDot) + "-" //$NON-NLS-1$
+ StringUtils.substring(version, lastDot + 1, version.length());
}
}
return version;
}
/**
* Resolves the deploy<code>deployTo</code> parameter to an <code>ArtifactRepository</code> instance (if set).
*
* @return ArtifactRepository instance of null if <code>deployTo</code> is not set.
* @throws MojoFailureException
* @throws MojoExecutionException
*/
private ArtifactRepository resolveRemoteRepo()
throws MojoFailureException, MojoExecutionException {
if (deployTo != null) {
Matcher matcher = DEPLOYTO_PATTERN.matcher(deployTo);
if (!matcher.matches()) {
throw new MojoFailureException(deployTo,
Messages.getString("EclipseToMavenMojo.invalidsyntaxforrepository"),
//$NON-NLS-1$
Messages.getString("EclipseToMavenMojo.invalidremoterepositorysyntax")); //$NON-NLS-1$
} else {
String id = matcher.group(1).trim();
String layout = matcher.group(2).trim();
String url = matcher.group(3).trim();
ArtifactRepositoryLayout repoLayout;
try {
repoLayout = (ArtifactRepositoryLayout) container.lookup(ArtifactRepositoryLayout.ROLE, layout);
} catch (ComponentLookupException e) {
throw new MojoExecutionException(
Messages.getString(
"EclipseToMavenMojo.cannotfindrepositorylayout",
layout), e); //$NON-NLS-1$
}
return artifactRepositoryFactory.createDeploymentArtifactRepository(id, url, repoLayout, true);
}
}
return null;
}
/** {@inheritDoc} */
public void contextualize(Context context)
throws ContextException {
this.container = (PlexusContainer) context.get(PlexusConstants.PLEXUS_KEY);
}
/**
* Get the group id as the tokens until last dot e.g. <code>org.eclipse.jdt</code> -> <code>org.eclipse</code>
*
* @param bundleName bundle name
* @return group id
*/
protected String createGroupId(String bundleName) {
int i = bundleName.lastIndexOf('.'); //$NON-NLS-1$
if (i > 0) {
return bundleName.substring(0, i);
} else {
return bundleName;
}
}
/**
* Get the artifact id as the tokens after last dot e.g. <code>org.eclipse.jdt</code> -> <code>jdt</code>
*
* @param bundleName bundle name
* @return artifact id
*/
protected String createArtifactId(String bundleName) {
int i = bundleName.lastIndexOf('.'); //$NON-NLS-1$
if (i > 0) {
return bundleName.substring(i + 1);
} else {
return bundleName;
}
}
/**
* Parses the "Require-Bundle" and convert it to a list of dependencies.
*
* @param requireBundle "Require-Bundle" entry
* @return an array of <code>Dependency</code>
*/
protected Dependency[] parseDependencies(String requireBundle) {
if (requireBundle == null) {
return new Dependency[0];
}
List dependencies = new ArrayList();
Analyzer analyzer = new Analyzer();
Map requireBundleHeader = analyzer.parseHeader(requireBundle);
// now iterates on bundles and extract dependencies
for (Iterator iter = requireBundleHeader.entrySet().iterator(); iter.hasNext(); ) {
Map.Entry entry = (Map.Entry) iter.next();
String bundleName = (String) entry.getKey();
Map attributes = (Map) entry.getValue();
String version = (String) attributes.get(Analyzer.BUNDLE_VERSION.toLowerCase());
boolean optional = "optional".equals(attributes.get("resolution:")); //$NON-NLS-1$ //$NON-NLS-2$
if (version == null) {
getLog().info(Messages.getString("EclipseToMavenMojo.missingversionforbundle",
bundleName)); //$NON-NLS-1$
version = "[0,)"; //$NON-NLS-1$
}
version = fixBuildNumberSeparator(version);
Dependency dep = new Dependency();
dep.setGroupId(createGroupId(bundleName));
dep.setArtifactId(createArtifactId(bundleName));
dep.setVersion(version);
dep.setOptional(optional);
dependencies.add(dep);
}
return (Dependency[]) dependencies.toArray(new Dependency[dependencies.size()]);
}
/**
* Fix the separator for the 4th token in a versions. In maven this must be "-", in OSGI it's "."
*
* @param versionRange input range
* @return modified version range
*/
protected String fixBuildNumberSeparator(String versionRange) {
// should not be called with a null versionRange, but a check doesn't hurt...
if (versionRange == null) {
return null;
}
StringBuffer newVersionRange = new StringBuffer();
Matcher matcher = VERSION_PATTERN.matcher(versionRange);
while (matcher.find()) {
String group = matcher.group();
if (StringUtils.countMatches(group, ".") > 2) //$NON-NLS-1$
{
// build number found, fix it
int lastDot = group.lastIndexOf('.'); //$NON-NLS-1$
group = StringUtils.substring(group, 0, lastDot) + "-" //$NON-NLS-1$
+ StringUtils.substring(group, lastDot + 1, group.length());
}
matcher.appendReplacement(newVersionRange, group);
}
matcher.appendTail(newVersionRange);
return newVersionRange.toString();
}
}