blob: 7d50626d341f84f41204c30a2bbdb92806343652 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2012 Red Hat, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*******************************************************************************/
package org.eclipse.m2e.jdt.internal;
import static org.apache.maven.shared.utils.StringUtils.isEmpty;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceVisitor;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.codehaus.plexus.util.xml.Xpp3Dom;
import org.apache.maven.artifact.versioning.ComparableVersion;
import org.apache.maven.model.Build;
import org.apache.maven.model.Model;
import org.apache.maven.model.Plugin;
import org.apache.maven.model.Resource;
import org.eclipse.m2e.core.MavenPlugin;
import org.eclipse.m2e.core.internal.index.IIndex;
import org.eclipse.m2e.core.internal.index.IndexedArtifact;
import org.eclipse.m2e.core.internal.index.IndexedArtifactFile;
import org.eclipse.m2e.core.internal.index.SearchExpression;
import org.eclipse.m2e.core.internal.index.SourcedSearchExpression;
import org.eclipse.m2e.core.project.conversion.AbstractProjectConversionParticipant;
/**
* Converts existing Eclipse Java projects by setting the maven compiler source and target values. It also tries to best
* match existing Java source directories with the corresponding source, test source, resource and test resource
* directories of the Maven model.
*
* @author Fred Bricon
*/
public class JavaProjectConversionParticipant extends AbstractProjectConversionParticipant {
private static final Logger log = LoggerFactory.getLogger(JavaProjectConversionParticipant.class);
private static final String DEFAULT_JAVA_SOURCE = "src/main/java"; //$NON-NLS-1$
private static final String DEFAULT_RESOURCES = "src/main/resources"; //$NON-NLS-1$
private static final String DEFAULT_JAVA_TEST_SOURCE = "src/test/java"; //$NON-NLS-1$
private static final String DEFAULT_TEST_RESOURCES = "src/test/resources"; //$NON-NLS-1$
private static final String DEFAULT_JAVA_VERSION = "1.5"; //$NON-NLS-1$
private static final String COMPILER_GROUP_ID = "org.apache.maven.plugins"; //$NON-NLS-1$
private static final String COMPILER_ARTIFACT_ID = "maven-compiler-plugin"; //$NON-NLS-1$
private static final String DEFAULT_COMPILER_VERSION = "3.8.0"; //$NON-NLS-1$
private static final String TARGET_KEY = "target"; //$NON-NLS-1$
private static final String SOURCE_KEY = "source"; //$NON-NLS-1$
private static final String RELEASE_KEY = "release"; //$NON-NLS-1$
private static final String COMPILER_ARGS_KEY = "compilerArgs"; //$NON-NLS-1$
private static final String CONFIGURATION_KEY = "configuration"; //$NON-NLS-1$
private static final float VERSION_9 = 9.0f;
public boolean accept(IProject project) throws CoreException {
boolean accepts = project != null && project.isAccessible() && project.hasNature(JavaCore.NATURE_ID);
return accepts;
}
public void convert(IProject project, Model model, IProgressMonitor monitor) throws CoreException {
if(!accept(project)) {
return;
}
IJavaProject javaProject = JavaCore.create(project);
if(javaProject == null) {
return;
}
log.debug("Applying Java conversion to " + project.getName()); //$NON-NLS-1$
configureBuildSourceDirectories(model, javaProject);
//Read existing Eclipse compiler settings
Map<String, String> options = javaProject.getOptions(true);
String source = options.get(JavaCore.COMPILER_SOURCE);
String target = options.get(JavaCore.COMPILER_CODEGEN_TARGET_PLATFORM);
boolean emptySource = isEmpty(source);
boolean emptyTarget = isEmpty(target);
if(emptySource && emptyTarget) {
return;
}
if(emptySource) {
source = target;
} else if(emptyTarget) {
target = source;
}
//We want to keep pom.xml configuration to a minimum so we rely on convention. If the java version == 1.5,
//we shouldn't need to add anything as recent maven-compiler-plugin versions target Java 1.5 by default
if(DEFAULT_JAVA_VERSION.equals(source) && DEFAULT_JAVA_VERSION.equals(target)) {
return;
}
//Configure Java version
boolean useProperties = false;//TODO Use preferences
if(useProperties) {
configureProperties(model, source, target);
} else {
configureCompilerPlugin(model, source, target, JavaSettingsUtils.hasPreviewFeatures(javaProject));
}
}
private void configureProperties(Model model, String source, String target) {
Properties properties = model.getProperties();
if(properties == null) {
properties = new Properties();
model.setProperties(properties);
}
if(canUseReleaseProperty(source, target)) {
properties.setProperty("maven.compiler.release", source); //$NON-NLS-1$
} else {
properties.setProperty("maven.compiler.source", source); //$NON-NLS-1$
properties.setProperty("maven.compiler.target", target); //$NON-NLS-1$
}
}
private void configureCompilerPlugin(Model model, String source, String target, boolean usesPreviewFeatures) {
Build build = getOrCreateBuild(model);
model.setBuild(build);
Plugin compiler = getOrCreateCompilerPlugin(build);
Xpp3Dom configuration = (Xpp3Dom) compiler.getConfiguration();
if(configuration == null) {
configuration = new Xpp3Dom(CONFIGURATION_KEY);
compiler.setConfiguration(configuration);
}
if(canUseReleaseProperty(source, target)) {
Xpp3Dom releaseDom = configuration.getChild(RELEASE_KEY);
if(releaseDom == null) {
releaseDom = new Xpp3Dom(RELEASE_KEY);
configuration.addChild(releaseDom);
}
releaseDom.setValue(source);
} else {
Xpp3Dom sourceDom = configuration.getChild(SOURCE_KEY);
if(sourceDom == null) {
sourceDom = new Xpp3Dom(SOURCE_KEY);
configuration.addChild(sourceDom);
}
sourceDom.setValue(source);
Xpp3Dom targetDom = configuration.getChild(TARGET_KEY);
if(targetDom == null) {
targetDom = new Xpp3Dom(TARGET_KEY);
configuration.addChild(targetDom);
}
targetDom.setValue(target);
}
if(usesPreviewFeatures) {
Xpp3Dom compilerArgsDom = configuration.getChild(COMPILER_ARGS_KEY);
if(compilerArgsDom == null) {
compilerArgsDom = new Xpp3Dom(COMPILER_ARGS_KEY);
configuration.addChild(compilerArgsDom);
}
String argKey = "arg";
boolean addFlag = true;
if(compilerArgsDom.getChildCount() > 0) {
argKey = compilerArgsDom.getChild(0).getName();
addFlag = !Arrays.stream(compilerArgsDom.getChildren())
.filter(c -> JavaSettingsUtils.ENABLE_PREVIEW_JVM_FLAG.equals(c.getValue())).findAny().isPresent();
}
if(addFlag) {
Xpp3Dom previewFlagDom = new Xpp3Dom(argKey);
previewFlagDom.setValue(JavaSettingsUtils.ENABLE_PREVIEW_JVM_FLAG);
compilerArgsDom.addChild(previewFlagDom);
}
}
compiler.setConfiguration(configuration);
}
private boolean canUseReleaseProperty(String source, String target) {
//source and target are guaranteed to be not null at this point
return source.equals(target) && asFloat(source) >= VERSION_9;
}
private float asFloat(String source) {
try {
return Float.parseFloat(source);
} catch(Exception ignored) {
}
return 0f;
}
private Plugin getOrCreateCompilerPlugin(Build build) {
build.flushPluginMap();//We need to force the re-generation of the plugin map as it may be stale
Plugin compiler = build.getPluginsAsMap().get(COMPILER_GROUP_ID + ":" + COMPILER_ARTIFACT_ID); //$NON-NLS-1$
if(compiler == null) {
compiler = build.getPluginsAsMap().get(COMPILER_ARTIFACT_ID);
}
if(compiler == null) {
compiler = new Plugin();
compiler.setGroupId(COMPILER_GROUP_ID);
compiler.setArtifactId(COMPILER_ARTIFACT_ID);
compiler.setVersion(getCompilerVersion());
build.addPlugin(compiler);
}
return compiler;
}
private void configureBuildSourceDirectories(Model model, IJavaProject javaProject) throws CoreException {
IClasspathEntry[] entries = javaProject.getRawClasspath();
Set<String> sources = new LinkedHashSet<String>();
Set<String> potentialTestSources = new LinkedHashSet<String>();
Set<String> potentialResourceDirectories = new LinkedHashSet<String>();
Set<String> potentialTestResourceDirectories = new LinkedHashSet<String>();
IPath projectPath = javaProject.getPath();
for(int i = 0; i < entries.length; i++ ) {
if(entries[i].getEntryKind() == IClasspathEntry.CPE_SOURCE) {
IPath path = entries[i].getPath().makeRelativeTo(projectPath);
if(path.isAbsolute()) {
//We only support paths relative to the project root, so we skip this one
continue;
}
String portablePath = path.toPortableString();
boolean isPotentialTestSource = isPotentialTestSource(path);
boolean isResource = false;
if(isPotentialTestSource) {
if(DEFAULT_TEST_RESOURCES.equals(portablePath)) {
isResource = potentialTestResourceDirectories.add(portablePath);
} else {
potentialTestSources.add(portablePath);
}
} else {
if(DEFAULT_RESOURCES.equals(portablePath)) {
isResource = potentialResourceDirectories.add(portablePath);
} else {
sources.add(portablePath);
}
}
if(!isResource) {
//For source folders not already flagged as resource folder, check if
// they contain non-java sources, so we can add them as resources too
boolean hasNonJavaResources = false;
IFolder folder = javaProject.getProject().getFolder(path);
if(folder.isAccessible()) {
NonJavaResourceVisitor nonJavaResourceVisitor = new NonJavaResourceVisitor();
try {
folder.accept(nonJavaResourceVisitor);
} catch(NonJavaResourceFoundException ex) {
//Expected
hasNonJavaResources = true;
} catch(CoreException ex) {
//385666 ResourceException is thrown in Helios
if(ex.getCause() instanceof NonJavaResourceFoundException) {
hasNonJavaResources = true;
} else {
log.error("An error occured while analysing {} : {}", folder, ex.getMessage());//$NON-NLS-1$
}
}
}
if(hasNonJavaResources) {
if(isPotentialTestSource) {
potentialTestResourceDirectories.add(portablePath);
} else {
potentialResourceDirectories.add(portablePath);
}
}
}
}
}
Build build = getOrCreateBuild(model);
if(!sources.isEmpty()) {
if(sources.size() > 1) {
//We don't know how to handle multiple sources, i.e. how to map to a resource or test source directory
//That should be dealt by setting the build-helper-plugin config (http://mojo.codehaus.org/build-helper-maven-plugin/usage.html)
log.warn("{} has multiple source entries, this is not supported yet", model.getArtifactId()); //$NON-NLS-1$
}
String sourceDirectory = sources.iterator().next();
if(!DEFAULT_JAVA_SOURCE.equals(sourceDirectory)) {
build.setSourceDirectory(sourceDirectory);
}
for(String resourceDirectory : potentialResourceDirectories) {
if(!DEFAULT_RESOURCES.equals(resourceDirectory) || potentialResourceDirectories.size() > 1) {
build.addResource(createResource(resourceDirectory));
}
}
}
if(!potentialTestSources.isEmpty()) {
if(potentialTestSources.size() > 1) {
log.warn("{} has multiple test source entries, this is not supported yet", model.getArtifactId()); //$NON-NLS-1$
}
String testSourceDirectory = potentialTestSources.iterator().next();
if(!DEFAULT_JAVA_TEST_SOURCE.equals(testSourceDirectory)) {
build.setTestSourceDirectory(testSourceDirectory);
}
for(String resourceDirectory : potentialTestResourceDirectories) {
if(!DEFAULT_TEST_RESOURCES.equals(resourceDirectory) || potentialTestResourceDirectories.size() > 1) {
build.addTestResource(createResource(resourceDirectory));
}
}
}
//Ensure we don't attach a new empty build definition to the model
if(build.getSourceDirectory() != null || build.getTestSourceDirectory() != null || !build.getResources().isEmpty()
|| !build.getTestResources().isEmpty()) {
model.setBuild(build);
}
}
private Resource createResource(String resourceDirectory) {
Resource r = new Resource();
r.setDirectory(resourceDirectory);
r.addExclude("**/*.java"); //$NON-NLS-1$
return r;
}
/**
* Checks if a given path has one of its segment ending with test or tests
*/
private boolean isPotentialTestSource(IPath path) {
for(String segment : path.segments()) {
String folderName = segment.toLowerCase();
if(folderName.matches(".*tests?")) { //$NON-NLS-1$
return true;
}
}
return false;
//TODO Maybe check if the folder has java files with a Test or TestSuite suffix?
}
private Build getOrCreateBuild(Model model) {
Build build = model.getBuild();
if(build == null) {
build = new Build();
}
return build;
}
/**
* Visitor implementation looking for non-Java resources. as soon as such resource is found, a
* {@link NonJavaResourceFoundException} is thrown.
*/
private static class NonJavaResourceVisitor implements IResourceVisitor {
//TODO either declare a complete list of extensions or switch to
// a different "ignore resource" strategy
private static final List<String> IGNORED_EXTENSIONS = Arrays.asList(".svn"); //$NON-NLS-1$
public NonJavaResourceVisitor() {
}
@SuppressWarnings("unused")
public boolean visit(IResource resource) throws CoreException {
String resourceName = resource.getProjectRelativePath().lastSegment();
if(resource.isHidden() || isIgnored(resourceName)) {
return false;
}
if(resource instanceof IFile) {
IFile file = (IFile) resource;
if(!"java".equals(file.getFileExtension())) {
throw new NonJavaResourceFoundException();
}
}
return true;
}
private boolean isIgnored(String resourceName) {
for(String extension : IGNORED_EXTENSIONS) {
if(resourceName.endsWith(extension)) {
return true;
}
}
return false;
}
}
private static class NonJavaResourceFoundException extends RuntimeException {
private static final long serialVersionUID = 1L;
public NonJavaResourceFoundException() {
}
@Override
public Throwable fillInStackTrace() {
//Overriding fillInStackTrace() reduces the stacktrace creation overhead,
//unneeded since this exception is used for flow control.
return this;
}
}
private String getCompilerVersion() {
//For test purposes only, must not be considered API behavior.
String version = System.getProperty("org.eclipse.m2e.jdt.conversion.compiler.version");//$NON-NLS-1$
if(version != null) {
return version;
}
return getMostRecentPluginVersion(COMPILER_GROUP_ID, COMPILER_ARTIFACT_ID, DEFAULT_COMPILER_VERSION);
}
/**
* Returns the highest, non-snapshot plugin version between the given reference version and the versions found in the
* Nexus indexes.
*/
@SuppressWarnings("restriction")
//TODO extract as API when stabilized?
private String getMostRecentPluginVersion(String groupId, String artifactId, String referenceVersion) {
Assert.isNotNull(groupId, "groupId can not be null");
Assert.isNotNull(artifactId, "artifactId can not be null");
String version = referenceVersion;
String partialKey = artifactId + " : " + groupId; //$NON-NLS-1$
try {
IIndex index = MavenPlugin.getIndexManager().getAllIndexes();
SearchExpression a = new SourcedSearchExpression(artifactId);
//For some reason, an exact search using :
//ISearchEngine searchEngine = M2EUIPluginActivator.getDefault().getSearchEngine(null)
//searchEngine.findVersions(groupId, artifactId, searchExpression, packaging)
//
//doesn't yield the expected results (the latest versions are not returned), so we rely on a fuzzier search
//and refine the results.
Map<String, IndexedArtifact> values = index.search(a, IIndex.SEARCH_PLUGIN);
if(!values.isEmpty()) {
SortedSet<ComparableVersion> versions = new TreeSet<ComparableVersion>();
ComparableVersion referenceComparableVersion = referenceVersion == null ? null
: new ComparableVersion(referenceVersion);
for(Map.Entry<String, IndexedArtifact> e : values.entrySet()) {
if(!(e.getKey().endsWith(partialKey))) {
continue;
}
for(IndexedArtifactFile f : e.getValue().getFiles()) {
if(groupId.equals(f.group) && artifactId.equals(f.artifact) && !f.version.contains("SNAPSHOT")) {
ComparableVersion v = new ComparableVersion(f.version);
if(referenceComparableVersion == null || v.compareTo(referenceComparableVersion) > 0) {
versions.add(v);
}
}
}
if(!versions.isEmpty()) {
List<String> sorted = new ArrayList<String>(versions.size());
for(ComparableVersion v : versions) {
sorted.add(v.toString());
}
Collections.reverse(sorted);
version = sorted.iterator().next();
}
}
}
} catch(CoreException e) {
log.error("Can not retrieve latest version of " + partialKey, e);
}
return version;
}
}