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