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}