blob: 6cd5342ccbd47f5cbf66108a6675f32235195dbb [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2005, 2006 Richard Hoefter and others.
* 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:
* Richard Hoefter (richard.hoefter@web.de) - initial API and implementation
* IBM Corporation - incorporating into Eclipse
*******************************************************************************/
package org.eclipse.ant.internal.ui.datatransfer;
import java.io.File;
import com.ibm.icu.text.MessageFormat;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import org.eclipse.core.resources.IFile;
import org.eclipse.jdt.core.IClassFile;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.ToolFactory;
import org.eclipse.jdt.core.util.IClassFileReader;
import org.eclipse.jdt.core.util.IConstantPool;
import org.eclipse.jdt.core.util.IConstantPoolConstant;
import org.eclipse.jdt.core.util.IConstantPoolEntry;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.swt.widgets.Shell;
/**
* Provides a method to analyze sources if it is possible to export projects to
* an Ant buildfile which compiles correctly.
*/
public class SourceAnalyzer {
/**
* Utility class.
*/
private SourceAnalyzer() {
}
/**
* Check if source directories of project has cycles or if there are
* dependencies between them that are not conform with classpath order.
*
* <p>
* NOTE: Unused references in classes are not considered if they cause
* cycles or classpath order problems. This is because this class analyzes
* the bytecode and indeed the compiler throws unused references away.
*/
public static void checkCycles(IJavaProject currentProject,
EclipseClasspath classpath, Shell shell) {
StringBuffer message = new StringBuffer();
Map src2dir = new TreeMap(); // map string to string
Map srcdir2classes = new TreeMap(); // map string to Set of strings
determineSources(currentProject, classpath, src2dir, srcdir2classes);
Map srcdir2sourcedirs = determineRequiredSrcDirs(src2dir,
srcdir2classes);
String projectName = currentProject.getProject().getName();
List cycle = new ArrayList();
if (isCyclic(srcdir2sourcedirs, cycle)) {
showCycleWarning(projectName, shell, cycle, message);
return;
}
checkBuildOrder(classpath, projectName, shell, srcdir2sourcedirs);
}
/**
* Determine all sources belonging to a source directory.
*/
private static void determineSources(IJavaProject currentProject,
EclipseClasspath classpath, Map src2dir, Map srcdir2classes) {
for (int i = 0; i < classpath.srcDirs.size(); i++) {
String srcDir = (String) classpath.srcDirs.get(i);
String classDir = (String) classpath.classDirs.get(i);
if (EclipseClasspath.isReference(srcDir)) {
continue;
}
File dir;
if (srcDir.equals(".")) { //$NON-NLS-1$
dir = currentProject.getResource().getLocation().toFile();
} else {
IFile file = currentProject.getProject().getFile(srcDir);
dir = file.getLocation().toFile();
}
if (EclipseClasspath.isLinkedResource(srcDir)) {
String link = classpath.resolveLinkedResource(srcDir);
dir = new File(link);
}
Set sources = findFiles(dir, ".java"); //$NON-NLS-1$
// find all required classfiles for each source directory
for (Iterator iter = sources.iterator(); iter.hasNext();) {
String srcFile = (String) iter.next();
src2dir.put(srcFile, srcDir);
IFile classFile = currentProject.getProject().getFile(
classDir + '/' + srcFile + ".class"); //$NON-NLS-1$
if (!classFile.exists()) {
// project was not compiled, check not possible
continue;
}
Set classes = (Set) srcdir2classes.get(srcDir);
if (classes == null) {
classes = new TreeSet();
}
classes.addAll(getRequiredClasses(classFile));
srcdir2classes.put(srcDir, classes);
}
}
}
/**
* Determine for each source directory which other source directories it
* requires.
*
* @return Map string to Set of strings. (Maps source dir to Set of required
* source dirs.)
*/
private static Map determineRequiredSrcDirs(Map src2dir, Map srcdir2classes) {
Map srcdir2sourcedirs = new TreeMap(); // map string to Set of strings
for (Iterator iter = srcdir2classes.keySet().iterator(); iter.hasNext();) {
String srcDir = (String) iter.next();
Set classes = (Set) srcdir2classes.get(srcDir);
for (Iterator iterator = classes.iterator(); iterator.hasNext();) {
String classname = (String) iterator.next();
String classsrc = (String) src2dir.get(classname);
// don't add reference to itself
if (classsrc != null && !classsrc.equals(srcDir)) {
Set sourcedirs = (Set) srcdir2sourcedirs.get(srcDir);
if (sourcedirs == null) {
sourcedirs = new TreeSet();
}
sourcedirs.add(classsrc);
srcdir2sourcedirs.put(srcDir, sourcedirs);
}
}
}
return srcdir2sourcedirs;
}
private static void showCycleWarning(String projectName, Shell shell,
List cycle, StringBuffer message) {
String m = MessageFormat.format(DataTransferMessages.SourceAnalyzer_0,
new String[] { projectName });
message.append(m);
message.append(ExportUtil.NEWLINE);
// print cycle path
for (Iterator iter = cycle.iterator(); iter.hasNext();) {
String s = (String) iter.next();
s = EclipseClasspath.getLinkedResourceName(s);
message.append(s);
message.append(" -> "); //$NON-NLS-1$
}
message.append(EclipseClasspath.getLinkedResourceName((String) cycle
.get(0)));
MessageDialog.openWarning(shell, DataTransferMessages.SourceAnalyzer_1,
message.toString());
}
/**
* Check if build order is correct.
*/
private static void checkBuildOrder(EclipseClasspath classpath,
String projectName, Shell shell, Map srcdir2sourcedirs) {
for (Iterator iter = srcdir2sourcedirs.keySet().iterator(); iter
.hasNext();) {
String srcdir = (String) iter.next();
Set sourcedirs = (Set) srcdir2sourcedirs.get(srcdir);
int classpathIndex = classpath.srcDirs.indexOf(srcdir);
for (Iterator iterator = sourcedirs.iterator(); iterator.hasNext();) {
String requiredSrc = (String) iterator.next();
int i = classpath.srcDirs.indexOf(requiredSrc);
if (i > classpathIndex) // wrong order
{
String s = MessageFormat.format(
DataTransferMessages.SourceAnalyzer_3,
new String[] { projectName });
MessageDialog.openWarning(shell,
DataTransferMessages.SourceAnalyzer_2, s
+ ExportUtil.NEWLINE + requiredSrc
+ " <-> " + srcdir //$NON-NLS-1$
+ ExportUtil.NEWLINE);
break;
}
}
}
}
/**
* Find all classes that are required by given class file.
*
* @param file
* a ".class" file
* @return set of strings, each contains a full qualified classname (forward
* slash as package separator)
*/
public static Set getRequiredClasses(IFile file) {
Set classes = new TreeSet();
IClassFile classFile = JavaCore.createClassFileFrom(file);
IClassFileReader reader = ToolFactory.createDefaultClassFileReader(
classFile, IClassFileReader.CONSTANT_POOL);
if (reader == null) {
// class not compiled
return classes;
}
IConstantPool pool = reader.getConstantPool();
for (int i = 0; i < pool.getConstantPoolCount(); i++) {
if (pool.getEntryKind(i) == IConstantPoolConstant.CONSTANT_Class) {
IConstantPoolEntry entry = pool.decodeEntry(i);
String classname = new String(entry.getClassInfoName());
// don't return inner classes
int index = classname.indexOf('$');
if (index != -1) {
classname = classname.substring(0, index);
}
classes.add(classname);
}
}
return classes;
}
/**
* Find all files with particular extension under given directory.
*
* @param dir
* directory to start search
* @param extension
* extension to search
* @return filenames relative to dir (without extension and with forward
* slashes)
*/
public static Set findFiles(File dir, String extension) {
Set visited = new TreeSet();
findFiles(dir, dir, extension, visited);
return visited;
}
private static void findFiles(File base, File dir, String extension,
Set visited) {
if (dir.isDirectory()) {
File[] children = dir.listFiles();
for (int i = 0; i < children.length; i++) {
findFiles(base, children[i], extension, visited);
}
} else if (dir.getAbsolutePath().endsWith(extension)) {
// remove base directory
String filename = ExportUtil.removePrefixAndSuffix(dir
.getAbsolutePath(),
base.getAbsolutePath() + File.separator, extension);
visited.add(filename.replace('\\', '/'));
}
}
/**
* Check if given graph that is described through a map is cyclic.
*
* @param srcdir2sourcedirs
* Maps string to set of strings. The keys are the graph nodes
* which are mapped to its neighbours.
* @param cycle
* filled with name of nodes which cause cycle
*/
private static boolean isCyclic(Map srcdir2sourcedirs, List cycle) {
return !isAcyclic(srcdir2sourcedirs, cycle);
}
private static boolean isAcyclic(Map srcdir2sourcedirs, List cycle) {
// standard graph theory
List visited = new ArrayList();
List exited = new ArrayList();
for (Iterator iter = srcdir2sourcedirs.keySet().iterator(); iter
.hasNext();) {
String srcdir = (String) iter.next();
if (!visited.contains(srcdir)) {
if (circleSearch(srcdir, srcdir2sourcedirs, visited, exited,
cycle)) {
return false;
}
}
}
return true;
}
private static boolean circleSearch(String srcdir, Map srcdir2sourcedirs,
List visited, List exited, List cycle) {
boolean res = false;
visited.add(srcdir);
cycle.add(srcdir);
Set sourcedirs = (Set) srcdir2sourcedirs.get(srcdir); // neighbours
if (sourcedirs != null) {
for (Iterator iter = sourcedirs.iterator(); iter.hasNext();) {
String src = (String) iter.next();
if (!visited.contains(src)) {
res = circleSearch(src, srcdir2sourcedirs, visited, exited,
cycle);
} else if (!exited.contains(src)) {
res = true;
}
if (res) {
break;
}
}
}
if (!res) {
cycle.clear();
}
exited.add(srcdir);
return res;
}
}