1 // SgfReader.java 2 3 package net.sf.gogui.sgf; 4 5 import java.io.BufferedReader; 6 import java.io.File; 7 import java.io.FileInputStream; 8 import java.io.FileNotFoundException; 9 import java.io.InputStream; 10 import java.io.InputStreamReader; 11 import java.io.IOException; 12 import java.io.StreamTokenizer; 13 import java.io.UnsupportedEncodingException; 14 import java.nio.charset.Charset; 15 import java.util.Map; 16 import java.util.TreeMap; 17 import java.util.TreeSet; 18 import java.util.ArrayList; 19 import java.util.Locale; 20 import java.util.Set; 21 import net.sf.gogui.game.GameInfo; 22 import net.sf.gogui.game.GameTree; 23 import net.sf.gogui.game.MarkType; 24 import net.sf.gogui.game.Node; 25 import net.sf.gogui.game.StringInfo; 26 import net.sf.gogui.game.StringInfoColor; 27 import net.sf.gogui.game.TimeSettings; 28 import net.sf.gogui.go.GoColor; 29 import static net.sf.gogui.go.GoColor.BLACK; 30 import static net.sf.gogui.go.GoColor.WHITE; 31 import static net.sf.gogui.go.GoColor.EMPTY; 32 import net.sf.gogui.go.GoPoint; 33 import net.sf.gogui.go.InvalidKomiException; 34 import net.sf.gogui.go.InvalidPointException; 35 import net.sf.gogui.go.Komi; 36 import net.sf.gogui.go.Move; 37 import net.sf.gogui.go.PointList; 38 import net.sf.gogui.util.ByteCountInputStream; 39 import net.sf.gogui.util.ProgressShow; 40 41 /** SGF reader. 42 @bug The error messages currently don't contain line numbers, see 43 implementation of getError(). */ 44 public final class SgfReader 45 { 46 /** Read SGF file from stream. 47 Default charset is ISO-8859-1 according to the SGF version 4 standard. 48 The charset property is only respected if the stream is a 49 FileInputStream, because it has to be reopened with a different 50 encoding. 51 The stream is closed after reading. 52 @param in Stream to read from. 53 @param file File name if input stream is a FileInputStream to allow 54 reopening the stream after a charset change 55 @param progressShow Callback to show progress, can be null 56 @param size Size of stream if progressShow != null 57 @throws SgfError If reading fails. */ SgfReader(InputStream in, File file, ProgressShow progressShow, long size)58 public SgfReader(InputStream in, File file, ProgressShow progressShow, 59 long size) 60 throws SgfError 61 { 62 m_file = file; 63 m_progressShow = progressShow; 64 m_size = size; 65 m_isFile = (in instanceof FileInputStream && file != null); 66 if (progressShow != null) 67 progressShow.showProgress(0); 68 try 69 { 70 // SGF FF 4 standard defines ISO-8859-1 as default 71 readSgf(in, "ISO-8859-1"); 72 } 73 catch (SgfCharsetChanged e1) 74 { 75 try 76 { 77 in.close(); 78 in = new FileInputStream(file); 79 } 80 catch (IOException e2) 81 { 82 throw new SgfError("Could not reset SGF stream after" 83 + " charset change."); 84 } 85 try 86 { 87 readSgf(in, m_newCharset); 88 } 89 catch (SgfCharsetChanged e3) 90 { 91 assert false; 92 } 93 } 94 finally 95 { 96 try 97 { 98 in.close(); 99 } 100 catch (IOException e) 101 { 102 System.err.println("Could not close SGF stream"); 103 } 104 } 105 } 106 107 /** Get game tree of loaded SGF file. 108 @return The game tree. */ getTree()109 public GameTree getTree() 110 { 111 return m_tree; 112 } 113 114 /** Get warnings that occurred during loading SGF file. 115 @return String with warning messages or null if no warnings. */ getWarnings()116 public String getWarnings() 117 { 118 if (m_warnings.isEmpty()) 119 return null; 120 StringBuilder result = new StringBuilder(m_warnings.size() * 80); 121 for (String s : m_warnings) 122 { 123 result.append(s); 124 result.append('\n'); 125 } 126 return result.toString(); 127 } 128 129 private static class SgfCharsetChanged 130 extends Exception 131 { 132 } 133 134 private final boolean m_isFile; 135 136 /** Has current node inconsistent FF3 overtime settings properties. */ 137 private boolean m_ignoreOvertime; 138 139 private int m_lastPercent; 140 141 private int m_boardSize; 142 143 private int m_byoyomiMoves; 144 145 private final long m_size; 146 147 private long m_byoyomi; 148 149 private long m_preByoyomi; 150 151 private ByteCountInputStream m_byteCountInputStream; 152 153 private java.io.Reader m_reader; 154 155 private GameTree m_tree; 156 157 private final ProgressShow m_progressShow; 158 159 /** Contains strings with warnings. */ 160 private final Set<String> m_warnings = new TreeSet<String>(); 161 162 private StreamTokenizer m_tokenizer; 163 164 private final File m_file; 165 166 private String m_newCharset; 167 168 /** Pre-allocated temporary buffer for use within functions. */ 169 private final StringBuilder m_buffer = new StringBuilder(512); 170 171 private final PointList m_pointList = new PointList(); 172 173 /** Map containing the properties of the current node. */ 174 private final Map<String,ArrayList<String>> m_props = 175 new TreeMap<String,ArrayList<String>>(); 176 177 /** Apply some fixes for broken SGF files. */ applyFixes()178 private void applyFixes() 179 { 180 Node root = m_tree.getRoot(); 181 GameInfo info = m_tree.getGameInfo(root); 182 if (root.hasSetup() && root.getPlayer() == null) 183 { 184 if (info.getHandicap() > 0) 185 { 186 root.setPlayer(WHITE); 187 } 188 else 189 { 190 boolean hasBlackChildMoves = false; 191 boolean hasWhiteChildMoves = false; 192 for (int i = 0; i < root.getNumberChildren(); ++i) 193 { 194 Move move = root.getChild(i).getMove(); 195 if (move == null) 196 continue; 197 if (move.getColor() == BLACK) 198 hasBlackChildMoves = true; 199 if (move.getColor() == WHITE) 200 hasWhiteChildMoves = true; 201 } 202 if (hasBlackChildMoves && ! hasWhiteChildMoves) 203 root.setPlayer(BLACK); 204 if (hasWhiteChildMoves && ! hasBlackChildMoves) 205 root.setPlayer(WHITE); 206 } 207 } 208 } 209 checkEndOfFile()210 private void checkEndOfFile() throws SgfError, IOException 211 { 212 while (true) 213 { 214 m_tokenizer.nextToken(); 215 int t = m_tokenizer.ttype; 216 if (t == '(') 217 throw getError("Multiple SGF trees not supported"); 218 else if (t == StreamTokenizer.TT_EOF) 219 return; 220 else if (t != ' ' && t != '\t' && t != '\n' && t != '\r') 221 { 222 setWarning("Extra text after SGF tree"); 223 return; 224 } 225 } 226 } 227 228 /** Check for obsolete long names for standard properties. 229 These are still used in some old SGF files. 230 @param property Property name 231 @return Short standard version of the property or original property */ checkForObsoleteLongProps(String property)232 private String checkForObsoleteLongProps(String property) 233 { 234 if (property.length() <= 2) 235 return property; 236 property = property.intern(); 237 String shortName = null; 238 if (property == "ADDBLACK") 239 shortName = "AB"; 240 else if (property == "ADDEMPTY") 241 shortName = "AE"; 242 else if (property == "ADDWHITE") 243 shortName = "AW"; 244 else if (property == "BLACK") 245 shortName = "B"; 246 else if (property == "BLACKRANK") 247 shortName = "BR"; 248 else if (property == "COMMENT") 249 shortName = "C"; 250 else if (property == "COPYRIGHT") 251 shortName = "CP"; 252 else if (property == "DATE") 253 shortName = "DT"; 254 else if (property == "EVENT") 255 shortName = "EV"; 256 else if (property == "GAME") 257 shortName = "GM"; 258 else if (property == "HANDICAP") 259 shortName = "HA"; 260 else if (property == "KOMI") 261 shortName = "KM"; 262 else if (property == "PLACE") 263 shortName = "PC"; 264 else if (property == "PLAYERBLACK") 265 shortName = "PB"; 266 else if (property == "PLAYERWHITE") 267 shortName = "PW"; 268 else if (property == "PLAYER") 269 shortName = "PL"; 270 else if (property == "RESULT") 271 shortName = "RE"; 272 else if (property == "ROUND") 273 shortName = "RO"; 274 else if (property == "RULES") 275 shortName = "RU"; 276 else if (property == "SIZE") 277 shortName = "SZ"; 278 else if (property == "WHITE") 279 shortName = "W"; 280 else if (property == "WHITERANK") 281 shortName = "WR"; 282 if (shortName != null) 283 return shortName; 284 return property; 285 } 286 createGameInfo(Node node)287 private GameInfo createGameInfo(Node node) 288 { 289 return node.createGameInfo(); 290 } 291 findRoot()292 private void findRoot() throws SgfError, IOException 293 { 294 while (true) 295 { 296 m_tokenizer.nextToken(); 297 int t = m_tokenizer.ttype; 298 if (t == '(') 299 { 300 // Better make sure that ( is followed by a node 301 m_tokenizer.nextToken(); 302 t = m_tokenizer.ttype; 303 if (t == ';') 304 { 305 m_tokenizer.pushBack(); 306 return; 307 } 308 else 309 setWarning("Extra text before SGF tree"); 310 } 311 else if (t == StreamTokenizer.TT_EOF) 312 throw getError("No root tree found"); 313 else 314 setWarning("Extra text before SGF tree"); 315 } 316 } 317 getBoardSize()318 private int getBoardSize() 319 { 320 if (m_boardSize == -1) 321 m_boardSize = 19; // Default size for Go in the SGF standard 322 return m_boardSize; 323 } 324 getError(String message)325 private SgfError getError(String message) 326 { 327 // Line numbers in error messages is currently disabled because 328 // StreamTokenizer.lineno() does not work as expected with Windows-style 329 // line endings (it seems to count CRLF as two line endings; last with 330 // Java 1.7). 331 /* 332 int lineNumber = m_tokenizer.lineno(); 333 if (m_file == null) 334 return new SgfError(lineNumber + ": " + message); 335 else 336 { 337 String s = m_file.getName() + ":" + lineNumber + ": " + message; 338 return new SgfError(s); 339 } 340 */ 341 if (m_file == null) 342 return new SgfError(message); 343 else 344 return new SgfError(m_file.getName() + ": " + message); 345 } 346 handleProps(Node node, boolean isRoot)347 private void handleProps(Node node, boolean isRoot) 348 throws IOException, SgfError, SgfCharsetChanged 349 { 350 // Handle SZ property first to be able to parse points 351 if (m_props.containsKey("SZ")) 352 { 353 ArrayList<String> values = m_props.get("SZ"); 354 m_props.remove("SZ"); 355 if (! isRoot) 356 setWarning("Size property not in root node ignored"); 357 else 358 { 359 try 360 { 361 int size = parseInt(values.get(0)); 362 if (size <= 0 || size > GoPoint.MAX_SIZE) 363 setWarning("Invalid board size value"); 364 assert m_boardSize == -1; 365 m_boardSize = size; 366 } 367 catch (NumberFormatException e) 368 { 369 setWarning("Invalid board size value"); 370 } 371 } 372 } 373 for (Map.Entry<String,ArrayList<String>> entry : m_props.entrySet()) 374 { 375 String p = entry.getKey(); 376 ArrayList<String> values = entry.getValue(); 377 String v = values.get(0); 378 if (p == "AB") 379 { 380 parsePointList(values); 381 node.addStones(BLACK, m_pointList); 382 } 383 else if (p == "AE") 384 { 385 parsePointList(values); 386 node.addStones(EMPTY, m_pointList); 387 } 388 else if (p == "AN") 389 set(node, StringInfo.ANNOTATION, v); 390 else if (p == "AW") 391 { 392 parsePointList(values); 393 node.addStones(WHITE, m_pointList); 394 } 395 else if (p == "B") 396 { 397 node.setMove(Move.get(BLACK, parsePoint(v))); 398 } 399 else if (p == "BL") 400 { 401 try 402 { 403 node.setTimeLeft(BLACK, Double.parseDouble(v)); 404 } 405 catch (NumberFormatException e) 406 { 407 } 408 } 409 else if (p == "BR") 410 set(node, StringInfoColor.RANK, BLACK, v); 411 else if (p == "BT") 412 set(node, StringInfoColor.TEAM, BLACK, v); 413 else if (p == "C") 414 node.setComment(v); 415 else if (p == "CA") 416 { 417 if (isRoot && m_isFile && m_newCharset == null) 418 { 419 m_newCharset = v.trim(); 420 if (Charset.isSupported(m_newCharset)) 421 throw new SgfCharsetChanged(); 422 else 423 setWarning("Unknown character set \"" + m_newCharset 424 + "\""); 425 } 426 } 427 else if (p == "CP") 428 set(node, StringInfo.COPYRIGHT, v); 429 else if (p == "CR") 430 parseMarked(node, MarkType.CIRCLE, values); 431 else if (p == "DT") 432 set(node, StringInfo.DATE, v); 433 else if (p == "FF") 434 { 435 int format = -1; 436 try 437 { 438 format = Integer.parseInt(v); 439 } 440 catch (NumberFormatException e) 441 { 442 } 443 if (format < 1 || format > 4) 444 setWarning("Unknown SGF file format version"); 445 } 446 else if (p == "GM") 447 { 448 // Some SGF files contain GM[], interpret as GM[1] 449 v = v.trim(); 450 if (! v.equals("") && ! v.equals("1")) 451 throw getError("Not a Go game"); 452 } 453 else if (p == "HA") 454 { 455 // Some SGF files contain HA[], interpret as unknown handicap 456 v = v.trim(); 457 if (! v.equals("")) 458 { 459 try 460 { 461 int handicap = Integer.parseInt(v); 462 if (handicap == 1 || handicap < 0) 463 setWarning("Invalid handicap value"); 464 else 465 createGameInfo(node).setHandicap(handicap); 466 } 467 catch (NumberFormatException e) 468 { 469 setWarning("Invalid handicap value"); 470 } 471 } 472 } 473 else if (p == "KM") 474 parseKomi(node, v); 475 else if (p == "LB") 476 { 477 for (int i = 0; i < values.size(); ++i) 478 { 479 String value = values.get(i); 480 int pos = value.indexOf(':'); 481 if (pos > 0) 482 { 483 GoPoint point = parsePoint(value.substring(0, pos)); 484 String text = value.substring(pos + 1); 485 node.setLabel(point, text); 486 } 487 } 488 } 489 else if (p == "MA" || p == "M") 490 parseMarked(node, MarkType.MARK, values); 491 else if (p == "OB") 492 { 493 try 494 { 495 node.setMovesLeft(BLACK, Integer.parseInt(v)); 496 } 497 catch (NumberFormatException e) 498 { 499 } 500 } 501 else if (p == "OM") 502 parseOvertimeMoves(v); 503 else if (p == "OP") 504 parseOvertimePeriod(v); 505 else if (p == "OT") 506 parseOvertime(node, v); 507 else if (p == "OW") 508 { 509 try 510 { 511 node.setMovesLeft(WHITE, Integer.parseInt(v)); 512 } 513 catch (NumberFormatException e) 514 { 515 } 516 } 517 else if (p == "PB") 518 set(node, StringInfoColor.NAME, BLACK, v); 519 else if (p == "PW") 520 set(node, StringInfoColor.NAME, WHITE, v); 521 else if (p == "PL") 522 node.setPlayer(parseColor(v)); 523 else if (p == "RE") 524 set(node, StringInfo.RESULT, v); 525 else if (p == "RO") 526 set(node, StringInfo.ROUND, v); 527 else if (p == "RU") 528 set(node, StringInfo.RULES, v); 529 else if (p == "SO") 530 set(node, StringInfo.SOURCE, v); 531 else if (p == "SQ") 532 parseMarked(node, MarkType.SQUARE, values); 533 else if (p == "SL") 534 parseMarked(node, MarkType.SELECT, values); 535 else if (p == "TB") 536 parseMarked(node, MarkType.TERRITORY_BLACK, values); 537 else if (p == "TM") 538 parseTime(node, v); 539 else if (p == "TR") 540 parseMarked(node, MarkType.TRIANGLE, values); 541 else if (p == "US") 542 set(node, StringInfo.USER, v); 543 else if (p == "W") 544 node.setMove(Move.get(WHITE, parsePoint(v))); 545 else if (p == "TW") 546 parseMarked(node, MarkType.TERRITORY_WHITE, values); 547 else if (p == "V") 548 { 549 try 550 { 551 node.setValue(Float.parseFloat(v)); 552 } 553 catch (NumberFormatException e) 554 { 555 } 556 } 557 else if (p == "WL") 558 { 559 try 560 { 561 node.setTimeLeft(WHITE, Double.parseDouble(v)); 562 } 563 catch (NumberFormatException e) 564 { 565 } 566 } 567 else if (p == "WR") 568 set(node, StringInfoColor.RANK, WHITE, v); 569 else if (p == "WT") 570 set(node, StringInfoColor.TEAM, WHITE, v); 571 else if (p != "FF" && p != "GN" && p != "AP") 572 node.addSgfProperty(p, values); 573 } 574 } 575 parseColor(String s)576 private GoColor parseColor(String s) throws SgfError 577 { 578 GoColor color; 579 s = s.trim().toLowerCase(Locale.ENGLISH); 580 if (s.equals("b") || s.equals("1")) 581 color = BLACK; 582 else if (s.equals("w") || s.equals("2")) 583 color = WHITE; 584 else 585 throw getError("Invalid color value"); 586 return color; 587 } 588 parseInt(String s)589 private int parseInt(String s) throws SgfError 590 { 591 int i = -1; 592 try 593 { 594 i = Integer.parseInt(s.trim()); 595 } 596 catch (NumberFormatException e) 597 { 598 throw getError("Number expected"); 599 } 600 return i; 601 } 602 parseKomi(Node node, String value)603 private void parseKomi(Node node, String value) throws SgfError 604 { 605 try 606 { 607 Komi komi = Komi.parseKomi(value); 608 createGameInfo(node).setKomi(komi); 609 if (komi != null && ! komi.isMultipleOf(0.5)) 610 setWarning("Komi is not a multiple of 0.5"); 611 } 612 catch (InvalidKomiException e) 613 { 614 setWarning("Invalid value for komi"); 615 } 616 } 617 parseMarked(Node node, MarkType type, ArrayList<String> values)618 private void parseMarked(Node node, MarkType type, 619 ArrayList<String> values) 620 throws SgfError 621 { 622 parsePointList(values); 623 for (GoPoint p : m_pointList) 624 node.addMarked(p, type); 625 } 626 parseOvertime(Node node, String value)627 private void parseOvertime(Node node, String value) 628 { 629 SgfUtil.Overtime overtime = SgfUtil.parseOvertime(value); 630 if (overtime == null) 631 // Preserve information 632 node.addSgfProperty("OT", value); 633 else 634 { 635 m_byoyomi = overtime.m_byoyomi; 636 m_byoyomiMoves = overtime.m_byoyomiMoves; 637 } 638 } 639 640 /** FF3 OM property */ parseOvertimeMoves(String value)641 private void parseOvertimeMoves(String value) 642 { 643 try 644 { 645 m_byoyomiMoves = Integer.parseInt(value); 646 } 647 catch (NumberFormatException e) 648 { 649 setWarning("Invalid value for byoyomi moves"); 650 m_ignoreOvertime = true; 651 } 652 } 653 654 /** FF3 OP property */ parseOvertimePeriod(String value)655 private void parseOvertimePeriod(String value) 656 { 657 try 658 { 659 m_byoyomi = (long)(Double.parseDouble(value) * 1000); 660 } 661 catch (NumberFormatException e) 662 { 663 setWarning("Invalid value for byoyomi time"); 664 m_ignoreOvertime = true; 665 } 666 } 667 668 /** Parse point value. 669 @return Point or null, if pass move 670 @throw SgfError On invalid value */ parsePoint(String s)671 private GoPoint parsePoint(String s) throws SgfError 672 { 673 s = s.trim().toLowerCase(Locale.ENGLISH); 674 if (s.equals("")) 675 return null; 676 if (s.length() > 2 677 || (s.length() == 2 && (s.charAt(1) < 'a' || s.charAt(1) > 'z'))) 678 { 679 // Try human-readable encoding as used by SmartGo 680 try 681 { 682 return GoPoint.parsePoint(s, GoPoint.MAX_SIZE); 683 } 684 catch (InvalidPointException e) 685 { 686 throwInvalidCoordinates(s); 687 } 688 } 689 else if (s.length() != 2) 690 throwInvalidCoordinates(s); 691 int boardSize = getBoardSize(); 692 if (s.equals("tt") && boardSize <= 19) 693 return null; 694 int x = s.charAt(0) - 'a'; 695 int y = boardSize - (s.charAt(1) - 'a') - 1; 696 if (x < 0 || x >= boardSize || y < 0 || y >= boardSize) 697 { 698 if (x == boardSize && y == -1) 699 { 700 // Some programs encode pass moves, e.g. as jj for boardsize 9 701 setWarning("Non-standard pass move encoding"); 702 return null; 703 } 704 throw getError("Coordinates \"" + s + "\" outside board size " 705 + boardSize); 706 } 707 return GoPoint.get(x, y); 708 } 709 parsePointList(ArrayList<String> values)710 private void parsePointList(ArrayList<String> values) throws SgfError 711 { 712 m_pointList.clear(); 713 for (int i = 0; i < values.size(); ++i) 714 { 715 String value = values.get(i); 716 int pos = value.indexOf(':'); 717 if (pos < 0) 718 { 719 GoPoint point = parsePoint(value); 720 if (point == null) 721 setWarning("Point list argument contains PASS"); 722 else 723 m_pointList.add(point); 724 } 725 else 726 { 727 GoPoint point1 = parsePoint(value.substring(0, pos)); 728 GoPoint point2 = parsePoint(value.substring(pos + 1)); 729 if (point1 == null || point2 == null) 730 { 731 setWarning("Compressed point list contains PASS"); 732 continue; 733 } 734 int xMin = Math.min(point1.getX(), point2.getX()); 735 int xMax = Math.max(point1.getX(), point2.getX()); 736 int yMin = Math.min(point1.getY(), point2.getY()); 737 int yMax = Math.max(point1.getY(), point2.getY()); 738 for (int x = xMin; x <= xMax; ++x) 739 for (int y = yMin; y <= yMax; ++y) 740 m_pointList.add(GoPoint.get(x, y)); 741 } 742 } 743 } 744 745 /** TM property. 746 According to FF4, TM needs to be a real value, but older SGF versions 747 allow a string with unspecified content. We try to parse a few known 748 formats. */ parseTime(Node node, String value)749 private void parseTime(Node node, String value) 750 { 751 value = value.trim(); 752 if (value.equals("") || value.equals("-")) 753 return; 754 long preByoyomi = SgfUtil.parseTime(value); 755 if (preByoyomi < 0) 756 { 757 setWarning("Unknown format in time property"); 758 node.addSgfProperty("TM", value); // Preserve information 759 } 760 else 761 m_preByoyomi = preByoyomi; 762 } 763 readNext(Node father, boolean isRoot)764 private Node readNext(Node father, boolean isRoot) 765 throws IOException, SgfError, SgfCharsetChanged 766 { 767 if (m_progressShow != null) 768 { 769 int percent; 770 if (m_size > 0) 771 { 772 long count = m_byteCountInputStream.getCount(); 773 percent = (int)(count * 100 / m_size); 774 } 775 else 776 percent = 100; 777 if (percent != m_lastPercent) 778 m_progressShow.showProgress(percent); 779 m_lastPercent = percent; 780 } 781 m_tokenizer.nextToken(); 782 int ttype = m_tokenizer.ttype; 783 if (ttype == '(') 784 { 785 Node node = father; 786 while (node != null) 787 node = readNext(node, false); 788 return father; 789 } 790 if (ttype == ')') 791 return null; 792 if (ttype == StreamTokenizer.TT_EOF) 793 { 794 setWarning("Game tree not closed"); 795 return null; 796 } 797 if (ttype != ';') 798 throw getError("Next node expected"); 799 Node son = new Node(); 800 if (father != null) 801 father.append(son); 802 m_ignoreOvertime = false; 803 m_byoyomiMoves = -1; 804 m_byoyomi = -1; 805 m_preByoyomi = -1; 806 m_props.clear(); 807 while (readProp()); 808 handleProps(son, isRoot); 809 setTimeSettings(son); 810 return son; 811 } 812 readProp()813 private boolean readProp() throws IOException, SgfError 814 { 815 m_tokenizer.nextToken(); 816 int ttype = m_tokenizer.ttype; 817 if (ttype == StreamTokenizer.TT_WORD) 818 { 819 // Use intern() to allow fast comparsion with == 820 String p = m_tokenizer.sval.toUpperCase(Locale.ENGLISH).intern(); 821 ArrayList<String> values = new ArrayList<String>(); 822 String s; 823 while ((s = readValue()) != null) 824 values.add(s); 825 if (values.isEmpty()) 826 { 827 setWarning("Property \"" + p + "\" has no value"); 828 return true; 829 } 830 p = checkForObsoleteLongProps(p); 831 if (m_props.containsKey(p)) 832 // Silently accept duplicate properties, as long as they have 833 // the same value (only check for single value properties) 834 if (m_props.get(p).size() > 1 || values.size() > 1 835 || ! values.get(0).equals(m_props.get(p).get(0))) 836 setWarning("Duplicate property " + p + " in node"); 837 m_props.put(p, values); 838 return true; 839 } 840 if (ttype != '\n') 841 // Don't pushBack newline, will confuse lineno() (Bug 4942853) 842 m_tokenizer.pushBack(); 843 return false; 844 } 845 readSgf(InputStream in, String charset)846 private void readSgf(InputStream in, String charset) 847 throws SgfError, SgfCharsetChanged 848 { 849 try 850 { 851 m_boardSize = -1; 852 if (m_progressShow != null) 853 { 854 m_byteCountInputStream = new ByteCountInputStream(in); 855 in = m_byteCountInputStream; 856 } 857 InputStreamReader reader; 858 try 859 { 860 reader = new InputStreamReader(in, charset); 861 } 862 catch (UnsupportedEncodingException e) 863 { 864 // Should actually not happen, because this function is only 865 // called with charset ISO-8859-1 (should be supported on every 866 // Java platform according to Charset documentation) or with a 867 // CA property value, which was already checked with 868 // Charset.isSupported() 869 setWarning("Character set \"" + charset + "\" not supported"); 870 reader = new InputStreamReader(in); 871 } 872 m_reader = new BufferedReader(reader); 873 m_tokenizer = new StreamTokenizer(m_reader); 874 findRoot(); 875 Node root = readNext(null, true); 876 Node node = root; 877 while (node != null) 878 node = readNext(node, false); 879 checkEndOfFile(); 880 getBoardSize(); // Set to default value if still unknown 881 m_tree = new GameTree(m_boardSize, root); 882 applyFixes(); 883 } 884 catch (FileNotFoundException e) 885 { 886 throw new SgfError("File not found."); 887 } 888 catch (IOException e) 889 { 890 throw new SgfError("IO error"); 891 } 892 catch (OutOfMemoryError e) 893 { 894 throw new SgfError("Out of memory"); 895 } 896 } 897 readValue()898 private String readValue() throws IOException, SgfError 899 { 900 m_tokenizer.nextToken(); 901 int ttype = m_tokenizer.ttype; 902 if (ttype != '[') 903 { 904 if (ttype != '\n') 905 // Don't pushBack newline, will confuse lineno() (Bug 4942853) 906 m_tokenizer.pushBack(); 907 return null; 908 } 909 m_buffer.setLength(0); 910 boolean quoted = false; 911 Character last = null; 912 while (true) 913 { 914 int c = m_reader.read(); 915 if (c < 0) 916 throw getError("Property value incomplete"); 917 if (quoted) 918 { 919 if (c != '\n' && c != '\r') 920 m_buffer.append((char)c); 921 last = Character.valueOf((char)c); 922 quoted = false; 923 } 924 else 925 { 926 if (c == ']') 927 break; 928 quoted = (c == '\\'); 929 if (! quoted) 930 { 931 // Transform all linebreaks allowed in SGF (LF, CR, LFCR, 932 // CRLF) to a single '\n' 933 boolean isLinebreak = (c == '\n' || c == '\r'); 934 boolean lastLinebreak = 935 (last != null && (last.charValue() == '\n' 936 || last.charValue() == '\r')); 937 boolean filterSecondLinebreak = 938 (isLinebreak && lastLinebreak && c != last.charValue()); 939 if (filterSecondLinebreak) 940 last = null; 941 else 942 { 943 if (isLinebreak) 944 m_buffer.append('\n'); 945 else 946 m_buffer.append((char)c); 947 last = Character.valueOf((char)c); 948 } 949 } 950 } 951 } 952 return m_buffer.toString(); 953 } 954 set(Node node, StringInfo type, String value)955 private void set(Node node, StringInfo type, String value) 956 { 957 GameInfo info = createGameInfo(node); 958 info.set(type, value); 959 } 960 set(Node node, StringInfoColor type, GoColor c, String value)961 private void set(Node node, StringInfoColor type, 962 GoColor c, String value) 963 { 964 GameInfo info = createGameInfo(node); 965 info.set(type, c, value); 966 } 967 setTimeSettings(Node node)968 private void setTimeSettings(Node node) 969 { 970 TimeSettings s = null; 971 if (m_preByoyomi > 0 972 && (m_ignoreOvertime || m_byoyomi <= 0 || m_byoyomiMoves <= 0)) 973 s = new TimeSettings(m_preByoyomi); 974 else if (m_preByoyomi <= 0 && ! m_ignoreOvertime && m_byoyomi > 0 975 && m_byoyomiMoves > 0) 976 s = new TimeSettings(0, m_byoyomi, m_byoyomiMoves); 977 else if (m_preByoyomi > 0 && ! m_ignoreOvertime && m_byoyomi > 0 978 && m_byoyomiMoves > 0) 979 s = new TimeSettings(m_preByoyomi, m_byoyomi, m_byoyomiMoves); 980 if (s != null) 981 node.createGameInfo().setTimeSettings(s); 982 } 983 setWarning(String message)984 private void setWarning(String message) 985 { 986 m_warnings.add(message); 987 } 988 throwInvalidCoordinates(String s)989 private void throwInvalidCoordinates(String s) throws SgfError 990 { 991 throw getError("Invalid coordinates \"" + s + "\""); 992 } 993 } 994