001    //$HeadURL: svn+ssh://jwilden@svn.wald.intevation.org/deegree/base/branches/2.5_testing/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: 29560 $, $Date: 2011-02-07 15:35:13 +0100 (Mo, 07 Feb 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         * prints current stactrace
496         */
497        public static void printStacktrace() {
498            System.out.println( StringTools.stackTraceToString( Thread.getAllStackTraces().get( Thread.currentThread() ) ) );
499        }
500    
501        /**
502         * transforms an array of StackTraceElements into a String
503         * 
504         * @param se
505         *            to put to String
506         * @return a String representation of the given Stacktrace.
507         */
508        public static String stackTraceToString( StackTraceElement[] se ) {
509    
510            StringBuffer sb = new StringBuffer();
511            for ( int i = 0; i < se.length; i++ ) {
512                sb.append( se[i].getClassName() + " " );
513                sb.append( se[i].getFileName() + " " );
514                sb.append( se[i].getMethodName() + "(" );
515                sb.append( se[i].getLineNumber() + ")\n" );
516            }
517            return sb.toString();
518        }
519    
520        /**
521         * Get the message and the class, as well as the stack trace of the passed Throwable and transforms it into a String
522         * 
523         * @param e
524         *            to get information from
525         * @return the String representation of the given Throwable
526         */
527        public static String stackTraceToString( Throwable e ) {
528            if ( e == null ) {
529                return "No Throwable given.";
530            }
531            StringBuffer sb = new StringBuffer();
532            sb.append( e.getMessage() ).append( "\n" );
533            sb.append( e.getClass().getName() ).append( "\n" );
534            sb.append( stackTraceToString( e.getStackTrace() ) );
535            return sb.toString();
536        }
537    
538        /**
539         * countString count the occurrences of token into target
540         * 
541         * @param target
542         * @param token
543         * 
544         * @return the number of occurrences of the given token in the given String
545         */
546        public static int countString( String target, String token ) {
547            int start = target.indexOf( token );
548            int count = 0;
549    
550            while ( start != -1 ) {
551                count++;
552                start = target.indexOf( token, start + 1 );
553            }
554    
555            return count;
556        }
557    
558        /**
559         * Extract all the strings that begin with "start" and end with "end" and store it into an array of String
560         * 
561         * @param target
562         * @param startString
563         * @param endString
564         * 
565         * @return <code>null</code> if no strings were found!!
566         */
567        public static String[] extractStrings( String target, String startString, String endString ) {
568            int start = target.indexOf( startString );
569    
570            if ( start == -1 ) {
571                return null;
572            }
573    
574            int count = countString( target, startString );
575            String[] subString = null;
576            if ( startString.equals( endString ) ) {
577                count = count / 2;
578                subString = new String[count];
579                for ( int i = 0; i < count; i++ ) {
580                    int tmp = target.indexOf( endString, start + 1 );
581                    subString[i] = target.substring( start, tmp + 1 );
582                    start = target.indexOf( startString, tmp + 1 );
583                }
584            } else {
585                subString = new String[count];
586                for ( int i = 0; i < count; i++ ) {
587                    subString[i] = target.substring( start, target.indexOf( endString, start + 1 ) + 1 );
588                    subString[i] = extractString( subString[i], startString, endString, true, true );
589                    start = target.indexOf( startString, start + 1 );
590                }
591            }
592    
593            return subString;
594        }
595    
596        /**
597         * extract a string contained between startDel and endDel, you can remove the delimiters if set true the parameters
598         * delStart and delEnd
599         * 
600         * @param target
601         *            to extract from
602         * @param startDel
603         *            to remove from the start
604         * @param endDel
605         *            string to remove from the end
606         * @param delStart
607         *            true if the start should be removed
608         * @param delEnd
609         *            true if the end should be removed
610         * 
611         * @return the extracted string from the given target. rb: Caution this method may not do what it should.
612         */
613        public static String extractString( String target, String startDel, String endDel, boolean delStart, boolean delEnd ) {
614            if ( target == null ) {
615                return null;
616            }
617            int start = target.indexOf( startDel );
618    
619            if ( start == -1 ) {
620                return null;
621            }
622    
623            String s = target.substring( start, target.indexOf( endDel, start + 1 ) + 1 );
624    
625            s = s.trim();
626    
627            if ( delStart ) {
628                while ( s.startsWith( startDel ) ) {
629                    s = s.substring( startDel.length(), s.length() ).trim();
630                }
631            }
632    
633            if ( delEnd ) {
634                while ( s.endsWith( endDel ) ) {
635                    s = s.substring( 0, s.length() - endDel.length() ).trim();
636                }
637            }
638    
639            return s;
640        }
641    
642        /**
643         * Initialize the substitution map with all normalization rules for a given locale and add this map to the static
644         * localeMap.
645         * 
646         * @param locale
647         * @throws IOException
648         * @throws SAXException
649         * @throws XMLParsingException
650         */
651        private static void initMap( String locale )
652                                throws IOException, SAXException, XMLParsingException {
653    
654            // read normalization file
655            StringBuffer sb = new StringBuffer( 1000 );
656            InputStream is = StringTools.class.getResourceAsStream( "/normalization.xml" );
657            if ( is == null ) {
658                is = StringTools.class.getResourceAsStream( "normalization.xml" );
659            }
660            BufferedReader br = new BufferedReader( new InputStreamReader( is ) );
661            String s = null;
662            while ( ( s = br.readLine() ) != null ) {
663                sb.append( s );
664            }
665            br.close();
666    
667            // transform into xml fragment
668            XMLFragment xml = new XMLFragment();
669            xml.load( new StringReader( sb.toString() ), StringTools.class.getResource( "normalization.xml" ).toString() ); // FIXME
670    
671            // create map
672            Map<String, String> substitutionMap = new HashMap<String, String>( 20 );
673    
674            // extract case attrib ( "toLower" or "toUpper" or missing ) for passed locale
675            String xpath = "Locale[@name = '" + Locale.GERMANY.getLanguage() + "']/@case";
676            String letterCase = XMLTools.getNodeAsString( xml.getRootElement(), xpath,
677                                                          CommonNamespaces.getNamespaceContext(), null );
678            if ( letterCase != null ) {
679                substitutionMap.put( "case", letterCase );
680            }
681    
682            // extract removeDoubles attrib ( "true" or "false" ) for passed locale
683            xpath = "Locale[@name = '" + Locale.GERMANY.getLanguage() + "']/@removeDoubles";
684            String removeDoubles = XMLTools.getNodeAsString( xml.getRootElement(), xpath,
685                                                             CommonNamespaces.getNamespaceContext(), null );
686            if ( removeDoubles != null && removeDoubles.length() > 0 ) {
687                substitutionMap.put( "removeDoubles", removeDoubles );
688            }
689    
690            // extract rules section for passed locale
691            xpath = "Locale[@name = '" + locale + "']/Rule";
692            List<Node> list = XMLTools.getNodes( xml.getRootElement(), xpath, CommonNamespaces.getNamespaceContext() );
693            if ( list != null ) {
694                // for ( int i = 0; i < list.size(); i++ ) {
695                for ( Node n : list ) {
696                    String src = XMLTools.getRequiredNodeAsString( n, "Source", CommonNamespaces.getNamespaceContext() );
697                    String target = XMLTools.getRequiredNodeAsString( n, "Target", CommonNamespaces.getNamespaceContext() );
698                    substitutionMap.put( src, target );
699                }
700            }
701    
702            // init localeMap if needed
703            if ( localeMap == null ) {
704                localeMap = new HashMap<String, Map<String, String>>( 20 );
705            }
706    
707            localeMap.put( locale, substitutionMap );
708        }
709    
710        /**
711         * The passed string gets normalized along the rules for the given locale as they are set in the file
712         * "./normalization.xml". If such rules are specified, the following order is obeyed:
713         * 
714         * <ol>
715         * <li>if the attribute "case" is set with "toLower" or "toUpper", the letters are switched to lower case or to
716         * upper case respectively.</li>
717         * <li>all rules given in the "Rule" elements are performed.</li>
718         * <li>if the attribute "removeDoubles" is set and not empty, all multi occurences of the letters given in this
719         * attribute are reduced to a single occurence.</li>
720         * </ol>
721         * 
722         * @param source
723         *            the String to normalize
724         * @param locale
725         *            the locale language defining the rules to choose, e.g. "de"
726         * @return the normalized String
727         * @throws IOException
728         * @throws SAXException
729         * @throws XMLParsingException
730         */
731        public static String normalizeString( String source, String locale )
732                                throws IOException, SAXException, XMLParsingException {
733    
734            if ( localeMap == null ) {
735                localeMap = new HashMap<String, Map<String, String>>( 20 );
736            }
737            Map<String, String> substitutionMap = localeMap.get( locale );
738    
739            if ( substitutionMap == null ) {
740                initMap( locale );
741            }
742            substitutionMap = localeMap.get( locale );
743    
744            String output = source;
745            Set<String> keys = substitutionMap.keySet();
746    
747            boolean toUpper = false;
748            boolean toLower = false;
749            boolean removeDoubles = false;
750    
751            for ( String key : keys ) {
752                if ( "case".equals( key ) ) {
753                    toUpper = "toUpper".equals( substitutionMap.get( key ) );
754                    toLower = "toLower".equals( substitutionMap.get( key ) );
755                }
756                if ( "removeDoubles".equals( key ) && substitutionMap.get( key ).length() > 0 ) {
757                    removeDoubles = true;
758                }
759            }
760    
761            // first: change letters to upper / lower case
762            if ( toUpper ) {
763                output = output.toUpperCase();
764            } else if ( toLower ) {
765                output = output.toLowerCase();
766            }
767    
768            // second: change string according to specified rules
769            for ( String key : keys ) {
770                if ( !"case".equals( key ) && !"removeDoubles".equals( key ) ) {
771                    output = output.replaceAll( key, substitutionMap.get( key ) );
772                }
773            }
774    
775            // third: remove doubles
776            if ( removeDoubles ) {
777                String doubles = substitutionMap.get( "removeDoubles" );
778                for ( int i = 0; i < doubles.length(); i++ ) {
779                    String remove = "" + doubles.charAt( i ) + "+";
780                    String replaceWith = "" + doubles.charAt( i );
781                    output = output.replaceAll( remove, replaceWith );
782                }
783            }
784            return output;
785        }
786    
787        /**
788         * prints a map with one line for each key-value pair
789         * @param map
790         * @param ps if ps is null System.out will be used 
791         */
792        public static final void printMap( Map<?, ?> map, PrintStream ps ) {
793            if ( ps == null ) {
794                ps = System.out;
795            }
796            Iterator<?> iter = map.keySet().iterator();
797            while ( iter.hasNext() ) {
798                Object key = (Object) iter.next();
799                Object value = map.get( key );
800                ps.println( key + " : " + value );
801            }
802        }
803    }