/** * Copyright (C) 2010 Olafur Gauti Gudmundsson * <p/> * 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 * <p/> * http://www.apache.org/licenses/LICENSE-2.0 * <p/> * 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.mongodb.morphia.ext; import com.mongodb.BasicDBList; import com.mongodb.BasicDBObject; import com.mongodb.DBCollection; import com.mongodb.DBObject; import com.mongodb.DefaultDBDecoder; import com.mongodb.DefaultDBEncoder; import org.bson.types.ObjectId; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mongodb.morphia.TestBase; import org.mongodb.morphia.annotations.Converters; import org.mongodb.morphia.annotations.Embedded; import org.mongodb.morphia.annotations.Entity; import org.mongodb.morphia.annotations.Id; import org.mongodb.morphia.converters.IntegerConverter; import org.mongodb.morphia.converters.SimpleValueConverter; import org.mongodb.morphia.converters.TypeConverter; import org.mongodb.morphia.entities.EntityWithListsAndArrays; import org.mongodb.morphia.mapping.MappedField; import javax.activation.MimeType; import javax.activation.MimeTypeParseException; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.notNullValue; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; /** * @author Scott Hernandez */ public class CustomConvertersTest extends TestBase { @Test public void customerIteratorConverter() { getMorphia().getMapper().getConverters().addConverter(ListToMapConvert.class); getMorphia().getMapper().getOptions().setStoreEmpties(false); getMorphia().getMapper().getOptions().setStoreNulls(false); getMorphia().map(EntityWithListsAndArrays.class); final EntityWithListsAndArrays entity = new EntityWithListsAndArrays(); entity.setListOfStrings(Arrays.asList("string 1", "string 2", "string 3")); getDs().save(entity); final DBObject dbObject = getDs().getCollection(EntityWithListsAndArrays.class).findOne(); Assert.assertFalse(dbObject.get("listOfStrings") instanceof BasicDBList); final EntityWithListsAndArrays loaded = getDs().find(EntityWithListsAndArrays.class).get(); Assert.assertEquals(entity.getListOfStrings(), loaded.getListOfStrings()); } @Test public void mimeType() throws UnknownHostException, MimeTypeParseException { getMorphia().map(MimeTyped.class); getDs().ensureIndexes(); MimeTyped entity = new MimeTyped(); entity.name = "test name"; entity.mimeType = new MimeType("text/plain"); //MimeTypeParseException final DBObject dbObject = getMorphia().toDBObject(entity); assertEquals("text/plain", dbObject.get("mimeType")); getDs().save(entity); // FAILS WITH ERROR HERE } @Before public void setup() { getMorphia().map(MyEntity.class, ValueObject.class); } @Test public void shouldUseSuppliedConverterToEncodeAndDecodeObject() { // given getMorphia().map(CharEntity.class); // when getDs().save(new CharEntity()); // then check the representation in the database is a number final BasicDBObject dbObj = (BasicDBObject) getDs().getCollection(CharEntity.class).findOne(); assertThat(dbObj.get("c"), is(instanceOf(int.class))); assertThat(dbObj.getInt("c"), is((int) 'a')); // then check CharEntity can be decoded from the database final CharEntity ce = getDs().find(CharEntity.class).get(); assertThat(ce.c, is(notNullValue())); assertThat(ce.c.charValue(), is('a')); } /** * This test is green when {@link MyEntity#valueObject} is annotated with {@code @Property}, as in this case the field is not * serialized * at all. However, the bson encoder would fail to encode the object of type ValueObject (as shown by {@link * #testFullBSONSerialization()}). */ @Test public void testDBObjectSerialization() { final MyEntity entity = new MyEntity(1L, new ValueObject(2L)); final DBObject dbObject = getMorphia().toDBObject(entity); assertEquals(new BasicDBObject("_id", 1L).append("valueObject", 2L), dbObject); assertEquals(entity, getMorphia().fromDBObject(getDs(), MyEntity.class, dbObject)); } /** * This test shows the full serialization, including bson encoding/decoding. */ @Test public void testFullBSONSerialization() { final MyEntity entity = new MyEntity(1L, new ValueObject(2L)); final DBObject dbObject = getMorphia().toDBObject(entity); final byte[] data = new DefaultDBEncoder().encode(dbObject); final DBObject decoded = new DefaultDBDecoder().decode(data, (DBCollection) null); final MyEntity actual = getMorphia().fromDBObject(getDs(), MyEntity.class, decoded); assertEquals(entity, actual); } static class CharacterToByteConverter extends TypeConverter implements SimpleValueConverter { public CharacterToByteConverter() { super(Character.class, char.class); } @Override public Object decode(final Class targetClass, final Object fromDBObject, final MappedField optionalExtraInfo) { if (fromDBObject == null) { return null; } final IntegerConverter intConverter = new IntegerConverter(); final Integer i = (Integer) intConverter.decode(targetClass, fromDBObject, optionalExtraInfo); return (char) i.intValue(); } @Override public Object encode(final Object value, final MappedField optionalExtraInfo) { final Character c = (Character) value; return (int) c.charValue(); } } @Converters(CharacterToByteConverter.class) static class CharEntity { private final Character c = 'a'; @Id private ObjectId id = new ObjectId(); } /** * This test shows an issue with an {@code @Embedded} class A inheriting from an {@code @Embedded} class B that both have a Converter * assigned (A has AConverter, B has BConverter). <p> When an object (here MyEntity) has a property/field of type A and is * deserialized, * the deserialization fails with a "org.mongodb.morphia.mapping.MappingException: No usable constructor for A" . </p> */ @Entity(noClassnameStored = true) private static class MyEntity { @Id private Long id; @Embedded private ValueObject valueObject; public MyEntity() { } public MyEntity(final Long id, final ValueObject valueObject) { this.id = id; this.valueObject = valueObject; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((id == null) ? 0 : id.hashCode()); result = prime * result + ((valueObject == null) ? 0 : valueObject.hashCode()); return result; } @Override public boolean equals(final Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final MyEntity other = (MyEntity) obj; if (id == null) { if (other.id != null) { return false; } } else if (!id.equals(other.id)) { return false; } if (valueObject == null) { if (other.valueObject != null) { return false; } } else if (!valueObject.equals(other.valueObject)) { return false; } return true; } } @Embedded @Converters(ValueObject.BConverter.class) private static class ValueObject { private long value; public ValueObject() { } public ValueObject(final long value) { this.value = value; } static class BConverter extends TypeConverter implements SimpleValueConverter { public BConverter() { this(ValueObject.class); } public BConverter(final Class<? extends ValueObject> clazz) { super(clazz); } protected ValueObject create(final Long source) { return new ValueObject(source); } @Override protected boolean isSupported(final Class<?> c, final MappedField optionalExtraInfo) { return c.isAssignableFrom(ValueObject.class); } @Override public ValueObject decode(final Class targetClass, final Object fromDBObject, final MappedField optionalExtraInfo) { if (fromDBObject == null) { return null; } return create((Long) fromDBObject); } @Override public Long encode(final Object value, final MappedField optionalExtraInfo) { if (value == null) { return null; } final ValueObject source = (ValueObject) value; return source.value; } } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + (int) (value ^ (value >>> 32)); return result; } @Override public boolean equals(final Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final ValueObject other = (ValueObject) obj; return value == other.value; } @Override public String toString() { return getClass().getSimpleName() + " [value=" + value + "]"; } } @Entity @Converters(MimeTypeConverter.class) public static class MimeTyped { @Id private ObjectId id; private String name; private javax.activation.MimeType mimeType; } public static class MimeTypeConverter extends TypeConverter { public MimeTypeConverter() { super(MimeType.class); } @Override public Object decode(final Class targetClass, final Object fromDBObject, final MappedField optionalExtraInfo) { try { return new MimeType(((BasicDBObject) fromDBObject).getString("mimeType")); } catch (MimeTypeParseException ex) { return new MimeType(); } } @Override public Object encode(final Object value, final MappedField optionalExtraInfo) { return ((MimeType) value).getBaseType(); } } @SuppressWarnings("unchecked") private static class ListToMapConvert extends TypeConverter { @Override protected boolean isSupported(final Class c, final MappedField mf) { return (mf != null) && mf.isMultipleValues() && !mf.isMap(); } @Override public Object decode(final Class<?> targetClass, final Object fromDBObject, final MappedField optionalExtraInfo) { if (fromDBObject != null) { Map<String, Object> map = (Map<String, Object>) fromDBObject; List<Object> list = new ArrayList<Object>(map.size()); for (Entry<String, Object> entry : map.entrySet()) { list.add(Integer.parseInt(entry.getKey()), entry.getValue()); } return list; } return null; } @Override public Object encode(final Object value, final MappedField optionalExtraInfo) { if (value != null) { Map<String, Object> map = new LinkedHashMap<String, Object>(); List<Object> list = (List<Object>) value; for (int i = 0; i < list.size(); i++) { map.put(i + "", list.get(i)); } return map; } return null; } } }