001    //$HeadURL: http://svn.wald.intevation.org/svn/deegree/base/trunk/src/org/deegree/framework/util/StringTools.java $
002    /*----------------------------------------------------------------------------
003     This file is part of deegree, http://deegree.org/
004     Copyright (C) 2001-2009 by:
005     Department of Geography, University of Bonn
006     and
007     lat/lon GmbH
008    
009     This library is free software; you can redistribute it and/or modify it under
010     the terms of the GNU Lesser General Public License as published by the Free
011     Software Foundation; either version 2.1 of the License, or (at your option)
012     any later version.
013     This library is distributed in the hope that it will be useful, but WITHOUT
014     ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
015     FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
016     details.
017     You should have received a copy of the GNU Lesser General Public License
018     along with this library; if not, write to the Free Software Foundation, Inc.,
019     59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
020    
021     Contact information:
022    
023     lat/lon GmbH
024     Aennchenstr. 19, 53177 Bonn
025     Germany
026     http://lat-lon.de/
027    
028     Department of Geography, University of Bonn
029     Prof. Dr. Klaus Greve
030     Postfach 1147, 53001 Bonn
031     Germany
032     http://www.geographie.uni-bonn.de/deegree/
033    
034     e-mail: info@deegree.org
035     ----------------------------------------------------------------------------*/
036    package org.deegree.framework.util;
037    
038    import java.io.BufferedReader;
039    import java.io.IOException;
040    import java.io.InputStream;
041    import java.io.InputStreamReader;
042    import java.io.PrintStream;
043    import java.io.StringReader;
044    import java.util.ArrayList;
045    import java.util.HashMap;
046    import java.util.Iterator;
047    import java.util.List;
048    import java.util.Locale;
049    import java.util.Map;
050    import java.util.Set;
051    import java.util.StringTokenizer;
052    
053    import org.deegree.framework.xml.XMLFragment;
054    import org.deegree.framework.xml.XMLParsingException;
055    import org.deegree.framework.xml.XMLTools;
056    import org.deegree.ogcbase.CommonNamespaces;
057    import org.w3c.dom.Node;
058    import org.xml.sax.SAXException;
059    
060    /**
061     * this is a collection of some methods that extends the functionality of the sun-java string class.
062     * 
063     * @author <a href="mailto:poth@lat-lon.de">Andreas Poth</a>
064     * @author last edited by: $Author: apoth $
065     * 
066     * @version $Revision: 30474 $, $Date: 2011-04-17 11:23:29 +0200 (Sun, 17 Apr 2011) $
067     */
068    public class StringTools {
069    
070        /**
071         * This map is used for methods normalizeString() and initMap().
072         * 
073         * key = locale language, e.g. "de" value = map of substitution rules for this locale
074         */
075        private static Map<String, Map<String, String>> localeMap;
076    
077        /**
078         * concatenates an array of strings using a
079         * 
080         * @see StringBuffer
081         * 
082         * @param size
083         *            estimated size of the target string
084         * @param objects
085         *            toString() will be called for each object to append it to the result string
086         * @return the concatenated String
087         */
088        public static String concat( int size, Object... objects ) {
089            StringBuilder sbb = new StringBuilder( size );
090            for ( int i = 0; i < objects.length; i++ ) {
091                sbb.append( objects[i] );
092            }
093            return sbb.toString();
094        }
095    
096        /**
097         * replaces occurences of a string fragment within a string by a new string.
098         * 
099         * @param target
100         *            is the original string
101         * @param from
102         *            is the string to be replaced
103         * @param to
104         *            is the string which will used to replace
105         * @param all
106         *            if it's true all occurences of the string to be replaced will be replaced. else only the first
107         *            occurence will be replaced.
108         * @return the changed target string
109         */
110        public static String replace( String target, String from, String to, boolean all ) {
111    
112            StringBuffer buffer = new StringBuffer( target.length() );
113            int copyFrom = 0;
114            char[] targetChars = null;
115            int lf = from.length();
116            int start = -1;
117            do {
118                start = target.indexOf( from );
119                copyFrom = 0;
120                if ( start == -1 ) {
121                    return target;
122                }
123    
124                targetChars = target.toCharArray();
125                while ( start != -1 ) {
126                    buffer.append( targetChars, copyFrom, start - copyFrom );
127                    buffer.append( to );
128                    copyFrom = start + lf;
129                    start = target.indexOf( from, copyFrom );
130                    if ( !all ) {
131                        start = -1;
132                    }
133                }
134                buffer.append( targetChars, copyFrom, targetChars.length - copyFrom );
135                target = buffer.toString();
136                buffer.delete( 0, buffer.length() );
137            } while ( target.indexOf( from ) > -1 && to.indexOf( from ) < 0 );
138    
139            return target;
140        }
141    
142        /**
143         * parse a string and return its tokens as array
144         * 
145         * @param s
146         *            string to parse
147         * @param delimiter
148         *            delimiter that marks the end of a token
149         * @param deleteDoubles
150         *            if it's true all string that are already within the resulting array will be deleted, so that there
151         *            will only be one copy of them.
152         * @return an Array of Strings
153         */
154        public static String[] toArray( String s, String delimiter, boolean deleteDoubles ) {
155            if ( s == null || s.equals( "" ) ) {
156                return new String[0];
157            }
158    
159            StringTokenizer st = new StringTokenizer( s, delimiter );
160            ArrayList<String> vec = new ArrayList<String>( st.countTokens() );
161    
162            if ( st.countTokens() > 0 ) {
163                for ( int i = 0; st.hasMoreTokens(); i++ ) {
164                    String t = st.nextToken();
165                    if ( ( t != null ) && ( t.length() > 0 ) ) {
166                        vec.add( t.trim() );
167                    }
168                }
169            } else {
170                vec.add( s );
171            }
172    
173            String[] kw = vec.toArray( new String[vec.size()] );
174            if ( deleteDoubles ) {
175                kw = deleteDoubles( kw );
176            }
177    
178            return kw;
179        }
180    
181        /**
182         * parse a string and return its tokens as typed List. empty fields will be removed from the list.
183         * 
184         * @param s
185         *            string to parse
186         * @param delimiter
187         *            delimiter that marks the end of a token
188         * @param deleteDoubles
189         *            if it's true all string that are already within the resulting array will be deleted, so that there
190         *            will only be one copy of them.
191         * @return a list of Strings
192         */
193        public static List<String> toList( String s, String delimiter, boolean deleteDoubles ) {
194            if ( s == null || s.equals( "" ) ) {
195                return new ArrayList<String>();
196            }
197    
198            StringTokenizer st = new StringTokenizer( s, delimiter );
199            ArrayList<String> vec = new ArrayList<String>( st.countTokens() );
200            for ( int i = 0; st.hasMoreTokens(); i++ ) {
201                String t = st.nextToken();
202                if ( ( t != null ) && ( t.length() > 0 ) ) {
203                    if ( deleteDoubles ) {
204                        if ( !vec.contains( t.trim() ) ) {
205                            vec.add( t.trim() );
206                        }
207                    } else {
208                        vec.add( t.trim() );
209                    }
210                }
211            }
212    
213            return vec;
214        }
215    
216        /**
217         * transforms a string array to one string. the array fields are separated by the submitted delimiter:
218         * 
219         * @param s
220         *            stringarray to transform
221         * @param delimiter
222         * @return the String representation of the given array
223         */
224        public static String arrayToString( String[] s, char delimiter ) {
225            StringBuffer res = new StringBuffer( s.length * 20 );
226    
227            for ( int i = 0; i < s.length; i++ ) {
228                res.append( s[i] );
229    
230                if ( i < ( s.length - 1 ) ) {
231                    res.append( delimiter );
232                }
233            }
234    
235            return res.toString();
236        }
237    
238        /**
239         * transforms a list to one string. the array fields are separated by the submitted delimiter:
240         * 
241         * @param s
242         *            stringarray to transform
243         * @param delimiter
244         * @return the String representation of the given list.
245         */
246        public static String listToString( List<?> s, char delimiter ) {
247            StringBuffer res = new StringBuffer( s.size() * 20 );
248    
249            for ( int i = 0; i < s.size(); i++ ) {
250                res.append( s.get( i ) );
251    
252                if ( i < ( s.size() - 1 ) ) {
253                    res.append( delimiter );
254                }
255            }
256    
257            return res.toString();
258        }
259    
260        /**
261         * transforms a double array to one string. the array fields are separated by the submitted delimiter:
262         * 
263         * @param s
264         *            string array to transform
265         * @param delimiter
266         * @return the String representation of the given array
267         */
268        public static String arrayToString( double[] s, char delimiter ) {
269            StringBuffer res = new StringBuffer( s.length * 20 );
270    
271            for ( int i = 0; i < s.length; i++ ) {
272                res.append( Double.toString( s[i] ) );
273    
274                if ( i < ( s.length - 1 ) ) {
275                    res.append( delimiter );
276                }
277            }
278    
279            return res.toString();
280        }
281    
282        /**
283         * transforms a float array to one string. the array fields are separated by the submitted delimiter:
284         * 
285         * @param s
286         *            float array to transform
287         * @param delimiter
288         * @return the String representation of the given array
289         */
290        public static String arrayToString( float[] s, char delimiter ) {
291            StringBuffer res = new StringBuffer( s.length * 20 );
292    
293            for ( int i = 0; i < s.length; i++ ) {
294                res.append( Float.toString( s[i] ) );
295    
296                if ( i < ( s.length - 1 ) ) {
297                    res.append( delimiter );
298                }
299            }
300    
301            return res.toString();
302        }
303    
304        /**
305         * transforms a int array to one string. the array fields are separated by the submitted delimiter:
306         * 
307         * @param s
308         *            stringarray to transform
309         * @param delimiter
310         * @return the String representation of the given array
311         */
312        public static String arrayToString( int[] s, char delimiter ) {
313            StringBuffer res = new StringBuffer( s.length * 20 );
314    
315            for ( int i = 0; i < s.length; i++ ) {
316                res.append( Integer.toString( s[i] ) );
317    
318                if ( i < ( s.length - 1 ) ) {
319                    res.append( delimiter );
320                }
321            }
322    
323            return res.toString();
324        }
325    
326        /**
327         * clears the begin and end of a string from the strings submitted
328         * 
329         * @param s
330         *            string to validate
331         * @param mark
332         *            string to remove from begin and end of <code>s</code>
333         * @return the substring of the given String without the mark at the and and the begin, and trimmed
334         */
335        public static String validateString( String s, String mark ) {
336            if ( s == null ) {
337                return null;
338            }
339    
340            if ( s.length() == 0 ) {
341                return s;
342            }
343    
344            s = s.trim();
345    
346            while ( s.startsWith( mark ) ) {
347                s = s.substring( mark.length(), s.length() ).trim();
348            }
349    
350            while ( s.endsWith( mark ) ) {
351                s = s.substring( 0, s.length() - mark.length() ).trim();
352            }
353    
354            return s;
355        }
356    
357        /**
358         * deletes all double entries from the submitted array
359         * 
360         * @param s
361         *            to remove the doubles from
362         * @return The string array without all doubled values
363         */
364        public static String[] deleteDoubles( String[] s ) {
365            ArrayList<String> vec = new ArrayList<String>( s.length );
366    
367            for ( int i = 0; i < s.length; i++ ) {
368                if ( !vec.contains( s[i] ) ) {
369                    vec.add( s[i] );
370                }
371            }
372    
373            return vec.toArray( new String[vec.size()] );
374        }
375    
376        /**
377         * removes all fields from the array that equals <code>s</code>
378         * 
379         * @param target
380         *            array where to remove the submitted string
381         * @param s
382         *            string to remove
383         * @return the String array with all exact occurrences of given String removed.
384         */
385        public static String[] removeFromArray( String[] target, String s ) {
386            ArrayList<String> vec = new ArrayList<String>( target.length );
387    
388            for ( int i = 0; i < target.length; i++ ) {
389                if ( !target[i].equals( s ) ) {
390                    vec.add( target[i] );
391                }
392            }
393    
394            return vec.toArray( new String[vec.size()] );
395        }
396    
397        /**
398         * checks if the submitted array contains the string <code>value</code>
399         * 
400         * @param target
401         *            array to check if it contains <code>value</code>
402         * @param value
403         *            string to check if it within the array
404         * @return true if the given value is contained (without case comparison) in the array, caution, if the value ends
405         *         with a comma ',' a substring will be taken to remove it (rb: For whatever reason??).
406         */
407        public static boolean contains( String[] target, String value ) {
408            if ( target == null || value == null ) {
409                return false;
410            }
411    
412            if ( value.endsWith( "," ) ) {
413                value = value.substring( 0, value.length() - 1 );
414            }
415    
416            for ( int i = 0; i < target.length; i++ ) {
417                if ( value.equalsIgnoreCase( target[i] ) ) {
418                    return true;
419                }
420            }
421    
422            return false;
423        }
424    
425        /**
426         * convert the array of string like [(x1,y1),(x2,y2)...] into an array of double [x1,y1,x2,y2...]
427         * 
428         * @param s
429         * @param delimiter
430         * 
431         * @return the array representation of the given String
432         */
433        public static double[] toArrayDouble( String s, String delimiter ) {
434            if ( s == null || "".equals( s.trim() ) ) {
435                return null;
436            }
437            StringTokenizer st = new StringTokenizer( s, delimiter );
438    
439            ArrayList<String> vec = new ArrayList<String>( st.countTokens() );
440    
441            for ( int i = 0; st.hasMoreTokens(); i++ ) {
442                String t = st.nextToken().replace( ' ', '+' );
443    
444                if ( ( t != null ) && ( t.length() > 0 ) ) {
445                    vec.add( t.trim().replace( ',', '.' ) );
446                }
447            }
448    
449            double[] array = new double[vec.size()];
450    
451            for ( int i = 0; i < vec.size(); i++ ) {
452                array[i] = Double.parseDouble( vec.get( i ) );
453            }
454    
455            return array;
456        }
457    
458        /**
459         * convert the array of string like [(x1,y1),(x2,y2)...] into an array of float values [x1,y1,x2,y2...]
460         * 
461         * @param s
462         * @param delimiter
463         * 
464         * @return the array representation of the given String
465         */
466        public static float[] toArrayFloat( String s, String delimiter ) {
467            if ( s == null ) {
468                return null;
469            }
470    
471            if ( s.equals( "" ) ) {
472                return null;
473            }
474    
475            StringTokenizer st = new StringTokenizer( s, delimiter );
476    
477            ArrayList<String> vec = new ArrayList<String>( st.countTokens() );
478            for ( int i = 0; st.hasMoreTokens(); i++ ) {
479                String t = st.nextToken().replace( ' ', '+' );
480                if ( ( t != null ) && ( t.length() > 0 ) ) {
481                    vec.add( t.trim().replace( ',', '.' ) );
482                }
483            }
484    
485            float[] array = new float[vec.size()];
486    
487            for ( int i = 0; i < vec.size(); i++ ) {
488                array[i] = Float.parseFloat( vec.get( i ) );
489            }
490    
491            return array;
492        }
493        
494        /**
495         * convert the array of string like [(x1,y1),(x2,y2)...] into an array of float values [x1,y1,x2,y2...]
496         * 
497         * @param s
498         * @param delimiter
499         * 
500         * @return the array representation of the given String
501         */
502        public static int[] toArrayInt( String s, String delimiter ) {
503            if ( s == null ) {
504                return null;
505            }
506    
507            if ( s.equals( "" ) ) {
508                return null;
509            }
510    
511            StringTokenizer st = new StringTokenizer( s, delimiter );
512    
513            ArrayList<String> vec = new ArrayList<String>( st.countTokens() );
514            for ( int i = 0; st.hasMoreTokens(); i++ ) {
515                String t = st.nextToken();
516                if ( ( t != null ) && ( t.length() > 0 ) ) {
517                    vec.add( t.trim() );
518                }
519            }
520    
521            int[] array = new int[vec.size()];
522    
523            for ( int i = 0; i < vec.size(); i++ ) {
524                array[i] = Integer.parseInt( vec.get( i ) );
525            }
526    
527            return array;
528        }
529    
530        /**
531         * prints current stactrace
532         */
533        public static void printStacktrace() {
534            System.out.println( StringTools.stackTraceToString( Thread.getAllStackTraces().get( Thread.currentThread() ) ) );
535        }
536    
537        /**
538         * transforms an array of StackTraceElements into a String
539         * 
540         * @param se
541         *            to put to String
542         * @return a String representation of the given Stacktrace.
543         */
544        public static String stackTraceToString( StackTraceElement[] se ) {
545    
546            StringBuffer sb = new StringBuffer();
547            for ( int i = 0; i < se.length; i++ ) {
548                sb.append( se[i].getClassName() + " " );
549                sb.append( se[i].getFileName() + " " );
550                sb.append( se[i].getMethodName() + "(" );
551                sb.append( se[i].getLineNumber() + ")\n" );
552            }
553            return sb.toString();
554        }
555    
556        /**
557         * Get the message and the class, as well as the stack trace of the passed Throwable and transforms it into a String
558         * 
559         * @param e
560         *            to get information from
561         * @return the String representation of the given Throwable
562         */
563        public static String stackTraceToString( Throwable e ) {
564            if ( e == null ) {
565                return "No Throwable given.";
566            }
567            StringBuffer sb = new StringBuffer();
568            sb.append( e.getMessage() ).append( "\n" );
569            sb.append( e.getClass().getName() ).append( "\n" );
570            sb.append( stackTraceToString( e.getStackTrace() ) );
571            return sb.toString();
572        }
573    
574        /**
575         * countString count the occurrences of token into target
576         * 
577         * @param target
578         * @param token
579         * 
580         * @return the number of occurrences of the given token in the given String
581         */
582        public static int countString( String target, String token ) {
583            int start = target.indexOf( token );
584            int count = 0;
585    
586            while ( start != -1 ) {
587                count++;
588                start = target.indexOf( token, start + 1 );
589            }
590    
591            return count;
592        }
593    
594        /**
595         * Extract all the strings that begin with "start" and end with "end" and store it into an array of String
596         * 
597         * @param target
598         * @param startString
599         * @param endString
600         * 
601         * @return <code>null</code> if no strings were found!!
602         */
603        public static String[] extractStrings( String target, String startString, String endString ) {
604            int start = target.indexOf( startString );
605    
606            if ( start == -1 ) {
607                return null;
608            }
609    
610            int count = countString( target, startString );
611            String[] subString = null;
612            if ( startString.equals( endString ) ) {
613                count = count / 2;
614                subString = new String[count];
615                for ( int i = 0; i < count; i++ ) {
616                    int tmp = target.indexOf( endString, start + 1 );
617                    subString[i] = target.substring( start, tmp + 1 );
618                    start = target.indexOf( startString, tmp + 1 );
619                }
620            } else {
621                subString = new String[count];
622                for ( int i = 0; i < count; i++ ) {
623                    subString[i] = target.substring( start, target.indexOf( endString, start + 1 ) + 1 );
624                    subString[i] = extractString( subString[i], startString, endString, true, true );
625                    start = target.indexOf( startString, start + 1 );
626                }
627            }
628    
629            return subString;
630        }
631    
632        /**
633         * extract a string contained between startDel and endDel, you can remove the delimiters if set true the parameters
634         * delStart and delEnd
635         * 
636         * @param target
637         *            to extract from
638         * @param startDel
639         *            to remove from the start
640         * @param endDel
641         *            string to remove from the end
642         * @param delStart
643         *            true if the start should be removed
644         * @param delEnd
645         *            true if the end should be removed
646         * 
647         * @return the extracted string from the given target. rb: Caution this method may not do what it should.
648         */
649        public static String extractString( String target, String startDel, String endDel, boolean delStart, boolean delEnd ) {
650            if ( target == null ) {
651                return null;
652            }
653            int start = target.indexOf( startDel );
654    
655            if ( start == -1 ) {
656                return null;
657            }
658    
659            String s = target.substring( start, target.indexOf( endDel, start + 1 ) + 1 );
660    
661            s = s.trim();
662    
663            if ( delStart ) {
664                while ( s.startsWith( startDel ) ) {
665                    s = s.substring( startDel.length(), s.length() ).trim();
666                }
667            }
668    
669            if ( delEnd ) {
670                while ( s.endsWith( endDel ) ) {
671                    s = s.substring( 0, s.length() - endDel.length() ).trim();
672                }
673            }
674    
675            return s;
676        }
677    
678        /**
679         * Initialize the substitution map with all normalization rules for a given locale and add this map to the static
680         * localeMap.
681         * 
682         * @param locale
683         * @throws IOException
684         * @throws SAXException
685         * @throws XMLParsingException
686         */
687        private static void initMap( String locale )
688                                throws IOException, SAXException, XMLParsingException {
689    
690            // read normalization file
691            StringBuffer sb = new StringBuffer( 1000 );
692            InputStream is = StringTools.class.getResourceAsStream( "/normalization.xml" );
693            if ( is == null ) {
694                is = StringTools.class.getResourceAsStream( "normalization.xml" );
695            }
696            BufferedReader br = new BufferedReader( new InputStreamReader( is ) );
697            String s = null;
698            while ( ( s = br.readLine() ) != null ) {
699                sb.append( s );
700            }
701            br.close();
702    
703            // transform into xml fragment
704            XMLFragment xml = new XMLFragment();
705            xml.load( new StringReader( sb.toString() ), StringTools.class.getResource( "normalization.xml" ).toString() ); // FIXME
706    
707            // create map
708            Map<String, String> substitutionMap = new HashMap<String, String>( 20 );
709    
710            // extract case attrib ( "toLower" or "toUpper" or missing ) for passed locale
711            String xpath = "Locale[@name = '" + Locale.GERMANY.getLanguage() + "']/@case";
712            String letterCase = XMLTools.getNodeAsString( xml.getRootElement(), xpath,
713                                                          CommonNamespaces.getNamespaceContext(), null );
714            if ( letterCase != null ) {
715                substitutionMap.put( "case", letterCase );
716            }
717    
718            // extract removeDoubles attrib ( "true" or "false" ) for passed locale
719            xpath = "Locale[@name = '" + Locale.GERMANY.getLanguage() + "']/@removeDoubles";
720            String removeDoubles = XMLTools.getNodeAsString( xml.getRootElement(), xpath,
721                                                             CommonNamespaces.getNamespaceContext(), null );
722            if ( removeDoubles != null && removeDoubles.length() > 0 ) {
723                substitutionMap.put( "removeDoubles", removeDoubles );
724            }
725    
726            // extract rules section for passed locale
727            xpath = "Locale[@name = '" + locale + "']/Rule";
728            List<Node> list = XMLTools.getNodes( xml.getRootElement(), xpath, CommonNamespaces.getNamespaceContext() );
729            if ( list != null ) {
730                // for ( int i = 0; i < list.size(); i++ ) {
731                for ( Node n : list ) {
732                    String src = XMLTools.getRequiredNodeAsString( n, "Source", CommonNamespaces.getNamespaceContext() );
733                    String target = XMLTools.getRequiredNodeAsString( n, "Target", CommonNamespaces.getNamespaceContext() );
734                    substitutionMap.put( src, target );
735                }
736            }
737    
738            // init localeMap if needed
739            if ( localeMap == null ) {
740                localeMap = new HashMap<String, Map<String, String>>( 20 );
741            }
742    
743            localeMap.put( locale, substitutionMap );
744        }
745    
746        /**
747         * The passed string gets normalized along the rules for the given locale as they are set in the file
748         * "./normalization.xml". If such rules are specified, the following order is obeyed:
749         * 
750         * <ol>
751         * <li>if the attribute "case" is set with "toLower" or "toUpper", the letters are switched to lower case or to
752         * upper case respectively.</li>
753         * <li>all rules given in the "Rule" elements are performed.</li>
754         * <li>if the attribute "removeDoubles" is set and not empty, all multi occurences of the letters given in this
755         * attribute are reduced to a single occurence.</li>
756         * </ol>
757         * 
758         * @param source
759         *            the String to normalize
760         * @param locale
761         *            the locale language defining the rules to choose, e.g. "de"
762         * @return the normalized String
763         * @throws IOException
764         * @throws SAXException
765         * @throws XMLParsingException
766         */
767        public static String normalizeString( String source, String locale )
768                                throws IOException, SAXException, XMLParsingException {
769    
770            if ( localeMap == null ) {
771                localeMap = new HashMap<String, Map<String, String>>( 20 );
772            }
773            Map<String, String> substitutionMap = localeMap.get( locale );
774    
775            if ( substitutionMap == null ) {
776                initMap( locale );
777            }
778            substitutionMap = localeMap.get( locale );
779    
780            String output = source;
781            Set<String> keys = substitutionMap.keySet();
782    
783            boolean toUpper = false;
784            boolean toLower = false;
785            boolean removeDoubles = false;
786    
787            for ( String key : keys ) {
788                if ( "case".equals( key ) ) {
789                    toUpper = "toUpper".equals( substitutionMap.get( key ) );
790                    toLower = "toLower".equals( substitutionMap.get( key ) );
791                }
792                if ( "removeDoubles".equals( key ) && substitutionMap.get( key ).length() > 0 ) {
793                    removeDoubles = true;
794                }
795            }
796    
797            // first: change letters to upper / lower case
798            if ( toUpper ) {
799                output = output.toUpperCase();
800            } else if ( toLower ) {
801                output = output.toLowerCase();
802            }
803    
804            // second: change string according to specified rules
805            for ( String key : keys ) {
806                if ( !"case".equals( key ) && !"removeDoubles".equals( key ) ) {
807                    output = output.replaceAll( key, substitutionMap.get( key ) );
808                }
809            }
810    
811            // third: remove doubles
812            if ( removeDoubles ) {
813                String doubles = substitutionMap.get( "removeDoubles" );
814                for ( int i = 0; i < doubles.length(); i++ ) {
815                    String remove = "" + doubles.charAt( i ) + "+";
816                    String replaceWith = "" + doubles.charAt( i );
817                    output = output.replaceAll( remove, replaceWith );
818                }
819            }
820            return output;
821        }
822    
823        /**
824         * prints a map with one line for each key-value pair
825         * @param map
826         * @param ps if ps is null System.out will be used 
827         */
828        public static final void printMap( Map<?, ?> map, PrintStream ps ) {
829            if ( ps == null ) {
830                ps = System.out;
831            }
832            Iterator<?> iter = map.keySet().iterator();
833            while ( iter.hasNext() ) {
834                Object key = (Object) iter.next();
835                Object value = map.get( key );
836                ps.println( key + " : " + value );
837            }
838        }
839    }