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}