Skip to content

Commit

Permalink
Gson type adapters generated, good milestone #74
Browse files Browse the repository at this point in the history
refined Constitution and defaults behaviour #71
  • Loading branch information
elucash committed Feb 3, 2015
1 parent bd47279 commit 9b82f93
Show file tree
Hide file tree
Showing 13 changed files with 248 additions and 119 deletions.
10 changes: 6 additions & 4 deletions gson/pom.xml
Expand Up @@ -66,18 +66,20 @@
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>
<!-- Test compile generate api --> <!-- Compile only generate api -->
<groupId>org.immutables</groupId> <groupId>org.immutables</groupId>
<artifactId>value</artifactId> <artifactId>value</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
<scope>test</scope> <scope>provided</scope>
<optional>true</optional>
</dependency> </dependency>
<dependency> <dependency>
<!-- Test compile generate sample adapters --> <!-- Compile only generate sample adapters -->
<groupId>org.immutables</groupId> <groupId>org.immutables</groupId>
<artifactId>value-standalone</artifactId> <artifactId>value-standalone</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
<scope>test</scope> <scope>provided</scope>
<optional>true</optional>
</dependency> </dependency>
<dependency> <dependency>
<!-- test only jersey dependency --> <!-- test only jersey dependency -->
Expand Down
23 changes: 8 additions & 15 deletions gson/src/org/immutables/gson/Gson.java
@@ -1,5 +1,6 @@
package org.immutables.gson; package org.immutables.gson;


import com.google.gson.GsonBuilder;
import com.google.gson.annotations.SerializedName; import com.google.gson.annotations.SerializedName;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;
import java.lang.annotation.ElementType; import java.lang.annotation.ElementType;
Expand All @@ -14,7 +15,13 @@
* <p> * <p>
* Type adapter factories are also registered statically as services * Type adapter factories are also registered statically as services
* {@code META-INF/services/com.google.gson.TypeAdapterFactory}. Easy way to configure * {@code META-INF/services/com.google.gson.TypeAdapterFactory}. Easy way to configure
* {@link com.google.gson.Gson} * {@link com.google.gson.Gson}.
* <p>
* Certain are gson options are supported for immutable objects in deliberate fashion:
* <ul>
* <li>{@link GsonBuilder#serializeNulls()} - When enabled, {@code null} fields and empty array
* fields will be included, otherwise omited</li>
* </ul>
*/ */
@Documented @Documented
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
Expand Down Expand Up @@ -73,18 +80,4 @@
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD) @Target(ElementType.METHOD)
public @interface Ignore {} public @interface Ignore {}

/**
* For {@link java.util.Set Set} or {@link java.util.List List} this will force output of
* JSON empty array if given collection is empty. By default, empty collection attribute will
* just
* be omitted.
* <p>
* For {@link com.google.common.base.Optional Optional} attributes it forces of output JSON
* {@code null} value for missing value, otherwise (by default) no absent attribute is written
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD)
public @interface ForceEmpty {}
} }
148 changes: 113 additions & 35 deletions gson/src/org/immutables/gson/stream/GsonMessageBodyProvider.java
Expand Up @@ -15,6 +15,9 @@
*/ */
package org.immutables.gson.stream; package org.immutables.gson.stream;


import org.immutables.value.Value.Style.ImplementationVisibility;
import java.lang.reflect.Field;
import org.immutables.value.Value;
import java.util.List; import java.util.List;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import com.google.common.base.Throwables; import com.google.common.base.Throwables;
Expand Down Expand Up @@ -60,7 +63,6 @@
@Produces(MediaType.WILDCARD) @Produces(MediaType.WILDCARD)
@SuppressWarnings({"resource", "unused"}) @SuppressWarnings({"resource", "unused"})
public class GsonMessageBodyProvider implements MessageBodyReader<Object>, MessageBodyWriter<Object> { public class GsonMessageBodyProvider implements MessageBodyReader<Object>, MessageBodyWriter<Object> {

private final Gson gson; private final Gson gson;
private final Set<MediaType> mediaTypes; private final Set<MediaType> mediaTypes;
private final Streamer streamer; private final Streamer streamer;
Expand All @@ -70,47 +72,36 @@ public class GsonMessageBodyProvider implements MessageBodyReader<Object>, Messa
* and {@link MediaType#APPLICATION_JSON_TYPE application/json} media type to match. * and {@link MediaType#APPLICATION_JSON_TYPE application/json} media type to match.
*/ */
public GsonMessageBodyProvider() { public GsonMessageBodyProvider() {
this(createGson(), true); this(new GsonProviderOptionsBuilder().build());
} }


/** /**
* Creates new provider with flexible setup. * Creates new provider with flexible setup.
* @param gson the fully configured gson instance * @param options options for provider
* @param allowJacksonIfAvailable allows Jackson streaming optimization if Jackson if available in
* classpath. Use {@code false} to disable optimization and use pure Gson.
* @param mediaTypes the media types to match provider
*/ */
public GsonMessageBodyProvider(Gson gson, boolean allowJacksonIfAvailable, MediaType... mediaTypes) { public GsonMessageBodyProvider(GsonProviderOptions options) {
this.gson = gson; this.gson = options.gson();
this.mediaTypes = mediaSetFrom(mediaTypes); this.mediaTypes = mediaSetFrom(options.mediaTypes());
this.streamer = createStreamer(allowJacksonIfAvailable); this.streamer = createStreamer(options.allowJackson(),
new StreamingOptions(options.gson(), options.lenient()));
} }


private static Gson createGson() { private static Streamer createStreamer(boolean allowJacksonIfAvailable, StreamingOptions options) {
GsonBuilder gsonBuilder = new GsonBuilder();
for (TypeAdapterFactory factory : ServiceLoader.load(TypeAdapterFactory.class)) {
gsonBuilder.registerTypeAdapterFactory(factory);
}
return gsonBuilder.create();
}

private static Streamer createStreamer(boolean allowJacksonIfAvailable) {
if (allowJacksonIfAvailable) { if (allowJacksonIfAvailable) {
try { try {
return new JacksonStreamer(); return new JacksonStreamer(options);
} catch (Throwable ex) { } catch (Throwable ex) {
System.err.println(ex.toString());// FIXME just during development
// cannot load jackson streamer class, fallback to default // cannot load jackson streamer class, fallback to default
} }
} }
return new GsonStreamer(); return new GsonStreamer(options);
} }


private static Set<MediaType> mediaSetFrom(MediaType[] mediaTypes) { private static Set<MediaType> mediaSetFrom(List<MediaType> mediaTypes) {
if (mediaTypes.length == 0) { if (mediaTypes.isEmpty()) {
return Collections.singleton(MediaType.APPLICATION_JSON_TYPE); return Collections.singleton(MediaType.APPLICATION_JSON_TYPE);
} }
return new HashSet<MediaType>(Arrays.asList(mediaTypes)); return new HashSet<MediaType>(mediaTypes);
} }


@Override @Override
Expand Down Expand Up @@ -180,13 +171,19 @@ private interface Streamer {


private static class GsonStreamer implements Streamer { private static class GsonStreamer implements Streamer {
private static final String CHARSET_NAME = "utf-8"; private static final String CHARSET_NAME = "utf-8";
private final StreamingOptions options;

GsonStreamer(StreamingOptions options) {
this.options = options;
}


@Override @Override
public void write(Gson gson, Type type, Object object, OutputStream stream) throws IOException { public void write(Gson gson, Type type, Object object, OutputStream stream) throws IOException {
@Nullable JsonWriter writer = null; @Nullable JsonWriter writer = null;
boolean wasOriginalException = false; boolean wasOriginalException = false;
try { try {
writer = new JsonWriter(new BufferedWriter(new OutputStreamWriter(stream, CHARSET_NAME))); writer = new JsonWriter(new BufferedWriter(new OutputStreamWriter(stream, CHARSET_NAME)));
options.setWriterOptions(writer);


gson.toJson(object, type, writer); gson.toJson(object, type, writer);


Expand Down Expand Up @@ -215,22 +212,14 @@ public Object read(Gson gson, Type type, InputStream stream) throws IOException
@Nullable JsonReader reader = null; @Nullable JsonReader reader = null;
try { try {
reader = new JsonReader(new BufferedReader(new InputStreamReader(stream, CHARSET_NAME))); reader = new JsonReader(new BufferedReader(new InputStreamReader(stream, CHARSET_NAME)));
options.setReaderOptions(reader);


return gson.fromJson(reader, type); return gson.fromJson(reader, type);


} catch (IOException ex) { } catch (IOException ex) {
throw ex; throw ex;
} catch (Exception ex) { } catch (Exception ex) {
throw new IOException(ex); throw new IOException(ex);
} finally {
// input stream should not be closed
// if (reader != null) {
// try {
// reader.close();
// } catch (IOException ex) {
// // ignoring io exception on reader close
// }
// }
} }
} }
} }
Expand All @@ -240,12 +229,24 @@ private static class JacksonStreamer implements Streamer {
.disable(JsonParser.Feature.AUTO_CLOSE_SOURCE) .disable(JsonParser.Feature.AUTO_CLOSE_SOURCE)
.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); .disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);


private final StreamingOptions options;

JacksonStreamer(StreamingOptions options) {
this.options = options;
}

@Override @Override
public void write(Gson gson, Type type, Object object, OutputStream stream) throws IOException { public void write(Gson gson, Type type, Object object, OutputStream stream) throws IOException {
@Nullable JsonGeneratorWriter writer = null; @Nullable JsonGeneratorWriter writer = null;
boolean wasOriginalException = false; boolean wasOriginalException = false;
try { try {
writer = new JsonGeneratorWriter(JSON_FACTORY.createGenerator(stream)); JsonGenerator generator = JSON_FACTORY.createGenerator(stream);
if (options.prettyPrinting) {
generator.useDefaultPrettyPrinter();
}
writer = new JsonGeneratorWriter(generator);
options.setWriterOptions(writer);

gson.toJson(object, type, writer); gson.toJson(object, type, writer);


} catch (IOException ex) { } catch (IOException ex) {
Expand Down Expand Up @@ -273,6 +274,7 @@ public Object read(Gson gson, Type type, InputStream stream) throws IOException
@Nullable JsonReader reader = null; @Nullable JsonReader reader = null;
try { try {
reader = new JsonParserReader(JSON_FACTORY.createParser(stream)); reader = new JsonParserReader(JSON_FACTORY.createParser(stream));
options.setReaderOptions(reader);


return gson.fromJson(reader, type); return gson.fromJson(reader, type);


Expand All @@ -292,4 +294,80 @@ public Object read(Gson gson, Type type, InputStream stream) throws IOException
} }
} }
} }

@Value.Immutable(singleton = true)
@Value.Style(
// typeBuilder = "GsonProviderOptionsBuilder",
jdkOnly = true,
visibility = ImplementationVisibility.PRIVATE)
public static abstract class GsonProviderOptions {
/**
* the fully configured gson instance.
* @return the gson instanse
*/
@Value.Default
public Gson gson() {
GsonBuilder gsonBuilder = new GsonBuilder();
for (TypeAdapterFactory factory : ServiceLoader.load(TypeAdapterFactory.class)) {
gsonBuilder.registerTypeAdapterFactory(factory);
}
return gsonBuilder.create();
}

/**
* Allows Jackson streaming optimization if Jackson if available in
* classpath. Use {@code false} to disable optimization and use pure Gson.
* @return {@code true} if Jackson allowed
*/
@Value.Default
public boolean allowJackson() {
return true;
}

/**
* if {@code true} - enables non strict parsing and serialization.
* @return true, if successful
*/
@Value.Default
public boolean lenient() {
return false;
}

public abstract List<MediaType> mediaTypes();
}

private static class StreamingOptions {
final boolean lenient;
final boolean serializeNulls;
final boolean htmlSafe;
final boolean prettyPrinting;

StreamingOptions(Gson gson, boolean lenient) {
this.lenient = lenient;
this.htmlSafe = accessField(gson, "htmlSafe", true);
this.serializeNulls = accessField(gson, "serializeNulls", false);
this.prettyPrinting = accessField(gson, "prettyPrinting", false);
}

private static boolean accessField(Gson gson, String name, boolean defaultValue) {
try {
Field field = Gson.class.getField(name);
field.setAccessible(true);
return field.get(gson) == Boolean.TRUE;
} catch (Exception ex) {
return defaultValue;
}
}

void setReaderOptions(JsonReader reader) {
reader.setLenient(lenient);
}

void setWriterOptions(JsonWriter writer) {
writer.setSerializeNulls(serializeNulls);
writer.setLenient(lenient);
writer.setHtmlSafe(htmlSafe);
writer.setIndent(prettyPrinting ? " " : "");
}
}
} }
50 changes: 50 additions & 0 deletions gson/test/org/immutables/gson/adapters/AdaptReadWriteTest.java
@@ -0,0 +1,50 @@
package org.immutables.gson.adapters;

import com.google.common.collect.ImmutableSet;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.junit.Test;
import static org.immutables.check.Checkers.*;

public class AdaptReadWriteTest {

private final Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(new AdaptTypeAdapters())
.setPrettyPrinting()
.create();

private final Adapt adapt =
ImmutableAdapt.of(ImmutableSet.of(
ImmutableAdapt.Inr.builder()
.addList(1, 2, 4)
.putMap("key", ImmutableAdapt.Nst.builder()
.string("a")
.value(1)
.build())
.putMap("other", ImmutableAdapt.Nst.builder()
.string("b")
.value(2)
.build())
.build(),
ImmutableAdapt.Inr.builder()
.addList(5, 6)
.putMap("ku", ImmutableAdapt.Nst.builder()
.string("x")
.value(11)
.build())
.putMap("la", ImmutableAdapt.Nst.builder()
.string("y")
.value(21)
.build())
.build()));

@Test
public void adapt() {
String json = gson.toJson(adapt);

System.out.println(json);
Adapt instance = gson.fromJson(json, Adapt.class);

check(instance).is(adapt);
}
}
12 changes: 9 additions & 3 deletions gson/test/org/immutables/gson/stream/JaxrsTest.java
Expand Up @@ -21,7 +21,13 @@
public class JaxrsTest { public class JaxrsTest {


private static final GsonMessageBodyProvider PURE_GSON_TEXT_PLAIN = private static final GsonMessageBodyProvider PURE_GSON_TEXT_PLAIN =
new GsonMessageBodyProvider(new Gson(), false, MediaType.TEXT_PLAIN_TYPE) {}; new GsonMessageBodyProvider(
new GsonProviderOptionsBuilder()
.gson(new Gson())
.addMediaTypes(MediaType.TEXT_PLAIN_TYPE)
.allowJackson(false)
.lenient(true)
.build()) {};


private static final URI SERVER_URI = URI.create("http://localhost:8997"); private static final URI SERVER_URI = URI.create("http://localhost:8997");
private static HttpServer httpServer; private static HttpServer httpServer;
Expand Down Expand Up @@ -59,7 +65,7 @@ public void gsonJacksonRoundtrip() {
.path("/") .path("/")
.request(MediaType.APPLICATION_JSON_TYPE) .request(MediaType.APPLICATION_JSON_TYPE)
.accept(MediaType.APPLICATION_JSON_TYPE) .accept(MediaType.APPLICATION_JSON_TYPE)
.post(Entity.json(Collections.singleton(13)), new GenericType<List<String>>() {}); .post(Entity.json(Collections.singletonList(13)), new GenericType<List<String>>() {});


check(result).isOf("a", "b", "c", "[13]"); check(result).isOf("a", "b", "c", "[13]");
} }
Expand All @@ -71,7 +77,7 @@ public void pureGsonRoundtrip() {
.path("/") .path("/")
.request(MediaType.TEXT_PLAIN_TYPE) .request(MediaType.TEXT_PLAIN_TYPE)
.accept(MediaType.TEXT_PLAIN_TYPE) .accept(MediaType.TEXT_PLAIN_TYPE)
.post(Entity.text(Collections.singleton("11")), new GenericType<List<String>>() {}); .post(Entity.text(Collections.singletonList("11")), new GenericType<List<String>>() {});


check(result).isOf("x", "y", "[11]"); check(result).isOf("x", "y", "[11]");
} }
Expand Down
Expand Up @@ -31,7 +31,7 @@
public class HiddenImplementation { public class HiddenImplementation {


void use() { void use() {
HiddenImplementation instance = HiddenImplementationBuilder.builder().build(); HiddenImplementation instance = new HiddenImplementationBuilder().build();
instance.toString(); instance.toString();
} }
} }

0 comments on commit 9b82f93

Please sign in to comment.