basbeu/theSofties

View on GitHub
app/src/main/java/ch/epfl/sweng/favors/database/DatabaseEntity.java

Summary

Maintainability
A
0 mins
Test Coverage
package ch.epfl.sweng.favors.database;

import android.databinding.Observable;
import android.databinding.ObservableField;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import ch.epfl.sweng.favors.database.fields.DatabaseBooleanField;
import ch.epfl.sweng.favors.database.fields.DatabaseField;
import ch.epfl.sweng.favors.database.fields.DatabaseLongField;
import ch.epfl.sweng.favors.database.fields.DatabaseObjectField;
import ch.epfl.sweng.favors.database.fields.DatabaseStringField;


/**
 * Database entity defines the capabilities
 *
 * The data in the entity is stored in the form of a map
 * indicating the type of data as key
 * and the data as value
 *
 * There are currently following datatypes (illustrated with examples)
 * String data - names and other personal information
 * long data - amount of tokens
 * boolean data - permissions, notification settings
 * Object data - location
 */
public abstract class DatabaseEntity implements Observable {

    protected static Database db = Database.getInstance();

    protected Map<DatabaseStringField, ObservableField<String>> stringData;
    protected Map<DatabaseLongField, ObservableField<Long>> longData;
    protected Map<DatabaseBooleanField, ObservableField<Boolean>> booleanData;
    protected Map<DatabaseObjectField, ObservableField<Object>> objectData;

    protected final String collection;
    protected String documentID;

    /**
     * The type of update
     * DATA update is issued when there have been local changes
     * FROM_DB update is issued there have been remote changes that should be pushed to the user's app
     * FROM_REQUEST if a dedicated set has been invoked to change something specific in this entity
     */
    public enum UpdateType{
        DATA,
        FROM_DB,
        FROM_REQUEST
    }

    private final static String TAG = "Favors_DatabaseHandler";

    public void setId(String documentId){
        this.documentID = documentId;
    }

    /**
     * @return documentID of this database entity
     */
    public String getId() {return documentID;}

    /**
     * Init a database object with all fields that are possible for the instanced collection in the database
     *
     * @param stringFieldsValues The possibles names of string objects in the database
     * @param longFieldsValues The possibles names of int objects in the database
     * @param booleanFieldsValues The possibles names of boolean objects in the database
     * @param objectFieldsValues The possibles names of generic objects in the database that must be test
     *                           later, often tables
     * @param collection The collection in the database
     * @param documentID If so, the doccumentId in the database
     */
    public DatabaseEntity(DatabaseStringField stringFieldsValues[], DatabaseLongField longFieldsValues[],
                          DatabaseBooleanField booleanFieldsValues[], DatabaseObjectField objectFieldsValues[],
                          String collection, String documentID){

        assert(collection != null);

        stringData = initMap(stringFieldsValues);
        longData = initMap(longFieldsValues);
        booleanData = initMap(booleanFieldsValues);
        objectData = initMap(objectFieldsValues);


        this.collection = collection;
        this.documentID = documentID;
    }

    /**
     * Init the map with a null value for every possible object of a specific type
     *
     * @param possibleValues An array containing the possible names of this kind of object
     * @param <T> The field enum type
     * @param <V> The type of objects
     * @return An initialised map
     */
    private <T extends DatabaseField, V> Map<T, ObservableField<V>>  initMap(final T[] possibleValues){
        if(possibleValues == null || possibleValues.length == 0){return null;}
        return new HashMap<T, ObservableField<V>>(){
            {
                for(T field : possibleValues){
                    this.put(field, new ObservableField<V>());
                }
            }

        };
    }

    /**
     * Return a uniform map with all data to send
     *
     * @return The maps with objects names and values
     */
    protected Map<String, Object> getEncapsulatedObjectOfMaps(){
        Map<String, Object> toSend = new HashMap<>();

        convertTypedMapToObjectMap(stringData, toSend);
        convertTypedMapToObjectMap(booleanData, toSend);
        convertTypedMapToObjectMap(longData, toSend);
        convertTypedMapToObjectMap(objectData, toSend);

        return toSend;
    }

    /**
     * Update local data with a generic content with Objects
     *
     * @param incomingData The map with object content and object value
     * @return True is successful
     */
    protected void updateLocalData(Map<String, Object> incomingData){
        if(incomingData == null){
            return;
        }
        convertObjectMapToTypedMap(incomingData, stringData, String.class);
        convertObjectMapToTypedMap(incomingData, booleanData, Boolean.class);
        convertObjectMapToTypedMap(incomingData, objectData, Object.class);
        convertObjectMapToTypedMap(incomingData, longData, Long.class);

        for (OnPropertyChangedCallback callback : callbacks){
            callback.onPropertyChanged(this, UpdateType.FROM_DB.ordinal());
        }

        notifyContentChange();
    }

    /**
     * Convert a string / object map to local typed object map
     *
     * @param from  The received map to convert
     * @param to    The map where to add typed object
     * @param <T>   The enum of map content
     * @param <V>   The ObservableField content type
     * @param <U>   The ObservableField
     */
    private <T extends DatabaseField, V , U extends ObservableField<V>> void convertObjectMapToTypedMap(Map<String, Object> from, Map<T, U> to, Class<V> clazz) {
        if(from == null || to == null){return;}
        for (Map.Entry<T, U> entry : to.entrySet()){
            T fieldName = entry.getKey();
            Object object = from.get(fieldName.toString());
            if(object != null){
                try {
                    to.get(fieldName).set(clazz.cast(object));
                } catch(ClassCastException e) {
                    Log.e(TAG, "Error while casting incomming data");
                }
            }
        }
    }

    /**
     * Convert the map containing some parameters in an String / Object map
     *
     * @param from  The original map to convert
     * @param to    The map where to add objects
     * @param <T>   The enum of map content
     * @param <V>   The ObservableField content type
     * @param <U>   The ObservableField
     */
    private <T extends DatabaseField, V, U extends ObservableField<V>> void convertTypedMapToObjectMap(Map<T, U> from, Map<String, Object> to) {
        if(from == null || to == null){return;}
        for (Map.Entry<T, U> entry : from.entrySet()){
            V value = (V) entry.getValue().get();
            if(value != null) to.put(entry.getKey().toString(), value);
        }
    }

    /**
     * resets the data of this database entity to the default value null
     * this clears all of the information that had previously been in this
     * database field
     */
    public void reset(){
        if(stringData != null)
            resetMap(stringData, null);
        if(booleanData != null)
            resetMap(booleanData, null);
        if(objectData != null)
            resetMap(objectData, null);
        if(longData != null)
            resetMap(longData,null);
        notifyContentChange();
    }

    /**
     * Clears a map of all the data that is contained in it and replaces it with the default value passed in.
     *
     * @param map Map that needs to be cleared of information
     * @param defaultValue What value should be placed in the map when being cleared
     * @param <K> Key type of the map
     * @param <V> Value contained in the observableField of the map
     */
    protected  <K,V> void resetMap(@NonNull Map<K, ObservableField<V>> map, V defaultValue) {
        for(K key : map.keySet()){
            map.get(key).set(defaultValue);
        }
    }

    List<OnPropertyChangedCallback> callbacks = Collections.synchronizedList(new ArrayList<>());

    @Override
    public void addOnPropertyChangedCallback(OnPropertyChangedCallback callback) {
        assert(callback != null);
        callbacks.add(callback);
    }

    @Override
    public void removeOnPropertyChangedCallback(OnPropertyChangedCallback callback) {
        callbacks.remove(callback);
    }

    /**
     * Callback that indicates a DATA update
     */
    private void notifyContentChange() {
        for (OnPropertyChangedCallback callback : callbacks){
            callback.onPropertyChanged(this, UpdateType.DATA.ordinal());
        }
    }

    /**
     * the database set method updates local content and
     * issues a callback indicating a FROM_REQUEST update
     *
     * @param id documentID of this database entity
     * @param content to be set (changed)
     */
    public void set(String id, Map<String, Object> content) {
        this.documentID = id;
        this.updateLocalData(content);
        for (OnPropertyChangedCallback callback : callbacks) {
            callback.onPropertyChanged(this, UpdateType.FROM_REQUEST.ordinal());
        }
    }

    /**
     * Get / set methods for the different types of data
     * @param field
     * @return
     */
    public String get(DatabaseStringField field) {
        if(stringData.get(field) != null)
            return stringData.get(field).get();
        else
            return null;
    }

    /**
     * Sets a new String in the database and issues a DATA update
     *
     * @param field in the database
     * @param value string to be put there
     */
    public void set(DatabaseStringField field, String value) {
        stringData.get(field).set(value);
        notifyContentChange();
    }

    public ObservableField<String> getObservableObject(DatabaseStringField field) {
        return stringData.get(field);
    }

    /**
     * Gets an Object like location from the database
     * TODO the if is redundant since objectData.get is nullable and will return without an exception
     * Although we agree it is good to explicitly show this, it would perhaps be better
     * to return an Optional Object
     *
     * @param field in the database representing an object
     * @return the Object from the database
     */
    @Nullable
    public Object get(DatabaseObjectField field) {
        if(objectData.get(field) != null)
            return objectData.get(field).get();
        else
            return null;
    }

    /**
     * Sets a new Object in the database and issues a DATA update
     *
     * @param field in the database
     * @param value Object to be put there
     */
    public void set(DatabaseObjectField field, Object value){
        objectData.get(field).set(value);
        notifyContentChange();
    }

    public ObservableField<Object> getObservableObject(DatabaseObjectField field) {
        return objectData.get(field);
    }

    /**
     * Gets a Long value from the database
     * TODO the if is redundant since longData.get is nullable and will return without an exception
     * Although we agree it is good to explicitly show this, it would perhaps be better
     * to return an Optional value
     *
     * @param field in the database representing a Long
     * @return the Long from the database
     */
    public Long get(DatabaseLongField field) {
        if(longData.get(field) != null)
            return longData.get(field).get();
        else
            return null;
    }

    /**
     * Sets a new long in the database and issues a DATA update
     *
     * @param field in the database
     * @param value long to be put in database
     */
    public void set(DatabaseLongField field, Long value) {
        longData.get(field).set(value);
        notifyContentChange();
    }

    public ObservableField<Long> getObservableObject(DatabaseLongField field) {
        return longData.get(field);
    }

    /**
     * Gets a Boolean value from the database
     * TODO the if is redundant since booleanData.get is nullable and will return without an exception
     * Although we agree it is good to explicitly show this, it would perhaps be better
     * to return an Optional value
     *
     * @param field in the database representing a Boolean
     * @return the Boolean from the database
     */
    public Boolean get(DatabaseBooleanField field) {
        if(booleanData.get(field) != null)
            return booleanData.get(field).get();
        else
            return null;
    }

    /**
     * Sets a new Boolean in the database and issues a DATA update
     *
     * @param field in the database
     * @param value Boolean to be put in database
     */
    public void set(DatabaseBooleanField field, Boolean value) {
        booleanData.get(field).set(value);
        notifyContentChange();
    }

    public ObservableField<Boolean> getObservableObject(DatabaseBooleanField field) {
        return booleanData.get(field);
    }

    /**
     * @return database entity for class extending this entity (user / favor)
     */
    public abstract DatabaseEntity copy();

}