View Javadoc

1   package org.apache.velocity.tools.generic;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.    
20   */
21  
22  import java.lang.reflect.Array;
23  import java.text.MessageFormat;
24  import java.util.ArrayList;
25  import java.util.Collection;
26  import java.util.Collections;
27  import java.util.Iterator;
28  import java.util.regex.Pattern;
29  
30  import org.apache.commons.beanutils.PropertyUtils;
31  import org.apache.velocity.tools.config.DefaultKey;
32  
33  /**
34   * Provides general utility methods for controlling the display of references.
35   * Currently, this class contains methods for "pretty printing" an array or
36   * {@link Collection}, methods for truncating the string value of a reference
37   * at a configured or specified length, methods for displaying an alternate
38   * value when a specified value is null, a method for generating whitespace, 
39   * a "printf" type of method for formatting messages, and
40   * methods for forcing values into "cells" of equal size (via truncation or
41   * padding with whitespace).
42   *
43   * <p><b>Example Use:</b>
44   * <pre>
45   * tools.xml...
46   * &lt;tools&gt;
47   *   &lt;toolbox scope="application"&gt;
48   *     &lt;tool class="org.apache.velocity.tools.generic.DisplayTool"/&gt;
49   *   &lt;/toolbox&gt;
50   * &lt;/tools&gt;
51   *
52   * template...
53   *   #set( $list = [1..5] )
54   *   $display.list($list)
55   *   $display.truncate("This is a long string.", 10)
56   *   Not Null: $display.alt("not null", "--")
57   *   Null: $display.alt($null, "--")
58   *
59   * output...
60   *   1, 2, 3, 4 and 5
61   *   This is...
62   *   Not Null: not null
63   *   Null: --
64   *   
65   * </pre></p>
66   *
67   * @since VelocityTools 2.0
68   * @author <a href="sean@somacity.com">Sean Legassick</a>
69   * @author <a href="dlr@collab.net">Daniel Rall</a>
70   * @author Nathan Bubna
71   * @version $Id: DisplayTool.java 463298 2006-10-12 16:10:32Z henning $
72   */
73  @DefaultKey("display")
74  public class DisplayTool extends LocaleConfig
75  {
76      public static final String LIST_DELIM_KEY = "listDelim";
77      public static final String LIST_FINAL_DELIM_KEY = "listFinalDelim";
78      public static final String TRUNCATE_LENGTH_KEY = "truncateLength";
79      public static final String TRUNCATE_SUFFIX_KEY = "truncateSuffix";
80      public static final String TRUNCATE_AT_WORD_KEY = "truncateAtWord";
81      public static final String CELL_LENGTH_KEY = "cellLength";
82      public static final String CELL_SUFFIX_KEY = "cellSuffix";
83      public static final String DEFAULT_ALTERNATE_KEY = "defaultAlternate";
84      public static final String ALLOWED_TAGS_KEY = "allowedTags";
85  
86      private String defaultDelim = ", ";
87      private String defaultFinalDelim = " and ";
88      private int defaultTruncateLength = 30;
89      private String defaultTruncateSuffix = "...";
90      private boolean defaultTruncateAtWord = false;
91      private int defaultCellLength = 30;
92      private String defaultCellSuffix = "...";
93      private String defaultAlternate = "null";
94      private String[] defaultAllowedTags = null;
95  
96      /**
97       * Does the actual configuration. This is protected, so
98       * subclasses may share the same ValueParser and call configure
99       * at any time, while preventing templates from doing so when 
100      * configure(Map) is locked.
101      */
102     protected void configure(ValueParser values)
103     {
104         super.configure(values);
105         
106         String listDelim = values.getString(LIST_DELIM_KEY);
107         if (listDelim != null)
108         {
109             setListDelimiter(listDelim);
110         }
111 
112         String listFinalDelim = values.getString(LIST_FINAL_DELIM_KEY);
113         if (listFinalDelim != null)
114         {
115             setListFinalDelimiter(listFinalDelim);
116         }
117 
118         Integer truncateLength = values.getInteger(TRUNCATE_LENGTH_KEY);
119         if (truncateLength != null)
120         {
121             setTruncateLength(truncateLength);
122         }
123 
124         String truncateSuffix = values.getString(TRUNCATE_SUFFIX_KEY);
125         if (truncateSuffix != null)
126         {
127             setTruncateSuffix(truncateSuffix);
128         }
129 
130         Boolean truncateAtWord = values.getBoolean(TRUNCATE_AT_WORD_KEY);
131         if (truncateAtWord != null)
132         {
133             setTruncateAtWord(truncateAtWord);
134         }
135 
136         Integer cellLength = values.getInteger(CELL_LENGTH_KEY);
137         if (cellLength != null)
138         {
139             setCellLength(cellLength);
140         }
141 
142         String cellSuffix = values.getString(CELL_SUFFIX_KEY);
143         if (cellSuffix != null)
144         {
145             setCellSuffix(cellSuffix);
146         }
147 
148         String defaultAlternate = values.getString(DEFAULT_ALTERNATE_KEY);
149         if (defaultAlternate != null)
150         {
151             setDefaultAlternate(defaultAlternate);
152         }
153 
154         String[] allowedTags = values.getStrings(ALLOWED_TAGS_KEY);
155         if (allowedTags != null)
156         {
157             setAllowedTags(allowedTags);
158         }
159     }
160 
161     public String getListDelimiter()
162     {
163         return this.defaultDelim;
164     }
165 
166     protected void setListDelimiter(String delim)
167     {
168         this.defaultDelim = delim;
169     }
170 
171     public String getListFinalDelimiter()
172     {
173         return this.defaultFinalDelim;
174     }
175 
176     protected void setListFinalDelimiter(String finalDelim)
177     {
178         this.defaultFinalDelim = finalDelim;
179     }
180 
181     public int getTruncateLength()
182     {
183         return this.defaultTruncateLength;
184     }
185 
186     protected void setTruncateLength(int maxlen)
187     {
188         this.defaultTruncateLength = maxlen;
189     }
190 
191     public String getTruncateSuffix()
192     {
193         return this.defaultTruncateSuffix;
194     }
195 
196     protected void setTruncateSuffix(String suffix)
197     {
198         this.defaultTruncateSuffix = suffix;
199     }
200 
201     public boolean getTruncateAtWord()
202     {
203         return this.defaultTruncateAtWord;
204     }
205 
206     protected void setTruncateAtWord(boolean atWord)
207     {
208         this.defaultTruncateAtWord = atWord;
209     }
210 
211     public String getCellSuffix()
212     {
213         return this.defaultCellSuffix;
214     }
215 
216     protected void setCellSuffix(String suffix)
217     {
218         this.defaultCellSuffix = suffix;
219     }
220 
221     public int getCellLength()
222     {
223         return this.defaultCellLength;
224     }
225 
226     protected void setCellLength(int maxlen)
227     {
228         this.defaultCellLength = maxlen;
229     }
230 
231     public String getDefaultAlternate()
232     {
233         return this.defaultAlternate;
234     }
235 
236     protected void setDefaultAlternate(String dflt)
237     {
238         this.defaultAlternate = dflt;
239     }
240 
241     public String[] getAllowedTags()
242     {
243         return this.defaultAllowedTags;
244     }
245 
246     protected void setAllowedTags(String[] tags)
247     {
248         this.defaultAllowedTags = tags;
249     }
250 
251 
252     /**
253      * Formats a collection or array into the form "A, B and C".
254      *
255      * @param list A collection or array.
256      * @return A String.
257      */
258     public String list(Object list)
259     {
260         return list(list, this.defaultDelim, this.defaultFinalDelim);
261     }
262 
263     /**
264      * Formats a collection or array into the form
265      * "A&lt;delim&gt;B&lt;delim&gt;C".
266      *
267      * @param list A collection or array.
268      * @param delim A String.
269      * @return A String.
270      */
271     public String list(Object list, String delim)
272     {
273         return list(list, delim, delim);
274     }
275 
276     /**
277      * Formats a collection or array into the form
278      * "A&lt;delim&gt;B&lt;finaldelim&gt;C".
279      * 
280      * @param list A collection or array.
281      * @param delim A String.
282      * @param finaldelim A String.
283      * @return A String.
284      */
285     public String list(Object list, String delim, String finaldelim)
286     {
287         return list(list, delim, finaldelim, null);
288     }
289 
290     /**
291      * Formats a specified property of collection or array of objects into the
292      * form "A&lt;delim&gt;B&lt;finaldelim&gt;C".
293      * 
294      * @param list A collection or array.
295      * @param delim A String.
296      * @param finaldelim A String.
297      * @param property An object property to format.
298      * @return A String.
299      */
300     public String list(Object list, String delim, String finaldelim,
301                        String property)
302     {
303         if (list == null)
304         {
305             return null;
306         }
307         if (list instanceof Collection)
308         {
309             return format((Collection) list, delim, finaldelim, property);
310         }
311         Collection items;
312         if (list.getClass().isArray())
313         {
314             int size = Array.getLength(list);
315             items = new ArrayList(size);
316             for (int i = 0; i < size; i++)
317             {
318                 items.add(Array.get(list, i));
319             }
320         }
321         else
322         {
323             items = Collections.singletonList(list);
324         }
325         return format(items, delim, finaldelim, property);
326     }
327 
328     /**
329      * Does the actual formatting of the collection.
330      */
331     protected String format(Collection list, String delim, String finaldelim,
332                             String property)
333     {
334         StringBuilder sb = new StringBuilder();
335         int size = list.size();
336         Iterator iterator = list.iterator();
337         for (int i = 0; i < size; i++)
338         {
339             if (property != null && property.length() > 0)
340             {
341                 sb.append(getProperty(iterator.next(), property));
342             }
343             else
344             {
345                 sb.append(iterator.next());
346             }
347             if (i < size - 2)
348             {
349                 sb.append(delim);
350             }
351             else if (i < size - 1)
352             {
353                 sb.append(finaldelim);
354             }
355         }
356         return sb.toString();
357     }
358 
359     /**
360      * @deprecated Will be unnecessary with Velocity 1.6
361      */
362     @Deprecated 
363     public String message(String format, Collection args)
364     {
365         return message(format, new Object[] { args });
366     }
367 
368     /**
369      * @deprecated Will be unnecessary with Velocity 1.6
370      */
371     @Deprecated 
372     public String message(String format, Object arg)
373     {
374         return message(format, new Object[] { arg });
375     }
376 
377     /**
378      * @deprecated Will be unnecessary with Velocity 1.6
379      */
380     @Deprecated 
381     public String message(String format, Object arg1, Object arg2)
382     {
383         return message(format, new Object[] { arg1, arg2 });
384     }
385 
386     /**
387      * Uses {@link MessageFormat} to format the specified String with
388      * the specified arguments. If there are no arguments, then the String
389      * is returned directly.  Please note that the format
390      * required here is quite different from that of
391      * {@link #printf(String,Object...)}.
392      *
393      * @since VelocityTools 2.0
394      */
395     public String message(String format, Object... args)
396     {
397         if (format == null)
398         {
399             return null;
400         }
401         if (args == null || args.length == 0)
402         {
403             return format;
404         }
405         else if (args.length == 1 && args[0] instanceof Collection)
406         {
407             Collection list = (Collection)args[0];
408             if (list.isEmpty())
409             {
410                 return format;
411             }
412             else
413             {
414                 args = list.toArray();
415             }
416         }
417         return MessageFormat.format(format, args);
418     }
419 
420     /**
421      * Uses {@link String#format(Locale,String,Object...} to format the specified String
422      * with the specified arguments.  Please note that the format
423      * required here is quite different from that of
424      * {@link #message(String,Object...)}.
425      *
426      * @see java.util.Formatter
427      * @since VelocityTools 2.0
428      */
429     public String printf(String format, Object... args)
430     {
431         if (format == null)
432         {
433             return null;
434         }
435         if (args == null || args.length == 0)
436         {
437             return format;
438         }
439         if (args.length == 1 && args[0] instanceof Collection)
440         {
441             Collection list = (Collection)args[0];
442             if (list.isEmpty())
443             {
444                 return format;
445             }
446             else
447             {
448                 args = list.toArray();
449             }
450         }
451         return String.format(getLocale(), format, args);
452     }
453 
454     /**
455      * Limits the string value of 'truncateMe' to the configured max length
456      * in characters (default is 30 characters).
457      * If the string gets curtailed, the configured suffix
458      * (default is "...") is used as the ending of the truncated string.
459      *
460      * @param truncateMe The value to be truncated.
461      * @return A String.
462      */
463     public String truncate(Object truncateMe)
464     {
465         return truncate(truncateMe, this.defaultTruncateLength);
466     }
467 
468     /**
469      * Limits the string value of 'truncateMe' to 'maxLength' characters.
470      * If the string gets curtailed, the configured suffix
471      * (default is "...") is used as the ending of the truncated string.
472      *
473      * @param maxLength An int with the maximum length.
474      * @param truncateMe The value to be truncated.
475      * @return A String.
476      */
477     public String truncate(Object truncateMe, int maxLength)
478     {
479         return truncate(truncateMe, maxLength, this.defaultTruncateSuffix);
480     }
481 
482     /**
483      * Limits the string value of 'truncateMe' to the configured max length
484      * in characters (default is 30 characters).
485      * If the string gets curtailed, the specified suffix
486      * is used as the ending of the truncated string.
487      *
488      * @param truncateMe The value to be truncated.
489      * @param suffix A String.
490      * @return A String.
491      */
492     public String truncate(Object truncateMe, String suffix)
493     {
494         return truncate(truncateMe, this.defaultTruncateLength, suffix);
495     }
496 
497     /**
498      * Limits the string value of 'truncateMe' to the specified max length in
499      * characters. If the string gets curtailed, the specified suffix is used as
500      * the ending of the truncated string.
501      * 
502      * @param truncateMe The value to be truncated.
503      * @param maxLength An int with the maximum length.
504      * @param suffix A String.
505      * @return A String.
506      */
507     public String truncate(Object truncateMe, int maxLength, String suffix)
508     {
509         return truncate(truncateMe, maxLength, suffix, defaultTruncateAtWord);
510     }
511 
512     /**
513      * Limits the string value of 'truncateMe' to the latest complete word
514      * within the specified maxLength. If the string gets curtailed, the
515      * specified suffix is used as the ending of the truncated string.
516      * 
517      * @param truncateMe The value to be truncated.
518      * @param maxLength An int with the maximum length.
519      * @param suffix A String.
520      * @param defaultTruncateAtWord Truncate at a word boundary if true.
521      * @return A String.
522      */
523     public String truncate(Object truncateMe, int maxLength, String suffix,
524                            boolean defaultTruncateAtWord)
525     {
526         if (truncateMe == null || maxLength <= 0)
527         {
528             return null;
529         }
530 
531         String string = String.valueOf(truncateMe);
532         if (string.length() <= maxLength)
533         {
534             return string;
535         }
536         if (suffix == null || maxLength - suffix.length() <= 0)
537         {
538             // either no need or no room for suffix
539             return string.substring(0, maxLength);
540         }
541         if (defaultTruncateAtWord)
542         {
543             // find the latest space within maxLength
544             int lastSpace = string.substring(0, maxLength - suffix.length() + 1)
545                             .lastIndexOf(" ");
546             if (lastSpace > suffix.length())
547             {
548                 return string.substring(0, lastSpace) + suffix;
549             }
550         }
551         // truncate to exact character and append suffix
552         return string.substring(0, maxLength - suffix.length()) + suffix;
553 
554     }
555 
556     /**
557      * Returns a string of spaces of the specified length.
558      * @param length the number of spaces to return
559      */
560     public String space(int length)
561     {
562         if (length < 0)
563         {
564             return null;
565         }
566 
567         StringBuilder space = new StringBuilder();
568         for (int i=0; i < length; i++)
569         {
570             space.append(' ');
571         }
572         return space.toString();
573     }
574 
575     /**
576      * Truncates or pads the string value of the specified object as necessary
577      * to ensure that the returned string's length equals the default cell size.
578      * @param obj the value to be put in the 'cell'
579      */
580     public String cell(Object obj)
581     {
582         return cell(obj, this.defaultCellLength);
583     }
584 
585     /**
586      * Truncates or pads the string value of the specified object as necessary
587      * to ensure that the returned string's length equals the specified cell size.
588      * @param obj the value to be put in the 'cell'
589      * @param cellsize the size of the cell into which the object must be placed
590      */
591     public String cell(Object obj, int cellsize)
592     {
593         return cell(obj, cellsize, this.defaultCellSuffix);
594     }
595 
596     /**
597      * Truncates or pads the string value of the specified object as necessary
598      * to ensure that the returned string's length equals the default cell size.
599      * If truncation is necessary, the specified suffix will replace the end of
600      * the string value to indicate that.
601      * @param obj the value to be put in the 'cell'
602      * @param suffix the suffix to put at the end of any values that need truncating
603      *               to indicate that they've been truncated
604      */
605     public String cell(Object obj, String suffix)
606     {
607         return cell(obj, this.defaultCellLength, suffix);
608     }
609 
610     /**
611      * Truncates or pads the string value of the specified object as necessary
612      * to ensure that the returned string's length equals the specified cell size.
613      * @param obj the value to be put in the 'cell'
614      * @param cellsize the size of the cell into which the object must be placed
615      * @param suffix the suffix to put at the end of any values that need truncating
616      *               to indicate that they've been truncated
617      */
618     public String cell(Object obj, int cellsize, String suffix)
619     {
620         if (obj == null || cellsize <= 0)
621         {
622             return null;
623         }
624 
625         String value = String.valueOf(obj);
626         if (value.length() == cellsize)
627         {
628             return value;
629         }
630         else if (value.length() > cellsize)
631         {
632             return truncate(value, cellsize, suffix);
633         }
634         else
635         {
636             return value + space(cellsize - value.length());
637         }    
638     }
639 
640     /**
641      * Changes the first character of the string value of the specified object
642      * to upper case and returns the resulting string.
643      *
644      * @param capitalizeMe The value to be capitalized.
645      */
646     public String capitalize(Object capitalizeMe)
647     {
648         if (capitalizeMe == null)
649         {
650             return null;
651         }
652 
653         String string = String.valueOf(capitalizeMe);
654         switch (string.length())
655         {
656             case 0:
657                 return string;
658             case 1:
659                 return string.toUpperCase();
660             default:
661                 StringBuilder out = new StringBuilder(string.length());
662                 out.append(string.substring(0,1).toUpperCase());
663                 out.append(string.substring(1, string.length()));
664                 return out.toString();
665         }
666     }
667 
668     /**
669      * Changes the first character of the string value of the specified object
670      * to lower case and returns the resulting string.
671      *
672      * @param uncapitalizeMe The value to be uncapitalized.
673      */
674     public String uncapitalize(Object uncapitalizeMe)
675     {
676         if (uncapitalizeMe == null)
677         {
678             return null;
679         }
680 
681         String string = String.valueOf(uncapitalizeMe);
682         switch (string.length())
683         {
684             case 0:
685                 return string;
686             case 1:
687                 return string.toLowerCase();
688             default:
689                 StringBuilder out = new StringBuilder(string.length());
690                 out.append(string.substring(0,1).toLowerCase());
691                 out.append(string.substring(1, string.length()));
692                 return out.toString();
693         }
694     }
695 
696     /**
697      * Returns a configured default value if specified value is null.
698      * @param checkMe
699      * @return a configured default value if the specified value is null.
700      */
701     public Object alt(Object checkMe)
702     {
703         return alt(checkMe, this.defaultAlternate);
704     }
705 
706     /**
707      * Returns the second argument if first argument specified is null.
708      * @param checkMe
709      * @param alternate
710      * @return the second argument if the first is null.
711      */
712     public Object alt(Object checkMe, Object alternate)
713     {
714         if (checkMe == null)
715         {
716             return alternate;
717         }
718         return checkMe;
719     }
720 
721     /**
722      * Inserts HTML line break tag (&lt;br /&gt;) in front of all newline
723      * characters of the string value of the specified object and returns the
724      * resulting string.
725      * @param obj
726      */
727     public String br(Object obj)
728     {
729         if (obj == null) 
730         {
731             return null;
732         }
733         else
734         {
735             return String.valueOf(obj).replaceAll("\n", "<br />\n");
736         }
737     }
738 
739     /**
740      * Removes HTML tags from the string value of the specified object and
741      * returns the resulting string.
742      * @param obj
743      */
744     public String stripTags(Object obj)
745     {
746         return stripTags(obj, defaultAllowedTags);
747     }
748 
749     /**
750      * Removes all not allowed HTML tags from the string value of the specified
751      * object and returns the resulting string.
752      * @param obj
753      * @param allowedTags An array of allowed tag names (i.e. "h1","br","img")
754      */
755     public String stripTags(Object obj, String... allowedTags)
756     {
757         if (obj == null)
758         {
759             return null;
760         }
761         
762         //build list of tags to be used in regex pattern
763         StringBuilder allowedTagList = new StringBuilder();
764         if (allowedTags != null)
765         {
766             for (String tag : allowedTags)
767             {
768                 if (tag !=null && tag.matches("[a-zA-Z0-9]+"))
769                 {
770                     if (allowedTagList.length() > 0)
771                     {
772                         allowedTagList.append("|");
773                     }
774                     allowedTagList.append(tag);
775                 }
776             }
777         }
778         String tagRule = "<[^>]*?>";
779         if (allowedTagList.length() > 0)
780         {
781             tagRule = "<(?!/?(" + allowedTagList.toString() + ")[\\s>/])[^>]*?>";
782         }
783         return Pattern.compile(tagRule, Pattern.CASE_INSENSITIVE)
784                 .matcher(String.valueOf(obj)).replaceAll("");
785     }
786 
787     /**
788      * Builds plural form of a passed word if 'value' is plural, otherwise
789      * returns 'singular'. Plural form is built using some basic English
790      * language rules for nouns which does not guarantee correct syntax of a
791      * result in all cases.
792      * @param value
793      * @param singular Singular form of a word.
794      */
795     public String plural(int value, String singular)
796     {
797         return plural(value, singular, null);
798     }
799 
800     /**
801      * Returns 'plural' parameter if passed 'value' is plural, otherwise
802      * 'singular' is returned.
803      * @param value
804      * @param singular Singular form of a word.
805      * @param plural Plural form of a word.
806      */
807     public String plural(int value, String singular, String plural)
808     {
809         if (value == 1 || value == -1)
810         {
811             return singular;
812         }
813         else if (plural != null)
814         {
815             return plural;
816         }
817         else if (singular == null || singular.length() == 0)
818         {
819             return singular;
820         }
821         else
822         {
823             //if the last letter is capital then we will append capital letters 
824             boolean isCapital = !singular.substring(singular.length() - 1)
825                                 .toLowerCase().equals(singular
826                                 .substring(singular.length() - 1));
827             
828             String word = singular.toLowerCase();
829             
830             if (word.endsWith("x") || word.endsWith("sh")
831                     || word.endsWith("ch") || word.endsWith("s"))
832             {
833                 return singular.concat(isCapital ? "ES" : "es");
834             }
835             else if (word.length() > 1
836                     && word.endsWith("y")
837                     && !word.substring(word.length() - 2, word.length() - 1)
838                             .matches("[aeiou]"))
839             {
840                 return singular.substring(0, singular.length() - 1)
841                         .concat(isCapital ? "IES" : "ies");
842             }
843             else
844             {
845                 return singular.concat(isCapital ? "S" : "s");
846             }
847         }
848     }
849 
850     /**
851      * Safely retrieves the specified property from the specified object.
852      * Subclasses that wish to perform more advanced, efficient, or just
853      * different property retrieval methods should override this method to do
854      * so.
855      */
856     protected Object getProperty(Object object, String property)
857     {
858         try
859         {
860             return PropertyUtils.getProperty(object, property);
861         }
862         catch (Exception e)
863         {
864             throw new IllegalArgumentException("Could not retrieve '"
865                     + property + "' from " + object + ": " + e);
866         }
867     }
868 
869     /**
870      * Returns the {@link Measurements} of the string value of the specified object.
871      */
872     public Measurements measure(Object measureMe)
873     {
874         if (measureMe == null)
875         {
876             return null;
877         }
878         return new Measurements(String.valueOf(measureMe));
879     }
880 
881 
882     /**
883      * Measures the dimensions of the string given to its constructor.
884      * Height is the number of lines in the string.
885      * Width is the number of characters in the longest line.
886      */
887     public static class Measurements
888     {
889         private int height;
890         private int width;
891 
892         public Measurements(String s)
893         {
894             String[] lines = s.split("\n");
895             height = lines.length;
896             for (String line : lines)
897             {
898                 if (line.length() > width)
899                 {
900                     width = line.length();
901                 }
902             }
903         }
904 
905         public int getHeight()
906         {
907             return height;
908         }
909 
910         public int getWidth()
911         {
912             return width;
913         }
914 
915         public String toString()
916         {
917             StringBuilder out = new StringBuilder(28);
918             out.append("{ height: ");
919             out.append(height);
920             out.append(", width: ");
921             out.append(width);
922             out.append(" }");
923             return out.toString();
924         }
925     }
926 
927 }