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; + } +}