0001: /*
0002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
0003: *
0004: * Copyright 1997-2008 Sun Microsystems, Inc. All rights reserved.
0005: *
0006: * The contents of this file are subject to the terms of either the GNU
0007: * General Public License Version 2 only ("GPL") or the Common
0008: * Development and Distribution License("CDDL") (collectively, the
0009: * "License"). You may not use this file except in compliance with the
0010: * License. You can obtain a copy of the License at
0011: * http://www.netbeans.org/cddl-gplv2.html
0012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
0013: * specific language governing permissions and limitations under the
0014: * License. When distributing the software, include this License Header
0015: * Notice in each file and include the License file at
0016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
0017: * particular file as subject to the "Classpath" exception as provided
0018: * by Sun in the GPL Version 2 section of the License file that
0019: * accompanied this code. If applicable, add the following below the
0020: * License Header, with the fields enclosed by brackets [] replaced by
0021: * your own identifying information:
0022: * "Portions Copyrighted [year] [name of copyright owner]"
0023: *
0024: * Contributor(s):
0025: *
0026: * The Original Software is NetBeans. The Initial Developer of the Original
0027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2008 Sun
0028: * Microsystems, Inc. All Rights Reserved.
0029: *
0030: * If you wish your version of this file to be governed by only the CDDL
0031: * or only the GPL Version 2, indicate your decision by adding
0032: * "[Contributor] elects to include this software in this distribution
0033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
0034: * single choice of license, a recipient has the option to distribute
0035: * your version of this file under either the CDDL, the GPL Version 2 or
0036: * to extend the choice of license to its licensees as provided above.
0037: * However, if you add GPL Version 2 code and therefore, elected the GPL
0038: * Version 2 license, then the option applies only if the new code is
0039: * made subject to such option by the copyright holder.
0040: */
0041: package org.netbeans.modules.ruby;
0042:
0043: import java.io.File;
0044: import java.io.IOException;
0045: import java.net.MalformedURLException;
0046: import java.net.URL;
0047: import java.util.ArrayList;
0048: import java.util.Collections;
0049: import java.util.EnumSet;
0050: import java.util.HashMap;
0051: import java.util.HashSet;
0052: import java.util.Iterator;
0053: import java.util.List;
0054: import java.util.Map;
0055: import java.util.Set;
0056: import java.util.logging.Logger;
0057: import java.util.regex.Pattern;
0058:
0059: import org.netbeans.modules.gsf.api.Index;
0060: import org.netbeans.modules.ruby.elements.IndexedField;
0061: import static org.netbeans.modules.gsf.api.Index.*;
0062: import org.netbeans.modules.gsf.api.NameKind;
0063: import org.netbeans.api.ruby.platform.RubyPlatform;
0064: import org.netbeans.api.ruby.platform.RubyPlatformManager;
0065: import org.netbeans.modules.ruby.elements.IndexedClass;
0066: import org.netbeans.modules.ruby.elements.IndexedElement;
0067: import org.netbeans.modules.ruby.elements.IndexedMethod;
0068: import org.openide.filesystems.FileObject;
0069: import org.openide.filesystems.URLMapper;
0070: import org.openide.modules.InstalledFileLocator;
0071: import org.openide.util.Exceptions;
0072:
0073: /**
0074: * Access to the index of known Ruby classes - core, libraries, gems, user projects, etc.
0075: *
0076: * @todo Pull out attributes, fields and constants from the index as well
0077: * @todo Store signature attributes for methods: private/protected?, documented?, returntype?
0078: * @todo When there are multiple method/field definitions, pick access level from one which sets it
0079: * @todo I do case-sensitive startsWith filtering here which is probably not good
0080: * @todo Abort when search list .size() > N
0081: *
0082: * @author Tor Norbye
0083: */
0084: public final class RubyIndex {
0085:
0086: private static final Logger LOGGER = Logger
0087: .getLogger(RubyIndex.class.getName());
0088:
0089: public static final String UNKNOWN_CLASS = "<Unknown>"; // NOI18N
0090: public static final String OBJECT = "Object"; // NOI18N
0091: private static final String CLASS = "Class"; // NOI18N
0092: private static final String MODULE = "Module"; // NOI18N
0093: static final Set<SearchScope> ALL_SCOPE = EnumSet
0094: .allOf(SearchScope.class);
0095: static final Set<SearchScope> SOURCE_SCOPE = EnumSet
0096: .of(SearchScope.SOURCE);
0097: private static String clusterUrl = null;
0098: private static final String CLUSTER_URL = "cluster:"; // NOI18N
0099: private static final String RUBYHOME_URL = "ruby:"; // NOI18N
0100: private static final String GEM_URL = "gem:"; // NOI18N
0101:
0102: private final Index index;
0103:
0104: /** Creates a new instance of RubyIndex */
0105: public RubyIndex(Index index) {
0106: this .index = index;
0107: }
0108:
0109: public static RubyIndex get(Index index) {
0110: return new RubyIndex(index);
0111: }
0112:
0113: private boolean search(String key, String name, NameKind kind,
0114: Set<SearchResult> result) {
0115: try {
0116: index.search(key, name, kind, ALL_SCOPE, result, null);
0117:
0118: return true;
0119: } catch (IOException ioe) {
0120: Exceptions.printStackTrace(ioe);
0121:
0122: return false;
0123: }
0124: }
0125:
0126: private boolean search(String key, String name, NameKind kind,
0127: Set<SearchResult> result, Set<SearchScope> scope) {
0128: try {
0129: index.search(key, name, kind, scope, result, null);
0130:
0131: return true;
0132: } catch (IOException ioe) {
0133: Exceptions.printStackTrace(ioe);
0134:
0135: return false;
0136: }
0137: }
0138:
0139: Set<IndexedClass> getClasses(String name, final NameKind kind,
0140: boolean includeAll, boolean skipClasses, boolean skipModules) {
0141: return getClasses(name, kind, includeAll, skipClasses,
0142: skipModules, ALL_SCOPE, null);
0143: }
0144:
0145: /**
0146: * Return the full set of classes that match the given name.
0147: *
0148: * @param name The name of the class - possibly a fqn like File::Stat, or just a class
0149: * name like Stat, or just a prefix like St.
0150: * @param kind Whether we want the exact name, or whether we're searching by a prefix.
0151: * @param includeAll If true, return multiple IndexedClasses for the same logical
0152: * class, one for each declaration point. For example, File is defined both in the
0153: * builtin stubs as well as in ftools.
0154: */
0155: public Set<IndexedClass> getClasses(String name,
0156: final NameKind kind, boolean includeAll,
0157: boolean skipClasses, boolean skipModules,
0158: Set<Index.SearchScope> scope, Set<String> uniqueClasses) {
0159: String classFqn = null;
0160:
0161: if (name != null) {
0162: if (name.indexOf("::") != -1) { // NOI18N
0163:
0164: int p = name.lastIndexOf("::"); // NOI18N
0165: classFqn = name.substring(0, p);
0166: name = name.substring(p + 2);
0167: } else if (name.endsWith(":")) {
0168: // User has typed something like "Test:" and wants completion on
0169: // for something like Test::Unit
0170: classFqn = name.substring(0, name.length() - 1);
0171: name = "";
0172: }
0173: }
0174:
0175: final Set<SearchResult> result = new HashSet<SearchResult>();
0176:
0177: // if (!isValid()) {
0178: // LOGGER.fine(String.format("LuceneIndex[%s] is invalid!\n", this.toString()));
0179: // return;
0180: // }
0181: String field;
0182:
0183: switch (kind) {
0184: case EXACT_NAME:
0185: case PREFIX:
0186: case CAMEL_CASE:
0187: case REGEXP:
0188: field = RubyIndexer.FIELD_CLASS_NAME;
0189:
0190: break;
0191:
0192: case CASE_INSENSITIVE_PREFIX:
0193: case CASE_INSENSITIVE_REGEXP:
0194: field = RubyIndexer.FIELD_CASE_INSENSITIVE_CLASS_NAME;
0195:
0196: break;
0197:
0198: default:
0199: throw new UnsupportedOperationException(kind.toString());
0200: }
0201:
0202: search(field, name, kind, result, scope);
0203:
0204: // TODO Prune methods to fit my scheme - later make lucene index smarter about how to prune its index search
0205: if (includeAll) {
0206: uniqueClasses = null;
0207: } else if (uniqueClasses == null) {
0208: uniqueClasses = new HashSet<String>();
0209: }
0210:
0211: final Set<IndexedClass> classes = new HashSet<IndexedClass>();
0212:
0213: for (SearchResult map : result) {
0214: String clz = map.getValue(RubyIndexer.FIELD_CLASS_NAME);
0215:
0216: if (clz == null) {
0217: // It's probably a module
0218: // XXX I need to handle this... for now punt
0219: continue;
0220: }
0221:
0222: // Lucene returns some inexact matches, TODO investigate why this is necessary
0223: if ((kind == NameKind.PREFIX) && !clz.startsWith(name)) {
0224: continue;
0225: } else if (kind == NameKind.CASE_INSENSITIVE_PREFIX
0226: && !clz.regionMatches(true, 0, name, 0, name
0227: .length())) {
0228: continue;
0229: }
0230:
0231: if (classFqn != null) {
0232: if (kind == NameKind.CASE_INSENSITIVE_PREFIX
0233: || kind == NameKind.CASE_INSENSITIVE_REGEXP) {
0234: if (!classFqn.equalsIgnoreCase(map
0235: .getValue(RubyIndexer.FIELD_IN))) {
0236: continue;
0237: }
0238: } else if (kind == NameKind.CAMEL_CASE) {
0239: String in = map.getValue(RubyIndexer.FIELD_IN);
0240: if (in != null) {
0241: // Superslow, make faster
0242: StringBuilder sb = new StringBuilder();
0243: // String prefix = null;
0244: int lastIndex = 0;
0245: int index;
0246: do {
0247:
0248: int nextUpper = -1;
0249: for (int i = lastIndex + 1; i < classFqn
0250: .length(); i++) {
0251: if (Character.isUpperCase(classFqn
0252: .charAt(i))) {
0253: nextUpper = i;
0254: break;
0255: }
0256: }
0257: index = nextUpper;
0258: String token = classFqn.substring(
0259: lastIndex, index == -1 ? classFqn
0260: .length() : index);
0261: // if ( lastIndex == 0 ) {
0262: // prefix = token;
0263: // }
0264: sb.append(token);
0265: // TODO - add in Ruby chars here?
0266: sb
0267: .append(index != -1 ? "[\\p{javaLowerCase}\\p{Digit}_\\$]*"
0268: : ".*"); // NOI18N
0269: lastIndex = index;
0270: } while (index != -1);
0271:
0272: final Pattern pattern = Pattern.compile(sb
0273: .toString());
0274: if (!pattern.matcher(in).matches()) {
0275: continue;
0276: }
0277: } else {
0278: continue;
0279: }
0280: } else {
0281: if (!classFqn.equals(map
0282: .getValue(RubyIndexer.FIELD_IN))) {
0283: continue;
0284: }
0285: }
0286: }
0287:
0288: String attrs = map.getValue(RubyIndexer.FIELD_CLASS_ATTRS);
0289: boolean isClass = true;
0290: if (attrs != null) {
0291: int flags = IndexedElement.stringToFlag(attrs, 0);
0292: isClass = (flags & IndexedClass.MODULE) == 0;
0293:
0294: }
0295:
0296: if (skipClasses && isClass) {
0297: continue;
0298: }
0299:
0300: if (skipModules && !isClass) {
0301: continue;
0302: }
0303:
0304: String fqn = map.getValue(RubyIndexer.FIELD_FQN_NAME);
0305:
0306: // Only return a single instance for this signature
0307: if (!includeAll) {
0308: if (uniqueClasses.contains(fqn)) { // use a map to point right to the class
0309: // Prefer the instance that provides documentation
0310:
0311: boolean replaced = false;
0312:
0313: int flags = 0;
0314: if (attrs != null) {
0315: flags = IndexedElement.stringToFlag(attrs, 0);
0316: }
0317:
0318: boolean isDocumented = (flags & IndexedElement.DOCUMENTED) != 0;
0319:
0320: if (isDocumented) {
0321: // Check the actual size of the documentation, and prefer the largest
0322: // method
0323: int length = 0;
0324: int documentedAt = attrs.indexOf(';');
0325:
0326: if (documentedAt != -1) {
0327: int end = attrs.indexOf(';',
0328: documentedAt + 1);
0329: if (end == -1) {
0330: end = attrs.length();
0331: }
0332: length = Integer.parseInt(attrs.substring(
0333: documentedAt + 1, end));
0334: }
0335:
0336: // This instance is documented. Replace the other instance...
0337: for (IndexedClass c : classes) {
0338: if (c.getSignature().equals(fqn)
0339: && (length > c
0340: .getDocumentationLength())) {
0341: classes.remove(c);
0342: replaced = true;
0343:
0344: break;
0345: }
0346: }
0347: }
0348:
0349: if (!replaced) {
0350: continue;
0351: }
0352: } else {
0353: uniqueClasses.add(fqn);
0354: }
0355: }
0356:
0357: classes.add(createClass(fqn, clz, map));
0358: }
0359:
0360: return classes;
0361: }
0362:
0363: /**
0364: * Return the set of classes that directly subclass the given class
0365: *
0366: * @param name The name of the class - possibly a fqn like File::Stat, or just a class
0367: * name like Stat, or just a prefix like St.
0368: * @param kind Whether we want the exact name, or whether we're searching by a prefix.
0369: * @param includeAll If true, return multiple IndexedClasses for the same logical
0370: * class, one for each declaration point. For example, File is defined both in the
0371: * builtin stubs as well as in ftools.
0372: */
0373: public Set<IndexedClass> getSubClasses(String name, String fqn,
0374: final NameKind kind, Set<Index.SearchScope> scope) {
0375: final Set<SearchResult> result = new HashSet<SearchResult>();
0376:
0377: // if (!isValid()) {
0378: // LOGGER.fine(String.format("LuceneIndex[%s] is invalid!\n", this.toString()));
0379: // return;
0380: // }
0381: String field = RubyIndexer.FIELD_EXTENDS_NAME;
0382: search(field, fqn, NameKind.EXACT_NAME, result, scope);
0383:
0384: final Set<IndexedClass> classes = new HashSet<IndexedClass>();
0385:
0386: for (SearchResult map : result) {
0387: String clz = map.getValue(RubyIndexer.FIELD_CLASS_NAME);
0388:
0389: if (clz == null) {
0390: // It's probably a module
0391: // XXX I need to handle this... for now punt
0392: continue;
0393: }
0394:
0395: // Lucene returns some inexact matches, TODO investigate why this is necessary
0396: if ((kind == NameKind.PREFIX) && !clz.startsWith(name)) {
0397: continue;
0398: } else if (kind == NameKind.CASE_INSENSITIVE_PREFIX
0399: && !clz.regionMatches(true, 0, name, 0, name
0400: .length())) {
0401: continue;
0402: }
0403:
0404: String cfqn = map.getValue(RubyIndexer.FIELD_FQN_NAME);
0405:
0406: // Only return a single instance for this signature
0407: classes.add(createClass(cfqn, clz, map));
0408: }
0409:
0410: return classes;
0411: }
0412:
0413: /**
0414: * Return a set of methods that match the given name prefix, and are in the given
0415: * class and module. If no class is specified, match methods across all classes.
0416: * Note that inherited methods are not checked. If you want to match inherited methods
0417: * you must call this method on each superclass as well as the mixin modules.
0418: */
0419: @SuppressWarnings("unchecked")
0420: // unchecked - lucene has source 1.4
0421: Set<IndexedMethod> getMethods(final String name, final String clz,
0422: NameKind kind) {
0423: return getMethods(name, clz, kind, ALL_SCOPE);
0424: }
0425:
0426: @SuppressWarnings("fallthrough")
0427: public Set<IndexedMethod> getMethods(final String name,
0428: final String clz, NameKind kind,
0429: Set<Index.SearchScope> scope) {
0430: boolean inherited = clz == null;
0431:
0432: // public void searchByCriteria(final String name, final ClassIndex.NameKind kind, /*final ResultConvertor<T> convertor,*/ final Set<String> result) throws IOException {
0433: final Set<SearchResult> result = new HashSet<SearchResult>();
0434:
0435: // if (!isValid()) {
0436: // LOGGER.fine(String.format("LuceneIndex[%s] is invalid!\n", this.toString()));
0437: // return;
0438: // }
0439: String field = RubyIndexer.FIELD_METHOD_NAME;
0440: NameKind originalKind = kind;
0441: if (kind == NameKind.EXACT_NAME) {
0442: // I can't do exact searches on methods because the method
0443: // entries include signatures etc. So turn this into a prefix
0444: // search and then compare chopped off signatures with the name
0445: kind = NameKind.PREFIX;
0446: }
0447:
0448: // No point in doing case insensitive searches on method names because
0449: // method names in Ruby are always case insensitive anyway
0450: // case CASE_INSENSITIVE_PREFIX:
0451: // case CASE_INSENSITIVE_REGEXP:
0452: // field = RubyIndexer.FIELD_CASE_INSENSITIVE_METHOD_NAME;
0453: // break;
0454:
0455: search(field, name, kind, result, scope);
0456:
0457: //return Collections.unmodifiableSet(result);
0458:
0459: // TODO Prune methods to fit my scheme - later make lucene index smarter about how to prune its index search
0460: final Set<IndexedMethod> methods = new HashSet<IndexedMethod>();
0461:
0462: for (SearchResult map : result) {
0463: if (clz != null) {
0464: String fqn = map.getValue(RubyIndexer.FIELD_FQN_NAME);
0465:
0466: if (!(clz.equals(fqn))) {
0467: continue;
0468: }
0469: }
0470:
0471: String[] signatures = map
0472: .getValues(RubyIndexer.FIELD_METHOD_NAME);
0473:
0474: if (signatures != null) {
0475: for (String signature : signatures) {
0476: // Skip weird methods... Think harder about this
0477: if (((name == null) || (name.length() == 0))
0478: && !Character.isLowerCase(signature
0479: .charAt(0))) {
0480: continue;
0481: }
0482:
0483: // Lucene returns some inexact matches, TODO investigate why this is necessary
0484: if ((kind == NameKind.PREFIX)
0485: && !signature.startsWith(name)) {
0486: continue;
0487: } else if (kind == NameKind.CASE_INSENSITIVE_PREFIX
0488: && !signature.regionMatches(true, 0, name,
0489: 0, name.length())) {
0490: continue;
0491: } else if (kind == NameKind.CASE_INSENSITIVE_REGEXP) {
0492: int len = signature.length();
0493: int end = signature.indexOf('(');
0494: if (end == -1) {
0495: end = signature.indexOf(';');
0496: if (end == -1) {
0497: end = len;
0498: }
0499: }
0500: String n = end != len ? signature.substring(0,
0501: end) : signature;
0502: try {
0503: if (!n.matches(name)) {
0504: continue;
0505: }
0506: } catch (Exception e) {
0507: // Silently ignore regexp failures in the search expression
0508: }
0509: } else if (originalKind == NameKind.EXACT_NAME) {
0510: // Make sure the name matches exactly
0511: // We know that the prefix is correct from the first part of
0512: // this if clause, by the signature may have more
0513: if (((signature.length() > name.length()) && (signature
0514: .charAt(name.length()) != '('))
0515: && (signature.charAt(name.length()) != ';')) {
0516: continue;
0517: }
0518: }
0519:
0520: // XXX THIS DOES NOT WORK WHEN THERE ARE IDENTICAL SIGNATURES!!!
0521: assert map != null;
0522: methods
0523: .add(createMethod(signature, map, inherited));
0524: }
0525: }
0526:
0527: String[] attributes = map
0528: .getValues(RubyIndexer.FIELD_ATTRIBUTE_NAME);
0529:
0530: if (attributes != null) {
0531: for (String signature : attributes) {
0532: // Skip weird methods... Think harder about this
0533: if (((name == null) || (name.length() == 0))
0534: && !Character.isLowerCase(signature
0535: .charAt(0))) {
0536: continue;
0537: }
0538:
0539: // Lucene returns some inexact matches, TODO investigate why this is necessary
0540: if (kind == NameKind.PREFIX
0541: && !signature.startsWith(name)) {
0542: continue;
0543: } else if (kind == NameKind.CASE_INSENSITIVE_PREFIX
0544: && !signature.regionMatches(true, 0, name,
0545: 0, name.length())) {
0546: continue;
0547: } else if (kind == NameKind.CASE_INSENSITIVE_REGEXP
0548: && !signature.matches(name)) {
0549: continue;
0550: } else if (originalKind == NameKind.EXACT_NAME) {
0551: // Make sure the name matches exactly
0552: // We know that the prefix is correct from the first part of
0553: // this if clause, by the signature may have more
0554: if (((signature.length() > name.length()) &&
0555: //(signature.charAt(name.length()) != '(')) &&
0556: (signature.charAt(name.length()) != ';'))) {
0557: continue;
0558: }
0559: }
0560:
0561: // XXX THIS DOES NOT WORK WHEN THERE ARE IDENTICAL SIGNATURES!!!
0562: assert map != null;
0563: // Create method for the attribute
0564: methods
0565: .add(createMethod(signature, map, inherited));
0566: }
0567: }
0568:
0569: // TODO - fields
0570: }
0571:
0572: return methods;
0573: }
0574:
0575: private IndexedMethod createMethod(String signature,
0576: SearchResult map, boolean inherited) {
0577: String clz = map.getValue(RubyIndexer.FIELD_CLASS_NAME);
0578: String module = map.getValue(RubyIndexer.FIELD_IN);
0579:
0580: if (clz == null) {
0581: // Module method?
0582: clz = module;
0583: } else if ((module != null) && (module.length() > 0)) {
0584: clz = module + "::" + clz;
0585: }
0586:
0587: String fileUrl = map.getPersistentUrl();
0588:
0589: String fqn = map.getValue(RubyIndexer.FIELD_FQN_NAME);
0590: String require = map.getValue(RubyIndexer.FIELD_REQUIRE);
0591:
0592: // Extract attributes
0593: int attributeIndex = signature.indexOf(';');
0594: String attributes = null;
0595: int flags = 0;
0596:
0597: if (attributeIndex != -1) {
0598: flags = IndexedElement.stringToFlag(signature,
0599: attributeIndex + 1);
0600:
0601: if (signature.length() > attributeIndex + 1) {
0602: attributes = signature.substring(attributeIndex + 1,
0603: signature.length());
0604: }
0605:
0606: signature = signature.substring(0, attributeIndex);
0607: }
0608:
0609: IndexedMethod m = IndexedMethod.create(this , signature, fqn,
0610: clz, fileUrl, require, attributes, flags);
0611:
0612: m.setInherited(inherited);
0613: return m;
0614: }
0615:
0616: private IndexedField createField(String signature,
0617: SearchResult map, boolean isInstance, boolean inherited) {
0618: String clz = map.getValue(RubyIndexer.FIELD_CLASS_NAME);
0619: String module = map.getValue(RubyIndexer.FIELD_IN);
0620:
0621: if (clz == null) {
0622: // Module method?
0623: clz = module;
0624: } else if ((module != null) && (module.length() > 0)) {
0625: clz = module + "::" + clz;
0626: }
0627:
0628: String fileUrl = map.getPersistentUrl();
0629:
0630: String fqn = map.getValue(RubyIndexer.FIELD_FQN_NAME);
0631: String require = map.getValue(RubyIndexer.FIELD_REQUIRE);
0632:
0633: int attributeIndex = signature.indexOf(';');
0634: String attributes = null;
0635: int flags = 0;
0636:
0637: if (attributeIndex != -1) {
0638: flags = IndexedElement.stringToFlag(signature,
0639: attributeIndex + 1);
0640:
0641: if (signature.length() > attributeIndex + 1) {
0642: attributes = signature.substring(attributeIndex + 1,
0643: signature.length());
0644: }
0645:
0646: signature = signature.substring(0, attributeIndex);
0647: }
0648:
0649: IndexedField m = IndexedField.create(this , signature, fqn, clz,
0650: fileUrl, require, attributes, flags);
0651: m.setInherited(inherited);
0652:
0653: return m;
0654: }
0655:
0656: private IndexedClass createClass(String fqn, String clz,
0657: SearchResult map) {
0658: String require = map.getValue(RubyIndexer.FIELD_REQUIRE);
0659:
0660: // TODO - how do I determine -which- file to associate with the file?
0661: // Perhaps the one that defines initialize() ?
0662: String fileUrl = map.getPersistentUrl();
0663:
0664: if (clz == null) {
0665: clz = map.getValue(RubyIndexer.FIELD_CLASS_NAME);
0666: }
0667:
0668: String attrs = map.getValue(RubyIndexer.FIELD_CLASS_ATTRS);
0669:
0670: int flags = 0;
0671: if (attrs != null) {
0672: flags = IndexedElement.stringToFlag(attrs, 0);
0673: }
0674:
0675: IndexedClass c = IndexedClass.create(this , clz, fqn, fileUrl,
0676: require, attrs, flags);
0677:
0678: return c;
0679: }
0680:
0681: // List of String[2]: 0: requirename, 1: fqn
0682: public Set<String[]> getRequires(final String name,
0683: final NameKind kind) {
0684: final Set<SearchResult> result = new HashSet<SearchResult>();
0685:
0686: String field = RubyIndexer.FIELD_REQUIRE;
0687:
0688: search(field, name, kind, result);
0689:
0690: // TODO Prune methods to fit my scheme - later make lucene index smarter about how to prune its index search
0691: final Map<String, String> fqns = new HashMap<String, String>();
0692:
0693: for (SearchResult map : result) {
0694: String[] r = map.getValues(field);
0695:
0696: if (r != null) {
0697: for (String require : r) {
0698: // Lucene returns some inexact matches, TODO investigate why this is necessary
0699: if (kind == NameKind.PREFIX
0700: && !require.startsWith(name)) {
0701: continue;
0702: } else if (kind == NameKind.CASE_INSENSITIVE_PREFIX
0703: && !require.regionMatches(true, 0, name, 0,
0704: name.length())) {
0705: continue;
0706: }
0707: assert map != null;
0708:
0709: // TODO - check if there's a rubygem which captures this
0710: // require and if so, remove it
0711: String fqn = map
0712: .getValue(RubyIndexer.FIELD_FQN_NAME);
0713:
0714: String there = fqns.get(require);
0715:
0716: if ((fqn != null)
0717: && ((there == null) || ((there != null) && (there
0718: .length() < fqn.length())))) {
0719: fqns.put(require, fqn);
0720: }
0721: }
0722: }
0723: }
0724:
0725: final Set<String[]> requires = new HashSet<String[]>();
0726:
0727: for (String require : fqns.keySet()) {
0728: String fqn = fqns.get(require);
0729: String[] item = new String[2];
0730: item[0] = require;
0731: item[1] = fqn;
0732: requires.add(item);
0733: }
0734:
0735: return requires;
0736: }
0737:
0738: public Set<String> getRequiresTransitively(Set<String> requires) {
0739: // Not yet implemented - this requires me to index the require-statements in the files
0740: return requires;
0741: }
0742:
0743: // List of String[2]: 0: requirename, 1: fqn
0744: public Set<String> getClassesIn(final String require) {
0745: final Set<SearchResult> result = new HashSet<SearchResult>();
0746:
0747: String field = RubyIndexer.FIELD_REQUIRE;
0748:
0749: search(field, require, NameKind.EXACT_NAME, result);
0750:
0751: final Set<String> fqns = new HashSet<String>();
0752:
0753: for (SearchResult map : result) {
0754: String fqn = map.getValue(RubyIndexer.FIELD_FQN_NAME);
0755:
0756: if (fqn != null) {
0757: fqns.add(fqn);
0758: }
0759: }
0760:
0761: return fqns;
0762: }
0763:
0764: public IndexedClass getSuperclass(String fqn) {
0765: final Set<SearchResult> result = new HashSet<SearchResult>();
0766:
0767: NameKind kind = NameKind.EXACT_NAME;
0768: String field = RubyIndexer.FIELD_FQN_NAME;
0769:
0770: search(field, fqn, kind, result);
0771:
0772: // XXX Uhm... there could be multiple... Shouldn't I return a set here?
0773: // (e.g. you can have your own class named File which has nothing to
0774: // do with the builtin, and has a separate super class...
0775:
0776: for (SearchResult map : result) {
0777: assert fqn.equals(map.getValue(RubyIndexer.FIELD_FQN_NAME));
0778:
0779: String extendsClass = map
0780: .getValue(RubyIndexer.FIELD_EXTENDS_NAME);
0781:
0782: if (extendsClass != null) {
0783: // Found the class name, now look it up in the index
0784: result.clear();
0785:
0786: if (!search(field, extendsClass, kind, result)) {
0787: return null;
0788: }
0789:
0790: // There should be exactly one match
0791: if (result.size() > 0) {
0792: SearchResult super Map = result.iterator().next();
0793: String super Fqn = super Map
0794: .getValue(RubyIndexer.FIELD_FQN_NAME);
0795:
0796: return createClass(super Fqn, extendsClass, super Map);
0797: } else {
0798: return null;
0799: }
0800: }
0801: }
0802:
0803: return null;
0804: }
0805:
0806: private boolean addSubclasses(String classFqn,
0807: Set<IndexedClass> classes, Set<String> seenClasses,
0808: Set<String> scannedClasses, boolean directOnly) {
0809: // Prevent problems with circular includes or redundant includes
0810: if (scannedClasses.contains(classFqn)) {
0811: return false;
0812: }
0813:
0814: scannedClasses.add(classFqn);
0815:
0816: String searchField = RubyIndexer.FIELD_EXTENDS_NAME;
0817:
0818: Set<SearchResult> result = new HashSet<SearchResult>();
0819:
0820: search(searchField, classFqn, NameKind.EXACT_NAME, result);
0821:
0822: boolean foundIt = result.size() > 0;
0823:
0824: // If this is a bogus class entry (no search rsults) don't continue
0825: if (!foundIt) {
0826: return foundIt;
0827: }
0828:
0829: for (SearchResult map : result) {
0830: String fqn = map.getValue(RubyIndexer.FIELD_FQN_NAME);
0831: if (!seenClasses.contains(fqn)) {
0832: IndexedClass clz = createClass(fqn, null, map);
0833: classes.add(clz);
0834: seenClasses.add(fqn);
0835:
0836: if (!directOnly) {
0837: addSubclasses(fqn, classes, seenClasses,
0838: scannedClasses, directOnly);
0839: }
0840: }
0841: }
0842:
0843: return foundIt;
0844: }
0845:
0846: /** Find the subclasses of the given class name, with the POSSIBLE fqn from the
0847: * context of the usage. */
0848: public Set<IndexedClass> getSubClasses(String fqn,
0849: String possibleFqn, String name, boolean directOnly) {
0850: //String field = RubyIndexer.FIELD_FQN_NAME;
0851: Set<IndexedClass> classes = new HashSet<IndexedClass>();
0852: Set<String> scannedClasses = new HashSet<String>();
0853: Set<String> seenClasses = new HashSet<String>();
0854:
0855: if (fqn != null) {
0856: addSubclasses(fqn, classes, seenClasses, scannedClasses,
0857: directOnly);
0858: } else {
0859: fqn = possibleFqn;
0860:
0861: // Try looking at the libraries too
0862: while ((classes.size() == 0) && (fqn.length() > 0)) {
0863: // TODO - use the boolvalue from addclasses instead!
0864: boolean found = addSubclasses(fqn + "::" + name,
0865: classes, seenClasses, scannedClasses,
0866: directOnly);
0867: if (found) {
0868: return classes;
0869: }
0870:
0871: int f = fqn.lastIndexOf("::");
0872:
0873: if (f == -1) {
0874: break;
0875: } else {
0876: fqn = fqn.substring(0, f);
0877: }
0878: }
0879:
0880: if (classes.size() == 0) {
0881: addSubclasses(name, classes, seenClasses,
0882: scannedClasses, directOnly);
0883: }
0884: }
0885:
0886: return classes;
0887: }
0888:
0889: /** Return the most distant method in the hierarchy that is overriding the given method, or null
0890: * @todo Make this method actually compute most distant ancestor
0891: * @todo Use arglist arity comparison to reject methods that are not overrides...
0892: */
0893: public IndexedMethod getOverridingMethod(String className,
0894: String methodName) {
0895: Set<IndexedMethod> methods = getInheritedMethods(className,
0896: methodName, NameKind.EXACT_NAME);
0897:
0898: // TODO - this is only returning ONE match, not the most distant one. I really need to
0899: // produce a RubyIndex method for this which can walk in there and do a decent job!
0900:
0901: for (IndexedMethod method : methods) {
0902: // getInheritedMethods may return methods ON fqn itself
0903: if (!method.getIn().equals(className)) {
0904: return method;
0905: }
0906: }
0907:
0908: return null;
0909: }
0910:
0911: /**
0912: * Get the set of inherited (through super classes and mixins) for the given fully qualified class name.
0913: * @param classFqn FQN: module1::module2::moduleN::class
0914: * @param prefix If kind is NameKind.PREFIX/CASE_INSENSITIVE_PREFIX, a prefix to filter methods by. Else,
0915: * if kind is NameKind.EXACT_NAME filter methods by the exact name.
0916: * @param kind Whether the prefix field should be taken as a prefix or a whole name
0917: */
0918: public Set<IndexedMethod> getInheritedMethods(String classFqn,
0919: String prefix, NameKind kind) {
0920: boolean haveRedirected = false;
0921:
0922: if ((classFqn == null) || classFqn.equals(OBJECT)) {
0923: // Redirect inheritance tree to Class to pick up methods in Class and Module
0924: classFqn = CLASS;
0925: haveRedirected = true;
0926: } else if (MODULE.equals(classFqn) || CLASS.equals(classFqn)) {
0927: haveRedirected = true;
0928: }
0929:
0930: //String field = RubyIndexer.FIELD_FQN_NAME;
0931: Set<IndexedMethod> methods = new HashSet<IndexedMethod>();
0932: Set<String> scannedClasses = new HashSet<String>();
0933: Set<String> seenSignatures = new HashSet<String>();
0934:
0935: if (prefix == null) {
0936: prefix = "";
0937: }
0938:
0939: addMethodsFromClass(prefix, kind, classFqn, methods,
0940: seenSignatures, scannedClasses, haveRedirected, false);
0941:
0942: return methods;
0943: }
0944:
0945: /** Return whether the specific class referenced (classFqn) was found or not. This is
0946: * not the same as returning whether any classes were added since it may add
0947: * additional methods from parents (Object/Class).
0948: */
0949: private boolean addMethodsFromClass(String prefix, NameKind kind,
0950: String classFqn, Set<IndexedMethod> methods,
0951: Set<String> seenSignatures, Set<String> scannedClasses,
0952: boolean haveRedirected, boolean inheriting) {
0953: // Prevent problems with circular includes or redundant includes
0954: if (scannedClasses.contains(classFqn)) {
0955: return false;
0956: }
0957:
0958: scannedClasses.add(classFqn);
0959:
0960: String searchField = RubyIndexer.FIELD_FQN_NAME;
0961:
0962: Set<SearchResult> result = new HashSet<SearchResult>();
0963:
0964: search(searchField, classFqn, NameKind.EXACT_NAME, result);
0965:
0966: boolean foundIt = result.size() > 0;
0967:
0968: // If this is a bogus class entry (no search rsults) don't continue
0969: if (!foundIt) {
0970: return foundIt;
0971: }
0972:
0973: String extendsClass = null;
0974:
0975: String classIn = null;
0976: int fqnIndex = classFqn.lastIndexOf("::"); // NOI18N
0977:
0978: if (fqnIndex != -1) {
0979: classIn = classFqn.substring(0, fqnIndex);
0980: }
0981:
0982: for (SearchResult map : result) {
0983: assert map != null;
0984:
0985: if (extendsClass == null) {
0986: extendsClass = map
0987: .getValue(RubyIndexer.FIELD_EXTENDS_NAME);
0988: }
0989:
0990: String includes = map.getValue(RubyIndexer.FIELD_INCLUDES);
0991:
0992: if (includes != null) {
0993: String[] in = includes.split(",");
0994:
0995: // I have Util::BacktraceFilter and Assertions, which are both
0996: // relative to ::,Test,Test::Unit
0997: for (String include : in) {
0998: // Try both with and without a package qualifier
0999: boolean isQualified = false;
1000:
1001: if (classIn != null) {
1002: isQualified = addMethodsFromClass(prefix, kind,
1003: classIn + "::" + include, methods,
1004: seenSignatures, scannedClasses,
1005: haveRedirected, true);
1006: }
1007:
1008: if (!isQualified) {
1009: addMethodsFromClass(prefix, kind, include,
1010: methods, seenSignatures,
1011: scannedClasses, haveRedirected, true);
1012: }
1013: }
1014: }
1015:
1016: String extendWith = map
1017: .getValue(RubyIndexer.FIELD_EXTEND_WITH);
1018:
1019: if (extendWith != null) {
1020: // Try both with and without a package qualifier
1021: boolean isQualified = false;
1022:
1023: if (classIn != null) {
1024: isQualified = addMethodsFromClass(prefix, kind,
1025: classIn + "::" + extendWith, methods,
1026: seenSignatures, scannedClasses,
1027: haveRedirected, true);
1028: }
1029:
1030: if (!isQualified) {
1031: addMethodsFromClass(prefix, kind, extendWith,
1032: methods, seenSignatures, scannedClasses,
1033: haveRedirected, true);
1034: }
1035: }
1036:
1037: String[] signatures = map
1038: .getValues(RubyIndexer.FIELD_METHOD_NAME);
1039:
1040: if (signatures != null) {
1041: for (String signature : signatures) {
1042: // Skip weird methods like "[]" etc. in completion lists... TODO Think harder about this
1043: if ((prefix.length() == 0)
1044: && !Character.isLowerCase(signature
1045: .charAt(0))) {
1046: continue;
1047: }
1048:
1049: // Prevent duplicates when method is redefined
1050: if (!seenSignatures.contains(signature)) {
1051: if (signature.startsWith(prefix)) {
1052: if (kind == NameKind.EXACT_NAME) {
1053: // Ensure that the method is not longer than the prefix
1054: if ((signature.length() > prefix
1055: .length())
1056: && (signature.charAt(prefix
1057: .length()) != '(')
1058: && (signature.charAt(prefix
1059: .length()) != ';')) {
1060: continue;
1061: }
1062: } else {
1063: // REGEXP, CAMELCASE filtering etc. not supported here
1064: assert (kind == NameKind.PREFIX)
1065: || (kind == NameKind.CASE_INSENSITIVE_PREFIX);
1066: }
1067:
1068: seenSignatures.add(signature);
1069:
1070: IndexedMethod method = createMethod(
1071: signature, map, inheriting);
1072: method.setSmart(!haveRedirected);
1073: methods.add(method);
1074: }
1075: }
1076: }
1077: }
1078:
1079: String[] attributes = map
1080: .getValues(RubyIndexer.FIELD_ATTRIBUTE_NAME);
1081:
1082: if (attributes != null) {
1083: for (String attribute : attributes) {
1084: // Skip weird methods like "[]" etc. in completion lists... TODO Think harder about this
1085: if ((prefix.length() == 0)
1086: && !Character.isLowerCase(attribute
1087: .charAt(0))) {
1088: continue;
1089: }
1090:
1091: // Prevent duplicates when method is redefined
1092: if (!seenSignatures.contains(attribute)) {
1093: if (attribute.startsWith(prefix)) {
1094: if (kind == NameKind.EXACT_NAME) {
1095: // Ensure that the method is not longer than the prefix
1096: if ((attribute.length() > prefix
1097: .length())
1098: && (attribute.charAt(prefix
1099: .length()) != '(')
1100: && (attribute.charAt(prefix
1101: .length()) != ';')) {
1102: continue;
1103: }
1104: } else {
1105: // REGEXP, CAMELCASE filtering etc. not supported here
1106: assert (kind == NameKind.PREFIX)
1107: || (kind == NameKind.CASE_INSENSITIVE_PREFIX);
1108: }
1109:
1110: seenSignatures.add(attribute);
1111:
1112: // TODO - create both getter and setter methods
1113: IndexedMethod method = createMethod(
1114: attribute, map, inheriting);
1115: method.setSmart(!haveRedirected);
1116: method
1117: .setMethodType(IndexedMethod.MethodType.ATTRIBUTE);
1118: methods.add(method);
1119: }
1120: }
1121: }
1122: }
1123: }
1124:
1125: if (classFqn.equals(OBJECT)) {
1126: return foundIt;
1127: }
1128:
1129: if (extendsClass == null) {
1130: if (haveRedirected) {
1131: addMethodsFromClass(prefix, kind, OBJECT, methods,
1132: seenSignatures, scannedClasses, true, true);
1133: } else {
1134: // Rather than inheriting directly from object,
1135: // let's go via Class (and Module) up to Object
1136: addMethodsFromClass(prefix, kind, CLASS, methods,
1137: seenSignatures, scannedClasses, true, true);
1138: }
1139: } else {
1140: if ("ActiveRecord::Base".equals(extendsClass)) { // NOI18N
1141: // Add in database fields as well
1142: addDatabaseProperties(prefix, kind, classFqn, methods);
1143: }
1144:
1145: // We're not sure we have a fully qualified path, so try some different candidates
1146: if (!addMethodsFromClass(prefix, kind, extendsClass,
1147: methods, seenSignatures, scannedClasses,
1148: haveRedirected, true)) {
1149: // Search by classIn
1150: String fqn = classIn;
1151:
1152: while (fqn != null) {
1153: if (addMethodsFromClass(prefix, kind, fqn + "::"
1154: + extendsClass, methods, seenSignatures,
1155: scannedClasses, haveRedirected, true)) {
1156: break;
1157: }
1158:
1159: int f = fqn.lastIndexOf("::"); // NOI18N
1160:
1161: if (f == -1) {
1162: break;
1163: } else {
1164: fqn = fqn.substring(0, f);
1165: }
1166: }
1167: }
1168: }
1169:
1170: return foundIt;
1171: }
1172:
1173: private void addDatabaseProperties(String prefix, NameKind kind,
1174: String classFqn, Set<IndexedMethod> methods) {
1175: // Query index for database related properties
1176: if (classFqn.indexOf("::") != -1) {
1177: // Don't know how to handle this scenario
1178: return;
1179: }
1180:
1181: String tableName = RubyUtils.tableize(classFqn);
1182:
1183: String searchField = RubyIndexer.FIELD_DB_TABLE;
1184: Set<SearchResult> result = new HashSet<SearchResult>();
1185: search(searchField, tableName, NameKind.EXACT_NAME, result);
1186:
1187: List<TableDefinition> tableDefs = new ArrayList<TableDefinition>();
1188: TableDefinition schema = null;
1189:
1190: for (SearchResult map : result) {
1191: assert map != null;
1192:
1193: String version = map.getValue(RubyIndexer.FIELD_DB_VERSION);
1194: assert tableName.equals(map
1195: .getValue(RubyIndexer.FIELD_DB_TABLE));
1196: String fileUrl = map.getPersistentUrl();
1197:
1198: TableDefinition def = new TableDefinition(tableName,
1199: version, fileUrl);
1200: tableDefs.add(def);
1201: String[] columns = map
1202: .getValues(RubyIndexer.FIELD_DB_COLUMN);
1203:
1204: if (columns != null) {
1205: for (String column : columns) {
1206: // TODO - do this filtering AFTER applying diffs when
1207: // I'm doing renaming of columns etc.
1208: def.addColumn(column);
1209: }
1210: }
1211:
1212: if (RubyIndexer.SCHEMA_INDEX_VERSION.equals(version)) {
1213: schema = def;
1214: // With a schema I don't need to look at anything else
1215: break;
1216: }
1217: }
1218:
1219: if (tableDefs.size() > 0) {
1220: Map<String, String> columnDefs = new HashMap<String, String>();
1221: Map<String, String> fileUrls = new HashMap<String, String>();
1222: Set<String> currentCols = new HashSet<String>();
1223: if (schema != null) {
1224: List<String> cols = schema.getColumns();
1225: if (cols != null) {
1226: for (String col : cols) {
1227: int typeIndex = col.indexOf(';');
1228: if (typeIndex != -1) {
1229: String name = col.substring(0, typeIndex);
1230: if (typeIndex < col.length() - 1
1231: && col.charAt(typeIndex + 1) == '-') {
1232: // Removing column - this is unlikely in a
1233: // schema.rb file!
1234: currentCols.remove(col);
1235: } else {
1236: currentCols.add(name);
1237: fileUrls.put(col, schema.getFileUrl());
1238: columnDefs.put(name, col);
1239: }
1240: } else {
1241: currentCols.add(col);
1242: columnDefs.put(col, col);
1243: fileUrls.put(col, schema.getFileUrl());
1244: }
1245: }
1246: }
1247: } else {
1248: // Apply migration files
1249: Collections.sort(tableDefs);
1250: for (TableDefinition def : tableDefs) {
1251: List<String> cols = def.getColumns();
1252: if (cols == null) {
1253: continue;
1254: }
1255:
1256: for (String col : cols) {
1257: int typeIndex = col.indexOf(';');
1258: if (typeIndex != -1) {
1259: String name = col.substring(0, typeIndex);
1260: if (typeIndex < col.length() - 1
1261: && col.charAt(typeIndex + 1) == '-') {
1262: // Removing column
1263: currentCols.remove(name);
1264: } else {
1265: currentCols.add(name);
1266: fileUrls.put(col, def.getFileUrl());
1267: columnDefs.put(name, col);
1268: }
1269: } else {
1270: currentCols.add(col);
1271: columnDefs.put(col, col);
1272: fileUrls.put(col, def.getFileUrl());
1273: }
1274: }
1275: }
1276: }
1277:
1278: // Finally, we've "applied" the migrations - just walk
1279: // through the datastructure and create completion matches
1280: // as appropriate
1281: for (String column : currentCols) {
1282: if (column.startsWith(prefix)) {
1283: if (kind == NameKind.EXACT_NAME) {
1284: // Ensure that the method is not longer than the prefix
1285: if ((column.length() > prefix.length())) {
1286: continue;
1287: }
1288: } else {
1289: // REGEXP, CAMELCASE filtering etc. not supported here
1290: assert (kind == NameKind.PREFIX)
1291: || (kind == NameKind.CASE_INSENSITIVE_PREFIX);
1292: }
1293:
1294: String c = columnDefs.get(column);
1295: String type = tableName;
1296: int semicolonIndex = c.indexOf(';');
1297: if (semicolonIndex != -1) {
1298: type = c.substring(semicolonIndex + 1);
1299: }
1300: String fileUrl = fileUrls.get(column);
1301:
1302: String signature = column;
1303: String fqn = tableName + "#" + column;
1304: String clz = type;
1305: String require = null;
1306: String attributes = "";
1307: int flags = 0;
1308:
1309: IndexedMethod method = IndexedMethod.create(this ,
1310: signature, fqn, clz, fileUrl, require,
1311: attributes, flags);
1312: method
1313: .setMethodType(IndexedMethod.MethodType.DBCOLUMN);
1314: method.setSmart(true);
1315: methods.add(method);
1316: }
1317: }
1318:
1319: if ("find_by_".startsWith(prefix)
1320: || "find_all_by".startsWith(prefix)) {
1321: // Generate dynamic finders
1322: for (String column : currentCols) {
1323: String methodOneName = "find_by_" + column;
1324: String methodAllName = "find_all_by_" + column;
1325: if (methodOneName.startsWith(prefix)
1326: || methodAllName.startsWith(prefix)) {
1327: if (kind == NameKind.EXACT_NAME) {
1328: // XXX methodOneName || methodAllName?
1329: // Ensure that the method is not longer than the prefix
1330: if ((column.length() > prefix.length())) {
1331: continue;
1332: }
1333: } else {
1334: // REGEXP, CAMELCASE filtering etc. not supported here
1335: assert (kind == NameKind.PREFIX)
1336: || (kind == NameKind.CASE_INSENSITIVE_PREFIX);
1337: }
1338:
1339: String type = columnDefs.get(column);
1340: type = type.substring(type.indexOf(';') + 1);
1341: String fileUrl = fileUrls.get(column);
1342:
1343: String clz = classFqn;
1344: String require = null;
1345: int flags = IndexedElement.STATIC;
1346: String attributes = IndexedElement
1347: .flagToString(flags)
1348: + ";;;"
1349: + "options(:first|:all),args(=>conditions|order|group|limit|offset|joins|readonly:bool|include|select|from|readonly:bool|lock:bool)";
1350:
1351: if (methodOneName.startsWith(prefix)) {
1352: String signature = methodOneName + "("
1353: + column + ",*options)";
1354: String fqn = tableName + "#" + signature;
1355: IndexedMethod method = IndexedMethod
1356: .create(this , signature, fqn, clz,
1357: fileUrl, require,
1358: attributes, flags);
1359: method.setInherited(false);
1360: method.setSmart(true);
1361: methods.add(method);
1362: }
1363: if (methodAllName.startsWith(prefix)) {
1364: String signature = methodAllName + "("
1365: + column + ",*options)";
1366: String fqn = tableName + "#" + signature;
1367: IndexedMethod method = IndexedMethod
1368: .create(this , signature, fqn, clz,
1369: fileUrl, require,
1370: attributes, flags);
1371: method.setInherited(false);
1372: method.setSmart(true);
1373: methods.add(method);
1374: }
1375: }
1376: }
1377:
1378: }
1379: }
1380: }
1381:
1382: private class TableDefinition implements
1383: Comparable<TableDefinition> {
1384: private String version;
1385: /** table is redundant, I only search by exact tablenames anyway */
1386: private String table;
1387: private String fileUrl;
1388: private List<String> cols;
1389:
1390: TableDefinition(String table, String version, String fileUrl) {
1391: this .table = table;
1392: this .version = version;
1393: this .fileUrl = fileUrl;
1394: }
1395:
1396: public int compareTo(RubyIndex.TableDefinition o) {
1397: // I can do string comparisons here because the strings
1398: // are all padded with zeroes on the left (so 100 is going
1399: // to be greater than 099, which wouldn't be true for "99".)
1400: return version.compareTo(o.version);
1401: }
1402:
1403: String getFileUrl() {
1404: return fileUrl;
1405: }
1406:
1407: void addColumn(String column) {
1408: if (cols == null) {
1409: cols = new ArrayList<String>();
1410: }
1411:
1412: cols.add(column);
1413: }
1414:
1415: List<String> getColumns() {
1416: return cols;
1417: }
1418: }
1419:
1420: public Set<String> getDatabaseTables(String prefix, NameKind kind) {
1421: // Query index for database related properties
1422:
1423: String searchField = RubyIndexer.FIELD_DB_TABLE;
1424: Set<SearchResult> result = new HashSet<SearchResult>();
1425: search(searchField, prefix, kind, result);
1426:
1427: Set<String> tables = new HashSet<String>();
1428: for (SearchResult map : result) {
1429: assert map != null;
1430:
1431: String tableName = map.getValue(RubyIndexer.FIELD_DB_TABLE);
1432: if (tableName != null) {
1433: tables.add(tableName);
1434: }
1435: }
1436:
1437: return tables;
1438: }
1439:
1440: public Set<IndexedField> getInheritedFields(String classFqn,
1441: String prefix, NameKind kind, boolean inherited) {
1442: boolean haveRedirected = false;
1443:
1444: if ((classFqn == null) || classFqn.equals(OBJECT)) {
1445: // Redirect inheritance tree to Class to pick up methods in Class and Module
1446: classFqn = CLASS;
1447: haveRedirected = true;
1448: } else if (MODULE.equals(classFqn) || CLASS.equals(classFqn)) {
1449: haveRedirected = true;
1450: }
1451:
1452: //String field = RubyIndexer.FIELD_FQN_NAME;
1453: Set<IndexedField> members = new HashSet<IndexedField>();
1454: Set<String> scannedClasses = new HashSet<String>();
1455: Set<String> seenSignatures = new HashSet<String>();
1456:
1457: boolean instanceVars = true;
1458: if (prefix == null) {
1459: prefix = "";
1460: } else if (prefix.startsWith("@@")) {
1461: instanceVars = false;
1462: prefix = prefix.substring(2);
1463: } else if (prefix.startsWith("@")) {
1464: prefix = prefix.substring(1);
1465: }
1466:
1467: addFieldsFromClass(prefix, kind, classFqn, members,
1468: seenSignatures, scannedClasses, haveRedirected,
1469: instanceVars, inherited);
1470:
1471: return members;
1472: }
1473:
1474: /** Return whether the specific class referenced (classFqn) was found or not. This is
1475: * not the same as returning whether any classes were added since it may add
1476: * additional methods from parents (Object/Class).
1477: */
1478: private boolean addFieldsFromClass(String prefix, NameKind kind,
1479: String classFqn, Set<IndexedField> methods,
1480: Set<String> seenSignatures, Set<String> scannedClasses,
1481: boolean haveRedirected, boolean instanceVars,
1482: boolean inheriting) {
1483: // Prevent problems with circular includes or redundant includes
1484: if (scannedClasses.contains(classFqn)) {
1485: return false;
1486: }
1487:
1488: scannedClasses.add(classFqn);
1489:
1490: String searchField = RubyIndexer.FIELD_FQN_NAME;
1491:
1492: Set<SearchResult> result = new HashSet<SearchResult>();
1493:
1494: search(searchField, classFqn, NameKind.EXACT_NAME, result);
1495:
1496: boolean foundIt = result.size() > 0;
1497:
1498: // If this is a bogus class entry (no search rsults) don't continue
1499: if (!foundIt) {
1500: return foundIt;
1501: }
1502:
1503: String extendsClass = null;
1504:
1505: String classIn = null;
1506: int fqnIndex = classFqn.lastIndexOf("::"); // NOI18N
1507:
1508: if (fqnIndex != -1) {
1509: classIn = classFqn.substring(0, fqnIndex);
1510: }
1511:
1512: for (SearchResult map : result) {
1513: assert map != null;
1514:
1515: if (extendsClass == null) {
1516: extendsClass = map
1517: .getValue(RubyIndexer.FIELD_EXTENDS_NAME);
1518: }
1519:
1520: String includes = map.getValue(RubyIndexer.FIELD_INCLUDES);
1521:
1522: if (includes != null) {
1523: String[] in = includes.split(",");
1524:
1525: // I have Util::BacktraceFilter and Assertions, which are both
1526: // relative to ::,Test,Test::Unit
1527: for (String include : in) {
1528: // Try both with and without a package qualifier
1529: boolean isQualified = false;
1530:
1531: if (classIn != null) {
1532: isQualified = addFieldsFromClass(prefix, kind,
1533: classIn + "::" + include, methods,
1534: seenSignatures, scannedClasses,
1535: haveRedirected, instanceVars, true);
1536: }
1537:
1538: if (!isQualified) {
1539: addFieldsFromClass(prefix, kind, include,
1540: methods, seenSignatures,
1541: scannedClasses, haveRedirected,
1542: instanceVars, true);
1543: }
1544: }
1545: }
1546:
1547: String extendWith = map
1548: .getValue(RubyIndexer.FIELD_EXTEND_WITH);
1549:
1550: if (extendWith != null) {
1551: // Try both with and without a package qualifier
1552: boolean isQualified = false;
1553:
1554: if (classIn != null) {
1555: isQualified = addFieldsFromClass(prefix, kind,
1556: classIn + "::" + extendWith, methods,
1557: seenSignatures, scannedClasses,
1558: haveRedirected, instanceVars, true);
1559: }
1560:
1561: if (!isQualified) {
1562: addFieldsFromClass(prefix, kind, extendWith,
1563: methods, seenSignatures, scannedClasses,
1564: haveRedirected, instanceVars, true);
1565: }
1566: }
1567:
1568: String[] fields = map
1569: .getValues(RubyIndexer.FIELD_FIELD_NAME);
1570:
1571: if (fields != null) {
1572: for (String field : fields) {
1573: // Skip weird methods like "[]" etc. in completion lists... TODO Think harder about this
1574: if ((prefix.length() == 0)
1575: && !Character.isLowerCase(field.charAt(0))) {
1576: continue;
1577: }
1578:
1579: // Prevent duplicates when method is redefined
1580: if (!seenSignatures.contains(field)) {
1581: // See if we need instancevars or classvars
1582: boolean isInstance = true;
1583: int signatureIndex = field.indexOf(';');
1584: if (signatureIndex != -1
1585: && field.indexOf('s',
1586: signatureIndex + 1) != -1) {
1587: isInstance = false;
1588: }
1589: if (isInstance != instanceVars) {
1590: continue;
1591: }
1592:
1593: if (field.startsWith(prefix)) {
1594: if (kind == NameKind.EXACT_NAME) {
1595: // Ensure that the method is not longer than the prefix
1596: if ((field.length() > prefix.length())
1597: && (field.charAt(prefix
1598: .length()) != '(')
1599: && (field.charAt(prefix
1600: .length()) != ';')) {
1601: continue;
1602: }
1603: } else {
1604: // REGEXP, CAMELCASE filtering etc. not supported here
1605: assert (kind == NameKind.PREFIX)
1606: || (kind == NameKind.CASE_INSENSITIVE_PREFIX);
1607: }
1608:
1609: seenSignatures.add(field);
1610:
1611: IndexedField f = createField(field, map,
1612: isInstance, inheriting);
1613: f.setSmart(!haveRedirected);
1614: methods.add(f);
1615: }
1616: }
1617: }
1618: }
1619: }
1620:
1621: if (classFqn.equals(OBJECT)) {
1622: return foundIt;
1623: }
1624:
1625: if (extendsClass == null) {
1626: if (haveRedirected) {
1627: addFieldsFromClass(prefix, kind, OBJECT, methods,
1628: seenSignatures, scannedClasses, true,
1629: instanceVars, true);
1630: } else {
1631: // Rather than inheriting directly from object,
1632: // let's go via Class (and Module) up to Object
1633: addFieldsFromClass(prefix, kind, CLASS, methods,
1634: seenSignatures, scannedClasses, true,
1635: instanceVars, true);
1636: }
1637: } else {
1638: // We're not sure we have a fully qualified path, so try some different candidates
1639: if (!addFieldsFromClass(prefix, kind, extendsClass,
1640: methods, seenSignatures, scannedClasses,
1641: haveRedirected, instanceVars, true)) {
1642: // Search by classIn
1643: String fqn = classIn;
1644:
1645: while (fqn != null) {
1646: if (addFieldsFromClass(prefix, kind, fqn + "::"
1647: + extendsClass, methods, seenSignatures,
1648: scannedClasses, haveRedirected,
1649: instanceVars, true)) {
1650: break;
1651: }
1652:
1653: int f = fqn.lastIndexOf("::"); // NOI18N
1654:
1655: if (f == -1) {
1656: break;
1657: } else {
1658: fqn = fqn.substring(0, f);
1659: }
1660: }
1661: }
1662: }
1663:
1664: return foundIt;
1665: }
1666:
1667: /** Return all the method or class definitions for the given FQN that are documented. */
1668: public Set<? extends IndexedElement> getDocumented(final String fqn) {
1669: assert (fqn != null) && (fqn.length() > 0);
1670:
1671: int hashIndex = fqn.indexOf('#');
1672:
1673: if (hashIndex == -1) {
1674: // Looking for a class or a module
1675: return getDocumentedClasses(fqn);
1676: } else {
1677: // Looking for a method
1678: String clz = fqn.substring(0, hashIndex);
1679: String method = fqn.substring(hashIndex + 1);
1680:
1681: return getDocumentedMethods(clz, method);
1682: }
1683: }
1684:
1685: private Set<IndexedClass> getDocumentedClasses(final String fqn) {
1686: final Set<SearchResult> result = new HashSet<SearchResult>();
1687: String field = RubyIndexer.FIELD_FQN_NAME;
1688:
1689: search(field, fqn, NameKind.EXACT_NAME, result);
1690:
1691: Set<IndexedClass> matches = new HashSet<IndexedClass>();
1692:
1693: for (SearchResult map : result) {
1694: assert map != null;
1695:
1696: String attributes = map
1697: .getValue(RubyIndexer.FIELD_CLASS_ATTRS);
1698:
1699: if (attributes != null) {
1700: int flags = IndexedElement.stringToFlag(attributes, 0);
1701: if ((flags & IndexedElement.DOCUMENTED) != 0) {
1702: matches.add(createClass(fqn, null, map));
1703: }
1704: }
1705: }
1706:
1707: return matches;
1708: }
1709:
1710: private Set<IndexedMethod> getDocumentedMethods(final String fqn,
1711: String method) {
1712: final Set<SearchResult> result = new HashSet<SearchResult>();
1713: String field = RubyIndexer.FIELD_FQN_NAME;
1714:
1715: search(field, fqn, NameKind.EXACT_NAME, result);
1716:
1717: Set<IndexedMethod> matches = new HashSet<IndexedMethod>();
1718:
1719: for (SearchResult map : result) {
1720: String[] signatures = map
1721: .getValues(RubyIndexer.FIELD_METHOD_NAME);
1722:
1723: if (signatures != null) {
1724: for (String signature : signatures) {
1725: // Skip weird methods... Think harder about this
1726: if (((method == null) || (method.length() == 0))
1727: && !Character.isLowerCase(signature
1728: .charAt(0))) {
1729: continue;
1730: }
1731:
1732: if (!signature.startsWith(method)) {
1733: continue;
1734: }
1735:
1736: // Make sure the name matches exactly
1737: // We know that the prefix is correct from the first part of
1738: // this if clause, by the signature may have more
1739: if (((signature.length() > method.length()) && (signature
1740: .charAt(method.length()) != '('))
1741: && (signature.charAt(method.length()) != ';')) {
1742: continue;
1743: }
1744:
1745: int attributes = signature.indexOf(';', method
1746: .length());
1747: if (attributes == -1) {
1748: continue;
1749: }
1750: int flags = IndexedElement.stringToFlag(signature,
1751: attributes + 1);
1752: if ((flags & IndexedElement.DOCUMENTED) != 0) {
1753: // Method is documented
1754: assert map != null;
1755: matches
1756: .add(createMethod(signature, map, false));
1757: }
1758: }
1759: }
1760:
1761: String[] attribs = map
1762: .getValues(RubyIndexer.FIELD_ATTRIBUTE_NAME);
1763:
1764: if (attribs != null) {
1765: for (String signature : attribs) {
1766: // Skip weird methods... Think harder about this
1767: if (((method == null) || (method.length() == 0))
1768: && !Character.isLowerCase(signature
1769: .charAt(0))) {
1770: continue;
1771: }
1772:
1773: if (!signature.startsWith(method)) {
1774: continue;
1775: }
1776:
1777: // Make sure the name matches exactly
1778: // We know that the prefix is correct from the first part of
1779: // this if clause, by the signature may have more
1780: if (((signature.length() > method.length()) &&
1781: //(signature.charAt(method.length()) != '(')) &&
1782: (signature.charAt(method.length()) != ';'))) {
1783: continue;
1784: }
1785:
1786: // TODO - index whether attributes are documented!
1787: //int attributes = signature.indexOf(';', method.length());
1788: //
1789: //if (attributes == -1) {
1790: // continue;
1791: //}
1792: //
1793: //if (signature.indexOf('d', attributes + 1) != -1) {
1794: // // Method is documented
1795: assert map != null;
1796: matches.add(createMethod(signature, map, false));
1797: //}
1798: }
1799: }
1800: }
1801:
1802: return matches;
1803: }
1804:
1805: /** Return the file url corresponding to the given require statement */
1806: public String getRequiredFileUrl(final String require) {
1807: final Set<SearchResult> result = new HashSet<SearchResult>();
1808:
1809: String field = RubyIndexer.FIELD_REQUIRE;
1810:
1811: search(field, require, NameKind.EXACT_NAME, result);
1812:
1813: // TODO Prune methods to fit my scheme - later make lucene index smarter about how to prune its index search
1814: for (SearchResult map : result) {
1815: String file = map.getPersistentUrl();
1816:
1817: if (file != null) {
1818: return file;
1819: }
1820: }
1821:
1822: return null;
1823: }
1824:
1825: static String getClusterUrl() {
1826: if (clusterUrl == null) {
1827: File f = InstalledFileLocator.getDefault().locate(
1828: "modules/org-netbeans-modules-ruby.jar", null,
1829: false); // NOI18N
1830:
1831: if (f == null) {
1832: throw new RuntimeException("Can't find cluster");
1833: }
1834:
1835: f = new File(f.getParentFile().getParentFile()
1836: .getAbsolutePath());
1837:
1838: try {
1839: f = f.getCanonicalFile();
1840: clusterUrl = f.toURI().toURL().toExternalForm();
1841: } catch (IOException ioe) {
1842: Exceptions.printStackTrace(ioe);
1843: }
1844: }
1845:
1846: return clusterUrl;
1847: }
1848:
1849: // For testing only
1850: static void setClusterUrl(String url) {
1851: clusterUrl = url;
1852: }
1853:
1854: static String getPreindexUrl(String url) {
1855: if (RubyIndexer.PREINDEXING) {
1856: Iterator<RubyPlatform> it = RubyPlatformManager
1857: .platformIterator();
1858: while (it.hasNext()) {
1859: RubyPlatform platform = it.next();
1860: String s = getGemHomeURL(platform);
1861:
1862: if (s != null && url.startsWith(s)) {
1863: return GEM_URL + url.substring(s.length());
1864: }
1865:
1866: s = platform.getHomeUrl();
1867:
1868: if (url.startsWith(s)) {
1869: url = RUBYHOME_URL + url.substring(s.length());
1870:
1871: return url;
1872: }
1873: }
1874: } else {
1875: // FIXME: use right platform
1876: RubyPlatform platform = RubyPlatformManager
1877: .getDefaultPlatform();
1878: String s = getGemHomeURL(platform);
1879:
1880: if (s != null && url.startsWith(s)) {
1881: return GEM_URL + url.substring(s.length());
1882: }
1883:
1884: s = platform.getHomeUrl();
1885:
1886: if (url.startsWith(s)) {
1887: url = RUBYHOME_URL + url.substring(s.length());
1888:
1889: return url;
1890: }
1891: }
1892:
1893: String s = getClusterUrl();
1894:
1895: if (url.startsWith(s)) {
1896: return CLUSTER_URL + url.substring(s.length());
1897: }
1898:
1899: return url;
1900: }
1901:
1902: /** Get the FileObject corresponding to a URL returned from the index */
1903: public static FileObject getFileObject(String url) {
1904: try {
1905: if (url.startsWith(RUBYHOME_URL)) {
1906: // TODO - resolve to correct platform
1907: // FIXME: per-platform now
1908: Iterator<RubyPlatform> it = RubyPlatformManager
1909: .platformIterator();
1910: while (it.hasNext()) {
1911: RubyPlatform platform = it.next();
1912: url = platform.getHomeUrl()
1913: + url.substring(RUBYHOME_URL.length());
1914: FileObject fo = URLMapper.findFileObject(new URL(
1915: url));
1916: if (fo != null) {
1917: return fo;
1918: }
1919: }
1920:
1921: return null;
1922: } else if (url.startsWith(GEM_URL)) {
1923: // FIXME: per-platform now
1924: Iterator<RubyPlatform> it = RubyPlatformManager
1925: .platformIterator();
1926: while (it.hasNext()) {
1927: RubyPlatform platform = it.next();
1928: if (!platform.hasRubyGemsInstalled()) {
1929: continue;
1930: }
1931: url = platform.getGemManager().getGemHomeUrl()
1932: + url.substring(GEM_URL.length());
1933: FileObject fo = URLMapper.findFileObject(new URL(
1934: url));
1935: if (fo != null) {
1936: return fo;
1937: }
1938: }
1939:
1940: return null;
1941: } else if (url.startsWith(CLUSTER_URL)) {
1942: url = getClusterUrl()
1943: + url.substring(CLUSTER_URL.length()); // NOI18N
1944: }
1945:
1946: return URLMapper.findFileObject(new URL(url));
1947: } catch (MalformedURLException mue) {
1948: Exceptions.printStackTrace(mue);
1949: }
1950:
1951: return null;
1952: }
1953:
1954: private static String getGemHomeURL(RubyPlatform platform) {
1955: return platform.hasRubyGemsInstalled() ? platform
1956: .getGemManager().getGemHomeUrl() : null;
1957: }
1958: }
|