project.cs.netinfservice.database.IODatabase.java Source code

Java tutorial

Introduction

Here is the source code for project.cs.netinfservice.database.IODatabase.java

Source

/**
 * Copyright 2012 Ericsson, Uppsala University
 *
 * 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.
 * 
 * Uppsala University
 *
 * Project CS course, Fall 2012
 *
 * Projekt DV/Project CS, is a course in which the students develop software for
 * distributed systems. The aim of the course is to give insights into how a big
 * project is run (from planning to realization), how to construct a complex
 * distributed system and to give hands-on experience on modern construction
 * principles and programming methods.
 *
 */
package project.cs.netinfservice.database;

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

import netinf.common.datamodel.DatamodelFactory;
import netinf.common.datamodel.Identifier;
import netinf.common.datamodel.InformationObject;

import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;
import org.json.simple.parser.ParseException;

import project.cs.netinfservice.application.MainNetInfActivity;
import project.cs.netinfservice.netinf.common.datamodel.SailDefinedLabelName;
import project.cs.netinfservice.netinf.node.search.SearchResult;
import project.cs.netinfservice.netinf.node.search.SearchResultImpl;
import project.cs.netinfservice.util.IOBuilder;
import project.cs.netinfutilities.UProperties;
import project.cs.netinfutilities.metadata.Metadata;
import project.cs.netinfutilities.metadata.MetadataParser;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;

import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;

/**
 * The database that contains the data corresponding to an information object
 * that is stored in the device.
 * 
 * @author Harold Martinez
 * @author Kim-Anh Tran
 *
 */
public class IODatabase extends SQLiteOpenHelper implements IODatabaseFactory {
    /** The current database version. */
    public static final int DATABASE_VERSION = 1;

    /** Debug Tag. */
    private static final String TAG = "IODatabase";

    /** The name of the database. */
    private static final String DATABASE_NAME = "IODatabase";

    /** The name of the table containing our Information Object information. */
    private static final String TABLE_IO = "IO";

    /** The name of the table containing the url values corresponding to each hash value. */
    private static final String TABLE_URL = "IO_url";

    /** The hash value corresponding to the IO. This is the primary key. */
    private static final String KEY_HASH = "hash";

    /** The hash algorithm used to create the hash value. */
    private static final String KEY_HASH_ALGORITHM = "hash_algorithm";

    /** The Filepath that determines the location of the file on the device. */
    private static final String KEY_FILEPATH = "filepath";

    /** The content type of the file associated with the IO. */
    private static final String KEY_CONTENT_TYPE = "content_type";

    /** The URL associated with the file. */
    private static final String KEY_URL = "url";

    /** The file size of the file associated with the IO. */
    private static final String KEY_FILE_SIZE = "file_size";

    /** Local File system (database) transmission used to transfer a resource. */
    public static final String LOCAL_TRANSMISSION = "project.cs.netinfservice.LOCAL_TRANSMISSION";

    /** Meta-data label for the filepath. */
    private final String mFilepathLabel;

    /** Meta-data label for the file size. */
    private final String mFilesizeLabel;

    /** Meta-data label for the url. */
    private final String mUrlLabel;

    /** The datamodel factory used for constructing the IO. */
    private DatamodelFactory mDatamodelFactory;

    /**
     * Creates a new Database for storing IO information.
     * 
     * @param context
     *       The application context
     * @param datamodelFactory
     *        The factory that is used in order to create information objects.
     */
    @Inject
    public IODatabase(DatamodelFactory datamodelFactory, @Assisted Context context) {
        // We skip the cursor object factory, since we don't need it
        super(context, DATABASE_NAME, null, 1);

        // Fetch properties
        UProperties instance = UProperties.INSTANCE;
        mFilepathLabel = instance.getPropertyWithName("metadata.filepath");
        mFilesizeLabel = instance.getPropertyWithName("metadata.filesize");
        mUrlLabel = instance.getPropertyWithName("metadata.url");

        // Get data model
        mDatamodelFactory = datamodelFactory;
    }

    /**
     * Called when the database is created for the first time. This is where the creation of
     * tables and the initial population of the tables should happen.
     * 
     * @param db
     *     The SQLite database.
     */
    @Override
    public void onCreate(SQLiteDatabase db) {
        String createIoTable = "CREATE TABLE " + TABLE_IO + "(" + KEY_HASH + " TEXT PRIMARY KEY,"
                + KEY_HASH_ALGORITHM + " TEXT NOT NULL, " + KEY_CONTENT_TYPE + " TEXT NOT NULL, " + KEY_FILEPATH
                + " TEXT NOT NULL, " + KEY_FILE_SIZE + " REAL NOT NULL CHECK(" + KEY_FILE_SIZE + " > 0.0))";

        String createUrlTable = "CREATE TABLE " + TABLE_URL + "(" + KEY_HASH + " TEXT NOT NULL, " + KEY_URL
                + " TEXT NOT NULL, " + "CONSTRAINT primarykey PRIMARY KEY " + "( " + KEY_HASH + ", " + KEY_URL
                + "), " + "FOREIGN KEY (" + KEY_HASH + ") " + "REFERENCES " + TABLE_IO + " ( " + KEY_HASH + ") "
                + "ON DELETE CASCADE )";

        db.execSQL(createIoTable);
        db.execSQL(createUrlTable);
    }

    /**
     * Called when the database has been opened. The implementation should check isReadOnly() 
     * before updating the database. This method is called after the database connection has 
     * been configured and after the database schema has been created, upgraded or downgraded 
     * as necessary. If the database connection must be configured in some way before the schema 
     * is created, upgraded, or downgraded, do it in onConfigure(SQLiteDatabase) instead.
     */
    @Override
    public void onOpen(SQLiteDatabase db) {
        super.onOpen(db);

        // Checks if the database is read-only or not
        if (!db.isReadOnly()) {
            // Enable foreign key constraints
            db.execSQL("PRAGMA foreign_keys=ON;");
        }
    }

    /**
     * Called when the database needs to be upgraded. The implementation should use this method
     * to drop tables, add tables, or do anything else it needs to upgrade to the new schema
     * version.
     * 
     * @param db
     *     The SQLite database.
     * @param oldVersion
     *     Old database version number.
     * @param newVersion
     *     New database version number.
     */
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        Log.d(TAG, "Upgrading database to version " + newVersion);

        // Drop tables to re-build them later
        db.execSQL("DROP TABLE IF EXISTS " + TABLE_IO);
        db.execSQL("DROP TABLE IF EXISTS " + TABLE_URL);

        // Re-run the creation of the database
        onCreate(db);
    }

    /**
     * Creates a new IODatabase object.
     * 
     * @param context
     *     The context where the database will be created for.
     * @return
     *     The new IO database object.
     */
    @Override
    public IODatabase create(Context context) {
        return new IODatabase(mDatamodelFactory, context);
    }

    /**
     * Inserts the specified information object into the database.
     * 
     * @param io               The information object to insert.
     * @throws DatabaseException    thrown if insert operation fails
     */
    @SuppressWarnings("unchecked")
    public void addIO(InformationObject io) throws DatabaseException {
        Log.d(TAG, "Adding information object into database.");

        // Extract the field values for inserting them into the database tables
        // Get the Identifier from the Information Object 
        Identifier identifier = io.getIdentifier();

        // Get hash, hashAlgorithm and content-type
        String hash = identifier.getIdentifierLabel(SailDefinedLabelName.HASH_CONTENT.getLabelName())
                .getLabelValue();

        String hashAlgorithm = identifier.getIdentifierLabel(SailDefinedLabelName.HASH_ALG.getLabelName())
                .getLabelValue();

        String contentType = identifier.getIdentifierLabel(SailDefinedLabelName.CONTENT_TYPE.getLabelName())
                .getLabelValue();

        // Extract meta data 
        String metadata = identifier.getIdentifierLabel(SailDefinedLabelName.META_DATA.getLabelName())
                .getLabelValue();

        // Extract the metadata to a map
        Map<String, Object> metadataMap = extractMetaData(metadata);

        String filePath = (String) metadataMap.get(mFilepathLabel);
        String fileSize = (String) metadataMap.get(mFilesizeLabel);

        // Create list of URLs
        Object urlJsonObject = metadataMap.get(mUrlLabel);

        // Populate urlList with one or several URLs
        List<String> urlList;

        /*
         *  If we the json object is an array, we have more than 1 url.
         *  Else, we only received one url. Determines the handling.
         */
        if (urlJsonObject instanceof ArrayList) {
            urlList = (ArrayList<String>) urlJsonObject;
        } else {
            String url = (String) urlJsonObject;
            urlList = new ArrayList<String>();
            urlList.add(url);
        }

        // If the objects hash is not in the database, insert it
        if (!containsIO(hash)) {
            Log.d(TAG, "New information object will be inserted into database.");
            // Insert the IO
            ContentValues ioEntry = createIOEntry(hash, hashAlgorithm, contentType, filePath, fileSize);
            insert(TABLE_IO, ioEntry);
        } else {
            Log.d(TAG, "Information object already exists in database.");
            // Check if the URLs that we want to insert already exist
            List<String> storedUrls = getURLs(hash);
            urlList.removeAll(storedUrls);
        }

        // Insert the URLs
        for (String url : urlList) {

            // Create a ContentValues object, which is recognized by ContentResolver
            ContentValues urlEntry = createUrlEntry(hash, url);
            insert(TABLE_URL, urlEntry);
        }
    }

    /**
     * Inserts a value in the database.
     * 
     * @param table
      *      The table where the values will be inserted
     * @param values
     *      The values that will be inserted
     * @throws
     *       SQLiteException   Thrown, if writing to the database failed.
     */
    private synchronized void insert(String table, ContentValues values) {
        // Insert value into table

        try {
            SQLiteDatabase db = this.getWritableDatabase();

            db.insert(table, null, values);

            db.close();
        } catch (SQLiteException e) {
            Log.e(TAG, "Failed writing to database.");
        }
    }

    /**
     * Returns the list of URL associated with a hash.
     * 
     * @param hash
     *     A hash from an information object
     * @return
      *     The list of URL in the IO_url table corresponding to the given hash
     */
    public List<String> getURLs(String hash) {
        List<String> urlList = new ArrayList<String>();

        Cursor cursor = null;

        // Make database query
        try {
            cursor = query(TABLE_URL, KEY_HASH, hash);
        } catch (DatabaseException e) {
            return urlList;
        }

        // add all results to list
        do {
            urlList.add(cursor.getString(1));
        } while (cursor.moveToNext());

        return urlList;
    }

    /**
     * Returns the information object specified by the hash value, if existent.
     * 
     * @param hash
     *     The hash value identifying the information object
     * @return
     *     The information object
     * @throws DatabaseException
     *     Thrown when the query does not return any value
     */
    public InformationObject getIO(String hash) throws DatabaseException {
        Log.d(TAG, "Searching for information object.");

        // Query. If it fails, it will throw a DatabaseException
        Cursor cursor = query(TABLE_IO, KEY_HASH, hash);

        Log.d(TAG, "Found information object.");

        // Create and send an intent for local transmission
        if (MainNetInfActivity.getActivity() != null) {
            Intent intent = new Intent(LOCAL_TRANSMISSION);
            MainNetInfActivity.getActivity().sendBroadcast(intent);
        }

        // Build a new IO to host the result
        IOBuilder builder = new IOBuilder(mDatamodelFactory);

        // Populate the fields of the IO
        builder.setHash(cursor.getString(0)).setHashAlgorithm(cursor.getString(1))
                .setContentType(cursor.getString(2)).addFilePathLocator(cursor.getString(3))
                .addMetaData(mFilepathLabel, cursor.getString(3)).addMetaData(mFilesizeLabel, cursor.getString(4));

        // Get URLs related to the hash
        cursor = query(TABLE_URL, KEY_HASH, hash);

        // Add URLs to metadata
        do {
            builder.addMetaData(mUrlLabel, cursor.getString(1));
        } while (cursor.moveToNext());

        // Return the Information Object built
        return builder.build();
    }

    /**
     * Returns the information object corresponding to the url,
     * if existent.
     * 
     * @param url
      *      The url that identifies the information object
     * @return
     *       The information object
     * @throws DatabaseException
     *        Is thrown if the url doesn't belong to any stored information object 
     */
    @SuppressWarnings("unchecked") // Because of urlArray.add(cursor..)
    public SearchResult searchIO(String url) throws DatabaseException {
        // Metadata holder. Uses our Metadata class
        Metadata metadata = new Metadata();

        // Find the hash identification of the corresponding object
        Cursor cursor = query(TABLE_URL, KEY_URL, url);
        String hash = cursor.getString(0);

        // Add all url fields
        JSONArray urlArray = new JSONArray();
        cursor = query(TABLE_URL, KEY_HASH, hash);
        do {
            urlArray.add(cursor.getString(1));
        } while (cursor.moveToNext());
        metadata.insert(KEY_URL, urlArray);

        // Build the metadata corresponding to the hash
        cursor = query(TABLE_IO, KEY_HASH, hash);

        metadata.insert(mFilepathLabel, cursor.getString(3));
        metadata.insert(mFilesizeLabel, cursor.getString(4));

        // Get the hash algorithm used to create the object's hash
        String hashAlg = cursor.getString(1);

        // Return a SearchResult object, accessible through the SearchResult interface
        return new SearchResultImpl(hash, hashAlg, metadata);
    }

    /**
     * Deletes the information object corresponding to the specified hash value from the database. 
     * 
     * @param hash
     *      The hash value identifying the information object.
     */
    public void deleteIO(String hash) {
        Log.d(TAG, "Deleting an information object from the database.");
        try {
            SQLiteDatabase db = getWritableDatabase();

            // Finds and deletes object
            db.delete(TABLE_IO, KEY_HASH + " = ?", new String[] { hash });

            db.close();
        } catch (SQLiteException e) {
            Log.e(TAG, "Failed deleting information object. "
                    + "Error occured due to an unexpected database problem.");
        }
    }

    /**
     * Deletes the information object that is specified from the database.
     * 
     * @param io
     *      The information object to delete.
     */
    public void deleteIO(InformationObject io) {
        Log.d(TAG, "Deleting an information object from the database.");

        // Get the Identifier from the InformationObject
        Identifier identifier = io.getIdentifier();

        // Extract the hash
        String hash = identifier.getIdentifierLabel(SailDefinedLabelName.HASH_CONTENT.getLabelName())
                .getLabelValue();

        // Delete the object by calling deleteIO(hash)
        deleteIO(hash);
    }

    /**
     * Returns the map corresponding to the meta data contained in the specified Metadata String.
     * 
     * @param metadata
     *      The complete meta-data String
     * @return
     *       A map containing the meta data key value pairs
     * @throws DatabaseException
     *        Thrown, if a failure occurred during extracting
     */
    private Map<String, Object> extractMetaData(String metadata) throws DatabaseException {
        // Parse metadata to create an JSON Object.
        Object jsonObject = JSONValue.parse(metadata);

        // If we could not get a JSONObject from the metadata, throw DatabaseException
        if (!(jsonObject instanceof JSONObject)) {
            Log.e(TAG, "Invalid metadata.");

            // Throw exception
            throw new DatabaseException("Metadata in the information object was an invalid JSONObject.");
        }

        // Create mapping for metadata. Object may be JSON Object or JSON Array
        Map<String, Object> metadataMap = null;

        // Parse metadata again, this time adding objects to the map to reflect "String" : "Value" 
        try {
            metadataMap = MetadataParser.toMap((JSONObject) jsonObject);
        } catch (ParseException e) {
            Log.e(TAG, "Error extracting metadata");
            throw new DatabaseException("The IO cannot be inserted into the database. "
                    + "Because the meta-data could not be extracted.", e);
        }

        // Return metadata Map (String, Object)
        return metadataMap;
    }

    /**
     * Returns a content value object representing an entry in the IO table.
     * 
     * @param hash
     *       The hash value of the IO
     * @param hashAlgorithm
     *      The hash algorithm
     * @param contentType
     *        The content type
     * @param filePath
     *        The file path
     * @param fileSize
     *        The file size
     * @return
     *       The corresponding content value
     */
    private ContentValues createIOEntry(String hash, String hashAlgorithm, String contentType, String filePath,
            String fileSize) {
        // Create a new ContentValues object, readable by ContentResolver
        ContentValues ioEntry = new ContentValues();

        // Add attributes
        ioEntry.put(KEY_HASH, hash);
        ioEntry.put(KEY_HASH_ALGORITHM, hashAlgorithm);
        ioEntry.put(KEY_CONTENT_TYPE, contentType);
        ioEntry.put(KEY_FILEPATH, filePath);
        ioEntry.put(KEY_FILE_SIZE, fileSize);

        // Return the ContentValues with attributes
        return ioEntry;
    }

    /**
     * Returns a content value object representing an entry in the IO_url table.
     * 
     * @param hash
     *        The hash value of the IO
     * @param url
     *        The url where it can be found
     * @return
     *       The corresponding content value
     */
    private ContentValues createUrlEntry(String hash, String url) {
        // Create new ContentValues object, which is processed by ContentResolver
        ContentValues urlEntry = new ContentValues();

        // Add attributes
        urlEntry.put(KEY_HASH, hash);
        urlEntry.put(KEY_URL, url);

        // Return ContentValues with attributes
        return urlEntry;
    }

    /**
     * Queries the database and returns a cursor.
     * 
     * @param table
     *       The table in which we want to query
     * @param key
     *       The key
     * @param value
     *       The corresponding value
     * @return
     *       A cursor pointing to the first row of results
     * @throws DatabaseException
     *        Thrown, if no entry was found for the specified key value pair
     */
    private synchronized Cursor query(String table, String key, String value) throws DatabaseException {
        SQLiteDatabase db = null;

        try {
            db = this.getReadableDatabase();
        } catch (SQLiteException e) {
            Log.e(TAG, "Querying database failed. Error during reading database.");
            throw new DatabaseException("Unexpected error while trying to read from database.");
        }

        // Makes the query for (key,value)
        Cursor cursor = db.query(table, null, key + "=?", new String[] { value }, null, null, null);

        // If there was any result, move the cursor to the first result
        if (cursor != null && cursor.getCount() != 0) {
            cursor.moveToFirst();
        } else {
            // Fails if it does not find anything
            db.close();
            throw new DatabaseException("The given key does not correspond to any IO : " + key);
        }

        db.close();

        // Return first object
        return cursor;
    }

    /**
     * Query the database to see if a hash already exists.
     * 
     * @param hash
     *        The hash of an information object
     * @return
     *       false if the hash does not exist,
     *      true  if it does
     */
    private boolean containsIO(String hash) {
        try {
            // Check if the IO we want to insert already exists
            query(TABLE_IO, KEY_HASH, hash);

        } catch (DatabaseException e) {
            // This exception is thrown if the query is empty, in that case we say that 
            // the IO is not stored
            return false;
        }

        return true;
    }
}