I have a table with > 200,000 rows (and might be a few million).
Sometimes the queries can take a few minutes to return a result.
I need to offer the end user the ability to abort the query and refine it.
To do that, I need to implement a query that can be cancelled or interrupted.
Is there a best practice for this?
The solution I came up with is to perform a series of smaller queries using LIMIT and OFFSET, checking for an interrupt between.
My concern is that repeatedly using LIMIT and OFFSET will result in searching the same data rows over and over, yielding a much slower query. Or does SQLite recognize it can start the next query at the row number where a prior identical search stopped?
public class InterruptableQuery {
public interface QueryResult {
void queryResult(boolean interrupted, List<MyData> results);
}
private QueryResult queryCallBack;
private volatile boolean _interrupted = false;
private volatile boolean _cancelled = false;
private SQLiteDatabase db;
public InterruptableQuery(SQLiteDatabase db, QueryResult qr) {
this.db = db;
queryCallBack = qr;
}
public void interruptQuery() { _interrupted = true; }
public void cancelQuery() { _cancelled = true; }
public void search(final String query) {
Thread searchThread = new Thread() {
#Override
public void run() {
boolean done = false;
int OFFSET = 100;
int offset = 0;
List<MyData> resultsList = new ArrayList<>();
while (!done && !_cancelled && !_interrupted) {
Cursor cursor = db.rawQuery(query + " LIMIT " + OFFSET + " OFFSET " + offset, null);
done = (cursor == null) || (cursor.getCount() < OFFSET);
if (cursor != null) {
if (cursor.moveToFirst()) {
do {
resultsList.add(new MyData(cursor));
} while (cursor.moveToNext());
}
cursor.close();
}
offset += OFFSET;
}
if (!_cancelled)
queryCallBack.queryResult(_interrupted, resultsList);
}
};
searchThread.start();
}
}
Related
For some reason in the following function the Cursor is not reading any marks. I don't know what I am doing wrong and I have been debugging this code for ours. When it runs it says the value of tagid and catid are both -1. No ping pong or pang :(
public String getCategoryNameByLawId(final int lawID){
final String[] categoryName = {"Success"+lawID};
final int[] tagID = {-1};
final int[] categoryID = {-1};
runnable = new Runnable() {
#Override
public void run() {
try {
openToRead();
String Query = "SELECT * from " + Constants.TABLE_LAW_TAG;
Cursor c1 = mSqLiteDatabase.rawQuery(Query, null);
if (c1.moveToFirst()) {
categoryName[0] = "ping";
while (c1.isAfterLast() == false) {
categoryName[0] = "pong";
try {
if (c1.getInt(c1.getColumnIndex(Constants.KEY_LAW_ID)) == lawID) {
int indexTagID = c1.getColumnIndex(Constants.KEY_TAG_ID);
tagID[0] = c1.getInt(indexTagID);
categoryName[0] = "pang";
}
} catch (Exception e) {
categoryName[0] = e.getMessage();
}
c1.moveToNext();
}
}
close();
openToRead();
Query = "SELECT * from " + Constants.TABLE_CATEGORY_TAG;
Cursor c2 = mSqLiteDatabase.rawQuery(Query, null);
if (c2.moveToFirst()) {
while (c2.isAfterLast() == false) {
if (c2.getInt(c2.getColumnIndex(Constants.KEY_TAG_ID)) == tagID[0]) {
int indexCategoryID = c2.getColumnIndex(Constants.KEY_CATEGORY_ID);
categoryID[0] = c2.getInt(indexCategoryID);
}
c2.moveToNext();
}
}
/*
exceptionHandler.alert(new RuntimeException(), "catid-" + categoryID[0]);
Query = "SELECT * from " + Constants.TABLE_CATEGORY;
Cursor c3 = mSqLiteDatabase.rawQuery(Query, null);
if (c3.moveToFirst()) {
while (c3.isAfterLast() == false) {
if (c3.getInt(c3.getColumnIndex(Constants.KEY_CATEGORY_ID)) == categoryID[0]) {
int indexCategoryName = c3.getColumnIndex(Constants.KEY_CATEGORY_NAME);
categoryName[0] = c3.getString(indexCategoryName);
}
c3.moveToNext();
}
}
exceptionHandler.alert(new RuntimeException(), "catnam-" + categoryName[0]);*/
close();
}
catch(Exception e){
categoryName[0] ="error";
}
}
};
new Thread(runnable).start();
return categoryName[0].toLowerCase() + " tagid: "+ tagID[0]+ " catid: "+ categoryID[0];
}
start() just starts the thread; it does not wait for it to finish.
The problem is that the value has not yet been found when you're executing return categoryName[0]....
Drop the Runnable indirection, and execute the database code directly.
If you really want to use a thread, you could wait for the thread to finish (call its join()), but this would not make sense because your main thread would be suspended as long as the database code is running. To be able to do other stuff in the main thread, you would have to reorganize your program so that the database code sends a separate message back to the main thread when it has the result. Have it look into CursorLoader (which needs a content provider, though).
I am working in Android application in which I am using ormlite. I am taking my phone book contacts and saving them in my local database, but the problem is that it is taking too much time like for almost 1500 contact it is taking almost 70 seconds.
I searched for the Bulk insert in ormlite, but I can't figure it out how to implement it in my following code.
public static void loadLocalPhoneBookSample(Context ctx) {
try{
ContentResolver contentRes = ctx.getContentResolver();
Cursor cur = null;
String selection = ContactsContract.Contacts.HAS_PHONE_NUMBER;
cur = contentRes.query(ContactsContract.Contacts.CONTENT_URI, PROJECTIONS, selection, null, Phone.DISPLAY_NAME + " ASC");
context = ctx;
if (cur.getCount() > 0) {
// create DB object
MUrgencyDBHelper db = new MUrgencyDBHelper(ctx);
RuntimeExceptionDao<ContactLocal, ?> contactDAO = db.getContactLocalIntDataDao();
UpdateBuilder<ContactLocal, ?> updateDAO = contactDAO.updateBuilder();
try {
updateDAO.updateColumnValue("isUseless", true);
updateDAO.update();
} catch (SQLException e) {
e.printStackTrace();
}finally {
// db.writeUnlock();
}
while (cur.moveToNext()) {
String id = cur.getString(cur.getColumnIndex(ContactsContract.Contacts._ID));
/** read names **/
String displayName = cur.getString(cur.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME));
/** Phone Numbers **/
Cursor pCur = contentRes.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,null, ContactsContract.CommonDataKinds.Phone.CONTACT_ID
+ " = ?", new String[] { id }, null);
while (pCur.moveToNext()) {
String number = pCur
.getString(pCur
.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
String formatedNo = number.replaceAll("\\s+", "").replace("+", "00").replace("-", "").trim();
try {
QueryBuilder<ContactLocal, ?> query = contactDAO.queryBuilder();
query.where().eq("mFormatedNumber", number);
ContactLocal contact = query.queryForFirst();
boolean addContact = false, alreadyUpdated = true;
if (contact == null) {
addContact = true;
contact = new ContactLocal();
contact.setFirstName(displayName.trim());
contact.setLastName(displayName.trim());
contact.setContactNumber(formatedNo);
}
// check if this contact was already updated before
if (contact.getContactNumber() == null || contact.getContactNumber().length() == 0) {
contact.setContFirstLastNo(number, displayName, displayName, number);
alreadyUpdated = false;
}
contact.setUseless(false);
// if not updated already, Create/Update
if (addContact) {
contactDAO.create(contact);
} else
contactDAO.update(contact);
}
}
pCur.close();
}
}
}
the problem is that it is taking too much time like for almost 1500 contact it is taking almost 70 seconds
#CarloB has the right answer in terms of doing the mass creates inside the dao. callBatchTasks(...) method. Here's the docs on that subject:
http://ormlite.com/docs/batch
To make things a bit faster, you could also go through and record all of the mFormatedNumber in another List and then query for them using an IN query. Use a raw in query to get back the mFormatedNumber that are already in the database:
results = dao.queryRaw(
"SELECT mFormatedNumber from Contact WHERE mFormatedNumber IN ?",
mFormatedNumberList);
For using raw queries with ORMLite, see:
http://ormlite.com/docs/raw-queries
So then you would make one query to see which of the contacts need to be created and then do all of the inserts from within a batch transaction.
Otherwise you are doing ~3000 synchronous database transactions and 40/sec on an Android device is unfortunately pretty typical.
Here is my revised version (might need a few syntax changes)
public static void loadLocalPhoneBookSample(Context ctx) {
try {
ContentResolver contentRes = ctx.getContentResolver();
String selection = ContactsContract.Contacts.HAS_PHONE_NUMBER;
Cursor cur = contentRes.query(ContactsContract.Contacts.CONTENT_URI, PROJECTIONS, selection, null, Phone.DISPLAY_NAME + " ASC");
context = ctx;
if (cur.getCount() > 0) {
// create DB object
MUrgencyDBHelper db = new MUrgencyDBHelper(ctx);
RuntimeExceptionDao<ContactLocal, ?> contactDAO = db.getContactLocalIntDataDao();
UpdateBuilder<ContactLocal, ?> updateDAO = contactDAO.updateBuilder();
try {
updateDAO.updateColumnValue("isUseless", true);
updateDAO.update();
} catch (SQLException e) {
e.printStackTrace();
}finally {
// db.writeUnlock();
}
ArrayList<ContactLocal> contacts = new ArrayList<>();
while (cur.moveToNext()) {
String id = cur.getString(cur.getColumnIndex(ContactsContract.Contacts._ID));
/** read names **/
String displayName = cur.getString(cur.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME));
/** Phone Numbers **/
Cursor pCur = contentRes.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,null, ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = ?", new String[] { id }, null);
while (pCur.moveToNext()) {
String number = pCur.getString(pCur.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
String formatedNo = number.replaceAll("\\s+", "").replace("+", "00").replace("-", "").trim();
try {
QueryBuilder<ContactLocal, ?> query = contactDAO.queryBuilder();
query.where().eq("mFormatedNumber", number);
ContactLocal contact = query.queryForFirst();
if (contact == null) {
contact = new ContactLocal();
contact.setFirstName(displayName.trim());
contact.setLastName(displayName.trim());
contact.setContactNumber(formatedNo);
}
contact.setUseless(false);
contacts.add(contact);
}
}
pCur.close();
}
contactDao.callBatchTasks(new Callable<Void>() {
public Void call() throws Exception {
for (ContactLocal contact : contacts) {
contactDAO.createOrUpdate(contact);
}
}
});
}
}
The main optimization is to use callBatchTasks. From the ormlite documentation:
Databases by default commit changes after every SQL operation. This method disables this "auto-commit" behavior so a number of changes can be made faster and then committed all at once.
By creating an ArrayList and keeping track of the changes, you can use callBatchTasks to create/update at the end all in one shot.
Also I noticed that alreadyUpdated was never accessed, so it's safe to remove.
Also Dao has a createOrUpdate method which is the same as the addContact if statement you had before.
.I start to write diet planner project and this is my database tables .I use external database and define tables foreign key there and copy it in asset folder and then connect it to my project.
standardUnit,Foods and standardFoodUnit are 3 tables which have static data and I filled them before,but EatenFood table is dynamically filled after Calculations.
I use model class and try to write databaseAdapter with androidhive database tutorial instruction.but because I started android recently I don't have any vision about it.
try to read book or online tutorial but they mixing up me more. now this is my question,I want to know for EatenFood table foreign key how can I put food-id value?I defined food_id INTEGER REFERENCES Foods ( _id ) in database before but in databaseAdapter class for insert or update or get function I don't know how can behave with this foreign key.
this is model class for EatenFood table
public class EatenFood {
int eatenfoodid;
boolean breakfast;
boolean lunch;
boolean snack;
boolean appetizers;
boolean dinner;
Data day;
String equivalent;
boolean dairy;
boolean vegetables;
boolean fruit;
boolean meat_bean_egg;
boolean bread_cereals;
boolean fat;
boolean suger;
double unitsum;
int food_id;
public boolean isAppetizers() {
return appetizers;
}
public void setAppetizers(boolean appetizers) {
this.appetizers = appetizers;
}
public Data getDay() {
return day;
}
public void setDay(Data day) {
this.day = day;
}
public double getUnitsum() {
return unitsum;
}
public void setUnitsum(double unitsum) {
this.unitsum = unitsum;
}
public int getFood_id() {
return food_id;
}
public void setFood_id(int food_id) {
this.food_id = food_id;
}
//all remaining getter and setter .........}
model class for food table
public class Foods {
int foodid;
String foodname;
boolean breakfast;
boolean lunch;
boolean snack;
boolean appetizers;
boolean dinner;
boolean mainfood;
boolean secondary;
public boolean isAppetizers() {
return appetizers;
}
public void setAppetizers(boolean appetizers) {
this.appetizers = appetizers;
}
public int getFoodid() {
return foodid;
}
public void setFoodid(int foodid) {
this.foodid = foodid;
}
//all remaining getter and setter .........}
DatabaseAdapter Functions
public class DatabaseAdapter {
private final String TAG = "DatabaseAdapter";
private DatabaseOpenHelper openHelper;
public Long insertEatenFood(EatenFood eatenfood) {
SQLiteDatabase myDataBase = null;
Long id = -1L;
try {
ContentValues values = new ContentValues();
values.put(TABLE_EATENFOOD_BREAKFAST, eatenfood.isBreakfast());
values.put(TABLE_EATENFOOD_LUNCH, eatenfood.isLunch());
values.put(TABLE_EATENFOOD_SNACK, eatenfood.isSnack());
values.put(TABLE_EATENFOOD_APPETIZERS, eatenfood.isAppetizers());
values.put(TABLE_EATENFOOD_DINNER, eatenfood.isDinner());
// values.put(TABLE_EATENFOOD_DATA, eatenfood.getDay().getClass());
values.put(TABLE_EATENFOOD_EQUIVALENT, eatenfood.getEquivalent());
values.put(TABLE_EATENFOOD_DAIRY, eatenfood.isDairy());
values.put(TABLE_EATENFOOD_VEGETABLES, eatenfood.isVegetables());
values.put(TABLE_EATENFOOD_FRUIT, eatenfood.isFruit());
values.put(TABLE_EATENFOOD_MEAT_BEAN_EGG,
eatenfood.isMeat_bean_egg());
values.put(TABLE_EATENFOOD_BREAD_CEREALS,
eatenfood.isBread_cereals());
values.put(TABLE_EATENFOOD_FAT, eatenfood.isFat());
values.put(TABLE_EATENFOOD_SUGER, eatenfood.isSuger());
values.put(TABLE_EATENFOOD_UNITSUM, eatenfood.getUnitsum());
myDataBase = openHelper.getWritableDatabase();
id = myDataBase.insert(TABLE_EATENFOOD, null, values);
} catch (Exception e) {
Log.e(TAG, "Exception: " + e.getMessage());
} finally {
if (myDataBase != null && myDataBase.isOpen())
myDataBase.close();
}
return id;
}
// update EateanFood table =====================================================
public int updateEatenFood(EatenFood eatenfood) {
SQLiteDatabase myDataBase = null;
int count = -1;
try {
myDataBase = openHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(TABLE_EATENFOOD_BREAKFAST, eatenfood.isBreakfast());
values.put(TABLE_EATENFOOD_LUNCH, eatenfood.isLunch());
values.put(TABLE_EATENFOOD_SNACK, eatenfood.isSnack());
values.put(TABLE_EATENFOOD_APPETIZERS, eatenfood.isAppetizers());
values.put(TABLE_EATENFOOD_DINNER, eatenfood.isDinner());
// values.put(TABLE_EATENFOOD_DATA, eatenfood.getDay().getClass());
values.put(TABLE_EATENFOOD_EQUIVALENT, eatenfood.getEquivalent());
values.put(TABLE_EATENFOOD_DAIRY, eatenfood.isDairy());
values.put(TABLE_EATENFOOD_VEGETABLES, eatenfood.isVegetables());
values.put(TABLE_EATENFOOD_FRUIT, eatenfood.isFruit());
values.put(TABLE_EATENFOOD_MEAT_BEAN_EGG,
eatenfood.isMeat_bean_egg());
values.put(TABLE_EATENFOOD_BREAD_CEREALS,
eatenfood.isBread_cereals());
values.put(TABLE_EATENFOOD_FAT, eatenfood.isFat());
values.put(TABLE_EATENFOOD_SUGER, eatenfood.isSuger());
values.put(TABLE_EATENFOOD_UNITSUM, eatenfood.getUnitsum());
count = myDataBase
.update(TABLE_EATENFOOD, values, TABLE_EATENFOOD_ID + "=?",
new String[] { String.valueOf(eatenfood
.getEatenfoodid()) });
} catch (Exception e) {
Log.e(TAG, "Exception: " + e.getMessage());
} finally {
myDataBase.close();
}
return count;
}
// Getting All EatenFood ================================================
public ArrayList<EatenFood> getEatenfoods() {
ArrayList<EatenFood> result = null;
SQLiteDatabase myDataBase = null;
Cursor cursor = null;
try {
myDataBase = openHelper.getWritableDatabase();
cursor = myDataBase.query(TABLE_EATENFOOD, new String[] { "*" }, null, null,
null, null, null);
if (cursor.moveToFirst()) {
result = new ArrayList<EatenFood>();
do {
result.add(extractEatenFood(cursor));
} while (cursor.moveToNext());
}
} catch (Exception e) {
Log.e(TAG, "Exception: " + e.getMessage());
}
finally {
if (cursor != null) {
cursor.close();
}
myDataBase.close();
}
return result;
}
// extractEatenFood=============================================================
private EatenFood extractEatenFood(Cursor cursor){
EatenFood eatenfood = new EatenFood();
eatenfood.setEatenfoodid(cursor.getInt(cursor.getColumnIndex(TABLE_EATENFOOD_ID)));
eatenfood.setBreakfast(cursor.getInt(cursor.getColumnIndex(TABLE_EATENFOOD_BREAKFAST)) != 0);
eatenfood.setLunch(cursor.getInt(cursor.getColumnIndex(TABLE_EATENFOOD_LUNCH))!=0);
eatenfood.setSnack(cursor.getInt(cursor.getColumnIndex(TABLE_EATENFOOD_SNACK))!=0);
eatenfood.setAppetizers(cursor.getInt(cursor.getColumnIndex(TABLE_EATENFOOD_APPETIZERS))!=0);
eatenfood.setDinner(cursor.getInt(cursor.getColumnIndex(TABLE_EATENFOOD_DINNER))!=0);
// ???????????????????????? baraye day k sabt beshe
eatenfood.setEquivalent(cursor.getString(cursor.getColumnIndex(TABLE_EATENFOOD_EQUIVALENT)));
eatenfood.setDairy(cursor.getInt(cursor.getColumnIndex(TABLE_EATENFOOD_DAIRY))!=0);
eatenfood.setVegetables(cursor.getInt(cursor.getColumnIndex(TABLE_EATENFOOD_VEGETABLES))!=0);
eatenfood.setFruit(cursor.getInt(cursor.getColumnIndex(TABLE_EATENFOOD_FRUIT))!=0);
eatenfood.setBread_cereals(cursor.getInt(cursor.getColumnIndex(TABLE_EATENFOOD_BREAD_CEREALS))!=0);
eatenfood.setFat(cursor.getInt(cursor.getColumnIndex(TABLE_EATENFOOD_FAT))!=0);
eatenfood.setSuger(cursor.getInt(cursor.getColumnIndex(TABLE_EATENFOOD_SUGER))!=0);
eatenfood.setFood_id(cursor.getInt(cursor.getColumnIndex(TABLE_EATENFOOD_F_FOODID)));
return eatenfood ;
}
Whenever you want to add a food into you're eatenfood table. You have to call getFoodid function on you're specific food object and get the food_id and after that insert into database with insertEatenFood function in you're DatabaseAdapter class.
It's better you mention you're whole example of you're question that's makes it more easy to help you.
Maybe you got a problem about how can you find the food_id's that you want to insert into you're eatenfood table. It's better to write you're algorithms after that you find out which food_id's gonna be needed for you're different users.
I'm getting this error :
E/AndroidRuntime(8223): Caused by: android.database.CursorWindowAllocationException: Cursor window allocation of 2048 kb failed. # Open Cursors=940 (# cursors opened by this proc=940)
I know its probably because I'm using the Cursor in a wrong way or not closing it at the right time. I think it could be because I'm filling the same cursor without closing/emptying it?
public static void NextAlarmTxt(){
int dan = c.get(Calendar.DAY_OF_WEEK);
long trenutnovrijeme = c.getTimeInMillis();
long bazavrijeme;
long pamti = 0;
String danString = dani(dan);
Cursor CursorDan = DatabaseManager.getAllDataDay(danString);
CursorDan.moveToFirst();
if (!CursorDan.isAfterLast())
{
do
{
bazavrijeme = CursorDan.getInt(2);
if (trenutnovrijeme<bazavrijeme)
{
if (pamti==0)
{
pamti = bazavrijeme;
}
if (pamti>0)
{
if (pamti > bazavrijeme)
{
pamti = bazavrijeme;
}
}
}
if (trenutnovrijeme > bazavrijeme)
{
dan = dan+1;
dani(dan);
CursorDan = DatabaseManager.getAllDataDay(danString);
}
}
while (CursorDan.moveToNext());
}
CursorDan.close();
text1.setText(new StringBuilder("Sljedeći : " ).append(pamti).toString());
}
public static String dani(int dan){
String danString = null;
if (dan==1)
{
danString = "Nedjelja";
}
else if (dan==2)
{
danString = "Ponedjeljak";
}
else if (dan==3)
{
danString = "Utorak";
}
else if (dan==4)
{
danString = "Srijeda";
}
else if (dan==5)
{
danString = "Četvrtak";
}
else if (dan==6)
{
danString = "Petak";
}
else if (dan==7)
{
danString = "Subota";
}
return danString;
}
Cursor Opened :
Cursor CursorDan = DatabaseManager.getAllDataDay(danString);
After that you iterate through the cursor,
do {
...
if (trenutnovrijeme > bazavrijeme)
{
dan = dan+1;
dani(dan);
CursorDan = DatabaseManager.getAllDataDay(danString);
}
....
}
while (CursorDan.moveToNext());
Now, with in the iteration loop, you overwrite the existing cursor with a new one, which leaves the existing one open. I am not sure what you are trying to achieve, but you should not be nesting cursor iteration this way and even if you do that, you should do it in a correct sequece.
Close existing cursor
Retrieve new cursor
Move new cursor to the first item by cursor.moveToFirst()
Not much code since I'm a bit at a loss on how to start.
I'm trying to create an application that backs up a Derby database and stores the users data. I have the code for the backup itself that can be run manually. I want to create a feature that will check a settings file, and execute a backup at the proper schedule (daily, weekly, monthly). I think I can make it check on start up, but there's an issue if the application is running, I'd like it to periodically check the time. There's a good possibility that this application will be left running for days on end.
I also want to allow the users to "sleep" the backup for a few hours if there scheduled time has come.
I can have a Thread.Sleep() called at bootup and have it check every X minute/hours. Similarly if they choose to sleep the backup. I'm not sure if that's the best way to do it. I assume any API call will probably do the same, but I'm wondering if I'm missing something in handling a thread like that.
Are there any functions/libraries in the Netbeans IDE & Platform that I'm leveraging, that I could hook into to help me build this functionality?
Thanks
Here's how I implemented it. I changed the method of implementation a bit, so this answer isn't exactly how the question was worded. I removed the "sleep" feature, but using the code I'm providing you could easily implement it. I have it partially implemented, and I'll show the code below. It basically involves creating a few classes that implement Runnable. The actual code to run the backup isn't shown. That would be platform specific anyway, so handle it as you would. We're using Derby, so that's how our backups are handled.
I'll break it down by class & function:
DatabaseBackupReminder. This class handles the prompt to the user that tells them how long since the last backup, and allows them to sleep the reminder for X number of hours. It is a thread itself, so it can be called somewhere else and slept too, so it's not constantly pinging the DB to see when the last backup was run.
public class DatabaseBackupReminder extends Thread
/*****************Variables ****************/
String backupFrequency; //How often to backup? Daily, Monthly, Weekly, or Never
String backupTimeOfDay; //I don't use this, but the idea was to autobackup morning,
//day, or night
boolean backupIsSet = false; //Have they set a schedule?
Timestamp lastBackupRunTime; //Pulled from our Derby database to see when the backup was run
Timestamp backupScheduleSetTime; //Pulled from the DB to see when the user set the schedule
//This is so, if they set it to daily, we will check 24hrs from the set date for
//backup validity
Period periodSinceLastBackup; //Using the Joda library, we use this to calculate
boolean backupEverRan; //We check to see if they've ever backed up
//Useful logic for clear dialog purposes.
public enum enumBackupFrequencies {DAILY, WEEKLY, MONTHLY, NEVER} //use a valueOf with the backupFrequency.
//We're using java 1.7, no Switch on strings, this is a workaround
Herd herd;//Herd is a custom datatype we use, it's basically a wrapper around a Derby table.
/*******************methods***************************************/
public DatabaseBackupReminder(String backupFrequency, String backupTimeOfDay, Timestamp lastBackupRunTime, Timestamp backupScheduleSetTime, Herd herd)
//Constructor
//Herd is a custom datatype we use, it's basically a wrapper around a Derby table.
boolean getBackupStillValid() //Checks based on lastBackupRunTime, and backupEverRan to see
//if we have a valid backup
public void promptForBackup(Period duration, boolean backupEverRunx)
//Take's the duration & has it ever run and displays a message.
//Something like "It's been 2 weeks, 1 days since your last backup"
//Not fully implemented, this was scrapped, but I'll explain how I think it should have worked below
public void run()
//Main thread for this reminder
public String timeSinceLastBackupString()
//Calls it based on objects values, not specific values, see method below
public String timeSinceLastBackupString(Period duration, boolean backupEverRunx)
//Constructs the string used in promptForBackup
/********full method code**********/
public DatabaseBackupReminder(String backupFrequency, String backupTimeOfDay, Timestamp lastBackupRunTime, Timestamp backupScheduleSetTime, Herd herd) {
this.herd = herd;
if (backupFrequency == null) {
backupFrequency = "";
}
if (backupTimeOfDay == null) {
backupTimeOfDay = "";
}
if (backupScheduleSetTime == null) {
this.backupScheduleSetTime = new Timestamp(0);
} else {
this.backupScheduleSetTime = backupScheduleSetTime;
}
this.backupFrequency = backupFrequency;
this.backupTimeOfDay = backupTimeOfDay;
if (lastBackupRunTime == null) {
this.lastBackupRunTime = new Timestamp(0);
} else {
this.lastBackupRunTime = lastBackupRunTime;
}
periodSinceLastBackup = new Period(this.lastBackupRunTime.getTime(), Calendar.getInstance().getTimeInMillis());
if (backupFrequency.trim().length() > 1) {
backupIsSet = true;
}
backupEverRan = false;
if (this.lastBackupRunTime.getTime() != 0) {
backupEverRan = true;
}
}
boolean getBackupStillValid() {
if (lastBackupRunTime.getTime() > 0) {
backupEverRan = true;
} else {
return false;
}
if (backupFrequency.trim().length() > 1) {
backupIsSet = true;
}
if (backupIsSet) {
switch (enumBackupFrequencies.valueOf(backupFrequency.trim().toUpperCase())) {
case DAILY:
if (periodSinceLastBackup.getYears() > 1 || periodSinceLastBackup.getMonths() > 1 || periodSinceLastBackup.getWeeks() > 1 || periodSinceLastBackup.getDays() >= 1) {
return false;
}
break;
case WEEKLY:
if (periodSinceLastBackup.getYears() > 1 || periodSinceLastBackup.getMonths() > 1 || periodSinceLastBackup.getWeeks() >= 1) {
return false;
}
break;
case MONTHLY:
if (periodSinceLastBackup.getYears() > 1 || periodSinceLastBackup.getMonths() >= 1) {
return false;
}
break;
case NEVER:
}
}
if (backupEverRan) {
return true;
} else {
return false;
}
}
public void run() {
if (backupIsSet) {
switch (enumBackupFrequencies.valueOf(backupFrequency.trim().toUpperCase())) {
case DAILY:
if (periodSinceLastBackup.getYears() > 1 || periodSinceLastBackup.getMonths() > 1 || periodSinceLastBackup.getWeeks() > 1 || periodSinceLastBackup.getDays() > 1) {
promptForBackup(periodSinceLastBackup, backupEverRan);
}
break;
case WEEKLY:
if (periodSinceLastBackup.getYears() > 1 || periodSinceLastBackup.getMonths() > 1 || periodSinceLastBackup.getWeeks() > 1) {
promptForBackup(periodSinceLastBackup, backupEverRan);
}
break;
case MONTHLY:
if (periodSinceLastBackup.getYears() > 1 || periodSinceLastBackup.getMonths() > 1) {
promptForBackup(periodSinceLastBackup, backupEverRan);
}
break;
case NEVER:
}
}
}
public void promptForBackup(Period duration, boolean backupEverRun) {
int response;
long delay = 0;
response = JOptionPane.showConfirmDialog(null, timeSinceLastBackupString(duration, backupEverRun));
if (response == JOptionPane.NO_OPTION) {
//TODO: open "how long to remind" dialog
BackupSnoozePanel snoozePanel = new BackupSnoozePanel();
JOptionPane.showMessageDialog(null, snoozePanel);
switch (snoozePanel.BackupDelayInterval.getSelectedIndex()) {
case 0:
delay = 5000; //5 seconds, for testing
//delay = 60 * 60 * 1000; // 1 hour
break;
case 1:
delay = 10000; //10 seconds, for testing
//delay = 4 * 60 * 60 * 1000; // 4 hours
break;
case 2:
delay = 15000; //15 seconds, for testing
//delay = 8 * 60 * 60 * 1000; // 8 hours
break;
case 3:
delay = 20000; //20 seconds, for testing
///delay = 12 * 60 * 60 * 1000; // 12 hours
break;
case 4:
delay = 0; //next boot
break;
}
} else {
//TODO: run backup
}
try {
if (delay > 0) {
//TODO: Code to sleep this reminder. Thread.sleep(delay) probably
}
} catch (Exception ex) {
//TODO: something to handle exceptions
}
}//end promptForBackup
public String timeSinceLastBackupString(Period duration, boolean backupEverRunx) {
if (!backupEverRunx) {
return "The backup has never been run. Would you like to run one?";
}
String durationString = "It has been ";
if (duration.getYears() >= 1) {
durationString += duration.getYears() + " years";
}
if (duration.getMonths() >= 1) {
durationString += duration.getMonths() + " months";
}
if (duration.getWeeks() >= 1) {
durationString += duration.getWeeks() + " weeks ";
}
if (duration.getDays() >= 1) {
durationString += duration.getDays() + " days";
}
durationString += " since your last backup. Would you like to run one?";
return durationString;
}
public String timeSinceLastBackupString() {
return timeSinceLastBackupString(periodSinceLastBackup, backupEverRan);
}
DatabaseBackupController. This class, as it's named, controls the whole process. From reminder, to executing the actual backup code.
public class DatabaseBackupController
/***********variables*************/
String scheduleText; //Daily, Weekly, Monthy, or Never. It's set in our options panel.
String timeText; //Time of day to run the backup, morning, day, or night. As 04:00-12:00 etc…
Timestamp lastBackupRun; //Timestamp from DB (Herd is our abstracted class) from when it was last run.
Timestamp lastBackupRunScheduleSetTime; //Timestamp from DB when the backup was set.
Herd herd; //Herd is a custom datatype we use, it's basically a wrapper around a Derby table.
/***********Method Headers**********/
public DatabaseBackupController(Herd herd) //Constructor
//Sets global variables, based on values in DB
public void setupBackupReminder()
//calls DatabaseBackupReminder, passes global variables.
public boolean checkBackupReminder()
//Checks to make sure the current backup is valid within the duration since last backup
public void runBackupPrompt()
//When we are in fact going to backup, calls BackupRunner instance.
/**********full method code****************/
public DatabaseBackupController(Herd herd) {
this.herd = herd;
scheduleText = herd.getBackupSchedule();
timeText = herd.getBackupTime();
lastBackupRun = herd.getBackupScheduledLastRun();
lastBackupRunScheduleSetTime = herd.getBackupScheduledDatetime();
}
public void setupBackupReminder() {
DatabaseBackupReminder dbReminder = new DatabaseBackupReminder(scheduleText, timeText, lastBackupRun, lastBackupRunScheduleSetTime, herd);
Thread dbBackupThread = new Thread(dbReminder);
dbBackupThread.start();
}//end setupBackupReminder
public boolean checkBackupReminder() {
DatabaseBackupReminder dbReminder = new DatabaseBackupReminder(scheduleText, timeText, lastBackupRun, lastBackupRunScheduleSetTime, herd);
return dbReminder.getBackupStillValid();
}
public void runBackupPrompt() {
DatabaseBackupReminder dbReminder = new DatabaseBackupReminder(scheduleText, timeText, lastBackupRun, lastBackupRunScheduleSetTime, herd);
int response = JOptionPane.showConfirmDialog(null, dbReminder.timeSinceLastBackupString());
if (response == JOptionPane.YES_OPTION) {
//NbPreferences.forModule(BackupSettingsPanel.class).putLong("databaseschedullastrun", Calendar.getInstance().getTimeInMillis());
LoadStatusDialog lsd;
lsd = null;
lsd = new LoadStatusDialog(WindowManager.getDefault().getMainWindow(), false, true);
lsd.setVisible(true);
Thread br = new Thread(new BackupRunner(herd,lsd));
br.start();
}
}
}//end class
Class DbBackupAction handles the local backup aspect. We backup locally, then send that file offsite in another class.
This implements runnable, so it will handle the backup asynchronously and not cause the whole program to hang up.
class DbBackupAction implements Runnable {
private boolean backupSuccess;
public DbBackupAction() {
this.backupSuccess = false;
}
public void runBackup() {
}
#Override
public void run() {
Connection connection = JDBCUtils.getConnection();
String dbName = NbPreferences.forModule(DatabasePanel.class).get("dbName", "");
try {
DerbyUtils.backUpDatabase(connection, dbName);
} catch (SQLException ex) {
Exceptions.printStackTrace(ex);
}
setBackupSuccess(true);
}
/**
* #return the backupSuccess
*/
public boolean isBackupSuccess() {
return backupSuccess;
}
/**
* #param backupSuccess the backupSuccess to set
*/
public void setBackupSuccess(boolean backupSuccess) {
this.backupSuccess = backupSuccess;
}
}
BackupRunner handles both onsite/offsite backups. Uses DbBackupAction && RemoteBackupAction
class BackupRunner implements Runnable {
Herd herd;
LoadStatusDialog lsd;
BackupRunner(Herd herd, LoadStatusDialog lsd) {
this.herd = herd;
this.lsd = lsd;
}
#Override
public void run() {
DbBackupAction dba = new DbBackupAction();
Thread dbaThread = new Thread(dba);
dbaThread.start();
while (dbaThread.isAlive()) {
try {
dbaThread.join();
} catch (InterruptedException ex) {
Exceptions.printStackTrace(ex);
}
}
if (dba.isBackupSuccess()) {
RemoteBackupAction rba = new RemoteBackupAction();
lsd.setProgressBarIndeterminate(true);
Thread rbaThread = new Thread(rba);
rbaThread.start();
while (rbaThread.isAlive()) {
try {
rbaThread.join();
} catch (InterruptedException ex) {
Exceptions.printStackTrace(ex);
}
}
if (rba.isBackupSuccess()) {
herd.setBackupScheduledLastRun(new Timestamp(Calendar.getInstance().getTimeInMillis()));
HerdService hs = new HerdDaoService();
hs.update(herd);
EventBus.publish(new RefreshStartScreenEvent());
}
}
}
}
RemoteBackupAction handles FTPing out backup offsite.
class RemoteBackupAction implements Runnable {
Thread thread;
LoadStatusDialog lsd;
File backupFile;
Pref pref;
private boolean backupSuccess;
public RemoteBackupAction() {
backupSuccess = false;
}
public void runThread() {
backupSuccess = true;
try {
DerbyUtils.remoteBackupDatabase(backupFile);
} catch (SQLException ex) {
JOptionPane.showMessageDialog(null,
"This option is not available when working offline.");
System.out.println("SQLExcption: " + ex);
backupSuccess = false;
} catch (Exception ex) {
JOptionPane.showMessageDialog(null,
"Unable to connection to ftp site for remote backup.");
System.out.println("IOExcption: " + ex);
backupSuccess = false;
}
}
public void startOffsiteBackup() {
pref = CentralLookup.getDefault().lookup(Pref.class);
System.out.println("pref.isOnline(): " + pref.isOnline());
if (!pref.isOnline()) {
JOptionPane.showMessageDialog(null,
"This option is not available when working offline.");
return;
}
File[] files = DerbyUtils.getListOfBackups();
if ((files == null) || (files.length < 1)) {
JOptionPane.showMessageDialog(null,
"There are no backup files available for upload. "
+ "Please create a local backup.");
return;
}
Backup[] backups = new Backup[files.length];
if (files.length > 0) {
Date[] dates = new Date[files.length];
String[] herdCodes = new String[files.length];
SimpleDateFormat inFormat = new SimpleDateFormat("yyyy-MM-dd-HH-mm");
for (int i = 0; i < files.length; i++) {
try {
String[] splitFileName = files[i].getName().split("_");
herdCodes[i] = splitFileName[0];
dates[i] = inFormat.parse(splitFileName[1].split("//.")[0]);
backups[i] = new Backup(herdCodes[i], files[i], files[i].getName(), dates[i]);
} catch (ParseException ex) {
Exceptions.printStackTrace(ex);
}
}
} else {
System.out.println("no backup files yet");
}
Arrays.sort(backups, Collections.reverseOrder());
if (backups[0] != null) {
this.backupFile = backups[0].getFile();
} else {
// Cancel button selected
return;
}
runThread();
}
/**
* #return the backupSuccess
*/
public boolean isBackupSuccess() {
return backupSuccess;
}
#Override
public void run() {
startOffsiteBackup();
}