Create Jackson Middleware (#60)

* Create Jackson string parser

* Create Jackson BufferedSource parser

* Create Jackson Reader parser
This commit is contained in:
Maksim Moiseikin 2017-01-15 19:37:16 +01:00 committed by Brian Plummer
parent cbcd9794e0
commit 9db2456118
16 changed files with 759 additions and 1 deletions

View file

@ -42,6 +42,7 @@ ext.versions = [
googlePlayServices : '9.6.1',
gson : '2.6.2',
moshi : '1.3.1',
jackson : '2.8.6',
guava : '19.0',
javapoet : '1.7.0',
immutables : '2.2.1',
@ -99,6 +100,8 @@ ext.libraries = [
okio : "com.squareup.okio:okio:$versions.okio",
gson : "com.google.code.gson:gson:$versions.gson",
moshi : "com.squareup.moshi:moshi:$versions.moshi",
jacksonCore : "com.fasterxml.jackson.core:jackson-core:$versions.jackson",
jacksonDatabind : "com.fasterxml.jackson.core:jackson-databind:$versions.jackson",
guava : "com.google.guava:guava:$versions.guava",
googlePlayServices : "com.google.android.gms:play-services-base:$versions.googlePlayServices",
googleMessagingCloud : "com.google.android.gms:play-services-gcm:$versions.googlePlayServices",

1
middleware-jackson/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,55 @@
apply plugin: 'com.android.library'
apply plugin: 'com.getkeepsafe.dexcount'
group = GROUP
version = VERSION_NAME
android {
compileSdkVersion versions.compileSdk
buildToolsVersion versions.buildTools
defaultConfig {
minSdkVersion versions.minSdk
targetSdkVersion versions.targetSdk
}
buildTypes {
debug {
minifyEnabled false
zipAlignEnabled false
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
lintOptions {
abortOnError true
disable 'InvalidPackage'
}
}
dependencies {
compile project(path: ':store')
compile libraries.jacksonCore
compile libraries.jacksonDatabind
compile libraries.rxJava
compile libraries.okio
compile libraries.dagger
compile libraries.supportAnnotations
testCompile libraries.mockito
testCompile libraries.junit
}
task sourcesJar(type: Jar) {
from android.sourceSets.main.java.srcDirs
classifier = 'sources'
}
artifacts {
archives sourcesJar
}
apply from: rootProject.file("gradle/maven-push.gradle")
apply from: rootProject.file("gradle/checkstyle.gradle")
apply from: rootProject.file("gradle/pmd.gradle")

View file

@ -0,0 +1,3 @@
POM_NAME=com.nytimes.android
POM_ARTIFACT_ID=middleware-jackson
POM_PACKAGING=aar

View file

@ -0,0 +1,11 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.nytimes.android.external.store.middleware.jackson">
<application android:allowBackup="true"
android:label="@string/app_name"
android:supportsRtl="true"
>
</application>
</manifest>

View file

@ -0,0 +1,119 @@
package com.nytimes.android.external.store.middleware.jackson;
import android.support.annotation.NonNull;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nytimes.android.external.cache.Preconditions;
import com.nytimes.android.external.store.base.Parser;
import java.io.Reader;
import java.lang.reflect.Type;
import okio.BufferedSource;
/**
* Factory which returns various Jackson {@link Parser} implementations.
*/
public final class JacksonParserFactory {
private JacksonParserFactory() {
}
/**
* Returns a new Parser which parses from a String to the specified type, using
* the provided {@link JsonFactory} instance.
*/
@NonNull
public static <T> Parser<String, T> createStringParser(@NonNull JsonFactory jsonFactory, @NonNull Type type) {
Preconditions.checkNotNull(jsonFactory, "jsonFactory cannot be null.");
Preconditions.checkNotNull(type, "type cannot be null.");
return new JacksonStringParser<>(jsonFactory, type);
}
/**
* Returns a new Parser which parses from a String to the specified type, using
* the provided {@link ObjectMapper} instance.
*/
@NonNull
public static <T> Parser<String, T> createStringParser(@NonNull ObjectMapper objectMapper, @NonNull Type type) {
Preconditions.checkNotNull(objectMapper, "objectMapper cannot be null.");
Preconditions.checkNotNull(type, "type cannot be null.");
return new JacksonStringParser<>(objectMapper, type);
}
/**
* Returns a new Parser which parses from a String to the specified type, using
* a new default {@link ObjectMapper} instance.
*/
@NonNull
public static <T> Parser<String, T> createStringParser(@NonNull Class<T> type) {
return createStringParser(new ObjectMapper(), type);
}
/**
* Returns a new Parser which parses from {@link BufferedSource} to the specified type, using
* the provided {@link JsonFactory} instance.
*/
@NonNull
public static <T> Parser<BufferedSource, T> createSourceParser(@NonNull JsonFactory jsonFactory,
@NonNull Type type) {
Preconditions.checkNotNull(jsonFactory, "jsonFactory cannot be null.");
Preconditions.checkNotNull(type, "type cannot be null.");
return new JacksonSourceParser<T>(jsonFactory, type);
}
/**
* Returns a new Parser which parses from {@link BufferedSource} to the specified type, using
* the provided {@link ObjectMapper} instance.
*/
@NonNull
public static <T> Parser<BufferedSource, T> createSourceParser(@NonNull ObjectMapper objectMapper,
@NonNull Type type) {
Preconditions.checkNotNull(objectMapper, "objectMapper cannot be null.");
Preconditions.checkNotNull(type, "type cannot be null.");
return new JacksonSourceParser<T>(objectMapper, type);
}
/**
* Returns a new Parser which parses from {@link BufferedSource} to the specified type, using
* a new default configured {@link ObjectMapper} instance.
*/
@NonNull
public static <T> Parser<BufferedSource, T> createSourceParser(@NonNull Type type) {
return createSourceParser(new ObjectMapper(), type);
}
/**
* Returns a new Parser which parses from {@link Reader} to the specified type, using
* the provided {@link JsonFactory} instance.
*/
@NonNull
public static <T> Parser<Reader, T> createReaderParser(@NonNull JsonFactory jsonFactory,
@NonNull Type type) {
Preconditions.checkNotNull(jsonFactory, "jsonFactory cannot be null.");
Preconditions.checkNotNull(type, "type cannot be null.");
return new JacksonReaderParser<>(jsonFactory, type);
}
/**
* Returns a new Parser which parses from {@link Reader} to the specified type, using
* the provided {@link ObjectMapper} instance.
*/
@NonNull
public static <T> Parser<Reader, T> createReaderParser(@NonNull ObjectMapper objectMapper,
@NonNull Type type) {
Preconditions.checkNotNull(objectMapper, "objectMapper cannot be null.");
Preconditions.checkNotNull(type, "type cannot be null.");
return new JacksonReaderParser<T>(objectMapper, type);
}
/**
* Returns a new Parser which parses from {@link Reader} to the specified type, using
* a new default configured {@link ObjectMapper} instance.
*/
@NonNull
public static <T> Parser<Reader, T> createReaderParser(@NonNull Type type) {
return createReaderParser(new ObjectMapper(), type);
}
}

View file

@ -0,0 +1,41 @@
package com.nytimes.android.external.store.middleware.jackson;
import android.support.annotation.NonNull;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nytimes.android.external.store.base.Parser;
import java.io.IOException;
import java.io.Reader;
import java.lang.reflect.Type;
import javax.inject.Inject;
public class JacksonReaderParser<Parsed> implements Parser<Reader, Parsed> {
private final ObjectMapper objectMapper;
private final JavaType parsedType;
@Inject
public JacksonReaderParser(@NonNull JsonFactory jsonFactory, @NonNull Type type) {
objectMapper = new ObjectMapper(jsonFactory);
parsedType = objectMapper.constructType(type);
}
@Inject
public JacksonReaderParser(@NonNull ObjectMapper objectMapper, @NonNull Type type) {
this.objectMapper = objectMapper;
parsedType = objectMapper.constructType(type);
}
@Override
public Parsed call(@NonNull Reader reader) {
try {
return objectMapper.readValue(reader, parsedType);
} catch (IOException e) {
return null;
}
}
}

View file

@ -0,0 +1,54 @@
package com.nytimes.android.external.store.middleware.jackson;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nytimes.android.external.store.base.Parser;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import javax.inject.Inject;
import okio.BufferedSource;
public class JacksonSourceParser<Parsed> implements Parser<BufferedSource, Parsed> {
private final ObjectMapper objectMapper;
private final JavaType parsedType;
@Inject
public JacksonSourceParser(@NonNull JsonFactory jsonFactory, @NonNull Type type) {
objectMapper = new ObjectMapper(jsonFactory);
parsedType = objectMapper.constructType(type);
}
@Inject
public JacksonSourceParser(@NonNull ObjectMapper objectMapper, @NonNull Type type) {
this.objectMapper = objectMapper;
parsedType = objectMapper.constructType(type);
}
@Override
@Nullable
@SuppressWarnings("PMD.EmptyCatchBlock")
public Parsed call(@NonNull BufferedSource source) {
InputStream inputStream = source.inputStream();
try {
return objectMapper.readValue(inputStream, parsedType);
} catch (IOException e) {
return null;
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {
}
}
}
}

View file

@ -0,0 +1,42 @@
package com.nytimes.android.external.store.middleware.jackson;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nytimes.android.external.store.base.Parser;
import java.io.IOException;
import java.lang.reflect.Type;
import javax.inject.Inject;
public class JacksonStringParser<Parsed> implements Parser<String, Parsed> {
private final ObjectMapper objectMapper;
private final JavaType parsedType;
@Inject
public JacksonStringParser(@NonNull JsonFactory jsonFactory, @NonNull Type type) {
objectMapper = new ObjectMapper(jsonFactory);
parsedType = objectMapper.constructType(type);
}
@Inject
public JacksonStringParser(@NonNull ObjectMapper objectMapper, @NonNull Type type) {
this.objectMapper = objectMapper;
parsedType = objectMapper.constructType(type);
}
@Override
@Nullable
public Parsed call(@NonNull String source) {
try {
return objectMapper.readValue(source, parsedType);
} catch (IOException e) {
return null;
}
}
}

View file

@ -0,0 +1,3 @@
<resources>
<string name="app_name">middleware-jackson</string>
</resources>

View file

@ -0,0 +1,131 @@
package com.nytimes.android.external.store.middleware.jackson;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nytimes.android.external.store.base.Fetcher;
import com.nytimes.android.external.store.base.Parser;
import com.nytimes.android.external.store.base.Persister;
import com.nytimes.android.external.store.base.Store;
import com.nytimes.android.external.store.base.impl.BarCode;
import com.nytimes.android.external.store.base.impl.ParsingStoreBuilder;
import com.nytimes.android.external.store.middleware.jackson.data.Foo;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.io.Reader;
import java.io.StringReader;
import rx.Observable;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class JacksonReaderParserStoreTest {
private static final String KEY = "key";
private static final String sourceString =
"{\"number\":123,\"string\":\"abc\",\"bars\":[{\"string\":\"def\"},{\"string\":\"ghi\"}]}";
@Rule
public ExpectedException expectedException = ExpectedException.none();
@Mock
Fetcher<Reader> fetcher;
@Mock
Persister<Reader> persister;
private final BarCode barCode = new BarCode("value", KEY);
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
Reader source = new StringReader(sourceString);
when(fetcher.fetch(barCode))
.thenReturn(Observable.just(source));
when(persister.read(barCode))
.thenReturn(Observable.<Reader>empty())
.thenReturn(Observable.just(source));
when(persister.write(barCode, source))
.thenReturn(Observable.just(true));
}
@Test
public void testDefaultJacksonReaderParser() {
Parser<Reader, Foo> parser = JacksonParserFactory.createReaderParser(Foo.class);
Store<Foo> store = ParsingStoreBuilder.<Reader, Foo>builder()
.persister(persister)
.fetcher(fetcher)
.parser(parser)
.open();
Foo result = store.get(barCode).toBlocking().first();
validateFoo(result);
verify(fetcher, times(1)).fetch(barCode);
}
@Test
public void testCustomJsonFactoryReaderParser() {
JsonFactory jsonFactory = new JsonFactory();
Parser<Reader, Foo> parser = JacksonParserFactory.createReaderParser(jsonFactory, Foo.class);
Store<Foo> store = ParsingStoreBuilder.<Reader, Foo>builder()
.persister(persister)
.fetcher(fetcher)
.parser(parser)
.open();
Foo result = store.get(barCode).toBlocking().first();
validateFoo(result);
verify(fetcher, times(1)).fetch(barCode);
}
private void validateFoo(Foo foo) {
assertNotNull(foo);
assertEquals(foo.number, 123);
assertEquals(foo.string, "abc");
assertEquals(foo.bars.size(), 2);
assertEquals(foo.bars.get(0).string, "def");
assertEquals(foo.bars.get(1).string, "ghi");
}
@Test
public void testNullJsonFactory() {
expectedException.expect(NullPointerException.class);
JacksonParserFactory.createReaderParser((JsonFactory) null, Foo.class);
}
@Test
public void testNullTypeWithValidJsonFactory() {
expectedException.expect(NullPointerException.class);
JacksonParserFactory.createReaderParser(new JsonFactory(), null);
}
@Test
public void testNullObjectMapper() {
expectedException.expect(NullPointerException.class);
JacksonParserFactory.createReaderParser((ObjectMapper) null, Foo.class);
}
@Test
public void testNullType() {
expectedException.expect(NullPointerException.class);
JacksonParserFactory.createStringParser(null);
}
}

View file

@ -0,0 +1,139 @@
package com.nytimes.android.external.store.middleware.jackson;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nytimes.android.external.store.base.Fetcher;
import com.nytimes.android.external.store.base.Parser;
import com.nytimes.android.external.store.base.Persister;
import com.nytimes.android.external.store.base.Store;
import com.nytimes.android.external.store.base.impl.BarCode;
import com.nytimes.android.external.store.base.impl.ParsingStoreBuilder;
import com.nytimes.android.external.store.middleware.jackson.data.Foo;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.io.ByteArrayInputStream;
import java.nio.charset.Charset;
import okio.BufferedSource;
import okio.Okio;
import rx.Observable;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class JacksonSourceParserStoreTest {
private static final String KEY = "key";
private static final String sourceString =
"{\"number\":123,\"string\":\"abc\",\"bars\":[{\"string\":\"def\"},{\"string\":\"ghi\"}]}";
@Rule
public ExpectedException expectedException = ExpectedException.none();
@Mock
Fetcher<BufferedSource> fetcher;
@Mock
Persister<BufferedSource> persister;
private final BarCode barCode = new BarCode("value", KEY);
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
BufferedSource bufferedSource = source(sourceString);
assertNotNull(bufferedSource);
when(fetcher.fetch(barCode))
.thenReturn(Observable.just(bufferedSource));
when(persister.read(barCode))
.thenReturn(Observable.<BufferedSource>empty())
.thenReturn(Observable.just(bufferedSource));
when(persister.write(barCode, bufferedSource))
.thenReturn(Observable.just(true));
}
@Test
public void testDefaultJacksonSourceParser() {
Parser<BufferedSource, Foo> parser = JacksonParserFactory.createSourceParser(Foo.class);
Store<Foo> store = ParsingStoreBuilder.<BufferedSource, Foo>builder()
.persister(persister)
.fetcher(fetcher)
.parser(parser)
.open();
Foo result = store.get(barCode).toBlocking().first();
validateFoo(result);
verify(fetcher, times(1)).fetch(barCode);
}
@Test
public void testCustomJsonFactorySourceParser() {
JsonFactory jsonFactory = new JsonFactory();
Parser<BufferedSource, Foo> parser = JacksonParserFactory.createSourceParser(jsonFactory, Foo.class);
Store<Foo> store = ParsingStoreBuilder.<BufferedSource, Foo>builder()
.persister(persister)
.fetcher(fetcher)
.parser(parser)
.open();
Foo result = store.get(barCode).toBlocking().first();
validateFoo(result);
verify(fetcher, times(1)).fetch(barCode);
}
private void validateFoo(Foo foo) {
assertNotNull(foo);
assertEquals(foo.number, 123);
assertEquals(foo.string, "abc");
assertEquals(foo.bars.size(), 2);
assertEquals(foo.bars.get(0).string, "def");
assertEquals(foo.bars.get(1).string, "ghi");
}
@Test
public void testNullJsonFactory() {
expectedException.expect(NullPointerException.class);
JacksonParserFactory.createStringParser((JsonFactory) null, Foo.class);
}
@Test
public void testNullTypeWithValidJsonFactory() {
expectedException.expect(NullPointerException.class);
JacksonParserFactory.createStringParser(new JsonFactory(), null);
}
@Test
public void testNullObjectMapper() {
expectedException.expect(NullPointerException.class);
JacksonParserFactory.createStringParser((ObjectMapper) null, Foo.class);
}
@Test
public void testNullType() {
expectedException.expect(NullPointerException.class);
JacksonParserFactory.createStringParser(null);
}
private static BufferedSource source(String data) {
return Okio.buffer(Okio.source(new ByteArrayInputStream(data.getBytes(Charset.defaultCharset()))));
}
}

View file

@ -0,0 +1,126 @@
package com.nytimes.android.external.store.middleware.jackson;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nytimes.android.external.store.base.Fetcher;
import com.nytimes.android.external.store.base.Parser;
import com.nytimes.android.external.store.base.Persister;
import com.nytimes.android.external.store.base.Store;
import com.nytimes.android.external.store.base.impl.BarCode;
import com.nytimes.android.external.store.base.impl.ParsingStoreBuilder;
import com.nytimes.android.external.store.middleware.jackson.data.Foo;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import rx.Observable;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class JacksonStringParserStoreTest {
private static final String KEY = "key";
private static final String source =
"{\"number\":123,\"string\":\"abc\",\"bars\":[{\"string\":\"def\"},{\"string\":\"ghi\"}]}";
@Rule
public ExpectedException expectedException = ExpectedException.none();
@Mock
Fetcher<String> fetcher;
@Mock
Persister<String> persister;
private final BarCode barCode = new BarCode("value", KEY);
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
when(fetcher.fetch(barCode))
.thenReturn(Observable.just(source));
when(persister.read(barCode))
.thenReturn(Observable.<String>empty())
.thenReturn(Observable.just(source));
when(persister.write(barCode, source))
.thenReturn(Observable.just(true));
}
@Test
public void testDefaultJacksonStringParser() {
Store<Foo> store = ParsingStoreBuilder.<String, Foo>builder()
.persister(persister)
.fetcher(fetcher)
.parser(JacksonParserFactory.createStringParser(Foo.class))
.open();
Foo result = store.get(barCode).toBlocking().first();
validateFoo(result);
verify(fetcher, times(1)).fetch(barCode);
}
@Test
public void testCustomJsonFactoryStringParser() {
JsonFactory jsonFactory = new JsonFactory();
Parser<String, Foo> parser = JacksonParserFactory.createStringParser(jsonFactory, Foo.class);
Store<Foo> store = ParsingStoreBuilder.<String, Foo>builder()
.persister(persister)
.fetcher(fetcher)
.parser(parser)
.open();
Foo result = store.get(barCode).toBlocking().first();
validateFoo(result);
verify(fetcher, times(1)).fetch(barCode);
}
private void validateFoo(Foo foo) {
assertNotNull(foo);
assertEquals(foo.number, 123);
assertEquals(foo.string, "abc");
assertEquals(foo.bars.size(), 2);
assertEquals(foo.bars.get(0).string, "def");
assertEquals(foo.bars.get(1).string, "ghi");
}
@Test
public void testNullJsonFactory() {
expectedException.expect(NullPointerException.class);
JacksonParserFactory.createStringParser((JsonFactory) null, Foo.class);
}
@Test
public void testNullTypeWithValidJsonFactory() {
expectedException.expect(NullPointerException.class);
JacksonParserFactory.createStringParser(new JsonFactory(), null);
}
@Test
public void testNullObjectMapper() {
expectedException.expect(NullPointerException.class);
JacksonParserFactory.createStringParser((ObjectMapper) null, Foo.class);
}
@Test
public void testNullType() {
expectedException.expect(NullPointerException.class);
JacksonParserFactory.createStringParser(null);
}
}

View file

@ -0,0 +1,12 @@
package com.nytimes.android.external.store.middleware.jackson.data;
public class Bar {
public String string;
public Bar() {
}
public Bar(String string) {
this.string = string;
}
}

View file

@ -0,0 +1,18 @@
package com.nytimes.android.external.store.middleware.jackson.data;
import java.util.List;
public class Foo {
public int number;
public String string;
public List<Bar> bars;
public Foo() {
}
public Foo(int number, String string, List<Bar> bars) {
this.number = number;
this.string = string;
this.bars = bars;
}
}

View file

@ -1 +1 @@
include ':app', ':store', ':middleware', ':cache', ':filesystem', ':middleware-moshi'
include ':app', ':store', ':middleware', ':cache', ':filesystem', ':middleware-moshi', ':middleware-jackson'