Save preferences JSON using UTF-8 encoding. Bulletproof prefs load. (#2657)

* Save preferences JSON using UTF-8 encoding. Bulletproof prefs load.

Fixes #2543. Fixes #2627.

Always use UTF-8 to write JSON because platform default encoding
might not be legal JSON (e.g. ISO 8859-1).

Also be more conservative about keeping backups if we fail to write.

* Handle case where backup prefs is better than more recent

* Recover from corrupted prefs with null starred list.

Fixes #2544. Replaces null with an empty list.

* Run tests with non-UTF-8 encoding

Make sure that we don't depend on UTF-8 being the default encoding
because it isn't true everywhere (e.g. Windows)

* Add test for non-ASCII chars in workspace.json

This depends on the default Java encoding being something
other than UTF-8 to test properly.
This commit is contained in:
Tom Morris 2020-06-06 05:00:01 -04:00 committed by GitHub
parent 5351e9f41c
commit e6ed8e5d62
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 50 additions and 14 deletions

View File

@ -40,6 +40,8 @@ import java.io.FileWriter;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map.Entry; import java.util.Map.Entry;
@ -307,13 +309,14 @@ public class FileProjectManager extends ProjectManager {
} }
protected boolean saveToFile(File file) throws IOException { protected boolean saveToFile(File file) throws IOException {
FileWriter writer = new FileWriter(file); OutputStream stream = new FileOutputStream(file);
boolean saveWasNeeded = saveNeeded(); boolean saveWasNeeded = saveNeeded();
try { try {
ParsingUtilities.defaultWriter.writeValue(writer, this); // writeValue(OutputStream) is documented to use JsonEncoding.UTF8
ParsingUtilities.defaultWriter.writeValue(stream, this);
saveProjectMetadata(); saveProjectMetadata();
} finally { } finally {
writer.close(); stream.close();
} }
return saveWasNeeded; return saveWasNeeded;
} }

View File

@ -39,6 +39,7 @@ import java.io.FileReader;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStreamWriter; import java.io.OutputStreamWriter;
import java.io.Writer; import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.time.Instant; import java.time.Instant;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneId; import java.time.ZoneId;
@ -60,25 +61,33 @@ public class ProjectMetadataUtilities {
public static void save(ProjectMetadata projectMeta, File projectDir) throws IOException { public static void save(ProjectMetadata projectMeta, File projectDir) throws IOException {
File tempFile = new File(projectDir, "metadata.temp.json"); File tempFile = new File(projectDir, "metadata.temp.json");
saveToFile(projectMeta, tempFile); saveToFile(projectMeta, tempFile);
if (tempFile.length() == 0) {
throw new IOException("Failed to save project metadata - keeping backups");
}
// TODO Do we want to make sure we can successfully deserialize the file too?
File file = new File(projectDir, "metadata.json"); File file = new File(projectDir, "metadata.json");
File oldFile = new File(projectDir, "metadata.old.json"); File oldFile = new File(projectDir, "metadata.old.json");
if (file.exists()) {
if(file.length() > 0) {
if (oldFile.exists()) { if (oldFile.exists()) {
oldFile.delete(); oldFile.delete();
} }
if (file.exists()) {
file.renameTo(oldFile); file.renameTo(oldFile);
} else {
file.delete();
}
} }
tempFile.renameTo(file); tempFile.renameTo(file);
} }
protected static void saveToFile(ProjectMetadata projectMeta, File metadataFile) throws IOException { protected static void saveToFile(ProjectMetadata projectMeta, File metadataFile) throws IOException {
Writer writer = new OutputStreamWriter(new FileOutputStream(metadataFile)); Writer writer = new OutputStreamWriter(new FileOutputStream(metadataFile), StandardCharsets.UTF_8);
try { try {
ParsingUtilities.defaultWriter.writeValue(writer, projectMeta); ParsingUtilities.saveWriter.writeValue(writer, projectMeta);
} finally { } finally {
writer.close(); writer.close();
} }
@ -121,7 +130,7 @@ public class ProjectMetadataUtilities {
* creation and modification times based on whatever files are available. * creation and modification times based on whatever files are available.
* *
* @param projectDir the project directory * @param projectDir the project directory
* @param id the proejct id * @param id the project id
* @return * @return
*/ */
static public ProjectMetadata recover(File projectDir, long id) { static public ProjectMetadata recover(File projectDir, long id) {

View File

@ -37,6 +37,7 @@ import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.logging.Logger;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
@ -100,6 +101,12 @@ public class PreferenceStore {
if (entries.get(key) != null) { if (entries.get(key) != null) {
JsonNode o = entries.get(key); JsonNode o = entries.get(key);
Object loaded = loadObject(o); Object loaded = loadObject(o);
if (loaded == null) {
if ("scripting.starred-expressions".contentEquals(key)) {
// HACK to work around preferences corruption
loaded = new TopList(10);
}
}
_prefs.put(key, loaded); _prefs.put(key, loaded);
} }
} }

View File

@ -27,6 +27,7 @@
package com.google.refine.io; package com.google.refine.io;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.testng.Assert.assertEquals;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@ -50,7 +51,7 @@ public class FileProjectManagerTests {
protected FileProjectManagerStub(File dir) { protected FileProjectManagerStub(File dir) {
super(dir); super(dir);
_projectsMetadata.put(1234L, mock(ProjectMetadata.class)); _projectsMetadata.put(5555L, mock(ProjectMetadata.class));
} }
} }
@ -72,8 +73,24 @@ public class FileProjectManagerTests {
" }\n" + " }\n" +
" }\n" + " }\n" +
" },\n" + " },\n" +
" \"projectIDs\" : [ 1234 ]\n" + " \"projectIDs\" : [ 5555 ]\n" +
" }"; " }";
TestUtils.isSerializedTo(manager, json); TestUtils.isSerializedTo(manager, json);
}
/**
* Test that we can save and restore non-ASCII characters.
* For best effectiveness, this should be run with a non-UTF8
* default encoding for Java e.g. java -Dfile.encoding=cp1252
* to simulate running on a Windows system
*/
@Test
public void saveReloadMultinationalCharacter () throws IOException {
FileProjectManager manager = new FileProjectManagerStub(workspaceDir);
manager.getPreferenceStore().put("testPref", "Refiné");
manager.saveWorkspace();
manager = new FileProjectManagerStub(workspaceDir);
assertEquals(manager.getPreferenceStore().get("testPref"), "Refiné");
} }
} }

View File

@ -57,7 +57,7 @@
<jee.port>3333</jee.port> <jee.port>3333</jee.port>
<refine.data>/tmp/refine</refine.data> <refine.data>/tmp/refine</refine.data>
<surefire.version>2.22.2</surefire.version> <surefire.version>2.22.2</surefire.version>
<surefireArgs></surefireArgs> <surefireArgs>-Dfile.encoding=cp1252</surefireArgs>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<jackson.version>2.11.0</jackson.version> <jackson.version>2.11.0</jackson.version>
</properties> </properties>