001/*----------------------------------------------------------------------------*/
002/* Copyright (c) FIRST 2008-2012. All Rights Reserved.                        */
003/* Open Source Software - may be modified and shared by FRC teams. The code   */
004/* must be accompanied by the FIRST BSD license file in the root directory of */
005/* the project.                                                               */
006/*----------------------------------------------------------------------------*/
007package edu.wpi.first.wpilibj;
008
009import java.io.File;
010import java.io.FileInputStream;
011import java.io.FileOutputStream;
012import java.io.IOException;
013import java.io.InputStream;
014import java.io.OutputStream;
015import java.util.Hashtable;
016import java.util.Vector;
017
018import edu.wpi.first.wpilibj.communication.FRCNetworkCommunicationsLibrary.tResourceType;
019import edu.wpi.first.wpilibj.communication.UsageReporting;
020import edu.wpi.first.wpilibj.networktables.NetworkTable;
021import edu.wpi.first.wpilibj.tables.ITable;
022import edu.wpi.first.wpilibj.tables.ITableListener;
023
024/**
025 * The preferences class provides a relatively simple way to save important
026 * values to the RoboRIO to access the next time the RoboRIO is booted.
027 *
028 * <p>This class loads and saves from a file inside the RoboRIO. The user can not
029 * access the file directly, but may modify values at specific fields which will
030 * then be saved to the file when {@link Preferences#save() save()} is
031 * called.</p>
032 *
033 * <p>This class is thread safe.</p>
034 *
035 * <p>This will also interact with {@link NetworkTable} by creating a table
036 * called "Preferences" with all the key-value pairs. To save using
037 * {@link NetworkTable}, simply set the boolean at position ~S A V E~ to true.
038 * Also, if the value of any variable is " in the {@link NetworkTable}, then
039 * that represents non-existence in the {@link Preferences} table</p>
040 *
041 * @author Joe Grinstead
042 */
043public class Preferences {
044
045    /**
046     * The Preferences table name
047     */
048    private static final String TABLE_NAME = "Preferences";
049    /**
050     * The value of the save field
051     */
052    private static final String SAVE_FIELD = "~S A V E~";
053    /**
054     * The file to save to
055     */
056    private static final String FILE_NAME = "/home/lvuser/wpilib-preferences.ini";
057    /**
058     * The characters to put between a field and value
059     */
060    private static final byte[] VALUE_PREFIX = {'=', '\"'};
061    /**
062     * The characters to put after the value
063     */
064    private static final byte[] VALUE_SUFFIX = {'\"', '\n'};
065    /**
066     * The newline character
067     */
068    private static final byte[] NEW_LINE = {'\n'};
069    /**
070     * The singleton instance
071     */
072    private static Preferences instance;
073
074    /**
075     * Returns the preferences instance.
076     *
077     * @return the preferences instance
078     */
079    public synchronized static Preferences getInstance() {
080        if (instance == null) {
081            instance = new Preferences();
082        }
083        return instance;
084    }
085    /**
086     * The semaphore for beginning reads and writes to the file
087     */
088    private final Object fileLock = new Object();
089    /**
090     * The semaphore for reading from the table
091     */
092    private final Object lock = new Object();
093    /**
094     * The actual values (String->String)
095     */
096    private Hashtable values;
097    /**
098     * The keys in the order they were read from the file
099     */
100    private Vector keys;
101    /**
102     * The comments that were in the file sorted by which key they appeared over
103     * (String->Comment)
104     */
105    private Hashtable comments;
106    /**
107     * The comment at the end of the file
108     */
109    private Comment endComment;
110
111    /**
112     * Creates a preference class that will automatically read the file in a
113     * different thread. Any call to its methods will be blocked until the
114     * thread is finished reading.
115     */
116    private Preferences() {
117        values = new Hashtable();
118        keys = new Vector();
119
120        // We synchronized on fileLock and then wait
121        // for it to know that the reading thread has started
122        synchronized (fileLock) {
123            new Thread() {
124                public void run() {
125                    read();
126                }
127            } .start();
128            try {
129                fileLock.wait();
130            } catch (InterruptedException ex) {
131            }
132        }
133
134        UsageReporting.report(tResourceType.kResourceType_Preferences, 0);
135    }
136
137    /**
138     * @return a vector of the keys
139     */
140    public Vector getKeys() {
141        synchronized (lock) {
142            return keys;
143        }
144    }
145
146    /**
147     * Puts the given value into the given key position
148     *
149     * @param key the key
150     * @param value the value
151     * @throws ImproperPreferenceKeyException if the key contains an illegal
152     * character
153     */
154    private void put(String key, String value) {
155        synchronized (lock) {
156            if (key == null) {
157                throw new NullPointerException();
158            }
159            ImproperPreferenceKeyException.confirmString(key);
160            if (values.put(key, value) == null) {
161                keys.addElement(key);
162            }
163            NetworkTable.getTable(TABLE_NAME).putString(key, value);
164        }
165    }
166
167    /**
168     * Puts the given string into the preferences table.
169     *
170     * <p>The value may not have quotation marks, nor may the key have any
171     * whitespace nor an equals sign</p>
172     *
173     * <p>This will <b>NOT</b> save the value to memory between power cycles, to
174     * do that you must call {@link Preferences#save() save()} (which must be
175     * used with care). at some point after calling this.</p>
176     *
177     * @param key the key
178     * @param value the value
179     * @throws NullPointerException if value is null
180     * @throws IllegalArgumentException if value contains a quotation mark
181     * @throws ImproperPreferenceKeyException if the key contains any whitespace
182     * or an equals sign
183     */
184    public void putString(String key, String value) {
185        if (value == null) {
186            throw new NullPointerException();
187        }
188        if (value.indexOf('"') != -1) {
189            throw new IllegalArgumentException("Can not put string:" + value + " because it contains quotation marks");
190        }
191        put(key, value);
192    }
193
194    /**
195     * Puts the given int into the preferences table.
196     *
197     * <p>The key may not have any whitespace nor an equals sign</p>
198     *
199     * <p>This will <b>NOT</b> save the value to memory between power cycles, to
200     * do that you must call {@link Preferences#save() save()} (which must be
201     * used with care) at some point after calling this.</p>
202     *
203     * @param key the key
204     * @param value the value
205     * @throws ImproperPreferenceKeyException if the key contains any whitespace
206     * or an equals sign
207     */
208    public void putInt(String key, int value) {
209        put(key, String.valueOf(value));
210    }
211
212    /**
213     * Puts the given double into the preferences table.
214     *
215     * <p>The key may not have any whitespace nor an equals sign</p>
216     *
217     * <p>This will <b>NOT</b> save the value to memory between power cycles, to
218     * do that you must call {@link Preferences#save() save()} (which must be
219     * used with care) at some point after calling this.</p>
220     *
221     * @param key the key
222     * @param value the value
223     * @throws ImproperPreferenceKeyException if the key contains any whitespace
224     * or an equals sign
225     */
226    public void putDouble(String key, double value) {
227        put(key, String.valueOf(value));
228    }
229
230    /**
231     * Puts the given float into the preferences table.
232     *
233     * <p>The key may not have any whitespace nor an equals sign</p>
234     *
235     * <p>This will <b>NOT</b> save the value to memory between power cycles, to
236     * do that you must call {@link Preferences#save() save()} (which must be
237     * used with care) at some point after calling this.</p>
238     *
239     * @param key the key
240     * @param value the value
241     * @throws ImproperPreferenceKeyException if the key contains any whitespace
242     * or an equals sign
243     */
244    public void putFloat(String key, float value) {
245        put(key, String.valueOf(value));
246    }
247
248    /**
249     * Puts the given boolean into the preferences table.
250     *
251     * <p>The key may not have any whitespace nor an equals sign</p>
252     *
253     * <p>This will <b>NOT</b> save the value to memory between power cycles, to
254     * do that you must call {@link Preferences#save() save()} (which must be
255     * used with care) at some point after calling this.</p>
256     *
257     * @param key the key
258     * @param value the value
259     * @throws ImproperPreferenceKeyException if the key contains any whitespace
260     * or an equals sign
261     */
262    public void putBoolean(String key, boolean value) {
263        put(key, String.valueOf(value));
264    }
265
266    /**
267     * Puts the given long into the preferences table.
268     *
269     * <p>The key may not have any whitespace nor an equals sign</p>
270     *
271     * <p>This will <b>NOT</b> save the value to memory between power cycles, to
272     * do that you must call {@link Preferences#save() save()} (which must be
273     * used with care) at some point after calling this.</p>
274     *
275     * @param key the key
276     * @param value the value
277     * @throws ImproperPreferenceKeyException if the key contains any whitespace
278     * or an equals sign
279     */
280    public void putLong(String key, long value) {
281        put(key, String.valueOf(value));
282    }
283
284    /**
285     * Returns the value at the given key.
286     *
287     * @param key the key
288     * @return the value (or null if none exists)
289     * @throws NullPointerException if the key is null
290     */
291    private String get(String key) {
292        synchronized (lock) {
293            if (key == null) {
294                throw new NullPointerException();
295            }
296            return (String) values.get(key);
297        }
298    }
299
300    /**
301     * Returns whether or not there is a key with the given name.
302     *
303     * @param key the key
304     * @return if there is a value at the given key
305     * @throws NullPointerException if key is null
306     */
307    public boolean containsKey(String key) {
308        return get(key) != null;
309    }
310
311    /**
312     * Remove a preference
313     *
314     * @param key the key
315     * @throws NullPointerException if key is null
316     */
317    public void remove(String key) {
318        synchronized (lock) {
319            if (key == null) {
320                throw new NullPointerException();
321            }
322            values.remove(key);
323            keys.removeElement(key);
324        }
325    }
326
327    /**
328     * Returns the string at the given key. If this table does not have a value
329     * for that position, then the given backup value will be returned.
330     *
331     * @param key the key
332     * @param backup the value to return if none exists in the table
333     * @return either the value in the table, or the backup
334     * @throws NullPointerException if the key is null
335     */
336    public String getString(String key, String backup) {
337        String value = get(key);
338        return value == null ? backup : value;
339    }
340
341    /**
342     * Returns the int at the given key. If this table does not have a value for
343     * that position, then the given backup value will be returned.
344     *
345     * @param key the key
346     * @param backup the value to return if none exists in the table
347     * @return either the value in the table, or the backup
348     * @throws IncompatibleTypeException if the value in the table can not be
349     * converted to an int
350     */
351    public int getInt(String key, int backup) {
352        String value = get(key);
353        if (value == null) {
354            return backup;
355        } else {
356            try {
357                return Integer.parseInt(value);
358            } catch (NumberFormatException e) {
359                throw new IncompatibleTypeException(value, "int");
360            }
361        }
362    }
363
364    /**
365     * Returns the double at the given key. If this table does not have a value
366     * for that position, then the given backup value will be returned.
367     *
368     * @param key the key
369     * @param backup the value to return if none exists in the table
370     * @return either the value in the table, or the backup
371     * @throws IncompatibleTypeException if the value in the table can not be
372     * converted to an double
373     */
374    public double getDouble(String key, double backup) {
375        String value = get(key);
376        if (value == null) {
377            return backup;
378        } else {
379            try {
380                return Double.parseDouble(value);
381            } catch (NumberFormatException e) {
382                throw new IncompatibleTypeException(value, "double");
383            }
384        }
385    }
386
387    /**
388     * Returns the boolean at the given key. If this table does not have a value
389     * for that position, then the given backup value will be returned.
390     *
391     * @param key the key
392     * @param backup the value to return if none exists in the table
393     * @return either the value in the table, or the backup
394     * @throws IncompatibleTypeException if the value in the table can not be
395     * converted to a boolean
396     */
397    public boolean getBoolean(String key, boolean backup) {
398        String value = get(key);
399        if (value == null) {
400            return backup;
401        } else {
402            if (value.equalsIgnoreCase("true")) {
403                return true;
404            } else if (value.equalsIgnoreCase("false")) {
405                return false;
406            } else {
407                throw new IncompatibleTypeException(value, "boolean");
408            }
409        }
410    }
411
412    /**
413     * Returns the float at the given key. If this table does not have a value
414     * for that position, then the given backup value will be returned.
415     *
416     * @param key the key
417     * @param backup the value to return if none exists in the table
418     * @return either the value in the table, or the backup
419     * @throws IncompatibleTypeException if the value in the table can not be
420     * converted to a float
421     */
422    public float getFloat(String key, float backup) {
423        String value = get(key);
424        if (value == null) {
425            return backup;
426        } else {
427            try {
428                return Float.parseFloat(value);
429            } catch (NumberFormatException e) {
430                throw new IncompatibleTypeException(value, "float");
431            }
432        }
433    }
434
435    /**
436     * Returns the long at the given key. If this table does not have a value
437     * for that position, then the given backup value will be returned.
438     *
439     * @param key the key
440     * @param backup the value to return if none exists in the table
441     * @return either the value in the table, or the backup
442     * @throws IncompatibleTypeException if the value in the table can not be
443     * converted to a long
444     */
445    public long getLong(String key, long backup) {
446        String value = get(key);
447        if (value == null) {
448            put(key, String.valueOf(backup));
449            return backup;
450        } else {
451            try {
452                return Long.parseLong(value);
453            } catch (NumberFormatException e) {
454                throw new IncompatibleTypeException(value, "long");
455            }
456        }
457    }
458
459    /**
460     * Saves the preferences to a file on the RoboRIO.
461     *
462     * <p>This should <b>NOT</b> be called often. Too many writes can damage the
463     * RoboRIO's flash memory. While it is ok to save once or twice a match, this
464     * should never be called every run of
465     * {@link IterativeRobot#teleopPeriodic()}.</p>
466     *
467     * <p>The actual writing of the file is done in a separate thread. However,
468     * any call to a get or put method will wait until the table is fully saved
469     * before continuing.</p>
470     */
471    public void save() {
472        synchronized (fileLock) {
473            new Thread() {
474                public void run() {
475                    write();
476                }
477            } .start();
478            try {
479                fileLock.wait();
480            } catch (InterruptedException ex) {
481            }
482        }
483    }
484
485    /**
486     * Internal method that actually writes the table to a file. This is called
487     * in its own thread when {@link Preferences#save() save()} is called.
488     */
489    private void write() {
490        synchronized (lock) {
491            synchronized (fileLock) {
492                fileLock.notifyAll();
493            }
494
495            File file = null;
496            FileOutputStream output = null;
497            try {
498                file = new File(FILE_NAME);
499
500                if (file.exists())
501                        file.delete();
502
503                file.createNewFile();
504
505                output = new FileOutputStream(file);
506
507                                output.write("[Preferences]\n".getBytes());
508
509                for (int i = 0; i < keys.size(); i++) {
510                    String key = (String) keys.elementAt(i);
511                    String value = (String) values.get(key);
512
513                    if (comments != null) {
514                        Comment comment = (Comment) comments.get(key);
515                        if (comment != null) {
516                            comment.write(output);
517                        }
518                    }
519
520                    output.write(key.getBytes());
521                    output.write(VALUE_PREFIX);
522                    output.write(value.getBytes());
523                    output.write(VALUE_SUFFIX);
524                }
525
526                if (endComment != null) {
527                    endComment.write(output);
528                }
529            } catch (IOException ex) {
530                ex.printStackTrace();
531            } finally {
532                if (output != null) {
533                    try {
534                        output.close();
535                    } catch (IOException ex) {
536                    }
537                }
538                NetworkTable.getTable(TABLE_NAME).putBoolean(SAVE_FIELD, false);
539            }
540        }
541    }
542
543    /**
544     * The internal method to read from a file. This will be called in its own
545     * thread when the preferences singleton is first created.
546     */
547    private void read() {
548        class EndOfStreamException extends Exception {
549        }
550
551        class Reader {
552
553            InputStream stream;
554
555            Reader(InputStream stream) {
556                this.stream = stream;
557            }
558
559            public char read() throws IOException, EndOfStreamException {
560                int input = stream.read();
561                if (input == -1) {
562                    throw new EndOfStreamException();
563                } else {
564                    // Check for carriage returns
565                    return input == '\r' ? '\n' : (char) input;
566                }
567            }
568
569            char readWithoutWhitespace() throws IOException, EndOfStreamException {
570                while (true) {
571                    char value = read();
572                    switch (value) {
573                    case ' ':
574                    case '\t':
575                        continue;
576                    default:
577                        return value;
578                    }
579                }
580            }
581        }
582
583        synchronized (lock) {
584            synchronized (fileLock) {
585                fileLock.notifyAll();
586            }
587
588            Comment comment = null;
589
590
591
592            File file = null;
593            FileInputStream input = null;
594            try {
595                file = new File(FILE_NAME);
596
597                if (file.exists()) {
598                        input = new FileInputStream(file);
599                    Reader reader = new Reader(input);
600
601                    StringBuffer buffer;
602
603                    while (true) {
604                        char value = reader.readWithoutWhitespace();
605
606                        if (value == '\n' || value == ';') {
607                            if (comment == null) {
608                                comment = new Comment();
609                            }
610
611                            if (value == '\n') {
612                                comment.addBytes(NEW_LINE);
613                            } else {
614                                buffer = new StringBuffer(30);
615                                for (; value != '\n'; value = reader.read()) {
616                                    buffer.append(value);
617                                }
618                                buffer.append('\n');
619                                comment.addBytes(buffer.toString().getBytes());
620                            }
621                        } else if (value == '[') {
622                                                        // Find the end of the section and the new line after it and throw it away
623                                                        while (reader.read() !=']');
624                                                        while (reader.read() != '\n');
625                                                } else {
626                            buffer = new StringBuffer(30);
627                            for (; value != '='; value = reader.readWithoutWhitespace()) {
628                                buffer.append(value);
629                            }
630                            String name = buffer.toString();
631                            buffer = new StringBuffer(30);
632
633                            boolean shouldBreak = false;
634
635                            value = reader.readWithoutWhitespace();
636                            if (value == '"') {
637                                for (value = reader.read(); value != '"'; value = reader.read()) {
638                                    buffer.append(value);
639                                }
640                                // Clear the line
641                                while (reader.read() != '\n');
642                            } else {
643                                try {
644                                    for (; value != '\n'; value = reader.readWithoutWhitespace()) {
645                                        buffer.append(value);
646                                    }
647                                } catch (EndOfStreamException e) {
648                                    shouldBreak = true;
649                                }
650                            }
651
652                            String result = buffer.toString();
653
654                            keys.addElement(name);
655                            values.put(name, result);
656                            NetworkTable.getTable(TABLE_NAME).putString(name, result);
657
658                            if (comment != null) {
659                                if (comments == null) {
660                                    comments = new Hashtable();
661                                }
662                                comments.put(name, comment);
663                                comment = null;
664                            }
665
666
667                            if (shouldBreak) {
668                                break;
669                            }
670                        }
671                    }
672                }
673            } catch (IOException ex) {
674                ex.printStackTrace();
675            } catch (EndOfStreamException ex) {
676                System.out.println("Done Reading");
677            }
678
679            if (input != null) {
680                try {
681                        input.close();
682                } catch (IOException ex) {
683                }
684            }
685
686            if (comment != null) {
687                endComment = comment;
688            }
689        }
690
691        NetworkTable.getTable(TABLE_NAME).putBoolean(SAVE_FIELD, false);
692        // TODO: Verify that this works even though it changes with subtables.
693        //       Should work since preferences shouldn't have subtables.
694        NetworkTable.getTable(TABLE_NAME).addTableListener(new ITableListener() {
695            public void valueChanged(ITable source, String key, Object value, boolean isNew) {
696                if (key.equals(SAVE_FIELD)) {
697                    if (((Boolean) value).booleanValue()) {
698                        save();
699                    }
700                } else {
701                    synchronized (lock) {
702                        if (!ImproperPreferenceKeyException.isAcceptable(key) || value.toString().indexOf('"') != -1) {
703                            if (values.contains(key) || keys.contains(key)) {
704                                values.remove(key);
705                                keys.removeElement(key);
706                                NetworkTable.getTable(TABLE_NAME).putString(key, "\"");
707                            }
708                        } else {
709                            if (values.put(key, value.toString()) == null) {
710                                keys.addElement(key);
711                            }
712                        }
713                    }
714                }
715            }
716        });
717    }
718
719    /**
720     * A class representing some comment lines in the ini file. This is used so
721     * that if a programmer ever directly modifies the ini file, then his/her
722     * comments will still be there after {@link Preferences#save() save()} is
723     * called.
724     */
725    private static class Comment {
726
727        /**
728         * A vector of byte arrays. Each array represents a line to write
729         */
730        private Vector bytes = new Vector();
731
732        /**
733         * Appends the given bytes to the comment.
734         *
735         * @param bytes the bytes to add
736         */
737        private void addBytes(byte[] bytes) {
738            this.bytes.addElement(bytes);
739        }
740
741        /**
742         * Writes this comment to the given stream
743         *
744         * @param stream the stream to write to
745         * @throws IOException if the stream has a problem
746         */
747        private void write(OutputStream stream) throws IOException {
748            for (int i = 0; i < bytes.size(); i++) {
749                stream.write((byte[]) bytes.elementAt(i));
750            }
751        }
752    }
753
754    /**
755     * This exception is thrown if the a value requested cannot be converted to
756     * the requested type.
757     */
758    public static class IncompatibleTypeException extends RuntimeException {
759
760        /**
761         * Creates an exception with a description based on the input
762         *
763         * @param value the value that can not be converted
764         * @param type the type that the value can not be converted to
765         */
766        public IncompatibleTypeException(String value, String type) {
767            super("Cannot convert \"" + value + "\" into " + type);
768        }
769    }
770
771    /**
772     * Should be thrown if a string can not be used as a key in the preferences
773     * file. This happens if the string contains a new line, a space, a tab, or
774     * an equals sign.
775     */
776    public static class ImproperPreferenceKeyException extends RuntimeException {
777
778        /**
779         * Instantiates an exception with a descriptive message based on the
780         * input.
781         *
782         * @param value the illegal key
783         * @param letter the specific character that made it illegal
784         */
785        public ImproperPreferenceKeyException(String value, char letter) {
786            super("Preference key \""
787                  + value + "\" is not allowed to contain letter with ASCII code:" + (byte) letter);
788        }
789
790        /**
791         * Tests if the given string is ok to use as a key in the preference
792         * table. If not, then a {@link ImproperPreferenceKeyException} will be
793         * thrown.
794         *
795         * @param value the value to test
796         */
797        public static void confirmString(String value) {
798            for (int i = 0; i < value.length(); i++) {
799                char letter = value.charAt(i);
800                switch (letter) {
801                case '=':
802                case '\n':
803                case '\r':
804                case ' ':
805                case '\t':
806                case '[':
807                case ']':
808                    throw new ImproperPreferenceKeyException(value, letter);
809                }
810            }
811        }
812
813        /**
814         * Returns whether or not the given string is ok to use in the
815         * preference table.
816         *
817         * @param value the string to check
818         * @return true if the given string is ok to use in the preference table
819         */
820        public static boolean isAcceptable(String value) {
821            for (int i = 0; i < value.length(); i++) {
822                char letter = value.charAt(i);
823                switch (letter) {
824                case '=':
825                case '\n':
826                case '\r':
827                case ' ':
828                case '\t':
829                case '[':
830                case ']':
831                    return false;
832                }
833            }
834            return true;
835        }
836    }
837}