package br.com.arsmachina.eloquentia.dao.mongodb; import java.io.IOException; import java.io.Serializable; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.List; import org.mongojack.DBCursor; import org.mongojack.DBQuery; import org.mongojack.JacksonDBCollection; import org.mongojack.MongoCollection; import org.mongojack.MongoJsonMappingException; import org.mongojack.WriteResult; import org.mongojack.internal.MongoJackModule; import org.mongojack.internal.object.BsonObjectGenerator; import br.com.arsmachina.dao.DAO; import br.com.arsmachina.dao.SortCriterion; import br.com.arsmachina.eloquentia.entity.Page; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.mongodb.BasicDBObject; import com.mongodb.DB; import com.mongodb.DBObject; import com.mongodb.MongoClient; import com.mongodb.MongoException; /** * Abstract DAO implementation. * * @author Thiago H. de Paula Figueiredo (http://machina.com.br/thiago) */ public abstract class AbstractDAOImpl<T, K extends Serializable> implements DAO<T, K> { final private static DB DB; final private JacksonDBCollection<T, K> dbCollection; final private Class<T> entityClass; final private Class<K> idClass; final private PrimaryKeyEncoder<T, K> primaryKeyEncoder; final private ObjectMapper objectMapper = MongoJackModule.configure(new ObjectMapper()); static { MongoClient mongoClient; try { mongoClient = new MongoClient(); } catch (UnknownHostException e) { throw new ExceptionInInitializerError(e); } DB = mongoClient.getDB("eloquentia"); } /** * Single constructor of this class. */ @SuppressWarnings("unchecked") public AbstractDAOImpl(PrimaryKeyEncoder<T, K> idValueExtractor) { this.primaryKeyEncoder = idValueExtractor; final Type genericSuperclass = getClass().getGenericSuperclass(); final ParameterizedType parameterizedType = ((ParameterizedType) genericSuperclass); entityClass = (Class<T>) parameterizedType.getActualTypeArguments()[0]; idClass = (Class<K>) parameterizedType.getActualTypeArguments()[1]; MongoCollection mongoCollection = entityClass.getAnnotation(MongoCollection.class); String collection = entityClass.getSimpleName().toLowerCase(); if (mongoCollection != null) { collection = mongoCollection.name(); } dbCollection = JacksonDBCollection.wrap(DB.getCollection(collection), entityClass, idClass); } public void delete(T object) { delete(primaryKeyEncoder.get(object)); } public void delete(K id) { dbCollection.removeById(id); } public void save(T object) { if (isPersistent(object)) { update(object); } else { final WriteResult<T, K> writeResult = dbCollection.insert(object); // FIXME: it seems insert() returns n=0 even when the write is successful. Investigate. // assertNumberOfChanges(writeResult); primaryKeyEncoder.set(object, writeResult.getSavedId()); } } public T update(T object) { if (!isPersistent(object)) { throw new RuntimeException("Object is not persistent for update: " + object); } K id = primaryKeyEncoder.get(object); final WriteResult<T, K> writeResult = dbCollection.updateById(id, object); assertNumberOfChanges(writeResult); return object; } protected void assertNumberOfChanges(final WriteResult<T, K> writeResult) { final int n = writeResult.getN(); if (n != 1) { throw new MongoException(String.format("AbstractDAOImpl.update() changed %d records instead of 1", n)); } } /** * Does nothing, as MongoDB doesn't have any persistence context or attached state. */ public void evict(T object) { } public boolean isPersistent(T object) { K id = primaryKeyEncoder.get(object); return id != null; } public long countAll() { return dbCollection.count(); } public T findById(K id) { return dbCollection.findOneById(id); } public List<T> findByIds(K... ids) { DBCursor<T> cursor = dbCollection.find(DBQuery.in("_id", (Object[]) ids)); return toList(cursor); } /** * Returns a {@link List} containing the objects returned in a {@link DBCursor}. * @param cursor a {@link DBCursor}. * @return a {@link List}. */ protected List<T> toList(DBCursor<T> cursor) { List<T> list = new ArrayList<T>(); for (T object : cursor) { list.add(object); } return list; } public List<T> findAll() { final DBCursor<T> cursor = dbCollection.find(); addSortCriteria(cursor, getDefaultSortCriteria()); return toList(cursor); } public List<T> findByExample(T example) { final DBCursor<T> cursor = dbCollection.find(convertToBasicDbObject(example)); addSortCriteria(cursor, getDefaultSortCriteria()); return toList(cursor); } public T refresh(T object) { return findById(primaryKeyEncoder.get(object)); } /** * Just returns the object unchanged, as MongoDB doesn't have anything similar to the * attached state of JPA. */ public T reattach(T object) { return object; } public List<T> findAll(int firstResult, int maxResults, SortCriterion... sortCriteria) { final DBCursor<T> cursor = dbCollection.find().skip(firstResult).limit(maxResults); addSortCriteria(cursor, sortCriteria); return toList(cursor); } /** * Adds sort criteria to a {@link DBCursor}. * @param cursor a {@link DBCursor}. * @param sortingCriteria an array of {@link SortCriterion}. */ protected void addSortCriteria(final DBCursor<T> cursor, SortCriterion... sortingCriteria) { if (sortingCriteria != null && sortingCriteria.length > 0) { BasicDBObject orderBy = new BasicDBObject(); for (SortCriterion sortCriterion : sortingCriteria) { orderBy.append(sortCriterion.getProperty(), sortCriterion.isAscending() ? 1 : -1); } cursor.sort(orderBy); } } /** * Adds skip and limit to a {@link DBCursor}. * @param cursor a {@link DBCursor}. * @param firstResult an <code>int</code> containg the number of results to be skipped. * @param maxResults an <code>int</code> containg the maximum number of results to be returned. */ protected void addSkipAndLimit(DBCursor<Page> cursor, int firstResult, int maxResults) { cursor.skip(firstResult).limit(maxResults); } /** * Returns the {@link JacksonDBCollection} for this entity class. * @return a {@link JacksonDBCollection}. */ protected JacksonDBCollection<T, K> getDbCollection() { return dbCollection; } /** * Copied from {@link JacksonDBCollection#convertToBasicDbObject(T)} * @param object * @return * @throws MongoException */ protected DBObject convertToBasicDbObject(T object) throws MongoException { if (object == null) { return null; } BsonObjectGenerator generator = new BsonObjectGenerator(); try { objectMapper.writeValue(generator, object); } catch (JsonMappingException e) { throw new MongoJsonMappingException(e); } catch (IOException e) { // This shouldn't happen throw new MongoException("Unknown error occurred converting BSON to object", e); } return generator.getDBObject(); } /** * Extracts the object from a {@link DBCursor} representing a search with 0 or 1 results. * If there are no results, <code>null</code> is returned. * * @param cursor a {@link DBCursor}. * @return a <code>T</code> instance or <code>null</code>. */ protected T objectOrNull(DBCursor<T> cursor) { return cursor.hasNext() ? cursor.next() : null; } }