org.wisdom.jongo.bridge.JongoCRUDService.java Source code

Java tutorial

Introduction

Here is the source code for org.wisdom.jongo.bridge.JongoCRUDService.java

Source

/*
 * #%L
 * Wisdom-Framework
 * %%
 * Copyright (C) 2015 Wisdom Framework
 * %%
 * 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.
 * #L%
 */
package org.wisdom.jongo.bridge;

import com.mongodb.DB;
import com.mongodb.WriteResult;
import org.bson.types.ObjectId;

import org.jongo.Jongo;
import org.jongo.MongoCollection;
import org.wisdom.api.model.*;
import org.wisdom.jongo.service.JongoCRUD;
import org.wisdom.jongo.service.MongoFilter;

import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;

import static org.jongo.Oid.withOid;

/**
 * Jongo Crud service for the Wisdom-Framework. Extends the provided crud service.
 *
 * @param <T> the entity class that you wish to use the crud services with.
 * @param <K> The type of the id field found in the entity class. Ie String, Long, ObjectId.
 *            NOTE: Jongo seems to be limited to  6 types ids that it recognizes. Three are auto created ids by the database
 *            Case 1: field named _id of type String or long annotated with @ObjectId.
 *            Case 2: string or long with any name, annotated with both @ObjectId and @Id.
 *            Case 3: Type org.bson.types.ObjectId named _id.
 *            There are 3 type where you must manually set the key before saving to the database.
 *            Case 1: type long name id. No annotations.
 *            Case 2: type long annotated with @Id named whatever you want.
 *            Case 3: type string annotated with @Id named whatever you want.
 *            All other case are currently not supported.
 */
public class JongoCRUDService<T, K extends Serializable> implements JongoCRUD<T, K> {

    private final Class<T> entityClass;
    private final Class<K> entityKeyClass;
    private final MongoCollection collection;
    private final Field idField;
    private Class idFieldType;
    private JongoRepository repository;

    //constant name of the field in the entity to be used for id
    private static final String ID = "_id";

    /**
     * Constructor
     *
     * @param clazz the class using the service.
     * @param db    the database the service is connecting to.
     */
    public JongoCRUDService(Class<T> clazz, DB db) {
        this.entityClass = clazz;
        Jongo jongo = new Jongo(db);
        collection = jongo.getCollection(entityClass.getSimpleName());
        this.idField = findIdField();
        entityKeyClass = (Class<K>) this.idField.getType();
    }

    /**
     * Sets the repository to use.
     *
     * @param repository the repository we want to interact with.
     */
    public void setRepository(JongoRepository repository) {
        this.repository = repository;
    }

    /**
     * Sets the idField type.
     *
     * @param idFieldType the type of the id field.
     */
    public void setIdFieldType(Class idFieldType) {
        this.idFieldType = idFieldType;
    }

    /**
     * Get the value of the id field from an entity.
     * @param o the entity who's field we wish to access.
     * @return the value from the field.
     */
    private K getEntityId(T o) {
        try {
            if (!idField.isAccessible()) {
                idField.setAccessible(true);
            }
            return (K) idField.get(o);
        } catch (IllegalAccessException e) {
            // TODO LOGGER HERE
            return null;
        }
    }

    /**
     * Check the fields in the entity class and parent class to find the correct id field.
     *
     * @return returns the field that has the correct annotations.
     */
    private Field findIdField() {
        if (idField != null) {
            return idField;
        }

        //check all declared fields first
        for (Field field : entityClass.getDeclaredFields()) {
            if (isEntityId(field)) {
                return field;
            }
        }
        //If not found above check in the parent classes
        for (Field field : entityClass.getFields()) {
            if (isEntityId(field)) {
                return field;
            }
        }

        throw new IllegalStateException("Cannot find the id field inside " + entityClass.getName());
    }

    /**
     * Check each field to see if it has the annotations we are looking for.
     *
     * @param field from an entity.
     * @return true if it has the correct annotations otherwise returns false. Assumes that there isn't more than
     * one field with correct annotations.
     */
    private boolean isEntityId(Field field) {
        Class type = field.getType();
        String name = field.getName();

        org.jongo.marshall.jackson.oid.ObjectId objectId = field
                .getAnnotation(org.jongo.marshall.jackson.oid.ObjectId.class);
        org.jongo.marshall.jackson.oid.Id id = field.getAnnotation(org.jongo.marshall.jackson.oid.Id.class);

        if (ID.equals(name) && objectId != null || id != null && objectId != null) {
            setIdFieldType(ObjectId.class);
            return true;
        }
        if (hasAnnotation(field, org.jongo.marshall.jackson.oid.Id.class) || type.equals(ObjectId.class)) { // objectId is null
            setIdFieldType(type);
            return true;
        }
        if (id == null && objectId == null && ID.equals(name)) {
            setIdFieldType(type);
            return true;
        }

        return false;
    }

    /**
     * Check if the filed is annotated.
     *
     * @param field      field from current class or parent class.
     * @param annotation the annotation we are searching for.
     * @return true if found false if not.
     */
    private boolean hasAnnotation(Field field, Class annotation) {
        for (Annotation ann : field.getAnnotations()) {
            if (ann.annotationType().getName().equals(annotation.getName())) {
                return true;
            }
        }
        return false;
    }

    /**
     * Gets the entity class that is using the database.
     *
     * @return the class.
     */
    @Override
    public Class<T> getEntityClass() {
        return entityClass;
    }

    /**
     * Get the type of the Id of the entity.
     *
     * @return type String.
     */
    @Override
    public Class<K> getIdClass() {
        return entityKeyClass;
    }

    /**
     * Save a new copy of the entity in the database if it doesn't not already exist. If the entity already exists
     * (i.e the same ID number) then it should update the existing copy.
     *
     * @param o the entity to save.
     * @return the updated entity with an id number.
     */
    @Override
    public T save(T o) {
        if (!o.getClass().equals(entityClass)) {
            // probably a super class
            o = createFromCustomConstructor(o);
        }
        WriteResult result = collection.save(o);
        if (result.getError() != null) {
            throw new RuntimeException(
                    "Cannot save instance " + o + " in " + collection.getName() + " : " + result.getLastError());
        } else {
            return o;
        }
    }

    private T createFromCustomConstructor(T o) {
        // Try to find a constructor that match the class of o
        try {
            Constructor<T> constructor = entityClass.getConstructor(o.getClass());
            if (!constructor.isAccessible()) {
                constructor.setAccessible(true);
            }
            return constructor.newInstance(o);
        } catch (NoSuchMethodException e) {
            throw new RuntimeException("The object " + o + " cannot be saved - incompatible type and no 'copy' "
                    + "constructor. The class " + entityClass.getName() + " requires a constructor accepting a "
                    + o.getClass().getName() + " has unique parameter.");
        } catch (InvocationTargetException e) {
            throw new RuntimeException(
                    "The object " + o + " cannot be saved - the constructor has thrown an " + "exception", e);
        } catch (InstantiationException e) {
            throw new RuntimeException("The object " + o + " cannot be saved - the class cannot be instantiated");
        } catch (IllegalAccessException e) {
            throw new RuntimeException("The object " + o + " cannot be saved - unaccessible constructor");
        }
    }

    /**
     * Save a new copy of the entity in the iterable list if it doesn't exist, or updates if it does exists.
     *
     * @param iterable the collection of entities to be saved.
     * @return an iterable of the collections of entities that were saved.
     */
    @Override
    public Iterable<T> save(Iterable<T> iterable) {
        List<T> list = new ArrayList<>();
        for (T t : iterable) {
            list.add(save(t));
        }
        iterable = list;
        return iterable;
    }

    /**
     * Find an object from the database by it's unique Id number.
     *
     * @param id the unique id of the object.
     * @return the object if it exists, otherwise return null.
     */
    @Override
    public T findOne(K id) {

        if (id == null) {
            return null;
        }

        if (idFieldType.equals(ObjectId.class)) {
            String oid = id.toString();
            if (ObjectId.isValid(oid)) {
                return collection.findOne(withOid(id.toString())).as(entityClass);
            } else {
                return null;
            }
        }

        if (idFieldType.equals(String.class) || idFieldType.equals(Long.class) || idFieldType.equals(Long.TYPE)) {
            return collection.findOne(createIdQuery(id)).as(entityClass);
        }

        throw new IllegalArgumentException("Id of type '" + id + "' is not supported");
    }

    /**
     * Find one entity using the Mongo filter which gives us access to mongo query string formats.
     *
     * @param filter what we are searching for.
     * @return the einity if found otherwise returns null.
     */
    @Override
    public T findOne(EntityFilter<T> filter) {
        if (filter instanceof MongoFilter) {
            final MongoFilter dbFilter = (MongoFilter) filter;
            return collection.findOne(dbFilter.getFilter(), dbFilter.getParams()).as(entityClass);
        } else {
            for (T entity : findAll()) {
                if (filter.accept(entity)) {
                    return entity;
                }
            }
        }
        return null;
    }

    /**
     * Find all of the objects in a Mongo Collection.
     *
     * @return an iterable of the entity type.
     */
    @Override
    public Iterable<T> findAll() {
        return collection.find().as(entityClass);
    }

    @Override
    public Iterable<T> findAll(Iterable<K> iterable) {
        List<T> entities = new ArrayList<>();
        for (K key : iterable) {
            T entity = findOne(key);
            if (entity == null) {
                throw new IllegalArgumentException(
                        "Cannot find an entity of type " + entityClass + " with id " + key);
            }
            entities.add(entity);
        }
        return entities;
    }

    /**
     * Find all of the objects in a Mongo Collection using a filter.
     *
     * @param filter what we want to search for.
     * @return an iterable of the entity type.
     */
    @Override
    public Iterable<T> findAll(EntityFilter<T> filter) {
        if (filter instanceof MongoFilter) {
            final MongoFilter dbFilter = (MongoFilter) filter;
            return collection.find(dbFilter.getFilter(), dbFilter.getParams()).as(entityClass);
        } else {
            List<T> entities = new ArrayList<>();
            for (T entity : findAll()) {
                if (filter.accept(entity)) {
                    entities.add(entity);
                }
            }
            return entities;
        }
    }

    /**
     * Delete an object by  from the collection if it exists.
     *
     * @param id of the object you wish to delete for.
     *           If the id doesn't exist there is an IllegalArgumentException.
     *           <p>
     *           Note: as far as I can tell jongo only supports remove for object id types and not others.
     */
    @Override
    public void delete(K id) {
        WriteResult result;
        if (idFieldType.equals(ObjectId.class)) {
            result = collection.remove(withOid(String.valueOf(id)));
        } else {
            result = collection.remove(createIdQuery(id));

        }
        //get n is number of docs effected by operation in mongo
        if (result.getN() == 0) {
            throw new IllegalArgumentException("Unable to delete Id '" + id + "' not found");
        }
    }

    /**
     * Delete an object by  from the collection if it exists.
     *
     * @param o is an object that is an entity. It needs to have a valid _id field.
     * @return returns the original object passed in.
     * If the id doesn't exist there is an IllegalArgumentException.
     */
    @Override
    public T delete(T o) {
        K id = getEntityId(o);
        delete(id);
        return o;
    }

    /**
     * Delete a list of objects in the form of iterable from the collection if they exist.
     *
     * @param iterable is an iterable of entities.
     * @return the original iterable.
     */
    @Override
    public Iterable<T> delete(Iterable<T> iterable) {
        for (T temp : iterable) {
            delete(temp);
        }
        return iterable;
    }

    /**
     * Method provided by jongo to delete everything in the collection. Use with caution.
     */
    public void deleteAllFromCollection() {
        collection.remove();
    }

    /**
     * Checks to see if the object exists in the Mongo Collection based on its ID.
     *
     * @param id of the object to search for.
     * @return true if found false if not found.
     */
    @Override
    public boolean exists(K id) {
        return findOne(id) != null;
    }

    /**
     * Count the number of objects that are of the entity type in a Mongo Collection.
     *
     * @return count as type Long.
     */
    @Override
    public long count() {
        return collection.count();
    }

    private String createIdQuery(K id) {
        //for ids that are of type string
        if (idFieldType.equals(String.class)) {
            return "{" + ID + " : '" + id + "'}";
        }
        //for ids that are of type long
        if (idFieldType.equals(Long.class) || idFieldType.equals(Long.TYPE)) {
            return "{" + ID + " : " + id + "}";
        }
        //any other id type than String, Long, or ObjectId are not currently support
        throw new IllegalArgumentException("Id of type '" + id + "' is not supported");

    }

    /**
     * Get the Repository.
     * @return the repository.
     */
    @Override
    public Repository getRepository() {
        return repository;
    }

    /**
     * Not Support by Mongo
     */
    @Override
    public void executeTransactionalBlock(Runnable runnable) throws HasBeenRollBackException {
        throw new UnsupportedOperationException("MongoDB does not support transactions");

    }

    /**
     * Not Support by Mongo
     */
    @Override
    public TransactionManager getTransactionManager() {
        throw new UnsupportedOperationException("MongoDB does not support transactions");
    }

    /**
     * Not Support by Mongo
     */
    @Override
    public <R> FluentTransaction<R>.Intermediate transaction(Callable<R> callable) {
        throw new UnsupportedOperationException("MongoDB does not support transactions");
    }

    /**
     * Not Support by Mongo
     */
    @Override
    public <R> FluentTransaction<R> transaction() {
        throw new UnsupportedOperationException("MongoDB does not support transactions");
    }

    /**
     * Not Support by Mongo
     */
    @Override
    public <A> A executeTransactionalBlock(Callable<A> callable) throws HasBeenRollBackException {
        throw new UnsupportedOperationException("MongoDB does not support transactions");
    }
}