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