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 }