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