Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consistent ordering of entries in jsondb #2437

Merged
merged 5 commits into from
Aug 8, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
package org.openhab.core.automation.internal.parser.gson;

import java.io.OutputStreamWriter;
import java.util.Map;
import java.util.Set;

import org.eclipse.jdt.annotation.NonNullByDefault;
Expand All @@ -23,6 +24,8 @@
import org.openhab.core.config.core.Configuration;
import org.openhab.core.config.core.ConfigurationDeserializer;
import org.openhab.core.config.core.ConfigurationSerializer;
import org.openhab.core.config.core.OrderingMapSerializer;
import org.openhab.core.config.core.OrderingSetSerializer;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
Expand All @@ -32,6 +35,7 @@
*
* @author Kai Kreuzer - Initial contribution
* @author Ana Dimova - add Instance Creators
* @author Sami Salonen - add sorting for maps and sets for minimal diffs
*
* @param <T> the type of the entities to parse
*/
Expand All @@ -45,6 +49,8 @@ public abstract class AbstractGSONParser<T> implements Parser<T> {
.registerTypeAdapter(CompositeTriggerType.class, new TriggerInstanceCreator()) //
.registerTypeAdapter(Configuration.class, new ConfigurationDeserializer()) //
.registerTypeAdapter(Configuration.class, new ConfigurationSerializer()) //
.registerTypeAdapter(Map.class, new OrderingMapSerializer()) //
.registerTypeAdapter(Set.class, new OrderingSetSerializer()) //
.create();

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,15 @@
*
* @author Yordan Mihaylov - Initial contribution
* @author Ana Dimova - provide serialization of multiple configuration values.
* @author Sami Salonen - property names are sorted for serialization for minimal diffs
*/
public class ConfigurationSerializer implements JsonSerializer<Configuration> {

@SuppressWarnings("unchecked")
@Override
public JsonElement serialize(Configuration src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject result = new JsonObject();
for (String propName : src.keySet()) {
src.keySet().stream().sorted().forEachOrdered((String propName) -> {
Object value = src.get(propName);
if (value instanceof List) {
JsonArray array = new JsonArray();
Expand All @@ -46,7 +47,7 @@ public JsonElement serialize(Configuration src, Type typeOfSrc, JsonSerializatio
} else {
result.add(propName, serializePrimitive(value));
}
}
});
return result;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.config.core;

import java.lang.reflect.Type;
import java.util.Comparator;
import java.util.Map;

import org.eclipse.jdt.annotation.NonNullByDefault;

import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;

/**
* Serializes map data by ordering the keys
*
* @author Sami Salonen - Initial contribution
*/
@NonNullByDefault
public class OrderingMapSerializer implements JsonSerializer<Map<String, Object>> {
ssalonen marked this conversation as resolved.
Show resolved Hide resolved

@Override
public JsonElement serialize(Map<String, Object> src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject ordered = new JsonObject();
src.entrySet().stream().sorted(Comparator.comparing(Map.Entry::getKey)).forEachOrdered(entry -> {
ordered.add(entry.getKey(), context.serialize(entry.getValue()));
});
return ordered;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.config.core;

import java.lang.reflect.Type;
import java.util.Set;

import org.eclipse.jdt.annotation.NonNullByDefault;

import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;

/**
* Serializes set by ordering the elements
*
* @author Sami Salonen - Initial contribution
*/
@NonNullByDefault
public class OrderingSetSerializer implements JsonSerializer<Set<Object>> {

@Override
public JsonElement serialize(Set<Object> src, Type typeOfSrc, JsonSerializationContext context) {
JsonArray ordered = new JsonArray();
src.stream().map(context::serialize).sorted().forEachOrdered(ordered::add);
ssalonen marked this conversation as resolved.
Show resolved Hide resolved
return ordered;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;
Expand All @@ -29,6 +30,8 @@
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.config.core.ConfigurationDeserializer;
import org.openhab.core.config.core.OrderingMapSerializer;
import org.openhab.core.config.core.OrderingSetSerializer;
import org.openhab.core.storage.Storage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -51,6 +54,8 @@
* @author Stefan Triller - Removed dependency to internal GSon packages
* @author Simon Kaufmann - Distinguish between inner and outer
* de-/serialization, keep json structures in map
* @author Sami Salonen - ordered inner and outer serialization of Maps,
* Sets and properties of Configuration
*/
@NonNullByDefault
public class JsonStorage<T> implements Storage<T> {
Expand Down Expand Up @@ -88,11 +93,19 @@ public JsonStorage(File file, @Nullable ClassLoader classLoader, int maxBackupFi
this.writeDelay = writeDelay;
this.maxDeferredPeriod = maxDeferredPeriod;

this.internalMapper = new GsonBuilder()
.registerTypeHierarchyAdapter(Map.class, new StorageEntryMapDeserializer()).setPrettyPrinting()
this.internalMapper = new GsonBuilder() //
.registerTypeHierarchyAdapter(Map.class, new OrderingMapSerializer()) //
.registerTypeAdapter(Set.class, new OrderingSetSerializer()) //
.registerTypeHierarchyAdapter(Configuration.class, new OrderingConfigurationSerializer()) //
.registerTypeHierarchyAdapter(Map.class, new StorageEntryMapDeserializer()) //
.setPrettyPrinting() //
.create();
this.entityMapper = new GsonBuilder() //
.registerTypeHierarchyAdapter(Map.class, new OrderingMapSerializer()) //
.registerTypeAdapter(Set.class, new OrderingSetSerializer()) //
.registerTypeAdapter(Configuration.class, new ConfigurationDeserializer()) //
.setPrettyPrinting() //
.create();
this.entityMapper = new GsonBuilder().registerTypeAdapter(Configuration.class, new ConfigurationDeserializer())
.setPrettyPrinting().create();

commitTimer = new Timer();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.core.storage.json.internal;

import java.lang.reflect.Type;
import java.util.Comparator;
import java.util.Map;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.config.core.Configuration;

import com.google.gson.JsonElement;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.google.gson.internal.LinkedTreeMap;

/**
* Serializes Configuration object with properties ordered
*
* @author Sami Salonen - Initial contribution
*/
@NonNullByDefault
public class OrderingConfigurationSerializer implements JsonSerializer<Configuration> {

@Override
public JsonElement serialize(Configuration src, Type typeOfSrc, JsonSerializationContext context) {
Map<String, Object> orderedProperties = new LinkedTreeMap<>();
src.getProperties().entrySet().stream().sorted(Comparator.comparing(Map.Entry::getKey))
.forEachOrdered(entry -> {
orderedProperties.put(entry.getKey(), entry.getValue());
});

return context.serialize(new Configuration(orderedProperties)); // XXX: re-normalizes unnecessarily
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
*/
package org.openhab.core.storage.json.internal;

import static org.junit.Assert.assertTrue;
import static org.junit.jupiter.api.Assertions.*;

import java.io.File;
Expand All @@ -26,10 +27,15 @@
import org.openhab.core.config.core.Configuration;
import org.openhab.core.test.java.JavaTest;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;

/**
* This test makes sure that the JsonStorage loads all stored numbers as BigDecimal
*
* @author Stefan Triller - Initial contribution
* @author Samie Salonen - test for ensuring ordering of keys in json
*/
public class JsonStorageTest extends JavaTest {

Expand Down Expand Up @@ -129,6 +135,48 @@ public void testStableOutput() throws IOException {
assertEquals(storageString1, storageString2);
}

@SuppressWarnings("null")
@Test
public void testOrdering() throws IOException {
objectStorage.put("DummyObject", new DummyObject());
{
objectStorage.put("a", new DummyObject());
objectStorage.put("b", new DummyObject());
persistAndReadAgain();
}
String storageStringAB = Files.readString(tmpFile.toPath());

{
objectStorage.remove("a");
objectStorage.remove("b");
objectStorage.put("b", new DummyObject());
objectStorage.put("a", new DummyObject());
persistAndReadAgain();
}
String storageStringBA = Files.readString(tmpFile.toPath());
assertEquals(storageStringAB, storageStringBA);

{
objectStorage = new JsonStorage<>(tmpFile, this.getClass().getClassLoader(), 0, 0, 0);
objectStorage.flush();
}
String storageStringReserialized = Files.readString(tmpFile.toPath());
assertEquals(storageStringAB, storageStringReserialized);
Gson gson = new GsonBuilder().create();

// Parse json. Gson preserves json object key ordering when we parse only JsonObject
JsonObject orderedMap = gson.fromJson(storageStringAB, JsonObject.class);
// Assert ordering of top level keys (uppercase first in alphabetical order, then lowercase items in
// alphabetical order)
assertArrayEquals(new String[] { "DummyObject", "a", "b" }, orderedMap.keySet().toArray());
// Ordering is ensured also for sub-keys of Configuration object
assertArrayEquals(
new String[] { "multiInt", "testBigDecimal", "testBoolean", "testDouble", "testFloat", "testInt",
"testLong", "testShort", "testString" },
orderedMap.getAsJsonObject("DummyObject").getAsJsonObject("value").getAsJsonObject("configuration")
.getAsJsonObject("properties").keySet().toArray());
}

private static class DummyObject {

private final Configuration configuration = new Configuration();
Expand Down