001package com.hfg.javascript; 002 003import java.io.*; 004import java.util.Collection; 005import java.util.HashSet; 006import java.util.Map; 007import java.util.Set; 008import java.util.regex.Matcher; 009import java.util.regex.Pattern; 010 011import com.hfg.util.collection.CollectionUtil; 012import com.hfg.util.collection.OrderedMap; 013import com.hfg.util.StringUtil; 014import com.hfg.xml.XMLException; 015 016//------------------------------------------------------------------------------ 017/** 018 A Javascript object (Map) container useful for creating JSON. 019 <div> 020 <a href='http://www.json.org/'>JSON.org</a> 021 </div> 022 <div> 023 JSON RFC: <a href='http://www.ietf.org/rfc/rfc4627.txt'>http://www.ietf.org/rfc/rfc4627.txt</a> 024 </div> 025 026 @author J. Alex Taylor, hairyfatguy.com 027 */ 028//------------------------------------------------------------------------------ 029// com.hfg Library 030// 031// This library is free software; you can redistribute it and/or 032// modify it under the terms of the GNU Lesser General Public 033// License as published by the Free Software Foundation; either 034// version 2.1 of the License, or (at your option) any later version. 035// 036// This library is distributed in the hope that it will be useful, 037// but WITHOUT ANY WARRANTY; without even the implied warranty of 038// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 039// Lesser General Public License for more details. 040// 041// You should have received a copy of the GNU Lesser General Public 042// License along with this library; if not, write to the Free Software 043// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 044// 045// J. Alex Taylor, President, Founder, CEO, COO, CFO, OOPS hairyfatguy.com 046// jataylor@hairyfatguy.com 047//------------------------------------------------------------------------------ 048 049public class JsObjMap extends OrderedMap<String, Object> implements JsCollection 050{ 051 private Set<String> mUnquotedValueKeys; 052 053 private static final Pattern KEY_PATTERN = Pattern.compile("(.+?)\\s*:"); 054 private static final Pattern UNQUOTED_VALUE_PATTERN = Pattern.compile("(.+?)\\s*[,}]"); 055 056 //########################################################################### 057 // CONSTRUCTORS 058 //########################################################################### 059 060 //-------------------------------------------------------------------------- 061 public JsObjMap() 062 { 063 064 } 065 066 //-------------------------------------------------------------------------- 067 public JsObjMap(int inInitialCapacity) 068 { 069 super(inInitialCapacity); 070 } 071 072 //-------------------------------------------------------------------------- 073 public JsObjMap(int inInitialCapacity, float inLoadFactor) 074 { 075 super(inInitialCapacity, inLoadFactor); 076 } 077 078 //-------------------------------------------------------------------------- 079 public JsObjMap(CharSequence inJSONString) 080 { 081 if (StringUtil.isSet(inJSONString)) 082 { 083 parse(inJSONString); 084 } 085 } 086 087 //-------------------------------------------------------------------------- 088 public JsObjMap(Map<String, Object> inMap) 089 { 090 if (CollectionUtil.hasValues(inMap)) 091 { 092 for (Entry<String, Object> entry : inMap.entrySet()) 093 { 094 put(entry.getKey(), entry.getValue()); 095 } 096 } 097 } 098 099 //########################################################################### 100 // PUBLIC METHODS 101 //########################################################################### 102 103 //-------------------------------------------------------------------------- 104 @Override 105 public String toString() 106 { 107 return toJavascript(); 108 } 109 110 //-------------------------------------------------------------------------- 111 public String toJSON() 112 { 113 ByteArrayOutputStream outStream; 114 try 115 { 116 outStream = new ByteArrayOutputStream(2048); 117 toJSON(outStream); 118 outStream.close(); 119 } 120 catch (Exception e) 121 { 122 throw new XMLException(e); 123 } 124 125 return outStream.toString(); 126 } 127 128 //--------------------------------------------------------------------------- 129 public void toJSON(OutputStream inStream) 130 { 131 PrintWriter writer = new PrintWriter(inStream); 132 133 toJSON(writer); 134 135 writer.flush(); 136 } 137 138 //-------------------------------------------------------------------------- 139 public void toJSON(Writer inWriter) 140 { 141 try 142 { 143 inWriter.write("{ "); 144 145 int i = 0; 146 for (String key : keySet()) 147 { 148 if (i > 0) 149 { 150 inWriter.write(", "); 151 } 152 153 inWriter.write("\""); 154 inWriter.write(JSONUtil.escapeString(key)); 155 inWriter.write("\": "); 156 157 if (mUnquotedValueKeys != null 158 && mUnquotedValueKeys.contains(key)) 159 { 160 inWriter.write(get(key).toString()); 161 } 162 else 163 { 164 inWriter.write(composeValueJavascript(get(key))); 165 } 166 167 i++; 168 } 169 170 inWriter.write(" }"); 171 } 172 catch (IOException e) 173 { 174 throw new RuntimeException(e); 175 } 176 } 177 178 179 //-------------------------------------------------------------------------- 180 /** 181 Produces a javascript serialization that will generally be the same as produced 182 by toJSON() except when values have been added via the putUnquoted() method. 183 */ 184 public String toJavascript() 185 { 186 ByteArrayOutputStream outStream; 187 try 188 { 189 outStream = new ByteArrayOutputStream(2048); 190 toJavascript(outStream); 191 outStream.close(); 192 } 193 catch (Exception e) 194 { 195 throw new XMLException(e); 196 } 197 198 return outStream.toString(); 199 } 200 201 //--------------------------------------------------------------------------- 202 /** 203 Produces a javascript serialization that will generally be the same as produced 204 by toJSON() except when values have been added via the putUnquoted() method. 205 */ 206 public void toJavascript(OutputStream inStream) 207 { 208 PrintWriter writer = new PrintWriter(inStream); 209 210 toJavascript(writer); 211 212 writer.flush(); 213 } 214 215 //-------------------------------------------------------------------------- 216 /** 217 Produces a javascript serialization that will generally be the same as produced 218 by toJSON() except when values have been added via the putUnquoted() method. 219 */ 220 public void toJavascript(Writer inWriter) 221 { 222 try 223 { 224 inWriter.write("{ "); 225 226 int i = 0; 227 for (String key : keySet()) 228 { 229 if (i > 0) 230 { 231 inWriter.write(", "); 232 } 233 234 inWriter.write("\""); 235 inWriter.write(JSONUtil.escapeString(key)); 236 inWriter.write("\": "); 237 238 239 if (mUnquotedValueKeys != null 240 && mUnquotedValueKeys.contains(key)) 241 { 242 inWriter.write(get(key).toString()); 243 } 244 else 245 { 246 inWriter.write(composeValueJavascript(get(key))); 247 } 248 i++; 249 } 250 251 inWriter.write(" }"); 252 } 253 catch (IOException e) 254 { 255 throw new RuntimeException(e); 256 } 257 } 258 259 //-------------------------------------------------------------------------- 260 public String getString(String inKey) 261 { 262 Object value = get(inKey); 263 return (value != null ? value.toString() : null); 264 } 265 266 //-------------------------------------------------------------------------- 267 /** 268 Puts the specified key / value pair into the map but does not quote the 269 value when calling toJavascript() [Note that not quoting a string value means 270 that the resulting serialization will no longer be valid JSON, but sending 271 function definitions in a JSON-like format can be useful when working with 272 javascript frameworks]. Numeric and boolean values should not be passed to 273 this method as they will be handle correctly by the standard put() method. 274 */ 275 public void putUnquoted(String inKey, Object inValue) 276 { 277 put(inKey, inValue); 278 279 if (null == mUnquotedValueKeys) 280 { 281 mUnquotedValueKeys = new HashSet<>(5); 282 } 283 284 mUnquotedValueKeys.add(inKey); 285 } 286 287 //-------------------------------------------------------------------------- 288 // If the values comes in too generically, we may need to dispatch to one of our specific put() methods. 289 public Object put(String inKey, Object inValue) 290 { 291 if (null == inKey) 292 { 293 throw new RuntimeException("A non-null value must be specified as the key!"); 294 } 295 296 Object result; 297 if (inValue != null) 298 { 299 if (inValue instanceof JsArray) 300 { 301 result = put(inKey, (JsArray) inValue); 302 } 303 else if (inValue instanceof Collection) 304 { 305 result = put(inKey, (Collection) inValue); 306 } 307 else if (inValue instanceof Object[]) 308 { 309 result = put(inKey, (Object[]) inValue); 310 } 311 else 312 { 313 result = super.put(inKey, inValue); 314 } 315 } 316 else 317 { 318 result = super.put(inKey, inValue); 319 } 320 321 return result; 322 } 323 324 //-------------------------------------------------------------------------- 325 public Object put(String inKey, JsArray inValue) 326 { 327 if (null == inKey) 328 { 329 throw new RuntimeException("A non-null value must be specified as the key!"); 330 } 331 332 return super.put(inKey, inValue); 333 } 334 335 //-------------------------------------------------------------------------- 336 public Object put(String inKey, Collection inValues) 337 { 338 if (null == inKey) 339 { 340 throw new RuntimeException("A non-null value must be specified as the key!"); 341 } 342 343 return super.put(inKey, inValues != null ? new JsArray(inValues) : null); 344 } 345 346 //-------------------------------------------------------------------------- 347 public Object put(String inKey, Object[] inValues) 348 { 349 if (null == inKey) 350 { 351 throw new RuntimeException("A non-null value must be specified as the key!"); 352 } 353 354 return super.put(inKey, inValues != null ? new JsArray(inValues) : null); 355 } 356 357 //-------------------------------------------------------------------------- 358 @Override 359 public Object remove(Object inKey) 360 { 361 if (mUnquotedValueKeys != null) 362 { 363 mUnquotedValueKeys.remove(inKey); 364 } 365 366 return super.remove(inKey); 367 } 368 369 //########################################################################### 370 // PRIVATE METHODS 371 //########################################################################### 372 373 //-------------------------------------------------------------------------- 374 private String composeValueJSON(Object inValue) 375 { 376 String value = "null"; 377 378 if (inValue != null) 379 { 380 if (inValue instanceof Number 381 || inValue instanceof Boolean) 382 { 383 // Numeric and boolean values are written without quotes 384 value = inValue.toString(); 385 } 386 else if (inValue instanceof JsArray) 387 { 388 value = ((JsArray) inValue).toJSON(); 389 } 390 else if (inValue instanceof JsObjMap) 391 { 392 value = ((JsObjMap) inValue).toJSON(); 393 } 394 else 395 { 396 // String values should be enclosed in double quotes 397 value = "\"" + JSONUtil.escapeString(inValue.toString()) + "\""; 398 } 399 } 400 401 return value; 402 } 403 404 //-------------------------------------------------------------------------- 405 private String composeValueJavascript(Object inValue) 406 { 407 String value = "null"; 408 409 if (inValue != null) 410 { 411 if (inValue instanceof Number 412 || inValue instanceof Boolean) 413 { 414 value = inValue.toString(); 415 } 416 else if (inValue instanceof JsArray) 417 { 418 value = ((JsArray) inValue).toJavascript(); 419 } 420 else if (inValue instanceof JsObjMap) 421 { 422 value = ((JsObjMap) inValue).toJavascript(); 423 } 424 else 425 { 426 value = "\"" + JSONUtil.escapeString(inValue.toString()) + "\""; 427 } 428 } 429 430 return value; 431 } 432 433 //-------------------------------------------------------------------------- 434 private void parse(CharSequence inString) 435 { 436 String str = inString.toString().trim(); 437 if (str.charAt(0) != '{' 438 || str.charAt(str.length() - 1) != '}') 439 { 440 throw new RuntimeException("JSON map must be enclosed with {}!"); 441 } 442 443 int index = 1; 444 445 String key = null; 446 while (index < str.length() - 1) 447 { 448 char theChar = str.charAt(index); 449 if (Character.isWhitespace(theChar)) 450 { 451 index++; 452 } 453 else if (theChar == ',') 454 { 455 index++; 456 } 457 else if (theChar == '[') 458 { 459 // Find the ending bracket 460 if (null == key) 461 { 462 throw new RuntimeException("Poorly structured JSON! Array at index " + index + " in map does not have a key!"); 463 } 464 465 int depth = 1; 466 char quote = ' '; 467 boolean inEscape = false; 468 boolean inValue = false; 469 int end = index + 1; 470 while (end < str.length() -1 471 && (depth != 1 472 || inValue 473 || str.charAt(end) != ']')) 474 { 475 char theEndChar = str.charAt(end); 476 if (theEndChar == '\\' 477 && inValue) 478 { 479 inEscape = ! inEscape; 480 } 481 else if (inEscape) 482 { 483 inEscape = false; 484 } 485 486 if (inEscape) 487 { 488 continue; 489 } 490 491 if (theEndChar == '\'' 492 && (str.charAt(end -1) != '\\' 493 || ! inEscape)) 494 { 495 if (! inValue) 496 { 497 inValue = true; 498 quote = '\''; 499 } 500 else if (quote == '\'') 501 { 502 inValue = false; 503 } 504 } 505 else if (theEndChar == '"' 506 && (str.charAt(end -1) != '\\' 507 || ! inEscape)) 508 { 509 if (! inValue) 510 { 511 inValue = true; 512 quote = '"'; 513 } 514 else if (quote == '"') 515 { 516 inValue = false; 517 } 518 } 519 else if (! inValue 520 && theEndChar == '[') 521 { 522 depth++; 523 } 524 else if (! inValue 525 && theEndChar == ']') 526 { 527 depth--; 528 } 529 530 end++; 531 } 532 533 JsArray value = new JsArray(str.substring(index, end + 1)); 534 put(key, value); 535 key = null; 536 index = end + 1; 537 } 538 else if (theChar == '{') 539 { 540 if (null == key) 541 { 542 throw new RuntimeException("Poorly structured JSON! Submap at index " + index + " in map does not have a key!"); 543 } 544 545 // Find the ending brace 546 int depth = 1; 547 char quote = ' '; 548 boolean inEscape = false; 549 boolean inValue = false; 550 int end = index + 1; 551 while (end < str.length() -1 552 && (depth != 1 553 || inValue 554 || str.charAt(end) != '}')) 555 { 556 char theEndChar = str.charAt(end); 557 if (theEndChar == '\\' 558 && inValue) 559 { 560 inEscape = ! inEscape; 561 } 562 else if (inEscape) 563 { 564 inEscape = false; 565 } 566 567 if (inEscape) 568 { 569 continue; 570 } 571 572 if (theEndChar == '\'' 573 && (str.charAt(end -1) != '\\' 574 || ! inEscape)) 575 { 576 if (! inValue) 577 { 578 inValue = true; 579 quote = '\''; 580 } 581 else if (quote == '\'') 582 { 583 inValue = false; 584 } 585 } 586 else if (theEndChar == '"' 587 && (str.charAt(end -1) != '\\' 588 || ! inEscape)) 589 { 590 if (! inValue) 591 { 592 inValue = true; 593 quote = '"'; 594 } 595 else if (quote == '"') 596 { 597 inValue = false; 598 } 599 } 600 else if (! inValue 601 && theEndChar == '{') 602 { 603 depth++; 604 } 605 else if (! inValue 606 && theEndChar == '}') 607 { 608 depth--; 609 } 610 611 end++; 612 } 613 614 JsObjMap value = new JsObjMap(str.substring(index, end + 1)); 615 put(key, value); 616 key = null; 617 index = end + 1; 618 } 619 else if (key != null) 620 { 621 if (theChar == '"') 622 { 623 boolean inEscape = false; 624 int valueStartIndex = index; 625 StringBuilder valueBuffer = new StringBuilder(); 626 // Continue until we find the next unescaped quote. 627 while ((index++) < str.length() - 1 628 && ((theChar = str.charAt(index)) != '"') || inEscape) 629 { 630 if (theChar == '\\') 631 { 632 inEscape = !inEscape; 633 } 634 else if (inEscape) 635 { 636 inEscape = false; 637 } 638 639// if (! inEscape) 640 { 641 valueBuffer.append(theChar); 642 } 643 } 644 645 if (index == str.length() - 1) 646 { 647 throw new RuntimeException("Problem parsing value @ position " + valueStartIndex + " in JSON string!"); 648 } 649 650 index++; // consume the trailing quote 651 put(key, JSONUtil.unescapeString(valueBuffer.toString())); 652// put(key, valueBuffer.toString()); 653 } 654 else 655 { 656 Matcher m = UNQUOTED_VALUE_PATTERN.matcher(str); 657 if (m.find(index)) 658 { 659 String stringValue = JSONUtil.unescapeString(m.group(1)); 660 index = m.end() - 1; 661 662 put(key, JSONUtil.convertStringValueToObject(stringValue)); 663 } 664 } 665 666 key = null; 667 } 668 else 669 { 670 // Parse the key name 671 672 // Is it in quotes? 673 if (theChar == '"') 674 { 675 boolean inEscape = false; 676 int valueStartIndex = index; 677 StringBuilder valueBuffer = new StringBuilder(); 678 // Continue until we find the next unescaped quote. 679 while ((index++) < str.length() - 1 680 && ((theChar = str.charAt(index)) != '"') || inEscape) 681 { 682 if (theChar == '\\') 683 { 684 inEscape = !inEscape; 685 } 686 else if (inEscape) 687 { 688 inEscape = false; 689 } 690 691// if (! inEscape) 692 { 693 valueBuffer.append(theChar); 694 } 695 } 696 697 if (index == str.length() - 1) 698 { 699 throw new RuntimeException("Problem parsing value @ position " + valueStartIndex + " in JSON string!"); 700 } 701 702 index++; // consume the trailing quote 703 key = JSONUtil.unescapeString(valueBuffer.toString()); 704 705 while (index < str.length() - 1 706 && str.charAt(index++) != ':') 707 { 708 } 709 710 if (str.charAt(index - 1) != ':') 711 { 712 throw new RuntimeException("Problem with format of hash string: " + StringUtil.singleQuote(inString) + "!"); 713 } 714 } 715 else 716 { 717 Matcher m = KEY_PATTERN.matcher(str); 718 if (m.find(index)) 719 { 720 key = JSONUtil.unescapeString(StringUtil.unquote(m.group(1))); 721// key = StringUtil.unquote(m.group(1)); 722 index = m.end(); 723 } 724 else 725 { 726 throw new RuntimeException("Problem with format of hash string: " + StringUtil.singleQuote(inString) + "!"); 727 } 728 } 729 } 730 } 731 } 732} 733