/* * 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.IOException; import java.util.ArrayList; import java.util.Collection; 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.LibraryInfo; import org.fife.rsta.ac.java.classreader.ClassFile; import org.fife.ui.autocomplete.CompletionProvider; /** * Reads entries from a source of class files, such as a jar or a "bin/" * directory. This class acts as an intermediary between a raw * <code>LibraryInfo</code> and the higher level Java completion classes. * It caches information about classes and refreshes that cache when * appropriate. * * @author Robert Futrell * @version 1.0 */ class JarReader { /** * Information about the jar or directory we're reading classes from. */ private LibraryInfo info; /** * This is essentially a tree model of all classes in the jar or * directory. It's a recursive mapping of <code>String</code>s to either * <code>Map</code>s or {@link ClassFile}s (which are lazily created and * may be <code>null</code>). At each level of the nested map, the string * key is a package name iff its corresponding value is a <code>Map</code>. * Examine that <code>Map</code>'s contents to explore the contents of * that package. If the corresponding value is a <code>ClassFile</code>, * then the string key's value is the name of that class. Finally, if * the corresponding value is <code>null</code>, then the string key's * value is the name of a class, but its contents have not yet been * loaded for use by the code completion library (<code>ClassFile</code>s * are lazily loaded to conserve memory). */ private TreeMap packageMap; private long lastModified; /** * Constructor. * * @param info The jar file to read from. This cannot be <code>null</code>. * @throws IOException If an IO error occurs reading from the jar file. */ public JarReader(LibraryInfo info) throws IOException { this.info = info; packageMap = new TreeMap(String.CASE_INSENSITIVE_ORDER); loadCompletions(); } /** * Gets the completions in this jar that match a given string. * * @param provider The parent completion provider. * @param pkgNames The text to match, split into tokens around the * '<code>.</code>' character. This should be (the start of) a * fully-qualified class, interface, or enum name. * @param addTo The list to add completion choices to. */ public void addCompletions(CompletionProvider provider, String[] pkgNames, Set addTo) { checkLastModified(); TreeMap map = packageMap; for (int i=0; i<pkgNames.length-1; i++) { Object obj = map.get(pkgNames[i]); if (obj instanceof TreeMap) { map = (TreeMap)obj; } else { return; } } String fromKey = pkgNames[pkgNames.length-1]; String toKey = fromKey + '{'; // Ascii char > largest valid class char SortedMap sm = map.subMap(fromKey, toKey); for (Iterator i=sm.keySet().iterator(); i.hasNext(); ) { Object obj = i.next(); //System.out.println(obj + " - " + sm.get(obj)); Object value = sm.get(obj); // See if this is a class, and we already have the ClassFile if (value instanceof ClassFile) { ClassFile cf = (ClassFile)value; boolean inPkg = false; // TODO: Pass me in if (inPkg || org.fife.rsta.ac.java.classreader.Util.isPublic(cf.getAccessFlags())) { addTo.add(new ClassCompletion(provider, cf)); } } // If a ClassFile isn't cached, it's either a class that hasn't // had its ClassFile cached yet, or a package. else { String[] items = new String[pkgNames.length]; System.arraycopy(pkgNames, 0, items, 0, pkgNames.length-1); items[items.length-1] = obj.toString(); ClassFile cf = getClassEntry(items); if (cf!=null) { boolean inPkg = false; // TODO: Pass me in if (inPkg || org.fife.rsta.ac.java.classreader.Util.isPublic(cf.getAccessFlags())) { addTo.add(new ClassCompletion(provider, cf)); } } else { StringBuffer sb = new StringBuffer(); for (int j=0; j<pkgNames.length-1; j++) { sb.append(pkgNames[j]).append('.'); } sb.append(obj.toString()); String text = sb.toString();//obj.toString(); addTo.add(new PackageNameCompletion(provider, text, fromKey)); } } } } /** * Checks whether the jar or class file directory has been modified since * the last use of this reader. If it has, then any cached * <code>ClassFile</code>s are cleared, in case any classes have been * updated. */ private void checkLastModified() { long newLastModified = info.getLastModified(); if (newLastModified!=0 && newLastModified!=lastModified) { int count = 0; count = clearClassFiles(packageMap); System.out.println("DEBUG: Cleared " + count + " cached ClassFiles"); lastModified = newLastModified; } } /** * Removes all <code>ClassFile</code>s from a map. * * @param map The map. * @return The number of class file entries removed. */ private int clearClassFiles(Map map) { int clearedCount = 0; for (Iterator i=map.entrySet().iterator(); i.hasNext(); ) { Map.Entry entry = (Map.Entry)i.next(); Object value = entry.getValue(); if (value instanceof ClassFile) { entry.setValue(null); clearedCount++; } else if (value instanceof Map) { clearedCount += clearClassFiles((Map)value); } } return clearedCount; } public boolean containsClass(String className) { String[] items = className.split("\\."); TreeMap m = packageMap; for (int i=0; i<items.length-1; i++) { // "value" can be a ClassFile "too early" here if className // is a nested class. // TODO: Handle nested classes better Object value = m.get(items[i]); if (!(value instanceof TreeMap)) { return false; } m = (TreeMap)value; } return m.containsKey(items[items.length-1]); } public boolean containsPackage(String pkgName) { String[] items = Util.splitOnChar(pkgName, '.'); TreeMap m = packageMap; for (int i=0; i<items.length; i++) { // "value" can be a ClassFile "too early" here if className // is a nested class. // TODO: Handle nested classes better Object value = m.get(items[i]); if (!(value instanceof TreeMap)) { return false; } m = (TreeMap)value; } return true; } public ClassFile getClassEntry(String[] items) { SortedMap map = packageMap; for (int i=0; i<items.length-1; i++) { if (map.containsKey(items[i])) { Object value = map.get(items[i]); if (!(value instanceof SortedMap)) { return null; } map = (SortedMap)value; } else { return null; } } String className = items[items.length-1]; if (map.containsKey(className)) { Object value = map.get(className); if (value instanceof Map) { // i.e., it's a package return null; } else if (value instanceof ClassFile) { // Already created ClassFile cf = (ClassFile)value; return cf; } else { // A class, just no ClassFile cached yet try { StringBuffer name = new StringBuffer(items[0]); for (int i=1; i<items.length; i++) { name.append('/').append(items[i]); } name.append(".class"); ClassFile cf = info.createClassFile(name.toString()); map.put(className, cf); return cf; } catch (IOException ioe) { ioe.printStackTrace(); } } } return null; } public void getClassesInPackage(List addTo, String[] pkgs, boolean inPkg) { SortedMap map = packageMap; for (int i=0; i<pkgs.length; i++) { if (map.containsKey(pkgs[i])) { Object value = map.get(pkgs[i]); if (!(value instanceof SortedMap)) { // We have a class with the same name as a package... return; } map = (SortedMap)value; } else { return; } } // We can't modify map during our iteration, so we save any // newly-created ClassFiles. Map newClassFiles = null; for (Iterator i=map.entrySet().iterator(); i.hasNext(); ) { Map.Entry entry = (Map.Entry)i.next(); Object value = entry.getValue(); if (value==null) { StringBuffer name = new StringBuffer(pkgs[0]); for (int j=1; j<pkgs.length; j++) { name.append('/').append(pkgs[j]); } name.append('/'); name.append((String)entry.getKey()).append(".class"); try { ClassFile cf = info.createClassFile(name.toString()); if (newClassFiles==null) { newClassFiles = new TreeMap(); } newClassFiles.put(entry.getKey(), cf); possiblyAddTo(addTo, cf, inPkg); } catch (IOException ioe) { ioe.printStackTrace(); break; } } else if (value instanceof ClassFile) { possiblyAddTo(addTo, (ClassFile)value, inPkg); } } if (newClassFiles!=null) { map.putAll(newClassFiles); } } /** * Looks through all classes in this jar or directory, trying to find any * whose unqualified names start with a given prefix. * * @param prefix The prefix of the class names. Case is ignored on this * parameter. * @return A list of {@link ClassFile}s representing classes in this * jar or directory whose unqualified names start with the prefix. * This will never be <code>null</code>, but may of course be * empty. */ public List getClassesWithNamesStartingWith(String prefix) { List res = new ArrayList(); String currentPkg = ""; // Don't use null; we're appending to it getClassesWithNamesStartingWithImpl(prefix, packageMap, currentPkg, res); return res; } /** * Method used to recursively scan our package map for classes whose names * start with a given prefix, ignoring case. * * @param prefix The prefix that the unqualified class names must match * (ignoring case). * @param map A piece of our package map. * @param currentPkg The package that <code>map</code> belongs to (i.e. * all levels of packages scanned before this one), separated by * '<code>/</code>'. * @param addTo The list to add any matching <code>ClassFile</code>s to. */ private void getClassesWithNamesStartingWithImpl(String prefix, Map map, String currentPkg, List addTo) { final int prefixLen = prefix.length(); // Loop through the map's entries, which are String keys mapping to // one of a Map (if the key is a package name), a ClassFile (if the // key is a class name), or null (if the key is a class name, but the // corresponding ClassFile has not been loaded yet). for (Iterator i=map.entrySet().iterator(); i.hasNext(); ) { Map.Entry entry = (Map.Entry)i.next(); String key = (String)entry.getKey(); Object value = entry.getValue(); if (value instanceof Map) { getClassesWithNamesStartingWithImpl(prefix, (Map)value, currentPkg + key + "/", addTo); } else { // value is either a ClassFile or null // If value is null, we only lazily create the ClassFile if // necessary (i.e. if the class name does match what they've // typed). String className = key; if (className.regionMatches(true, 0, prefix, 0, prefixLen)) { if (value==null) { String fqClassName = currentPkg + className + ".class"; try { value = info.createClassFile(fqClassName); entry.setValue(value); // Update the map } catch (IOException ioe) { ioe.printStackTrace(); } } if (value!=null) { // possibly null if IOException above addTo.add(/*(ClassFile)*/value); } } } } } /** * Returns the physical file on disk.<p> * * Modifying the returned object will <em>not</em> have any effect on * code completion; e.g. changing the source location will not have any * effect. * * @return The info. */ public LibraryInfo getLibraryInfo() { return (LibraryInfo)info.clone(); } public SortedMap getPackageEntry(String[] pkgs) { SortedMap map = packageMap; for (int i=0; i<pkgs.length; i++) { if (map.containsKey(pkgs[i])) { Object value = map.get(pkgs[i]); if (!(value instanceof SortedMap)) { // ClassFile or null return null; } map = (SortedMap)value; } else { return null; } } return map; } private void loadCompletions() throws IOException { packageMap = info.createPackageMap(); lastModified = info.getLastModified(); } private void possiblyAddTo(Collection addTo, ClassFile cf, boolean inPkg) { if (inPkg || org.fife.rsta.ac.java.classreader.Util.isPublic(cf.getAccessFlags())) { addTo.add(cf); } } }