/*
* @copyright 2013 Philip Warner
* @license GNU General Public License
*
* This file is part of Book Catalogue.
*
* Book Catalogue is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Book Catalogue is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Book Catalogue. If not, see <http://www.gnu.org/licenses/>.
*/
package com.eleybourn.bookcatalogue.datamanager;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Set;
import android.annotation.TargetApi;
import android.database.Cursor;
import android.database.sqlite.SQLiteCursor;
import android.os.Build;
import android.os.Bundle;
import com.eleybourn.bookcatalogue.utils.Utils;
/**
* Class to manage a version of a set of related data.
*
* @author pjw
*
*/
public class DataManager {
// Generic validators; if field-specific defaults are needed, create a new one.
protected static DataValidator integerValidator = new IntegerValidator("0");
protected static DataValidator nonBlankValidator = new NonBlankValidator();
protected static DataValidator blankOrIntegerValidator = new OrValidator(new BlankValidator(),
new IntegerValidator("0"));
protected static DataValidator blankOrFloatValidator = new OrValidator(new BlankValidator(),
new FloatValidator("0.00"));
// FieldValidator blankOrDateValidator = new Fields.OrValidator(new Fields.BlankValidator(),
// new Fields.DateValidator());
/** Raw data storage */
protected final Bundle mBundle = new Bundle();
/** Storage for the data-related code */
private final DatumHash mData = new DatumHash();
/** The last validator exception caught by this object */
private ArrayList<ValidatorException> mValidationExceptions = new ArrayList<ValidatorException>();
/** A list of cross-validators to apply if all fields pass simple validation. */
private ArrayList<DataCrossValidator> mCrossValidators = new ArrayList<DataCrossValidator>();
/**
* Erase everything in this instance
*
* @return self, for chaining
*/
public DataManager clear() {
mBundle.clear();
mData.clear();
mValidationExceptions.clear();
mCrossValidators.clear();
return this;
}
/**
* Class to manage the collection of Datum objects for this DataManager
*
* @author pjw
*/
private static class DatumHash extends Hashtable<String,Datum> {
private static final long serialVersionUID = -650159534364183779L;
/**
* Get the specified Datum, and create a stub if not present
*/
@Override
public Datum get(Object key) {
Datum datum = super.get(key);
if (datum == null) {
datum = new Datum(key.toString(), null, true);
this.put(key.toString(), datum);
}
return datum;
}
}
/**
* Add a validator for the specified Datum
*
* @param key Key to the Datum
* @param validator Validator
*
* @return the DataManager, for chaining
*/
public DataManager addValidator(String key, DataValidator validator) {
mData.get(key).setValidator(validator);
return this;
}
/**
* Add an Accessor for the specified Datum
*
* @param key Key to the Datum
* @param accessor Accessor
*
* @return the DataManager, for chaining
*/
public DataManager addAccessor(String key, DataAccessor accessor) {
mData.get(key).setAccessor(accessor);
return this;
}
/**
* Get the data object specified by the passed key
*
* @param key Key of data object
*
* @return Data object
*/
public Object get(String key) {
return get(mData.get(key));
}
/**
* Get the data object specified by the passed {@link #Datum}
*
* @param datum Datum
*
* @return Data object
*/
public Object get(Datum datum) {
return datum.get(this, mBundle);
}
/** Retrieve a boolean value */
public boolean getBoolean(String key) {
return mData.get(key).getBoolean(this, mBundle);
}
/** Store a boolean value */
public DataManager putBoolean(String key, boolean value) {
mData.get(key).putBoolean(this, mBundle, value);
return this;
}
/** Store a boolean value */
public DataManager putBoolean(Datum datum, boolean value) {
datum.putBoolean(this, mBundle, value);
return this;
}
/** Get a double value */
public double getDouble(String key) {
return mData.get(key).getDouble(this, mBundle);
}
/** Store a double value */
public DataManager putDouble(String key, double value) {
mData.get(key).putDouble(this, mBundle, value);
return this;
}
/** Store a double value */
public DataManager putDouble(Datum datum, double value) {
datum.putDouble(this, mBundle, value);
return this;
}
/** Get a float value */
public float getFloat(String key) {
return mData.get(key).getFloat(this, mBundle);
}
/** Store a float value */
public DataManager putFloat(String key, float value) {
mData.get(key).putFloat(this, mBundle, value);
return this;
}
/** Store a float value */
public DataManager putFloat(Datum datum, float value) {
datum.putFloat(this, mBundle, value);
return this;
}
/** Get an int value */
public int getInt(String key) {
return mData.get(key).getInt(this, mBundle);
}
/** Store an int value */
public DataManager putInt(String key, int value) {
mData.get(key).putInt(this, mBundle, value);
return this;
}
/** Store an int value */
public DataManager putInt(Datum datum, int value) {
datum.putInt(this, mBundle, value);
return this;
}
/** Get a long value */
public long getLong(long key) {
return mData.get(key).getLong(this, mBundle);
}
/** Store a long value */
public DataManager putLong(String key, long value) {
mData.get(key).putLong(this, mBundle, value);
return this;
}
/** Store a long value */
public DataManager putLong(Datum datum, long value) {
datum.putLong(this, mBundle, value);
return this;
}
/** Get a String value */
public String getString(String key) {
return mData.get(key).getString(this, mBundle);
}
/** Get a String value */
public String getString(Datum datum) {
return datum.getString(this, mBundle);
}
/** Store a String value */
public DataManager putString(String key, String value) {
mData.get(key).putString(this, mBundle, value);
return this;
}
public DataManager putString(Datum datum, String value) {
datum.putString(this, mBundle, value);
return this;
}
/**
* Store all passed values in our collection.
* We do the labourious method here to allow Accessors to do their thing.
*
* @param src
* @return
*/
public DataManager putAll(Bundle src) {
for(String key: src.keySet()) {
Object o = src.get(key);
if (o instanceof String) {
putString(key, (String)o);
} else if (o instanceof Integer) {
putInt(key, (Integer)o);
} else if (o instanceof Long) {
putLong(key, (Long)o);
} else if (o instanceof Double) {
putDouble(key, (Double)o);
} else if (o instanceof Float) {
putFloat(key, (Float)o);
} else if (o instanceof Serializable) {
this.putSerializable(key, (Serializable)o);
} else {
// THIS IS NOT IDEAL!
if (o != null) {
putString(key, o.toString());
} else {
System.out.println("NULL value for key '" + key + "'");
}
}
}
return this;
}
/**
* Store the contents of the passed cursor
*
* @param cursor
*/
public void putAll(Cursor cursor) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
putAll_Api11(cursor);
} else {
putAllLegacy(cursor);
}
}
/**
* Store the contents of the passed cursor
* For API before 11, just store as strings
*
* @param cursor
*/
public void putAllLegacy(Cursor cursor) {
cursor.moveToFirst();
for(int i = 0; i < cursor.getColumnCount(); i++) {
final String name = cursor.getColumnName(i);
final String value = cursor.getString(i);
putString(name, value);
}
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
/**
* Store the contents of the passed cursor
* For API after 11, store the correct type
*
* @param cursor
*/
private void putAll_Api11(Cursor cursor) {
cursor.moveToFirst();
for(int i = 0; i < cursor.getColumnCount(); i++) {
final String name = cursor.getColumnName(i);
switch(cursor.getType(i)) {
case SQLiteCursor.FIELD_TYPE_STRING:
putString(name, cursor.getString(i));
break;
case SQLiteCursor.FIELD_TYPE_INTEGER:
putLong(name, cursor.getLong(i));
break;
case SQLiteCursor.FIELD_TYPE_FLOAT:
putDouble(name, cursor.getDouble(i));
break;
case SQLiteCursor.FIELD_TYPE_NULL:
break;
case SQLiteCursor.FIELD_TYPE_BLOB:
throw new RuntimeException("Unsupported column type: 'blob'");
default:
throw new RuntimeException("Unsupported column type: " + cursor.getType(i));
}
}
}
/**
* Get the serializable object from the collection.
* We currently do not use a Datum for special access.
*
* @param key Key of object
*
* @return The data
*/
public Object getSerializable(String key) {
return mData.get(key).getSerializable(this, mBundle);
}
/**
* Get the serializable object from the collection.
* We currently do not use a Datum for special access.
*
* @param key Key of object
* @param value The serializable object
*
* @return The data manager for chaining
*/
public DataManager putSerializable(String key, Serializable value) {
mData.get(key).putSerializable(this, mBundle, value);
return this;
}
/**
* Loop through and apply validators, generating a Bundle collection as a by-product.
* The Bundle collection is then used in cross-validation as a second pass, and finally
* passed to each defined cross-validator.
*
* @param values The Bundle collection to fill
*
* @return boolean True if all validation passed.
*/
public boolean validate() {
boolean isOk = true;
mValidationExceptions.clear();
// First, just validate individual fields with the cross-val flag set false
if (!doValidate(false))
isOk = false;
// Now re-run with cross-val set to true.
if (!doValidate(true))
isOk = false;
// Finally run the local cross-validation
Iterator<DataCrossValidator> i = mCrossValidators.iterator();
while (i.hasNext()) {
DataCrossValidator v = i.next();
try {
v.validate(this);
} catch(ValidatorException e) {
mValidationExceptions.add(e);
isOk = false;
}
}
return isOk;
}
/**
* Internal utility routine to perform one loop validating all fields.
*
* @param values The Bundle to fill in/use.
* @param crossValidating Flag indicating if this is a cross validation pass.
*/
private boolean doValidate(boolean crossValidating) {
boolean isOk = true;
for(String key: mData.keySet()) {
Datum datum = mData.get(key);
if (datum.hasValidator()) {
try {
datum.getValidator().validate(this, datum, crossValidating);
} catch(ValidatorException e) {
mValidationExceptions.add(e);
isOk = false;
}
}
}
return isOk;
}
/**
* Check if the underlying data contains the specified key.
*
* @param key
* @return
*/
public boolean containsKey(String key) {
Datum datum = mData.get(key);
if (datum.getAccessor() == null) {
return mBundle.containsKey(key);
} else {
return datum.getAccessor().isPresent(this, datum, mBundle);
}
}
/**
* Remove the specified key from this collection
*
* @param key Key of data to remove.
*
* @return
*/
public Datum remove(String key) {
Datum datum = mData.remove(key);
mBundle.remove(key);
return datum;
}
/**
* Get the current set of data
* @return
*/
public Set<String> keySet() {
return mData.keySet();
}
/**
* Retrieve the text message associated with the last validation exception t occur.
*
* @return res The resource manager to use when looking up strings.
*/
public String getValidationExceptionMessage(android.content.res.Resources res) {
if (mValidationExceptions.size() == 0)
return "No error";
else {
String message = "";
Iterator<ValidatorException> i = mValidationExceptions.iterator();
int cnt = 1;
if (i.hasNext())
message = "(" + cnt + ") " + i.next().getFormattedMessage(res);
while (i.hasNext()) {
cnt ++;
message += " (" + cnt + ") " + i.next().getFormattedMessage(res) + "\n";
}
return message;
}
}
/**
* Format the passed bundle in a way that is convenient for display
*
* @param b Bundle to format
*
* @return Formatted string
*/
public String getDataAsString() {
return Utils.bundleToString(mBundle);
}
/**
* Append a string to a list value in this collection
*
* @param key
* @param value
*/
public void appendOrAdd(String key, String value) {
String s = Utils.encodeListItem(value, '|');
if (!containsKey(key) || getString(key).length() == 0) {
putString(key, s);
} else {
String curr = getString(key);
putString(key, curr + "|" + s);
}
}
}