/*
* Copyright (c) 2011-2014 Jeppetto and Jonathan Thompson
*
* 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.iternine.jeppetto.dao.mongodb;
import org.iternine.jeppetto.dao.AccessControlContext;
import org.iternine.jeppetto.dao.AccessControlContextProvider;
import org.iternine.jeppetto.dao.AccessControlException;
import org.iternine.jeppetto.dao.AccessControlDAO;
import org.iternine.jeppetto.dao.AccessType;
import org.iternine.jeppetto.dao.Condition;
import org.iternine.jeppetto.dao.ConditionType;
import org.iternine.jeppetto.dao.FailedBatchException;
import org.iternine.jeppetto.dao.JeppettoException;
import org.iternine.jeppetto.dao.NoSuchItemException;
import org.iternine.jeppetto.dao.OptimisticLockException;
import org.iternine.jeppetto.dao.Projection;
import org.iternine.jeppetto.dao.ProjectionType;
import org.iternine.jeppetto.dao.QueryModel;
import org.iternine.jeppetto.dao.QueryModelDAO;
import org.iternine.jeppetto.dao.Sort;
import org.iternine.jeppetto.dao.SortDirection;
import org.iternine.jeppetto.dao.TooManyItemsException;
import org.iternine.jeppetto.dao.annotation.AccessControl;
import org.iternine.jeppetto.dao.annotation.Accessor;
import org.iternine.jeppetto.dao.annotation.Creator;
import org.iternine.jeppetto.dao.mongodb.enhance.DBObjectUtil;
import org.iternine.jeppetto.dao.mongodb.enhance.DirtyableDBObject;
import org.iternine.jeppetto.dao.mongodb.enhance.DirtyableDBObjectList;
import org.iternine.jeppetto.dao.mongodb.enhance.DirtyableDBObjectMap;
import org.iternine.jeppetto.dao.mongodb.enhance.EnhancerHelper;
import org.iternine.jeppetto.dao.mongodb.enhance.MongoDBDecoder;
import org.iternine.jeppetto.dao.mongodb.enhance.UpdateObject;
import org.iternine.jeppetto.dao.mongodb.projections.ProjectionCommands;
import org.iternine.jeppetto.enhance.Enhancer;
import com.mongodb.BasicDBObject;
import com.mongodb.DB;
import com.mongodb.DBCollection;
import com.mongodb.DBCursor;
import com.mongodb.DBDecoder;
import com.mongodb.DBDecoderFactory;
import com.mongodb.DBObject;
import com.mongodb.DuplicateKeyException;
import com.mongodb.MongoException;
import com.mongodb.WriteConcern;
import org.bson.types.ObjectId;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
/**
* Provides a QueryModelDAO implementation for Mongo.
* <p/>
* This QueryModelDAO implementation for MongoDB provides bi-directional ODM (Object to Document Mapping) as well
* as the rich query capabilities found in the core org.iternine.jeppetto.dao package.
*
* Instantiation requires a daoProperties Map<String, Object> that will look for the following keys (and expected
* values):
*
* <table>
* <tr>
* <td>Key</td>
* <td>Required?</td>
* <td>Description</td>
* </tr>
* <tr>
* <td>db</td>
* <td>Yes</td>
* <td>Instance of a configured com.mongodb.DB</td>
* </tr>
* <tr>
* <td>collection</td>
* <td>No</td>
* <td>Name of the backing collection for this object. Defaults to the name of the entity class.</td>
* </tr>
* <tr>
* <td>viewOf</td>
* <td>No</td>
* <td>Name of the a class of which this DAO's entity class is a view.</td>
* </tr>
* <tr>
* <td>uniqueIndexes</td>
* <td>No</td>
* <td>List<String> of various MongoDB index values that will be ensured to exist and must be unique.</td>
* </tr>
* <tr>
* <td>nonUniqueIndexes</td>
* <td>No</td>
* <td>List<String> of various MongoDB index values that will be ensured to exist and need not be unique.</td>
* </tr>
* <tr>
* <td>optimisticLockEnabled</td>
* <td>No</td>
* <td>Boolean to indicate if instances of the tracked type should be protected by a Jeppetto-managed lock version field.</td>
* </tr>
* <tr>
* <td>shardKeyPattern</td>
* <td>No</td>
* <td>A comma-separated string of fields that are used to determine the shard key(s) for the collection.</td>
* </tr>
* <tr>
* <td>saveNulls</td>
* <td>No</td>
* <td>Boolean to indicate whether null fields should be included in MongoDB documents.</td>
* </tr>
* <tr>
* <td>writeConcern</td>
* <td>No</td>
* <td>String, one of the values as indicated at http://www.mongodb.org/display/DOCS/Replica+Set+Semantics. If
* not specified, the DAO defaults to "SAFE".</td>
* </tr>
* <tr>
* <td>showQueries</td>
* <td>No</td>
* <td>Boolean to indicate if executed queries should be logged. Note that logging will need to be enabled for
* the DAO's package as well.</td>
* </tr>
* </table>
*/
public class MongoDBQueryModelDAO<T, ID>
implements QueryModelDAO<T, ID>, AccessControlDAO<T, ID> {
//-------------------------------------------------------------
// Constants
//-------------------------------------------------------------
private static final String ID_FIELD = "_id";
private static final String OPTIMISTIC_LOCK_VERSION_FIELD = "__olv";
private static final String ACCESS_CONTROL_FIELD = "__acl";
private static final Pattern READ_PATTERN = Pattern.compile("^R");
//-------------------------------------------------------------
// Variables - Private
//-------------------------------------------------------------
private DBCollection dbCollection;
private Enhancer<T> dirtyableDBObjectEnhancer;
private BasicDBObject fieldsToRetrieve;
private DBDecoderFactory decoderFactory;
private AccessControlContextProvider accessControlContextProvider;
private Map<String, Set<String>> uniqueIndexes;
private boolean optimisticLockEnabled;
private List<String> shardKeys;
// private boolean saveNulls;
private WriteConcern defaultWriteConcern;
private Logger queryLogger;
private Enhancer<T> updateObjectEnhancer;
//-------------------------------------------------------------
// Constructors
//-------------------------------------------------------------
protected MongoDBQueryModelDAO(Class<T> entityClass, Map<String, Object> daoProperties) {
this(entityClass, daoProperties, null);
}
@SuppressWarnings( { "unchecked" })
protected MongoDBQueryModelDAO(Class<T> entityClass, Map<String, Object> daoProperties,
AccessControlContextProvider accessControlContextProvider) {
String collectionName = daoProperties.containsKey("collection") ? (String) daoProperties.get("collection")
: entityClass.getSimpleName();
this.dbCollection = ((DB) daoProperties.get("db")).getCollection(collectionName);
this.dirtyableDBObjectEnhancer = EnhancerHelper.getDirtyableDBObjectEnhancer(entityClass);
this.decoderFactory = new DBDecoderFactory() {
@Override
public DBDecoder create() {
return new MongoDBDecoder(dirtyableDBObjectEnhancer.getEnhancedClass());
}
};
this.accessControlContextProvider = accessControlContextProvider;
this.uniqueIndexes = ensureIndexes((List<String>) daoProperties.get("uniqueIndexes"), true);
ensureIndexes((List<String>) daoProperties.get("nonUniqueIndexes"), false);
this.optimisticLockEnabled = Boolean.parseBoolean((String) daoProperties.get("optimisticLockEnabled"));
this.shardKeys = extractShardKeys((String) daoProperties.get("shardKeyPattern"));
// this.saveNulls = Boolean.parseBoolean((String) daoProperties.get("saveNulls"));
this.fieldsToRetrieve = identifyFieldsToRetrieve(entityClass, (String) daoProperties.get("viewOf"));
if (daoProperties.containsKey("writeConcern")) {
this.defaultWriteConcern = WriteConcern.valueOf((String) daoProperties.get("writeConcern"));
} else {
this.defaultWriteConcern = WriteConcern.SAFE;
}
if (Boolean.parseBoolean((String) daoProperties.get("showQueries"))) {
queryLogger = LoggerFactory.getLogger(getClass());
}
this.updateObjectEnhancer = EnhancerHelper.getUpdateObjectEnhancer(getCollectionClass());
}
//-------------------------------------------------------------
// Implementation - GenericDAO
//-------------------------------------------------------------
@Override
public T findById(ID id)
throws NoSuchItemException, JeppettoException {
try {
QueryModel queryModel = new QueryModel();
queryModel.addCondition(buildIdCondition(id));
if (accessControlContextProvider != null) {
queryModel.setAccessControlContext(accessControlContextProvider.getCurrent());
}
return findUniqueUsingQueryModel(queryModel);
} catch (IllegalArgumentException e) {
throw new NoSuchItemException(getCollectionClass().getSimpleName(), id.toString());
}
}
@Override
public Iterable<T> findByIds(ID... ids)
throws JeppettoException {
QueryModel queryModel = new QueryModel();
queryModel.addCondition(buildIdCondition(Arrays.asList(ids)));
if (accessControlContextProvider != null) {
queryModel.setAccessControlContext(accessControlContextProvider.getCurrent());
}
return findUsingQueryModel(queryModel);
}
@Override
public Iterable<T> findAll()
throws JeppettoException {
QueryModel queryModel = new QueryModel();
if (accessControlContextProvider != null) {
queryModel.setAccessControlContext(accessControlContextProvider.getCurrent());
}
return findUsingQueryModel(queryModel);
}
@Override
public void save(T entity)
throws OptimisticLockException, JeppettoException {
T enhancedEntity = dirtyableDBObjectEnhancer.enhance(entity);
DirtyableDBObject dbo = (DirtyableDBObject) enhancedEntity;
if (dbo.isPersisted(dbCollection)) {
if (accessControlContextProvider != null) {
verifyWriteAllowed(dbo, accessControlContextProvider.getCurrent());
}
} else {
if (dbo.get(ID_FIELD) == null) {
dbo.put(ID_FIELD, new ObjectId()); // If the id isn't explicitly set, assume intent is for mongo ids
}
if (accessControlContextProvider != null) {
assessAndAssignAccessControl(dbo, accessControlContextProvider.getCurrent());
}
}
DBObject identifyingQuery = buildIdentifyingQuery(dbo);
if (MongoDBSession.isActive()) {
MongoDBSession.trackForSave(this, identifyingQuery, enhancedEntity, createIdentifyingQueries(dbo));
} else {
trueSave(identifyingQuery, dbo);
}
}
@Override
public void delete(T entity)
throws JeppettoException {
DBObject dbo = (DBObject) dirtyableDBObjectEnhancer.enhance(entity);
if (accessControlContextProvider != null) {
verifyWriteAllowed(dbo, accessControlContextProvider.getCurrent());
}
DBObject identifyingQuery = buildIdentifyingQuery(dbo);
for (String shardKey : shardKeys) {
identifyingQuery.put(shardKey, dbo.get(shardKey));
}
deleteByIdentifyingQuery(identifyingQuery);
}
@Override
public void deleteById(ID id)
throws JeppettoException {
QueryModel queryModel = new QueryModel();
queryModel.addCondition(buildIdCondition(id));
if (accessControlContextProvider != null) {
queryModel.setAccessControlContext(accessControlContextProvider.getCurrent());
}
deleteByIdentifyingQuery(buildQueryObject(queryModel, AccessType.ReadWrite));
}
@Override
public void deleteByIds(ID... ids)
throws FailedBatchException, JeppettoException {
QueryModel queryModel = new QueryModel();
queryModel.addCondition(buildIdCondition(Arrays.asList(ids)));
if (accessControlContextProvider != null) {
queryModel.setAccessControlContext(accessControlContextProvider.getCurrent());
}
deleteByIdentifyingQuery(buildQueryObject(queryModel, AccessType.ReadWrite));
}
@Override
public <U extends T> U getUpdateObject() {
T updateObject = updateObjectEnhancer.newInstance();
((UpdateObject) updateObject).setPrefix(""); // Root object, so start with an empty prefix.
//noinspection unchecked
return (U) updateObject;
}
@Override
public <U extends T> T updateById(U updateObject, ID id)
throws JeppettoException {
throw new RuntimeException("To be implemented using findAndModify...");
}
@Override
public <U extends T> Iterable<T> updateByIds(U updateObject, ID... ids)
throws FailedBatchException, JeppettoException {
QueryModel queryModel = new QueryModel();
queryModel.addCondition(buildIdCondition(Arrays.asList(ids)));
if (accessControlContextProvider != null) {
queryModel.setAccessControlContext(accessControlContextProvider.getCurrent());
}
updateUsingQueryModel(updateObject, queryModel);
return null; // TODO: implement...also, deal w/ modified FailedBatchException
}
@Override
public void flush()
throws JeppettoException {
if (MongoDBSession.isActive()) {
MongoDBSession.flush(this);
}
}
//-------------------------------------------------------------
// Implementation - QueryModelDAO
//-------------------------------------------------------------
public T findUniqueUsingQueryModel(QueryModel queryModel)
throws NoSuchItemException, TooManyItemsException, JeppettoException {
// Need to revisit te way caching works as it will miss some items...focus only on the identifyingQuery
// instead of secondary cache keys...
MongoDBCommand command = buildCommand(queryModel, AccessType.Read);
if (MongoDBSession.isActive()) {
// noinspection unchecked
T cached = (T) MongoDBSession.getObjectFromCache(dbCollection.getName(), command.getQuery());
if (cached != null) {
DBObject identifyingQuery = buildIdentifyingQuery((DBObject) cached);
MongoDBSession.trackForSave(this, identifyingQuery, cached, createIdentifyingQueries((DBObject) cached));
return cached;
}
}
// noinspection unchecked
T result = (T) command.singleResult(dbCollection);
((DirtyableDBObject) result).markPersisted(dbCollection);
if (MongoDBSession.isActive()) {
DBObject identifyingQuery = buildIdentifyingQuery((DBObject) result);
MongoDBSession.trackForSave(this, identifyingQuery, result, createIdentifyingQueries((DBObject) result));
}
return result;
}
public Iterable<T> findUsingQueryModel(QueryModel queryModel)
throws JeppettoException {
MongoDBCommand command = buildCommand(queryModel, AccessType.Read);
DBCursor dbCursor = command.cursor(dbCollection);
if (queryModel.getSorts() != null) {
dbCursor.sort(processSorts(queryModel.getSorts()));
}
if (queryModel.getFirstResult() > 0) {
dbCursor = dbCursor.skip(queryModel.getFirstResult()); // dbCursor is zero-indexed, firstResult is one-indexed
}
if (queryModel.getMaxResults() > 0) {
dbCursor = dbCursor.limit(queryModel.getMaxResults());
}
final DBCursor finalDbCursor = dbCursor;
return new Iterable<T>() {
@Override
public Iterator<T> iterator() {
return new Iterator<T>() {
@Override
public boolean hasNext() {
return finalDbCursor.hasNext();
}
@Override
@SuppressWarnings( { "unchecked" })
public T next() {
DBObject result = finalDbCursor.next();
((DirtyableDBObject) result).markPersisted(dbCollection);
if (MongoDBSession.isActive()) {
MongoDBSession.trackForSave(MongoDBQueryModelDAO.this,
buildIdentifyingQuery(result),
(T) result,
createIdentifyingQueries(result));
}
return (T) result;
}
@Override
public void remove() {
finalDbCursor.remove();
}
};
}
};
}
public Object projectUsingQueryModel(QueryModel queryModel)
throws JeppettoException {
try {
return buildCommand(queryModel, AccessType.Read).singleResult(dbCollection);
} catch (NoSuchItemException e) {
return null; // TODO: evaluate if correct
}
}
@Override
public void deleteUsingQueryModel(QueryModel queryModel)
throws JeppettoException {
try {
DBObject deleteQuery = buildQueryObject(queryModel, AccessType.ReadWrite);
if (queryLogger != null) {
queryLogger.debug("Deleting {}s identified by {}",
new Object[] { getCollectionClass().getSimpleName(), deleteQuery.toMap() } );
}
dbCollection.remove(deleteQuery, getWriteConcern());
} catch (MongoException e) {
throw new JeppettoException(e);
}
}
@Override
public <U extends T> T updateUniqueUsingQueryModel(U updateObject, QueryModel queryModel)
throws JeppettoException {
throw new RuntimeException("To be implemented w/ findAndModify...");
}
@Override
public <U extends T> Iterable<T> updateUsingQueryModel(U updateObject, QueryModel queryModel)
throws JeppettoException {
DBObject updateClause = ((UpdateObject) updateObject).getUpdateClause();
DBObject identifyingQuery = buildQueryObject(queryModel, AccessType.ReadWrite);
if (updateClause.keySet().size() == 0) {
if (queryLogger != null) {
queryLogger.debug("Bypassing update identified by {}; no changes", identifyingQuery.toMap());
}
return Collections.emptyList();
}
if (queryLogger != null) {
queryLogger.debug("Reference-based update of {} identified by {} with document {}",
getCollectionClass().getSimpleName(), identifyingQuery.toMap(), updateClause.toMap());
}
try {
dbCollection.update(identifyingQuery, updateClause, false, true, getWriteConcern());
} catch (MongoException e) {
throw new JeppettoException(e);
}
return null; // TODO: implement...
}
@Override
public Condition buildCondition(String conditionField, ConditionType conditionType, Iterator argsIterator) {
if (conditionField.equals("id")) {
return buildIdCondition(argsIterator.next());
} else {
return new Condition(conditionField, MongoDBOperator.valueOf(conditionType.name()).buildConstraint(argsIterator));
}
}
@Override
public Projection buildProjection(String projectionField, ProjectionType projectionType, Iterator argsIterator) {
return new Projection(projectionField, projectionType);
}
//-------------------------------------------------------------
// Implementation - AccessControlDAO
//-------------------------------------------------------------
@Override
public void save(T object, AccessControlContext accessControlContext)
throws OptimisticLockException, AccessControlException, JeppettoException {
ensureAccessControlEnabled();
T enhancedEntity = dirtyableDBObjectEnhancer.enhance(object);
DirtyableDBObject dbo = (DirtyableDBObject) enhancedEntity;
if (dbo.isPersisted(dbCollection)) {
verifyWriteAllowed(dbo, accessControlContext);
} else {
if (dbo.get(ID_FIELD) == null) {
dbo.put(ID_FIELD, new ObjectId()); // If the id isn't explicitly set, assume intent is for mongo ids
}
assessAndAssignAccessControl(dbo, accessControlContext);
}
DBObject identifyingQuery = buildIdentifyingQuery(dbo);
if (MongoDBSession.isActive()) {
MongoDBSession.trackForSave(this, identifyingQuery, enhancedEntity, createIdentifyingQueries(dbo));
} else {
trueSave(identifyingQuery, dbo);
}
}
@Override
public void grantAccess(ID id, String accessId, AccessType accessType)
throws NoSuchItemException, AccessControlException {
ensureAccessControlEnabled();
grantAccess(id, accessId, accessType, accessControlContextProvider.getCurrent());
}
@Override
public void grantAccess(ID id, String accessId, AccessType accessType, AccessControlContext accessControlContext)
throws NoSuchItemException, AccessControlException {
ensureAccessControlEnabled();
if (accessType == AccessType.None) {
revokeAccess(id, accessId, accessControlContext);
return;
}
QueryModel queryModel = new QueryModel();
queryModel.addCondition(buildIdCondition(id));
queryModel.setAccessControlContext(accessControlContext);
DBObject dbo = (DBObject) findUniqueUsingQueryModel(queryModel);
verifyWriteAllowed(dbo, accessControlContext);
@SuppressWarnings( { "unchecked" })
Map<String, String> accessControl = (Map<String, String>) dbo.get(ACCESS_CONTROL_FIELD);
DBObject accessUpdate;
if (accessControl == null) {
accessUpdate = new BasicDBObject("$set", new BasicDBObject(ACCESS_CONTROL_FIELD,
new BasicDBObject(accessId, accessType.shortName())));
} else {
accessUpdate = new BasicDBObject("$set", new BasicDBObject(ACCESS_CONTROL_FIELD + "." + accessId,
accessType.shortName()));
}
DBObject identifyingQuery = buildIdentifyingQuery(dbo);
for (String shardKey : shardKeys) {
identifyingQuery.put(shardKey, dbo.get(shardKey));
}
if (queryLogger != null) {
queryLogger.info("Granting access to object identified by {} to {}.", identifyingQuery.toMap(), accessId);
}
try {
dbCollection.update(identifyingQuery, accessUpdate, true, false, getWriteConcern());
} catch (MongoException e) {
throw new JeppettoException(e);
}
}
@Override
public void revokeAccess(ID id, String accessId)
throws NoSuchItemException, AccessControlException {
ensureAccessControlEnabled();
revokeAccess(id, accessId, accessControlContextProvider.getCurrent());
}
@Override
public void revokeAccess(ID id, String accessId, AccessControlContext accessControlContext)
throws NoSuchItemException, AccessControlException {
ensureAccessControlEnabled();
QueryModel queryModel = new QueryModel();
queryModel.addCondition(buildIdCondition(id));
queryModel.setAccessControlContext(accessControlContext);
DBObject dbo = (DBObject) findUniqueUsingQueryModel(queryModel);
verifyWriteAllowed(dbo, accessControlContext);
@SuppressWarnings("unchecked")
Map<String, String> accessControl = (Map<String, String>) dbo.get(ACCESS_CONTROL_FIELD);
if (accessControl == null) {
return;
}
DBObject accessUpdate = new BasicDBObject("$pull", new BasicDBObject(ACCESS_CONTROL_FIELD, accessId));
DBObject identifyingQuery = buildIdentifyingQuery(dbo);
for (String shardKey : shardKeys) {
identifyingQuery.put(shardKey, dbo.get(shardKey));
}
if (queryLogger != null) {
// Note: if revoke happens while another thread has object in memory and calls save(), it will succeed.
queryLogger.info("Revoking access to object identified by {} to {}.", identifyingQuery.toMap(), accessId);
}
try {
dbCollection.update(identifyingQuery, accessUpdate, true, false, getWriteConcern());
} catch (MongoException e) {
throw new JeppettoException(e);
}
}
@Override
public Map<String, AccessType> getGrantedAccesses(ID id)
throws NoSuchItemException, AccessControlException {
ensureAccessControlEnabled();
return getGrantedAccesses(id, accessControlContextProvider.getCurrent());
}
@Override
public Map<String, AccessType> getGrantedAccesses(ID id, AccessControlContext accessControlContext)
throws NoSuchItemException, AccessControlException {
ensureAccessControlEnabled();
QueryModel queryModel = new QueryModel();
queryModel.addCondition(buildIdCondition(id));
queryModel.setAccessControlContext(accessControlContext);
DBObject dbo = (DBObject) findUniqueUsingQueryModel(queryModel);
// We limit it to writers.
verifyWriteAllowed(dbo, accessControlContext);
@SuppressWarnings("unchecked")
Map<String, String> accessControl = (Map<String, String>) dbo.get(ACCESS_CONTROL_FIELD);
if (accessControl == null || accessControl.size() == 0) {
return Collections.emptyMap();
} else if (accessControl.size() == 1) {
Map.Entry<String, String> entry = accessControl.entrySet().iterator().next();
return Collections.singletonMap(entry.getKey(), AccessType.getAccessTypeFromShortName(entry.getValue()));
} else {
Map<String, AccessType> result = new HashMap<String, AccessType>();
for (Map.Entry<String, String> entry : accessControl.entrySet()) {
result.put(entry.getKey(), AccessType.getAccessTypeFromShortName(entry.getValue()));
}
return result;
}
}
@Override
public AccessControlContextProvider getAccessControlContextProvider() {
return accessControlContextProvider;
}
//-------------------------------------------------------------
// Methods - Protected
//-------------------------------------------------------------
/**
* This method allows subclasses an opportunity to add any important data to a cache key, such
* as the __acl from the AccessControlMongoDAO.
*
* TODO: revisit these methods...
*
* @param key key to augment
* @return augmented key
*/
protected DBObject augmentObjectCacheKey(DBObject key) {
return key;
}
protected DBObject[] createIdentifyingQueries(DBObject dbo) {
int uniqueIndexCount = uniqueIndexes.size() + 1;
List<DBObject> queries = new ArrayList<DBObject>(uniqueIndexCount);
queries.add(buildIdentifyingQuery(dbo));
for (Collection<String> indexFields : uniqueIndexes.values()) {
DBObject query = new BasicDBObject();
for (String indexField : indexFields) {
query.put(indexField, getFieldValueFrom(dbo, indexField));
}
queries.add(augmentObjectCacheKey(query));
}
return queries.toArray(new DBObject[uniqueIndexCount]);
}
//-------------------------------------------------------------
// Methods - Protected - Final
//-------------------------------------------------------------
protected final DBObject buildIdentifyingQuery(DBObject dbo) {
//noinspection unchecked
return buildIdentifyingQuery((ID) dbo.get(ID_FIELD));
}
protected final DBObject buildIdentifyingQuery(ID... ids) {
if (ids == null || ids.length == 0) {
throw new IllegalArgumentException("Invalid 'ids' array: " + (ids == null ? "(null)" : "(empty)"));
}
if (ids.length == 1) {
if (ids[0] == null) {
throw new IllegalArgumentException("Id cannot be null.");
} else if (ids[0] instanceof String && ObjectId.isValid((String) ids[0])) {
return new BasicDBObject(ID_FIELD, new ObjectId((String) ids[0]));
} else {
return new BasicDBObject(ID_FIELD, ids[0]);
}
}
List<Object> idList = new ArrayList<Object>();
for (Object id : ids) {
if (id == null) {
throw new IllegalArgumentException("Id cannot be null.");
} else if (id instanceof String && ObjectId.isValid((String) id)) {
idList.add(new ObjectId((String) id));
} else {
idList.add(id);
}
}
return new BasicDBObject(ID_FIELD, new BasicDBObject("$in", idList));
}
protected final void deleteByIdentifyingQuery(DBObject identifyingQuery) {
if (MongoDBSession.isActive()) {
MongoDBSession.trackForDelete(this, identifyingQuery);
} else {
trueRemove(identifyingQuery);
}
}
protected final void trueSave(final DBObject identifyingQuery, final DirtyableDBObject dbo) {
if (optimisticLockEnabled) {
Integer optimisticLockVersion = (Integer) dbo.get(OPTIMISTIC_LOCK_VERSION_FIELD);
int optimisticLockVersionValue = optimisticLockVersion == null ? 0 : optimisticLockVersion;
// TODO: should this modification of identifyingQuery been done earlier (in save())?
identifyingQuery.put(OPTIMISTIC_LOCK_VERSION_FIELD, optimisticLockVersionValue);
dbo.put(OPTIMISTIC_LOCK_VERSION_FIELD, optimisticLockVersionValue + 1);
}
for (String shardKey : shardKeys) {
identifyingQuery.put(shardKey, dbo.get(shardKey));
}
final DBObject optimalDbo = determineOptimalDBObject(dbo);
if (optimalDbo.keySet().size() == 0) {
if (queryLogger != null) {
queryLogger.debug("Bypassing save on object identified by {}; optimization rendered no changes.",
identifyingQuery.toMap());
}
return;
}
if (queryLogger != null) {
queryLogger.debug("Saving {} identified by {} with document {}", getCollectionClass().getSimpleName(),
identifyingQuery.toMap(), optimalDbo.toMap());
}
try {
dbCollection.update(identifyingQuery, optimalDbo, true, false, getWriteConcern());
} catch (DuplicateKeyException e) {
if (optimisticLockEnabled && e.getMessage().contains("$_id_")) {
Integer localOptimisticLockVersion = (Integer) dbo.get(OPTIMISTIC_LOCK_VERSION_FIELD) - 1;
if (localOptimisticLockVersion == 0) {
throw new JeppettoException("New object's id already in use -- review id generation strategy.", e);
}
identifyingQuery.removeField(OPTIMISTIC_LOCK_VERSION_FIELD);
DBObject result = dbCollection.findOne(identifyingQuery);
if (result == null) {
throw new OptimisticLockException("Probably an OptimisticLockException, but conflicting object "
+ "identified by " + identifyingQuery + " no longer exists.");
}
Integer remoteOptimisticLockVersion = (Integer) result.get(OPTIMISTIC_LOCK_VERSION_FIELD);
if (remoteOptimisticLockVersion != null && remoteOptimisticLockVersion > localOptimisticLockVersion) {
throw new OptimisticLockException("Local version = " + localOptimisticLockVersion
+ ", remote version = " + remoteOptimisticLockVersion);
}
}
throw new JeppettoException(e);
} catch (MongoException e) {
throw new JeppettoException(e);
}
dbo.markPersisted(dbCollection);
}
protected final void trueRemove(final DBObject identifyingQuery) {
if (queryLogger != null) {
queryLogger.debug("Removing {}s matching {}",
new Object[] { getCollectionClass().getSimpleName(), identifyingQuery.toMap() } );
}
dbCollection.remove(identifyingQuery, getWriteConcern());
}
protected final DBCollection getDbCollection() {
return dbCollection;
}
protected final Class<T> getCollectionClass() {
return dirtyableDBObjectEnhancer.getBaseClass();
}
//-------------------------------------------------------------
// Methods - Private
//-------------------------------------------------------------
private BasicDBObject identifyFieldsToRetrieve(Class<T> entityClass, String viewOfClassName) {
if (viewOfClassName == null) {
return null; // Null is equivalent to retrieving all fields
}
BasicDBObject fields = new BasicDBObject();
try {
determineFieldsToRetrieve(entityClass, Class.forName(viewOfClassName), "", fields);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
if (optimisticLockEnabled) {
fields.put(OPTIMISTIC_LOCK_VERSION_FIELD, 1);
}
if (accessControlContextProvider != null) {
fields.put(ACCESS_CONTROL_FIELD, 1);
}
if (fields.containsField("id")) {
fields.remove("id"); // Included by default, no need to specify
} else {
fields.put(ID_FIELD, 0); // Need to explicitly exclude it
}
return fields;
}
private void determineFieldsToRetrieve(Class entityClass, Class viewOfClass, String fieldPrefix, BasicDBObject fields) {
Map<String, Class> entityFieldMap = getFieldMap(entityClass, fieldPrefix);
Map<String, Class> viewOfFieldMap = getFieldMap(viewOfClass, fieldPrefix);
for (Map.Entry<String, Class> entry : entityFieldMap.entrySet()) {
String fieldName = entry.getKey();
Class fieldClass = entry.getValue();
if (DBObjectUtil.needsNoConversion(fieldClass)
|| Collection.class.isAssignableFrom(fieldClass)
|| fieldClass.equals(viewOfFieldMap.get(fieldName))) {
fields.put(fieldName, 1);
} else {
determineFieldsToRetrieve(fieldClass, viewOfFieldMap.get(fieldName), fieldName + ".", fields);
}
}
}
private Map<String, Class> getFieldMap(Class clazz, String fieldPrefix) {
Map<String, Class> fieldMap = new HashMap<String, Class>();
List<Method> methods = new ArrayList<Method>();
Collections.addAll(methods, clazz.getDeclaredMethods());
Collections.addAll(methods, clazz.getMethods());
for (Method method : methods) {
if (method.getDeclaringClass().equals(Object.class)
|| method.getReturnType().equals(void.class)
|| Modifier.isFinal(method.getModifiers())
|| Modifier.isAbstract(method.getModifiers())
|| method.getParameterTypes().length != 0) {
continue;
}
String methodName = method.getName();
String upperCaseFieldName;
if (methodName.startsWith("get")) {
upperCaseFieldName = methodName.substring(3);
} else if (methodName.startsWith("is")) {
upperCaseFieldName = methodName.substring(2);
} else {
continue;
}
try {
//noinspection unchecked
clazz.getMethod("set".concat(upperCaseFieldName), method.getReturnType());
} catch (NoSuchMethodException e) {
continue;
}
String fieldName = upperCaseFieldName.substring(0, 1).toLowerCase().concat(upperCaseFieldName.substring(1));
fieldMap.put(fieldPrefix.concat(fieldName), method.getReturnType());
}
return fieldMap;
}
private DBObject processSorts(List<Sort> sorts) {
DBObject orderBy = new BasicDBObject();
for (Sort sort : sorts) {
orderBy.put(sort.getField(), sort.getSortDirection() == SortDirection.Ascending ? 1 : -1);
}
return orderBy;
}
// Special case for 'id' queries as it maps to _id within MongoDB.
private Condition buildIdCondition(Object argument) {
if (argument instanceof String && ObjectId.isValid((String) argument)) {
return new Condition(ID_FIELD, new ObjectId((String) argument));
} else if (Iterable.class.isAssignableFrom(argument.getClass())) {
List<Object> objectIds = new ArrayList<Object>();
//noinspection ConstantConditions
for (Object argumentItem : (Iterable) argument) {
if (argumentItem instanceof String && ObjectId.isValid((String) argumentItem)) {
objectIds.add(new ObjectId((String) argumentItem));
} else if (argumentItem instanceof ObjectId) {
objectIds.add( argumentItem);
}
}
return new Condition(ID_FIELD, new BasicDBObject("$in", objectIds));
} else {
return new Condition(ID_FIELD, argument);
}
}
private Object getFieldValueFrom(DBObject dbo, String field) {
if (!field.contains(".")) {
return dbo.get(field);
}
Object value = dbo;
for (String subField : field.split(".")) {
value = ((DBObject) value).get(subField);
if (value == null) {
break;
}
}
return value;
}
private MongoDBCommand buildCommand(QueryModel queryModel, AccessType accessType) {
BasicDBObject query = buildQueryObject(queryModel, accessType);
MongoDBCommand command;
if (queryModel.getProjection() == null) {
command = new BasicDBObjectCommand(query, fieldsToRetrieve, decoderFactory);
} else {
command = ProjectionCommands.forProjection(queryModel.getProjection(), query);
}
if (queryLogger != null) {
return QueryLoggingCommand.wrap(command, queryLogger);
} else {
return command;
}
}
private Map<String, Set<String>> ensureIndexes(List<String> indexes, final boolean unique) {
if (indexes == null || indexes.size() == 0) {
return Collections.emptyMap();
}
Map<String, Set<String>> result = new HashMap<String, Set<String>>();
BasicDBObject options = new BasicDBObject();
options.put("unique", unique);
options.put("background", Boolean.TRUE);
for (final String index : indexes) {
final DBObject keys = new BasicDBObject();
String[] indexFields = index.split(",");
for (String indexField : indexFields) {
indexField = indexField.trim();
if (indexField.startsWith("+")) {
keys.put(indexField.substring(1), 1);
} else if (indexField.startsWith("-")) {
keys.put(indexField.substring(1), -1);
} else {
keys.put(indexField, 1);
}
}
result.put(index, keys.keySet());
if (queryLogger != null) {
queryLogger.debug("Ensuring index {} on {}", keys.toMap(), getCollectionClass().getSimpleName());
}
dbCollection.createIndex(keys, options);
}
return result;
}
private List<String> extractShardKeys(String shardKeyPattern) {
if (shardKeyPattern == null) {
return Collections.emptyList();
}
String[] shardKeyParts = shardKeyPattern.split(",");
List<String> shardKeys = new ArrayList<String>(shardKeyParts.length);
for (String shardKey : shardKeyParts) {
shardKey = shardKey.trim();
if (shardKey.equals("id") || shardKey.equals("_id")) {
continue;
}
shardKeys.add(shardKey);
}
return shardKeys;
}
private BasicDBObject buildQueryObject(QueryModel queryModel, AccessType accessType) {
BasicDBObject query = new BasicDBObject();
List<Condition> allCriteria = new ArrayList<Condition>();
if (queryModel.getConditions() != null) {
allCriteria.addAll(queryModel.getConditions());
}
for (Map.Entry<String, List<Condition>> associationConditions : queryModel.getAssociationConditions().entrySet()) {
for (Condition condition : associationConditions.getValue()) {
condition.setField(associationConditions.getKey() + "." + condition.getField());
allCriteria.add(condition);
}
}
// TODO: optimize this -- iterating over everything above and again here
for (Condition condition : allCriteria) {
// we need to ensure that all condition objects are mongodb-safe
// examples of "unsafe" things are: Sets, Enum, POJOs.
Object rawConstraint = condition.getConstraint();
Object constraint = (rawConstraint == null) ? null
: DBObjectUtil.toDBObject(rawConstraint);
// XXX : if annotation specifies multiple conditions on single field the
// first condition will be overwritten here
query.put(condition.getField(), constraint);
}
if (accessControlContextProvider != null) {
if (!annotationAllowsAccess(queryModel.getAccessControlContext(), accessType)) {
if (accessType == AccessType.Read) {
query.put(ACCESS_CONTROL_FIELD + "." + queryModel.getAccessControlContext().getAccessId(), READ_PATTERN);
} else {
query.put(ACCESS_CONTROL_FIELD + "." + queryModel.getAccessControlContext().getAccessId(), accessType.shortName());
}
}
}
return query;
}
private DBObject determineOptimalDBObject(DirtyableDBObject dirtyableDBObject) {
if (!dirtyableDBObject.isPersisted(dbCollection)) {
// dirtyableDBObject.includeNullValuedKeys(saveNulls);
return dirtyableDBObject;
}
DBObject settableItems = new BasicDBObject();
DBObject unsettableItems = new BasicDBObject();
walkDirtyableDBObject("", dirtyableDBObject, settableItems, unsettableItems);
if (optimisticLockEnabled) {
// TODO: Don't like re-reading this value here, when handled in calling method
settableItems.put(OPTIMISTIC_LOCK_VERSION_FIELD, dirtyableDBObject.get(OPTIMISTIC_LOCK_VERSION_FIELD));
}
DBObject optimalDBObject = new BasicDBObject();
if (settableItems.keySet().size() > 0) {
optimalDBObject.put("$set", settableItems);
}
if (unsettableItems.keySet().size() > 0) {
optimalDBObject.put("$unset", unsettableItems);
}
return optimalDBObject;
}
private void walkDirtyableDBObject(String prefix, DirtyableDBObject dirtyableDBObject,
DBObject settableItems, DBObject unsettableItems) {
for (Iterator<String> dirtyKeys = dirtyableDBObject.getDirtyKeys(); dirtyKeys.hasNext(); ) {
String dirtyKey = dirtyKeys.next();
Object dirtyObject = dirtyableDBObject.get(dirtyKey);
if (dirtyObject == null) {
unsettableItems.put(prefix + dirtyKey, null);
} else if (dirtyObject instanceof DirtyableDBObjectList) { // NB: encompasses DirtyableDBObjectSet
DirtyableDBObjectList dirtyableDBObjectList = (DirtyableDBObjectList) dirtyObject;
if (!dirtyableDBObjectList.isPersisted(dbCollection) || dirtyableDBObjectList.isRewrite()) {
settableItems.put(prefix + dirtyKey, dirtyableDBObjectList);
continue;
}
walkDirtyableDBObject(prefix + dirtyKey + ".", dirtyableDBObjectList, settableItems, unsettableItems);
} else if (dirtyObject instanceof DirtyableDBObjectMap) {
DirtyableDBObjectMap dirtyableDBObjectMap = (DirtyableDBObjectMap) dirtyObject;
if (!dirtyableDBObjectMap.isPersisted(dbCollection)) {
settableItems.put(prefix + dirtyKey, dirtyableDBObjectMap);
continue;
}
for (Object removedKey : dirtyableDBObjectMap.getRemovedKeys()) {
unsettableItems.put(prefix + dirtyKey + "." + removedKey, 1);
}
walkDirtyableDBObject(prefix + dirtyKey + ".", dirtyableDBObjectMap, settableItems, unsettableItems);
} else if (dirtyObject instanceof DirtyableDBObject) {
if (!((DirtyableDBObject) dirtyObject).isPersisted(dbCollection)) {
settableItems.put(prefix + dirtyKey, dirtyObject);
} else {
walkDirtyableDBObject(prefix + dirtyKey + ".", (DirtyableDBObject) dirtyObject, settableItems, unsettableItems);
}
} else {
settableItems.put(prefix + dirtyKey, DBObjectUtil.toDBObject(dirtyObject));
}
}
}
private WriteConcern getWriteConcern() {
return defaultWriteConcern;
}
private void ensureAccessControlEnabled() {
if (accessControlContextProvider == null) {
throw new AccessControlException("Access Control is not enabled. No AccessControlContextProvider specified.");
}
}
private void verifyWriteAllowed(DBObject dbo, AccessControlContext accessControlContext)
throws AccessControlException {
@SuppressWarnings( { "unchecked" })
Map<String, String> accessControl = (Map<String, String>) dbo.get(ACCESS_CONTROL_FIELD);
if (accessControlContext == null
|| (!AccessType.ReadWrite.shortName().equals(accessControl.get(accessControlContext.getAccessId()))
&& !annotationAllowsAccess(accessControlContext, AccessType.ReadWrite))) {
throw new AccessControlException("Unable to write " + dbo.toMap() + " with " + accessControlContext);
}
}
private void assessAndAssignAccessControl(DBObject dbo, AccessControlContext accessControlContext)
throws AccessControlException {
AccessControl accessControl = getAccessControlAnnotation();
if (accessControl != null) {
for (Creator creator : accessControl.creators()) {
switch (creator.type()) {
case Identified:
if (accessControlContext.getAccessId() != null) {
dbo.put(ACCESS_CONTROL_FIELD, Collections.singletonMap(accessControlContext.getAccessId(),
creator.grantedAccess().shortName()));
return;
}
break;
case Role:
if (accessControlContext.getRoles() != null && accessControlContext.getRoles().contains(creator.typeValue())) {
dbo.put(ACCESS_CONTROL_FIELD, Collections.singletonMap(accessControlContext.getAccessId(),
creator.grantedAccess().shortName()));
return;
}
break;
case Anonymous:
// No explicit grants given.
dbo.put(ACCESS_CONTROL_FIELD, Collections.emptyMap());
return;
}
}
throw new AccessControlException("Unable to create " + dbo.getClass().getSuperclass().getSimpleName()
+ " with " + accessControlContext
+ ". Check object's @AccessControl annotation.");
} else {
// When no annotation is present, any user can create the object. If user is unknown, no explicit grants.
// Otherwise, ReadWrite access is given to the caller.
if (accessControlContext.getAccessId() == null) {
dbo.put(ACCESS_CONTROL_FIELD, Collections.emptyMap());
} else {
dbo.put(ACCESS_CONTROL_FIELD, Collections.singletonMap(accessControlContext.getAccessId(),
AccessType.ReadWrite.shortName()));
}
}
}
private boolean annotationAllowsAccess(AccessControlContext accessControlContext, AccessType accessType) {
if (accessType == null) {
return false;
}
AccessControl accessControl;
if ((accessControl = getAccessControlAnnotation()) == null) {
return false;
}
Set<String> roles = accessControlContext == null ? Collections.<String>emptySet() : accessControlContext.getRoles();
for (Accessor accessor : accessControl.accessors()) {
if (accessor.access().allows(accessType)
&& (accessor.type() == Accessor.Type.Anyone
|| (accessor.type() == Accessor.Type.Role && roles != null && roles.contains(accessor.typeValue())))) {
return true;
}
}
return false;
}
private AccessControl getAccessControlAnnotation() {
for (Class daoInterface : getClass().getInterfaces()) {
// noinspection unchecked
AccessControl accessControl = (AccessControl) daoInterface.getAnnotation(AccessControl.class);
if (accessControl != null) {
return accessControl;
}
}
return null;
}
}