/* * Copyright 2013-2016 Cel Skeggs * * This file is part of the CCRE, the Common Chicken Runtime Engine. * * The CCRE is free software: you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License as published by the Free * Software Foundation, either version 3 of the License, or (at your option) any * later version. * * The CCRE is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. * * You should have received a copy of the GNU Lesser General Public License * along with the CCRE. If not, see <http://www.gnu.org/licenses/>. */ package ccre.storage; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintStream; import java.util.HashMap; import ccre.channel.BooleanCell; import ccre.channel.FloatCell; import ccre.log.Logger; import ccre.util.UniqueIds; import ccre.verifier.FlowPhase; import ccre.verifier.SetupPhase; /** * A storage segment - a place to store various pieces of data. A StorageSegment * can be obtained using StorageProvider. */ public final class StorageSegment { private final HashMap<String, String> data = new HashMap<String, String>(); private String name; private boolean modified = false; /** * Load a map from a properties-like file. * * @param input the InputStream to read from. * @param keepInvalidLines whether or not to save invalid lines under backup * keys. * @param target the map to put the loaded keys into. * @throws IOException if reading from the input fails for some reason. */ @SetupPhase public static void loadProperties(InputStream input, boolean keepInvalidLines, HashMap<String, String> target) throws IOException { BufferedReader din = new BufferedReader(new InputStreamReader(input)); try { while (true) { String line = din.readLine(); if (line == null) { break; } int ind = line.indexOf('='); if (ind == -1) {// Invalid or empty line. if (!line.isEmpty() && keepInvalidLines) { Logger.warning("Invalid line ignored in configuration: " + line + " - saving under backup key."); target.put(UniqueIds.global.nextHexId("unknown-" + System.currentTimeMillis() + "-" + line.hashCode()), line); } continue; } String key = line.substring(0, ind), value = line.substring(ind + 1); target.put(key, value); } } finally { din.close(); } } StorageSegment(String unescaped_name) { if (unescaped_name == null) { throw new NullPointerException("Storage names cannot be null"); } StringBuilder buf = new StringBuilder(unescaped_name); for (int i = buf.length() - 1; i >= 0; i--) { char c = buf.charAt(i); if (c == ' ') { buf.setCharAt(i, '_'); } else if (!(Character.isUpperCase(c) || Character.isLowerCase(c) || Character.isDigit(c))) { // escape any "weird" characters buf.setCharAt(i, '$'); buf.insert(i + 1, (int) c); } } this.name = buf.toString(); try { InputStream target = Storage.openInput("ccre_storage_" + name); if (target == null) { Logger.info("No data file for: " + name + " - assuming empty."); } else { try { loadProperties(target, true, data); } finally { target.close(); } } } catch (IOException ex) { Logger.warning("Error reading storage: " + name, ex); } } /** * Get a String value for the specified key. * * @param key the key to look up. * @return the String contained there, or null if the key doesn't exist. */ @SetupPhase public synchronized String getStringForKey(String key) { return data.get(key); } /** * Set the string value behind the specified key. * * @param key the key to put the String under. * @param value the String to store under this key. */ @FlowPhase public synchronized void setStringForKey(String key, String value) { if (value == null) { data.remove(key); } else { data.put(key, value); } modified = true; } /** * Flush the segment. This attempts to make sure that all data is stored on * disk (or somewhere else, depending on the provider). If this is not * called, data might not be saved! */ @SetupPhase public synchronized void flush() { if (modified) { try { PrintStream pout = new PrintStream(Storage.openOutput("ccre_storage_" + name)); try { for (String key : data.keySet()) { if (key.contains("=")) { Logger.warning("Invalid key ignored during save: " + key + " - saving under backup key."); data.put(UniqueIds.global.nextHexId("badkey-" + System.currentTimeMillis() + "-" + key.hashCode()), key); } else { String value = data.get(key); if (value != null) { pout.println(key + "=" + value); } } } } finally { pout.close(); } } catch (IOException ex) { Logger.warning("Error writing storage: " + name, ex); } modified = false; } } /** * Get the name of this segment, if available. * * @return the segment's name, or null if none exists. */ public String getName() { return name; } /** * Attach a FloatHolder to this storage segment. This will restore data if * it has been stored as modified in the segment. This will save the data of * the float holder as it updates, although you will need to call flush() to * ensure that the data is saved. * * This will only overwrite the current value of the FloatHolder if the data * was saved when the FloatHolder had the same default (value when this * method is called). This means that you can modify the contents using * either the StorageSegment or by changing the FloatHolder's original * value. * * @param name the name to save the holder under. * @param holder the holder to save. */ @SetupPhase public void attachFloatHolder(String name, final FloatCell holder) { final String key = "float_holder_" + name, default_key = "float_holder_default_" + name; final float originalValue = holder.get(); String vraw = getStringForKey(key); if (vraw != null) { try { float value = Float.parseFloat(vraw); String draw = getStringForKey(default_key); float default_ = draw == null ? Float.NaN : Float.parseFloat(draw); // If the default is the same as the holder's default, then load // the value if (draw == null || Float.floatToIntBits(default_) == Float.floatToIntBits(originalValue)) { Logger.config("Loaded config for " + name + ": def:" + default_ + " old:" + originalValue + " new:" + value); holder.set(value); } // Otherwise, the default has changed from the holder, and // therefore we want the updated value from the holder } catch (NumberFormatException ex) { Logger.warning("Invalid float value: '" + vraw + "'!", ex); } } holder.send((value) -> { setStringForKey(key, Float.toString(value)); setStringForKey(default_key, Float.toString(originalValue)); }); } /** * Attach a BooleanHolder to this storage segment. This will restore data if * it has been stored as modified in the segment. This will save the data of * the boolean holder as it updates, although you will need to call flush() * to ensure that the data is saved. * * This will only overwrite the current value of the BooleanHolder if the * data was saved when the BooleanHolder had the same default (value when * this method is called). This means that you can modify the contents using * either the StorageSegment or by changing the BooleanHolder's original * value. * * @param name the name to save the holder under. * @param holder the holder to save. */ @SetupPhase public void attachBooleanHolder(String name, final BooleanCell holder) { final String key = "boolean_holder_" + name, default_key = "boolean_holder_default_" + name; final boolean originalValue = holder.get(); String vraw = getStringForKey(key); if (vraw != null) { try { boolean value = Boolean.parseBoolean(vraw); String draw = getStringForKey(default_key); // If the default is the same as the holder's default, then load // the value if (draw == null || Boolean.parseBoolean(draw) == originalValue) { Logger.config("Loaded config for " + name + ": def:" + draw + " old:" + originalValue + " new:" + value); holder.set(value); } // Otherwise, the default has changed from the holder, and // therefore we want the updated value from the holder } catch (NumberFormatException ex) { Logger.warning("Invalid boolean value: '" + vraw + "'!", ex); } } holder.send((value) -> { setStringForKey(key, Boolean.toString(value)); setStringForKey(default_key, Boolean.toString(originalValue)); }); } }