/**
 * Copyright (C) 2013-2017 Vasilis Vryniotis 
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.datumbox.framework.common.storage.abstracts;

import com.datumbox.framework.common.storage.interfaces.BigMap;
import com.datumbox.framework.common.storage.interfaces.StorageConfiguration;
import com.datumbox.framework.common.storage.interfaces.StorageEngine;
import com.datumbox.framework.common.utilities.ReflectionMethods;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * The AbstractStorageEngine is the base class for all concrete storage engines.
 * Those classes can be used in a try-with-resources statement block. Moreover this class
 * setups a shutdown hook which ensures that the storage engine will automatically call close()
 * before the JVM is terminated. Finally it contains methods to store complex objects
 * with fields that point to other serialized objects.
 * 
 * @author Vasilis Vryniotis 
 */
public abstract class AbstractStorageEngine implements StorageEngine {

    protected String storageName;
    protected final SC storageConfiguration;

    /**
     * Logger for all Storage Engines.
     */
    protected final Logger logger = LoggerFactory.getLogger(getClass());

    private final AtomicBoolean isClosed = new AtomicBoolean(false);
    
    private Thread hook;

    /**
     * Protected Constructor which is responsible for adding the Shutdown hook and storing the storage name and configuration.
     *
     * @param storageName
     * @param storageConfiguration
     */
    protected AbstractStorageEngine(String storageName, SC storageConfiguration) {
        this.storageName = storageName;
        this.storageConfiguration = storageConfiguration;

        hook = new Thread(() -> {
            AbstractStorageEngine.this.hook = null;
            if(AbstractStorageEngine.this.isClosed()) {
                return;
            }
            AbstractStorageEngine.this.close();
        });
        Runtime.getRuntime().addShutdownHook(hook);

        logger.trace("Opened storage {}", storageName);
    }

    /** {@inheritDoc} */
    @Override
    public String getStorageName() {
        return storageName;
    }
    
    /** {@inheritDoc} */
    @Override
    public boolean isClosed() {
        return isClosed.get();
    }
    
    /** {@inheritDoc} */
    @Override
    public void close() {
        if(isClosed() == false && hook != null) {
            //remove hook to save memory
            Runtime.getRuntime().removeShutdownHook(hook);
            hook = null;
        }
        isClosed.set(true);
    }
    
    /**
     * Ensures the connection is not closed.
     */
    protected void assertConnectionOpen() {
        if(isClosed()) {
            throw new RuntimeException("The connection is already closed.");
        }
    }

    /**
     * This method is called before serializing the objects. It extracts all the not-serializable BigMap references
     * of the provided object and stores them in a Map. Then it replaces all the references of the provided object
     * with nulls to avoid their serialization. The main idea is that we temporarily remove from the object any reference
     * that will cause problems during the serialization phase.
     *
     * @param serializableObject
     * @param 
     * @return
     */
    protected  Map preSerializer(T serializableObject) {
        Map objReferences = new HashMap<>();
        for(Field field : ReflectionMethods.getAllFields(new LinkedList<>(), serializableObject.getClass())) {

            if (field.isAnnotationPresent(BigMap.class)) {
                field.setAccessible(true);

                try {
                    Object value = field.get(serializableObject);
                    if(!isSerializableBigMap(value)) { //if the field is annotated with BigMap AND the value is not serializable
                        //extract the reference to the object and put it in the map.
                        objReferences.put(field.getName(), value);
                        //then replace the reference with null to avoid serialization.
                        field.set(serializableObject, null);
                    }
                }
                catch (IllegalArgumentException | IllegalAccessException ex) {
                    throw new RuntimeException(ex);
                }
            }
        }

        return objReferences;
    }

    /**
     * Returns a list of non-serializable bigmap classes which are dependent to the StorageEngine.
     *
     * @return
     */
    protected abstract Set nonSerializableBigMaps();

    private boolean isSerializableBigMap(Object value) {
        Class valueClass = value.getClass();
        //Check if the class is not serializable directly
        if(!Serializable.class.isAssignableFrom(valueClass)) {
            return false;
        }

        //Check the black listed bigmap types of the storage engine
        if(nonSerializableBigMaps().contains(valueClass)) {
            return false;
        }

        //Also check that we are not dealing with a SynchronizedMap that has a non-serializable map inside.
        if(valueClass.getCanonicalName().equals("java.util.Collections.SynchronizedMap")) {
            try {
                Field field = valueClass.getDeclaredField("m");
                field.setAccessible(true);
                if(!Serializable.class.isAssignableFrom(field.get(value).getClass())) {
                    return false;
                }
            }
            catch (IllegalAccessException | NoSuchFieldException ex) {
                throw new RuntimeException(ex);
            }

        }

        return true;
    }

    /**
     * This method is called after the object serialization. It moves all the not-serializable BigMap references from
     * the Map back to the provided object. The main idea is that once the serialization is completed, we are allowed to
     * restore back all the references which were removed by the preSerializer.
     *
     * @param serializableObject
     * @param objReferences
     * @param 
     */
    protected  void postSerializer(T serializableObject, Map objReferences) {
        for(Field field : ReflectionMethods.getAllFields(new LinkedList<>(), serializableObject.getClass())) {
            String fieldName = field.getName();

            Object ref = objReferences.remove(fieldName);
            if(ref != null) { //if a reference is found in the map
                field.setAccessible(true);

                try {
                    //restore the reference in the object
                    field.set(serializableObject, ref);
                }
                catch (IllegalAccessException ex) {
                    throw new RuntimeException(ex);
                }
            }
        }
    }

    /**
     * This method is called after the object deserialization. It initializes all BigMaps of the serializable object which
     * have a null value. The main idea is that once an object is deserialized it will contain nulls in all the BigMap fields
     * which were not serialized. For all of those fields we call their initialization methods.
     *
     * @param serializableObject
     * @param 
     */
    protected  void postDeserializer(T serializableObject) {
        Method method = null;
        for(Field field : ReflectionMethods.getAllFields(new LinkedList<>(), serializableObject.getClass())) {
            if (field.isAnnotationPresent(BigMap.class)) { //look only for BigMaps
                field.setAccessible(true);
                try {
                    if(field.get(serializableObject) == null) { //initialize it only if null. this makes it safe it the BigMap was serialized in the file.

                        //lazy initialize the correct method once
                        if(method == null) {
                            method = ReflectionMethods.findMethod(serializableObject, "initializeBigMapField", this, field);
                        }

                        ReflectionMethods.invokeMethod(serializableObject, method, this, field);
                    }
                }
                catch (IllegalAccessException ex) {
                    throw new RuntimeException(ex);
                }
            }
        }

    }

}

 

Ads help maintain this website.