/*******************************************************************************
* Copyright (c) 2015 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.eclipse.boot.dash.cloudfoundry;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.debug.core.ILaunchConfigurationType;
import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
import org.eclipse.debug.core.ILaunchDelegate;
import org.eclipse.debug.core.ILaunchManager;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants;
import org.eclipse.jdt.launching.IRuntimeClasspathEntry;
import org.eclipse.jdt.launching.JavaLaunchDelegate;
import org.eclipse.jdt.launching.JavaRuntime;
import org.springframework.ide.eclipse.boot.core.BootPropertyTester;
import org.springframework.ide.eclipse.boot.dash.BootDashActivator;
import org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate;
import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil;
/**
* Resolves all the package fragment roots and main type for a give Java
* project. It includes handling all required Java projects for the given java
* project.
*
*/
public class JavaPackageFragmentRootHandler {
private IJavaProject javaProject;
private IType type;
public JavaPackageFragmentRootHandler(IJavaProject javaProject) {
this.javaProject = javaProject;
}
public IPackageFragmentRoot[] getPackageFragmentRoots(IProgressMonitor monitor) throws Exception {
IPath[] classpathEntries = null;
ILaunchConfiguration configuration = null;
try {
if (BootPropertyTester.isBootProject(javaProject.getProject())) {
configuration = BootLaunchConfigurationDelegate.createConf(javaProject);
Set<String> modes = new HashSet<String>();
modes.add(ILaunchManager.RUN_MODE);
ILaunchDelegate[] delegates = configuration.getType().getDelegates(modes);
if (delegates != null && delegates.length > 0) {
JavaLaunchDelegate javaDelegate = null;
for (ILaunchDelegate del : delegates) {
if (BootLaunchConfigurationDelegate.TYPE_ID.equals(del.getId())
&& del.getDelegate() instanceof JavaLaunchDelegate) {
javaDelegate = (JavaLaunchDelegate) del.getDelegate();
break;
}
}
if (javaDelegate != null) {
String[] pathValues = javaDelegate.getClasspath(configuration);
if (pathValues != null) {
classpathEntries = new IPath[pathValues.length];
for (int i = 0; i < pathValues.length && i < classpathEntries.length; i++) {
classpathEntries[i] = new Path(pathValues[i]);
}
}
}
}
} else {
throw ExceptionUtil.coreException("The project : " + javaProject.getProject().getName()
+ " does not appear to be a Spring Boot project. Only Spring Boot projects can be deployed through the Boot Dashboard.");
}
} finally {
if (configuration != null) {
configuration.delete();
}
}
if (classpathEntries != null && classpathEntries.length > 0) {
List<IPackageFragmentRoot> pckRoots = new ArrayList<IPackageFragmentRoot>();
// Since the java project may have other required projects, fetch
// the
// ordered
// list of required projects that will be used to search for package
// fragment roots
// corresponding to the resolved class paths. The order of the java
// projects to search in should
// start with the most immediate list of required projects of the
// java
// project.
List<IJavaProject> javaProjectsToSearch = getOrderedJavaProjects(javaProject);
// Find package fragment roots corresponding to the path entries.
// Search through all java projects, not just the immediate java
// project
// for the application that is being pushed to CF.
for (IPath path : classpathEntries) {
for (IJavaProject javaProject : javaProjectsToSearch) {
IPackageFragmentRoot[] roots = javaProject.getPackageFragmentRoots();
if (roots != null) {
List<IPackageFragmentRoot> foundRoots = new ArrayList<IPackageFragmentRoot>();
for (IPackageFragmentRoot packageFragmentRoot : roots) {
// Note that a class path entry may correspond to a
// Java project's target directory.
// Different fragment roots may use the same target
// directory, so relationship between fragment roots
// to a class path entry may be many-to-one.
if (isRootAtEntry(packageFragmentRoot, path)) {
foundRoots.add(packageFragmentRoot);
}
}
// Stop after the first successful search
if (!foundRoots.isEmpty()) {
pckRoots.addAll(foundRoots);
break;
}
}
}
}
return pckRoots.toArray(new IPackageFragmentRoot[pckRoots.size()]);
} else {
return null;
}
}
/**
* Attempts to resolve a main type in the associated Java project. Throws
* {@link CoreException} if it failed to resolve main type or no main type
* found.
*
* @param monitor
* @return non-null Main type.
* @throws CoreException
* if failure occurred while resolving main type or no main type
* found
*/
public IType getMainType(IProgressMonitor monitor) throws Exception {
if (type == null) {
type = new JavaTypeResolver(javaProject).getMainTypesFromSource(monitor);
if (type == null) {
throw ExceptionUtil.coreException(
"No main type found. Verify that the project can be packaged as a jar application and contains a main type in source. War packaging of projects is not yet supported.");
}
}
return type;
}
protected List<IJavaProject> getOrderedJavaProjects(IJavaProject project) {
List<String> collectedProjects = new ArrayList<String>();
getOrderedJavaProjectNames(Arrays.asList(project.getProject().getName()), collectedProjects);
List<IJavaProject> projects = new ArrayList<IJavaProject>();
for (String name : collectedProjects) {
IProject prj = ResourcesPlugin.getWorkspace().getRoot().getProject(name);
if (prj != null) {
IJavaProject jvPrj = JavaCore.create(prj);
if (jvPrj != null && jvPrj.exists()) {
projects.add(jvPrj);
}
}
}
return projects;
}
protected void getOrderedJavaProjectNames(List<String> sameLevelRequiredProjects, List<String> collectedProjects) {
// The order in which required projects are collected is as follows,
// with the RHS
// being required projects of the LHS
// A -> BC
// B -> D
// C -> E
// = total 5 projects, added in the order that they are encountered.
// so final ordered list should be ABCDE
if (sameLevelRequiredProjects == null) {
return;
}
List<String> nextLevelRequiredProjects = new ArrayList<String>();
// First add the current level java projects in the order they appear
// and also collect each one's required names.
for (String name : sameLevelRequiredProjects) {
try {
IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(name);
if (project != null) {
IJavaProject jvPrj = JavaCore.create(project);
if (jvPrj != null && jvPrj.exists()) {
if (!collectedProjects.contains(name)) {
collectedProjects.add(name);
}
String[] names = jvPrj.getRequiredProjectNames();
if (names != null && names.length > 0) {
for (String reqName : names) {
if (!nextLevelRequiredProjects.contains(reqName)) {
nextLevelRequiredProjects.add(reqName);
}
}
}
}
}
} catch (JavaModelException e) {
BootDashActivator.log(e);
}
}
// Now recurse to fetch the required projects for the
// list of java projects that were added at the current level above
if (!nextLevelRequiredProjects.isEmpty()) {
getOrderedJavaProjectNames(nextLevelRequiredProjects, collectedProjects);
}
}
protected ILaunchConfiguration createConfiguration(IType type) throws Exception {
ILaunchManager manager = DebugPlugin.getDefault().getLaunchManager();
ILaunchConfigurationType configType = manager
.getLaunchConfigurationType(IJavaLaunchConfigurationConstants.ID_JAVA_APPLICATION);
ILaunchConfigurationWorkingCopy workingCopy = configType.newInstance(null,
manager.generateLaunchConfigurationName(type.getTypeQualifiedName('.')));
workingCopy.setAttribute(IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME, type.getFullyQualifiedName());
workingCopy.setAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME,
type.getJavaProject().getElementName());
workingCopy.setMappedResources(new IResource[] { type.getUnderlyingResource() });
return workingCopy;
}
protected IPath[] getRuntimeClasspaths(ILaunchConfiguration configuration) throws Exception {
IRuntimeClasspathEntry[] entries = JavaRuntime.computeUnresolvedRuntimeClasspath(configuration);
entries = JavaRuntime.resolveRuntimeClasspath(entries, configuration);
ArrayList<IPath> userEntries = new ArrayList<IPath>(entries.length);
for (int i = 0; i < entries.length; i++) {
if (entries[i].getClasspathProperty() == IRuntimeClasspathEntry.USER_CLASSES) {
String location = entries[i].getLocation();
if (location != null) {
IPath entry = Path.fromOSString(location);
if (!userEntries.contains(entry)) {
userEntries.add(entry);
}
}
}
}
return userEntries.toArray(new IPath[userEntries.size()]);
}
/**
*
* Determines if the given package fragment root corresponds to the class
* path entry path.
* <p/>
* Note that different package fragment roots may point to the same class
* path entry.
* <p/>
* Example:
* <p/>
* A Java project may have the following package fragment roots:
* <p/>
* - src/main/java
* <p/>
* - src/main/resources
* <p/>
* Both may be using the same output folder:
* <p/>
* target/classes.
* <p/>
* In this case, the output folder will have a class path entry -
* target/classes - and it will be the same for both roots, and this method
* will return true for both roots if passed the entry for target/classes
*
* @param root
* to check if it corresponds to the given class path entry path
* @param entry
* @return true if root is at the given entry
*/
private static boolean isRootAtEntry(IPackageFragmentRoot root, IPath entry) {
try {
IClasspathEntry cpe = root.getRawClasspathEntry();
if (cpe.getEntryKind() == IClasspathEntry.CPE_SOURCE) {
IPath outputLocation = cpe.getOutputLocation();
if (outputLocation == null) {
outputLocation = root.getJavaProject().getOutputLocation();
}
IPath location = ResourcesPlugin.getWorkspace().getRoot().findMember(outputLocation).getLocation();
if (entry.equals(location)) {
return true;
}
}
} catch (JavaModelException e) {
BootDashActivator.log(e);
}
IResource resource = root.getResource();
if (resource != null && entry.equals(resource.getLocation())) {
return true;
}
IPath path = root.getPath();
if (path != null && entry.equals(path)) {
return true;
}
return false;
}
}