/*
*
*/
package org.smartly.packages.mongo.impl;
import com.mongodb.*;
import org.smartly.commons.logging.Level;
import org.smartly.commons.logging.Logger;
import org.smartly.commons.logging.util.LoggingUtils;
import org.smartly.commons.util.*;
import org.smartly.packages.mongo.impl.i18n.MongoTranslationManager;
import org.smartly.packages.mongo.impl.util.MongoUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
/**
* @author angelo.geminiani
*/
public abstract class AbstractMongoService {
// ------------------------------------------------------------------------
// Constants
// ------------------------------------------------------------------------
private static final String _ID = IMongoConstants.ID;
private static final String MODIFIER_INC = "$inc";
private static final int EARTH_RADIUS_mt = 6378160; // earth radius in mt.
private static final String LOCALE_BASE_FIELD = IMongoConstants.LANG_BASE; // used in embedded localizations. i.e. "description":{"base":"hello", "it":"ciao"}
private static final String WILDCHAR = IMongoConstants.WILDCHAR;
// ------------------------------------------------------------------------
// variables
// ------------------------------------------------------------------------
private final DB _db;
private final String _collName;
private final DBCollection _coll;
private final String[] _langCodes; // used from translation manager
private final MongoTranslationManager _tm;
// ------------------------------------------------------------------------
// constructor
// ------------------------------------------------------------------------
public AbstractMongoService(final DB db, final String collName,
final String[] langCodes) {
_db = db;
_collName = collName;
_coll = _db.getCollection(_collName);
_langCodes = null != langCodes ? langCodes : new String[0];
_tm = new MongoTranslationManager(db, collName, langCodes);
}
// ------------------------------------------------------------------------
// p u b l i c
// ------------------------------------------------------------------------
// <editor-fold defaultstate="collapsed" desc=" Languages, Collection and DB">
public final String getName() {
return null != _coll ? _coll.getName() : "";
}
public final String getFullName() {
return null != _coll ? _coll.getFullName() : "";
}
public final DBCollection getCollection() {
return _coll;
}
public final String getCollectionName() {
return _collName;
}
/**
* Returns supported languages
*
* @return
*/
public final String[] getLanguages() {
return _langCodes;
}
protected final DB getDb() {
return _db;
}
public void drop() {
if (null != _coll) {
_coll.drop();
}
}
// </editor-fold>
// <editor-fold defaultstate="collapsed" desc=" INDEXES">
public List<DBObject> getIndexes() {
if (null != _coll) {
return _coll.getIndexInfo();
}
return new ArrayList<DBObject>();
}
public int countIndexes() {
if (null != _coll) {
return this.getIndexes().size();
}
return 0;
}
public int dropIndexes() {
if (null != _coll) {
int count = 0;
final List<DBObject> indexes = this.getIndexes();
for (final DBObject index : indexes) {
try {
final DBObject key = (DBObject) index.get("key");
if (null != key) {
if (key.containsField(_ID)) {
continue;
}
}
_coll.dropIndex(index.get("name").toString());
count++;
} catch (Throwable t) {
this.getLogger().severe(FormatUtils.format("Unable to drop index: {0}", t));
}
}
return count;
}
return 0;
}
public void ensureIndex(final DBObject index) {
if (null != _coll) {
_coll.ensureIndex(index);
}
}
public void ensureIndex(final String fieldName, final boolean ascending) {
if (null != _coll) {
final DBObject index = new BasicDBObject(fieldName,
ascending ? 1 : -1);
_coll.ensureIndex(index);
}
}
public void ensureIndex(final String[] fieldNames,
final boolean ascending, final boolean unique) {
if (null != _coll) {
final DBObject index = new BasicDBObject();
for (final String fieldName : fieldNames) {
index.put(fieldName, ascending ? 1 : -1);
}
_coll.ensureIndex(index, null, unique);
}
}
// </editor-fold>
// <editor-fold defaultstate="collapsed" desc=" UPSERT (update/insert)">
/**
* Saves document(s) to the database.
* if doc doesn't have an _id, one will be added
* you can get the _id that was added from doc after the insert
*
* @param item document to save
*/
public void insert(final DBObject item) throws StandardCodedException {
if (null != _coll) {
final WriteResult result = _coll.insert(item);
if (StringUtils.hasText(result.getError())) {
throw this.getError418(result.getError());
}
}
}
/**
* Update or Insert an object and return 1 if object was updated or 0
* if was insert.
*
* @param object Object to save.
* @return 0=insert, 1=update/overwrite
* @throws StandardCodedException
*/
public int upsert(final DBObject object) throws StandardCodedException {
try {
final Object id = this.getOrCreateId(object);
if (null != id) {
final boolean overwritten = this.overwrite(object, id);
return overwritten ? 1 : 0;
} else {
throw this.getError500("Expected a valid ID!");
}
} catch (Throwable t) {
throw this.getError500(t);
}
}
/**
* Update existing document also with partial data.
*
* @param object
* @return
* @throws StandardCodedException
*/
public DBObject merge(final DBObject object,
final String[] excludeProperties) throws StandardCodedException {
try {
final Object id = this.getId(object);
if (null != id) {
return this.merge(object, id, excludeProperties);
} else {
//-- insert new object --//
this.upsert(object);
return object;
}
} catch (Throwable t) {
throw this.getError500(t);
}
}
/**
* Update existing Object. Only consistent (not null and not empty) fields are updated.
*
* @param object
* @return
* @throws StandardCodedException
*/
public int updateFields(final DBObject object) throws StandardCodedException {
try {
final Object id = this.getOrCreateId(object);
if (null != id) {
final boolean overwritten = this.update(object, id);
return overwritten ? 1 : 0;
} else {
throw this.getError500("Expected a valid ID!");
}
} catch (Throwable t) {
throw this.getError500(t);
}
}
/**
* Update existing Object
*
* @param query The filter
* @param object Object or Operation
* @return True if existing data has been updated
* @throws StandardCodedException
*/
public boolean update(final DBObject query, final DBObject object)
throws StandardCodedException {
return this.nativeUpdate(query, object, false, false);
}
/**
* Update existing Object or insert new one if does not exists and
* "upsert" parameter is true.
*
* @param query The filter
* @param object Object or Operation
* @param upsert Boolean
* @return True if existing data has been updated
* @throws StandardCodedException
*/
public boolean update(final DBObject query, final DBObject object,
final boolean upsert)
throws StandardCodedException {
return this.nativeUpdate(query, object, upsert, false);
}
// </editor-fold>
// <editor-fold defaultstate="collapsed" desc=" REMOVE">
public int removeAll() throws StandardCodedException {
final List<DBObject> data = this.find();
int counter = 0;
for (final DBObject item : data) {
this.removeOne(item);
counter++;
}
// remove localizations
this.removeLocalizations();
return counter;
}
public int removeOne(final DBObject object) throws StandardCodedException {
final Object id = this.getOrCreateId(object);
return this.removeOne(id);
}
public DBObject removeById(final Object id) throws StandardCodedException {
final DBObject item = this.findById(id);
if (null != item) {
this.removeOne(id);
}
return item;
}
public int removeOne(final Object id) throws StandardCodedException {
if (null != id) {
final DBObject filter;
if (id instanceof String) {
final Pattern pattern = MongoUtils.patternEquals((String) id);
filter = new BasicDBObject(_ID, pattern);
} else {
filter = new BasicDBObject(_ID, id);
}
return this.remove(filter);
} else {
throw this.getError500("Expected a valid ID!");
}
}
public int remove(final DBObject filter) throws StandardCodedException {
try {
final WriteResult result;
result = _coll.remove(filter);
if (StringUtils.hasText(result.getError())) {
throw this.getError418(result.getError());
}
return result.getN();
} catch (Throwable t) {
throw this.getError500(t);
}
}
// </editor-fold>
// <editor-fold defaultstate="collapsed" desc=" FIND, DISTINCT, GET">
public boolean exists(final Object id) {
if (null != _coll && null != id) {
final DBObject filter = new BasicDBObject(_ID, id);
return _coll.count(filter) > 0;
}
return false;
}
public DBObject findById(final Object id) {
return this.findById(id, null);
}
public DBObject findById(final Object id, final String[] fieldNames) {
if (null != _coll && null != id) {
final DBObject filter;
if (id instanceof String) {
if (StringUtils.hasText((String) id)) {
final Pattern pattern = MongoUtils.patternEquals((String) id);
filter = new BasicDBObject(_ID, pattern);
} else {
return null;
}
} else {
filter = new BasicDBObject(_ID, id);
}
final DBObject fields = this.getFields(fieldNames, true);
return null != fields
? _coll.findOne(filter, fields)
: _coll.findOne(filter);
}
return null;
}
public DBObject findOne() {
return null != _coll ? _coll.findOne() : null;
}
public DBObject findOne(final DBObject filter) {
return this.findOne(filter, null);
}
public DBObject findOne(final DBObject filter, final String[] fieldNames) {
final DBObject fields = this.getFields(fieldNames, true);
return null != fields
? _coll.findOne(filter, fields)
: _coll.findOne(filter);
}
public List<DBObject> findByIds(final Collection ids) {
return this.findByIds(ids, null);
}
public List<DBObject> findByIds(final Collection ids, final String[] fieldNames) {
final DBObject query = MongoUtils.queryEquals(_ID, ids);
return this.find(query, fieldNames, null, null);
}
public List<DBObject> findByIds(final Object[] ids) {
return this.findByIds(ids, null);
}
public List<DBObject> findByIds(final Object[] ids, final String[] fieldNames) {
final DBObject query = MongoUtils.queryEquals(_ID, ids);
return this.find(query, fieldNames, null, null);
}
public List<DBObject> find(final DBObject filter) {
return this.find(filter, null, null, null);
}
public List<DBObject> find() {
if (null != _coll) {
final DBCursor cursor = _coll.find();
return null != cursor ? cursor.toArray() : new ArrayList<DBObject>();
}
return null;
}
public List<DBObject> find(final String[] fieldNames) {
return this.find(null, fieldNames, null, null);
}
public List<DBObject> find(final DBObject filter,
final String[] fieldNames,
final String[] sortAscBy,
final String[] sortDescBy) {
return this.find(filter, fieldNames, 0, 0, sortAscBy, sortDescBy);
}
/**
* Return list of items.
*
* @param filter (Optional) filter
* @param fieldNames (Optional)
* @param skip Number of results to skip (0 = no skip)
* @param limit Number of results to return (0 = all)
* @param sortAscBy (Optional) Array of field names to sort ASC
* @param sortDescBy (Optional) Array of field names to sort DESC
* @return
*/
public List<DBObject> find(final DBObject filter, final String[] fieldNames,
final int skip, final int limit,
final String[] sortAscBy, final String[] sortDescBy) {
if (null != _coll) {
final DBCursor cursor = this.cursor(
filter, fieldNames, skip, limit, sortAscBy, sortDescBy);
return null != cursor
? cursor.toArray()
: new ArrayList<DBObject>();
}
return null;
}
public List distinct(final String key, final DBObject query) {
if (null != _coll) {
return _coll.distinct(key, null != query ? query : new BasicDBObject());
}
return null;
}
public MongoPage paged(final DBObject filter,
final String[] fieldNames,
final int skip, final int limit,
final String[] sortAscBy, final String[] sortDescBy) {
final MongoPage result = new MongoPage();
// result cursor
final DBCursor cursor = this.cursor(filter, fieldNames, skip, limit,
sortAscBy, sortDescBy);
try {
final int count = MongoUtils.queryIsOR(filter)
? this.count(filter, false)
: this.count(cursor, true);
final int pageCount = MathUtils.paging(limit, count);
final int pageNr = skip > 0 ? skip / limit + 1 : 1;
//final int count = null != cursor ? cursor.count() : 0;
final List<DBObject> list = null != cursor
? cursor.toArray()
: new ArrayList<DBObject>();
result.setItems(list);
result.setCount(count);
result.setPageCount(pageCount);
result.setPageNr(pageNr);
} finally {
if (null != cursor) {
cursor.close();
}
}
return result;
}
public Iterable<DBObject> aggregate(final DBObject firstOp,
final DBObject... additionslOps) throws Exception {
if (null != _coll) {
final AggregationOutput output = _coll.aggregate(firstOp, additionslOps);
if (null != output) {
final String error = output.getCommandResult().getErrorMessage();
if (StringUtils.hasText(error)) {
throw new Exception(error);
}
return output.results();
}
}
return null;
}
public List<DBObject> geoNear(final Double[] coord,
final int maxDistance) {
final double[] dc = new double[]{coord[0], coord[1]};
return geoNear(dc, maxDistance);
}
/**
* geoNear is a db command and is used as this.
*
* @param coord Lon and Lat
* @return List
*/
public List<DBObject> geoNear(final double[] coord,
final int maxDistance) {
if (null != _db && null != _coll) {
final BasicDBObject cmd = new BasicDBObject();
cmd.put("geoNear", _coll.getName());
// maxDistance
if (maxDistance > 0) {
cmd.put("maxDistance", getMaxDistance(maxDistance));
cmd.put("distanceMultiplier", EARTH_RADIUS_mt); // result data are in mt.
}
//int coord[] = {50, 50};
cmd.put("near", coord);
cmd.put("num", 10);
final CommandResult result = _db.command(cmd);
// retrieve response from result
if (result.ok()) {
final Object list = result.get("results");
if (list instanceof List) {
return (List) list;
}
} else {
// error
final String err = result.getErrorMessage();
this.getLogger().log(Level.SEVERE, err);
}
}
return new ArrayList<DBObject>();
}
// </editor-fold>
// <editor-fold defaultstate="collapsed" desc=" LOCALIZATION ">
public final void addLocalization(final Object id,
final String lang, final String field,
final Object value) {
try {
final MongoTranslationManager srvc = this.getTranslationManager();
srvc.add(id, field, lang, value);
} catch (Throwable ex) {
}
}
public final int removeLocalizations() {
final MongoTranslationManager srvc = this.getTranslationManager();
return srvc.removeAll();
}
public final int removeLocalizations(final DBObject item) {
final String id = MongoUtils.getString(item, IMongoConstants.ID);
return this.removeLocalizations(id);
}
public final int removeLocalizations(final Object id) {
try {
final MongoTranslationManager srvc = this.getTranslationManager();
return srvc.removeAll(id);
} catch (Throwable ex) {
}
return -1;
}
public final void removeLocalization(
final Object id, final String lang, final String[] fields) {
for (final String field : fields) {
this.removeLocalization(id, lang, field);
}
}
public final void removeLocalization(final Object id,
final String lang, final String field) {
try {
final MongoTranslationManager srvc = this.getTranslationManager();
srvc.remove(id, field, lang);
} catch (Throwable ex) {
}
}
public final void localize(final MongoPage page,
final String lang, final String[] fields) {
this.localize(MongoPage.getItems(page), lang, fields, false);
}
public final void localize(final MongoPage page,
final String lang, final String[] fields,
final boolean onlyExistingFields) {
this.localize(MongoPage.getItems(page), lang, fields, onlyExistingFields);
}
/**
* Localize a list of objects.
*
* @param list Name of collection. i.e. "documents"
* @param list List of objects to localize
* @param lang The language. i.e. "it"
* @param fields Array of field names to localize. i.e. {"name", "description"}
*/
public final void localize(final List<DBObject> list,
final String lang, final String[] fields) {
this.localize(list, lang, fields, false);
}
public final void localize(final List<DBObject> list,
final String lang, final String[] fields,
final boolean onlyExistingFields) {
if (!CollectionUtils.isEmpty(list) && StringUtils.hasText(lang)) {
for (final DBObject item : list) {
this.localize(item, lang, fields, onlyExistingFields);
}
}
}
/**
* Localize an object
*
* @param item The Object to localize
* @param lang The language. i.e. "it"
* @param fields Array of field names to localize. i.e. {"name", "description"}
*/
public final void localize(final DBObject item,
final String lang,
final String[] fields) {
this.localize(item, lang, fields, false);
}
public final void localize(final DBObject item,
final String lang,
final String[] fields,
final boolean onlyExistingFields) {
if (null != fields && fields.length > 0 && null != item && StringUtils.hasText(lang)) {
//-- define fields to localize --//
final String[] fields_to_localize;
if (fields.length == 1 && WILDCHAR.equalsIgnoreCase(fields[0])) {
// all fields
final Set<String> keys = item.keySet();
fields_to_localize = keys.toArray(new String[keys.size()]);
} else {
fields_to_localize = fields;
}
//-- loop on fields to localize --//
try {
final MongoTranslationManager srvc = this.getTranslationManager();
for (final String field : fields_to_localize) {
if (onlyExistingFields && !item.containsField(field)) {
continue;
}
localize(srvc, item, lang, field);
}
} catch (Throwable ex) {
}
}
}
/**
* Retrieve e a list of "Field Id" values from translation collection.
*
* @param lang Language
* @param filterText Text to search for
* @return List of fieldIds.
*/
public final List<String> getTranslatedFieldIds(final String lang,
final String filterText) {
return this.getTranslationManager().getTranslatedFieldIds(lang, filterText);
}
// </editor-fold>
//<editor-fold defaultstate="collapsed" desc=" COUNT ">
public int count(final DBObject filter) {
return this.count(filter, true);
}
public int count(final DBObject filter,
final boolean usecursorCount) {
final DBCursor cursor = this.cursor(filter, new String[]{"_id"}, 0, 0, null, null);
return this.count(cursor, usecursorCount);
}
public int count(final DBCursor cursor,
final boolean usecursorCount) {
if (usecursorCount) {
return null != cursor ? cursor.count() : 0;
} else {
return null != cursor ? cursor.toArray().size() : 0;
}
}
//</editor-fold>
public void inc(final DBObject item, final String field, final int value) {
// db.analytic.update({"url" : "www.example.com"}, {"$inc" : {"pageviews" : 1}})
if (null != _coll && null != item && StringUtils.hasText(field) && value > 0) {
final Object id = this.getId(item);
final DBObject query = new BasicDBObject();
query.put(_ID, id);
final DBObject action = new BasicDBObject();
action.put(MODIFIER_INC, new BasicDBObject(field, value));
_coll.update(query, action);
}
}
// ------------------------------------------------------------------------
// p r o t e c t e d
// ------------------------------------------------------------------------
protected Logger getLogger() {
return LoggingUtils.getLogger(this);
}
protected MongoTranslationManager getTranslationManager() {
return _tm;
}
// ------------------------------------------------------------------------
// p r i v a t e
// ------------------------------------------------------------------------
private StandardCodedException getError500(final Throwable t) {
final String message = ExceptionUtils.getRealMessage(t);
return this.getError500(message);
}
private StandardCodedException getError500(final String message) {
return new StandardCodedException(
StandardCodedException.ERROR_500_SERVERERROR,
message);
}
private StandardCodedException getError418(final String message) {
return new StandardCodedException(
StandardCodedException.ERROR_418_IAMTEAPOT,
message);
}
private String getUUID() {
return MongoUtils.createUUID();
}
private boolean isUpdatedExisting(final WriteResult wr) {
if (null != wr) {
return this.isUpdatedExisting(wr.getLastError());
}
return true;
}
private boolean isUpdatedExisting(final CommandResult cmdRes) {
if (null != cmdRes) {
return cmdRes.getBoolean("updatedExisting", true);
}
return true;
}
private Object getId(final DBObject object) {
return MongoUtils.getId(object);
}
private Object getOrCreateId(final DBObject object) {
if (null != object) {
if (object.containsField(_ID)) {
return object.get(_ID);
} else {
return this.getUUID();
}
}
return null;
}
private boolean overwrite(final DBObject object, final Object id)
throws StandardCodedException {
final DBObject filter = new BasicDBObject(_ID, id);
// removeOne empty arrays
final DBObject item = this.removeEmptyArrays(object);
// save
return this.nativeUpdate(filter, item, true, false);
// object.put(_ID, id); // add _id
}
private boolean update(final DBObject object, final Object id)
throws StandardCodedException {
final DBObject filter = new BasicDBObject(_ID, id);
// removeOne empty arrays
final DBObject item = this.removeEmptyArrays(object);
// removeOne _id for update of other fields
item.removeField(_ID);
final DBObject operation = new BasicDBObject("$set", item);
// save
return this.nativeUpdate(filter, operation, true, false);
// object.put(_ID, id); // add _id
}
private boolean nativeUpdate(final DBObject query, final DBObject object,
final boolean upsert, final boolean multi)
throws StandardCodedException {
final WriteResult result = _coll.update(query, object, upsert, multi);
if (StringUtils.hasText(result.getError())) {
throw this.getError418(result.getError());
}
return this.isUpdatedExisting(result);
}
private DBObject merge(final DBObject object, final Object id,
final String[] excludeProperties)
throws StandardCodedException {
WriteResult result;
final DBObject existing = this.findById(id);
MongoUtils.merge(object, existing, excludeProperties);
// removeOne empty arrays before save
final DBObject item = this.removeEmptyArrays(existing);
// save
result = _coll.save(item);
if (StringUtils.hasText(result.getError())) {
throw this.getError418(result.getError());
}
return item;
}
private DBObject removeEmptyArrays(final DBObject object) {
return MongoUtils.removeEmptyArrays(object);
}
/**
* Return an object containing fields to include or fields to exclude.
* i.e. "{thumbnail:0}" exclude 'thumbnail' field.
*
* @param fields
* @param include
* @return
*/
private DBObject getFields(final String[] fields, final boolean include) {
return MongoUtils.getFields(fields, include);
}
private DBCursor cursor(final DBObject filter, final String[] fieldNames,
final int skip, final int limit,
final String[] sortAscBy, final String[] sortDescBy) {
if (null != _coll) {
DBCursor cursor;
final DBObject fields = this.getFields(fieldNames, true);
final DBObject sort = MongoUtils.getSortFields(sortAscBy, sortDescBy);
if (null != filter) {
cursor = null != fields
? _coll.find(filter, fields)
: _coll.find(filter);
} else {
cursor = null != fields
? _coll.find(new BasicDBObject(), fields)
: _coll.find();
}
// limit data
if (skip > 0 || limit > 0) {
if (skip > 0) {
cursor = cursor.skip(skip);
}
if (limit > 0) {
cursor = cursor.limit(limit);
}
}
// sort
if (null != cursor && null != sort) {
cursor = cursor.sort(sort);
}
return cursor;
}
return null;
}
// --------------------------------------------------------------------
// S T A T I C
// --------------------------------------------------------------------
/**
* Returns maxDistance value relative to earth radius
*
* @param maxDistanceInMeters max distance in meters
* @return maxDistance value relative to earth radius
*/
private static double getMaxDistance(final int maxDistanceInMeters) {
return maxDistanceInMeters / EARTH_RADIUS_mt;
}
private static void localize(final MongoTranslationManager translator,
final DBObject item,
final String lang,
final String field) {
final Object id = MongoUtils.getId(item);
if (field.indexOf(".") == -1) {
// standard field name
final DBObject value = MongoUtils.getDBObject(item, field, null);
if (null != value) {
// EMBEDDED TRANSLATIONS
localize(item, lang, field, value);
} else {
final Object translated = translator.get(id, field, lang);
if (!StringUtils.isNULL(translated)) {
item.put(field, translated);
} else {
// value is not replaced
// this.getLogger().info(StringUtils.format( "value not replaced for '{0}'. Keep original '{1}'", field, item.get(field)));
}
}
} else {
// path (i.e. 'types.description') [ONLY FOR EMBEDDED]
localizePath(item, lang, field);
}
}
private static void localizePath(final DBObject item,
final String lang,
final String path) {
final String[] tokens = StringUtils.split(path, ".");
if (tokens.length > 1) {
final String[] a = CollectionUtils.removeTokenFromArray(tokens, tokens.length - 1);
final String new_path = CollectionUtils.toDelimitedString(a, ".");
final Object propertyBean = MongoUtils.getByPath(item, new_path);
final String fieldName = CollectionUtils.getLast(tokens);
if (propertyBean instanceof List) {
final List list = (List) propertyBean;
for (final Object obj : list) {
if (obj instanceof DBObject) {
final DBObject dbo = (DBObject) obj;
final DBObject translation = MongoUtils.getDBObject(dbo, fieldName);
localize(dbo, lang, fieldName, translation);
}
}
} else if (propertyBean instanceof DBObject) {
final DBObject dbo = (DBObject) propertyBean;
final DBObject translation = MongoUtils.getDBObject(dbo, fieldName);
localize(dbo, lang, fieldName, translation);
}
}
}
private static void localize(final DBObject item,
final String lang,
final String field,
final DBObject translation) {
if (translation.containsField(lang)) {
item.put(field, translation.get(lang));
} else if (translation.containsField(LOCALE_BASE_FIELD)) {
item.put(field, translation.get(LOCALE_BASE_FIELD));
}
}
}