package org.mongodb.morphia.converters; import com.mongodb.DBObject; import org.mongodb.morphia.logging.Logger; import org.mongodb.morphia.logging.MorphiaLoggerFactory; import org.mongodb.morphia.mapping.MappedField; import org.mongodb.morphia.mapping.Mapper; import org.mongodb.morphia.mapping.MapperOptions; import org.mongodb.morphia.mapping.MappingException; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; import static java.lang.String.format; /** * Defines a bundle of converters */ public abstract class Converters { private static final Logger LOG = MorphiaLoggerFactory.get(Converters.class); private final Mapper mapper; private final List<TypeConverter> untypedTypeEncoders = new LinkedList<TypeConverter>(); private final Map<Class, List<TypeConverter>> tcMap = new ConcurrentHashMap<Class, List<TypeConverter>>(); private final List<Class<? extends TypeConverter>> registeredConverterClasses = new ArrayList<Class<? extends TypeConverter>>(); /** * Creates a bundle with a particular Mapper. * * @param mapper the Mapper to use */ public Converters(final Mapper mapper) { this.mapper = mapper; } /** * Adds a TypeConverter to this bundle. * * @param clazz the converter to add * @return the new instance */ public TypeConverter addConverter(final Class<? extends TypeConverter> clazz) { return addConverter(mapper.getOptions().getObjectFactory().createInstance(clazz)); } /** * Add a type converter. If it is a duplicate for an existing type, it will override that type. * * @param tc the converter to add * @return the TypeConverter passed in */ public TypeConverter addConverter(final TypeConverter tc) { if (tc.getSupportedTypes() != null) { for (final Class c : tc.getSupportedTypes()) { addTypedConverter(c, tc); } } else { untypedTypeEncoders.add(tc); } registeredConverterClasses.add(tc.getClass()); tc.setMapper(mapper); return tc; } /** * decode the {@link com.mongodb.DBObject} and provide the corresponding java (type-safe) object * <br><b>NOTE: mf might be null</b> * * @param c the class to create and populate * @param fromDBObject the DBObject to use when populating the new instance * @param mf the MappedField that contains the metadata useful for decoding * @return the new instance */ public Object decode(final Class c, final Object fromDBObject, final MappedField mf) { Class toDecode = c; if (toDecode == null) { toDecode = fromDBObject.getClass(); } return getEncoder(toDecode).decode(toDecode, fromDBObject, mf); } /** * encode the type safe java object into the corresponding {@link com.mongodb.DBObject} * * @param o The object to encode * @return the encoded version of the object */ public Object encode(final Object o) { if (o == null) { return null; } return encode(o.getClass(), o); } /** * encode the type safe java object into the corresponding {@link com.mongodb.DBObject} * * @param c The type to use when encoding * @param o The object to encode * @return the encoded version of the object */ public Object encode(final Class c, final Object o) { return getEncoder(c).encode(o); } /** * Creates an entity and populates its state based on the dbObject given. This method is primarily an internal method. Reliance on * this method may break your application in future releases. * * @param dbObj the object state to use * @param mf the MappedField containing the metadata to use when decoding in to a field * @param targetEntity then entity to hold the state from the database */ public void fromDBObject(final DBObject dbObj, final MappedField mf, final Object targetEntity) { final Object object = mf.getDbObjectValue(dbObj); if (object != null) { final TypeConverter enc = getEncoder(mf); final Object decodedValue = enc.decode(mf.getType(), object, mf); try { mf.setFieldValue(targetEntity, decodedValue); } catch (IllegalArgumentException e) { throw new MappingException(format("Error setting value from converter (%s) for %s to %s", enc.getClass().getSimpleName(), mf.getFullName(), decodedValue), e); } } } /** * @param field the field to check with * @return true if there is a converter for the type of the field */ public boolean hasDbObjectConverter(final MappedField field) { final TypeConverter converter = getEncoder(field); return converter != null && !(converter instanceof IdentityConverter) && !(converter instanceof SimpleValueConverter); } /** * @param c the type to check * @return true if there is a converter for the type */ public boolean hasDbObjectConverter(final Class c) { final TypeConverter converter = getEncoder(c); return converter != null && !(converter instanceof IdentityConverter) && !(converter instanceof SimpleValueConverter); } /** * @param o the object/type to check * @return true if there is a SimpleValueConverter for the type represented by o * @see SimpleValueConverter */ public boolean hasSimpleValueConverter(final Object o) { if (o == null) { return false; } if (o instanceof Class) { return hasSimpleValueConverter((Class) o); } else if (o instanceof MappedField) { return hasSimpleValueConverter((MappedField) o); } else { return hasSimpleValueConverter(o.getClass()); } } /** * @param c the type to check * @return true if there is a SimpleValueConverter for the type represented by c * @see SimpleValueConverter */ public boolean hasSimpleValueConverter(final Class c) { return (getEncoder(c) instanceof SimpleValueConverter); } /** * @param c the type to check * @return true if there is a SimpleValueConverter for the type represented by c * @see SimpleValueConverter */ public boolean hasSimpleValueConverter(final MappedField c) { return (getEncoder(c) instanceof SimpleValueConverter); } /** * @param tcClass the type to check * @return true if a converter of this type has been registered */ public boolean isRegistered(final Class<? extends TypeConverter> tcClass) { return registeredConverterClasses.contains(tcClass); } /** * Removes the type converter. * * @param tc the converter to remove */ public void removeConverter(final TypeConverter tc) { if (tc.getSupportedTypes() == null) { untypedTypeEncoders.remove(tc); registeredConverterClasses.remove(tc.getClass()); } else { for (final Entry<Class, List<TypeConverter>> entry : tcMap.entrySet()) { List<TypeConverter> list = entry.getValue(); if (list.contains(tc)) { list.remove(tc); } if (list.isEmpty()) { tcMap.remove(entry.getKey()); } } registeredConverterClasses.remove(tc.getClass()); } } /** * Converts an entity to a DBObject * * @param containingObject The object to convert * @param mf the MappedField to extract * @param dbObj the DBObject to populate * @param opts the options to apply */ public void toDBObject(final Object containingObject, final MappedField mf, final DBObject dbObj, final MapperOptions opts) { final Object fieldValue = mf.getFieldValue(containingObject); final TypeConverter enc = getEncoder(fieldValue, mf); final Object encoded = enc.encode(fieldValue, mf); if (encoded != null || opts.isStoreNulls()) { dbObj.put(mf.getNameToStore(), encoded); } } protected TypeConverter getEncoder(final Class c) { final List<TypeConverter> tcs = tcMap.get(c); if (tcs != null) { if (tcs.size() > 1) { LOG.warning("Duplicate converter for " + c + ", returning first one from " + tcs); } return tcs.get(0); } for (final TypeConverter tc : untypedTypeEncoders) { if (tc.canHandle(c)) { return tc; } } return null; } protected TypeConverter getEncoder(final Object val, final MappedField mf) { List<TypeConverter> tcs = null; if (val != null) { tcs = tcMap.get(val.getClass()); } if (tcs == null || (!tcs.isEmpty() && tcs.get(0) instanceof IdentityConverter)) { tcs = tcMap.get(mf.getType()); } if (tcs != null) { if (tcs.size() > 1) { LOG.warning("Duplicate converter for " + mf.getType() + ", returning first one from " + tcs); } return tcs.get(0); } for (final TypeConverter tc : untypedTypeEncoders) { if (tc.canHandle(mf) || (val != null && tc.isSupported(val.getClass(), mf))) { return tc; } } return null; } private void addTypedConverter(final Class type, final TypeConverter tc) { if (tcMap.containsKey(type)) { tcMap.get(type).add(0, tc); LOG.warning("Added duplicate converter for " + type + " ; " + tcMap.get(type)); } else { final List<TypeConverter> values = new ArrayList<TypeConverter>(); values.add(tc); tcMap.put(type, values); } } private TypeConverter getEncoder(final MappedField mf) { return getEncoder(null, mf); } }