From 06eb74a162f310bd04ee50a403de827ac34cc986 Mon Sep 17 00:00:00 2001 From: Karoliine Holter Date: Wed, 6 Mar 2024 21:26:35 +0200 Subject: [PATCH] Add TypeAdapterFactory for validating Gobpie configuration --- .../GobPieConfValidatorAdapterFactory.java | 79 +++++++++++++++++++ src/main/java/gobpie/GobPieConfReader.java | 13 ++- src/test/java/GobPieConfTest.java | 18 +---- 3 files changed, 90 insertions(+), 20 deletions(-) create mode 100644 src/main/java/api/json/GobPieConfValidatorAdapterFactory.java diff --git a/src/main/java/api/json/GobPieConfValidatorAdapterFactory.java b/src/main/java/api/json/GobPieConfValidatorAdapterFactory.java new file mode 100644 index 0000000..5a4a46d --- /dev/null +++ b/src/main/java/api/json/GobPieConfValidatorAdapterFactory.java @@ -0,0 +1,79 @@ +package api.json; + +import com.google.gson.Gson; +import com.google.gson.JsonParseException; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.internal.bind.ReflectiveTypeAdapterFactory; +import com.google.gson.reflect.TypeToken; + +import java.lang.reflect.Field; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * A class for handling unexpected fields in GobPie configuration. + * Code adapted from a Gson library issue. + * + * @since 0.0.4 + */ + +public class GobPieConfValidatorAdapterFactory implements TypeAdapterFactory { + + @Override + public TypeAdapter create(Gson gson, TypeToken type) { + // If the type adapter is a reflective type adapter, we want to modify the implementation using reflection. + // The trick is to replace the Map object used to look up the property name. + // Instead of returning null if the property is not found, + // we throw a Json exception to terminate the deserialization. + TypeAdapter delegateAdapter = gson.getDelegateAdapter(this, type); + + // Check if the type adapter is a reflective, cause this solution only work for reflection. + if (delegateAdapter instanceof ReflectiveTypeAdapterFactory.Adapter) { + + try { + // Get reference to the existing boundFields. + Field f = findBoundField(delegateAdapter.getClass()); + f.setAccessible(true); + // Finally, push our custom map back using reflection. + f.set(delegateAdapter, getBoundFields(f, delegateAdapter)); + } catch (Exception e) { + // Should never happen if the implementation doesn't change. + throw new IllegalStateException(e); + } + + } + return delegateAdapter; + } + + @SuppressWarnings("unchecked") + private static Object getBoundFields(Field f, TypeAdapter delegate) throws IllegalAccessException { + Object boundFields = f.get(delegate); + + // Then replace it with our implementation throwing exception if the value is null. + boundFields = new LinkedHashMap<>((Map) boundFields) { + + @Override + public Object get(Object key) { + Object value = super.get(key); + if (value == null) { + throw new JsonParseException(String.valueOf(key)); + } + return value; + } + + }; + return boundFields; + } + + private static Field findBoundField(Class startingClass) throws NoSuchFieldException { + for (Class c = startingClass; c != null; c = c.getSuperclass()) { + try { + return c.getDeclaredField("boundFields"); + } catch (NoSuchFieldException e) { + // OK: continue with superclasses + } + } + throw new NoSuchFieldException("boundFields starting from " + (startingClass != null ? startingClass.getName() : null)); + } +} diff --git a/src/main/java/gobpie/GobPieConfReader.java b/src/main/java/gobpie/GobPieConfReader.java index a9d89aa..65552fd 100644 --- a/src/main/java/gobpie/GobPieConfReader.java +++ b/src/main/java/gobpie/GobPieConfReader.java @@ -1,5 +1,6 @@ package gobpie; +import api.json.GobPieConfValidatorAdapterFactory; import com.google.gson.*; import magpiebridge.core.MagpieServer; import org.apache.commons.lang3.exception.ExceptionUtils; @@ -93,21 +94,25 @@ public GobPieConfiguration readGobPieConfiguration() { public GobPieConfiguration parseGobPieConf() { try { log.debug("Reading GobPie configuration from json"); - Gson gson = new GsonBuilder().create(); + Gson gson = new GsonBuilder() + .registerTypeAdapterFactory(new GobPieConfValidatorAdapterFactory()) + .create(); // Read json object JsonObject jsonObject = JsonParser.parseReader(new FileReader(gobPieConfFileName)).getAsJsonObject(); // Convert json object to GobPieConfiguration object log.debug("GobPie configuration read from json"); return gson.fromJson(jsonObject, GobPieConfiguration.class); - } catch (FileNotFoundException e) { - throw new GobPieException("Could not locate GobPie configuration file.", e, GobPieExceptionType.GOBPIE_CONF_EXCEPTION); } catch (JsonSyntaxException e) { throw new GobPieException("GobPie configuration file syntax is wrong.", e, GobPieExceptionType.GOBPIE_CONF_EXCEPTION); + } catch (JsonParseException e) { + throw new GobPieException("There was an unknown option \"" + e.getMessage() + "\" in the GobPie configuration. Please check for any typos.", e, GobPieExceptionType.GOBPIE_CONF_EXCEPTION); + } catch (FileNotFoundException e) { + throw new GobPieException("Could not locate GobPie configuration file.", e, GobPieExceptionType.GOBPIE_CONF_EXCEPTION); } } -/** + /** * Method for forwarding Error messages to MagpieServer. * * @param popUpMessage The message shown on the pop-up message. diff --git a/src/test/java/GobPieConfTest.java b/src/test/java/GobPieConfTest.java index 5eec878..1675b79 100644 --- a/src/test/java/GobPieConfTest.java +++ b/src/test/java/GobPieConfTest.java @@ -137,22 +137,8 @@ void testReadGobPieConfigurationWithWrongJSONSyntax() { @Test void testReadGobPieConfigurationWithExtraField() { GobPieConfReader gobPieConfReader = preFileSetup(5); - GobPieConfiguration expectedGobPieConfiguration = - new GobPieConfiguration.Builder() - .setGoblintConf("goblint.json") - .setGoblintExecutable("/home/user/goblint/analyzer/goblint") - .setPreAnalyzeCommand(new String[]{"cmake", "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON", "-B", "build"}) - .setAbstractDebugging(false) - .setShowCfg(false) - .setIncrementalAnalysis(true) - .setExplodeGroupWarnings(false) - .createGobPieConfiguration(); - - GobPieConfiguration actualGobPieConfiguration = gobPieConfReader.readGobPieConfiguration(); - assertTrue(systemOut.getLines().anyMatch(line -> line.contains("Reading GobPie configuration from json"))); - verify(magpieServer).forwardMessageToClient(new MessageParams(MessageType.Error, "There was an unknown option in the GobPie configuration. Please check for any typos.")); - //assertTrue(systemOut.getLines().anyMatch(line -> line.contains("GobPie configuration read from json"))); - assertEquals(expectedGobPieConfiguration, actualGobPieConfiguration); + GobPieException thrown = assertThrows(GobPieException.class, gobPieConfReader::readGobPieConfiguration); + assertEquals("There was an unknown option \"extraField\" in the GobPie configuration. Please check for any typos.", thrown.getMessage()); } /**