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