001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2007 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041:
042: package org.netbeans.modules.websvc.rest.support;
043:
044: import java.util.ArrayList;
045: import java.util.LinkedList;
046: import java.util.List;
047: import java.util.regex.Matcher;
048: import java.util.regex.Pattern;
049:
050: /**
051: * <p>API for performing inflections (pluralization, singularization, and so on)
052: * on various strings. These inflections will be useful in code generators that
053: * convert things like database table names into Java class names.</p>
054: *
055: * <p>The <code>getInstance()</code> method returns a singleton instance of
056: * this class with a default set of rules, which can then be customized.
057: * Rules added during customization will take precedence over the standard ones.
058: * Use the <code>addIrregular()</code>, <code>addPlural()</code>, <code>addSingular()</code>,
059: * and <code>addUncountable()</code> methods to add additional rules ot the default
060: * ones.</p>
061: *
062: * <p><strong>IMPLEMENTATION NOTE</strong> - The default implementation is
063: * intended to be functionally compatible with the <code>Inflector::inflections</code>
064: * class in Ruby on Rails. The <code>gsub()</code> method on Ruby strings
065: * matches regular expressions anywhere in the input. However, nearly all of
066: * the actual patterns used in this module use <code>$</code> at the end to
067: * match the end of the input string (so that only the last word in a multiple
068: * word phrase will be singularized or pluralized). Therefore, the Java versions
069: * of the regular expressions have been modified to capture all text before the
070: * interesting characters at the end, and emit them as part of the result, so
071: * that the entire string can be matched against a pattern once.</p>
072: */
073: public class Inflector {
074:
075: // ------------------------------------------------------------ Constructors
076:
077: /**
078: * <p>Private constructor to avoid instantiation.</p>
079: */
080: private Inflector() {
081:
082: addPlural("$", "s", false);
083: addPlural("(.*)$", "\\1s");
084: addPlural("(.*)(ax|test)is$", "\\1\\2es");
085: addPlural("(.*)(octop|vir)us$", "\\1\\2i");
086: addPlural("(.*)(alias|status)$", "\\1\\2es");
087: addPlural("(.*)(bu)s$", "\\1\\2ses");
088: addPlural("(.*)(buffal|tomat)o$", "\\1\\2oes");
089: addPlural("(.*)([ti])um$", "\\1\\2a");
090: addPlural("(.*)sis$", "\\1ses");
091: addPlural("(.*)(?:([^f])fe|([lr])f)$", "\\1\\3ves");
092: addPlural("(.*)(hive)$", "\\1\\2s");
093: addPlural("(.*)(tive)$", "\\1\\2s"); // Added for consistency with singular rules
094: addPlural("(.*)([^aeiouy]|qu)y$", "\\1\\2ies");
095: addPlural("(.*)(series)$", "\\1\\2"); // Added for consistency with singular rules
096: addPlural("(.*)(movie)$", "\\1\\2s"); // Added for consistency with singular rules
097: addPlural("(.*)(x|ch|ss|sh)$", "\\1\\2es");
098: addPlural("(.*)(matr|vert|ind)ix|ex$", "\\1\\2ices");
099: addPlural("(.*)(o)$", "\\1\\2es"); // Added for consistency with singular rules
100: addPlural("(.*)(shoe)$", "\\1\\2s"); // Added for consistency with singular rules
101: addPlural("(.*)([m|l])ouse$", "\\1\\2ice");
102: addPlural("^(ox)$", "\\1en");
103: addPlural("(.*)(vert|ind)ex$", "\\1\\2ices"); // Added for consistency with singular rules
104: addPlural("(.*)(matr)ix$", "\\1\\2ices"); // Added for consistency with singular rules
105: addPlural("(.*)(quiz)$", "\\1\\2zes");
106:
107: addSingular("(.*)s$", "\\1");
108: addSingular("(.*)(n)ews$", "\\1\\2ews");
109: addSingular("(.*)([ti])a$", "\\1\\2um");
110: addSingular(
111: "(.*)((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$",
112: "\\1\\2sis");
113: addSingular("(.*)(^analy)ses$", "\\1\\2sis");
114: addSingular("(.*)([^f])ves$", "\\1\\2fe");
115: addSingular("(.*)(hive)s$", "\\1\\2");
116: addSingular("(.*)(tive)s$", "\\1\\2");
117: addSingular("(.*)([lr])ves$", "\\1\\2f");
118: addSingular("(.*)([^aeiouy]|qu)ies$", "\\1\\2y");
119: addSingular("(.*)(s)eries$", "\\1\\2eries");
120: addSingular("(.*)(m)ovies$", "\\1\\2ovie");
121: addSingular("(.*)(x|ch|ss|sh)es$", "\\1\\2");
122: addSingular("(.*)([m|l])ice$", "\\1\\2ouse");
123: addSingular("(.*)(bus)es$", "\\1\\2");
124: addSingular("(.*)(o)es$", "\\1\\2");
125: addSingular("(.*)(shoe)s$", "\\1\\2");
126: addSingular("(.*)(cris|ax|test)es$", "\\1\\2is");
127: addSingular("(.*)(octop|vir)i$", "\\1\\2us");
128: addSingular("(.*)(alias|status)es$", "\\1\\2");
129: addSingular("^(ox)en", "\\1");
130: addSingular("(.*)(vert|ind)ices$", "\\1\\2ex");
131: addSingular("(.*)(matr)ices$", "\\1\\2ix");
132: addSingular("(.*)(quiz)zes$", "\\1\\2");
133:
134: addIrregular("child", "children");
135: addIrregular("man", "men");
136: addIrregular("move", "moves");
137: addIrregular("person", "people");
138: addIrregular("sex", "sexes");
139:
140: addUncountable("equipment");
141: addUncountable("fish");
142: addUncountable("information");
143: addUncountable("money");
144: addUncountable("rice");
145: addUncountable("series");
146: addUncountable("sheep");
147: addUncountable("species");
148:
149: }
150:
151: // -------------------------------------------------------- Static Variables
152:
153: /**
154: * <p>The singleton instance returned by the default <code>getInstance()</code>
155: * method.</p>
156: */
157: private transient static Inflector instance = null;
158:
159: /**
160: * <p>List of <code>Replacer</code>s for performing replacement operations
161: * on matches for plural words.</p>
162: */
163: private List plurals = new LinkedList();
164:
165: /**
166: * <p>List of <code>Replacer</code>s for performing replacement operations
167: * on matches for addSingular words.</p>
168: */
169: private List singulars = new ArrayList();
170:
171: /**
172: * <p>List of words that represent addUncountable concepts that cannot be
173: * pluralized or singularized.</p>
174: */
175: private List uncountables = new LinkedList();
176:
177: // ------------------------------------------------------ Instance Variables
178:
179: // ---------------------------------------------------------- Static Methods
180:
181: /**
182: * <p>Return a fully configured {@link Inflector} instance that can be used
183: * for performing transformations.</p>
184: */
185: public static Inflector getInstance() {
186:
187: if (instance == null) {
188: instance = new Inflector();
189: }
190: return instance;
191:
192: }
193:
194: // ---------------------------------------------------------- Public Methods
195:
196: /**
197: * <p>Convert strings to <code>EmbeddedCamelCase</code>. Embedded
198: * underscores will be removed.</p>
199: *
200: * @param word Word to be converted
201: */
202: public String camelize(String word) {
203:
204: return camelize(word, false);
205:
206: }
207:
208: /**
209: * <p>Convert word strings consisting of lower case letters and
210: * underscore characters between words into <code>embeddedCamelCase</code>
211: * or <code>EmbeddedCamelCase</code>, depending on the <code>lower</code>
212: * flag. Embedded underscores will be removed. Embedded '/'
213: * characters will be replaced by '.', making this method useful
214: * in converting path-like names into fully qualified classnames.</p>
215: *
216: * <p><strong>IMPLEMENTATION DIFFERENCE</strong> - The Rails version of this
217: * method also converts '/' characters to '::' because that reflects
218: * the normal syntax for fully qualified names in Ruby.</p>
219: *
220: * <table border="1" width="100%">
221: * <tr>
222: * <th>Input</th>
223: * <th>Output</th>
224: * </tr>
225: * <tr>
226: * <td>"foo_bar", false</td>
227: * <td>"FooBar"</td>
228: * </tr>
229: * <tr>
230: * <td>"foo_bar", true</td>
231: * <td>"fooBar"</td>
232: * </tr>
233: * <tr>
234: * <td>"foo_bar/baz", false</td>
235: * <td>"FooBar.Baz"</td>
236: * </tr>
237: * <tr>
238: * <td>"foo_bar/baz", true</td>
239: * <td>"fooBar.Baz"</td>
240: * </tr>
241: * </table>
242: *
243: * @param word Word to be converted
244: * @param flag Flag indicating that the initial character should
245: * be lower cased instead of upper cased
246: */
247: public String camelize(String word, boolean flag) {
248: if (word.length() == 0)
249: return word;
250:
251: StringBuffer sb = new StringBuffer(word.length());
252: if (flag) {
253: sb.append(Character.toLowerCase(word.charAt(0)));
254: } else {
255: sb.append(Character.toUpperCase(word.charAt(0)));
256: }
257: boolean capitalize = false;
258: for (int i = 1; i < word.length(); i++) {
259: char ch = word.charAt(i);
260: if (capitalize) {
261: sb.append(Character.toUpperCase(ch));
262: capitalize = false;
263: } else if (ch == '_') {
264: capitalize = true;
265: } else if (ch == '/') {
266: capitalize = true;
267: sb.append('.');
268: } else {
269: sb.append(ch);
270: }
271: }
272: return sb.toString();
273:
274: }
275:
276: /**
277: * <p>Create and return a simple class name that corresponds to a
278: * addPlural table name. Any leading schema name will be trimmed.</p>
279: *
280: * <table border="1" width="100%">
281: * <tr>
282: * <th>Input</th>
283: * <th>Output</th>
284: * </tr>
285: * <tr>
286: * <td>"foo_bars"</td>
287: * <td>"FooBar"</td>
288: * </tr>
289: * <tr>
290: * <td>"baz"</td>
291: * <td>"Baz"</td>
292: * </tr>
293: * </table>
294: *
295: * @param tableName Table name to be converted
296: */
297: public String classify(String tableName) {
298:
299: int period = tableName.lastIndexOf('.');
300: if (period >= 0) {
301: tableName = tableName.substring(period + 1);
302: }
303: return camelize(singularize(tableName));
304:
305: }
306:
307: /**
308: * <p>Replace underscores in the specified word with dashes.</p>
309: *
310: * <table border="1" width="100%">
311: * <tr>
312: * <th>Input</th>
313: * <th>Output</th>
314: * </tr>
315: * <tr>
316: * <td>"foo_bar"</td>
317: * <td>"foo-bar"</td>
318: * </tr>
319: * <tr>
320: * <td>"baz"</td>
321: * <td>"baz"</td>
322: * </tr>
323: * </table>
324: *
325: * @param word Word to be converted
326: */
327: public String dasherize(String word) {
328:
329: return word.replace('_', '-');
330:
331: }
332:
333: /**
334: * <p>Remove any package name from a fully qualified class name,
335: * returning only the simple classname.</p>
336: *
337: * <table border="1" width="100%">
338: * <tr>
339: * <th>Input</th>
340: * <th>Output</th>
341: * </tr>
342: * <tr>
343: * <td>"java.util.Map"</td>
344: * <td>"Map"</td>
345: * </tr>
346: * <tr>
347: * <td>"String"</td>
348: * <td>"String"</td>
349: * </tr>
350: * </table>
351: *
352: * @param className Fully qualified class name to be converted
353: */
354: public String demodulize(String className) {
355:
356: int period = className.lastIndexOf('.');
357: if (period >= 0) {
358: return className.substring(period + 1);
359: } else {
360: return className;
361: }
362:
363: }
364:
365: /**
366: * <p>Create and return a foreign key name from a class name,
367: * separating the "id" suffix with an underscore.</p>
368: */
369: public String foreignKey(String className) {
370:
371: return foreignKey(className, true);
372:
373: }
374:
375: /**
376: * <p>Create and return a foreign key name from a class name,
377: * optionally inserting an underscore before the "id" portion.</p>
378: *
379: * <table border="1" width="100%">
380: * <tr>
381: * <th>Input</th>
382: * <th>Output</th>
383: * </tr>
384: * <tr>
385: * <td>"com.mymodel.Order", false</td>
386: * <td>"orderid"</td>
387: * </tr>
388: * <tr>
389: * <td>"com.mymodel.Order", true</td>
390: * <td>"order_id"</td>
391: * </tr>
392: * <tr>
393: * <td>"Message", false</td>
394: * <td>"messageid"</td>
395: * </tr>
396: * <tr>
397: * <td>"Message", true</td>
398: * <td>"message_id"</td>
399: * </tr>
400: * </table>
401: *
402: * @param className Class name for which to create a foreign key
403: * @param underscore Flag indicating whether an underscore should
404: * be emitted between the class name and the "id" suffix
405: */
406: public String foreignKey(String className, boolean underscore) {
407:
408: return underscore(demodulize(className)
409: + (underscore ? "_id" : "id"));
410:
411: }
412:
413: /**
414: * <p>Capitalize the first word in a lower cased and underscored string,
415: * turn underscores into spaces, and string any trailing "_id". Like
416: * <code>titleize()</code>, this is meant for creating pretty output,
417: * and is not intended for code generation.</p>
418: *
419: * <table border="1" width="100%">
420: * <tr>
421: * <th>Input</th>
422: * <th>Output</th>
423: * </tr>
424: * <tr>
425: * <td>"employee_salary"</td>
426: * <td>"Employee salary"</td>
427: * </tr>
428: * <tr>
429: * <td>"author_id"</td>
430: * <td>"Author"</td>
431: * </tr>
432: * </table>
433: *
434: * @param words Word string to be converted
435: */
436: public String humanize(String words) {
437:
438: if (words.endsWith("_id")) {
439: words = words.substring(0, words.length() - 3);
440: }
441: StringBuffer sb = new StringBuffer(words.length());
442: sb.append(Character.toUpperCase(words.charAt(0)));
443: for (int i = 1; i < words.length(); i++) {
444: char ch = words.charAt(i);
445: if (ch == '_') {
446: sb.append(' ');
447: } else {
448: sb.append(ch);
449: }
450: }
451: return sb.toString();
452:
453: }
454:
455: /**
456: * <p>Turn a number into a corresponding ordinal string used to
457: * denote the position in an ordered sequence.</p>
458: *
459: * <table border="1" width="100%">
460: * <tr>
461: * <th>Input</th>
462: * <th>Output</th>
463: * </tr>
464: * <tr>
465: * <td>1</td>
466: * <td>"1st"</td>
467: * </tr>
468: * <tr>
469: * <td>2</td>
470: * <td>"2nd"</td>
471: * </tr>
472: * <tr>
473: * <td>3</td>
474: * <td>"3rd"</td>
475: * </tr>
476: * <tr>
477: * <td>4</td>
478: * <td>"rth"</td>
479: * </tr>
480: * <tr>
481: * <td>1002</td>
482: * <td>"1002nd"</td>
483: * </tr>
484: * <tr>
485: * <td>2012</td>
486: * <td>"2012th"</td>
487: * </tr>
488: * </table>
489: *
490: * @param number Number to be converted
491: */
492: public String ordinalize(int number) {
493:
494: int modulo = number % 100;
495: if ((modulo >= 11) && (modulo <= 13)) {
496: return "" + number + "th";
497: }
498: switch (number % 10) {
499: case 1:
500: return "" + number + "st";
501: case 2:
502: return "" + number + "nd";
503: case 3:
504: return "" + number + "rd";
505: default:
506: return "" + number + "th";
507: }
508:
509: }
510:
511: /**
512: * <p>Return a addPlural version of the specified (addSingular) word.</p>
513: *
514: *
515: * @param word Singular word to be converted
516: */
517: public String pluralize(String word) {
518:
519: // Scan uncountables and leave alone
520: for (int i = 0; i < uncountables.size(); i++) {
521: if (uncountables.get(i).equals(word)) {
522: return word;
523: }
524: }
525:
526: // Scan our patterns for a match and return the correct replacement
527: for (int i = 0; i < plurals.size(); i++) {
528: Replacer replacer = (Replacer) plurals.get(i);
529: if (replacer.matches(word)) {
530: return replacer.replacement();
531: }
532: }
533:
534: // Return the original string unchanged
535: return word;
536:
537: }
538:
539: /**
540: * <p>Return a addSingular version of the specified (addPlural) word.</p>
541: *
542: *
543: * @param word Plural word to be converted
544: */
545: public String singularize(String word) {
546:
547: // Scan uncountables and leave alone
548: for (int i = 0; i < uncountables.size(); i++) {
549: if (uncountables.get(i).equals(word)) {
550: return word;
551: }
552: }
553:
554: // Scan our patterns for a match and return the correct replacement
555: for (int i = 0; i < singulars.size(); i++) {
556: Replacer replacer = (Replacer) singulars.get(i);
557: if (replacer.matches(word)) {
558: return replacer.replacement();
559: }
560: }
561:
562: // Return the original string unchanged
563: return word;
564:
565: }
566:
567: /**
568: * <p>Convert the simple name of a model class into the corresponding
569: * name of a database table, by uncamelizing, inserting underscores,
570: * and pluralizing the last word.</p>
571: *
572: * <table border="1" width="100%">
573: * <tr>
574: * <th>Input</th>
575: * <th>Output</th>
576: * </tr>
577: * <tr>
578: * <td>"RawScaledScorer"</td>
579: * <td>"raw_scaled_scorers"</td>
580: * </tr>
581: * <tr>
582: * <td>"fancyCategory"</td>
583: * <td>"fancy_categories"</td>
584: * </tr>
585: * </table>
586: *
587: * @param className Class name to be converted
588: */
589: public String tableize(String className) {
590:
591: return pluralize(underscore(className));
592:
593: }
594:
595: /**
596: * <p>Capitalize all the words, and replace some characters in the string
597: * to create a nicer looking title. This is meant for creating pretty
598: * output, and is not intended for code generation.</p>
599: *
600: * <table border="1" width="100%">
601: * <tr>
602: * <th>Input</th>
603: * <th>Output</th>
604: * </tr>
605: * <tr>
606: * <td>"the honeymooners"</td>
607: * <td>"The Honeymooners"</td>
608: * </tr>
609: * <tr>
610: * <td>"x-men: the last stand"</td>
611: * <td>"X Men: The Last Stand"</td>
612: * </tr>
613: * </table>
614: *
615: * @param words Word string to be converted
616: */
617: public String titleize(String words) {
618:
619: StringBuffer sb = new StringBuffer(words.length());
620: boolean capitalize = true; // To get the first character right
621: for (int i = 0; i < words.length(); i++) {
622: char ch = words.charAt(i);
623: if (Character.isWhitespace(ch)) {
624: sb.append(' ');
625: capitalize = true;
626: } else if (ch == '-') {
627: sb.append(' ');
628: capitalize = true;
629: } else if (capitalize) {
630: sb.append(Character.toUpperCase(ch));
631: capitalize = false;
632: } else {
633: sb.append(ch);
634: }
635: }
636: return sb.toString();
637:
638: }
639:
640: /**
641: * <p>The reverse of <code>camelize()</code>, makes an underscored form
642: * from the expression in the string. Changes "." to "/" to convert
643: * fully qualified class names into paths.</p>
644: *
645: * <table border="1" width="100%">
646: * <tr>
647: * <th>Input</th>
648: * <th>Output</th>
649: * </tr>
650: * <tr>
651: * <td>"FooBar"</td>
652: * <td>"foo_bar"</td>
653: * </tr>
654: * <tr>
655: * <td>"fooBar"</td>
656: * <td>"foo_bar"</td>
657: * </tr>
658: * <tr>
659: * <td>"FooBar.Baz"</td>
660: * <td>"foo_bar/baz"</td>
661: * </tr>
662: * <tr>
663: * <td>"FooBar.Baz"</td>
664: * <td>"foo_bar/baz"</td>
665: * </tr>
666: * </table>
667: *
668: * @param word Camel cased word to be converted
669: */
670: public String underscore(String word) {
671:
672: StringBuffer sb = new StringBuffer(word.length() + 5);
673: boolean uncapitalize = false;
674: for (int i = 0; i < word.length(); i++) {
675: char ch = word.charAt(i);
676: if (uncapitalize) {
677: sb.append(Character.toLowerCase(ch));
678: uncapitalize = false;
679: } else if (ch == '.') {
680: sb.append('/');
681: uncapitalize = true;
682: } else if (Character.isUpperCase(ch)) {
683: if (i > 0) {
684: sb.append('_');
685: }
686: sb.append(Character.toLowerCase(ch));
687: } else {
688: sb.append(ch);
689: }
690: }
691: return sb.toString();
692:
693: }
694:
695: // --------------------------------------------------- Customization Methods
696:
697: /**
698: * <p>Add the addSingular and addPlural forms of words that cannot be
699: * converted using the normal rules.</p>
700: *
701: *
702: * @param singular Singular form of the word
703: * @param plural Plural form of the word
704: */
705: public void addIrregular(String singular, String plural) {
706:
707: addPlural("(.*)(" + singular.substring(0, 1) + ")"
708: + singular.substring(1) + "$", "\\1\\2"
709: + plural.substring(1));
710: addSingular("(.*)(" + plural.substring(0, 1) + ")"
711: + plural.substring(1) + "$", "\\1\\2"
712: + singular.substring(1));
713:
714: }
715:
716: /**
717: * <p>Add a match pattern and replacement rule for converting addPlural
718: * forms to addSingular forms. By default, matches will be case
719: * insensitive.</p>
720: *
721: *
722: * @param match Match pattern regular expression
723: * @param rule Replacement rule
724: */
725: public void addPlural(String match, String rule) {
726:
727: addPlural(match, rule, true);
728:
729: }
730:
731: /**
732: * <p>Add a match pattern and replacement rule for converting addPlural
733: * forms to addSingular forms.</p>
734: *
735: *
736: * @param match Match pattern regular expression
737: * @param rule Replacement rule
738: * @param insensitive Flag indicating this match should be case insensitive
739: */
740: public void addPlural(String match, String rule, boolean insensitive) {
741:
742: plurals.add(0, new Replacer(match, rule, insensitive));
743:
744: }
745:
746: /**
747: * <p>Add a match pattern and replacement rule for converting addSingular
748: * forms to addPlural forms. By default, matches will be case insensitive.</p>
749: *
750: *
751: * @param match Match pattern regular expression
752: * @param rule Replacement rule
753: */
754: public void addSingular(String match, String rule) {
755:
756: addSingular(match, rule, true);
757:
758: }
759:
760: /**
761: * <p>Add a match pattern and replacement rule for converting addSingular
762: * forms to addPlural forms.</p>
763: *
764: *
765: * @param match Match pattern regular expression
766: * @param rule Replacement rule
767: * @param insensitive Flag indicating this match should be case insensitive
768: */
769: public void addSingular(String match, String rule,
770: boolean insensitive) {
771:
772: singulars.add(0, new Replacer(match, rule, insensitive));
773:
774: }
775:
776: /**
777: * <p>Add a word that cannot be converted between addSingular and addPlural.</p>
778: *
779: *
780: * @param word Word to be added
781: */
782: public void addUncountable(String word) {
783:
784: uncountables.add(0, word.toLowerCase());
785:
786: }
787:
788: // --------------------------------------------------------- Private Classes
789:
790: /**
791: * <p>Internal class that uses a regular expression matcher to both
792: * match the specified regular expression to a specified word, and
793: * (if successful) perform the appropriate substitutions.</p>
794: */
795: private class Replacer {
796:
797: // --------------------------------------------------------- Constructor
798:
799: public Replacer(String match, String rule, boolean insensitive) {
800:
801: pattern = Pattern.compile(match,
802: insensitive ? Pattern.CASE_INSENSITIVE : 0);
803: this .rule = rule;
804:
805: }
806:
807: // -------------------------------------------------- Instance Variables
808:
809: private String input = null;
810: private Matcher matcher = null;
811: private Pattern pattern = null;
812: private String rule = null;
813:
814: // ------------------------------------------------------ Public Methods
815:
816: /**
817: * <p>Return <code>true</code> if our regular expression pattern matches
818: * the specified input. If it does, save necessary state information so
819: * that the <code>replacement()</code> method will return appropriate
820: * results based on the <code>rule</code> specified to our constructor.</p>
821: *
822: * @param input Input characters to be matched
823: */
824: public boolean matches(String input) {
825:
826: matcher = pattern.matcher(input);
827: if (matcher.matches()) {
828: this .input = input;
829: return true;
830: } else {
831: this .input = null;
832: this .matcher = null;
833: return false;
834: }
835:
836: }
837:
838: /**
839: * <p>Return a replacement string based on the <code>rule</code> that
840: * was specified to our constructor. This method <strong>MUST</strong>
841: * only be called when the <code>matches()</code> method has returned
842: * <code>true</code>.</p>
843: */
844: public String replacement() {
845:
846: StringBuffer sb = new StringBuffer();
847: boolean group = false;
848: for (int i = 0; i < rule.length(); i++) {
849: char ch = rule.charAt(i);
850: if (group) {
851: sb.append(matcher.group(Character.digit(ch, 10)));
852: group = false;
853: } else if (ch == '\\') {
854: group = true;
855: } else {
856: sb.append(ch);
857: }
858: }
859: return sb.toString();
860:
861: }
862:
863: }
864:
865: }
|