33import java .io .BufferedWriter ;
44import java .io .File ;
55import java .io .IOException ;
6+ import java .nio .file .AtomicMoveNotSupportedException ;
67import java .nio .file .Files ;
8+ import java .nio .file .StandardCopyOption ;
79import javax .annotation .Nonnull ;
810import org .json .JSONArray ;
11+ import org .json .JSONException ;
912import org .json .JSONObject ;
1013
1114public class JsonFileStorage {
@@ -22,9 +25,22 @@ public class JsonFileStorage {
2225 public JsonFileStorage (@ Nonnull final File file , @ Nonnull Log logger ) {
2326 this .logger = logger ;
2427 this .file = file ;
28+ deleteLeftoverTemporaryFile ();
2529 this .json = readJsonFile (file );
2630 }
2731
32+ /**
33+ * Deletes the temporary file of a save that was stopped before its move. The json file still
34+ * holds the previous save then, and a leftover file would be taken for other stored SDK data.
35+ */
36+ private void deleteLeftoverTemporaryFile () {
37+ try {
38+ Files .deleteIfExists (temporaryFileOf (file ).toPath ());
39+ } catch (IOException e ) {
40+ logger .w ("[JsonFileStorage] deleteLeftoverTemporaryFile, Failed to delete the temporary file, reason: [" + e + "]" );
41+ }
42+ }
43+
2844 /**
2945 * If key exists changes its value, otherwise adds new key-value pair
3046 * Null key or value is not allowed
@@ -39,26 +55,56 @@ public synchronized void add(@Nonnull final String key, @Nonnull Object value) {
3955 }
4056
4157 /**
42- * Saves changes to the disk/db/memory. When the data cannot be serialized, the file keeps what
43- * it had.
58+ * Saves changes to the disk/db/memory. The data is written next to the file and then moved over
59+ * it, so a process stopped while saving leaves the previous file whole. When the data cannot be
60+ * serialized, the file keeps what it had.
4461 */
4562 public synchronized void save () {
4663 logger .i ("[JsonFileStorage] save, Saving json file: [" + file .getAbsolutePath () + "]" );
4764
48- // Serialized before the file is opened: opening truncates it, and toString returns null instead of throwing
65+ // toString returns null instead of throwing, so a failed serialization has to be caught before anything is written
4966 String content = json .toString ();
5067 if (content == null ) {
5168 logger .e ("[JsonFileStorage] save, Failed to serialize the data, the json file is left as it is: [" + file .getAbsolutePath () + "]" );
5269 return ;
5370 }
5471
55- try (BufferedWriter writer = Files .newBufferedWriter (file .toPath ())) {
56- writer .write (content );
72+ File temporary = temporaryFileOf (file );
73+ try {
74+ try (BufferedWriter writer = Files .newBufferedWriter (temporary .toPath ())) {
75+ writer .write (content );
76+ }
77+ replaceFile (temporary , file );
5778 } catch (IOException e ) {
5879 logger .e ("[JsonFileStorage] save, Failed to save json file, reason: [" + e .getMessage () + "]" );
5980 }
6081 }
6182
83+ /**
84+ * The file a save writes before moving it over the json file.
85+ *
86+ * @param file the json file
87+ * @return the temporary file next to it
88+ */
89+ static File temporaryFileOf (@ Nonnull File file ) {
90+ return new File (file .getAbsolutePath () + ".tmp" );
91+ }
92+
93+ /**
94+ * Moves a file over another, atomically where the file system supports it.
95+ *
96+ * @param source the file to move
97+ * @param target the file it replaces
98+ * @throws IOException when the move fails
99+ */
100+ private static void replaceFile (@ Nonnull File source , @ Nonnull File target ) throws IOException {
101+ try {
102+ Files .move (source .toPath (), target .toPath (), StandardCopyOption .REPLACE_EXISTING , StandardCopyOption .ATOMIC_MOVE );
103+ } catch (AtomicMoveNotSupportedException e ) {
104+ Files .move (source .toPath (), target .toPath (), StandardCopyOption .REPLACE_EXISTING );
105+ }
106+ }
107+
62108 /**
63109 * Removes key-value pair
64110 * Null key is not allowed
@@ -225,6 +271,9 @@ private JSONObject readJsonFile(@Nonnull final File file) {
225271 } catch (IOException e ) {
226272 logger .e ("[JsonFileStorage] readJsonFile, Failed to read json file, reason: [" + e .getMessage () + "]" );
227273 return new JSONObject ();
274+ } catch (JSONException e ) {
275+ logger .e ("[JsonFileStorage] readJsonFile, The json file is not valid json, starting with empty storage that replaces it on the next save: [" + file .getAbsolutePath () + "], reason: [" + e .getMessage () + "]" );
276+ return new JSONObject ();
228277 }
229278 }
230279}
0 commit comments