001/**
002 *
003 * Copyright (c) 2014, the Railo Company Ltd. All rights reserved.
004 *
005 * This library is free software; you can redistribute it and/or
006 * modify it under the terms of the GNU Lesser General Public
007 * License as published by the Free Software Foundation; either 
008 * version 2.1 of the License, or (at your option) any later version.
009 * 
010 * This library is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013 * Lesser General Public License for more details.
014 * 
015 * You should have received a copy of the GNU Lesser General Public 
016 * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
017 * 
018 **/
019package lucee.commons.lang;
020
021import java.nio.charset.CharsetEncoder;
022import java.util.HashMap;
023import java.util.Iterator;
024import java.util.Map;
025
026import lucee.runtime.exp.PageException;
027import lucee.runtime.op.Caster;
028import lucee.runtime.type.Collection;
029import lucee.runtime.type.util.ArrayUtil;
030
031
032
033
034
035/**
036 * Util to do some additional String Operations
037 */
038public final class StringUtil {
039        
040        private static final char[] SPECIAL_WHITE_SPACE_CHARS=new char[]{
041                 0x85           // NEL, Next line
042                ,0xa0           // no-break space
043                ,0x1680         // ogham space mark
044                ,0x180e         // mongolian vowel separator
045                ,0x2000         // en quad
046                ,0x2001         // em quad
047                ,0x2002         // en space
048                ,0x2003         // em space
049                ,0x2004         // three-per-em space
050                ,0x2005         // four-per-em space
051                ,0x2006         // six-per-em space
052                ,0x2007         // figure space
053                ,0x2008         // punctuation space
054                ,0x2009         // thin space
055                ,0x200A         // hair space
056                ,0x2028         // line separator
057                ,0x2029         // paragraph separator
058                ,0x202F         // narrow no-break space
059                ,0x205F         // medium mathematical space
060                ,0x3000         // ideographic space
061        };
062        
063    
064        /**
065         * do first Letter Upper case
066         * @param str String to operate
067         * @return uppercase string
068         */
069        public static String ucFirst(String str) {
070                if(str==null) return null;
071                else if(str.length()<=1) return str.toUpperCase();
072                else {
073                        return str.substring(0,1).toUpperCase()+str.substring(1);
074                }
075        }
076
077
078    public static String capitalize( String input, char[] delims ) {
079
080        if (isEmpty(input)) return input;
081
082        if (ArrayUtil.isEmpty(delims))
083            delims = new char[]{ '.', '-', '(', ')' };
084
085        StringBuilder sb = new StringBuilder( input.length() );
086
087        boolean isLastDelim = true,isLastSpace = true;
088        int len=input.length();
089        for (int i=0; i<len; i++) {
090
091            char c = input.charAt( i );
092
093            if ( Character.isWhitespace(c) ) {
094
095                if ( !isLastSpace )
096                    sb.append( ' ' );
097
098                isLastSpace = true;
099            } 
100            else {
101
102                sb.append( ( isLastSpace || isLastDelim ) ? Character.toUpperCase( c ) : c );
103
104                isLastDelim = _contains(delims, c );
105                isLastSpace = false;
106            }
107        }
108
109        return sb.toString();
110    }
111
112        
113        private static boolean _contains(char[] chars, char c) {
114                for ( int i=0; i<chars.length; i++ ) {
115                        if(chars[i]==c) return true;
116                }
117                return false;
118        }
119
120
121        /**
122         * do first Letter Upper case
123         * @param str String to operate
124         * @return lower case String
125         */
126        public static String lcFirst(String str) {
127                if(str==null) return null;
128                else if(str.length()<=1) return str.toLowerCase();
129                else {
130                        return str.substring(0,1).toLowerCase()+str.substring(1);
131                }
132        }
133        
134        /**
135         * Unescapes HTML Tags
136         * @param html html code  to escape
137         * @return escaped html code
138         */
139        public static String unescapeHTML(String html) {
140                return HTMLEntities.unescapeHTML(html);
141        }
142        
143        /**
144         * Escapes XML Tags
145         * @param html html code to unescape
146         * @return unescaped html code
147         */
148        public static String escapeHTML(String html) {
149                return HTMLEntities.escapeHTML(html);
150        }
151
152        /**
153         * escapes JS sensitive characters
154         * @param str String to escape
155         * @return escapes String
156         */
157        public static String escapeJS(String str, char quotesUsed) {
158                return escapeJS(str,quotesUsed, (CharsetEncoder)null);
159        }
160
161        public static String escapeJS(String str, char quotesUsed, java.nio.charset.Charset charset) {
162                return escapeJS(str, quotesUsed, charset==null?null:charset.newEncoder());
163        }
164
165        /**
166         * escapes JS sensitive characters
167         * @param str String to escape
168         * @param charset if not null, it checks if the given string is supported by the encoding, if not, lucee encodes the string
169         * @return escapes String
170         */
171        public static String escapeJS(String str, char quotesUsed, CharsetEncoder enc) {
172                char[] arr=str.toCharArray();
173                StringBuilder rtn=new StringBuilder(arr.length);
174                rtn.append(quotesUsed);
175                
176                for(int i=0;i<arr.length;i++) {
177                        if(arr[i] < 128){
178                                switch(arr[i]) {
179                                        case '\\': rtn.append("\\\\"); break;
180                                        case '\n': rtn.append("\\n"); break;
181                                        case '\r': rtn.append("\\r"); break;
182                                        case '\f': rtn.append("\\f"); break;
183                                        case '\b': rtn.append("\\b"); break;
184                                        case '\t': rtn.append("\\t"); break;
185                                        case '"' : 
186                                                if(quotesUsed=='"') rtn.append("\\\"");
187                                                else rtn.append('"'); 
188                                        break;
189                                        case '\'': 
190                                                if(quotesUsed=='\'') rtn.append("\\\'");
191                                                else rtn.append('\''); 
192                                        break;
193                                        case '/': 
194                                                // escape </script>
195                                                if(
196                                                        i>0 && arr[i-1]=='<'
197                                                        && i+1<arr.length && arr[i+1]=='s'
198                                                        && i+2<arr.length && arr[i+2]=='c'
199                                                        && i+3<arr.length && arr[i+3]=='r'
200                                                        && i+4<arr.length && arr[i+4]=='i'
201                                                        && i+5<arr.length && arr[i+5]=='p'
202                                                        && i+6<arr.length && arr[i+6]=='t'
203                                                        && i+7<arr.length && (isWhiteSpace(arr[i+7]) || arr[i+7]=='>')
204                                                        
205                                                ) {
206                                                        rtn.append("\\/");
207                                                        break;
208                                                } 
209                                        
210                                        default : rtn.append(arr[i]); break;
211                                }
212                        }
213                        else if(enc==null || !enc.canEncode(arr[i])) {
214                                if (arr[i] < 0x10)                   rtn.append("\\u000");
215                            else if (arr[i] < 0x100)         rtn.append( "\\u00");
216                            else if (arr[i] < 0x1000)        rtn.append( "\\u0");
217                            else                                                rtn.append( "\\u");
218                                rtn.append(Integer.toHexString(arr[i]));
219                        }
220                        else {
221                                rtn.append(arr[i]);
222                        }
223                }
224                return rtn.append(quotesUsed).toString();
225        }
226
227        /**
228         * reapeats a string
229         * @param str string to repeat
230         * @param count how many time string will be reapeted
231         * @return reapted string
232         */
233    public static String repeatString(String str,int count) {
234        if(count<=0) return "";
235        char[] chars = str.toCharArray();
236        char[] rtn=new char[chars.length*count];
237        int pos=0;
238        for(int i=0;i<count;i++) {
239            for(int y=0;y<chars.length;y++)rtn[pos++]=chars[y];
240            //rtn.append(str);
241        }
242        return new String(rtn);
243    }
244
245        /**
246         * translate, like method toString, a object to a string, but when value is null value will be translated to a empty String (""). 
247         * @param o Object to convert
248         * @return converted String
249         */
250        public static String toStringEmptyIfNull(Object o) {
251                if(o==null)return "";
252                return o.toString();
253        }
254        
255        public static String emptyIfNull(String str) {
256                if(str==null)return "";
257                return str;
258        }
259        
260        public static String emptyIfNull(Collection.Key key) {
261                if(key==null)return "";
262                return key.getString();
263        }
264        
265        /**
266         * escape all special characters of the regular expresson language
267         * @param str String to escape
268         * @return escaped String
269         */
270        public static String reqExpEscape(String str) {
271                char[] arr = str.toCharArray();
272                StringBuilder sb=new StringBuilder(str.length()*2);
273                
274                for(int i=0;i<arr.length;i++) {
275                        sb.append('\\');
276                        sb.append(arr[i]);
277                }
278                
279                return sb.toString();
280        }
281        
282        /**
283         * translate a string to a valid identity variable name
284         * @param varName variable name template to translate
285         * @return translated variable name
286         */
287        public static String toIdentityVariableName(String varName) {
288                char[] chars=varName.toCharArray();
289                long changes=0;
290
291                StringBuilder rtn=new StringBuilder(chars.length+2);
292                rtn.append("CF");               
293                
294                for(int i=0;i<chars.length;i++) {
295                        char c=chars[i];
296                        if((c>='a' && c<='z') ||(c>='A' && c<='Z') ||(c>='0' && c<='9'))
297                                rtn.append(c);
298                        else {  
299                                rtn.append('_');
300                                changes+=(c*(i+1));
301                        }
302                }
303                
304                return rtn.append(changes).toString();
305        }
306        /**
307         * translate a string to a valid classname string
308         * @param str string to translate
309         * @return translated String
310         */
311        public static String toClassName(String str) {
312                StringBuilder rtn=new StringBuilder();
313                String[] arr=str.split("[\\\\|//]");
314                for(int i=0;i<arr.length;i++) {
315                        if(arr[i].length()==0)continue;
316                        if(rtn.length()!=0)rtn.append('.');
317                        char[] chars=arr[i].toCharArray();
318                        long changes=0;
319                        for(int y=0;y<chars.length;y++) {
320                                char c=chars[y];
321                                if(y==0 && (c>='0' && c<='9'))rtn.append("_"+c);
322                                else if((c>='a' && c<='z') ||(c>='A' && c<='Z') ||(c>='0' && c<='9'))
323                                        rtn.append(c);
324                                else {  
325                                        rtn.append('_');
326                                        changes+=(c*(i+1));
327                                }
328                        }
329                        if(changes>0)rtn.append(changes);
330                }
331                return rtn.toString();
332        }
333
334        /**
335         * translate a string to a valid variable string
336         * @param str string to translate
337         * @return translated String
338         */
339        public static String toVariableName(String str) {
340                return toVariableName(str, true,false);
341        }
342        
343
344        public static String toJavaClassName(String str) {
345                return toVariableName(str, true, true);
346        }
347        
348        public static String toVariableName(String str, boolean addIdentityNumber, boolean allowDot) {
349                
350                StringBuilder rtn=new StringBuilder();
351                char[] chars=str.toCharArray();
352                long changes=0;
353                boolean doCorrect=true;
354                for(int i=0;i<chars.length;i++) {
355                        char c=chars[i];
356                        if(i==0 && (c>='0' && c<='9'))rtn.append("_"+c);
357                        else if((c>='a' && c<='z') ||(c>='A' && c<='Z') ||(c>='0' && c<='9') || c=='_' || c=='$' || (allowDot && c=='.'))
358                                rtn.append(c);
359                        else {  
360                            doCorrect=false;
361                                rtn.append('_');
362                                changes+=(c*(i+1));
363                        }
364                }
365                
366                if(addIdentityNumber && changes>0)rtn.append(changes);
367                //print.ln(" - "+rtn);
368                
369                if(doCorrect)return correctReservedWord(rtn.toString());
370                return rtn.toString();
371        }
372        
373
374        /**
375         * if given string is a keyword it will be replaced with none keyword
376         * @param str
377         * @return corrected word
378         */
379        private static String correctReservedWord(String str) {
380                char first=str.charAt(0);
381                
382                switch(first) {
383                case 'a':
384                        if(str.equals("abstract")) return "_"+str;
385                break;
386                case 'b':
387                        if(str.equals("boolean")) return "_"+str;
388                        else if(str.equals("break")) return "_"+str;
389                        else if(str.equals("byte")) return "_"+str;
390                break;
391                case 'c':
392                        if(str.equals("case")) return "_"+str;
393                        else if(str.equals("catch")) return "_"+str;
394                        else if(str.equals("char")) return "_"+str;
395                        else if(str.equals("const")) return "_"+str;
396                        else if(str.equals("class")) return "_"+str;
397                        else if(str.equals("continue")) return "_"+str;
398                break;
399                case 'd':
400                        if(str.equals("default")) return "_"+str;
401                        else if(str.equals("do")) return "_"+str;
402                        else if(str.equals("double")) return "_"+str;
403                break;
404                case 'e':
405                        if(str.equals("else")) return "_"+str;
406                        else if(str.equals("extends")) return "_"+str;
407                        else if(str.equals("enum")) return "_"+str;
408                break;
409                case 'f':
410                        if(str.equals("false")) return "_"+str;
411                        else if(str.equals("final")) return "_"+str;
412                        else if(str.equals("finally")) return "_"+str;
413                        else if(str.equals("float")) return "_"+str;
414                        else if(str.equals("for")) return "_"+str;
415                break;
416                case 'g':
417                        if(str.equals("goto")) return "_"+str;
418                break;
419                case 'i':
420                        if(str.equals("if")) return "_"+str;
421                        else if(str.equals("implements")) return "_"+str;
422                        else if(str.equals("import")) return "_"+str;
423                        else if(str.equals("instanceof")) return "_"+str;
424                        else if(str.equals("int")) return "_"+str;
425                        else if(str.equals("interface")) return "_"+str;
426                break;
427                case 'n':
428                        if(str.equals("native")) return "_"+str;
429                        else if(str.equals("new")) return "_"+str;
430                        else if(str.equals("null")) return "_"+str;
431                break;
432                case 'p':
433                        if(str.equals("package")) return "_"+str;
434                        else if(str.equals("private")) return "_"+str;
435                        else if(str.equals("protected")) return "_"+str;
436                        else if(str.equals("public")) return "_"+str;
437                break;
438                case 'r':
439                        if(str.equals("return")) return "_"+str;
440                break;
441                case 's':
442                        if(str.equals("short")) return "_"+str;
443                        else if(str.equals("static")) return "_"+str;
444                        else if(str.equals("strictfp")) return "_"+str;
445                        else if(str.equals("super")) return "_"+str;
446                        else if(str.equals("switch")) return "_"+str;
447                        else if(str.equals("synchronized")) return "_"+str;
448                break;
449                case 't':
450                        if(str.equals("this")) return "_"+str;
451                        else if(str.equals("throw")) return "_"+str;
452                        else if(str.equals("throws")) return "_"+str;
453                        else if(str.equals("transient")) return "_"+str;
454                        else if(str.equals("true")) return "_"+str;
455                        else if(str.equals("try")) return "_"+str;
456                break;
457                case 'v':
458                        if(str.equals("void")) return "_"+str;
459                        else if(str.equals("volatile")) return "_"+str;
460                break;
461                case 'w':
462                        if(str.equals("while")) return "_"+str;
463                break;
464                }
465                return str;
466                
467        }
468        
469        /** 
470     * This function returns a string with whitespace stripped from the beginning of str 
471     * @param str String to clean 
472     * @return cleaned String 
473     */ 
474    public static String ltrim(String str,String defaultValue) { 
475                if(str==null) return defaultValue;
476            int len = str.length(); 
477            int st = 0; 
478
479            while ((st < len) && (str.charAt(st) <= ' ')) { 
480                st++; 
481            } 
482            return ((st > 0)) ? str.substring(st) : str; 
483    } 
484
485    /** 
486     * This function returns a string with whitespace stripped from the end of str 
487     * @param str String to clean 
488     * @return cleaned String 
489     */ 
490    public static String rtrim(String str,String defaultValue) { 
491        if(str==null) return defaultValue;
492            int len = str.length(); 
493
494            while ((0 < len) && (str.charAt(len-1) <= ' ')) { 
495                len--; 
496            } 
497            return (len < str.length()) ? str.substring(0, len) : str; 
498    }
499    
500        /**
501         * trim given value, return defaultvalue when input is null
502         * @param str
503         * @param defaultValue
504         * @return trimmed string or defaultValue
505         */
506        public static String trim(String str,String defaultValue) {
507                if(str==null) return defaultValue;
508                return str.trim();
509        }
510        
511        /**
512         * 
513         * @param c character to check
514         * @param checkSpecialWhiteSpace if set to true, lucee checks also uncommon white spaces.
515         * @return
516         */
517        public static boolean isWhiteSpace(char c,boolean checkSpecialWhiteSpace) {
518                if(Character.isWhitespace(c)) return true;
519                if(checkSpecialWhiteSpace) {
520                        for(int i=0;i<SPECIAL_WHITE_SPACE_CHARS.length;i++){
521                                if(c==SPECIAL_WHITE_SPACE_CHARS[i]) return true;
522                        }
523                }
524                return false;
525        }
526        
527
528        public static boolean isWhiteSpace(char c) {
529                return isWhiteSpace(c,false);
530        }
531
532        /**
533         * trim given value, return defaultvalue when input is null
534         * this function no only removes the "classic" whitespaces, 
535         * it also removes Byte order masks forgotten to remove when reading a UTF file. 
536         * @param str
537         * @param removeBOM if set to true, Byte Order Mask that got forgotten get removed as well
538         * @param removeSpecialWhiteSpace if set to true, lucee removes also uncommon white spaces.
539         * @param defaultValue
540         * @return trimmed string or defaultValue
541         */
542        public static String trim(String str,boolean removeBOM,boolean removeSpecialWhiteSpace,String defaultValue) {
543                if(str==null) return defaultValue;
544                if(str.isEmpty()) return str;
545                // remove leading BOM Marks
546                if(removeBOM) {
547                        // UTF-16, big-endian
548                if (str.charAt(0) == '\uFEFF') str=str.substring(1);
549                else if (str.charAt(0) == '\uFFFD') str=str.substring(1);
550                
551                // UTF-16, little-endian
552                else if (str.charAt(0) == '\uFFFE') str=str.substring(1);
553                        
554                // UTF-8
555                else if(str.length()>=2) {
556                        // TODO i get this from UTF-8 files generated by suplime text, i was expecting something else
557                        if (str.charAt(0) == '\uBBEF' && str.charAt(1) == '\uFFFD') str=str.substring(2);
558                }
559                }
560                
561                if(removeSpecialWhiteSpace) {
562                        int len = str.length();
563                int startIndex = 0,endIndex=len-1;
564                // left
565                while ((startIndex < len) && isWhiteSpace(str.charAt(startIndex),true)) {
566                    startIndex++;
567                }
568                // right
569                while ((startIndex < endIndex) && isWhiteSpace(str.charAt(endIndex),true)) {
570                    endIndex--;
571                }
572                return ((startIndex > 0) || (endIndex+1 < len)) ? str.substring(startIndex, endIndex+1) : str;
573                }
574        
575        return str.trim();
576        }
577        
578        /**
579     * return if in a string are line feeds or not
580     * @param str string to check
581     * @return translated string
582     */
583    public static boolean hasLineFeed(String str) {
584        int len=str.length();
585        char c;
586        for(int i=0;i<len;i++) {
587            c=str.charAt(i);
588            if(c=='\n' || c=='\r') return true;
589        }
590        return false;
591    }
592
593    /**
594     * remove all white spaces followd by whitespaces
595     * @param str strring to translate
596     * @return translated string
597     */
598    public static String suppressWhiteSpace(String str) {
599        int len=str.length();
600        StringBuilder sb=new StringBuilder(len);
601        //boolean wasWS=false;
602        
603        char c;
604        char buffer=0;
605        for(int i=0;i<len;i++) {
606            c=str.charAt(i);
607            if(c=='\n' || c=='\r')              buffer='\n';
608            else if(isWhiteSpace(c))    {
609                if(buffer==0)buffer=c;
610            }
611            else {
612                if(buffer!=0){
613                        sb.append(buffer);
614                        buffer=0;
615                }
616                sb.append(c);
617            }
618            //sb.append(c);
619        }
620        if(buffer!=0)sb.append(buffer);
621        
622        return sb.toString();
623    }
624        
625
626
627    /**
628     * returns string, if given string is null or lengt 0 return default value
629     * @param value
630     * @param defaultValue
631     * @return value or default value
632     */
633    public static String toString(String value, String defaultValue) {
634        return value==null || value.length()==0?defaultValue:value;
635    }
636
637    /**
638     * returns string, if given string is null or lengt 0 return default value
639     * @param value
640     * @param defaultValue
641     * @return value or default value
642     */
643    public static String toString(Object value, String defaultValue) {
644        if(value==null) return defaultValue;
645        return toString(value.toString(), defaultValue);
646    }
647
648    /**
649     * cut string to max size if the string is greater, otherweise to nothing
650     * @param content
651     * @param max 
652     * @return cutted string
653     */
654    
655    public static String max(String content,int max) {
656        return max(content, max,"");
657    }
658    
659    public static String max(String content,int max, String dotDotDot) {
660        if(content==null) return null;
661        if(content.length()<=max) return content;
662        
663        return content.substring(0,max)+dotDotDot;
664    }
665    
666
667    /**
668     * performs a replace operation on a string
669     *  
670     * @param input - the string input to work on 
671     * @param find - the substring to find
672     * @param repl - the substring to replace the matches with
673     * @param firstOnly - if true then only the first occurrence of {@code find} will be replaced
674     * @param ignoreCase - if true then matches will not be case sensitive
675     * @return
676     */
677        public static String replace( String input, String find, String repl, boolean firstOnly, boolean ignoreCase ) {
678
679                int findLen = find.length();
680
681                if ( findLen == 0 )
682                        return input;
683
684                String scan = input;
685
686        if ( ignoreCase ) {
687            
688            scan = scan.toLowerCase();
689            find = find.toLowerCase();
690        }
691        else if ( findLen == repl.length() ) {
692
693                if ( find.equals( repl ) )
694                        return input;
695                
696                if ( !firstOnly && findLen == 1 )
697                        return input.replace( find.charAt(0), repl.charAt(0) );
698        }
699
700                int pos = scan.indexOf( find );
701
702                if (pos == -1)
703                        return input;
704
705                int start = 0;
706                StringBuilder sb = new StringBuilder( repl.length() > find.length() ? (int)Math.ceil( input.length() * 1.2 ) : input.length() );
707
708                while ( pos != -1 ) {
709            
710            sb.append( input.substring( start, pos ) );
711            sb.append( repl );
712            
713            start = pos + findLen;
714
715            if ( firstOnly )
716                break;
717
718                        pos = scan.indexOf( find, start );
719        }
720                
721        if ( input.length() > start )
722                sb.append( input.substring( start ) );
723
724        return sb.toString();
725    }
726    
727        
728        /**
729         * maintains the legacy signature of this method where matches are CaSe sensitive (sets the default of ignoreCase to false). 
730         * 
731         * @param input - the string input to work on 
732     * @param find - the substring to find
733     * @param repl - the substring to replace the matches with
734     * @param firstOnly - if true then only the first occurrence of {@code find} will be replaced
735     * @return - calls replace( input, find, repl, firstOnly, false )
736         */
737        public static String replace( String input, String find, String repl, boolean firstOnly ) {
738         
739                return replace( input, find, repl, firstOnly, false );
740        }
741        
742
743        /**
744         * performs a CaSe sensitive replace all
745         * 
746         * @param input - the string input to work on 
747     * @param find - the substring to find
748     * @param repl - the substring to replace the matches with
749     * @return - calls replace( input, find, repl, false, false )
750         */
751        public static String replace( String input, String find, String repl ) {
752                 
753                return replace( input, find, repl, false, false );
754        }
755        
756    
757    /**
758     * adds zeros add the begin of a int example: addZeros(2,3) return "002"
759     * @param i number to add nulls
760     * @param size 
761     * @return min len of return value;
762     */
763    public static String addZeros(int i, int size) {
764        String rtn=Caster.toString(i);
765        if(rtn.length()<size) return repeatString("0",size-rtn.length())+rtn;
766        return rtn;
767    }
768
769    
770    /**
771     * adds zeros add the begin of a int example: addZeros(2,3) return "002"
772     * @param i number to add nulls
773     * @param size 
774     * @return min len of return value;
775     */
776    public static String addZeros(long i, int size) {
777        String rtn=Caster.toString(i);
778        if(rtn.length()<size) return repeatString("0",size-rtn.length())+rtn;
779        return rtn;
780    }
781
782        public static int indexOf(String haystack, String needle) {
783                if(haystack==null) return -1;
784                return haystack.indexOf(needle);
785        }
786        
787        public static int indexOfIgnoreCase(String haystack, String needle) {
788                if(StringUtil.isEmpty(haystack) || StringUtil.isEmpty(needle)) return -1;
789                needle=needle.toLowerCase();
790                
791                int lenHaystack=haystack.length();
792                int lenNeedle=needle.length();
793                
794                char lastNeedle=needle.charAt(lenNeedle-1);
795                char c;
796                outer:for(int i=lenNeedle-1;i<lenHaystack;i++) {
797                        c=Character.toLowerCase(haystack.charAt(i));
798                        if(c==lastNeedle) {
799                                for(int y=0;y<lenNeedle-1;y++) {
800                                        if(needle.charAt(y)!=Character.toLowerCase(haystack.charAt(i-(lenNeedle-1)+y)))
801                                                        continue outer;
802                                }
803                                return i-(lenNeedle-1);
804                        }
805                }
806                
807                
808                return -1;
809        }
810    
811    /**
812     * Tests if this string starts with the specified prefix.
813     * @param str string to check first char
814     * @param prefix the prefix.
815     * @return is first of given type
816     */
817    public static boolean startsWith(String str, char prefix) {
818        return str!=null && str.length()>0 && str.charAt(0)==prefix;
819    }
820    
821    public static boolean startsWith(String str, char prefix1, char prefix2) {
822        return str!=null && str.length()>0 && (str.charAt(0)==prefix1 || str.charAt(0)==prefix2);
823    }
824    
825    /**
826     * Tests if this string ends with the specified suffix.
827     * @param str string to check first char
828     * @param suffix the suffix.
829     * @return is last of given type
830     */
831    public static boolean endsWith(String str, char suffix) {
832        return str!=null && str.length()>0 && str.charAt(str.length()-1)==suffix;
833    }
834    
835    public static boolean endsWith(String str, char prefix1, char prefix2) {
836        return str!=null && str.length()>0 && (str.charAt(str.length()-1)==prefix1 || str.charAt(str.length()-1)==prefix2);
837    }
838
839    /**
840     * Tests if this string ends with the specified suffix.
841     * @param str string to check first char
842     * @param suffix the suffix.
843     * @return is last of given type
844     */
845    /**
846     * Helper functions to query a strings start portion. The comparison is case insensitive.
847     *
848     * @param base  the base string.
849     * @param start  the starting text.
850     *
851     * @return true, if the string starts with the given starting text.
852     */
853    public static boolean startsWithIgnoreCase(final String base, final String start) {
854        if (base.length() < start.length()) {
855            return false;
856        }
857        return base.regionMatches(true, 0, start, 0, start.length());
858    }
859
860    /**
861     * Helper functions to query a strings end portion. The comparison is case insensitive.
862     *
863     * @param base  the base string.
864     * @param end  the ending text.
865     *
866     * @return true, if the string ends with the given ending text.
867     */
868    public static boolean endsWithIgnoreCase(final String base, final String end) {
869        if (base.length() < end.length()) {
870            return false;
871        }
872        return base.regionMatches(true, base.length() - end.length(), end, 0, end.length());
873    }
874
875    
876
877    /**
878     * returns if byte arr is a BOM character Stream (UTF-8,UTF-16)
879    * @param barr
880    * @return is BOM or not
881    */
882   public static  boolean isBOM(byte[] barr) {
883        return barr.length>=3 && barr[0]==0xEF && barr[1]==0xBB && barr[2]==0xBF;
884    }
885
886    /**
887     * return "" if value is null otherwise return same string
888     * @param str
889     * @return string (not null)
890     */
891    public static String valueOf(String str) {
892        if(str==null)return "";
893        return str;
894    }
895
896    
897    /**
898     * cast a string a lower case String, is faster than the String.toLowerCase, if all Character are already Low Case
899     * @param str
900     * @return lower case value
901     */
902    public static String toLowerCase(String str) {
903        int len=str.length();
904        char c;
905        for(int i=0;i<len;i++) {
906            c=str.charAt(i);
907            if(!((c>='a' && c<='z') || (c>='0' && c<='9'))) {
908                return str.toLowerCase();
909            }
910        }
911        
912        return str;
913    }
914    public static String toUpperCase(String str) {
915        int len=str.length();
916        char c;
917        for(int i=0;i<len;i++) {
918            c=str.charAt(i);
919            if(!((c>='A' && c<='Z') || (c>='0' && c<='9'))) {
920                return str.toUpperCase();
921            }
922        }
923        
924        return str;
925    }
926
927    /**
928     * soundex function
929     * @param str
930     * @return soundex from given string
931     */
932    public static String soundex(String str) {
933        return new org.apache.commons.codec.language.Soundex().soundex(str);
934    }
935
936    /**
937     * return the last character of a string, if string ist empty return 0;
938     * @param str string to get last character
939     * @return last character
940     */
941    public static char lastChar(String str) {
942        if(str==null || str.length()==0) return 0;
943        return str.charAt(str.length()-1);
944    }
945    
946
947    /**
948     * 
949     * @param str
950     * @return return if a String is "Empty", that means NULL or String with length 0 (whitespaces will not counted) 
951     */
952    public static boolean isEmpty(String str) {
953        return str==null || str.length()==0;
954    }
955    /**
956     * 
957     * @param str
958     * @return return if a String is "Empty", that means NULL or String with length 0 (whitespaces will not counted) 
959     */
960    public static boolean isEmpty(String str, boolean trim) {
961        if(!trim) return isEmpty(str);
962        return str==null || str.trim().length()==0;
963    }
964    
965    /**
966     * return the first character of a string, if string ist empty return 0;
967     * @param str string to get first character
968     * @return first character
969     */
970    public static char firstChar(String str) {
971        if(isEmpty(str)) return 0;
972        return str.charAt(0);
973    }
974    
975        public static String removeWhiteSpace(String str) {
976                if(isEmpty(str)) return str;
977                StringBuilder sb=new StringBuilder();
978                char[] carr = str.trim().toCharArray();
979                for(int i=0;i<carr.length;i++) {
980                        if(!isWhiteSpace(carr[i]))sb.append(carr[i]);
981                }
982                return sb.toString();
983        }
984
985        public static String replaceLast(String str, char from, char to) {
986                int index = str.lastIndexOf(from);
987                if(index==-1)return str;
988                return str.substring(0,index)+to+str.substring(index+1);
989        }
990        public static String replaceLast(String str, String from, String to) {
991                int index = str.lastIndexOf(from);
992                if(index==-1)return str;
993                return str.substring(0,index)+to+str.substring(index+from.length());
994        }
995
996        /**
997         * removes quotes(",') that wraps the string
998         * @param string
999         * @return
1000         */
1001        public static String removeQuotes(String string,boolean trim) {
1002                if(trim)string=string.trim();
1003                if((StringUtil.startsWith(string, '"') && StringUtil.endsWith(string, '"')) || (StringUtil.startsWith(string, '\'') && StringUtil.endsWith(string, '\''))){
1004                        string= string.substring(1,string.length()-1);
1005                        if(trim)string=string.trim();
1006                }
1007                return string;
1008        }
1009
1010        public static boolean isEmpty(Object obj, boolean trim) {
1011                if(obj==null) return true;
1012                if(obj instanceof String)return isEmpty((String)obj,trim);
1013                if(obj instanceof StringBuffer)return isEmpty((StringBuffer)obj,trim);
1014                if(obj instanceof StringBuilder)return isEmpty((StringBuilder)obj,trim);
1015                if(obj instanceof Collection.Key)return isEmpty(((Collection.Key)obj).getString(),trim);
1016                return false;
1017        }
1018        
1019        public static boolean isEmpty(Object obj) {
1020                if(obj==null) return true;
1021                if(obj instanceof String)return isEmpty((String)obj);
1022                if(obj instanceof Collection.Key)return isEmpty(((Collection.Key)obj).getString());
1023                if(obj instanceof StringBuffer)return isEmpty((StringBuffer)obj);
1024                if(obj instanceof StringBuilder)return isEmpty((StringBuilder)obj);
1025                return false;
1026        }
1027
1028        public static boolean isEmpty(StringBuffer sb,boolean trim) {
1029                if(trim) return sb==null || sb.toString().trim().length()==0;
1030                return sb==null || sb.length()==0;
1031        }
1032        public static boolean isEmpty(StringBuilder sb,boolean trim) {
1033                if(trim) return sb==null || sb.toString().trim().length()==0;
1034                return sb==null || sb.length()==0;
1035        }
1036
1037        public static boolean isEmpty(StringBuffer sb) {
1038                return sb==null || sb.length()==0;
1039        }
1040
1041        public static boolean isEmpty(StringBuilder sb) {
1042                return sb==null || sb.length()==0;
1043        }
1044
1045        public static String removeStarting(String str, String sub) {
1046                if(isEmpty(str) || isEmpty(sub) || !str.startsWith(sub)) return str;
1047                return str.substring(sub.length());
1048        }
1049
1050        public static String removeStartingIgnoreCase(String str, String sub) {
1051                if(isEmpty(sub) || !startsWithIgnoreCase(str, sub)) return str;
1052                return str.substring(sub.length());
1053        }
1054
1055        
1056        public static String[] merge(String str, String[] arr) {
1057                String[] narr=new String[arr.length+1];
1058        narr[0]=str;
1059        for(int i=0;i<arr.length;i++) {
1060                narr[i+1]=arr[i];
1061        }
1062        return narr;
1063        
1064        }
1065
1066        public static int length(String str) {
1067                if(str==null) return 0;
1068                return str.length();
1069        }
1070
1071        public static int length(String str, boolean trim) {
1072                if(str==null) return 0;
1073                return str.trim().length();
1074        }
1075
1076        public static boolean hasUpperCase(String str) {
1077                if(isEmpty(str)) return false;
1078                return !str.equals(str.toLowerCase());
1079        }
1080
1081        public static boolean contains(String str, String substr) {
1082                if(str==null) return false;
1083                return str.indexOf(substr)!=-1;
1084        }
1085
1086        public static boolean containsIgnoreCase(String str, String substr) {
1087                return indexOfIgnoreCase(str,substr)!=-1;
1088        }
1089
1090        public static String substringEL(String str, int index,String defaultValue) {
1091                if(str==null || index<0 || index>str.length()) return defaultValue;
1092                return str.substring(index);
1093        }
1094
1095        /**
1096         * translate a string in camel notation to a string in hypen notation
1097         * example:
1098         * helloWorld -> hello-world
1099         * @param str
1100         * @return
1101         */
1102        public static String camelToHypenNotation(String str) {
1103                if(isEmpty(str)) return str;
1104                
1105                StringBuilder sb=new StringBuilder();
1106                //int len=str.length();
1107                char c;
1108                
1109                sb.append(Character.toLowerCase(str.charAt(0)));
1110                for(int i=1;i<str.length();i++){
1111                        c=str.charAt(i);
1112                        if(Character.isUpperCase(c)){
1113                                sb.append('-');
1114                                sb.append(Character.toLowerCase(c));
1115                        }
1116                        else sb.append(c);
1117                }
1118                return sb.toString();
1119        }
1120
1121        /**
1122         * translate a string in hypen notation to a string in camel notation
1123         * example:
1124         * hello-world -> helloWorld
1125         * @param str
1126         * @return
1127         */
1128        public static String hypenToCamelNotation(String str) {
1129                if(isEmpty(str)) return str;
1130                
1131                StringBuilder sb=new StringBuilder();
1132                int len=str.length();
1133                char c;
1134                
1135                for(int i=0;i<str.length();i++){
1136                        c=str.charAt(i);
1137                        if(c=='-'){
1138                                if(len>++i) sb.append(Character.toUpperCase(str.charAt(i)));
1139                        }
1140                        else sb.append(c);
1141                }
1142                return sb.toString();
1143        }
1144
1145        public static boolean isAscii(String str) {
1146
1147                if ( str == null )
1148                        return false;
1149
1150                for(int i=str.length()-1;i>=0;i--){
1151
1152                        if( str.charAt(i) > 127 )
1153                        return false;
1154                }
1155                return true;
1156        }
1157
1158
1159        /**
1160         * returns true if all characters in the string are letters
1161         *
1162         * @param str
1163         * @return
1164         */
1165        public static boolean isAllAlpha(String str) {
1166
1167                if ( str == null )  return false;
1168
1169                for (int i=str.length()-1; i >= 0; i--) {
1170
1171                        if ( !Character.isLetter( str.charAt(i) ) )
1172                                return false;
1173                }
1174
1175                return true;
1176        }
1177
1178
1179        /**
1180         * returns true if the input string has letters and they are all UPPERCASE
1181         *
1182         * @param str
1183         * @return
1184         */
1185        public static boolean isAllUpperCase(String str) {
1186
1187                if ( str == null )  return false;
1188
1189                boolean hasLetters = false;
1190                char c;
1191
1192                for (int i=str.length()-1; i >= 0; i--) {
1193
1194                        c = str.charAt(i);
1195                        if ( Character.isLetter( c )) {
1196
1197                                if ( !Character.isUpperCase( c ))
1198                                        return false;
1199
1200                                hasLetters = true;
1201                        }
1202                }
1203
1204                return hasLetters;
1205        }
1206
1207
1208        public static boolean isWhiteSpace(String str) {
1209                if(str==null) return false;
1210                for(int i=str.length()-1;i>=0;i--){
1211                        if(!isWhiteSpace(str.charAt(i))) return false;
1212                }
1213                return true;
1214        }
1215
1216        /**
1217         * this method works different from the regular substring method, the regular substring method takes startIndex and endIndex as second and third argument,
1218         * this method takes offset and length
1219         * @param str
1220         * @param off
1221         * @param len
1222         * @return
1223         */
1224        public static String substring(String str, int off, int len) {
1225                return str.substring(off,off+len);
1226        }
1227        
1228
1229        public static String insertAt(String str, CharSequence substring, int pos) {
1230
1231                if (isEmpty(substring))
1232                        return str;
1233
1234                int len = str.length();
1235
1236                StringBuilder sb = new StringBuilder(len + substring.length());
1237
1238                if (pos > len)
1239                        pos = len;
1240
1241                if (pos > 0)
1242                        sb.append(str.substring(0, pos));
1243
1244                sb.append(substring);
1245                sb.append(str.substring(pos));
1246
1247                return sb.toString();
1248        }
1249
1250        
1251        /**
1252         * this is the public entry point for the replaceMap() method
1253         * 
1254         * @param input - the string on which the replacements should be performed.
1255         * @param map - a java.util.Map with key/value pairs where the key is the substring to find and the value is the substring with which to replace the matched key 
1256         * @param ignoreCase - if true then matches will not be case sensitive
1257         * @return
1258         * @throws PageException 
1259         */
1260        public static String replaceMap( String input, Map map, boolean ignoreCase ) throws PageException {
1261                 
1262                return replaceMap( input, map, ignoreCase, true );
1263        }
1264
1265    
1266        /**
1267         * this is the core of the replaceMap() method.  
1268         * 
1269         * it is called once from the public entry point and then internally from resolveInternals()
1270         * 
1271         * when doResolveInternals is true -- this method calls resolveInternals.  therefore, calls from resolveInternals() 
1272         * must pass false to that param to avoid an infinite ping-pong loop 
1273         * 
1274         * @param input - the string on which the replacements should be performed.
1275         * @param map - a java.util.Map with key/value pairs where the key is the substring to find and the value is the substring with which to replace the matched key
1276         * @param ignoreCase - if true then matches will not be case sensitive
1277         * @param doResolveInternals - only the initial call (from the public entry point) should pass true
1278         * @return
1279         * @throws PageException 
1280         */
1281    private static String replaceMap( String input, Map map, boolean ignoreCase, boolean doResolveInternals ) throws PageException {
1282        if ( doResolveInternals )
1283            map = resolveInternals( map, ignoreCase, 0 );
1284        
1285        String result = input;
1286        Iterator<Map.Entry> it = map.entrySet().iterator();
1287        Map.Entry e;
1288        while ( it.hasNext() ) {
1289            e = it.next();
1290            result = replace( result, Caster.toString(e.getKey()), Caster.toString(e.getValue()), false, ignoreCase );
1291        }
1292        return result;
1293    }
1294        
1295        
1296    
1297    /**
1298     * resolves internal values within the map, so if the map has a key "{signature}" 
1299     * and its value is "Team {group}" and there's a key with the value {group} whose
1300     * value is "Lucee", then {signature} will resolve to "Team Lucee".
1301     * 
1302     *  {signature} = "Team {group}"
1303     *  {group}     = "Lucee"
1304     * 
1305     * then signature will resolve to
1306     * 
1307     *  {signature} = "Team Lucee"
1308     * 
1309     * @param map - key/value pairs for find key/replace with value
1310     * @param ignoreCase - if true then matches will not be case sensitive
1311     * @param count - used internally as safety valve to ensure that we don't go into infinite loop if two values reference each-other
1312     * @return 
1313     * @throws PageException 
1314     */
1315    private static Map resolveInternals( Map map, boolean ignoreCase, int count ) throws PageException {
1316        Map result = new HashMap();
1317        Iterator<Map.Entry> it = map.entrySet().iterator();
1318        boolean isModified = false;
1319        Map.Entry e;
1320        String v,r;
1321        while ( it.hasNext() ) {
1322            e = it.next();
1323            v = Caster.toString( e.getValue() );
1324            r = replaceMap( v, map, ignoreCase, false );                // pass false for last arg so that replaceMap() will not call this method in an infinite loop
1325            result.put( Caster.toString( e.getKey() ), r );
1326            if ( !v.equalsIgnoreCase( r ) )
1327                isModified = true;
1328        }
1329                
1330        if ( isModified && count++ < map.size() )
1331            result = resolveInternals( result, ignoreCase, count );     // recursive call
1332        
1333        return result;
1334    }
1335
1336
1337        public static String toStringNative(Object obj,String defaultValue) {
1338                return obj==null?defaultValue:obj.toString();
1339        }
1340}