Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion sdk-java/src/main/java/ly/count/sdk/java/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -1533,7 +1533,11 @@ public Config disableAutoSendUserProperties() {
* Provide an initial SDK behavior settings payload to apply at init time,
* before the first {@code /o/sdk?method=sc} fetch returns. The value is
* the same JSON shape the Countly server emits: {@code {"v":1,"t":<ts>,
* "c":{...}}}. The seed is only used when nothing has been persisted yet.
* "c":{...}}}. The seed is only used when nothing has been persisted yet,
* and it stands in for the server's settings: the first answer from the
* server replaces it, so a setting the answer does not carry goes back to
* this configuration. A {@code "cr":true} here can be lifted by that answer;
* use {@link #setRequiresConsent(boolean)} for a requirement that must hold.
*
* @param sdkBehaviorSettings serialized config JSON, or {@code null} for none
* @return {@code this} instance for method chaining
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import javax.annotation.Nonnull;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

public class JsonFileStorage {
Expand All @@ -22,9 +25,22 @@ public class JsonFileStorage {
public JsonFileStorage(@Nonnull final File file, @Nonnull Log logger) {
this.logger = logger;
this.file = file;
deleteLeftoverTemporaryFile();
this.json = readJsonFile(file);
}

/**
* Deletes the temporary file of a save that was stopped before its move. The json file still
* holds the previous save then, and a leftover file would be taken for other stored SDK data.
*/
private void deleteLeftoverTemporaryFile() {
try {
Files.deleteIfExists(temporaryFileOf(file).toPath());
} catch (IOException e) {
logger.w("[JsonFileStorage] deleteLeftoverTemporaryFile, Failed to delete the temporary file, reason: [" + e + "]");
}
}

/**
* If key exists changes its value, otherwise adds new key-value pair
* Null key or value is not allowed
Expand All @@ -39,26 +55,56 @@ public synchronized void add(@Nonnull final String key, @Nonnull Object value) {
}

/**
* Saves changes to the disk/db/memory. When the data cannot be serialized, the file keeps what
* it had.
* Saves changes to the disk/db/memory. The data is written next to the file and then moved over
* it, so a process stopped while saving leaves the previous file whole. When the data cannot be
* serialized, the file keeps what it had.
*/
public synchronized void save() {
logger.i("[JsonFileStorage] save, Saving json file: [" + file.getAbsolutePath() + "]");

// Serialized before the file is opened: opening truncates it, and toString returns null instead of throwing
// toString returns null instead of throwing, so a failed serialization has to be caught before anything is written
String content = json.toString();
if (content == null) {
logger.e("[JsonFileStorage] save, Failed to serialize the data, the json file is left as it is: [" + file.getAbsolutePath() + "]");
return;
}

try (BufferedWriter writer = Files.newBufferedWriter(file.toPath())) {
writer.write(content);
File temporary = temporaryFileOf(file);
try {
try (BufferedWriter writer = Files.newBufferedWriter(temporary.toPath())) {
writer.write(content);
}
replaceFile(temporary, file);
} catch (IOException e) {
logger.e("[JsonFileStorage] save, Failed to save json file, reason: [" + e.getMessage() + "]");
}
}

/**
* The file a save writes before moving it over the json file.
*
* @param file the json file
* @return the temporary file next to it
*/
static File temporaryFileOf(@Nonnull File file) {
return new File(file.getAbsolutePath() + ".tmp");
}

/**
* Moves a file over another, atomically where the file system supports it.
*
* @param source the file to move
* @param target the file it replaces
* @throws IOException when the move fails
*/
private static void replaceFile(@Nonnull File source, @Nonnull File target) throws IOException {
try {
Files.move(source.toPath(), target.toPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException e) {
Files.move(source.toPath(), target.toPath(), StandardCopyOption.REPLACE_EXISTING);
}
}

/**
* Removes key-value pair
* Null key is not allowed
Expand Down Expand Up @@ -225,6 +271,9 @@ private JSONObject readJsonFile(@Nonnull final File file) {
} catch (IOException e) {
logger.e("[JsonFileStorage] readJsonFile, Failed to read json file, reason: [" + e.getMessage() + "]");
return new JSONObject();
} catch (JSONException e) {
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() + "]");
return new JSONObject();
}
}
}
Loading