1 /*
2  *  Avis event router.
3  *
4  *  Copyright (C) 2008 Matthew Phillips <avis@mattp.name>
5  *
6  *  This program is free software: you can redistribute it and/or
7  *  modify it under the terms of the GNU General Public License
8  *  version 3 as published by the Free Software Foundation.
9  *
10  *  This program is distributed in the hope that it will be useful,
11  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13  *  General Public License for more details.
14  *
15  *  You should have received a copy of the GNU General Public License
16  *  along with this program. If not, see <http://www.gnu.org/licenses/>.
17  */
18 package org.avis.util;
19 
20 import java.util.List;
21 import java.util.Map;
22 
23 import java.nio.charset.CharacterCodingException;
24 
25 import static java.lang.Integer.toHexString;
26 import static java.lang.String.CASE_INSENSITIVE_ORDER;
27 import static java.lang.System.arraycopy;
28 import static java.lang.System.identityHashCode;
29 import static java.util.Arrays.asList;
30 import static java.util.Arrays.sort;
31 
32 import static org.avis.io.XdrCoding.fromUTF8;
33 import static org.avis.io.XdrCoding.toUTF8;
34 
35 
36 /**
37  * General text formatting utilities.
38  *
39  * @author Matthew Phillips
40  */
41 public final class Text
42 {
43   private static final char [] HEX_TABLE =
44     {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
45      'a', 'b', 'c', 'd', 'e', 'f'};
46 
47   private static final String [] EMPTY_STRING_ARRAY = new String [0];
48 
Text()49   private Text ()
50   {
51     // cannot be instantiated
52   }
53 
54   /**
55    * Return just the name (minus the package) of an object's class.
56    */
className(Object object)57   public static String className (Object object)
58   {
59     return className (object.getClass ());
60   }
61 
62   /**
63    * Return just the name (minus the package) of a class.
64    */
className(Class<?> type)65   public static String className (Class<?> type)
66   {
67     String name = type.getName ();
68 
69     return name.substring (name.lastIndexOf ('.') + 1);
70   }
71 
72   /**
73    * Generate a short exception message without package name and
74    * message (if null).
75    */
shortException(Throwable ex)76   public static String shortException (Throwable ex)
77   {
78     if (ex.getMessage () == null)
79       return className (ex.getClass ());
80     else
81       return className (ex.getClass ()) + ": " + ex.getMessage ();
82   }
83 
84   /**
85    * Generate a hex ID for an object.
86    */
idFor(Object instance)87   public static String idFor (Object instance)
88   {
89     return toHexString (identityHashCode (instance));
90   }
91 
92   /**
93    * Generate a string value of the notification.
94    *
95    * @param attributes The attribute name/value pairs.
96    *
97    * @return The string formatted version of the notification attributes.
98    */
formatNotification(Map<String, Object> attributes)99   public static String formatNotification (Map<String, Object> attributes)
100   {
101     String [] names = new String [attributes.size ()];
102 
103     attributes.keySet ().toArray (names);
104 
105     sort (names, CASE_INSENSITIVE_ORDER);
106 
107     StringBuilder str = new StringBuilder (names.length * 16);
108     boolean first = true;
109 
110     for (String name : names)
111     {
112       if (!first)
113         str.append ('\n');
114 
115       first = false;
116 
117       appendEscaped (str, name, " :");
118 
119       str.append (": ");
120 
121       appendValue (str, attributes.get (name));
122     }
123 
124     return str.toString ();
125   }
126 
appendValue(StringBuilder str, Object value)127   private static void appendValue (StringBuilder str, Object value)
128   {
129     if (value instanceof String)
130     {
131       str.append ('"');
132       appendEscaped (str, (String)value, '"');
133       str.append ('"');
134     } else if (value instanceof Number)
135     {
136       str.append (value);
137 
138       if (value instanceof Long)
139         str.append ('L');
140     } else
141     {
142       str.append ('[');
143       appendHexBytes (str, (byte [])value);
144       str.append (']');
145     }
146   }
147 
148   /**
149    * Append a string to a builder, escaping (with '\') any instances
150    * of a special character.
151    */
appendEscaped(StringBuilder builder, String string, char charToEscape)152   public static void appendEscaped (StringBuilder builder,
153                                     String string, char charToEscape)
154   {
155     for (int i = 0; i < string.length (); i++)
156     {
157       char c = string.charAt (i);
158 
159       if (c == charToEscape)
160         builder.append ('\\');
161 
162       builder.append (c);
163     }
164   }
165 
166   /**
167    * Append a string to a builder, escaping (with '\') any instances
168    * of a set of special characters.
169    */
appendEscaped(StringBuilder builder, String string, String charsToEscape)170   public static void appendEscaped (StringBuilder builder,
171                                     String string, String charsToEscape)
172   {
173     for (int i = 0; i < string.length (); i++)
174     {
175       char c = string.charAt (i);
176 
177       if (charsToEscape.indexOf (c) != -1)
178         builder.append ('\\');
179 
180       builder.append (c);
181     }
182   }
183 
184   /**
185    * Append a byte array to a builder in form: 01 e2 fe ff ...
186    */
appendHexBytes(StringBuilder str, byte [] bytes)187   public static void appendHexBytes (StringBuilder str, byte [] bytes)
188   {
189     boolean first = true;
190 
191     for (byte b : bytes)
192     {
193       if (!first)
194         str.append (' ');
195 
196       first = false;
197 
198       appendHex (str, b);
199     }
200   }
201 
202   /**
203    * Append the hex form of a byte to a builder.
204    */
appendHex(StringBuilder str, byte b)205   public static void appendHex (StringBuilder str, byte b)
206   {
207     str.append (HEX_TABLE [(b >>> 4) & 0x0F]);
208     str.append (HEX_TABLE [(b >>> 0) & 0x0F]);
209   }
210 
211   /**
212    * Parse a string expression as a hex-coded unsigned byte.
213    *
214    * @return A byte in the range 0 - 255 if sign is ignored.
215    */
hexToByte(String byteExpr)216   public static byte hexToByte (String byteExpr)
217     throws InvalidFormatException
218   {
219     if (byteExpr.length () == 0)
220     {
221       throw new InvalidFormatException ("Byte value cannot be empty");
222     } else if (byteExpr.length () > 2)
223     {
224       throw new InvalidFormatException
225         ("Byte value too long: \"" + byteExpr + "\"");
226     }
227 
228     int value = 0;
229 
230     for (int i = 0; i < byteExpr.length (); i++)
231       value = (value << 4) | hexValue (byteExpr.charAt (i));
232 
233     return (byte)value;
234   }
235 
236   /**
237    * Parse a string expression as a value. Values may be quoted
238    * strings ("string"), numbers (0.1, 3, 123456789L), or byte arrays
239    * ([0a ff de ad]).
240    *
241    * @param expr The string expression.
242    *
243    * @return The value.
244    *
245    * @throws InvalidFormatException if expr is not parseable.
246    *
247    * @see #stringToNumber(String)
248    * @see #stringToOpaque(String)
249    * @see #quotedStringToString(String)
250    */
stringToValue(String expr)251   public static Object stringToValue (String expr)
252     throws InvalidFormatException
253   {
254     char firstChar = expr.charAt (0);
255 
256     if (firstChar == '"' || firstChar == '\'')
257       return quotedStringToString (expr);
258     else if (firstChar >= '0' && firstChar <= '9')
259       return stringToNumber (expr);
260     else if (firstChar == '[')
261       return stringToOpaque (expr);
262     else
263       throw new InvalidFormatException
264         ("Unrecognised value expression: \"" + expr + "\"");
265   }
266 
267   /**
268    * Parse a numeric int, long or double value. e.g. 32L, 3.14, 42.
269    */
stringToNumber(String valueExpr)270   public static Number stringToNumber (String valueExpr)
271     throws InvalidFormatException
272   {
273     try
274     {
275       if (valueExpr.indexOf ('.') != -1)
276         return Double.valueOf (valueExpr);
277       else if (valueExpr.endsWith ("L") || valueExpr.endsWith ("l"))
278         return Long.decode (valueExpr.substring (0, valueExpr.length () - 1));
279       else
280         return Integer.decode (valueExpr);
281     } catch (NumberFormatException ex)
282     {
283       throw new InvalidFormatException ("Invalid number: " + valueExpr);
284     }
285   }
286 
287   /**
288    * Parse a string value in the format "string", allowing escaped "'s
289    * inside the string.
290    */
quotedStringToString(String valueExpr)291   public static String quotedStringToString (String valueExpr)
292     throws InvalidFormatException
293   {
294     if (valueExpr.length () == 0)
295       throw new InvalidFormatException ("Empty string");
296 
297     char quote = valueExpr.charAt (0);
298 
299     if (quote != '\'' && quote != '"')
300       throw new InvalidFormatException ("String must start with a quote");
301 
302     int last = findFirstNonEscaped (valueExpr, 1, quote);
303 
304     if (last == -1)
305       throw new InvalidFormatException ("Missing terminating quote in string");
306     else if (last != valueExpr.length () - 1)
307       throw new InvalidFormatException ("Extra characters following string");
308 
309     return stripBackslashes (valueExpr.substring (1, last));
310   }
311 
312   /**
313    * Parse an opaque value expression e.g. [00 0f 01].
314    */
stringToOpaque(String valueExpr)315   public static byte [] stringToOpaque (String valueExpr)
316     throws InvalidFormatException
317   {
318     if (valueExpr.length () < 2)
319       throw new InvalidFormatException ("Opaque value too short");
320     else if (valueExpr.charAt (0) != '[')
321       throw new InvalidFormatException ("Missing '[' at start of opaque");
322 
323     int closingBrace = valueExpr.indexOf (']');
324 
325     if (closingBrace == -1)
326       throw new InvalidFormatException ("Missing closing \"]\"");
327     else if (closingBrace != valueExpr.length () - 1)
328       throw new InvalidFormatException ("Junk at end of opaque value");
329 
330     return hexToBytes (valueExpr.substring (1, closingBrace));
331   }
332 
333   /**
334    * Parse a series of hex pairs as a sequence of unsigned bytes.
335    * Pairs may be separated by optional whitespace. e.g. "0A FF 00 01"
336    * or "deadbeef".
337    */
hexToBytes(String string)338   public static byte [] hexToBytes (String string)
339     throws InvalidFormatException
340   {
341     string = string.replaceAll ("\\s+", "");
342 
343     if (string.length () % 2 != 0)
344       throw new InvalidFormatException ("Hex bytes must be a set of hex pairs");
345 
346     byte [] bytes = new byte [string.length () / 2];
347 
348     for (int i = 0; i < string.length (); i += 2)
349       bytes [i / 2] = hexToByte (string.substring (i, i + 2));
350 
351     return bytes;
352   }
353 
354   /**
355    * Turn an array of bytes into a hex-encoded string e.g. "00 01 aa de".
356    */
bytesToHex(byte [] bytes)357   public static String bytesToHex (byte [] bytes)
358   {
359     StringBuilder str = new StringBuilder (bytes.length * 3);
360 
361     appendHexBytes (str, bytes);
362 
363     return str.toString ();
364   }
365 
366   /**
367    * Turn a data block expression into a block of bytes.
368    *
369    * Formats:
370    * <pre>
371    *   Hex pairs: [0a 02 ff 31]
372    *   String:    "hello"
373    *   Raw data:  #data
374    * </pre>
375    *
376    * @param expr The data block expression
377    * @return The data.
378    *
379    * @throws InvalidFormatException if the expression was not valid.
380    */
dataToBytes(byte [] expr)381   public static byte [] dataToBytes (byte [] expr)
382     throws InvalidFormatException
383   {
384     if (expr.length == 0)
385       throw new InvalidFormatException ("Expression cannot be empty");
386 
387     try
388     {
389       switch (expr [0])
390       {
391         case '[':
392           return stringToOpaque (fromUTF8 (expr, 0, expr.length).trim ());
393         case '"':
394           return toUTF8 (quotedStringToString (fromUTF8 (expr, 0, expr.length).trim ()));
395         case '#':
396           return slice (expr, 1, expr.length);
397         default:
398           throw new InvalidFormatException ("Unknown data block format");
399       }
400     } catch (CharacterCodingException ex)
401     {
402       throw new InvalidFormatException ("Invalid UTF-8 string");
403     }
404   }
405 
slice(byte [] bytes, int start, int end)406   public static byte [] slice (byte [] bytes, int start, int end)
407   {
408     byte [] slice = new byte [end - start];
409 
410     arraycopy (bytes, start, slice, 0, slice.length);
411 
412     return slice;
413   }
414 
415   /**
416    * Find the first index of the given character, skipping instances
417    * that are escaped by '\'.
418    */
findFirstNonEscaped(String str, char toFind)419   public static int findFirstNonEscaped (String str, char toFind)
420   {
421     return findFirstNonEscaped (str, 0, toFind);
422   }
423 
424   /**
425    * Find the first index of the given character, skipping instances
426    * that are escaped by '\'.
427    */
findFirstNonEscaped(String str, int start, char toFind)428   public static int findFirstNonEscaped (String str, int start, char toFind)
429   {
430     boolean escaped = false;
431 
432     for (int i = start; i < str.length (); i++)
433     {
434       char c = str.charAt (i);
435 
436       if (c == '\\')
437       {
438         escaped = true;
439       } else
440       {
441         if (!escaped && c == toFind)
442           return i;
443 
444         escaped = false;
445       }
446     }
447 
448     return -1;
449   }
450 
451   /**
452    * Remove any \'s from a string.
453    */
stripBackslashes(String text)454   public static String stripBackslashes (String text)
455     throws InvalidFormatException
456   {
457     if (text.indexOf ('\\') != -1)
458     {
459       StringBuilder buff = new StringBuilder (text.length ());
460 
461       for (int i = 0; i < text.length (); i++)
462       {
463         char c = text.charAt (i);
464 
465         if (c != '\\')
466         {
467           buff.append (c);
468         } else
469         {
470           i++;
471 
472           if (i < text.length ())
473             buff.append (text.charAt (i));
474           else
475             throw new InvalidFormatException ("Invalid trailing \\");
476         }
477       }
478 
479       text = buff.toString ();
480     }
481 
482     return text;
483   }
484 
485   /**
486    * Shortcut to execute split on any whitespace character.
487    */
split(String text)488   public static String [] split (String text)
489   {
490     return split (text, "\\s+");
491   }
492 
493   /**
494    * String.split ("") returns {""} rather than {} like you might
495    * expect: this returns empty array on "".
496    */
split(String text, String regex)497   public static String [] split (String text, String regex)
498   {
499     if (text.length () == 0)
500       return EMPTY_STRING_ARRAY;
501     else
502       return text.split (regex);
503   }
504 
505   /**
506    * Join a list of objects into a string.
507    *
508    * @param items The items to stringify.
509    *
510    * @return The stringified list.
511    */
join(Object [] items)512   public static String join (Object [] items)
513   {
514     return join (items, ", ");
515   }
516 
517   /**
518    * Join a list of objects into a string.
519    *
520    * @param items The items to stringify.
521    * @param separator The separator between items.
522    *
523    * @return The stringified list.
524    */
join(Object [] items, String separator)525   public static String join (Object [] items, String separator)
526   {
527     return join (asList (items), separator);
528   }
529 
530   /**
531    * Join a list of objects into a string.
532    *
533    * @param items The items to stringify.
534    * @param separator The separator between items.
535    *
536    * @return The stringified list.
537    */
join(List<?> items, String separator)538   public static String join (List<?> items, String separator)
539   {
540     StringBuilder str = new StringBuilder ();
541 
542     boolean first = true;
543 
544     for (Object item : items)
545     {
546       if (!first)
547         str.append (separator);
548 
549       first = false;
550 
551       str.append (item);
552     }
553 
554     return str.toString ();
555   }
556 
557   /**
558    * Generate human friendly string dump of a Map.
559    */
mapToString(Map<?, ?> map)560   public static String mapToString (Map<?, ?> map)
561   {
562     StringBuilder str = new StringBuilder ();
563     boolean first = true;
564 
565     for (Map.Entry<?, ?> entry : map.entrySet ())
566     {
567       if (!first)
568         str.append (", ");
569 
570       first = false;
571 
572       str.append ('{');
573       str.append (entry.getKey ()).append (" = ").append (entry.getValue ());
574       str.append ('}');
575     }
576 
577     return str.toString ();
578   }
579 
580   /**
581    * Expand C-like backslash codes such as \n \x90 etc into their
582    * literal values.
583    * @throws InvalidFormatException
584    */
expandBackslashes(String text)585   public static String expandBackslashes (String text)
586     throws InvalidFormatException
587   {
588     if (text.indexOf ('\\') != -1)
589     {
590       StringBuilder buff = new StringBuilder (text.length ());
591 
592       for (int i = 0; i < text.length (); i++)
593       {
594         char c = text.charAt (i);
595 
596         if (c == '\\')
597         {
598           c = text.charAt (++i);
599 
600           switch (c)
601           {
602             case 'n':
603               c = '\n'; break;
604             case 't':
605               c = '\t'; break;
606             case 'b':
607               c = '\b'; break;
608             case 'r':
609               c = '\r'; break;
610             case 'f':
611               c = '\f'; break;
612             case 'a':
613               c = 7; break;
614             case 'v':
615               c = 11; break;
616             case '0':
617             case '1':
618             case '2':
619             case '3':
620             case '4':
621             case '5':
622             case '6':
623             case '7':
624               int value = c - '0';
625               int end = Math.min (text.length (), i + 3);
626 
627               while (i + 1 < end && octDigit (text.charAt (i + 1)))
628               {
629                 c = text.charAt (++i);
630                 value = value * 8 + (c - '0');
631               }
632 
633               c = (char)value;
634               break;
635             case 'x':
636               value = 0;
637               end = Math.min (text.length (), i + 3);
638 
639               do
640               {
641                 c = text.charAt (++i);
642                 value = value * 16 + hexValue (c);
643               } while (i + 1 < end && hexDigit (text.charAt (i + 1)));
644 
645               c = (char)value;
646               break;
647           }
648         }
649 
650         buff.append (c);
651       }
652 
653       text = buff.toString ();
654     }
655 
656     return text;
657   }
658 
octDigit(char c)659   private static boolean octDigit (char c)
660   {
661     return c >= '0' && c <= '7';
662   }
663 
hexDigit(char c)664   private static boolean hexDigit (char c)
665   {
666     return (c >= '0' && c <= '9') ||
667            (c >= 'a' && c <= 'f') ||
668            (c >= 'A' && c <= 'F');
669   }
670 
hexValue(char c)671   private static int hexValue (char c)
672     throws InvalidFormatException
673   {
674     if (c >= '0' && c <= '9')
675       return c - '0';
676     else if (c >= 'a' && c <= 'f')
677       return c - 'a' + 10;
678     else if (c >= 'A' && c <= 'F')
679       return c - 'A' + 10;
680     else
681       throw new InvalidFormatException ("Not a valid hex character: " + c);
682   }
683 }
684