001package squidpony.store.json; 002 003import com.badlogic.gdx.Gdx; 004import com.badlogic.gdx.Preferences; 005import com.badlogic.gdx.utils.JsonWriter; 006import squidpony.LZSPlus; 007import squidpony.StringStringMap; 008import squidpony.Garbler; 009 010import java.util.Map; 011 012/** 013 * Helps games store information in libGDX's Preferences class as Strings, then get it back out. 014 * Created by Tommy Ettinger on 9/16/2016. 015 */ 016public class JsonStorage { 017 public final Preferences preferences; 018 public final String storageName; 019 public final JsonConverter json; 020 protected StringStringMap contents; 021 public boolean compress = true; 022 public long[] garbleKey; 023 024 /** 025 * Please don't use this constructor if possible; it simply calls {@link #JsonStorage(String)} with the constant 026 * String "nameless". This could easily overlap with other files/sections in Preferences, so you should always 027 * prefer giving a String argument to the constructor, typically the name of the game. 028 * @see #JsonStorage(String) the recommended constructor to use 029 */ 030 public JsonStorage() 031 { 032 this("nameless", new long[0]); 033 } 034 035 /** 036 * Creates a JsonStorage with the given fileName to save using Preferences from libGDX. The name should generally 037 * be the name of this game or application, and must be a valid name for a file (so no slashes, backslashes, colons, 038 * semicolons, or commas for certain, and other non-alphanumeric characters are also probably invalid). You should 039 * not assume anything is present in the Preferences storage unless you have put it there, and this applies doubly 040 * to games or applications other than your own; you should avoid values for fileName that might overlap with 041 * another game's Preferences values. 042 * <br> 043 * To organize saved data into sub-sections, you specify logical units (like different players' saved games) with a 044 * String outerName when you call {@link #store(String)}, and can further distinguish data under the outerName when 045 * you call {@link #put(String, Object)} to put each individual item into the saved storage with its own innerName. 046 * <br> 047 * Calling this also sets up custom serializers for several important types in SquidLib; char[][], OrderedMap, 048 * IntDoubleOrderedMap, FakeLanguageGen, GreasedRegion, and notably Pattern from RegExodus all have smaller 049 * serialized representations than the default. OrderedMap allows non-String keys, which gets around a limitation in 050 * JSON maps normally, and both FakeLanguageGen and Pattern are amazingly smaller with the custom representation. 051 * The custom char[][] representation is about half the normal size by omitting commas after each char. 052 * @param fileName the valid file name to create or open from Preferences; typically the name of the game/app. 053 */ 054 public JsonStorage(final String fileName) 055 { 056 this(fileName, new long[0]); 057 } 058 059 /** 060 * Creates a JsonStorage with the given fileName to save using Preferences from libGDX. The name should generally 061 * be the name of this game or application, and must be a valid name for a file (so no slashes, backslashes, colons, 062 * semicolons, or commas for certain, and other non-alphanumeric characters are also probably invalid). You should 063 * not assume anything is present in the Preferences storage unless you have put it there, and this applies doubly 064 * to games or applications other than your own; you should avoid values for fileName that might overlap with 065 * another game's Preferences values. This constructor also allows you to specify a "garble" String; if this is 066 * non-null, it will be used as a key to obfuscate the output and de-obfuscate the loaded input using fairly basic 067 * methods. If garble is null, it is ignored. 068 * <br> 069 * To organize saved data into sub-sections, you specify logical units (like different players' saved games) with a 070 * String outerName when you call {@link #store(String)}, and can further distinguish data under the outerName when 071 * you call {@link #put(String, Object)} to put each individual item into the saved storage with its own innerName. 072 * <br> 073 * Calling this also sets up custom serializers for several important types in SquidLib; char[][], OrderedMap, 074 * IntDoubleOrderedMap, FakeLanguageGen, GreasedRegion, and notably Pattern from RegExodus all have smaller 075 * serialized representations than the default. OrderedMap allows non-String keys, which gets around a limitation in 076 * JSON maps normally, and both FakeLanguageGen and Pattern are amazingly smaller with the custom representation. 077 * The custom char[][] representation is about half the normal size by omitting commas after each char. 078 * @param fileName the valid file name to create or open from Preferences; typically the name of the game/app. 079 * @param garble a String that will be used as a key to obfuscate the saved output if non-null 080 */ 081 public JsonStorage(final String fileName, final String garble) 082 { 083 storageName = fileName; 084 preferences = Gdx.app.getPreferences(storageName); 085 json = new JsonConverter(JsonWriter.OutputType.minimal); 086 contents = new StringStringMap(16, 0.2f); 087 garbleKey = Garbler.makeKeyArray(5, garble); 088 } 089 /** 090 * Creates a JsonStorage with the given fileName to save using Preferences from libGDX. The name should generally 091 * be the name of this game or application, and must be a valid name for a file (so no slashes, backslashes, colons, 092 * semicolons, or commas for certain, and other non-alphanumeric characters are also probably invalid). You should 093 * not assume anything is present in the Preferences storage unless you have put it there, and this applies doubly 094 * to games or applications other than your own; you should avoid values for fileName that might overlap with 095 * another game's Preferences values. This constructor also allows you to specify a "garble" long array; if this is 096 * non-empty, it will be used as a key to obfuscate the output and de-obfuscate the loaded input using fairly basic 097 * methods. If garble is null or empty, it is ignored. 098 * <br> 099 * To organize saved data into sub-sections, you specify logical units (like different players' saved games) with a 100 * String outerName when you call {@link #store(String)}, and can further distinguish data under the outerName when 101 * you call {@link #put(String, Object)} to put each individual item into the saved storage with its own innerName. 102 * <br> 103 * Calling this also sets up custom serializers for several important types in SquidLib; char[][], OrderedMap, 104 * IntDoubleOrderedMap, FakeLanguageGen, GreasedRegion, and notably Pattern from RegExodus all have smaller 105 * serialized representations than the default. OrderedMap allows non-String keys, which gets around a limitation in 106 * JSON maps normally, and both FakeLanguageGen and Pattern are amazingly smaller with the custom representation. 107 * The custom char[][] representation is about half the normal size by omitting commas after each char. 108 * @param fileName the valid file name to create or open from Preferences; typically the name of the game/app. 109 * @param garble a long array that will be used as a key to obfuscate the saved output if non-null 110 */ 111 public JsonStorage(final String fileName, final long[] garble) { 112 storageName = fileName; 113 preferences = Gdx.app.getPreferences(storageName); 114 json = new JsonConverter(JsonWriter.OutputType.minimal); 115 contents = new StringStringMap(16, 0.2f); 116 if (garble == null || garble.length == 0) 117 garbleKey = null; 118 else { 119 garbleKey = new long[garble.length]; 120 System.arraycopy(garble, 0, garbleKey, 0, garble.length); 121 } 122 } 123 124 /** 125 * Prepares to store the Object {@code o} to be retrieved with {@code innerName} in the current group of objects. 126 * Does not write to a permanent location until {@link #store(String)} is called. The innerName used to store an 127 * object is required to get it back again, and can also be used to remove it before storing (or storing again). 128 * @param innerName one of the two Strings needed to retrieve this later 129 * @param o the Object to prepare to store 130 * @return this for chaining 131 */ 132 public JsonStorage put(String innerName, Object o) 133 { 134 contents.put(innerName, json.toJson(o)); 135 return this; 136 } 137 138 /** 139 * Actually stores all objects that had previously been prepared with {@link #put(String, Object)}, with 140 * {@code outerName} used as a key to retrieve any object in the current group. Flushes the preferences, making the 141 * changes permanent (until overwritten), but does not change the current group (you may want to call this method 142 * again with additional items in the current group, and that would simply involve calling put() again). If you want 143 * to clear the current group, use {@link #clear()}. If you want to remove just one object from the current group, 144 * use {@link #remove(String)}. 145 * @param outerName one of the two Strings needed to retrieve any of the objects in the current group 146 * @return this for chaining 147 */ 148 public JsonStorage store(String outerName) 149 { 150 if(garbleKey == null) { 151 if (compress) 152 preferences.putString(outerName, LZSPlus.compress(json.toJson(contents, StringStringMap.class))); 153 else 154 preferences.putString(outerName, json.toJson(contents, StringStringMap.class)); 155 } 156 else 157 { 158 if (compress) 159 preferences.putString(outerName, LZSPlus.compress(json.toJson(contents, StringStringMap.class), garbleKey)); 160 else 161 preferences.putString(outerName, Garbler.garble(json.toJson(contents, StringStringMap.class), garbleKey)); 162 } 163 preferences.flush(); 164 return this; 165 } 166 167 /** 168 * Gets a String representation of the data that would be saved when {@link #store(String)} is called. This can be 169 * useful for finding particularly problematic objects that require unnecessary space when serialized. 170 * @return a String that previews what would be stored permanently when {@link #store(String)} is called 171 */ 172 public String show() { 173 if (garbleKey == null) { 174 if (compress) 175 return LZSPlus.compress(json.toJson(contents, StringStringMap.class)); 176 else 177 return json.toJson(contents, StringStringMap.class); 178 } 179 else 180 { 181 if (compress) 182 return LZSPlus.compress(json.toJson(contents, StringStringMap.class), garbleKey); 183 else 184 return Garbler.garble(json.toJson(contents, StringStringMap.class), garbleKey); 185 186 } 187 } 188 189 /** 190 * Clears the current group of objects; recommended if you intend to store under multiple outerName keys. 191 * @return this for chaining 192 */ 193 public JsonStorage clear() 194 { 195 contents.clear(); 196 return this; 197 } 198 199 /** 200 * Removes one object from the current group by the {@code innerName} it was prepared with using 201 * {@link #put(String, Object)}. This does not affect already-stored objects unless {@link #store(String)} is called 202 * after this, in which case the new version of the current group, without the object this removed, is stored. 203 * @param innerName the String key used to put an object in the current group with {@link #put(String, Object)} 204 * @return this for chaining 205 */ 206 public JsonStorage remove(String innerName) 207 { 208 contents.remove(innerName); 209 return this; 210 } 211 212 /** 213 * Gets an object from the storage by the given {@code outerName} key from {@link #store(String)} and 214 * {@code innerName} key from {@link #put(String, Object)}, and uses the class given by {@code type} for the 215 * returned value, assuming it matches the object that was originally put with those keys. If no such object is 216 * present, returns null. Results are undefined if {@code type} doesn't match the actual class of the stored object. 217 * @param outerName the key used to store the group of objects with {@link #store(String)} 218 * @param innerName the key used to store the specific object with {@link #put(String, Object)} 219 * @param type the class of the value; for a class like RNG, use {@code RNG.class}, but changed to fit 220 * @param <T> the type of the value to retrieve; if type was {@code RNG.class}, this would be {@code RNG} 221 * @return the retrieved value if successful, or null otherwise 222 */ 223 public <T> T get(String outerName, String innerName, Class<T> type) 224 { 225 StringStringMap om; 226 String got; 227 if(garbleKey == null) { 228 if (compress) 229 got = LZSPlus.decompress(preferences.getString(outerName)); 230 else 231 got = preferences.getString(outerName); 232 } 233 else 234 { 235 if (compress) 236 got = LZSPlus.decompress(preferences.getString(outerName), garbleKey); 237 else 238 got = Garbler.degarble(preferences.getString(outerName), garbleKey); 239 } 240 if(got == null) return null; 241 om = json.fromJson(StringStringMap.class, got); 242 if(om == null) return null; 243 return json.fromJson(type, om.get(innerName)); 244 } 245 246 /** 247 * Gets the approximate size of the currently-stored preferences. This assumes UTF-16 storage, which is the case for 248 * GWT's LocalStorage. Since GWT is restricted to the size the browser permits for LocalStorage, and this limit can 249 * be rather small (about 5 MB, sometimes more but not reliably), this method is especially useful there, but it may 250 * yield inaccurate sizes on other platforms that save Preferences data differently. 251 * @return the size, in bytes, of the already-stored preferences 252 */ 253 public int preferencesSize() 254 { 255 Map<String, ?> p = preferences.get(); 256 int byteSize = 0; 257 for(String k : p.keySet()) 258 { 259 byteSize += k.length(); 260 byteSize += preferences.getString(k, "").length(); 261 } 262 return byteSize * 2; 263 } 264 265}