/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common
* Development and Distribution License("CDDL") (collectively, the
* "License"). You may not use this file except in compliance with the
* License. You can obtain a copy of the License at
* http://www.netbeans.org/cddl-gplv2.html
* or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
* specific language governing permissions and limitations under the
* License. When distributing the software, include this License Header
* Notice in each file and include the License file at
* nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
* particular file as subject to the "Classpath" exception as provided
* by Sun in the GPL Version 2 section of the License file that
* accompanied this code. If applicable, add the following below the
* License Header, with the fields enclosed by brackets [] replaced by
* your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
*
* Contributor(s):
*
* The Original Software is NetBeans. The Initial Developer of the Original
* Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
* Microsystems, Inc. All Rights Reserved.
*
* If you wish your version of this file to be governed by only the CDDL
* or only the GPL Version 2, indicate your decision by adding
* "[Contributor] elects to include this software in this distribution
* under the [CDDL or GPL Version 2] license." If you do not indicate a
* single choice of license, a recipient has the option to distribute
* your version of this file under either the CDDL, the GPL Version 2 or
* to extend the choice of license to its licensees as provided above.
* However, if you add GPL Version 2 code and therefore, elected the GPL
* Version 2 license, then the option applies only if the new code is
* made subject to such option by the copyright holder.
*/
package org.netbeans.modules.scala.debugger.projects;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.HashSet;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import org.netbeans.api.java.platform.JavaPlatformManager;
import org.netbeans.spi.debugger.jpda.SourcePathProvider;
import org.netbeans.spi.debugger.ContextProvider;
import org.netbeans.api.java.classpath.ClassPath;
import org.netbeans.api.java.classpath.GlobalPathRegistry;
import org.netbeans.api.java.classpath.GlobalPathRegistryEvent;
import org.netbeans.api.java.classpath.GlobalPathRegistryListener;
import org.netbeans.api.java.platform.JavaPlatform;
import org.netbeans.api.java.project.JavaProjectConstants;
import org.netbeans.api.java.queries.SourceForBinaryQuery;
import org.netbeans.api.project.Project;
import org.netbeans.api.project.ProjectUtils;
import org.netbeans.api.project.SourceGroup;
import org.netbeans.api.project.Sources;
import org.netbeans.api.project.ui.OpenProjects;
import org.netbeans.spi.java.classpath.ClassPathProvider;
import org.netbeans.spi.java.classpath.support.ClassPathSupport;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileStateInvalidException;
import org.openide.filesystems.FileUtil;
import org.openide.filesystems.JarFileSystem;
import org.openide.filesystems.URLMapper;
import org.openide.util.Exceptions;
import org.openide.util.WeakListeners;
/**
*
* @author Jan Jancura
*/
public class SourcePathProviderImpl extends SourcePathProvider {
private static boolean verbose =
System.getProperty ("netbeans.debugger.sourcepathproviderimpl") != null;
private static Logger logger = Logger.getLogger("org.netbeans.modules.scala.debugger.projects");
private static final Pattern thisDirectoryPattern = Pattern.compile("(/|\\A)\\./");
private static final Pattern parentDirectoryPattern = Pattern.compile("(/|\\A)([^/]+?)/\\.\\./");
/** Contains all known source paths + jdk source path for JPDAStart task */
private ClassPath originalSourcePath;
/** Contains the additional source roots, added at a later time to the original roots. */
private Set<String> additionalSourceRoots;
/** Contains just the source paths which are selected for debugging. */
private ClassPath smartSteppingSourcePath;
private String[] projectSourceRoots;
private PropertyChangeSupport pcs;
private PathRegistryListener pathRegistryListener;
public SourcePathProviderImpl () {
pcs = new PropertyChangeSupport (this);
}
public SourcePathProviderImpl (ContextProvider contextProvider) {
pcs = new PropertyChangeSupport (this);
//this.session = (Session) contextProvider.lookupFirst
// (null, Session.class);
Map properties = contextProvider.lookupFirst(null, Map.class);
// 2) get default allSourceRoots of source roots used for stepping
if (properties != null) {
smartSteppingSourcePath = (ClassPath) properties.get ("sourcepath");
ClassPath jdkCP = (ClassPath) properties.get ("jdksources");
if ( (jdkCP == null) && (JavaPlatform.getDefault () != null) )
jdkCP = JavaPlatform.getDefault ().getSourceFolders ();
originalSourcePath = jdkCP == null ?
smartSteppingSourcePath :
ClassPathSupport.createProxyClassPath (
new ClassPath[] {
jdkCP,
smartSteppingSourcePath
}
);
projectSourceRoots = getSourceRoots(originalSourcePath);
Set<FileObject> preferredRoots = new HashSet<FileObject>();
preferredRoots.addAll(Arrays.asList(originalSourcePath.getRoots()));
Set<FileObject> globalRoots = new TreeSet<FileObject>(new FileObjectComparator());
globalRoots.addAll(GlobalPathRegistry.getDefault().getSourceRoots());
globalRoots.removeAll(preferredRoots);
ClassPath globalCP = ClassPathSupport.createClassPath(globalRoots.toArray(new FileObject[0]));
originalSourcePath = ClassPathSupport.createProxyClassPath(
originalSourcePath,
globalCP
);
} else {
pathRegistryListener = new PathRegistryListener();
GlobalPathRegistry.getDefault().addGlobalPathRegistryListener(
WeakListeners.create(GlobalPathRegistryListener.class,
pathRegistryListener,
GlobalPathRegistry.getDefault()));
JavaPlatformManager.getDefault ().addPropertyChangeListener(
WeakListeners.propertyChange(pathRegistryListener,
JavaPlatformManager.getDefault()));
List<FileObject> allSourceRoots = new ArrayList<FileObject>();
Set<FileObject> preferredRoots = new HashSet<FileObject>();
Set<FileObject> addedBinaryRoots = new HashSet<FileObject>();
Project mainProject = OpenProjects.getDefault().getMainProject();
if (mainProject != null) {
SourceGroup[] sgs = ProjectUtils.getSources(mainProject).getSourceGroups(JavaProjectConstants.SOURCES_TYPE_JAVA);
for (SourceGroup sg : sgs) {
ClassPath ecp = ClassPath.getClassPath(sg.getRootFolder(), ClassPath.EXECUTE);
if (ecp == null) {
ecp = ClassPath.getClassPath(sg.getRootFolder(), ClassPath.SOURCE);
}
if (ecp != null) {
FileObject[] binaryRoots = ecp.getRoots();
for (FileObject fo : binaryRoots) {
if (addedBinaryRoots.contains(fo)) {
continue;
}
addedBinaryRoots.add(fo);
try {
FileObject[] roots = SourceForBinaryQuery.findSourceRoots(fo.getURL()).getRoots();
for (FileObject fr : roots) {
if (!preferredRoots.contains(fr)) {
allSourceRoots.add(fr);
preferredRoots.add(fr);
}
}
} catch (FileStateInvalidException ex) {
Exceptions.printStackTrace(ex);
}
}
}
}
}
Set<FileObject> globalRoots = new TreeSet<FileObject>(new FileObjectComparator());
globalRoots.addAll(GlobalPathRegistry.getDefault().getSourceRoots());
for (FileObject fo : globalRoots) {
if (!preferredRoots.contains(fo)) {
allSourceRoots.add(fo);
}
}
List<FileObject> allSourceRoots1 = new ArrayList<FileObject>(allSourceRoots.size());
for (FileObject fo: allSourceRoots) {
if (FileUtil.isArchiveFile(fo)) {
allSourceRoots1.add(FileUtil.getArchiveRoot(fo));
} else {
allSourceRoots1.add(fo);
}
}
originalSourcePath = ClassPathSupport.createClassPath (
allSourceRoots1.toArray(new FileObject [allSourceRoots1.size()])
);
projectSourceRoots = getSourceRoots(originalSourcePath);
JavaPlatform[] platforms = JavaPlatformManager.getDefault ().
getInstalledPlatforms ();
int i, k = platforms.length;
for (i = 0; i < k; i++) {
FileObject[] roots = platforms [i].getSourceFolders ().
getRoots ();
int j, jj = roots.length;
for (j = 0; j < jj; j++)
allSourceRoots1.remove (roots [j]);
}
smartSteppingSourcePath = ClassPathSupport.createClassPath (
allSourceRoots1.toArray
(new FileObject [allSourceRoots1.size()])
);
}
if (verbose)
System.out.println
("SPPI: init originalSourcePath " + originalSourcePath);
if (verbose)
System.out.println (
"SPPI: init smartSteppingSourcePath " + smartSteppingSourcePath
);
}
/**
* Translates a relative path ("java/lang/Thread.java") to url
* ("file:///C:/Sources/java/lang/Thread.java"). Uses GlobalPathRegistry
* if global == true.
*
* @param relativePath a relative path (java/lang/Thread.java)
* @param global true if global path should be used
* @return url or <code>null</code>
*/
public String getURL (String relativePath, boolean global) { if (verbose) System.out.println ("SPPI: getURL " + relativePath + " global " + global);
FileObject fo;
relativePath = normalize(relativePath);
if (originalSourcePath == null) {
fo = GlobalPathRegistry.getDefault().findResource(relativePath);
} else {
synchronized (this) {
if (!global) {
fo = smartSteppingSourcePath.findResource(relativePath);
if (verbose) System.out.println ("SPPI: fo " + fo);
} else {
fo = originalSourcePath.findResource(relativePath);
if (verbose) System.out.println ("SPPI: fo " + fo);
}
}
}
if (fo == null) return null;
try {
return fo.getURL ().toString ();
} catch (FileStateInvalidException e) { if (verbose) System.out.println ("SPPI: FileStateInvalidException");
return null;
}
}
/**
* Translates a relative path to all possible URLs.
* Uses GlobalPathRegistry if global == true.
*
* @param relativePath a relative path (java/lang/Thread.java)
* @param global true if global path should be used
* @return url
*/
public String[] getAllURLs (String relativePath, boolean global) { if (verbose) System.out.println ("SPPI: getURL " + relativePath + " global " + global);
List<FileObject> fos;
relativePath = normalize(relativePath);
if (originalSourcePath == null) {
fos = new ArrayList<FileObject>();
for (ClassPath cp : GlobalPathRegistry.getDefault().getPaths(ClassPath.SOURCE)) {
fos.addAll(cp.findAllResources(relativePath));
}
} else {
synchronized (this) {
if (!global) {
fos = smartSteppingSourcePath.findAllResources(relativePath);
if (verbose) System.out.println ("SPPI: fos " + fos);
} else {
fos = originalSourcePath.findAllResources(relativePath);
if (verbose) System.out.println ("SPPI: fos " + fos);
}
}
}
List<String> urls = new ArrayList<String>(fos.size());
for (FileObject fo : fos) {
try {
urls.add(fo.getURL().toString());
} catch (FileStateInvalidException e) { if (verbose) System.out.println ("SPPI: FileStateInvalidException for "+fo);
// skip it
}
}
return urls.toArray(new String[0]);
}
/**
* Returns relative path for given url.
*
* @param url a url of resource file
* @param directorySeparator a directory separator character
* @param includeExtension whether the file extension should be included
* in the result
*
* @return relative path
*/
public String getRelativePath (
String url,
char directorySeparator,
boolean includeExtension
) {
// 1) url -> FileObject
FileObject fo = null; if (verbose) System.out.println ("SPPI: getRelativePath " + url);
try {
fo = URLMapper.findFileObject (new URL (url)); if (verbose) System.out.println ("SPPI: fo " + fo);
} catch (MalformedURLException e) {
//e.printStackTrace ();
return null;
}
String relativePath = smartSteppingSourcePath.getResourceName (
fo,
directorySeparator,
includeExtension
);
if (relativePath == null) {
// fallback to FileObject's class path
ClassPath cp = ClassPath.getClassPath (fo, ClassPath.SOURCE);
if (cp == null)
cp = ClassPath.getClassPath (fo, ClassPath.COMPILE);
if (cp == null) return null;
relativePath = cp.getResourceName (
fo,
directorySeparator,
includeExtension
);
}
return relativePath;
}
/**
* Returns the source root (if any) for given url.
*
* @param url a url of resource file
*
* @return the source root or <code>null</code> when no source root was found.
*/
@Override
public synchronized String getSourceRoot(String url) {
FileObject fo;
try {
fo = URLMapper.findFileObject(new java.net.URL(url));
} catch (java.net.MalformedURLException ex) {
fo = null;
}
FileObject[] roots = null;
if (fo != null) {
ClassPath cp = ClassPath.getClassPath(fo, ClassPath.SOURCE);
if (cp != null) {
roots = cp.getRoots();
}
}
if (roots == null) {
roots = originalSourcePath.getRoots();
}
for (FileObject fileObject : roots) {
try {
String rootURL = fileObject.getURL().toString();
if (url.startsWith(rootURL)) {
String root = getRoot(fileObject);
if (root != null) {
return root;
}
}
} catch (FileStateInvalidException ex) {
// Invalid source root - skip
}
}
return null; // not found
}
private String[] getSourceRoots(ClassPath classPath) {
FileObject[] sourceRoots = classPath.getRoots();
List<String> roots = new ArrayList<String>(sourceRoots.length);
for (FileObject fo : sourceRoots) {
String root = getRoot(fo);
if (root != null) {
roots.add(root);
}
}
return roots.toArray(new String[0]);
}
/**
* Returns allSourceRoots of original source roots.
*
* @return allSourceRoots of original source roots
*/
public synchronized String[] getOriginalSourceRoots () {
return getSourceRoots(originalSourcePath);
}
/**
* Returns array of source roots.
*
* @return array of source roots
*/
public synchronized String[] getSourceRoots () {
return getSourceRoots(smartSteppingSourcePath);
}
/**
* Returns the project's source roots.
*
* @return array of source roots belonging to the project
*/
public String[] getProjectSourceRoots() {
return projectSourceRoots;
}
/**
* Sets array of source roots.
*
* @param sourceRoots a new array of sourceRoots
*/
public void setSourceRoots (String[] sourceRoots) {
if (logger.isLoggable(Level.FINE)) {
logger.fine("SourcePathProviderImpl.setSourceRoots("+java.util.Arrays.asList(sourceRoots)+")");
}
Set<String> newRoots = new HashSet<String>(Arrays.asList(sourceRoots));
ClassPath oldCP = null;
ClassPath newCP = null;
synchronized (this) {
List<FileObject> sourcePath = new ArrayList<FileObject>(
Arrays.asList(smartSteppingSourcePath.getRoots()));
List<FileObject> sourcePathOriginal = new ArrayList<FileObject>(
Arrays.asList(originalSourcePath.getRoots()));
// First check whether there are some new source roots
Set<String> newOriginalRoots = new HashSet<String>(newRoots);
for (FileObject fo : sourcePathOriginal) {
newOriginalRoots.remove(getRoot(fo));
}
if (!newOriginalRoots.isEmpty()) {
for (String root : newOriginalRoots) {
FileObject fo = getFileObject(root);
if (fo != null) {
sourcePathOriginal.add(fo);
}
}
originalSourcePath =
ClassPathSupport.createClassPath(
sourcePathOriginal.toArray(new FileObject[0]));
if (additionalSourceRoots == null) {
additionalSourceRoots = new HashSet<String>();
}
additionalSourceRoots.addAll(newOriginalRoots);
}
// Then correct the smart-stepping path
Set<String> newSteppingRoots = new HashSet<String>(newRoots);
for (FileObject fo : sourcePath) {
newSteppingRoots.remove(getRoot(fo));
}
Set<FileObject> removedSteppingRoots = new HashSet<FileObject>();
Set<FileObject> removedOriginalRoots = new HashSet<FileObject>();
for (FileObject fo : sourcePath) {
String spr = getRoot(fo);
if (!newRoots.contains(spr)) {
removedSteppingRoots.add(fo);
if (additionalSourceRoots != null && additionalSourceRoots.contains(spr)) {
removedOriginalRoots.add(fo);
additionalSourceRoots.remove(spr);
if (additionalSourceRoots.size() == 0) {
additionalSourceRoots = null;
}
}
}
}
if (removedOriginalRoots.size() > 0) {
sourcePathOriginal.removeAll(removedOriginalRoots);
originalSourcePath =
ClassPathSupport.createClassPath(
sourcePathOriginal.toArray(new FileObject[0]));
}
if (newSteppingRoots.size() > 0 || removedSteppingRoots.size() > 0) {
for (String root : newSteppingRoots) {
FileObject fo = getFileObject(root);
if (fo != null) {
sourcePath.add(fo);
}
}
sourcePath.removeAll(removedSteppingRoots);
oldCP = smartSteppingSourcePath;
smartSteppingSourcePath =
ClassPathSupport.createClassPath(
sourcePath.toArray(new FileObject[0]));
newCP = smartSteppingSourcePath;
}
}
if (oldCP != null) {
pcs.firePropertyChange (PROP_SOURCE_ROOTS, oldCP, newCP);
}
}
/**
* Adds property change listener.
*
* @param l new listener.
*/
public void addPropertyChangeListener (PropertyChangeListener l) {
pcs.addPropertyChangeListener (l);
}
/**
* Removes property change listener.
*
* @param l removed listener.
*/
public void removePropertyChangeListener (
PropertyChangeListener l
) {
pcs.removePropertyChangeListener (l);
}
// helper methods ..........................................................
/**
* Normalizes the given path by removing unnecessary "." and ".." sequences.
* This normalization is needed because the compiler stores source paths like "foo/../inc.jsp" into .class files.
* Such paths are not supported by our ClassPath API.
* TODO: compiler bug? report to JDK?
*
* @param path path to normalize
* @return normalized path without "." and ".." elements
*/
public static String normalize(String path) {
for (Matcher m = thisDirectoryPattern.matcher(path); m.find(); )
{
path = m.replaceAll("$1");
m = thisDirectoryPattern.matcher(path);
}
for (Matcher m = parentDirectoryPattern.matcher(path); m.find(); )
{
if (!m.group(2).equals("..")) {
path = path.substring(0, m.start()) + m.group(1) + path.substring(m.end());
m = parentDirectoryPattern.matcher(path);
}
}
return path;
}
/**
* Returns source root for given ClassPath root as String, or <code>null</code>.
*/
private static String getRoot(FileObject fileObject) {
File f = null;
String path = "";
try {
if (fileObject.getFileSystem () instanceof JarFileSystem) {
f = ((JarFileSystem) fileObject.getFileSystem ()).getJarFile ();
if (!fileObject.isRoot()) {
path = "!/"+fileObject.getPath();
}
} else {
f = FileUtil.toFile (fileObject);
}
} catch (FileStateInvalidException ex) {
}
if (f != null) {
return f.getAbsolutePath () + path;
} else {
return null;
}
}
/**
* Returns FileObject for given String.
*/
private FileObject getFileObject (String file) {
File f = new File (file);
FileObject fo = FileUtil.toFileObject (f);
String path = null;
if (fo == null && file.contains("!/")) {
int index = file.indexOf("!/");
f = new File(file.substring(0, index));
fo = FileUtil.toFileObject (f);
path = file.substring(index + "!/".length());
}
if (fo != null && FileUtil.isArchiveFile (fo)) {
fo = FileUtil.getArchiveRoot (fo);
if (path !=null) {
fo = fo.getFileObject(path);
}
}
return fo;
}
private class PathRegistryListener implements GlobalPathRegistryListener, PropertyChangeListener {
public void pathsAdded(GlobalPathRegistryEvent event) {
List<FileObject> addedRoots = new ArrayList<FileObject>();
for (ClassPath cp : event.getChangedPaths()) {
for (FileObject fo : cp.getRoots()) {
addedRoots.add(fo);
}
}
if (addedRoots.size() > 0) {
synchronized (SourcePathProviderImpl.this) {
List<FileObject> sourcePaths = new ArrayList<FileObject>(
Arrays.asList(originalSourcePath.getRoots()));
sourcePaths.addAll(addedRoots);
originalSourcePath =
ClassPathSupport.createClassPath(
sourcePaths.toArray(new FileObject[0]));
sourcePaths = new ArrayList<FileObject>(
Arrays.asList(smartSteppingSourcePath.getRoots()));
sourcePaths.addAll(addedRoots);
smartSteppingSourcePath =
ClassPathSupport.createClassPath(
sourcePaths.toArray(new FileObject[0]));
}
pcs.firePropertyChange (PROP_SOURCE_ROOTS, null, null);
}
}
public void pathsRemoved(GlobalPathRegistryEvent event) {
List<FileObject> removedRoots = new ArrayList<FileObject>();
for (ClassPath cp : event.getChangedPaths()) {
for (FileObject fo : cp.getRoots()) {
removedRoots.add(fo);
}
}
if (removedRoots.size() > 0) {
synchronized (SourcePathProviderImpl.this) {
List<FileObject> sourcePaths = new ArrayList<FileObject>(
Arrays.asList(originalSourcePath.getRoots()));
sourcePaths.removeAll(removedRoots);
originalSourcePath =
ClassPathSupport.createClassPath(
sourcePaths.toArray(new FileObject[0]));
sourcePaths = new ArrayList<FileObject>(
Arrays.asList(smartSteppingSourcePath.getRoots()));
sourcePaths.removeAll(removedRoots);
smartSteppingSourcePath =
ClassPathSupport.createClassPath(
sourcePaths.toArray(new FileObject[0]));
}
pcs.firePropertyChange (PROP_SOURCE_ROOTS, null, null);
}
}
public void propertyChange(PropertyChangeEvent evt) {
// JDK sources changed
JavaPlatform[] platforms = JavaPlatformManager.getDefault ().
getInstalledPlatforms ();
boolean changed = false;
synchronized (SourcePathProviderImpl.this) {
List<FileObject> sourcePaths = new ArrayList<FileObject>(
Arrays.asList(originalSourcePath.getRoots()));
for(JavaPlatform jp : platforms) {
FileObject[] roots = jp.getSourceFolders().getRoots ();
for (FileObject fo : roots) {
if (!sourcePaths.contains(fo)) {
sourcePaths.add(fo);
changed = true;
}
}
}
if (changed) {
originalSourcePath =
ClassPathSupport.createClassPath(
sourcePaths.toArray(new FileObject[0]));
}
}
if (changed) {
pcs.firePropertyChange (PROP_SOURCE_ROOTS, null, null);
}
}
}
private static final class FileObjectComparator implements Comparator<FileObject> {
public int compare(FileObject fo1, FileObject fo2) {
String r1 = getRoot(fo1);
String r2 = getRoot(fo2);
if (r1 == null) {
return -1;
}
if (r2 == null) {
return +1;
}
return r1.compareTo(r2);
}
}
}