Skip to content

Commit

Permalink
Allow external(custom) CodecRegistry for serialization and querying i…
Browse files Browse the repository at this point in the history
…n mongo adapter.

This change will allow users to use other serialization frameworks than Gson (eg. jackson or native
 BSON PojoCodec(s) etc.). Backwards compatibility with Gson is preserved.

1. Moved bson/gson adapters to a separate package `bson4gson` which can be developed independently.
2. Changed RepositorySetup.Builder to accept CodecRegistry (if not set, defaults to Gson delegate).
3. Upgraded Gson library to 2.8.5 (from 2.8.0) because of new method `gson.newBuilder()` which allows
registering new TypeAdapterFactories with existing gson instance.
  • Loading branch information
asereda-gs committed Sep 10, 2018
1 parent 0be8792 commit 37b43b5
Show file tree
Hide file tree
Showing 24 changed files with 750 additions and 313 deletions.
2 changes: 1 addition & 1 deletion gson/pom.xml
Expand Up @@ -33,7 +33,7 @@
<!-- Required Gson dependency -->
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.0</version>
<version>2.8.5</version>
</dependency>
<dependency>
<!-- Compile only META-INF/services generator -->
Expand Down
Expand Up @@ -13,19 +13,19 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
package org.immutables.mongo.repository.internal;
package org.immutables.mongo.bson4gson;

import com.google.gson.internal.LazilyParsedNumber;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import java.io.IOException;
import java.io.Reader;
import java.util.regex.Pattern;
import javax.annotation.concurrent.NotThreadSafe;
import org.bson.AbstractBsonReader;
import org.bson.AbstractBsonReader.State;
import org.bson.BsonType;
import org.bson.types.Decimal128;

import javax.annotation.concurrent.NotThreadSafe;
import java.io.IOException;
import java.io.Reader;

import static com.google.common.base.Preconditions.checkNotNull;

/**
Expand All @@ -43,7 +43,7 @@
* @see <a href="http://bsonspec.org/">BSON spec</a>
*/
@NotThreadSafe
public class BsonReader extends JsonReader {
public class BsonReader extends JsonReader implements Wrapper<org.bson.BsonReader> {

private static final Reader UNREADABLE_READER = new Reader() {
@Override
Expand Down Expand Up @@ -72,6 +72,11 @@ public void close() throws IOException {
this.delegate = checkNotNull(delegate, "delegate");
}

@Override
public org.bson.BsonReader unwrap() {
return this.delegate;
}

private void advance() {
delegate.readBsonType();
}
Expand Down Expand Up @@ -276,23 +281,4 @@ public void skipValue() throws IOException {
delegate.skipValue();
}

public Pattern nextPattern() {
return Pattern.compile(delegate.readRegularExpression().getPattern());
}

public long nextTimeInstant() {
return delegate.readDateTime();
}

public Decimal128 nextDecimal() {
return delegate.readDecimal128();
}

public byte[] nextObjectId() {
return delegate.readObjectId().toByteArray();
}

public byte[] nextBinary() {
return delegate.readBinaryData().getData();
}
}
Expand Up @@ -13,20 +13,18 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
package org.immutables.mongo.repository.internal;
package org.immutables.mongo.bson4gson;

import com.google.gson.internal.LazilyParsedNumber;
import org.bson.types.Decimal128;

import javax.annotation.concurrent.NotThreadSafe;
import java.io.Closeable;
import java.io.IOException;
import java.io.Writer;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.regex.Pattern;
import javax.annotation.concurrent.NotThreadSafe;
import org.bson.BsonBinary;
import org.bson.BsonRegularExpression;
import org.bson.types.Decimal128;
import org.bson.types.ObjectId;

import static com.google.common.base.Preconditions.checkNotNull;

/**
Expand All @@ -41,7 +39,8 @@
* @see <a href="http://bsonspec.org/">BSON spec</a>
*/
@NotThreadSafe
public class BsonWriter extends com.google.gson.stream.JsonWriter {
public class BsonWriter extends com.google.gson.stream.JsonWriter implements Wrapper<org.bson.BsonWriter> {

private static final Writer UNWRITABLE_WRITER = new Writer() {
@Override public void write(char[] buffer, int offset, int counter) {
throw new AssertionError();
Expand All @@ -61,6 +60,11 @@ public class BsonWriter extends com.google.gson.stream.JsonWriter {
this.delegate = checkNotNull(delegate, "delegate");
}

@Override
public org.bson.BsonWriter unwrap() {
return this.delegate;
}

@Override
public com.google.gson.stream.JsonWriter beginArray() throws IOException {
delegate.writeStartArray();
Expand Down Expand Up @@ -169,28 +173,34 @@ public com.google.gson.stream.JsonWriter value(Number value) throws IOException
return value(value.longValue());
}
if (value instanceof BigDecimal) {
String string = ((BigDecimal) value).toPlainString();
final BigDecimal decimal = (BigDecimal) value;
try {
return value(Decimal128.parse(string));
} catch (NumberFormatException ex) {
return value(new Decimal128(decimal));
} catch (NumberFormatException|AssertionError ex) {
// Decimal128 throws AssertionError instead of NumberFormatException for out of range values
// see https://jira.mongodb.org/browse/JAVA-2937
// fallback to serializing to string
return value(string);
return value(decimal.toPlainString());
}
}
if (value instanceof BigInteger) {
String string = value.toString();
final BigInteger integer = (BigInteger) value;
try {
return value(Decimal128.parse(string));
} catch (NumberFormatException ex) {
// BigDecimal is a wrapper for BigInteger anyway
BigDecimal decimal = new BigDecimal(integer);
return value(new Decimal128(decimal));
} catch (NumberFormatException|AssertionError ex) {
// Decimal128 throws AssertionError instead of NumberFormatException for out of range values
// see https://jira.mongodb.org/browse/JAVA-2937
// fallback to serializing to string
return value(string);
return value(integer.toString());
}
}
// by default we resort to floating point
return value(value.doubleValue());
}

public com.google.gson.stream.JsonWriter value(Decimal128 decimal) {
private com.google.gson.stream.JsonWriter value(Decimal128 decimal) {
delegate.writeDecimal128(decimal);
return this;
}
Expand All @@ -207,19 +217,4 @@ public void close() throws IOException {
}
}

public void valueBinary(byte[] data) {
delegate.writeBinaryData(new BsonBinary(data));
}

public void valueObjectId(byte[] data) {
delegate.writeObjectId(new ObjectId(data));
}

public void value(Pattern pattern) {
delegate.writeRegularExpression(new BsonRegularExpression(pattern.pattern()));
}

public void valueTimeInstant(long value) {
delegate.writeDateTime(value);
}
}
195 changes: 195 additions & 0 deletions mongo/src/org/immutables/mongo/bson4gson/Codecs.java
@@ -0,0 +1,195 @@
/*
Copyright 2013-2015 Immutables Authors and Contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package org.immutables.mongo.bson4gson;

import com.google.common.base.Preconditions;
import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import org.bson.AbstractBsonReader;
import org.bson.codecs.Codec;
import org.bson.codecs.DecoderContext;
import org.bson.codecs.EncoderContext;
import org.bson.codecs.configuration.CodecConfigurationException;
import org.bson.codecs.configuration.CodecRegistry;

import java.io.IOException;

/**
* Set of utilities to bridge <a href="http://bsonspec.org/">BSON</a> and
* <a href="https://github.com/google/gson">Gson</a> standard classes like
* {@link TypeAdapter} / {@link Codec}(s).
*/
public final class Codecs {

private Codecs() {
}

/**
* Build a TypeAdapter from {@link Codec} opposite of {@link #codecFromTypeAdapter(Class, TypeAdapter)}.
*
* @param codec existing codec
* @return type adapter which delegates calls to a codec.
*/
public static <T> TypeAdapter<T> typeAdapterFromCodec(final Codec<T> codec) {
Preconditions.checkNotNull(codec, "codec");
return new TypeAdapter<T>() {
@Override
public void write(JsonWriter out, T value) throws IOException {
BsonWriter writer = (BsonWriter) out;
org.bson.BsonWriter delegate = writer.unwrap();
codec.encode(delegate, value, EncoderContext.builder().build());
}

@Override
public T read(JsonReader in) throws IOException {
BsonReader reader = (BsonReader) in;
org.bson.BsonReader delegate = reader.unwrap();
return codec.decode(delegate, DecoderContext.builder().build());
}
};
}

/**
* Gson Factory which gives preference to existing adapters from {@code gson} instance. However,
* if type is not supported it will query {@link CodecRegistry} to create one (if possible).
*
* <p>This allows supporting Bson types by Gson natively (eg. for {@link org.bson.types.ObjectId}).
*
* @param registry existing registry which will be used if type is unknown to {@code gson}.
* @return factory which delegates to {@code registry} for unknown types.
*/
public static TypeAdapterFactory delegatingTypeAdapterFactory(final CodecRegistry registry) {
Preconditions.checkNotNull(registry, "registry");
return new TypeAdapterFactory() {
@Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
boolean hasAdapter;
try {
TypeAdapter<T> adapter = gson.getDelegateAdapter(this, type);
hasAdapter = !isReflectiveTypeAdapter(adapter);
} catch (IllegalArgumentException e) {
hasAdapter = false;
}

if (hasAdapter) {
return null;
}

try {
@SuppressWarnings("unchecked")
Codec<T> codec = (Codec<T>) registry.get(type.getRawType());
return typeAdapterFromCodec(codec);
} catch (CodecConfigurationException e1) {
return null;
}

}
};
}

/**
* Build a codec from {@link TypeAdapter}. Opposite of {@link #typeAdapterFromCodec(Codec)}.
*
* @param type type handled by this adapter
* @param adapter existing adapter
* @param <T> codec value type
* @throws CodecConfigurationException if adapter is not supported
* @return new instance of the codec which handles {@code type}.
*/
public static <T> Codec<T> codecFromTypeAdapter(Class<T> type, TypeAdapter<T> adapter) {
if (isReflectiveTypeAdapter(adapter)) {
throw new CodecConfigurationException(String.format("%s can't be build from %s " +
"(for type %s)", TypeAdapterCodec.class.getSimpleName(),
adapter.getClass().getName(), type.getName()));
}
return new TypeAdapterCodec<>(type, adapter);
}

/**
* Given existing {@code Gson} instance builds a {@link CodecRegistry}.
*
* @param gson preconfigured instance
* @return wrapper for {@code gson}.
*/
public static CodecRegistry codecRegistryFromGson(final Gson gson) {
Preconditions.checkNotNull(gson, "gson");
return new CodecRegistry() {
@Override
public <T> Codec<T> get(Class<T> clazz) {
return codecFromTypeAdapter(clazz, gson.getAdapter(clazz));
}
};
}

static <A> boolean isReflectiveTypeAdapter(TypeAdapter<A> adapter) {
Preconditions.checkNotNull(adapter, "adapter");
return adapter instanceof com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.Adapter;
}

/**
* Codec which delegates all calls to existing type adapter.
*
* @param <T> type handled by this codec
*/
private static class TypeAdapterCodec<T> implements Codec<T> {
private final Class<T> clazz;
private final TypeAdapter<T> adapter;

private TypeAdapterCodec(Class<T> type, TypeAdapter<T> adapter) {
this.clazz = Preconditions.checkNotNull(type, "type");
Preconditions.checkArgument(!isReflectiveTypeAdapter(adapter),
"Type adapter %s for type '%s' is not supported."
+ " This may happen when using default RepositorySetup.forUri and"
+ " META-INF/services/..TypeAdapterFactory files are not compiled or accessible."
+ " Alternatively this may happen if creating custom RepositorySetup with Gson instance,"
+ " which does not have type adapters registered.", adapter.getClass().getName(), type);
this.adapter = adapter;
}

@Override
public T decode(org.bson.BsonReader reader, DecoderContext decoderContext) {
if (!(reader instanceof AbstractBsonReader)) {
throw new UnsupportedOperationException(String.format("Only readers of type %s supported. Yours is %s",
AbstractBsonReader.class.getName(), reader.getClass().getName()));
}

try {
return adapter.read(new BsonReader((AbstractBsonReader) reader));
} catch (IOException e) {
throw new RuntimeException(String.format("Couldn't read %s", clazz), e);
}
}

@Override
public void encode(org.bson.BsonWriter writer, T value, EncoderContext encoderContext) {
try {
adapter.write(new BsonWriter(writer), value);
} catch (IOException e) {
throw new RuntimeException(String.format("Couldn't write value of class %s: %s", clazz.getName(), value), e);
}
}

@Override
public Class<T> getEncoderClass() {
return clazz;
}

}
}

0 comments on commit 37b43b5

Please sign in to comment.