001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one
003 *  or more contributor license agreements.  See the NOTICE file
004 *  distributed with this work for additional information
005 *  regarding copyright ownership.  The ASF licenses this file
006 *  to you under the Apache License, Version 2.0 (the
007 *  "License"); you may not use this file except in compliance
008 *  with the License.  You may obtain a copy of the License at
009 *
010 *        http://www.apache.org/licenses/LICENSE-2.0
011 *
012 *  Unless required by applicable law or agreed to in writing,
013 *  software distributed under the License is distributed on an
014 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 *  KIND, either express or implied.  See the License for the
016 *  specific language governing permissions and limitations
017 *  under the License.
018 */
019
020package org.apache.isis.core.commons.lang;
021
022import java.io.File;
023import java.util.Arrays;
024import java.util.Collections;
025import java.util.List;
026import java.util.StringTokenizer;
027
028import com.google.common.base.Strings;
029
030import org.apache.isis.applib.util.Enums;
031
032public final class StringExtensions {
033    
034    static final char SPACE = ' ';
035
036    private StringExtensions() {
037    }
038
039    // ////////////////////////////////////////////////////////////
040    // naturalName, naturalize, simpleName, camel, memberIdFor
041    // ////////////////////////////////////////////////////////////
042
043    /**
044     * Returns a word spaced version of the specified name, so there are spaces
045     * between the words, where each word starts with a capital letter. E.g.,
046     * "NextAvailableDate" is returned as "Next Available Date".
047     */
048    public static String asNaturalName2(final String name) {
049    
050        final int length = name.length();
051    
052        if (length <= 1) {
053            return name.toUpperCase();// ensure first character is upper case
054        }
055    
056        final StringBuffer naturalName = new StringBuffer(length);
057    
058        char previousCharacter;
059        char character = Character.toUpperCase(name.charAt(0));// ensure first
060                                                               // character is
061                                                               // upper case
062        naturalName.append(character);
063        char nextCharacter = name.charAt(1);
064    
065        for (int pos = 2; pos < length; pos++) {
066            previousCharacter = character;
067            character = nextCharacter;
068            nextCharacter = name.charAt(pos);
069    
070            if (previousCharacter != StringExtensions.SPACE) {
071                if (Character.isUpperCase(character) && !Character.isUpperCase(previousCharacter)) {
072                    naturalName.append(StringExtensions.SPACE);
073                }
074                if (Character.isUpperCase(character) && Character.isLowerCase(nextCharacter) && Character.isUpperCase(previousCharacter)) {
075                    naturalName.append(StringExtensions.SPACE);
076                }
077                if (Character.isDigit(character) && !Character.isDigit(previousCharacter)) {
078                    naturalName.append(StringExtensions.SPACE);
079                }
080            }
081            naturalName.append(character);
082        }
083        naturalName.append(nextCharacter);
084        return naturalName.toString();
085    }
086
087    public static String asNaturalName(final String extendee) {
088
089        int pos = 0;
090
091        // find first upper case character
092        while ((pos < extendee.length()) && Character.isLowerCase(extendee.charAt(pos))) {
093            pos++;
094        }
095
096        if (pos == extendee.length()) {
097            return "invalid name";
098        }
099        return naturalized(extendee, pos);
100    }
101
102    public static String asNaturalized(final String extendee) {
103        return naturalized(extendee, 0);
104    }
105
106    private static String naturalized(final String name, final int startingPosition) {
107        if (name.length() <= startingPosition) {
108            throw new IllegalArgumentException("string shorter than starting position provided");
109        }
110        final StringBuffer s = new StringBuffer(name.length() - startingPosition);
111        for (int j = startingPosition; j < name.length(); j++) { // process
112                                                                 // english name
113                                                                 // - add spaces
114            if ((j > startingPosition) && isStartOfNewWord(name.charAt(j), name.charAt(j - 1))) {
115                s.append(' ');
116            }
117            if (j == startingPosition) {
118                s.append(Character.toUpperCase(name.charAt(j)));
119            } else {
120                s.append(name.charAt(j));
121            }
122        }
123        final String str = s.toString();
124        return str;
125    }
126
127    private static boolean isStartOfNewWord(final char c, final char previousChar) {
128        return Character.isUpperCase(c) || Character.isDigit(c) && !Character.isDigit(previousChar);
129    }
130
131    public static String asSimpleName(final String extendee) {
132        final int lastDot = extendee.lastIndexOf('.');
133        if (lastDot == -1) {
134            return extendee;
135        }
136        if (lastDot == extendee.length() - 1) {
137            throw new IllegalArgumentException("Name cannot end in '.'");
138        }
139        return extendee.substring(lastDot + 1);
140    }
141    
142    public static String asCamel(final String extendee) {
143        final StringBuffer b = new StringBuffer(extendee.length());
144        final StringTokenizer t = new StringTokenizer(extendee);
145        b.append(t.nextToken());
146        while (t.hasMoreTokens()) {
147            final String token = t.nextToken();
148            b.append(token.substring(0, 1).toUpperCase()); // replace spaces
149                                                           // with
150            // camelCase
151            b.append(token.substring(1));
152        }
153        return b.toString();
154    }
155
156    // TODO: combine with camel
157    public static String asCamelLowerFirst(final String extendee) {
158        final StringBuffer b = new StringBuffer(extendee.length());
159        final StringTokenizer t = new StringTokenizer(extendee);
160        b.append(asLowerFirst(t.nextToken()));
161        while (t.hasMoreTokens()) {
162            final String token = t.nextToken();
163            b.append(token.substring(0, 1).toUpperCase()); // replace spaces
164                                                           // with camelCase
165            b.append(token.substring(1).toLowerCase());
166        }
167        return b.toString();
168    }
169
170    public static String asLowerDashed(String extendee) {
171        return extendee.toLowerCase().replaceAll("\\s+", "-");
172    }
173
174    public static String asPascal(final String extendee) {
175        return capitalize(asCamel(extendee));
176    }
177
178    public static String asMemberIdFor(final String extendee) {
179        return asLowerFirst(asCamel(extendee));
180    }
181
182    // ////////////////////////////////////////////////////////////
183    // capitalize, lowerFirst, firstWord
184    // ////////////////////////////////////////////////////////////
185
186    public static String capitalize(final String extendee) {
187        if (extendee == null || extendee.length() == 0) {
188            return extendee;
189        }
190        if (extendee.length() == 1) {
191            return extendee.toUpperCase();
192        }
193        return Character.toUpperCase(extendee.charAt(0)) + extendee.substring(1);
194    }
195
196    /**
197     * Simply forces first char to be lower case.
198     */
199    public static String asLowerFirst(final String extendee) {
200        if (Strings.isNullOrEmpty(extendee)) {
201            return extendee;
202        }
203        if (extendee.length() == 1) {
204            return extendee.toLowerCase();
205        }
206        return extendee.substring(0, 1).toLowerCase() + extendee.substring(1);
207    }
208
209    public static String asFirstWord(final String extendee) {
210        final String[] split = extendee.split(" ");
211        return split[0];
212    }
213
214
215    // ////////////////////////////////////////////////////////////
216    // in, combinePaths, splitOnCommas
217    // ////////////////////////////////////////////////////////////
218
219    public static boolean in(final String extendee, final String[] strings) {
220        for (final String strCandidate : strings) {
221            if (strCandidate.equals(extendee)) {
222                return true;
223            }
224        }
225        return false;
226    }
227
228    public static String combinePaths(final String extendee, final String... furtherPaths) {
229        final StringBuilder pathBuf = new StringBuilder(extendee);
230        for (final String furtherPath : furtherPaths) {
231            if (pathBuf.charAt(pathBuf.length() - 1) != File.separatorChar) {
232                pathBuf.append(File.separatorChar);
233            }
234            pathBuf.append(furtherPath);
235        }
236        return pathBuf.toString();
237    }
238
239    public static List<String> splitOnCommas(final String commaSeparatedExtendee) {
240        if (commaSeparatedExtendee == null) {
241            return null;
242        }
243        final String removeLeadingWhiteSpace = removeLeadingWhiteSpace(commaSeparatedExtendee);
244        // special handling
245        if (removeLeadingWhiteSpace.length() == 0) {
246            return Collections.emptyList();
247        }
248        final String[] splitAsArray = removeLeadingWhiteSpace.split("\\W*,\\W*");
249        return Arrays.asList(splitAsArray);
250    }
251
252
253    private static final char CARRIAGE_RETURN = '\n';
254    private static final char LINE_FEED = '\r';
255
256    /**
257     * Converts any <tt>\n</tt> to <tt>line.separator</tt>
258     * 
259     * @param extendee
260     * @return
261     */
262    public static String lineSeparated(final String extendee) {
263        final StringBuilder buf = new StringBuilder();
264        final String lineSeparator = System.getProperty("line.separator");
265        boolean lastWasLineFeed = false;
266        for (final char c : extendee.toCharArray()) {
267            final boolean isLineFeed = c == LINE_FEED;
268            final boolean isCarriageReturn = c == CARRIAGE_RETURN;
269            if (isCarriageReturn) {
270                buf.append(lineSeparator);
271                lastWasLineFeed = false;
272            } else {
273                if (lastWasLineFeed) {
274                    buf.append(LINE_FEED);
275                }
276                if (isLineFeed) {
277                    lastWasLineFeed = true;
278                } else {
279                    buf.append(c);
280                    lastWasLineFeed = false;
281                }
282            }
283        }
284        if (lastWasLineFeed) {
285            buf.append(LINE_FEED);
286        }
287        return buf.toString();
288    }
289
290    // ////////////////////////////////////////////////////////////
291    // removeTabs, removeLeadingWhiteSpace, stripLeadingSlash, stripNewLines,
292    // normalize
293    // ////////////////////////////////////////////////////////////
294
295    public static String removeTabs(final String extendee) {
296        // quick return - jvm java should always return here
297        if (extendee.indexOf('\t') == -1) {
298            return extendee;
299        }
300        final StringBuffer buf = new StringBuffer();
301        for (int i = 0; i < extendee.length(); i++) {
302            // a bit clunky to stay with j# api
303            if (extendee.charAt(i) != '\t') {
304                buf.append(extendee.charAt(i));
305            }
306        }
307        return buf.toString();
308    }
309
310    public static String removeLeadingWhiteSpace(final String extendee) {
311        if (extendee == null) {
312            return null;
313        }
314        return extendee.replaceAll("^\\W*", "");
315    }
316
317    public static String stripNewLines(final String extendee) {
318        return extendee.replaceAll("[\r\n]", "");
319    }
320
321    public static String stripLeadingSlash(final String extendee) {
322        if (!extendee.startsWith("/")) {
323            return extendee;
324        }
325        if (extendee.length() < 2) {
326            return "";
327        }
328        return extendee.substring(1);
329    }
330
331    /**
332     * Condenses any whitespace to a single character
333     * 
334     * @param extendee
335     * @return
336     */
337    public static String normalized(final String extendee) {
338        if (extendee == null) {
339            return null;
340        }
341        return extendee.replaceAll("\\s+", " ");
342    }
343
344    public static String removePrefix(final String extendee, final String prefix) {
345        return extendee.startsWith(prefix) 
346                ? extendee.substring(prefix.length()) 
347                : extendee;
348    }
349
350    public static String enumTitle(String enumName) {
351        return Enums.getFriendlyNameOf(enumName);
352    }
353
354    public static String enumDeTitle(String enumFriendlyName) {
355        return Enums.getEnumNameFromFriendly(enumFriendlyName);
356    }
357
358    /*
359     * eg converts <tt>HiddenFacetForMemberAnnotation</tt> to <tt>HFFMA</tt>.
360     */
361    public static String toAbbreviation(final String extendee) {
362        final StringBuilder buf = new StringBuilder();
363        for(char c: extendee.toCharArray()) {
364            if(Character.isUpperCase(c)) {
365                buf.append(c);
366            }
367        }
368        return buf.toString();
369    }
370
371    
372    // //////////////////////////////////////
373    // copied in from Apache Commons
374    // //////////////////////////////////////
375
376    
377    public static final String EMPTY = "";
378    /**
379     * <p>The maximum size to which the padding constant(s) can expand.</p>
380     */
381    private static final int PAD_LIMIT = 8192;
382
383    /**
384     * <p>Repeat a String <code>repeat</code> times to form a
385     * new String.</p>
386     *
387     * <pre>
388     * StringUtils.repeat(null, 2) = null
389     * StringUtils.repeat("", 0)   = ""
390     * StringUtils.repeat("", 2)   = ""
391     * StringUtils.repeat("a", 3)  = "aaa"
392     * StringUtils.repeat("ab", 2) = "abab"
393     * StringUtils.repeat("a", -2) = ""
394     * </pre>
395     *
396     * @param extendee  the String to repeat, may be null
397     * @param repeat  number of times to repeat str, negative treated as zero
398     * @return a new String consisting of the original String repeated,
399     *  <code>null</code> if null String input
400     */
401    public static String repeat(final String extendee, int repeat) {
402        // Performance tuned for 2.0 (JDK1.4)
403
404        if (extendee == null) {
405            return null;
406        }
407        if (repeat <= 0) {
408            return EMPTY;
409        }
410        int inputLength = extendee.length();
411        if (repeat == 1 || inputLength == 0) {
412            return extendee;
413        }
414        if (inputLength == 1 && repeat <= PAD_LIMIT) {
415            return padding(repeat, extendee.charAt(0));
416        }
417
418        int outputLength = inputLength * repeat;
419        switch (inputLength) {
420            case 1 :
421                char ch = extendee.charAt(0);
422                char[] output1 = new char[outputLength];
423                for (int i = repeat - 1; i >= 0; i--) {
424                    output1[i] = ch;
425                }
426                return new String(output1);
427            case 2 :
428                char ch0 = extendee.charAt(0);
429                char ch1 = extendee.charAt(1);
430                char[] output2 = new char[outputLength];
431                for (int i = repeat * 2 - 2; i >= 0; i--, i--) {
432                    output2[i] = ch0;
433                    output2[i + 1] = ch1;
434                }
435                return new String(output2);
436            default :
437                StringBuilder buf = new StringBuilder(outputLength);
438                for (int i = 0; i < repeat; i++) {
439                    buf.append(extendee);
440                }
441                return buf.toString();
442        }
443    }
444
445    /**
446     * <p>Returns padding using the specified delimiter repeated
447     * to a given length.</p>
448     *
449     * <pre>
450     * StringUtils.padding(0, 'e')  = ""
451     * StringUtils.padding(3, 'e')  = "eee"
452     * StringUtils.padding(-2, 'e') = IndexOutOfBoundsException
453     * </pre>
454     *
455     * <p>Note: this method doesn't not support padding with
456     * <a href="http://www.unicode.org/glossary/#supplementary_character">Unicode Supplementary Characters</a>
457     * as they require a pair of <code>char</code>s to be represented.
458     * If you are needing to support full I18N of your applications
459     * consider using {@link #repeat(String, int)} instead. 
460     * </p>
461     *
462     * @param repeat  number of times to repeat delim
463     * @param padChar  character to repeat
464     * @return String with repeated character
465     * @throws IndexOutOfBoundsException if <code>repeat &lt; 0</code>
466     * @see #repeat(String, int)
467     */
468    private static String padding(int repeat, char padChar) throws IndexOutOfBoundsException {
469        if (repeat < 0) {
470            throw new IndexOutOfBoundsException("Cannot pad a negative amount: " + repeat);
471        }
472        final char[] buf = new char[repeat];
473        for (int i = 0; i < buf.length; i++) {
474            buf[i] = padChar;
475        }
476        return new String(buf);
477    }
478
479    public static boolean startsWith(final String extendee, final String prefix) {
480        final int length = prefix.length();
481        if (length >= extendee.length()) {
482            return false;
483        } else {
484            final char startingCharacter = extendee.charAt(length);
485            return extendee.startsWith(prefix) && Character.isUpperCase(startingCharacter);
486        }
487    }
488
489    public static String combinePath(final String extendee, final String suffix) {
490        if (Strings.isNullOrEmpty(extendee) && Strings.isNullOrEmpty(suffix)) {
491            return "";
492        }
493        if (Strings.isNullOrEmpty(extendee)) {
494            return suffix;
495        }
496        if (Strings.isNullOrEmpty(suffix)) {
497            return extendee;
498        }
499        if (extendee.endsWith("/") || suffix.startsWith("/")) {
500            return extendee + suffix;
501        }
502        return extendee + "/" + suffix;
503    }
504
505    /**
506     * Returns the name of a Java entity without any prefix. A prefix is defined
507     * as the first set of lowercase letters and the name is characters from,
508     * and including, the first upper case letter. If no upper case letter is
509     * found then an empty string is returned.
510     * 
511     * <p>
512     * Calling this method with the following Java names will produce these
513     * results:
514     * 
515     * <pre>
516     *                     getCarRegistration        -&gt; CarRegistration
517     *                     CityMayor -&gt; CityMayor
518     *                     isReady -&gt; Ready
519     * </pre>
520     * 
521     */
522    public static String asJavaBaseName(final String javaName) {
523        int pos = 0;
524    
525        // find first upper case character
526        final int len = javaName.length();
527    
528        while ((pos < len) && (javaName.charAt(pos) != '_') && Character.isLowerCase(javaName.charAt(pos))) {
529            pos++;
530        }
531    
532        if (pos >= len) {
533            return "";
534        }
535    
536        if (javaName.charAt(pos) == '_') {
537            pos++;
538        }
539    
540        if (pos >= len) {
541            return "";
542        }
543    
544        final String baseName = javaName.substring(pos);
545        final char firstChar = baseName.charAt(0);
546    
547        if (Character.isLowerCase(firstChar)) {
548            return Character.toUpperCase(firstChar) + baseName.substring(1);
549        } else {
550            return baseName;
551        }
552    }
553
554    public static String asJavaBaseNameStripAccessorPrefixIfRequired(final String javaNameExtendee) {
555        if (javaNameExtendee.startsWith("is") || javaNameExtendee.startsWith("get")) {
556            return asJavaBaseName(javaNameExtendee);
557        } else {
558            return StringExtensions.asCapitalizedName(javaNameExtendee);
559        }
560    }
561
562    public static String asCapitalizedName(final String extendee) {
563        return Character.toUpperCase(extendee.charAt(0)) + extendee.substring(1);
564    }
565
566
567    public static String asPluralName(final String extendee) {
568        String pluralName;
569        if (extendee.endsWith("y")) {
570            pluralName = extendee.substring(0, extendee.length() - 1) + "ies";
571        } else if (extendee.endsWith("s") || extendee.endsWith("x")) {
572            pluralName = extendee + "es";
573        } else {
574            pluralName = extendee + 's';
575        }
576        return pluralName;
577    }
578
579    public static String toCamelCase(final String extendee) {
580        final String nameLower = extendee.toLowerCase();
581        final StringBuilder buf = new StringBuilder();
582        boolean capitalizeNext = false;
583        for (int i = 0; i < nameLower.length(); i++) {
584            final char ch = nameLower.charAt(i);
585            if (ch == '_') {
586                capitalizeNext = true;
587            } else {
588                if (capitalizeNext) {
589                    buf.append(Character.toUpperCase(ch));
590                } else {
591                    buf.append(ch);
592                }
593                capitalizeNext = false;
594            }
595        }
596        return buf.toString();
597    }
598
599
600
601    
602}