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 }