com.tomtom.speedtools.mongodb.DaoUtils.java Source code

Java tutorial

Introduction

Here is the source code for com.tomtom.speedtools.mongodb.DaoUtils.java

Source

/*
 * Copyright (C) 2012-2016. TomTom International BV (http://tomtom.com).
 *
 * 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 com.tomtom.speedtools.mongodb;

import com.mongodb.*;
import com.tomtom.speedtools.json.Json;
import com.tomtom.speedtools.mongodb.mappers.EntityMapper;
import com.tomtom.speedtools.mongodb.mappers.MapperError;
import com.tomtom.speedtools.mongodb.mappers.MapperException;
import com.tomtom.speedtools.time.UTCTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

/**
 * This class provides generic methods to operate on MongoDB Collections. Most methods are package private to make sure
 * they are only used by DAO's.
 */
public final class DaoUtils {
    private static final Logger LOG = LoggerFactory.getLogger(DaoUtils.class);
    private static final boolean UPSERT = true; // Indicates 'upsert' in case of missing record.
    private static final boolean NO_UPSERT = false; // Indicates 'no upsert' in case of missing record.
    private static final boolean NO_MULTI = false; // Indicates 'no multi' for queries.

    @SuppressWarnings("StaticNonFinalField")
    private static WriteConcern writeConcern = WriteConcern.SAFE;

    private DaoUtils() {
        // Prevent instantiation.
    }

    /**
     * Finds a single entity that satisfies a specified query filter.
     *
     * @param collection Collection to get the entity from.
     * @param mapper     Mapper to be used to reconstitute the object stored in the collection.
     * @param query      The query to select the entity.
     * @param <T>        The type of the reconstituted object.
     * @return The object in the collection for the given query.
     * @throws EntityNotFoundException Thrown if no object if found with the given id. This will NOT log an error.
     * @throws InternalDaoException    Thrown when an unknown error has occurred. The error will have been logged.
     */
    @Nonnull
    public static <T> T findOne(@Nonnull final DBCollection collection, @Nonnull final EntityMapper<T> mapper,
            @Nonnull final MongoDBQuery query) throws EntityNotFoundException, InternalDaoException {
        assert collection != null;
        assert mapper != null;
        assert query != null;

        DBObject dbObject = null;
        try {
            dbObject = collection.findOne(query.toDBObject());
            if (dbObject != null) {
                final T entity = mapper.fromDb(dbObject);
                // Cannot be null because dbObject was not null.
                assert entity != null;
                return entity;
            } else {
                final String message = "Entity not found: " + query + ", collection=" + collection.getName() + '.';
                LOG.debug("findOne: {}", message);
                throw new EntityNotFoundException(message);
            }
        } catch (final MapperException | MongoException e) {
            final String message = "Entity could not be mapped: " + query + ", collection=" + collection.getName()
                    + ", object=" + Json.toStringJson(dbObject) + '.';
            LOG.error("findOne: " + message, e);
            throw new InternalDaoException(message, e);
        }
    }

    /**
     * Finds entities that satisfy a specified query filter.
     *
     * @param collection Collection to get the entity from.
     * @param mapper     Mapper to be used to reconstitute the object stored in the collection.
     * @param query      The query to select the entity.
     * @param <T>        The type of the reconstituted object.
     * @return The objects in the collection for the given query.
     * @throws InternalDaoException Thrown when an unknown error has occurred. The error will have been logged.
     */
    @Nonnull
    public static <T> List<T> find(@Nonnull final DBCollection collection, @Nonnull final EntityMapper<T> mapper,
            @Nonnull final MongoDBQuery query) throws InternalDaoException {
        return find(collection, mapper, false, query);
    }

    /**
     * Finds entities that satisfy a specified query filter.
     *
     * @param <T>                Element type.
     * @param collection         Collection to get the entity from.
     * @param mapper             Mapper to be used to reconstitute the object stored in the collection.
     * @param ignoreMapperErrors Ignore mapping exception, skip entities with mapping errors.
     * @param query              The query to select the entity.
     * @return The objects in the collection for the given query.
     * @throws InternalDaoException Thrown when an unknown error has occurred. The error will have been logged.
     */
    @Nonnull
    public static <T> List<T> find(@Nonnull final DBCollection collection, @Nonnull final EntityMapper<T> mapper,
            final boolean ignoreMapperErrors, @Nonnull final MongoDBQuery query) throws InternalDaoException {
        return find(collection, mapper, ignoreMapperErrors, query, MongoDBSorting.DEFAULT_INSTANCE,
                MongoDBPaging.DEFAULT_INSTANCE);
    }

    /**
     * Finds entities that satisfy a specified query filter.
     *
     * @param <T>                Element type.
     * @param collection         Collection to get the entity from.
     * @param mapper             Mapper to be used to reconstitute the object stored in the collection.
     * @param ignoreMapperErrors Ignore mapping exception, skip entities with mapping errors.
     * @param query              The query to select the entity.
     * @param sorting            The sorting parameters for this query.
     * @param paging             The paging parameters for this query.
     * @return The objects in the collection for the given query.
     * @throws InternalDaoException Thrown when an unknown error has occurred. The error will have been logged.
     */
    @Nonnull
    public static <T> List<T> find(@Nonnull final DBCollection collection, @Nonnull final EntityMapper<T> mapper,
            final boolean ignoreMapperErrors, @Nonnull final MongoDBQuery query,
            @Nonnull final MongoDBSorting sorting, @Nonnull final MongoDBPaging paging)
            throws InternalDaoException {
        assert collection != null;
        assert mapper != null;
        assert query != null;

        try {
            final DBCursor dbCursor = collection.find(query.toDBObject());
            final DBCursor cursor = paging.apply(sorting.apply(dbCursor));

            final List<MapperError> errors = new LinkedList<>();
            final List<T> result = new ArrayList<>();
            while (cursor.hasNext()) {

                final DBObject dbValue = cursor.next();
                final T entity = mapper.fromDb(dbValue, errors);
                result.add(entity);
            }

            if (!errors.isEmpty()) {
                final String message = "Mapper errors found: " + query + ", collection=" + collection.getName()
                        + '.';
                LOG.error("find: {} Errors: '{}'", message, Json.toJson(errors));
                if (!ignoreMapperErrors) {
                    throw new InternalDaoException(message);
                }
            }
            return result;
        } catch (final MapperException | MongoException e) {
            final String message = "Entity could not be mapped: " + query + ", collection=" + collection.getName()
                    + '.';
            LOG.error("find: " + message, e);
            throw new InternalDaoException(message, e);
        }
    }

    /**
     * Store the given object in the given collections.
     *
     * @param <T>        The type of the object to te stored.
     * @param collection Collection in which to store the object.
     * @param mapper     Mapper to be used to transform the object.
     * @param entity     The object to be stored.
     * @throws EntityStoreException Thrown if the object cannot be transformed. The error will have been logged.
     */
    public static <T> void storeEntity(@Nonnull final DBCollection collection,
            @Nonnull final EntityMapper<T> mapper, @Nonnull final T entity) throws EntityStoreException {
        assert collection != null;
        assert mapper != null;
        assert entity != null;

        try {
            // Create MongoDB record.
            final DBObject dbObject = mapper.toDb(entity);

            // Cannot be null because entity was not null.
            assert dbObject != null;

            // Add a "last modified" timestamp to each record.
            dbObject.put(MongoDBKeyNames.LAST_MODIFIED_KEY, UTCTime.now().toDate());

            collection.save(dbObject, writeConcern);
        } catch (final MapperException | MongoException e) {
            final String message = "Map entity failed: type=" + entity.getClass().getSimpleName() + ", collection="
                    + collection.getName() + '.';
            LOG.error("storeEntity: " + message, e);
            throw new EntityStoreException(message, e);
        }
    }

    /**
     * Removes the given object from the given collection. The entity must fully match the database record to be
     * removed.
     *
     * @param <T>        The type of the object to remove.
     * @param collection Collection from which to remove the object.
     * @param mapper     Mapper to be used to transform the object.
     * @param entity     The object to be removed.
     * @throws EntityRemoveException Thrown if the object cannot be removed. The error will have been logged.
     */
    public static <T> void removeEntity(@Nonnull final DBCollection collection,
            @Nonnull final EntityMapper<T> mapper, @Nonnull final T entity) throws EntityRemoveException {
        assert collection != null;
        assert mapper != null;
        assert entity != null;

        try {
            final DBObject dbObject = mapper.toDb(entity);
            collection.remove(dbObject);
        } catch (final MapperException | MongoException e) {
            final String message = "Map entity failed: type=" + entity.getClass().getSimpleName() + ", collection="
                    + collection.getName() + '.';
            LOG.error("removeEntity: " + message, e);
            throw new EntityRemoveException(message, e);
        }
    }

    /**
     * Removes the given object from the given collection.
     *
     * @param <T>        Element type.
     * @param collection Collection from which to remove the object.
     * @param field      The field to match.
     * @param value      The value of the field.
     * @throws EntityRemoveException Thrown if the object cannot be removed. The error will have been logged.
     */
    public static <T> void removeEntityByField(@Nonnull final DBCollection collection,
            @Nonnull final EntityMapper<?>.Field<T> field, @Nonnull final T value) throws EntityRemoveException {
        assert collection != null;
        assert field != null;
        assert value != null;

        try {
            final MongoDBQuery dbQuery = new MongoDBQuery().eq(field, value);
            collection.remove(dbQuery.toDBObject());
        } catch (final MapperException | MongoException e) {
            final String message = "Remove entity failed: field=" + field.getFieldName() + ", collection="
                    + collection.getName() + '.';
            LOG.error("removeEntityByField: " + message, e);
            throw new EntityRemoveException(message, e);
        }
    }

    /**
     * Update a document in a collection. This will always update the last modified time as well.
     *
     * @param collection Collection that contains the document.
     * @param query      Query to find the document.
     * @param update     Update for document.
     * @throws EntityStoreException    New document cannot be stored. The error will have been logged.
     * @throws EntityNotFoundException The document was not found. The error will have been logged.
     */
    public static void update(@Nonnull final DBCollection collection, @Nonnull final MongoDBQuery query,
            @Nonnull final MongoDBUpdate update) throws EntityStoreException, EntityNotFoundException {
        update(collection, query, update, true);
    }

    /**
     * Update a document in a collection.
     *
     * @param collection         Collection that contains the document.
     * @param query              Query to find the document.
     * @param update             Update for document.
     * @param updateLastModified True if the last modified time needs to be adjusted as well (to now).
     * @throws EntityStoreException    New document cannot be stored. The error will have been logged.
     * @throws EntityNotFoundException The document was not found. The error will have been logged.
     */
    public static void update(@Nonnull final DBCollection collection, @Nonnull final MongoDBQuery query,
            @Nonnull final MongoDBUpdate update, final boolean updateLastModified)
            throws EntityStoreException, EntityNotFoundException {
        assert collection != null;
        assert query != null;
        assert update != null;
        try {

            // Update last modified time.
            if (updateLastModified) {
                update.setRaw(MongoDBKeyNames.LAST_MODIFIED_KEY, UTCTime.now().toDate());
            }

            // Make sure upsert is set to false to not create new records on the fly.
            final WriteResult result = collection.update(query.toDBObject(), update.toDBObject(), NO_UPSERT,
                    NO_MULTI, writeConcern);
            if (result.getN() == 0) {
                final String message = "Couldn't find entity to update, query: " + query + ", update: " + update
                        + ", " + "collection: " + collection.getName() + '.';
                LOG.error("update: {}", message);
                throw new EntityNotFoundException(message);
            }
        } catch (final MapperException | MongoException e) {
            final String message = "Couldn't map entity to update, query: " + query + ", update: " + update + ", "
                    + "collection: " + collection.getName() + '.';
            LOG.error("update: " + message, e);
            throw new EntityStoreException(message, e);
        }
    }

    /**
     * Returns number of entities that satisfy a specified query filter. When the collections is large and indexes are
     * missing this call can be a performance killer.
     *
     * @param collection Collection to get the entity from.
     * @param query      The query to select the entity.
     * @return The number of objects in the collection for the given query.
     * @throws InternalDaoException Thrown when an unknown error has occurred. The error will have been logged.
     */
    public static long count(@Nonnull final DBCollection collection, @Nonnull final MongoDBQuery query)
            throws InternalDaoException {
        assert collection != null;
        assert query != null;

        try {
            LOG.debug("count: collection={}, query={}", collection.getName(), query);
            return collection.count(query.toDBObject());
        } catch (final MapperException e) {
            final String message = "Entity could not be mapped: " + query + ", collection=" + collection.getName()
                    + '.';
            LOG.error("count: " + message, e);
            throw new InternalDaoException(message, e);
        }
    }

    /**
     * Update or insert a document in a collection. This will always update the last modified time as well.
     *
     * @param collection Collection that contains the document.
     * @param query      Query to find the document.
     * @param value      Update for document.
     * @throws EntityStoreException New document cannot be stored. The error will have been logged.
     */
    public static void upsert(@Nonnull final DBCollection collection, @Nonnull final MongoDBQuery query,
            @Nonnull final DBObject value) throws EntityStoreException {
        upsert(collection, query, value, true);
    }

    /**
     * Update or insert a document in a collection.
     *
     * @param collection         Collection that contains the document.
     * @param query              Query to find the document.
     * @param value              Update (or initial value) for document.
     * @param updateLastModified True if the last modified time needs to be adjusted as well (to now).
     * @return Number of records updated (0 if inserted, 1 if updated).
     * @throws EntityStoreException New document cannot be stored. The error will have been logged.
     */
    public static int upsert(@Nonnull final DBCollection collection, @Nonnull final MongoDBQuery query,
            @Nonnull final DBObject value, final boolean updateLastModified) throws EntityStoreException {
        assert collection != null;
        assert query != null;
        assert value != null;
        try {

            // Update last modified time.
            if (updateLastModified) {
                value.put(MongoDBKeyNames.LAST_MODIFIED_KEY, UTCTime.now().toDate());
            }

            // Make sure upsert is set to true to create the object if it is not found.
            final WriteResult result = collection.update(query.toDBObject(), value, UPSERT, NO_MULTI, writeConcern);
            final int nr = result.getN();
            if (nr == 0) {
                LOG.debug("upsert: Inserted new object, query={}, collection={}", query, collection.getName());
            }
            return nr;
        } catch (final MapperException | MongoException e) {
            final String message = "Couldn't map entity to update, query: " + query + ", update: " + value + ", "
                    + "collection: " + collection.getName() + '.';
            LOG.error("upsert: " + message, e);
            throw new EntityStoreException(message, e);
        }
    }

    /**
     * This retrieves the global write concern of the DAO utils for save() and update() calls.
     *
     * @return Global write concern.
     */
    @Nonnull
    public static WriteConcern getWriteConcern() {
        return writeConcern;
    }

    /**
     * This sets the global write concern of the DAO utils for save() and update() calls.
     *
     * @param newWriteConcern Write concern.
     */
    public static void setWriteConcern(@Nonnull final WriteConcern newWriteConcern) {
        assert newWriteConcern != null;
        writeConcern = newWriteConcern;
    }
}