diff --git a/.gitignore b/.gitignore index 8fcbbb5..c7385f2 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,8 @@ dependency-reduced-pom.xml *.ipr *.iws *.idea -*.log \ No newline at end of file +*.log + +# Mac OS + +.DS_Store diff --git a/src/main/java/com/taboola/backstage/Backstage.java b/src/main/java/com/taboola/backstage/Backstage.java index cbecd3f..5e1843c 100644 --- a/src/main/java/com/taboola/backstage/Backstage.java +++ b/src/main/java/com/taboola/backstage/Backstage.java @@ -2,6 +2,7 @@ import com.taboola.backstage.internal.*; import com.taboola.backstage.internal.config.CommunicationConfig; +import com.taboola.backstage.internal.config.SerializationConfig; import com.taboola.backstage.internal.factories.BackstageEndpointsFactory; import com.taboola.backstage.internal.factories.BackstageEndpointsRetrofitFactory; import com.taboola.backstage.internal.BackstageInternalTools; @@ -16,7 +17,7 @@ * {@code * Backstage backstage = Backstage.builder().build(); * } - * + * * * Example : getting all {@link com.taboola.backstage.model.media.campaigns.Campaign Campaigns} that belong to my account id *
@@ -135,6 +136,7 @@ public static class BackstageBuilder {
         private static final String VERSION = "1.0.10";
         private static final Integer DEFAULT_MAX_IDLE_CONNECTIONS = 5;
         private static final Long DEFAULT_KEEP_ALIVE_DURATION_MILLIS = 300_000L;
+        private static final SerializationConfig DEFAULT_SERIALIZATION_CONFIG = new SerializationConfig();
 
         private String baseUrl;
         private String authBaseUrl;
@@ -147,6 +149,7 @@ public static class BackstageBuilder {
         private Boolean performClientValidations;
         private Boolean debug;
         private Boolean organizeDynamicColumns;
+        private SerializationConfig serializationConfig;
 
         public BackstageBuilder setBaseUrl(String baseUrl) {
             this.baseUrl = baseUrl;
@@ -203,70 +206,79 @@ public BackstageBuilder setOrganizeDynamicColumns(Boolean organizeDynamicColumns
             return this;
         }
 
+        public BackstageBuilder setSerializationConfig(SerializationConfig serializationConfig) {
+            this.serializationConfig = serializationConfig;
+            return this;
+        }
+
         public Backstage build() {
             organizeState();
             String finalUserAgent = String.format("Backstage/%s (%s)", VERSION, userAgent);
             CommunicationConfig config = new CommunicationConfig(baseUrl, authBaseUrl, connectionTimeoutMillis, readTimeoutMillis, writeTimeoutMillis, maxIdleConnections,
                     keepAliveDurationMillis, finalUserAgent, debug);
-            CommunicationFactory communicator = new CommunicationFactory(config);
+            CommunicationFactory communicator = new CommunicationFactory(config, serializationConfig);
             BackstageEndpointsFactory endpointsFactory = new BackstageEndpointsRetrofitFactory(communicator);
             return new Backstage(
-                new BackstageInternalToolsImpl(endpointsFactory),
-                new CampaignsServiceImpl(performClientValidations, endpointsFactory.createEndpoint(BackstageCampaignsEndpoint.class)),
-                new AuthenticationServiceImpl(endpointsFactory.createAuthEndpoint(BackstageAuthenticationEndpoint.class)),
-                new UserServiceImpl(endpointsFactory.createEndpoint(BackstageAccountEndpoint.class)),
-                new CampaignItemsServiceImpl(performClientValidations, endpointsFactory.createEndpoint(BackstageCampaignItemsEndpoint.class)),
-                new DictionaryServiceImpl(endpointsFactory.createEndpoint(BackstageDictionaryEndpoint.class)),
-                new ReportsServiceImpl(endpointsFactory.createEndpoint(BackstageMediaReportsEndpoint.class), endpointsFactory.createEndpoint(BackstagePublisherReportsEndpoint.class), organizeDynamicColumns),
-                new AccountsServiceImpl(endpointsFactory.createEndpoint(BackstageAccountEndpoint.class)),
-                new CampaignPostalTargetingServiceImpl(performClientValidations, endpointsFactory.createEndpoint(BackstagePostalTargetingEndpoint.class))
+                    new BackstageInternalToolsImpl(endpointsFactory),
+                    new CampaignsServiceImpl(performClientValidations, endpointsFactory.createEndpoint(BackstageCampaignsEndpoint.class)),
+                    new AuthenticationServiceImpl(endpointsFactory.createAuthEndpoint(BackstageAuthenticationEndpoint.class)),
+                    new UserServiceImpl(endpointsFactory.createEndpoint(BackstageAccountEndpoint.class)),
+                    new CampaignItemsServiceImpl(performClientValidations, endpointsFactory.createEndpoint(BackstageCampaignItemsEndpoint.class)),
+                    new DictionaryServiceImpl(endpointsFactory.createEndpoint(BackstageDictionaryEndpoint.class)),
+                    new ReportsServiceImpl(endpointsFactory.createEndpoint(BackstageMediaReportsEndpoint.class), endpointsFactory.createEndpoint(BackstagePublisherReportsEndpoint.class), organizeDynamicColumns),
+                    new AccountsServiceImpl(endpointsFactory.createEndpoint(BackstageAccountEndpoint.class)),
+                    new CampaignPostalTargetingServiceImpl(performClientValidations, endpointsFactory.createEndpoint(BackstagePostalTargetingEndpoint.class))
             );
         }
 
         private void organizeState() {
-            if(baseUrl == null) {
+            if (baseUrl == null) {
                 baseUrl = DEFAULT_BACKSTAGE_HOST;
             }
 
-            if(authBaseUrl == null) {
+            if (authBaseUrl == null) {
                 authBaseUrl = DEFAULT_AUTH_BACKSTAGE_HOST;
             }
 
-            if(connectionTimeoutMillis == null) {
+            if (connectionTimeoutMillis == null) {
                 connectionTimeoutMillis = 0L;
             }
 
-            if(readTimeoutMillis == null) {
+            if (readTimeoutMillis == null) {
                 readTimeoutMillis = 0L;
             }
 
-            if(writeTimeoutMillis == null) {
+            if (writeTimeoutMillis == null) {
                 writeTimeoutMillis = 0L;
             }
 
-            if(maxIdleConnections == null) {
+            if (maxIdleConnections == null) {
                 maxIdleConnections = DEFAULT_MAX_IDLE_CONNECTIONS;
             }
 
-            if(keepAliveDurationMillis == null) {
+            if (keepAliveDurationMillis == null) {
                 keepAliveDurationMillis = DEFAULT_KEEP_ALIVE_DURATION_MILLIS;
             }
 
-            if(userAgent == null) {
+            if (userAgent == null) {
                 userAgent = DEFAULT_USER_AGENT;
             }
 
-            if(performClientValidations == null) {
+            if (performClientValidations == null) {
                 performClientValidations = true;
             }
 
-            if(debug == null) {
+            if (debug == null) {
                 debug = false;
             }
 
-            if(organizeDynamicColumns == null) {
+            if (organizeDynamicColumns == null) {
                 organizeDynamicColumns = true;
             }
+
+            if (serializationConfig == null) {
+                serializationConfig = DEFAULT_SERIALIZATION_CONFIG;
+            }
         }
     }
 }
diff --git a/src/main/java/com/taboola/backstage/internal/CommunicationFactory.java b/src/main/java/com/taboola/backstage/internal/CommunicationFactory.java
index 453853c..d990d6a 100644
--- a/src/main/java/com/taboola/backstage/internal/CommunicationFactory.java
+++ b/src/main/java/com/taboola/backstage/internal/CommunicationFactory.java
@@ -1,16 +1,15 @@
 package com.taboola.backstage.internal;
 
-import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.databind.DeserializationFeature;
 import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.PropertyNamingStrategy;
 import com.taboola.backstage.internal.config.CommunicationConfig;
+import com.taboola.backstage.internal.config.SerializationConfig;
 import com.taboola.backstage.internal.interceptors.CommunicationInterceptor;
 import com.taboola.backstage.internal.interceptors.UserAgentInterceptor;
+import com.taboola.backstage.internal.serialization.SerializationMapperCreator;
+
 import okhttp3.ConnectionPool;
 import okhttp3.OkHttpClient;
 import okhttp3.logging.HttpLoggingInterceptor;
-
 import retrofit2.Retrofit;
 import retrofit2.converter.jackson.JacksonConverterFactory;
 
@@ -28,21 +27,12 @@ public final class CommunicationFactory {
     private final Retrofit retrofit;
     private final Retrofit authRetrofit;
 
-    public CommunicationFactory(CommunicationConfig config) {
-        this.objectMapper = createObjectMapper();
-
-        Retrofit.Builder retrofitBuilder = createRetrofitBuilder(config);
-
-        this.authRetrofit = retrofitBuilder.baseUrl(config.getAuthenticationBaseUrl()).build();
-        this.retrofit = retrofitBuilder.baseUrl(config.getBackstageBaseUrl()).build();
-    }
+    public CommunicationFactory(CommunicationConfig communicationConfig, SerializationConfig serializationConfig) {
+        this.objectMapper = SerializationMapperCreator.createObjectMapper(serializationConfig);
+        Retrofit.Builder retrofitBuilder = createRetrofitBuilder(communicationConfig);
 
-    private ObjectMapper createObjectMapper() {
-        ObjectMapper objectMapper = new ObjectMapper();
-        objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);
-        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
-        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
-        return objectMapper;
+        this.authRetrofit = retrofitBuilder.baseUrl(communicationConfig.getAuthenticationBaseUrl()).build();
+        this.retrofit = retrofitBuilder.baseUrl(communicationConfig.getBackstageBaseUrl()).build();
     }
 
     private HttpLoggingInterceptor createLoggingInterceptor(CommunicationConfig config) {
diff --git a/src/main/java/com/taboola/backstage/internal/config/SerializationConfig.java b/src/main/java/com/taboola/backstage/internal/config/SerializationConfig.java
new file mode 100644
index 0000000..c48c885
--- /dev/null
+++ b/src/main/java/com/taboola/backstage/internal/config/SerializationConfig.java
@@ -0,0 +1,40 @@
+package com.taboola.backstage.internal.config;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class SerializationConfig {
+    private Map, Class> mixins;
+    private boolean shouldIgnoreAnySetterAnnotation;
+
+    public SerializationConfig() {
+        mixins = new HashMap<>();
+        shouldIgnoreAnySetterAnnotation = false;
+    }
+
+    public SerializationConfig setMixins(Map, Class> mixins) {
+        this.mixins = mixins;
+        return this;
+    }
+
+    public SerializationConfig setShouldIgnoreAnySetterAnnotation() {
+        this.shouldIgnoreAnySetterAnnotation = true;
+        return this;
+    }
+
+    public Map, Class> getMixins() {
+        return mixins;
+    }
+
+    public boolean shouldIgnoreAnySetterAnnotation() {
+        return shouldIgnoreAnySetterAnnotation;
+    }
+
+    @Override
+    public String toString() {
+        return "SerializationConfig{" +
+                "mixins=" + mixins +
+                ", shouldIgnoreAnySetterAnnotation=" + shouldIgnoreAnySetterAnnotation +
+                '}';
+    }
+}
diff --git a/src/main/java/com/taboola/backstage/internal/serialization/IgnoreAnySetterSerializationIntrospector.java b/src/main/java/com/taboola/backstage/internal/serialization/IgnoreAnySetterSerializationIntrospector.java
new file mode 100644
index 0000000..8d52bb4
--- /dev/null
+++ b/src/main/java/com/taboola/backstage/internal/serialization/IgnoreAnySetterSerializationIntrospector.java
@@ -0,0 +1,17 @@
+package com.taboola.backstage.internal.serialization;
+
+import com.fasterxml.jackson.databind.introspect.Annotated;
+import com.fasterxml.jackson.databind.introspect.AnnotatedMethod;
+import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;
+
+public class IgnoreAnySetterSerializationIntrospector extends JacksonAnnotationIntrospector {
+    @Override
+    public boolean hasAnySetterAnnotation(AnnotatedMethod am) {
+        return false;
+    }
+
+    @Override
+    public Boolean hasAnySetter(Annotated a) {
+        return false;
+    }
+}
diff --git a/src/main/java/com/taboola/backstage/internal/serialization/SerializationMapperCreator.java b/src/main/java/com/taboola/backstage/internal/serialization/SerializationMapperCreator.java
new file mode 100644
index 0000000..36f07ba
--- /dev/null
+++ b/src/main/java/com/taboola/backstage/internal/serialization/SerializationMapperCreator.java
@@ -0,0 +1,23 @@
+package com.taboola.backstage.internal.serialization;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.taboola.backstage.internal.config.SerializationConfig;
+
+public class SerializationMapperCreator {
+    public static ObjectMapper createObjectMapper(SerializationConfig serializationConfig) {
+        ObjectMapper objectMapper = new ObjectMapper();
+        objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);
+        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
+        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+        serializationConfig.getMixins().forEach(objectMapper::addMixIn);
+
+        if (serializationConfig.shouldIgnoreAnySetterAnnotation()) {
+            objectMapper.setAnnotationIntrospector(new IgnoreAnySetterSerializationIntrospector());
+        }
+
+        return objectMapper;
+    }
+}
diff --git a/src/test/java/com/taboola/backstage/internal/CommunicationFactoryTest.java b/src/test/java/com/taboola/backstage/internal/CommunicationFactoryTest.java
index cd2d632..5625b50 100644
--- a/src/test/java/com/taboola/backstage/internal/CommunicationFactoryTest.java
+++ b/src/test/java/com/taboola/backstage/internal/CommunicationFactoryTest.java
@@ -1,6 +1,7 @@
 package com.taboola.backstage.internal;
 
 import com.taboola.backstage.internal.config.CommunicationConfig;
+import com.taboola.backstage.internal.config.SerializationConfig;
 import org.junit.Assert;
 import org.junit.Before;
 import com.taboola.backstage.BackstageTestBase;
@@ -18,8 +19,9 @@ public class CommunicationFactoryTest extends BackstageTestBase {
 
     @Before
     public void beforeTest() {
-        CommunicationConfig config = new CommunicationConfig("http://localhost", "http://localhost", 1L, 1L, 1L, 1, 60L, "Dummy-Agent", true);
-        testInstance = new CommunicationFactory(config);
+        CommunicationConfig communicationConfig = new CommunicationConfig("http://localhost", "http://localhost", 1L, 1L, 1L, 1, 60L, "Dummy-Agent", true);
+        SerializationConfig serializationConfig = new SerializationConfig();
+        testInstance = new CommunicationFactory(communicationConfig, serializationConfig);
     }
 
     @Test
diff --git a/src/test/java/com/taboola/backstage/internal/config/SerializationConfigTest.java b/src/test/java/com/taboola/backstage/internal/config/SerializationConfigTest.java
new file mode 100644
index 0000000..82c2d02
--- /dev/null
+++ b/src/test/java/com/taboola/backstage/internal/config/SerializationConfigTest.java
@@ -0,0 +1,15 @@
+package com.taboola.backstage.internal.config;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class SerializationConfigTest {
+
+    @Test
+    public void serializationConfig_defaultConstructor_configInitializedWithDefaultValues() {
+        SerializationConfig serializationConfig = new SerializationConfig();
+        Assert.assertNotNull("Missing mixin", serializationConfig.getMixins());
+        Assert.assertEquals("Mixin is not empty by default", 0, serializationConfig.getMixins().size());
+        Assert.assertFalse("Wrong default value for shouldIgnoreAnySetterAnnotation", serializationConfig.shouldIgnoreAnySetterAnnotation());
+    }
+}
diff --git a/src/test/java/com/taboola/backstage/internal/serialization/SerializationMapperCreatorTest.java b/src/test/java/com/taboola/backstage/internal/serialization/SerializationMapperCreatorTest.java
new file mode 100644
index 0000000..937860f
--- /dev/null
+++ b/src/test/java/com/taboola/backstage/internal/serialization/SerializationMapperCreatorTest.java
@@ -0,0 +1,88 @@
+package com.taboola.backstage.internal.serialization;
+
+import com.fasterxml.jackson.annotation.JsonAnySetter;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.taboola.backstage.internal.config.SerializationConfig;
+import com.taboola.backstage.internal.serialization.SerializationMapperCreator;
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+public class SerializationMapperCreatorTest {
+
+    @Rule
+    public ExpectedException expectedException = ExpectedException.none();
+
+    @Test
+    public void createObjectMapper_defaultSerializationConfig_objectMapperWithDefaultValues() {
+        ObjectMapper objectMapper = SerializationMapperCreator.createObjectMapper(new SerializationConfig());
+
+        Assert.assertEquals("Invalid property naming strategy", PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES, objectMapper.getPropertyNamingStrategy());
+        Assert.assertEquals("Invalid mixin count", 0, objectMapper.mixInCount());
+    }
+
+    @Test
+    public void createObjectMapper_serializationConfigWithMixins_objectMapperIsCreatedWithMixins() {
+        Map, Class> mixins = new HashMap<>();
+        mixins.put(SampleApi.class, SampleMixIn.class);
+        SerializationConfig serializationConfig = new SerializationConfig().setMixins(mixins);
+        ObjectMapper objectMapper = SerializationMapperCreator.createObjectMapper(serializationConfig);
+
+        Assert.assertEquals("Invalid mixin count", 1, objectMapper.mixInCount());
+        Assert.assertEquals("Invalid property naming strategy", SampleMixIn.class, objectMapper.findMixInClassFor(SampleApi.class));
+    }
+
+    @Test
+    public void createObjectMapper_defaultSerializationConfigAndApiObjectHasAnySetter_anySetterIsCalledOnSerialization() throws IOException {
+        expectedException.expect(Exception.class);
+        expectedException.expectMessage("unknown field");
+        ObjectMapper objectMapper = SerializationMapperCreator.createObjectMapper(new SerializationConfig());
+
+        objectMapper.readValue("{ \"id\": 1, \"test\": \"test\" }", SampleApi.class);
+    }
+
+    @Test
+    public void createObjectMapper_serializationConfigWithoutAnySetterAndApiObjectHasAnySetter_anySetterIsCalledOnSerialization() throws IOException {
+        expectedException.expect(Exception.class);
+        expectedException.expectMessage("unknown field");
+        ObjectMapper objectMapper = SerializationMapperCreator.createObjectMapper(new SerializationConfig());
+
+        objectMapper.readValue("{ \"id\": 1, \"test\": \"test\" }", SampleApi.class);
+    }
+
+    @Test
+    public void createObjectMapper_serializationConfigWithIgnoreAnySetterAndApiObjectHasAnySetter_anySetterIgnored() throws IOException {
+        SerializationConfig serializationConfig = new SerializationConfig().setShouldIgnoreAnySetterAnnotation();
+        ObjectMapper objectMapper = SerializationMapperCreator.createObjectMapper(serializationConfig);
+
+        SampleApi sampleApi = objectMapper.readValue("{ \"id\": 1, \"test\": \"test\" }", SampleApi.class);
+        Assert.assertEquals("Id parsed incorrectly", 1, sampleApi.id);
+        Assert.assertNull("Name is parsed incorrectly", sampleApi.name);
+    }
+
+    private static class SampleApi {
+        @JsonProperty("id")
+        int id;
+
+        @JsonProperty("name")
+        String name;
+
+        @JsonAnySetter
+        public void handlerUnknownSetter(String field, Object value) throws Exception {
+            throw new Exception("unknown field");
+        }
+    }
+
+    private abstract class SampleMixIn {
+        @JsonIgnore
+        private String name;
+    }
+}