/*
* 03/21/2010
*
* Copyright (C) 2010 Robert Futrell
* robert_futrell at users.sourceforge.net
* http://fifesoft.com/rsyntaxtextarea
*
* This library is distributed under a modified BSD license. See the included
* RSTALanguageSupport.License.txt file for details.
*/
package org.fife.rsta.ac.java;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import org.fife.rsta.ac.java.buildpath.JarLibraryInfo;
import org.fife.rsta.ac.java.buildpath.LibraryInfo;
import org.fife.rsta.ac.java.buildpath.SourceLocation;
import org.fife.rsta.ac.java.classreader.ClassFile;
import org.fife.rsta.ac.java.rjc.ast.ImportDeclaration;
import org.fife.ui.autocomplete.CompletionProvider;
/**
* Manages a list of jars and gets completions from them. This can be shared
* amongst multiple {@link JavaCompletionProvider} instances.
*
* @author Robert Futrell
* @version 1.0
*/
public class JarManager {
/**
* Locations of class files to get completions from.
*/
private List classFileSources;
/**
* Whether to check datestamps on jars/directories when completion
* information is requested.
*/
private static boolean checkModified;
/**
* Constructor.
*/
public JarManager() {
classFileSources = new ArrayList();
setCheckModifiedDatestamps(true);
}
/**
* Adds completions matching the specified text to a list.
*
* @param p The parent completion provider.
* @param text The text to match.
* @param addTo The list to add completion choices to.
*/
public void addCompletions(CompletionProvider p, String text, Set addTo) {
/*
* The commented-out code below is probably replaced by the rest of the code
* in this method...
TODO: Verify me!!!
*
// Add any completions matching the text for each jar we know about
String[] pkgNames = Util.splitOnChar(text, '.');
for (int i=0; i<jars.size(); i++) {
JarReader jar = (JarReader)jars.get(i);
jar.addCompletions(p, pkgNames, addTo);
}
*/
if (text.length()==0) {
return;
}
// If what they've typed is qualified, add qualified completions.
if (text.indexOf('.')>-1) {
String[] pkgNames = Util.splitOnChar(text, '.');
for (int i=0; i<classFileSources.size(); i++) {
JarReader jar = (JarReader)classFileSources.get(i);
jar.addCompletions(p, pkgNames, addTo);
}
}
// If they are (possibly) typing an unqualified class name, see if
// what they're typing matches any classes in any of jar jars, and if
// so, add completions for them too. This allows the user to get
// completions for classes not in their import statements.
// Thanks to Guilherme Joao Frantz and Jonatas Schuler for the patch!
else {//if (text.indexOf('.')==-1) {
String lowerCaseText = text.toLowerCase();
for (int i=0; i<classFileSources.size(); i++) {
JarReader jar = (JarReader) classFileSources.get(i);
List classFiles = jar.getClassesWithNamesStartingWith(lowerCaseText);
if (classFiles!=null) {
for (Iterator j=classFiles.iterator(); j.hasNext(); ) {
ClassFile cf = (ClassFile)j.next();
if (org.fife.rsta.ac.java.classreader.Util.isPublic(cf.getAccessFlags())) {
addTo.add(new ClassCompletion(p, cf));
}
}
}
}
}
}
/**
* Adds a jar to read from. This is a convenience method for folks only
* reading classes from jar files.
*
* @param jarFile The jar to add. This cannot be <code>null</code>.
* @return Whether this jar was added (e.g. it wasn't already loaded, or
* it has a new source path).
* @throws IOException If an IO error occurs.
* @see #addClassFileSource(LibraryInfo)
* @see #addCurrentJreClassFileSource()
* @see #getClassFileSources()
* @see #removeClassFileSource(File)
*/
public boolean addClassFileSource(File jarFile) throws IOException {
if (jarFile==null) {
throw new IllegalArgumentException("jarFile cannot be null");
}
return addClassFileSource(new JarLibraryInfo(jarFile));
}
/**
* Adds a class file source to read from.
*
* @param info The source to add. If this is <code>null</code>, then
* the current JVM's main JRE jar (rt.jar, or classes.jar on OS X)
* will be added. If this source has already been added, adding it
* again will do nothing (except possibly update its attached source
* location).
* @return Whether this source was added (e.g. it wasn't already loaded, or
* it has a new source path).
* @throws IOException If an IO error occurs.
* @see #addClassFileSource(File)
* @see #addCurrentJreClassFileSource()
* @see #getClassFileSources()
* @see #removeClassFileSource(LibraryInfo)
*/
public boolean addClassFileSource(LibraryInfo info) throws IOException {
if (info==null) {
throw new IllegalArgumentException("info cannot be null");
}
// First see if this jar is already on the "build path."
for (int i=0; i<classFileSources.size(); i++) {
JarReader jar = (JarReader)classFileSources.get(i);
LibraryInfo info2 = jar.getLibraryInfo();
if (info2.equals(info)) {
// Only update if the source location is different.
SourceLocation source = info.getSourceLocation();
SourceLocation source2 = info2.getSourceLocation();
if ((source==null && source2!=null) ||
(source!=null && !source.equals(source2))) {
classFileSources.set(i, new JarReader((LibraryInfo)info.clone()));
return true;
}
return false;
}
}
// If it isn't on the build path, add it now.
classFileSources.add(new JarReader(info));
return true;
}
/**
* Adds the current JVM's rt.jar (or class.jar if on OS X) to the list of
* class file sources. If the application is running in a JDK, the
* associated source zip is also located and used.
*
* @throws IOException If an IO error occurs.
* @see #addClassFileSource(LibraryInfo)
*/
public void addCurrentJreClassFileSource() throws IOException {
addClassFileSource(LibraryInfo.getMainJreJarInfo());
}
/**
* Removes all class file sources from the "build path."
*
* @see #removeClassFileSource(LibraryInfo)
* @see #removeClassFileSource(File)
* @see #addClassFileSource(LibraryInfo)
* @see #getClassFileSources()
*/
public void clearClassFileSources() {
classFileSources.clear();
}
/**
* Returns whether the "last modified" time stamp on jars and class
* directories should be checked whenever completions are requested, and
* if the jar/directory has been modified since the last time, reload any
* cached class file data. This allows for code completion to update
* whenever dependencies are rebuilt, but has the side effect of increased
* file I/O. By default this option is enabled; if you somehow find the
* file I/O to be a bottleneck (perhaps accessing jars over a slow NFS
* mount), you can disable this option.
*
* @return Whether jars/directories are checked for modification since
* the last access, and clear any cached completion information if
* so.
* @see #setCheckModifiedDatestamps(boolean)
*/
public static boolean getCheckModifiedDatestamps() {
return checkModified;
}
public ClassFile getClassEntry(String className) {
String[] items = Util.splitOnChar(className, '.');
for (int i=0; i<classFileSources.size(); i++) {
JarReader jar = (JarReader)classFileSources.get(i);
ClassFile cf = jar.getClassEntry(items);
if (cf!=null) {
return cf;
}
}
return null;
}
/**
* Returns a list of all classes/interfaces/enums with a given (unqualified)
* name. There may be several, since the name is unqualified.
*
* @param name The unqualified name of a type declaration.
* @param importDeclarations The imports of the compilation unit, if any.
* @return A list of type declarations with the given name, or
* <code>null</code> if there are none.
*/
public List getClassesWithUnqualifiedName(String name,
List importDeclarations) {
// Might be more than one class/interface/enum with the same name.
List result = null;
// Loop through all of our imports.
for (int i=0; i<importDeclarations.size(); i++) {
ImportDeclaration idec = (ImportDeclaration)importDeclarations.get(i);
// Static imports are for fields/methods, not classes
if (!idec.isStatic()) {
// Wildcard => See if package contains a class with this name
if (idec.isWildcard()) {
String qualified = idec.getName();
qualified = qualified.substring(0, qualified.indexOf('*'));
qualified += name;
ClassFile entry = getClassEntry(qualified);
if (entry!=null) {
if (result==null) {
result = new ArrayList(1); // Usually small
}
result.add(entry);
}
}
// Not wildcard => fully-qualified class/interface name
else {
String name2 = idec.getName();
String unqualifiedName2 = name2.substring(name2.lastIndexOf('.')+1);
if (unqualifiedName2.equals(name)) {
ClassFile entry = getClassEntry(name2);
if (entry!=null) { // Should always be true
if (result==null) {
result = new ArrayList(1); // Usually small
}
result.add(entry);
}
else {
System.err.println("ERROR: Class not found! - " + name2);
}
}
}
}
}
// Also check java.lang
String qualified = "java.lang." + name;
ClassFile entry = getClassEntry(qualified);
if (entry!=null) {
if (result==null) {
result = new ArrayList(1); // Usually small
}
result.add(entry);
}
return result;
}
/**
*
* @param pkgName A package name.
* @return A list of all classes in that package.
*/
public List getClassesInPackage(String pkgName, boolean inPkg) {
List list = new ArrayList();
String[] pkgs = Util.splitOnChar(pkgName, '.');
for (int i=0; i<classFileSources.size(); i++) {
JarReader jar = (JarReader)classFileSources.get(i);
jar.getClassesInPackage(list, pkgs, inPkg);
}
return list;
}
/**
* Returns the jars on the "build path."
*
* @return A list of {@link ClassFileSource}s. Modifying a
* <tt>ClassFileSource</tt> in this list will have no effect on
* this completion provider; in order to do that, you must re-add
* the jar via {@link #addClassFileSource(LibraryInfo)}. If there
* are no jars on the "build path," this will be an empty list.
* @see #addClassFileSource(LibraryInfo)
*/
public List getClassFileSources() {
List jarList = new ArrayList(classFileSources.size());
for (Iterator i=classFileSources.iterator(); i.hasNext(); ) {
JarReader reader = (JarReader)i.next();
jarList.add(reader.getLibraryInfo().clone());
}
return jarList;
}
public SortedMap getPackageEntry(String pkgName) {
String[] pkgs = Util.splitOnChar(pkgName, '.');
SortedMap map = new TreeMap();
for (int i=0; i<classFileSources.size(); i++) {
JarReader jar = (JarReader)classFileSources.get(i);
SortedMap map2 = jar.getPackageEntry(pkgs);
if (map2!=null) {
mergeMaps(map, map2);
}
}
return map;
}
public SourceLocation getSourceLocForClass(String className) {
SourceLocation sourceLoc = null;
for (int i=0; i<classFileSources.size(); i++) {
JarReader jar = (JarReader)classFileSources.get(i);
if (jar.containsClass(className)) {
sourceLoc = jar.getLibraryInfo().getSourceLocation();
break;
}
}
return sourceLoc;
}
private void mergeMaps(SortedMap map, SortedMap toAdd) {
for (Iterator i=toAdd.entrySet().iterator(); i.hasNext(); ) {
Map.Entry entry = (Map.Entry)i.next();
Object key = entry.getKey();
Object value = entry.getValue();
if (map.containsKey(key)) {
if ((map.get(key) instanceof SortedMap) &&
(value instanceof SortedMap)) {
mergeMaps((SortedMap)map.get(key), (SortedMap)value);
}
}
else {
map.put(key, value);
}
}
}
/**
* Removes a jar from the "build path." This is a convenience method for
* folks only adding and removing jar sources.
*
* @param jar The jar to remove.
* @return Whether the jar was removed. This will be <code>false</code>
* if the jar was not on the build path.
* @see #removeClassFileSource(LibraryInfo)
* @see #addClassFileSource(LibraryInfo)
* @see #getClassFileSources()
*/
public boolean removeClassFileSource(File jar) {
return removeClassFileSource(new JarLibraryInfo(jar));
}
/**
* Removes a class file source from the "build path."
*
* @param toRemove The source to remove.
* @return Whether source jar was removed. This will be <code>false</code>
* if the source was not on the build path.
* @see #removeClassFileSource(File)
* @see #addClassFileSource(LibraryInfo)
* @see #getClassFileSources()
*/
public boolean removeClassFileSource(LibraryInfo toRemove) {
for (Iterator i=classFileSources.iterator(); i.hasNext(); ) {
JarReader reader = (JarReader)i.next();
LibraryInfo info = reader.getLibraryInfo();
if (info.equals(toRemove)) {
i.remove();
return true;
}
}
return false;
}
/**
* Sets whether the "last modified" time stamp on jars and class
* directories should be checked whenever completions are requested, and
* if the jar/directory has been modified since the last time, reload any
* cached class file data. This allows for code completion to update
* whenever dependencies are rebuilt, but has the side effect of increased
* file I/O. By default this option is enabled; if you somehow find the
* file I/O to be a bottleneck (perhaps accessing jars over a slow NFS
* mount), you can disable this option.
*
* @param check Whether to check if any jars/directories have been
* modified since the last access, and clear any cached completion
* information if so.
* @see #getCheckModifiedDatestamps()
*/
public static void setCheckModifiedDatestamps(boolean check) {
checkModified = check;
}
}