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