jsondb.JsonDBTemplate.java Source code

Java tutorial

Introduction

Here is the source code for jsondb.JsonDBTemplate.java

Source

/*
 * Copyright (c) 2016 Farooq Khan
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to
 * deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
 * sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package jsondb;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.charset.CharacterCodingException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.jxpath.JXPathContext;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import jsondb.annotation.DBRef;

import jsondb.crypto.CryptoUtil;
import jsondb.crypto.ICipher;
import jsondb.events.CollectionFileChangeListener;
import jsondb.events.EventListenerList;
import jsondb.io.JsonFileLockException;
import jsondb.io.JsonReader;
import jsondb.io.JsonWriter;
import jsondb.query.Update;
import jsondb.query.ddl.AddOperation;
import jsondb.query.ddl.CollectionSchemaUpdate;
import jsondb.query.ddl.DeleteOperation;
import jsondb.query.ddl.RenameOperation;
import java.lang.reflect.Field;
import java.util.UUID;
import java.util.logging.Level;
import org.pmw.tinylog.Logger;

/**
 * @version 1.0 25-Sep-2016
 */
public class JsonDBTemplate implements JsonDBOperations {

    private JsonDBConfig dbConfig = null;
    private final boolean encrypted;
    private File lockFilesLocation;
    private EventListenerList eventListenerList;

    private Map<String, CollectionMetaData> cmdMap;
    private AtomicReference<Map<String, File>> fileObjectsRef = new AtomicReference<>(new ConcurrentHashMap<>());
    private AtomicReference<Map<String, Map<Object, ?>>> collectionsRef = new AtomicReference<Map<String, Map<Object, ?>>>(
            new ConcurrentHashMap<String, Map<Object, ?>>());
    private AtomicReference<Map<String, JXPathContext>> contextsRef = new AtomicReference<Map<String, JXPathContext>>(
            new ConcurrentHashMap<String, JXPathContext>());

    public JsonDBTemplate(JsonDBConfig config) {
        this.dbConfig = config;

        if (dbConfig.getCipher() == null) {
            Logger.info("Die Verschlsselung ist deaktiviert");
            this.encrypted = false;
        } else {
            Logger.info("Die Verschlsselung ist aktiviert");
            this.encrypted = true;
        }
        this.initialize();
        eventListenerList = new EventListenerList(dbConfig, cmdMap);
    }

    public JsonDBTemplate(String dbFilesLocation, String iojsondbtestsmodel, String pre, Object object, boolean b,
            Object object0) {
        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
    }

    private void initialize() {
        if (!dbConfig.getDbFilesLocation().exists()) {
            try {
                Files.createDirectory(dbConfig.getDbFilesPath());
            } catch (IOException e) {
                Logger.error("DbFiles directory does not exist. Failed to create a new empty DBFiles directory {}",
                        e);
                throw new InvalidJsonDbApiUsageException(
                        "DbFiles directory does not exist. Failed to create a new empty DBFiles directory "
                                + dbConfig.getDbFilesLocationString());
            }
        } else if (dbConfig.getDbFilesLocation().isFile()) {
            throw new InvalidJsonDbApiUsageException(
                    "Specified DbFiles directory is actually a file cannot use it as a directory");
        }
        this.lockFilesLocation = new File(dbConfig.getDbFilesLocation(), "lock");
        if (!lockFilesLocation.exists()) {
            lockFilesLocation.mkdirs();
        }

        cmdMap = CollectionMetaData.builder(dbConfig);

        loadDB();

        // Auto-cleanup at shutdown
        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                eventListenerList.shutdown();
            }
        });
    }

    /* (non-Javadoc)
    * @see io.jsondb.JsonDBOperations#reLoadDB()
     */
    @Override
    public void reLoadDB() {
        loadDB();
    }

    private synchronized void loadDB() {
        for (String collectionName : cmdMap.keySet()) {
            File collectionFile = new File(dbConfig.getDbFilesLocation(),
                    dbConfig.getDBPrefix() + collectionName + ".json");
            if (collectionFile.exists()) {
                reloadCollection(collectionName);
            } else if (collectionsRef.get().containsKey(collectionName)) {
                //this probably is a reload attempt after a collection .json was deleted.
                //that is the reason even though the file does not exist a entry into collectionsRef still exists.
                contextsRef.get().remove(collectionName);
                collectionsRef.get().remove(collectionName);
            }
        }
    }

    /* (non-Javadoc)
    * @see io.jsondb.JsonDBOperations#reloadCollection(java.lang.String)
     */
    public void reloadCollection(String collectionName) {
        CollectionMetaData cmd = cmdMap.get(collectionName);
        cmd.getCollectionLock().writeLock().lock();
        try {
            File collectionFile = fileObjectsRef.get().get(collectionName);
            if (null == collectionFile) {
                // Lets create a file now
                collectionFile = new File(dbConfig.getDbFilesLocation(),
                        dbConfig.getDBPrefix() + collectionName + ".json");
                if (!collectionFile.exists()) {
                    throw new InvalidJsonDbApiUsageException("Collection by name '" + collectionName
                            + "' cannot be found at " + collectionFile.getAbsolutePath());
                }
                Map<String, File> fileObjectMap = fileObjectsRef.get();
                Map<String, File> newFileObjectmap = new ConcurrentHashMap<String, File>(fileObjectMap);
                newFileObjectmap.put(collectionName, collectionFile);
                fileObjectsRef.set(newFileObjectmap);
            }
            if (null != cmd && null != collectionFile) {
                Map<Object, ?> collection = loadCollection(collectionFile, collectionName, cmd);
                if (null != collection) {
                    JXPathContext newContext = JXPathContext.newContext(collection.values());
                    contextsRef.get().put(collectionName, newContext);
                    collectionsRef.get().put(collectionName, collection);
                } else {
                    //Since this is a reload attempt its possible the .json files have disappeared in the interim a very rare thing
                    contextsRef.get().remove(collectionName);
                    collectionsRef.get().remove(collectionName);
                }
            }
        } finally {
            cmd.getCollectionLock().writeLock().unlock();
        }
    }

    private <T> Map<Object, T> loadCollection(File collectionFile, String collectionName, CollectionMetaData cmd) {
        @SuppressWarnings("unchecked")
        Class<T> entity = cmd.getClazz();
        Method getterMethodForId = cmd.getIdAnnotatedFieldGetterMethod();

        JsonReader jr = null;
        Map<Object, T> collection = new LinkedHashMap<Object, T>();

        String line = null;
        int lineNo = 1;
        try {
            jr = new JsonReader(dbConfig, collectionFile);

            while ((line = jr.readLine()) != null) {
                if (lineNo == 1) {
                    SchemaVersion v = dbConfig.getObjectMapper().readValue(line, SchemaVersion.class);
                    cmd.setActualSchemaVersion(v.getSchemaVersion());
                } else {
                    T row = dbConfig.getObjectMapper().readValue(line, entity);
                    Object id = Util.getIdForEntity(row, getterMethodForId);
                    collection.put(id, row);
                }
                lineNo++;
            }
        } catch (JsonParseException je) {
            Logger.error("Failed Json Parsing for file {} line {}", collectionFile.getName(), lineNo, je);
            return null;
        } catch (JsonMappingException jm) {
            Logger.error("Failed Mapping Parsed Json to Entity {} for file {} line {}", entity.getSimpleName(),
                    collectionFile.getName(), lineNo, jm);
            return null;
        } catch (CharacterCodingException ce) {
            Logger.error("Unsupported Character Encoding in file {} expected Encoding {}", collectionFile.getName(),
                    dbConfig.getCharset().displayName(), ce);
            return null;
        } catch (JsonFileLockException jfe) {
            Logger.error("Failed to acquire lock for collection file {}", collectionFile.getName(), jfe);
            return null;
        } catch (FileNotFoundException fe) {
            Logger.error("Collection file {} not found", collectionFile.getName(), fe);
            return null;
        } catch (IOException e) {
            Logger.error("Some IO Exception reading the Json File {}", collectionFile.getName(), e);
            return null;
        } catch (Throwable t) {
            Logger.error("Throwable Caught ", collectionFile.getName(), t);
            return null;
        } finally {
            if (null != jr) {
                jr.close();
            }
        }
        return collection;
    }

    /* (non-Javadoc)
    * @see org.jsondb.JsonDBOperations#addCollectionFileChangeListener(org.jsondb.CollectionFileChangeListener)
     */
    @Override
    public void addCollectionFileChangeListener(CollectionFileChangeListener listener) {
        eventListenerList.addCollectionFileChangeListener(listener);
    }

    /* (non-Javadoc)
    * @see org.jsondb.JsonDBOperations#removeCollectionFileChangeListener(org.jsondb.CollectionFileChangeListener)
     */
    @Override
    public void removeCollectionFileChangeListener(CollectionFileChangeListener listener) {
        eventListenerList.removeCollectionFileChangeListener(listener);
    }

    /* (non-Javadoc)
    * @see io.jsondb.JsonDBOperations#hasCollectionFileChangeListener()
     */
    @Override
    public boolean hasCollectionFileChangeListener() {
        return eventListenerList.hasCollectionFileChangeListener();
    }

    /* (non-Javadoc)
    * @see io.jsondb.JsonDBOperations#createCollection(java.lang.Class)
     */
    @Override
    public <T> void createCollection(Class<T> entityClass) {
        createCollection(Util.determineCollectionName(entityClass));
    }

    /* (non-Javadoc)
    * @see io.jsondb.JsonDBOperations#createCollection(java.lang.String)
     */
    @Override
    public <T> void createCollection(String collectionName) {
        CollectionMetaData cmd = cmdMap.get(collectionName);
        if (null == cmd) {
            throw new InvalidJsonDbApiUsageException(
                    "No class found with @Document Annotation and attribute collectionName as: " + collectionName);
        }
        @SuppressWarnings("unchecked")
        Map<Object, T> collection = (Map<Object, T>) collectionsRef.get().get(collectionName);
        if (null != collection) {
            throw new InvalidJsonDbApiUsageException("Collection by name '" + collectionName + "' already exists.");
        }

        cmd.getCollectionLock().writeLock().lock();

        // Some other thread might have created same collection when this thread reached this point
        if (collectionsRef.get().get(collectionName) != null) {
            return;
        }

        try {
            String collectionFileName = dbConfig.getDBPrefix() + collectionName + ".json";
            File fileObject = new File(dbConfig.getDbFilesLocation(), collectionFileName);
            try {
                fileObject.createNewFile();
            } catch (IOException e) {
                Logger.error("IO Exception creating the collection file {}", collectionFileName, e);
                throw new InvalidJsonDbApiUsageException(
                        "Unable to create a collection file for collection: " + collectionName);
            }

            if (Util.stampVersion(dbConfig, fileObject, cmd.getSchemaVersion())) {
                collection = new LinkedHashMap<Object, T>();
                collectionsRef.get().put(collectionName, collection);
                contextsRef.get().put(collectionName, JXPathContext.newContext(collection.values()));
                fileObjectsRef.get().put(collectionName, fileObject);
                cmd.setActualSchemaVersion(cmd.getSchemaVersion());
            } else {
                fileObject.delete();
                throw new JsonDBException("Failed to stamp version for collection: " + collectionName);
            }
        } finally {
            cmd.getCollectionLock().writeLock().unlock();
        }
    }

    /* (non-Javadoc)
    * @see io.jsondb.JsonDBOperations#dropCollection(java.lang.Class)
     */
    @Override
    public <T> void dropCollection(Class<T> entityClass) {
        dropCollection(Util.determineCollectionName(entityClass));
    }

    /* (non-Javadoc)
    * @see io.jsondb.JsonDBOperations#dropCollection(java.lang.String)
     */
    @Override
    public void dropCollection(String collectionName) {
        CollectionMetaData cmd = cmdMap.get(collectionName);
        if ((null == cmd) || (!collectionsRef.get().containsKey(collectionName))) {
            throw new InvalidJsonDbApiUsageException(
                    "Collection by name '" + collectionName + "' not found. Create collection first.");
        }
        cmd.getCollectionLock().writeLock().lock();
        try {
            File toDelete = fileObjectsRef.get().get(collectionName);
            try {
                Files.deleteIfExists(toDelete.toPath());
            } catch (IOException e) {
                Logger.error("IO Exception deleting the collection file {}", toDelete.getName(), e);
                throw new InvalidJsonDbApiUsageException(
                        "Unable to create a collection file for collection: " + collectionName);
            }
            //cmdMap.remove(collectionName); //Do not remove it from the CollectionMetaData Map.
            //Someone might want to re insert a new collection of this type.
            fileObjectsRef.get().remove(collectionName);
            collectionsRef.get().remove(collectionName);
            contextsRef.get().remove(collectionName);
        } finally {
            cmd.getCollectionLock().writeLock().unlock();
        }
    }

    /* (non-Javadoc)
    * @see org.jsondb.JsonDBOperations#updateCollectionSchema(org.jsondb.query.CollectionSchemaUpdate, java.lang.Class)
     */
    @Override
    public <T> void updateCollectionSchema(CollectionSchemaUpdate update, Class<T> entityClass) {
        updateCollectionSchema(update, Util.determineCollectionName(entityClass));
    }

    /* (non-Javadoc)
    * @see org.jsondb.JsonDBOperations#updateCollectionSchema(org.jsondb.query.CollectionSchemaUpdate, java.lang.String)
     */
    @Override
    public <T> void updateCollectionSchema(CollectionSchemaUpdate update, String collectionName) {
        CollectionMetaData cmd = cmdMap.get(collectionName);
        @SuppressWarnings("unchecked")
        Map<Object, T> collection = (Map<Object, T>) collectionsRef.get().get(collectionName);
        if ((null == cmd) || (null == collection)) {
            throw new InvalidJsonDbApiUsageException(
                    "Collection by name '" + collectionName + "' not found. Create collection first.");
        }
        boolean reloadCollectionAsSomethingChanged = false;
        //We only take care of ADD and RENAME, the deletes will be taken care of automatically.
        if (null != update) {
            Map<String, RenameOperation> renOps = update.getRenameOperations();
            if (renOps.size() > 0) {
                reloadCollectionAsSomethingChanged = true;
                cmd.getCollectionLock().writeLock().lock();

                for (Entry<String, RenameOperation> updateEntry : renOps.entrySet()) {
                    String oldKey = updateEntry.getKey();

                    RenameOperation op = updateEntry.getValue();
                    String newKey = op.getNewName();

                    JsonWriter jw;
                    try {
                        jw = new JsonWriter(dbConfig, cmd, collectionName,
                                fileObjectsRef.get().get(collectionName));
                    } catch (IOException ioe) {
                        Logger.error("Failed to obtain writer for " + collectionName, ioe);
                        throw new JsonDBException("Failed to save " + collectionName, ioe);
                    }
                    jw.renameKeyInJsonFile(collection.values(), true, oldKey, newKey);
                }
                cmd.getCollectionLock().writeLock().unlock();
            }

            Map<String, AddOperation> addOps = update.getAddOperations();
            if (addOps.size() > 0) {
                reloadCollectionAsSomethingChanged = true;
                cmd.getCollectionLock().writeLock().lock();

                for (Entry<String, AddOperation> updateEntry : addOps.entrySet()) {
                    AddOperation op = updateEntry.getValue();

                    Object value = null;
                    if (op.isSecret()) {
                        value = dbConfig.getCipher().encrypt((String) op.getDefaultValue());
                    } else {
                        value = op.getDefaultValue();
                    }

                    String fieldName = updateEntry.getKey();
                    Method setterMethod = cmd.getSetterMethodForFieldName(fieldName);
                    for (T object : collection.values()) {
                        Util.setFieldValueForEntity(object, value, setterMethod);
                    }
                }

                JsonWriter jw;
                try {
                    jw = new JsonWriter(dbConfig, cmd, collectionName, fileObjectsRef.get().get(collectionName));
                } catch (IOException ioe) {
                    Logger.error("Failed to obtain writer for " + collectionName, ioe);
                    throw new JsonDBException("Failed to save " + collectionName, ioe);
                }
                jw.reWriteJsonFile(collection.values(), true);
                cmd.getCollectionLock().writeLock().unlock();
            }

            Map<String, DeleteOperation> delOps = update.getDeleteOperations();
            if ((renOps.size() < 1 && addOps.size() < 1) && (delOps.size() > 0)) {
                //There were no ADD operations but there are some DELETE operations so we have to just flush the collection once
                //This would not have been necessary if there was even 1 ADD operation

                reloadCollectionAsSomethingChanged = true;
                cmd.getCollectionLock().writeLock().lock();

                JsonWriter jw;
                try {
                    jw = new JsonWriter(dbConfig, cmd, collectionName, fileObjectsRef.get().get(collectionName));
                } catch (IOException ioe) {
                    Logger.error("Failed to obtain writer for " + collectionName, ioe);
                    throw new JsonDBException("Failed to save " + collectionName, ioe);
                }
                jw.reWriteJsonFile(collection.values(), true);
                cmd.getCollectionLock().writeLock().unlock();
            }
            if (reloadCollectionAsSomethingChanged) {
                reloadCollection(collectionName);
            }
        }
    }

    /* (non-Javadoc)
    * @see io.jsondb.JsonDBOperations#getCollectionNames()
     */
    @Override
    public Set<String> getCollectionNames() {
        return collectionsRef.get().keySet();
    }

    /* (non-Javadoc)
    * @see io.jsondb.JsonDBOperations#getCollectionName(java.lang.Class)
     */
    @Override
    public String getCollectionName(Class<?> entityClass) {
        return Util.determineCollectionName(entityClass);
    }

    /* (non-Javadoc)
    * @see io.jsondb.JsonDBOperations#getCollection(java.lang.Class)
     */
    @SuppressWarnings("unchecked")
    @Override
    public <T> List<T> getCollection(Class<T> entityClass) {
        String collectionName = Util.determineCollectionName(entityClass);
        Map<Object, T> collection = (Map<Object, T>) collectionsRef.get().get(collectionName);
        if (null == collection) {
            createCollection(collectionName);
            collection = (Map<Object, T>) collectionsRef.get().get(collectionName);
        }

        CollectionMetaData cmd = cmdMap.get(collectionName);
        List<T> newCollection = new ArrayList<T>();
        try {
            for (T document : collection.values()) {
                Object obj = Util.deepCopy(document);
                if (encrypted && cmd.hasSecret() && null != obj) {
                    CryptoUtil.decryptFields(obj, cmd, dbConfig.getCipher());
                }
                newCollection.add((T) obj);
            }
        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
            Logger.error("Error when decrypting value for a @Secret annotated field for entity: " + collectionName,
                    e);
            throw new JsonDBException(
                    "Error when decrypting value for a @Secret annotated field for entity: " + collectionName, e);
        }
        return newCollection;
    }

    /* (non-Javadoc)
    * @see io.jsondb.JsonDBOperations#collectionExists(java.lang.Class)
     */
    @Override
    public <T> boolean collectionExists(Class<T> entityClass) {
        return collectionExists(Util.determineCollectionName(entityClass));
    }

    /* (non-Javadoc)
    * @see io.jsondb.JsonDBOperations#collectionExists(java.lang.String)
     */
    @Override
    public boolean collectionExists(String collectionName) {
        CollectionMetaData collectionMeta = cmdMap.get(collectionName);
        if (null == collectionMeta) {
            return false;
        }
        collectionMeta.getCollectionLock().readLock().lock();
        try {
            return collectionsRef.get().containsKey(collectionName);
        } finally {
            collectionMeta.getCollectionLock().readLock().unlock();
        }
    }

    /* (non-Javadoc)
    * @see io.jsondb.JsonDBOperations#isCollectionReadonly(java.lang.Class)
     */
    @Override
    public <T> boolean isCollectionReadonly(Class<T> entityClass) {
        return isCollectionReadonly(Util.determineCollectionName(entityClass));
    }

    /* (non-Javadoc)
    * @see io.jsondb.JsonDBOperations#isCollectionReadonly(java.lang.String)
     */
    @Override
    public <T> boolean isCollectionReadonly(String collectionName) {
        CollectionMetaData cmd = cmdMap.get(collectionName);
        return cmd.isReadOnly();
    }

    /* (non-Javadoc)
    * @see io.jsondb.JsonDBOperations#find(java.lang.String, java.lang.Class)
     */
    @Override
    public <T> List<T> find(String jxQuery, Class<T> entityClass) {
        return find(jxQuery, Util.determineCollectionName(entityClass));
    }

    /* (non-Javadoc)
    * @see io.jsondb.JsonDBOperations#find(java.lang.String, java.lang.String)
     */
    @SuppressWarnings("unchecked")
    @Override
    public <T> List<T> find(String jxQuery, String collectionName) {
        CollectionMetaData cmd = cmdMap.get(collectionName);
        Map<Object, T> collection = (Map<Object, T>) collectionsRef.get().get(collectionName);
        if ((null == cmd) || (null == collection)) {
            throw new InvalidJsonDbApiUsageException(
                    "Collection by name '" + collectionName + "' not found. Create collection first.");
        }
        cmd.getCollectionLock().readLock().lock();
        try {
            JXPathContext context = contextsRef.get().get(collectionName);
            Iterator<T> resultItr = context.iterate(jxQuery);
            List<T> newCollection = new ArrayList<T>();
            while (resultItr.hasNext()) {
                T document = resultItr.next();
                Object obj = Util.deepCopy(document);
                if (encrypted && cmd.hasSecret() && null != obj) {
                    CryptoUtil.decryptFields(obj, cmd, dbConfig.getCipher());
                }
                newCollection.add((T) obj);
            }
            return newCollection;
        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
            Logger.error("Error when decrypting value for a @Secret annotated field for entity: " + collectionName,
                    e);
            throw new JsonDBException(
                    "Error when decrypting value for a @Secret annotated field for entity: " + collectionName, e);
        } finally {
            cmd.getCollectionLock().readLock().unlock();
        }
    }

    /* (non-Javadoc)
    * @see io.jsondb.JsonDBOperations#findAll(java.lang.Class)
     */
    @Override
    public <T> List<T> findAll(Class<T> entityClass) {
        return findAll(Util.determineCollectionName(entityClass));
    }

    /* (non-Javadoc)
    * @see io.jsondb.JsonDBOperations#findAll(java.lang.String)
     */
    @SuppressWarnings("unchecked")
    @Override
    public <T> List<T> findAll(String collectionName) {
        CollectionMetaData cmd = cmdMap.get(collectionName);
        Map<Object, T> collection = (Map<Object, T>) collectionsRef.get().get(collectionName);
        if ((null == cmd) || (null == collection)) {
            throw new InvalidJsonDbApiUsageException(
                    "Collection by name '" + collectionName + "' not found. Create collection first.");
        }
        cmd.getCollectionLock().readLock().lock();
        try {
            List<T> newCollection = new ArrayList<T>();
            for (T document : collection.values()) {
                T obj = (T) Util.deepCopy(document);
                if (encrypted && cmd.hasSecret() && null != obj) {
                    CryptoUtil.decryptFields(obj, cmd, dbConfig.getCipher());
                    newCollection.add(obj);
                } else {
                    newCollection.add((T) obj);
                }
            }
            return newCollection;
        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
            Logger.error("Error when decrypting value for a @Secret annotated field for entity: " + collectionName,
                    e);
            throw new JsonDBException(
                    "Error when decrypting value for a @Secret annotated field for entity: " + collectionName, e);
        } finally {
            cmd.getCollectionLock().readLock().unlock();
        }
    }

    /* (non-Javadoc)
    * @see io.jsondb.JsonDBOperations#findById(java.lang.Object, java.lang.Class)
     */
    @Override
    public <T> T findById(Object id, Class<T> entityClass) {
        return findById(id, Util.determineCollectionName(entityClass));
    }

    /* (non-Javadoc)
    * @see io.jsondb.JsonDBOperations#findById(java.lang.Object, java.lang.String)
     */
    @SuppressWarnings("unchecked")
    @Override
    public <T> T findById(Object id, String collectionName) {
        CollectionMetaData cmd = cmdMap.get(collectionName);
        Map<Object, T> collection = (Map<Object, T>) collectionsRef.get().get(collectionName);

        if ((null == cmd) || null == collection) {
            throw new InvalidJsonDbApiUsageException(
                    "Collection by name '" + collectionName + "' not found. Create collection first.");
        }

        cmd.getCollectionLock().readLock().lock();

        try {
            Object obj = Util.deepCopy(collection.get(id));
            if (encrypted && cmd.hasSecret() && null != obj) {
                CryptoUtil.decryptFields(obj, cmd, dbConfig.getCipher());
            }
            return (T) obj;
        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
            Logger.error("Error when decrypting value for a @Secret annotated field for entity: " + collectionName,
                    e);
            throw new JsonDBException(
                    "Error when decrypting value for a @Secret annotated field for entity: " + collectionName, e);
        } finally {
            cmd.getCollectionLock().readLock().unlock();
        }
    }

    /* (non-Javadoc)
    * @see io.jsondb.JsonDBOperations#findOne(java.lang.String, java.lang.Class)
     */
    @Override
    public <T> T findOne(String jxQuery, Class<T> entityClass) {
        return findOne(jxQuery, Util.determineCollectionName(entityClass));
    }

    /* (non-Javadoc)
    * @see io.jsondb.JsonDBOperations#findOne(java.lang.String, java.lang.String)
     */
    @SuppressWarnings("unchecked")
    @Override
    public <T> T findOne(String jxQuery, String collectionName) {
        CollectionMetaData collectionMeta = cmdMap.get(collectionName);
        if ((null == collectionMeta) || (!collectionsRef.get().containsKey(collectionName))) {
            throw new InvalidJsonDbApiUsageException(
                    "Collection by name '" + collectionName + "' not found. Create collection first");
        }
        collectionMeta.getCollectionLock().readLock().lock();
        try {
            JXPathContext context = contextsRef.get().get(collectionName);
            Iterator<T> resultItr = context.iterate(jxQuery);
            while (resultItr.hasNext()) {
                T document = resultItr.next();
                Object obj = Util.deepCopy(document);
                if (encrypted && collectionMeta.hasSecret() && null != obj) {
                    CryptoUtil.decryptFields(obj, collectionMeta, dbConfig.getCipher());
                }
                return (T) obj; // Return the first element we find.
            }
            return null;
        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
            Logger.error("Error when decrypting value for a @Secret annotated field for entity: " + collectionName,
                    e);
            throw new JsonDBException(
                    "Error when decrypting value for a @Secret annotated field for entity: " + collectionName, e);
        } finally {
            collectionMeta.getCollectionLock().readLock().unlock();
        }
    }

    /* (non-Javadoc)
    * @see io.jsondb.JsonDBOperations#insert(java.lang.Object)
     */
    @Override
    public <T> void insert(Object objectToSave) {
        if (null == objectToSave) {
            throw new InvalidJsonDbApiUsageException("Null Object cannot be inserted into DB");
        }
        Util.ensureNotRestricted(objectToSave);
        System.out.println("entity collection name: " + Util.determineEntityCollectionName(objectToSave));
        insert(objectToSave, Util.determineEntityCollectionName(objectToSave));

    }

    /* (non-Javadoc)
    * @see io.jsondb.JsonDBOperations#insert(java.lang.Object, java.lang.String)
     */
    @SuppressWarnings("unchecked")
    @Override
    public <T> void insert(Object objectToSave, String collectionName) {
        if (null == objectToSave) {
            throw new InvalidJsonDbApiUsageException("Null Object cannot be inserted into DB");
        }
        Util.ensureNotRestricted(objectToSave);
        Object objToSave = Util.deepCopy(objectToSave);
        CollectionMetaData cmd = cmdMap.get(collectionName);
        cmd.getCollectionLock().writeLock().lock();
        try {
            Map<Object, T> collection = (Map<Object, T>) collectionsRef.get().get(collectionName);
            if (null == collection) {
                throw new InvalidJsonDbApiUsageException(
                        "Collection by name '" + collectionName + "' not found. Create collection first");
            }
            Object id = Util.getIdForEntity(objectToSave, cmd.getIdAnnotatedFieldGetterMethod());
            if (encrypted && cmd.hasSecret()) {
                CryptoUtil.encryptFields(objToSave, cmd, dbConfig.getCipher());
            }
            if (cmd.hasDBRef()) {

            }
            if (null == id) {
                id = Util.setIdForEntity(objToSave, cmd.getIdAnnotatedFieldSetterMethod());
            } else if (collection.containsKey(id)) {
                throw new InvalidJsonDbApiUsageException(
                        "Object already present in Collection. Use Update or Upsert operation instead of Insert");
            }

            JsonWriter jw;
            try {
                jw = new JsonWriter(dbConfig, cmd, collectionName, fileObjectsRef.get().get(collectionName));
            } catch (IOException ioe) {
                Logger.error("Failed to obtain writer for " + collectionName, ioe);
                throw new JsonDBException("Failed to save " + collectionName, ioe);
            }

            boolean appendResult = jw.appendToJsonFile(collection.values(), objToSave);

            if (appendResult) {
                collection.put(Util.deepCopy(id), (T) objToSave);
            }
        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
            Logger.error("Error when encrypting value for a @Secret annotated field for entity: " + collectionName,
                    e);
            throw new JsonDBException(
                    "Error when encrypting value for a @Secret annotated field for entity: " + collectionName, e);
        } finally {
            cmd.getCollectionLock().writeLock().unlock();
        }
    }

    /* (non-Javadoc)
    * @see io.jsondb.JsonDBOperations#insert(java.util.Collection, java.lang.Class)
     */
    @Override
    public <T> void insert(Collection<? extends T> batchToSave, Class<T> entityClass) {
        insert(batchToSave, Util.determineCollectionName(entityClass));
    }

    /* (non-Javadoc)
    * @see io.jsondb.JsonDBOperations#insert(java.util.Collection, java.lang.String)
     */
    @SuppressWarnings("unchecked")
    @Override
    public <T> void insert(Collection<? extends T> batchToSave, String collectionName) {
        if (null == batchToSave) {
            throw new InvalidJsonDbApiUsageException("Null Object batch cannot be inserted into DB");
        }
        CollectionMetaData collectionMeta = cmdMap.get(collectionName);
        collectionMeta.getCollectionLock().writeLock().lock();
        try {
            Map<Object, T> collection = (Map<Object, T>) collectionsRef.get().get(collectionName);
            if (null == collection) {
                throw new InvalidJsonDbApiUsageException(
                        "Collection by name '" + collectionName + "' not found. Create collection first");
            }
            CollectionMetaData cmd = cmdMap.get(collectionName);
            Set<Object> uniqueIds = new HashSet<Object>();
            Map<Object, T> newCollection = new LinkedHashMap<Object, T>();
            for (T o : batchToSave) {
                Object obj = Util.deepCopy(o);
                Object id = Util.getIdForEntity(obj, cmd.getIdAnnotatedFieldGetterMethod());
                if (encrypted && cmd.hasSecret()) {
                    CryptoUtil.encryptFields(obj, cmd, dbConfig.getCipher());
                }
                if (null == id) {
                    id = Util.setIdForEntity(obj, cmd.getIdAnnotatedFieldSetterMethod());
                } else if (collection.containsKey(id)) {
                    throw new InvalidJsonDbApiUsageException(
                            "Object already present in Collection. Use Update or Upsert operation instead of Insert");
                }
                if (!uniqueIds.add(id)) {
                    throw new InvalidJsonDbApiUsageException(
                            "Duplicate object with id: " + id + " within the passed in parameter");
                }
                newCollection.put(Util.deepCopy(id), (T) obj);
            }

            JsonWriter jw;
            try {
                jw = new JsonWriter(dbConfig, cmd, collectionName, fileObjectsRef.get().get(collectionName));
            } catch (IOException ioe) {
                Logger.error("Failed to obtain writer for " + collectionName, ioe);
                throw new JsonDBException("Failed to save " + collectionName, ioe);
            }
            boolean appendResult = jw.appendToJsonFile(collection.values(), newCollection.values());

            if (appendResult) {
                collection.putAll(newCollection);
            }
        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
            Logger.error("Error when encrypting value for a @Secret annotated field for entity: " + collectionName,
                    e);
            throw new JsonDBException(
                    "Error when encrypting value for a @Secret annotated field for entity: " + collectionName, e);
        } finally {
            collectionMeta.getCollectionLock().writeLock().unlock();
        }
    }

    /* (non-Javadoc)
    * @see io.jsondb.JsonDBOperations#save(java.lang.Object, java.lang.Class)
     */
    @Override
    public <T> void save(Object objectToSave, Class<T> entityClass) {
        save(objectToSave, Util.determineCollectionName(entityClass));
    }

    /* (non-Javadoc)
    * @see io.jsondb.JsonDBOperations#save(java.lang.Object, java.lang.String)
     */
    @Override
    public <T> void save(Object objectToSave, String collectionName) {
        if (null == objectToSave) {
            throw new InvalidJsonDbApiUsageException("Null Object cannot be updated into DB");
        }
        Util.ensureNotRestricted(objectToSave);
        Object objToSave = Util.deepCopy(objectToSave);
        CollectionMetaData collectionMeta = cmdMap.get(collectionName);
        collectionMeta.getCollectionLock().writeLock().lock();
        try {
            @SuppressWarnings("unchecked")
            Map<Object, T> collection = (Map<Object, T>) collectionsRef.get().get(collectionName);
            if (null == collection) {
                throw new InvalidJsonDbApiUsageException(
                        "Collection by name '" + collectionName + "' not found. Create collection first.");
            }

            CollectionMetaData cmd = cmdMap.get(collectionName);
            Object id = Util.getIdForEntity(objToSave, cmd.getIdAnnotatedFieldGetterMethod());

            T existingObject = collection.get(id);
            if (null == existingObject) {
                throw new InvalidJsonDbApiUsageException(String.format(
                        "Document with Id: '%s' not found in Collection by name '%s' not found. Insert or Upsert the object first.",
                        id, collectionName));
            }
            if (encrypted && cmd.hasSecret()) {
                CryptoUtil.encryptFields(objToSave, cmd, dbConfig.getCipher());
            }
            JsonWriter jw = null;
            try {
                jw = new JsonWriter(dbConfig, cmd, collectionName, fileObjectsRef.get().get(collectionName));
            } catch (IOException ioe) {
                Logger.error("Failed to obtain writer for " + collectionName, ioe);
                throw new JsonDBException("Failed to save " + collectionName, ioe);
            }
            @SuppressWarnings("unchecked")
            boolean updateResult = jw.updateInJsonFile(collection, id, (T) objToSave);
            if (updateResult) {
                @SuppressWarnings("unchecked")
                T newObject = (T) objToSave;
                collection.put(id, newObject);
            }
        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
            Logger.error("Error when encrypting value for a @Secret annotated field for entity: " + collectionName,
                    e);
            throw new JsonDBException(
                    "Error when encrypting value for a @Secret annotated field for entity: " + collectionName, e);
        } finally {
            collectionMeta.getCollectionLock().writeLock().unlock();
        }
    }

    /* (non-Javadoc)
    * @see org.jsondb.JsonDBOperations#remove(java.lang.Object, java.lang.Class)
     */
    @Override
    public <T> T remove(Object objectToRemove, Class<T> entityClass) {
        return remove(objectToRemove, Util.determineCollectionName(entityClass));
    }

    /* (non-Javadoc)
    * @see org.jsondb.JsonDBOperations#remove(java.lang.Object, java.lang.String)
     */
    @Override
    public <T> T remove(Object objectToRemove, String collectionName) {
        if (null == objectToRemove) {
            throw new InvalidJsonDbApiUsageException("Null Object cannot be removed from DB");
        }
        Util.ensureNotRestricted(objectToRemove);

        CollectionMetaData collectionMeta = cmdMap.get(collectionName);
        collectionMeta.getCollectionLock().writeLock().lock();
        try {
            @SuppressWarnings("unchecked")
            Map<Object, T> collection = (Map<Object, T>) collectionsRef.get().get(collectionName);
            if (null == collection) {
                throw new InvalidJsonDbApiUsageException(
                        "Collection by name '" + collectionName + "' not found. Create collection first.");
            }

            CollectionMetaData cmd = cmdMap.get(collectionName);
            Object id = Util.getIdForEntity(objectToRemove, cmd.getIdAnnotatedFieldGetterMethod());
            if (!collection.containsKey(id)) {
                throw new InvalidJsonDbApiUsageException(
                        String.format("Objects with Id %s not found in collection %s", id, collectionName));
            }

            JsonWriter jw;
            try {
                jw = new JsonWriter(dbConfig, cmd, collectionName, fileObjectsRef.get().get(collectionName));
            } catch (IOException ioe) {
                Logger.error("Failed to obtain writer for " + collectionName, ioe);
                throw new JsonDBException("Failed to save " + collectionName, ioe);
            }
            boolean substractResult = jw.removeFromJsonFile(collection, id);
            if (substractResult) {
                T objectRemoved = collection.remove(id);
                // Don't need to clone it, this object no more exists in the collection
                return objectRemoved;
            } else {
                return null;
            }
        } finally {
            collectionMeta.getCollectionLock().writeLock().unlock();
        }
    }

    /* (non-Javadoc)
    * @see org.jsondb.JsonDBOperations#remove(java.util.Collection, java.lang.Class)
     */
    @Override
    public <T> List<T> remove(Collection<? extends T> batchToRemove, Class<T> entityClass) {
        return remove(batchToRemove, Util.determineCollectionName(entityClass));
    }

    /* (non-Javadoc)
    * @see org.jsondb.JsonDBOperations#remove(java.util.Collection, java.lang.String)
     */
    @Override
    public <T> List<T> remove(Collection<? extends T> batchToRemove, String collectionName) {
        if (null == batchToRemove) {
            throw new InvalidJsonDbApiUsageException("Null Object batch cannot be removed from DB");
        }
        CollectionMetaData cmd = cmdMap.get(collectionName);
        cmd.getCollectionLock().writeLock().lock();
        try {
            @SuppressWarnings("unchecked")
            Map<Object, T> collection = (Map<Object, T>) collectionsRef.get().get(collectionName);
            if (null == collection) {
                throw new InvalidJsonDbApiUsageException(
                        "Collection by name '" + collectionName + "' not found. Create collection first.");
            }

            Set<Object> removeIds = new HashSet<Object>();

            for (T o : batchToRemove) {
                Object id = Util.getIdForEntity(o, cmd.getIdAnnotatedFieldGetterMethod());
                if (collection.containsKey(id)) {
                    removeIds.add(id);
                }
            }

            if (removeIds.size() < 1) {
                return null;
            }

            JsonWriter jw;
            try {
                jw = new JsonWriter(dbConfig, cmd, collectionName, fileObjectsRef.get().get(collectionName));
            } catch (IOException ioe) {
                Logger.error("Failed to obtain writer for " + collectionName, ioe);
                throw new JsonDBException("Failed to save " + collectionName, ioe);
            }
            boolean substractResult = jw.removeFromJsonFile(collection, removeIds);

            List<T> removedObjects = null;
            if (substractResult) {
                removedObjects = new ArrayList<T>();
                for (Object id : removeIds) {
                    // Don't need to clone it, this object no more exists in the collection
                    removedObjects.add(collection.remove(id));
                }
            }
            return removedObjects;
        } finally {
            cmd.getCollectionLock().writeLock().unlock();
        }
    }

    /* (non-Javadoc)
    * @see org.jsondb.JsonDBOperations#upsert(java.lang.Object)
     */
    @Override
    public <T> void upsert(Object objectToSave) {
        if (null == objectToSave) {
            throw new InvalidJsonDbApiUsageException("Null Object cannot be upserted into DB");
        }
        Util.ensureNotRestricted(objectToSave);
        upsert(objectToSave, Util.determineEntityCollectionName(objectToSave));
    }

    /* (non-Javadoc)
    * @see org.jsondb.JsonDBOperations#upsert(java.lang.Object, java.lang.String)
     */
    @SuppressWarnings("unchecked")
    @Override
    public <T> void upsert(Object objectToSave, String collectionName) {
        if (null == objectToSave) {
            throw new InvalidJsonDbApiUsageException("Null Object cannot be upserted into DB");
        }
        Util.ensureNotRestricted(objectToSave);
        Object objToSave = Util.deepCopy(objectToSave);
        CollectionMetaData collectionMeta = cmdMap.get(collectionName);
        collectionMeta.getCollectionLock().writeLock().lock();
        try {
            Map<Object, T> collection = (Map<Object, T>) collectionsRef.get().get(collectionName);
            if (null == collection) {
                throw new InvalidJsonDbApiUsageException(
                        "Collection by name '" + collectionName + "' not found. Create collection first");
            }
            CollectionMetaData cmd = cmdMap.get(collectionName);
            Object id = Util.getIdForEntity(objectToSave, cmd.getIdAnnotatedFieldGetterMethod());
            if (encrypted && cmd.hasSecret()) {
                CryptoUtil.encryptFields(objToSave, cmd, dbConfig.getCipher());
            }

            boolean insert = true;
            if (null == id) {
                id = Util.setIdForEntity(objToSave, cmd.getIdAnnotatedFieldSetterMethod());
            } else if (collection.containsKey(id)) {
                insert = false;
            }

            JsonWriter jw;
            try {
                jw = new JsonWriter(dbConfig, cmd, collectionName, fileObjectsRef.get().get(collectionName));
            } catch (IOException ioe) {
                Logger.error("Failed to obtain writer for " + collectionName, ioe);
                throw new JsonDBException("Failed to save " + collectionName, ioe);
            }

            if (insert) {
                boolean insertResult = jw.appendToJsonFile(collection.values(), objToSave);
                if (insertResult) {
                    collection.put(Util.deepCopy(id), (T) objToSave);
                }
            } else {
                boolean updateResult = jw.updateInJsonFile(collection, id, (T) objToSave);
                if (updateResult) {
                    T newObject = (T) objToSave;
                    collection.put(id, newObject);
                }
            }
        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
            Logger.error("Error when encrypting value for a @Secret annotated field for entity: " + collectionName,
                    e);
            throw new JsonDBException(
                    "Error when encrypting value for a @Secret annotated field for entity: " + collectionName, e);
        } finally {
            collectionMeta.getCollectionLock().writeLock().unlock();
        }
    }

    /* (non-Javadoc)
    * @see org.jsondb.JsonDBOperations#upsert(java.util.Collection, java.lang.Class)
     */
    @Override
    public <T> void upsert(Collection<? extends T> batchToSave, Class<T> entityClass) {
        upsert(batchToSave, Util.determineCollectionName(entityClass));
    }

    /* (non-Javadoc)
    * @see org.jsondb.JsonDBOperations#upsert(java.util.Collection, java.lang.String)
     */
    @SuppressWarnings("unchecked")
    @Override
    public <T> void upsert(Collection<? extends T> batchToSave, String collectionName) {
        if (null == batchToSave) {
            throw new InvalidJsonDbApiUsageException("Null Object batch cannot be upserted into DB");
        }
        CollectionMetaData collectionMeta = cmdMap.get(collectionName);
        collectionMeta.getCollectionLock().writeLock().lock();
        try {
            Map<Object, T> collection = (Map<Object, T>) collectionsRef.get().get(collectionName);
            if (null == collection) {
                throw new InvalidJsonDbApiUsageException(
                        "Collection by name '" + collectionName + "' not found. Create collection first");
            }
            CollectionMetaData cmd = cmdMap.get(collectionName);
            Set<Object> uniqueIds = new HashSet<Object>();

            Map<Object, T> collectionToInsert = new LinkedHashMap<Object, T>();
            Map<Object, T> collectionToUpdate = new LinkedHashMap<Object, T>();

            for (T o : batchToSave) {
                Object obj = Util.deepCopy(o);
                Object id = Util.getIdForEntity(obj, cmd.getIdAnnotatedFieldGetterMethod());
                if (encrypted && cmd.hasSecret()) {
                    CryptoUtil.encryptFields(obj, cmd, dbConfig.getCipher());
                }
                boolean insert = true;
                if (null == id) {
                    id = Util.setIdForEntity(obj, cmd.getIdAnnotatedFieldSetterMethod());
                } else if (collection.containsKey(id)) {
                    insert = false;
                }
                if (!uniqueIds.add(id)) {
                    throw new InvalidJsonDbApiUsageException(
                            "Duplicate object with id: " + id + " within the passed in parameter");
                }
                if (insert) {
                    collectionToInsert.put(Util.deepCopy(id), (T) obj);
                } else {
                    collectionToUpdate.put(Util.deepCopy(id), (T) obj);
                }
            }

            JsonWriter jw;
            try {
                jw = new JsonWriter(dbConfig, cmd, collectionName, fileObjectsRef.get().get(collectionName));
            } catch (IOException ioe) {
                Logger.error("Failed to obtain writer for " + collectionName, ioe);
                throw new JsonDBException("Failed to save " + collectionName, ioe);
            }

            if (collectionToInsert.size() > 0) {
                boolean insertResult = jw.appendToJsonFile(collection.values(), collectionToInsert.values());
                if (insertResult) {
                    collection.putAll(collectionToInsert);
                }
            }

            if (collectionToUpdate.size() > 0) {
                boolean updateResult = jw.updateInJsonFile(collection, collectionToUpdate);
                if (updateResult) {
                    collection.putAll(collectionToUpdate);
                }
            }
        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
            Logger.error("Error when encrypting value for a @Secret annotated field for entity: " + collectionName,
                    e);
            throw new JsonDBException(
                    "Error when encrypting value for a @Secret annotated field for entity: " + collectionName, e);
        } finally {
            collectionMeta.getCollectionLock().writeLock().unlock();
        }
    }

    /* (non-Javadoc)
    * @see org.jsondb.JsonDBOperations#findAndRemove(java.lang.String, java.lang.Class)
     */
    @Override
    public <T> T findAndRemove(String jxQuery, Class<T> entityClass) {
        return findAndRemove(jxQuery, Util.determineCollectionName(entityClass));
    }

    /* (non-Javadoc)
    * @see org.jsondb.JsonDBOperations#findAndRemove(java.lang.String, java.lang.String)
     */
    @Override
    public <T> T findAndRemove(String jxQuery, String collectionName) {
        if (null == jxQuery) {
            throw new InvalidJsonDbApiUsageException("Query string cannot be null.");
        }
        CollectionMetaData cmd = cmdMap.get(collectionName);
        @SuppressWarnings("unchecked")
        Map<Object, T> collection = (Map<Object, T>) collectionsRef.get().get(collectionName);
        if ((null == cmd) || (null == collection)) {
            throw new InvalidJsonDbApiUsageException(
                    "Collection by name '" + collectionName + "' not found. Create collection first.");
        }
        cmd.getCollectionLock().writeLock().lock();
        try {
            JXPathContext context = contextsRef.get().get(collectionName);
            @SuppressWarnings("unchecked")
            Iterator<T> resultItr = context.iterate(jxQuery);
            T objectToRemove = null;
            while (resultItr.hasNext()) {
                objectToRemove = resultItr.next();
                break; // Use only the first element we find.
            }
            if (null != objectToRemove) {
                Object idToRemove = Util.getIdForEntity(objectToRemove, cmd.getIdAnnotatedFieldGetterMethod());
                if (!collection.containsKey(idToRemove)) { //This will never happen since the object was located based of jxQuery
                    throw new InvalidJsonDbApiUsageException(String
                            .format("Objects with Id %s not found in collection %s", idToRemove, collectionName));
                }

                JsonWriter jw;
                try {
                    jw = new JsonWriter(dbConfig, cmd, collectionName, fileObjectsRef.get().get(collectionName));
                } catch (IOException ioe) {
                    Logger.error("Failed to obtain writer for " + collectionName, ioe);
                    throw new JsonDBException("Failed to save " + collectionName, ioe);
                }
                boolean substractResult = jw.removeFromJsonFile(collection, idToRemove);
                if (substractResult) {
                    T objectRemoved = collection.remove(idToRemove);
                    // Don't need to clone it, this object no more exists in the collection
                    return objectRemoved;
                } else {
                    Logger.error("Unexpected, Failed to substract the object");
                }
            }
            return null; //Either the jxQuery found nothing or actual FileIO failed to substract it.
        } finally {
            cmd.getCollectionLock().writeLock().unlock();
        }
    }

    /* (non-Javadoc)
    * @see org.jsondb.JsonDBOperations#findAllAndRemove(java.lang.String, java.lang.Class)
     */
    @Override
    public <T> List<T> findAllAndRemove(String jxQuery, Class<T> entityClass) {
        return findAllAndRemove(jxQuery, Util.determineCollectionName(entityClass));
    }

    /* (non-Javadoc)
    * @see org.jsondb.JsonDBOperations#findAllAndRemove(java.lang.String, java.lang.String)
     */
    @Override
    public <T> List<T> findAllAndRemove(String jxQuery, String collectionName) {
        CollectionMetaData cmd = cmdMap.get(collectionName);
        @SuppressWarnings("unchecked")
        Map<Object, T> collection = (Map<Object, T>) collectionsRef.get().get(collectionName);
        if ((null == cmd) || (null == collection)) {
            throw new InvalidJsonDbApiUsageException(
                    "Collection by name '" + collectionName + "' not found. Create collection first.");
        }
        cmd.getCollectionLock().writeLock().lock();
        try {
            JXPathContext context = contextsRef.get().get(collectionName);
            @SuppressWarnings("unchecked")
            Iterator<T> resultItr = context.iterate(jxQuery);
            Set<Object> removeIds = new HashSet<Object>();
            while (resultItr.hasNext()) {
                T objectToRemove = resultItr.next();
                Object idToRemove = Util.getIdForEntity(objectToRemove, cmd.getIdAnnotatedFieldGetterMethod());
                removeIds.add(idToRemove);
            }

            if (removeIds.size() < 1) {
                return null;
            }

            JsonWriter jw;
            try {
                jw = new JsonWriter(dbConfig, cmd, collectionName, fileObjectsRef.get().get(collectionName));
            } catch (IOException ioe) {
                Logger.error("Failed to obtain writer for " + collectionName, ioe);
                throw new JsonDBException("Failed to save " + collectionName, ioe);
            }
            boolean substractResult = jw.removeFromJsonFile(collection, removeIds);

            List<T> removedObjects = null;
            if (substractResult) {
                removedObjects = new ArrayList<T>();
                for (Object id : removeIds) {
                    // Don't need to clone it, this object no more exists in the collection
                    removedObjects.add(collection.remove(id));
                }
            }
            return removedObjects;

        } finally {
            cmd.getCollectionLock().writeLock().unlock();
        }
    }

    /* (non-Javadoc)
    * @see org.jsondb.JsonDBOperations#findAndModify(java.lang.String, org.jsondb.query.Update, java.lang.Class)
     */
    @Override
    public <T> T findAndModify(String jxQuery, Update update, Class<T> entityClass) {
        return findAndModify(jxQuery, update, Util.determineCollectionName(entityClass));
    }

    /* (non-Javadoc)
    * @see org.jsondb.JsonDBOperations#findAndModify(java.lang.String, org.jsondb.query.Update, java.lang.String)
     */
    @SuppressWarnings("unchecked")
    @Override
    public <T> T findAndModify(String jxQuery, Update update, String collectionName) {
        CollectionMetaData cmd = cmdMap.get(collectionName);
        Map<Object, T> collection = (Map<Object, T>) collectionsRef.get().get(collectionName);
        if ((null == cmd) || (null == collection)) {
            throw new InvalidJsonDbApiUsageException(
                    "Collection by name '" + collectionName + "' not found. Create collection first.");
        }
        cmd.getCollectionLock().writeLock().lock();
        try {
            JXPathContext context = contextsRef.get().get(collectionName);
            Iterator<T> resultItr = context.iterate(jxQuery);
            T objectToModify = null;
            T clonedModifiedObject = null;

            while (resultItr.hasNext()) {
                objectToModify = resultItr.next();
                break; // Use only the first element we find.
            }
            if (null != objectToModify) {
                //Clone it because we dont want to touch the in-memory object until we have really saved it
                clonedModifiedObject = (T) Util.deepCopy(objectToModify);
                for (Entry<String, Object> entry : update.getUpdateData().entrySet()) {
                    Object newValue = Util.deepCopy(entry.getValue());
                    if (encrypted && cmd.hasSecret() && cmd.isSecretField(entry.getKey())) {
                        newValue = dbConfig.getCipher().encrypt(newValue.toString());
                    }
                    try {
                        BeanUtils.copyProperty(clonedModifiedObject, entry.getKey(), newValue);
                    } catch (IllegalAccessException | InvocationTargetException e) {
                        Logger.error(
                                "Failed to copy updated data into existing collection document using BeanUtils", e);
                        return null;
                    }
                }

                Object idToModify = Util.getIdForEntity(clonedModifiedObject,
                        cmd.getIdAnnotatedFieldGetterMethod());
                JsonWriter jw = null;
                try {
                    jw = new JsonWriter(dbConfig, cmd, collectionName, fileObjectsRef.get().get(collectionName));
                } catch (IOException ioe) {
                    Logger.error("Failed to obtain writer for " + collectionName, ioe);
                    throw new JsonDBException("Failed to save " + collectionName, ioe);
                }
                boolean updateResult = jw.updateInJsonFile(collection, idToModify, clonedModifiedObject);
                if (updateResult) {
                    collection.put(idToModify, clonedModifiedObject);
                    //Clone it once more because we want to disconnect it from the in-memory objects before returning.
                    T returnObj = (T) Util.deepCopy(clonedModifiedObject);
                    if (encrypted && cmd.hasSecret() && null != returnObj) {
                        CryptoUtil.decryptFields(returnObj, cmd, dbConfig.getCipher());
                    }
                    return returnObj;
                }
            }
            return null;
        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
            Logger.error("Error when decrypting value for a @Secret annotated field for entity: " + collectionName,
                    e);
            throw new JsonDBException(
                    "Error when decrypting value for a @Secret annotated field for entity: " + collectionName, e);
        } finally {
            cmd.getCollectionLock().writeLock().unlock();
        }
    }

    /* (non-Javadoc)
    * @see io.jsondb.JsonDBOperations#findAllAndModify(java.lang.String, io.jsondb.query.Update, java.lang.Class)
     */
    @Override
    public <T> List<T> findAllAndModify(String jxQuery, Update update, Class<T> entityClass) {
        return findAllAndModify(jxQuery, update, Util.determineCollectionName(entityClass));
    }

    /* (non-Javadoc)
    * @see io.jsondb.JsonDBOperations#findAllAndModify(java.lang.String, io.jsondb.query.Update, java.lang.String)
     */
    @SuppressWarnings("unchecked")
    @Override
    public <T> List<T> findAllAndModify(String jxQuery, Update update, String collectionName) {
        CollectionMetaData cmd = cmdMap.get(collectionName);
        Map<Object, T> collection = (Map<Object, T>) collectionsRef.get().get(collectionName);
        if ((null == cmd) || (null == collection)) {
            throw new InvalidJsonDbApiUsageException("Die Collection mit dem Namen '" + collectionName
                    + "' wurde nicht gefunden. Bitte zuvor anlegen.");
        }
        cmd.getCollectionLock().writeLock().lock();
        try {
            JXPathContext context = contextsRef.get().get(collectionName);
            Iterator<T> resultItr = context.iterate(jxQuery);
            Map<Object, T> clonedModifiedObjects = new HashMap<Object, T>();

            while (resultItr.hasNext()) {
                T objectToModify = resultItr.next();
                T clonedModifiedObject = (T) Util.deepCopy(objectToModify);

                for (Entry<String, Object> entry : update.getUpdateData().entrySet()) {
                    Object newValue = Util.deepCopy(entry.getValue());
                    if (encrypted && cmd.hasSecret() && cmd.isSecretField(entry.getKey())) {
                        newValue = dbConfig.getCipher().encrypt(newValue.toString());
                    }
                    try {
                        BeanUtils.copyProperty(clonedModifiedObject, entry.getKey(), newValue);
                    } catch (IllegalAccessException | InvocationTargetException e) {
                        Logger.error(
                                "Failed to copy updated data into existing collection document using BeanUtils", e);
                        return null;
                    }
                }
                Object id = Util.getIdForEntity(clonedModifiedObject, cmd.getIdAnnotatedFieldGetterMethod());
                clonedModifiedObjects.put(id, clonedModifiedObject);
            }

            JsonWriter jw = null;
            try {
                jw = new JsonWriter(dbConfig, cmd, collectionName, fileObjectsRef.get().get(collectionName));
            } catch (IOException ioe) {
                Logger.error("Failed to obtain writer for " + collectionName, ioe);
                throw new JsonDBException("Failed to save " + collectionName, ioe);
            }
            boolean updateResult = jw.updateInJsonFile(collection, clonedModifiedObjects);
            if (updateResult) {
                collection.putAll(clonedModifiedObjects);
                //Clone it once more because we want to disconnect it from the in-memory objects before returning.
                List<T> returnObjects = new ArrayList<T>();
                for (T obj : clonedModifiedObjects.values()) {
                    //Clone it once more because we want to disconnect it from the in-memory objects before returning.
                    T returnObj = (T) Util.deepCopy(obj);
                    if (encrypted && cmd.hasSecret() && null != returnObj) {
                        CryptoUtil.decryptFields(returnObj, cmd, dbConfig.getCipher());
                    }
                    returnObjects.add(returnObj);
                }
                return returnObjects;
            }
            return null;
        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
            Logger.error("Error when decrypting value for a @Secret annotated field for entity: " + collectionName,
                    e);
            throw new JsonDBException(
                    "Error when decrypting value for a @Secret annotated field for entity: " + collectionName, e);
        } finally {
            cmd.getCollectionLock().writeLock().unlock();
        }
    }

    /* (non-Javadoc)
    * @see io.jsondb.JsonDBOperations#changeEncryption(io.jsondb.crypto.ICipher)
     */
    @SuppressWarnings("unchecked")
    @Override
    public <T> void changeEncryption(ICipher newCipher) {
        if (!encrypted) {
            throw new InvalidJsonDbApiUsageException("DB is not encrypted, nothing to change for EncryptionKey");
        }

        for (Entry<String, Map<Object, ?>> entry : collectionsRef.get().entrySet()) {
            CollectionMetaData cmd = cmdMap.get(entry.getKey());
            if (cmd.hasSecret()) {
                cmd.getCollectionLock().writeLock().lock();
            }
        }
        String collectionName = null;
        try {
            for (Entry<String, Map<Object, ?>> entry : collectionsRef.get().entrySet()) {
                collectionName = entry.getKey();
                Map<Object, T> collection = (Map<Object, T>) entry.getValue();

                CollectionMetaData cmd = cmdMap.get(collectionName);
                if (cmd.hasSecret()) {
                    Map<Object, T> reCryptedObjects = new LinkedHashMap<Object, T>();
                    for (Entry<Object, T> object : collection.entrySet()) {
                        T clonedObject = (T) Util.deepCopy(object.getValue());
                        CryptoUtil.decryptFields(clonedObject, cmd, dbConfig.getCipher());
                        CryptoUtil.encryptFields(clonedObject, cmd, newCipher);
                        //We will reuse the Id in the previous collection, should hopefully not cause any issues
                        reCryptedObjects.put(object.getKey(), clonedObject);
                    }
                    JsonWriter jw = null;
                    try {
                        jw = new JsonWriter(dbConfig, cmd, collectionName,
                                fileObjectsRef.get().get(collectionName));
                    } catch (IOException ioe) {
                        Logger.error("Failed to obtain writer for " + collectionName, ioe);
                        throw new JsonDBException("Failed to save " + collectionName, ioe);
                    }
                    boolean updateResult = jw.updateInJsonFile(collection, reCryptedObjects);
                    if (!updateResult) {
                        throw new JsonDBException(
                                "Failed to write re-crypted collection data to .json files, database might have become insconsistent");
                    }
                    collection.putAll(reCryptedObjects);
                }
            }
            dbConfig.setCipher(newCipher);
        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
            Logger.error("Error when encrypting value for a @Secret annotated field for entity: " + collectionName,
                    e);
            throw new JsonDBException(
                    "Error when encrypting value for a @Secret annotated field for entity: " + collectionName, e);
        } finally {
            for (Entry<String, Map<Object, ?>> entry : collectionsRef.get().entrySet()) {
                CollectionMetaData cmd = cmdMap.get(entry.getKey());
                if (cmd.hasSecret()) {
                    cmd.getCollectionLock().writeLock().unlock();
                }
            }
        }
    }

    /* (non-Javadoc)
    * @see org.jsondb.JsonDBOperations#backup(java.lang.String)
     */
    @Override
    public void backup(String backupPath) {
        // TODO Auto-generated method stub

    }

    /* (non-Javadoc)
    * @see org.jsondb.JsonDBOperations#restore(java.lang.String, boolean)
     */
    @Override
    public void restore(String restorePath, boolean merge) {
        // TODO Auto-generated method stub

    }

    public void join(String sourceID, String sourceCollection, String destID, String destiCollection) {
        Object sourceObj = this.findById(sourceID, sourceCollection);
        Object destObj = this.findById(destID, destiCollection);

        Class sc = sourceObj.getClass();
        Class dc = destObj.getClass();

        String scgettermethod = null;
        String scsettermethod = null;
        String destCollection = null;
        String destField = null;
        String dcgettermethod = null;
        String dcsettermethod = null;

        Field[] fields = sc.getDeclaredFields();
        for (Field field : fields) {
            DBRef d = field.getDeclaredAnnotation(DBRef.class);
            if (d != null) {
                // destCollection = d.destcollection();
                destField = d.destfieldname();
                // sourcefield = field.getName();
                scgettermethod = "get" + field.getName();
                scsettermethod = "set" + field.getName();
                Method[] methods = sc.getDeclaredMethods();
                for (Method method : methods) {
                    if (method.getName().equalsIgnoreCase(scsettermethod)) {
                        scsettermethod = method.getName();
                    }
                    if (method.getName().equalsIgnoreCase(scgettermethod)) {
                        scgettermethod = method.getName();
                    }
                }
            }
        }
        try {
            Method s = sc.getDeclaredMethod(scgettermethod, (Class[]) null);
            String p = (String) s.invoke(sourceObj);

            Field df = dc.getDeclaredField(destField);
            dcgettermethod = "get" + df.getName();
            dcsettermethod = "set" + df.getName();
            Method[] methods = dc.getDeclaredMethods();
            for (Method method : methods) {
                if (method.getName().equalsIgnoreCase(dcsettermethod)) {
                    dcsettermethod = method.getName();
                }
                if (method.getName().equalsIgnoreCase(dcgettermethod)) {
                    dcgettermethod = method.getName();
                }
            }
            if (p == null) {
                String rs = UUID.randomUUID().toString();
                Method sm = sc.getDeclaredMethod(scsettermethod, rs.getClass());
                sm.invoke(sourceObj, rs);

                Method dm = dc.getDeclaredMethod(dcsettermethod, rs.getClass());
                dm.invoke(destObj, rs);
            } else {
                Method d = dc.getDeclaredMethod(dcsettermethod, p.getClass());
                d.invoke(destObj, p);
            }
            this.upsert(sourceObj);
            this.upsert(destObj);

        } catch (NoSuchMethodException ex) {
            Logger.error("NoSuchMethodExeption");
            java.util.logging.Logger.getLogger(JsonDBTemplate.class.getName()).log(Level.SEVERE, null, ex);
        } catch (SecurityException ex) {
            Logger.error("SecurityException");
            java.util.logging.Logger.getLogger(JsonDBTemplate.class.getName()).log(Level.SEVERE, null, ex);
        } catch (IllegalAccessException ex) {
            Logger.error("IllegalAccessException");
            java.util.logging.Logger.getLogger(JsonDBTemplate.class.getName()).log(Level.SEVERE, null, ex);
        } catch (IllegalArgumentException ex) {
            Logger.error("IllegalArgumentException");
            java.util.logging.Logger.getLogger(JsonDBTemplate.class.getName()).log(Level.SEVERE, null, ex);
        } catch (InvocationTargetException ex) {
            Logger.error("InvokationTargetException");
            java.util.logging.Logger.getLogger(JsonDBTemplate.class.getName()).log(Level.SEVERE, null, ex);
        } catch (NoSuchFieldException ex) {
            Logger.error("Feld not found");
            java.util.logging.Logger.getLogger(JsonDBTemplate.class.getName()).log(Level.SEVERE, null, ex);
        }
        this.reloadCollection(destCollection);
        this.reloadCollection(sourceCollection);
    }
}