org.glassfish.jersey.message.internal.NounInflector Maven / Gradle / Ivy
Show all versions of jaxrs-ri Show documentation
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 2010-2012 Oracle and/or its affiliates. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in compliance with the License. You can
* obtain a copy of the License at
* http://glassfish.java.net/public/CDDL+GPL_1_1.html
* or packager/legal/LICENSE.txt. See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at packager/legal/LICENSE.txt.
*
* GPL Classpath Exception:
* Oracle designates this particular file as subject to the "Classpath"
* exception as provided by Oracle in the GPL Version 2 section of the License
* file that accompanied this code.
*
* Modifications:
* If applicable, add the following below the License Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyright [year] [name of copyright owner]"
*
* Contributor(s):
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*/
package org.glassfish.jersey.message.internal;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* API for performing inflections (pluralization, singularization, and so on)
* on various strings. These inflections will be useful in code generators that
* convert things like database table names into Java class names.
*
* The {@code getInstance()} method returns a singleton instance of
* this class with a default set of rules, which can then be customized.
* Rules added during customization will take precedence over the standard ones.
* Use the {@code addIrregular()}, {@code addPlural()}, {@code addSingular()},
* and {@code addUncountable()} methods to add additional rules ot the default
* ones.
*
* IMPLEMENTATION NOTE - The default implementation is
* intended to be functionally compatible with the {@code Inflector::inflections}
* class in Ruby on Rails. The {@code gsub()} method on Ruby strings
* matches regular expressions anywhere in the input. However, nearly all of
* the actual patterns used in this component use {@code $} at the end to
* match the end of the input string (so that only the last word in a multiple
* word phrase will be singularized or pluralized). Therefore, the Java versions
* of the regular expressions have been modified to capture all text before the
* interesting characters at the end, and emit them as part of the result, so
* that the entire string can be matched against a pattern once.
*
* @author Florian Rosenberg
*/
final class NounInflector {
// ------------------------------------------------------------ Constructors
/**
* Private constructor to avoid instantiation.
*/
private NounInflector() {
addPlural("$", "s", false);
addPlural("(.*)$", "\\1s");
addPlural("(.*)(ax|test)is$", "\\1\\2es");
addPlural("(.*)(octop|vir)us$", "\\1\\2i");
addPlural("(.*)(alias|status)$", "\\1\\2es");
addPlural("(.*)(bu)s$", "\\1\\2ses");
addPlural("(.*)(buffal|tomat)o$", "\\1\\2oes");
addPlural("(.*)([ti])um$", "\\1\\2a");
addPlural("(.*)sis$", "\\1ses");
addPlural("(.*)(?:([^f])fe|([lr])f)$", "\\1\\3ves");
addPlural("(.*)(hive)$", "\\1\\2s");
addPlural("(.*)(tive)$", "\\1\\2s"); // Added for consistency with singular rules
addPlural("(.*)([^aeiouy]|qu)y$", "\\1\\2ies");
addPlural("(.*)(series)$", "\\1\\2"); // Added for consistency with singular rules
addPlural("(.*)(movie)$", "\\1\\2s"); // Added for consistency with singular rules
addPlural("(.*)(x|ch|ss|sh)$", "\\1\\2es");
addPlural("(.*)(matr|vert|ind)ix|ex$", "\\1\\2ices");
addPlural("(.*)(o)$", "\\1\\2es"); // Added for consistency with singular rules
addPlural("(.*)(shoe)$", "\\1\\2s"); // Added for consistency with singular rules
addPlural("(.*)([m|l])ouse$", "\\1\\2ice");
addPlural("^(ox)$", "\\1en");
addPlural("(.*)(vert|ind)ex$", "\\1\\2ices"); // Added for consistency with singular rules
addPlural("(.*)(matr)ix$", "\\1\\2ices"); // Added for consistency with singular rules
addPlural("(.*)(quiz)$", "\\1\\2zes");
addSingular("(.*)s$", "\\1");
addSingular("(.*)(n)ews$", "\\1\\2ews");
addSingular("(.*)([ti])a$", "\\1\\2um");
addSingular("(.*)((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$", "\\1\\2sis");
addSingular("(.*)(^analy)ses$", "\\1\\2sis");
addSingular("(.*)([^f])ves$", "\\1\\2fe");
addSingular("(.*)(hive)s$", "\\1\\2");
addSingular("(.*)(tive)s$", "\\1\\2");
addSingular("(.*)([lr])ves$", "\\1\\2f");
addSingular("(.*)([^aeiouy]|qu)ies$", "\\1\\2y");
addSingular("(.*)(s)eries$", "\\1\\2eries");
addSingular("(.*)(m)ovies$", "\\1\\2ovie");
addSingular("(.*)(x|ch|ss|sh)es$", "\\1\\2");
addSingular("(.*)([m|l])ice$", "\\1\\2ouse");
addSingular("(.*)(bus)es$", "\\1\\2");
addSingular("(.*)(o)es$", "\\1\\2");
addSingular("(.*)(shoe)s$", "\\1\\2");
addSingular("(.*)(cris|ax|test)es$", "\\1\\2is");
addSingular("(.*)(octop|vir)i$", "\\1\\2us");
addSingular("(.*)(alias|status)es$", "\\1\\2");
addSingular("^(ox)en", "\\1");
addSingular("(.*)(vert|ind)ices$", "\\1\\2ex");
addSingular("(.*)(matr)ices$", "\\1\\2ix");
addSingular("(.*)(quiz)zes$", "\\1\\2");
addIrregular("child", "children");
addIrregular("man", "men");
addIrregular("move", "moves");
addIrregular("person", "people");
addIrregular("sex", "sexes");
addUncountable("equipment");
addUncountable("fish");
addUncountable("information");
addUncountable("money");
addUncountable("rice");
addUncountable("series");
addUncountable("sheep");
addUncountable("species");
}
// -------------------------------------------------------- Static Variables
/**
* The singleton instance returned by the default getInstance()
* method.
*/
private transient static NounInflector instance = null;
/**
* List of Replacer
s for performing replacement operations
* on matches for plural words.
*/
private List plurals = new LinkedList();
/**
* List of Replacer
s for performing replacement operations
* on matches for addSingular words.
*/
private List singulars = new ArrayList();
/**
* List of words that represent addUncountable concepts that cannot be
* pluralized or singularized.
*/
private List uncountables = new LinkedList();
// ------------------------------------------------------ Instance Variables
// ---------------------------------------------------------- Static Methods
/**
* Return a fully configured {@link NounInflector} instance that can be used
* for performing transformations.
*/
public static NounInflector getInstance() {
if (instance == null) {
instance = new NounInflector();
}
return instance;
}
// ---------------------------------------------------------- Public Methods
/**
* Convert strings to EmbeddedCamelCase
. Embedded
* underscores will be removed.
*
* @param word Word to be converted
*/
public String camelize(String word) {
return camelize(word, false);
}
/**
* Convert word strings consisting of lower case letters and
* underscore characters between words into embeddedCamelCase
* or EmbeddedCamelCase
, depending on the lower
* flag. Embedded underscores will be removed. Embedded '/'
* characters will be replaced by '.', making this method useful
* in converting path-like names into fully qualified classnames.
*
* IMPLEMENTATION DIFFERENCE - The Rails version of this
* method also converts '/' characters to '::' because that reflects
* the normal syntax for fully qualified names in Ruby.
*
*
*
* Input
* Output
*
*
* "foo_bar", false
* "FooBar"
*
*
* "foo_bar", true
* "fooBar"
*
*
* "foo_bar/baz", false
* "FooBar.Baz"
*
*
* "foo_bar/baz", true
* "fooBar.Baz"
*
*
*
* @param word Word to be converted
* @param flag Flag indicating that the initial character should
* be lower cased instead of upper cased
*/
public String camelize(String word, boolean flag) {
if (word.length() == 0) {
return word;
}
StringBuilder sb = new StringBuilder(word.length());
if (flag) {
sb.append(Character.toLowerCase(word.charAt(0)));
} else {
sb.append(Character.toUpperCase(word.charAt(0)));
}
boolean capitalize = false;
for (int i = 1; i < word.length(); i++) {
char ch = word.charAt(i);
if (capitalize) {
sb.append(Character.toUpperCase(ch));
capitalize = false;
} else if (ch == '_') {
capitalize = true;
} else if (ch == '/') {
capitalize = true;
sb.append('.');
} else {
sb.append(ch);
}
}
return sb.toString();
}
/**
* Create and return a simple class name that corresponds to a
* addPlural table name. Any leading schema name will be trimmed.
*
*
*
* Input
* Output
*
*
* "foo_bars"
* "FooBar"
*
*
* "baz"
* "Baz"
*
*
*
* @param tableName Table name to be converted
*/
public String classify(String tableName) {
int period = tableName.lastIndexOf('.');
if (period >= 0) {
tableName = tableName.substring(period + 1);
}
return camelize(singularize(tableName));
}
/**
* Replace underscores in the specified word with dashes.
*
*
*
* Input
* Output
*
*
* "foo_bar"
* "foo-bar"
*
*
* "baz"
* "baz"
*
*
*
* @param word Word to be converted
*/
public String dasherize(String word) {
return word.replace('_', '-');
}
/**
* Remove any package name from a fully qualified class name,
* returning only the simple classname.
*
*
*
* Input
* Output
*
*
* "java.util.Map"
* "Map"
*
*
* "String"
* "String"
*
*
*
* @param className Fully qualified class name to be converted
*/
public String demodulize(String className) {
int period = className.lastIndexOf('.');
if (period >= 0) {
return className.substring(period + 1);
} else {
return className;
}
}
/**
* Create and return a foreign key name from a class name,
* separating the "id" suffix with an underscore.
*/
public String foreignKey(String className) {
return foreignKey(className, true);
}
/**
* Create and return a foreign key name from a class name,
* optionally inserting an underscore before the "id" portion.
*
*
*
* Input
* Output
*
*
* "com.mymodel.Order", false
* "orderid"
*
*
* "com.mymodel.Order", true
* "order_id"
*
*
* "Message", false
* "messageid"
*
*
* "Message", true
* "message_id"
*
*
*
* @param className Class name for which to create a foreign key
* @param underscore Flag indicating whether an underscore should
* be emitted between the class name and the "id" suffix
*/
public String foreignKey(String className, boolean underscore) {
return underscore(demodulize(className) + (underscore ? "_id" : "id"));
}
/**
* Capitalize the first word in a lower cased and underscored string,
* turn underscores into spaces, and string any trailing "_id". Like
* titleize()
, this is meant for creating pretty output,
* and is not intended for code generation.
*
*
*
* Input
* Output
*
*
* "employee_salary"
* "Employee salary"
*
*
* "author_id"
* "Author"
*
*
*
* @param words Word string to be converted
*/
public String humanize(String words) {
if (words.endsWith("_id")) {
words = words.substring(0, words.length() - 3);
}
StringBuilder sb = new StringBuilder(words.length());
sb.append(Character.toUpperCase(words.charAt(0)));
for (int i = 1; i < words.length(); i++) {
char ch = words.charAt(i);
if (ch == '_') {
sb.append(' ');
} else {
sb.append(ch);
}
}
return sb.toString();
}
/**
* Turn a number into a corresponding ordinal string used to
* denote the position in an ordered sequence.
*
*
*
* Input
* Output
*
*
* 1
* "1st"
*
*
* 2
* "2nd"
*
*
* 3
* "3rd"
*
*
* 4
* "rth"
*
*
* 1002
* "1002nd"
*
*
* 2012
* "2012th"
*
*
*
* @param number Number to be converted
*/
public String ordinalize(int number) {
int modulo = number % 100;
if ((modulo >= 11) && (modulo <= 13)) {
return "" + number + "th";
}
switch (number % 10) {
case 1:
return "" + number + "st";
case 2:
return "" + number + "nd";
case 3:
return "" + number + "rd";
default:
return "" + number + "th";
}
}
/**
* Return a addPlural version of the specified (addSingular) word.
*
*
* @param word Singular word to be converted
*/
public String pluralize(String word) {
// Scan uncountables and leave alone
for (int i = 0; i < uncountables.size(); i++) {
if (uncountables.get(i).equals(word)) {
return word;
}
}
// Scan our patterns for a match and return the correct replacement
for (int i = 0; i < plurals.size(); i++) {
String replacement = plurals.get(i).replacement(word);
if (replacement != null) {
return replacement;
}
}
// Return the original string unchanged
return word;
}
/**
* Return a addSingular version of the specified (addPlural) word.
*
*
* @param word Plural word to be converted
*/
public String singularize(String word) {
// Scan uncountables and leave alone
for (int i = 0; i < uncountables.size(); i++) {
if (uncountables.get(i).equals(word)) {
return word;
}
}
// Scan our patterns for a match and return the correct replacement
for (int i = 0; i < singulars.size(); i++) {
String replacement = singulars.get(i).replacement(word);
if (replacement != null) {
return replacement;
}
}
// Return the original string unchanged
return word;
}
/**
* Convert the simple name of a model class into the corresponding
* name of a database table, by uncamelizing, inserting underscores,
* and pluralizing the last word.
*
*
*
* Input
* Output
*
*
* "RawScaledScorer"
* "raw_scaled_scorers"
*
*
* "fancyCategory"
* "fancy_categories"
*
*
*
* @param className Class name to be converted
*/
public String tableize(String className) {
return pluralize(underscore(className));
}
/**
* Capitalize all the words, and replace some characters in the string
* to create a nicer looking title. This is meant for creating pretty
* output, and is not intended for code generation.
*
*
*
* Input
* Output
*
*
* "the honeymooners"
* "The Honeymooners"
*
*
* "x-men: the last stand"
* "X Men: The Last Stand"
*
*
*
* @param words Word string to be converted
*/
public String titleize(String words) {
StringBuilder sb = new StringBuilder(words.length());
boolean capitalize = true; // To get the first character right
for (int i = 0; i < words.length(); i++) {
char ch = words.charAt(i);
if (Character.isWhitespace(ch)) {
sb.append(' ');
capitalize = true;
} else if (ch == '-') {
sb.append(' ');
capitalize = true;
} else if (capitalize) {
sb.append(Character.toUpperCase(ch));
capitalize = false;
} else {
sb.append(ch);
}
}
return sb.toString();
}
public String decapitalize(String word) {
// do nothing if null or empty
if ((word == null) || (word.length() < 1)) {
return word;
}
// or if already decapitalized
char first = word.charAt(0);
if (Character.isLowerCase(first)) {
return word;
}
// otherwise turn the first character to lower case and attach the rest
StringBuilder sb = new StringBuilder(word.length());
sb.append(Character.toLowerCase(first));
sb.append(word.substring(1));
return sb.toString();
}
/**
* The reverse of camelize()
, makes an underscored form
* from the expression in the string. Changes "." to "/" to convert
* fully qualified class names into paths.
*
*
*
* Input
* Output
*
*
* "FooBar"
* "foo_bar"
*
*
* "fooBar"
* "foo_bar"
*
*
* "FooBar.Baz"
* "foo_bar/baz"
*
*
* "FooBar.Baz"
* "foo_bar/baz"
*
*
*
* @param word Camel cased word to be converted
*/
public String underscore(String word) {
StringBuilder sb = new StringBuilder(word.length() + 5);
boolean uncapitalize = false;
for (int i = 0; i < word.length(); i++) {
char ch = word.charAt(i);
if (uncapitalize) {
sb.append(Character.toLowerCase(ch));
uncapitalize = false;
} else if (ch == '.') {
sb.append('/');
uncapitalize = true;
} else if (Character.isUpperCase(ch)) {
if (i > 0) {
sb.append('_');
}
sb.append(Character.toLowerCase(ch));
} else {
sb.append(ch);
}
}
return sb.toString();
}
// --------------------------------------------------- Customization Methods
/**
* Add the addSingular and addPlural forms of words that cannot be
* converted using the normal rules.
*
*
* @param singular Singular form of the word
* @param plural Plural form of the word
*/
public void addIrregular(String singular, String plural) {
addPlural("(.*)(" + singular.substring(0, 1) + ")" + singular.substring(1) + "$",
"\\1\\2" + plural.substring(1));
addSingular("(.*)(" + plural.substring(0, 1) + ")" + plural.substring(1) + "$",
"\\1\\2" + singular.substring(1));
}
/**
* Add a match pattern and replacement rule for converting addPlural
* forms to addSingular forms. By default, matches will be case
* insensitive.
*
*
* @param match Match pattern regular expression
* @param rule Replacement rule
*/
public void addPlural(String match, String rule) {
addPlural(match, rule, true);
}
/**
* Add a match pattern and replacement rule for converting addPlural
* forms to addSingular forms.
*
*
* @param match Match pattern regular expression
* @param rule Replacement rule
* @param insensitive Flag indicating this match should be case insensitive
*/
public void addPlural(String match, String rule, boolean insensitive) {
plurals.add(0, new Replacer(match, rule, insensitive));
}
/**
* Add a match pattern and replacement rule for converting addSingular
* forms to addPlural forms. By default, matches will be case insensitive.
*
*
* @param match Match pattern regular expression
* @param rule Replacement rule
*/
public void addSingular(String match, String rule) {
addSingular(match, rule, true);
}
/**
* Add a match pattern and replacement rule for converting addSingular
* forms to addPlural forms.
*
*
* @param match Match pattern regular expression
* @param rule Replacement rule
* @param insensitive Flag indicating this match should be case insensitive
*/
public void addSingular(String match, String rule, boolean insensitive) {
singulars.add(0, new Replacer(match, rule, insensitive));
}
/**
* Add a word that cannot be converted between addSingular and addPlural.
*
*
* @param word Word to be added
*/
public void addUncountable(String word) {
uncountables.add(0, word.toLowerCase());
}
// --------------------------------------------------------- Private Classes
/**
* Internal class that uses a regular expression matcher to both
* match the specified regular expression to a specified word, and
* (if successful) perform the appropriate substitutions.
*/
private static class Replacer {
// --------------------------------------------------------- Constructor
public Replacer(String match, String rule, boolean insensitive) {
pattern = Pattern.compile(match,
insensitive ? Pattern.CASE_INSENSITIVE : 0);
this.rule = rule;
}
// -------------------------------------------------- Instance Variables
private Pattern pattern = null;
private String rule = null;
// ------------------------------------------------------ Public Methods
/**
* Replace the input if it matches the pattern.
*
* @param input the input string.
* @return the replacement, if the input matches, otherwise null.
*/
public String replacement(String input) {
Matcher matcher = pattern.matcher(input);
if (matcher.matches()) {
StringBuilder sb = new StringBuilder();
boolean group = false;
for (int i = 0; i < rule.length(); i++) {
char ch = rule.charAt(i);
if (group) {
sb.append(matcher.group(Character.digit(ch, 10)));
group = false;
} else if (ch == '\\') {
group = true;
} else {
sb.append(ch);
}
}
return sb.toString();
} else {
return null;
}
}
}
}