001: /*******************************************************************************
002: * Copyright (c) 2000, 2006 IBM Corporation and others.
003: * All rights reserved. This program and the accompanying materials
004: * are made available under the terms of the Eclipse Public License v1.0
005: * which accompanies this distribution, and is available at
006: * http://www.eclipse.org/legal/epl-v10.html
007: *
008: * Contributors:
009: * IBM Corporation - initial API and implementation
010: *******************************************************************************/package org.eclipse.jdt.internal.corext.refactoring.rename;
011:
012: import com.ibm.icu.text.BreakIterator;
013: import java.util.ArrayList;
014: import java.util.List;
015:
016: import org.eclipse.core.runtime.Assert;
017:
018: import org.eclipse.jdt.core.IJavaProject;
019: import org.eclipse.jdt.core.JavaCore;
020:
021: import org.eclipse.jdt.internal.ui.text.JavaWordIterator;
022:
023: /**
024: * This class contains methods for suggesting new names for variables or methods
025: * whose name consists at least partly of the name of their declaring type (or
026: * in case of methods, the return type or a parameter type).
027: *
028: * The methods return the newly suggested method or variable name in case of a
029: * match, or null in case nothing matched.
030: *
031: * In any case, prefixes and suffixes are removed from variable names. As method
032: * names have no configurable suffixes or prefixes, they are left unchanged. The
033: * remaining name is called "stripped element name".
034: *
035: * After the match according to the strategy, prefixes and suffixes are
036: * reapplied to the names.
037: *
038: * EXACT STRATEGY (always performed).
039: * ----------------------------------------------------------------
040: *
041: * The stripped element name is directly compared with the type name:
042: *
043: * a) the first character must match case-insensitive
044: *
045: * b) all other characters must match case-sensitive
046: *
047: * In case of a match, the new type name is returned (first character adapted,
048: * respectively). Suffixes/Prefixes are reapplied.
049: *
050: * Note that this also matches fields with names like "SomeField", "fsomeField",
051: * and method names like "JavaElement()".
052: *
053: * EMBEDDED STRATEGY (performed second if chosen by user).
054: * ----------------------------------------------------------------
055: *
056: * A search is performed in the stripped element name for the old type name:
057: *
058: * a) the first character must match case-insensitive
059: *
060: * b) all other characters must match case-sensitive
061: *
062: * c) the stripped element name must end after the type name, or the next
063: * character must be a non-letter, or the next character must be upper cased.
064: *
065: * In case of a match, the new type is inserted into the stripped element name,
066: * replacing the old type name, first character adapted to the correct case.
067: * Suffixes/Prefixes are reapplied.
068: *
069: * Note that this also matches methods with names like "createjavaElement()" or
070: * fields like "fjavaElementCache".
071: *
072: * SUFFIX STRATEGY (performed third if chosen by user)
073: * ----------------------------------------------------------------
074: *
075: * The new and old type names are analyzed for "camel case suffixes", that is,
076: * substrings which begin with an uppercased letter. For example,
077: * "SimpleJavaElement" is split into the three hunks "Simple",
078: * "Java", and "Element". If one type name has more suffixes than the
079: * other, both are stripped to the smaller size.
080: *
081: * Then, a search is performed in the stripped variable name hunks from back to
082: * front. At least the last hunk must be found, others may then extend the match.
083: * Each hunk must match like in the exact strategy, i.e.
084: *
085: * a) the first character must match case-insensitive
086: *
087: * b) all other characters must match case-sensitive
088: *
089: * In case of a match, the matched hunks of the new type replace
090: * the hunks of the old type. Suffixes/Prefixes are reapplied.
091: *
092: * Note that numbers and other non-letter characters belong to the previous
093: * camel case substring.
094: *
095: *
096: * @since 3.2
097: *
098: */
099: public class RenamingNameSuggestor {
100:
101: /*
102: * ADDITIONAL OPTIONS
103: * ----------------------------------------------------------------
104: *
105: * There are two additional flags which may be set in this class to allow
106: * better matching of special cases:
107: *
108: * a) Special treatment of leading "I"s in type names, i.e. interface names
109: * like "IJavaElement". If the corresponding flag is set, leading "I"s are
110: * stripped from type names if the second char is also uppercase to allow
111: * exact matching of variable names like "javaElement" for type
112: * "IJavaElement". Note that embedded matching already matches cases like
113: * this.
114: *
115: * b) Special treatment of all-uppercase type names or all-uppercase type
116: * name camel-case hunks, i.e. names like "AST" or "PersonalURL". If the
117: * corresponding flag is set, the type name hunks will be transformed such
118: * that variables like "fAst", "ast", "personalUrl", or "url" are found as
119: * well. The target name will be transformed too if it is an
120: * all-uppercase type name camel-case hunk as well.
121: *
122: * NOTE that in exact or embedded mode, the whole type name must be
123: * all-uppercase to allow matching custom-lowercased variable names, i.e.
124: * there are no attempts to "guess" which hunk of the new name should be lowercased
125: * to match a partly lowercased variable name. In suffix mode, hunks of the
126: * new type which are at the same position as in the old type will be
127: * lowercased if necessary.
128: *
129: * c) Support for (english) plural forms. If the corresponding flag is set, the
130: * suggestor will try to match variables which have plural forms of the
131: * type name, for example "handies" for "Handy" or "phones" for "MobilePhone".
132: * The target name will be transformed as well, i.e. conversion like
133: * "fHandies" -> "fPhones" are supported.
134: *
135: */
136:
137: public static final int STRATEGY_EXACT = 1;
138: public static final int STRATEGY_EMBEDDED = 2;
139: public static final int STRATEGY_SUFFIX = 3;
140:
141: private static final String PLURAL_S = "s"; //$NON-NLS-1$
142: private static final String PLURAL_IES = "ies"; //$NON-NLS-1$
143: private static final String SINGULAR_Y = "y"; //$NON-NLS-1$
144:
145: private int fStrategy;
146: private String[] fFieldPrefixes;
147: private String[] fFieldSuffixes;
148: private String[] fStaticFieldPrefixes;
149: private String[] fStaticFieldSuffixes;
150: private String[] fLocalPrefixes;
151: private String[] fLocalSuffixes;
152: private String[] fArgumentPrefixes;
153: private String[] fArgumentSuffixes;
154:
155: private boolean fExtendedInterfaceNameMatching;
156: private boolean fExtendedAllUpperCaseHunkMatching;
157: private boolean fExtendedPluralMatching;
158:
159: public RenamingNameSuggestor() {
160: this (STRATEGY_SUFFIX);
161: }
162:
163: public RenamingNameSuggestor(int strategy) {
164:
165: Assert.isTrue(strategy >= 1 && strategy <= 3);
166:
167: fStrategy = strategy;
168: fExtendedInterfaceNameMatching = true;
169: fExtendedAllUpperCaseHunkMatching = true;
170: fExtendedPluralMatching = true;
171:
172: resetPrefixes();
173: }
174:
175: public String suggestNewFieldName(IJavaProject project,
176: String oldFieldName, boolean isStatic, String oldTypeName,
177: String newTypeName) {
178:
179: initializePrefixesAndSuffixes(project);
180:
181: if (isStatic)
182: return suggestNewVariableName(fStaticFieldPrefixes,
183: fStaticFieldSuffixes, oldFieldName, oldTypeName,
184: newTypeName);
185: else
186: return suggestNewVariableName(fFieldPrefixes,
187: fFieldSuffixes, oldFieldName, oldTypeName,
188: newTypeName);
189: }
190:
191: public String suggestNewLocalName(IJavaProject project,
192: String oldLocalName, boolean isArgument,
193: String oldTypeName, String newTypeName) {
194:
195: initializePrefixesAndSuffixes(project);
196:
197: if (isArgument)
198: return suggestNewVariableName(fArgumentPrefixes,
199: fArgumentSuffixes, oldLocalName, oldTypeName,
200: newTypeName);
201: else
202: return suggestNewVariableName(fLocalPrefixes,
203: fLocalSuffixes, oldLocalName, oldTypeName,
204: newTypeName);
205: }
206:
207: public String suggestNewMethodName(String oldMethodName,
208: String oldTypeName, String newTypeName) {
209:
210: Assert.isNotNull(oldMethodName);
211: Assert.isNotNull(oldTypeName);
212: Assert.isNotNull(newTypeName);
213: Assert.isTrue(oldMethodName.length() > 0);
214: Assert.isTrue(oldTypeName.length() > 0);
215: Assert.isTrue(newTypeName.length() > 0);
216:
217: resetPrefixes();
218:
219: return match(oldTypeName, newTypeName, oldMethodName);
220: }
221:
222: public String suggestNewVariableName(String[] prefixes,
223: String[] suffixes, String oldVariableName,
224: String oldTypeName, String newTypeName) {
225:
226: Assert.isNotNull(prefixes);
227: Assert.isNotNull(suffixes);
228: Assert.isNotNull(oldVariableName);
229: Assert.isNotNull(oldTypeName);
230: Assert.isNotNull(newTypeName);
231: Assert.isTrue(oldVariableName.length() > 0);
232: Assert.isTrue(oldTypeName.length() > 0);
233: Assert.isTrue(newTypeName.length() > 0);
234:
235: final String usedPrefix = findLongestPrefix(oldVariableName,
236: prefixes);
237: final String usedSuffix = findLongestSuffix(oldVariableName,
238: suffixes);
239: final String strippedVariableName = oldVariableName.substring(
240: usedPrefix.length(), oldVariableName.length()
241: - usedSuffix.length());
242:
243: String newVariableName = match(oldTypeName, newTypeName,
244: strippedVariableName);
245: return (newVariableName != null) ? usedPrefix + newVariableName
246: + usedSuffix : null;
247: }
248:
249: // -------------------------------------- Match methods
250:
251: private String match(final String oldTypeName,
252: final String newTypeName, final String strippedVariableName) {
253:
254: String oldType = oldTypeName;
255: String newType = newTypeName;
256:
257: if (fExtendedInterfaceNameMatching && isInterfaceName(oldType)
258: && isInterfaceName(newType)) {
259: oldType = getInterfaceName(oldType);
260: newType = getInterfaceName(newType);
261: }
262:
263: String newVariableName = matchDirect(oldType, newType,
264: strippedVariableName);
265:
266: if (fExtendedPluralMatching && newVariableName == null
267: && canPluralize(oldType))
268: newVariableName = matchDirect(pluralize(oldType),
269: pluralize(newType), strippedVariableName);
270:
271: return newVariableName;
272: }
273:
274: private String matchDirect(String oldType, String newType,
275: final String strippedVariableName) {
276: /*
277: * Use all strategies applied by the user. Always start with exact
278: * matching.
279: *
280: * Note that suffix matching may not match the whole type name if the
281: * new type name has a smaller camel case chunk count.
282: */
283:
284: String newVariableName = exactMatch(oldType, newType,
285: strippedVariableName);
286: if (newVariableName == null && fStrategy >= STRATEGY_EMBEDDED)
287: newVariableName = embeddedMatch(oldType, newType,
288: strippedVariableName);
289: if (newVariableName == null && fStrategy >= STRATEGY_SUFFIX)
290: newVariableName = suffixMatch(oldType, newType,
291: strippedVariableName);
292:
293: return newVariableName;
294: }
295:
296: private String exactMatch(final String oldTypeName,
297: final String newTypeName, final String strippedVariableName) {
298:
299: String newName = exactDirectMatch(oldTypeName, newTypeName,
300: strippedVariableName);
301: if (newName != null)
302: return newName;
303:
304: if (fExtendedAllUpperCaseHunkMatching
305: && isUpperCaseCamelCaseHunk(oldTypeName)) {
306: String oldTN = getFirstUpperRestLowerCased(oldTypeName);
307: String newTN = isUpperCaseCamelCaseHunk(newTypeName) ? getFirstUpperRestLowerCased(newTypeName)
308: : newTypeName;
309: newName = exactDirectMatch(oldTN, newTN,
310: strippedVariableName);
311: }
312:
313: return newName;
314: }
315:
316: private String exactDirectMatch(final String oldTypeName,
317: final String newTypeName, final String strippedVariableName) {
318:
319: if (strippedVariableName.equals(oldTypeName))
320: return newTypeName;
321:
322: if (strippedVariableName.equals(getLowerCased(oldTypeName)))
323: return getLowerCased(newTypeName);
324:
325: return null;
326: }
327:
328: private String embeddedMatch(String oldTypeName,
329: String newTypeName, String strippedVariableName) {
330:
331: // possibility of a match?
332: final String lowerCaseVariable = strippedVariableName
333: .toLowerCase();
334: final String lowerCaseOldTypeName = oldTypeName.toLowerCase();
335: int presumedIndex = lowerCaseVariable
336: .indexOf(lowerCaseOldTypeName);
337:
338: while (presumedIndex != -1) {
339: // it may be there
340: final String presumedTypeName = strippedVariableName
341: .substring(presumedIndex, presumedIndex
342: + oldTypeName.length());
343: final String prefix = strippedVariableName.substring(0,
344: presumedIndex);
345: final String suffix = strippedVariableName
346: .substring(presumedIndex + oldTypeName.length());
347:
348: // can match at all? (depends on suffix)
349: if (startsNewHunk(suffix)) {
350:
351: String name = exactMatch(oldTypeName, newTypeName,
352: presumedTypeName);
353: if (name != null)
354: return prefix + name + suffix;
355: }
356:
357: // did not match -> find next occurrence
358: presumedIndex = lowerCaseVariable.indexOf(
359: lowerCaseOldTypeName, presumedIndex + 1);
360: }
361:
362: return null;
363: }
364:
365: private String suffixMatch(final String oldType,
366: final String newType, final String strippedVariableName) {
367:
368: // get an array of all camel-cased elements from both types + the
369: // variable
370: String[] suffixesOld = getSuffixes(oldType);
371: String[] suffixesNew = getSuffixes(newType);
372: String[] suffixesVar = getSuffixes(strippedVariableName);
373:
374: // get an equal-sized array of the last n camel-cased elements
375: int min = Math.min(suffixesOld.length, suffixesNew.length);
376: String[] suffixesOldEqual = new String[min];
377: String[] suffixesNewEqual = new String[min];
378: System.arraycopy(suffixesOld, suffixesOld.length - min,
379: suffixesOldEqual, 0, min);
380: System.arraycopy(suffixesNew, suffixesNew.length - min,
381: suffixesNewEqual, 0, min);
382:
383: // find endIndex. endIndex is the index of the last hunk of the old type
384: // name in the variable name.
385: int endIndex = -1;
386: for (int j = suffixesVar.length - 1; j >= 0; j--) {
387: String newHunkName = exactMatch(
388: suffixesOldEqual[suffixesOldEqual.length - 1],
389: suffixesNewEqual[suffixesNewEqual.length - 1],
390: suffixesVar[j]);
391: if (newHunkName != null) {
392: endIndex = j;
393: break;
394: }
395: }
396:
397: if (endIndex == -1)
398: return null; // last hunk not found -> no match
399:
400: int stepBack = 0;
401: int lastSuffixMatched = -1;
402: int hunkInVarName = -1;
403: for (int i = suffixesOldEqual.length - 1; i >= 0; i--) {
404:
405: hunkInVarName = endIndex - stepBack;
406: stepBack++;
407:
408: if (hunkInVarName < 0) {
409: // we have reached the beginning of the variable name
410: break;
411: }
412:
413: // try to match this hunk:
414: String newHunkName = exactMatch(suffixesOldEqual[i],
415: suffixesNewEqual[i], suffixesVar[hunkInVarName]);
416:
417: if (newHunkName == null)
418: break; // only match complete suffixes
419:
420: suffixesVar[hunkInVarName] = newHunkName;
421: lastSuffixMatched = i;
422: }
423:
424: if (lastSuffixMatched == 0) {
425: // we have matched ALL type hunks in the variable name,
426: // insert any new prefixes of the new type name
427: int newPrefixes = suffixesNew.length
428: - suffixesNewEqual.length;
429: if (newPrefixes > 0) {
430:
431: // Propagate lowercased start to the front
432: if (Character.isLowerCase(suffixesVar[hunkInVarName]
433: .charAt(0))
434: && Character
435: .isUpperCase(suffixesOldEqual[lastSuffixMatched]
436: .charAt(0))) {
437: suffixesVar[hunkInVarName] = getUpperCased(suffixesVar[hunkInVarName]);
438: suffixesNew[0] = getLowerCased(suffixesNew[0]);
439: }
440:
441: String[] newVariableName = new String[suffixesVar.length
442: + newPrefixes];
443: System.arraycopy(suffixesVar, 0, newVariableName, 0,
444: hunkInVarName); // hunks before type name in variable name
445: System.arraycopy(suffixesNew, 0, newVariableName,
446: hunkInVarName, newPrefixes); // new hunks in new type name
447: System.arraycopy(suffixesVar, hunkInVarName,
448: newVariableName, hunkInVarName + newPrefixes,
449: suffixesVar.length - hunkInVarName); // matched + rest hunks
450: suffixesVar = newVariableName;
451: }
452: }
453:
454: String varName = concat(suffixesVar);
455: if (varName.equals(strippedVariableName))
456: return null; // no "silly suggestions"
457: else
458: return varName;
459: }
460:
461: // ---------------- Helper methods
462:
463: /**
464: * True if the string is the beginning of a new camel case hunk. False if it
465: * is not.
466: */
467: private boolean startsNewHunk(String string) {
468:
469: if (string.length() == 0)
470: return true;
471:
472: return isLegalChar(string.charAt(0));
473: }
474:
475: /**
476: * True if hunk is longer than 1 character and all letters in the hunk are
477: * uppercase. False if not.
478: */
479: private boolean isUpperCaseCamelCaseHunk(String hunk) {
480: if (hunk.length() < 2)
481: return false;
482:
483: for (int i = 0; i < hunk.length(); i++) {
484: if (!isLegalChar(hunk.charAt(i)))
485: return false;
486: }
487: return true;
488: }
489:
490: /**
491: * False if the character is a letter and it is lowercase. True in all other
492: * cases.
493: */
494: private boolean isLegalChar(char c) {
495: if (Character.isLetter(c))
496: return Character.isUpperCase(c);
497: return true;
498: }
499:
500: /**
501: * Grab a list of camelCase-separated suffixes from the typeName, for
502: * example:
503: *
504: * "JavaElementName" => { "Java", "Element", "Name }
505: *
506: * "ASTNode" => { "AST", "Node" }
507: *
508: */
509: private String[] getSuffixes(String typeName) {
510: List suffixes = new ArrayList();
511: JavaWordIterator iterator = new JavaWordIterator();
512: iterator.setText(typeName);
513: int lastmatch = 0;
514: int match;
515: while ((match = iterator.next()) != BreakIterator.DONE) {
516: suffixes.add(typeName.substring(lastmatch, match));
517: lastmatch = match;
518: }
519: return (String[]) suffixes.toArray(new String[0]);
520: }
521:
522: private String concat(String[] suffixesNewEqual) {
523: StringBuffer returner = new StringBuffer();
524: for (int j = 0; j < suffixesNewEqual.length; j++) {
525: returner.append(suffixesNewEqual[j]);
526: }
527: return returner.toString();
528: }
529:
530: private String getLowerCased(String name) {
531: if (name.length() > 1)
532: return Character.toLowerCase(name.charAt(0))
533: + name.substring(1);
534: else
535: return name.toLowerCase();
536: }
537:
538: private String getUpperCased(String name) {
539: if (name.length() > 1)
540: return Character.toUpperCase(name.charAt(0))
541: + name.substring(1);
542: else
543: return name.toLowerCase();
544: }
545:
546: private String getFirstUpperRestLowerCased(String name) {
547: if (name.length() > 1)
548: return Character.toUpperCase(name.charAt(0))
549: + name.substring(1).toLowerCase();
550: else
551: return name.toLowerCase();
552: }
553:
554: private boolean isInterfaceName(String typeName) {
555: return ((typeName.length() >= 2) && typeName.charAt(0) == 'I' && Character
556: .isUpperCase(typeName.charAt(1)));
557: }
558:
559: private String getInterfaceName(String typeName) {
560: return typeName.substring(1);
561: }
562:
563: private String findLongestPrefix(String name, String[] prefixes) {
564: String usedPrefix = ""; //$NON-NLS-1$
565: int bestLen = 0;
566: for (int i = 0; i < prefixes.length; i++) {
567: if (name.startsWith(prefixes[i])) {
568: if (prefixes[i].length() > bestLen) {
569: bestLen = prefixes[i].length();
570: usedPrefix = prefixes[i];
571: }
572: }
573: }
574: return usedPrefix;
575: }
576:
577: private String findLongestSuffix(String name, String[] suffixes) {
578: String usedPrefix = ""; //$NON-NLS-1$
579: int bestLen = 0;
580: for (int i = 0; i < suffixes.length; i++) {
581: if (name.endsWith(suffixes[i])) {
582: if (suffixes[i].length() > bestLen) {
583: bestLen = suffixes[i].length();
584: usedPrefix = suffixes[i];
585: }
586: }
587: }
588: return usedPrefix;
589: }
590:
591: /**
592: * Returns true if the type name can be pluralized by a string operation.
593: * This is always the case if it does not already end with an "s".
594: */
595: private boolean canPluralize(String typeName) {
596: return !typeName.endsWith(PLURAL_S);
597: }
598:
599: private String pluralize(String typeName) {
600: if (typeName.endsWith(SINGULAR_Y))
601: typeName = typeName.substring(0, typeName.length() - 1)
602: .concat(PLURAL_IES);
603: else if (!typeName.endsWith(PLURAL_S))
604: typeName = typeName.concat(PLURAL_S);
605: return typeName;
606: }
607:
608: private void resetPrefixes() {
609: String[] empty = new String[0];
610: fFieldPrefixes = empty;
611: fFieldSuffixes = empty;
612: fStaticFieldPrefixes = empty;
613: fStaticFieldSuffixes = empty;
614: fLocalPrefixes = empty;
615: fLocalSuffixes = empty;
616: fArgumentPrefixes = empty;
617: fArgumentSuffixes = empty;
618: }
619:
620: private void initializePrefixesAndSuffixes(IJavaProject project) {
621: fFieldPrefixes = readCommaSeparatedPreference(project,
622: JavaCore.CODEASSIST_FIELD_PREFIXES);
623: fFieldSuffixes = readCommaSeparatedPreference(project,
624: JavaCore.CODEASSIST_FIELD_SUFFIXES);
625: fStaticFieldPrefixes = readCommaSeparatedPreference(project,
626: JavaCore.CODEASSIST_STATIC_FIELD_PREFIXES);
627: fStaticFieldSuffixes = readCommaSeparatedPreference(project,
628: JavaCore.CODEASSIST_STATIC_FIELD_SUFFIXES);
629: fLocalPrefixes = readCommaSeparatedPreference(project,
630: JavaCore.CODEASSIST_LOCAL_PREFIXES);
631: fLocalSuffixes = readCommaSeparatedPreference(project,
632: JavaCore.CODEASSIST_LOCAL_SUFFIXES);
633: fArgumentPrefixes = readCommaSeparatedPreference(project,
634: JavaCore.CODEASSIST_ARGUMENT_PREFIXES);
635: fArgumentSuffixes = readCommaSeparatedPreference(project,
636: JavaCore.CODEASSIST_ARGUMENT_SUFFIXES);
637: }
638:
639: private String[] readCommaSeparatedPreference(IJavaProject project,
640: String option) {
641: String list = project.getOption(option, true);
642: return list == null ? new String[0] : list.split(","); //$NON-NLS-1$
643: }
644:
645: }
|