/*
* Copyright (c) 2014 - 2015 Ngewi Fet <ngewif@gmail.com>
* Copyright (c) 2014 Yongxin Wang <fefe.wyx@gmail.com>
*
* 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 org.gnucash.android.export;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.os.Environment;
import android.support.annotation.NonNull;
import android.util.Log;
import com.crashlytics.android.Crashlytics;
import org.gnucash.android.BuildConfig;
import org.gnucash.android.app.GnuCashApplication;
import org.gnucash.android.db.DatabaseSchema;
import org.gnucash.android.db.adapter.AccountsDbAdapter;
import org.gnucash.android.db.adapter.BooksDbAdapter;
import org.gnucash.android.db.adapter.BudgetAmountsDbAdapter;
import org.gnucash.android.db.adapter.BudgetsDbAdapter;
import org.gnucash.android.db.adapter.CommoditiesDbAdapter;
import org.gnucash.android.db.adapter.PricesDbAdapter;
import org.gnucash.android.db.adapter.RecurrenceDbAdapter;
import org.gnucash.android.db.adapter.ScheduledActionDbAdapter;
import org.gnucash.android.db.adapter.SplitsDbAdapter;
import org.gnucash.android.db.adapter.TransactionsDbAdapter;
import java.io.File;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
/**
* Base class for the different exporters
*
* @author Ngewi Fet <ngewif@gmail.com>
* @author Yongxin Wang <fefe.wyx@gmail.com>
*/
public abstract class Exporter {
/**
* Tag for logging
*/
protected static String LOG_TAG = "Exporter";
/**
* Application folder on external storage
* @deprecated Use {@link #BASE_FOLDER_PATH} instead
*/
@Deprecated
public static final String LEGACY_BASE_FOLDER_PATH = Environment.getExternalStorageDirectory() + "/" + BuildConfig.APPLICATION_ID;
/**
* Application folder on external storage
*/
public static final String BASE_FOLDER_PATH = GnuCashApplication.getAppContext().getExternalFilesDir(null).getAbsolutePath();
/**
* Export options
*/
protected final ExportParams mExportParams;
/**
* Cache directory to which files will be first exported before moved to final destination.
* <p>There is a different cache dir per export format, which has the name of the export format.<br/>
* The cache dir is cleared every time a new {@link Exporter} is instantiated.
* The files created here are only accessible within this application, and should be copied to SD card before they can be shared
* </p>
*/
private final File mCacheDir;
private static final SimpleDateFormat EXPORT_FILENAME_DATE_FORMAT = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US);
/**
* Adapter for retrieving accounts to export
* Subclasses should close this object when they are done with exporting
*/
protected final AccountsDbAdapter mAccountsDbAdapter;
protected final TransactionsDbAdapter mTransactionsDbAdapter;
protected final SplitsDbAdapter mSplitsDbAdapter;
protected final ScheduledActionDbAdapter mScheduledActionDbAdapter;
protected final PricesDbAdapter mPricesDbAdapter;
protected final CommoditiesDbAdapter mCommoditiesDbAdapter;
protected final BudgetsDbAdapter mBudgetsDbAdapter;
protected final Context mContext;
private String mExportCacheFilePath;
/**
* Database being currently exported
*/
protected final SQLiteDatabase mDb;
/**
* GUID of the book being exported
*/
protected String mBookUID;
public Exporter(ExportParams params, SQLiteDatabase db) {
this.mExportParams = params;
mContext = GnuCashApplication.getAppContext();
if (db == null) {
mAccountsDbAdapter = AccountsDbAdapter.getInstance();
mTransactionsDbAdapter = TransactionsDbAdapter.getInstance();
mSplitsDbAdapter = SplitsDbAdapter.getInstance();
mPricesDbAdapter = PricesDbAdapter.getInstance();
mCommoditiesDbAdapter = CommoditiesDbAdapter.getInstance();
mBudgetsDbAdapter = BudgetsDbAdapter.getInstance();
mScheduledActionDbAdapter = ScheduledActionDbAdapter.getInstance();
mDb = GnuCashApplication.getActiveDb();
} else {
mDb = db;
mSplitsDbAdapter = new SplitsDbAdapter(db);
mTransactionsDbAdapter = new TransactionsDbAdapter(db, mSplitsDbAdapter);
mAccountsDbAdapter = new AccountsDbAdapter(db, mTransactionsDbAdapter);
mPricesDbAdapter = new PricesDbAdapter(db);
mCommoditiesDbAdapter = new CommoditiesDbAdapter(db);
RecurrenceDbAdapter recurrenceDbAdapter = new RecurrenceDbAdapter(db);
mBudgetsDbAdapter = new BudgetsDbAdapter(db, new BudgetAmountsDbAdapter(db), recurrenceDbAdapter);
mScheduledActionDbAdapter = new ScheduledActionDbAdapter(db, recurrenceDbAdapter);
}
mBookUID = new File(mDb.getPath()).getName(); //this depends on the database file always having the name of the book GUID
mExportCacheFilePath = null;
mCacheDir = new File(mContext.getCacheDir(), params.getExportFormat().name());
mCacheDir.mkdir();
purgeDirectory(mCacheDir);
}
/**
* Strings a string of any characters not allowed in a file name.
* All unallowed characters are replaced with an underscore
* @param inputName Raw file name input
* @return Sanitized file name
*/
public static String sanitizeFilename(String inputName) {
return inputName.replaceAll("[^a-zA-Z0-9-_\\.]", "_");
}
/**
* Builds a file name based on the current time stamp for the exported file
* @param format Format to use when exporting
* @param bookName Name of the book being exported. This name will be included in the generated file name
* @return String containing the file name
*/
public static String buildExportFilename(ExportFormat format, String bookName) {
return EXPORT_FILENAME_DATE_FORMAT.format(new Date(System.currentTimeMillis()))
+ "_gnucash_export_" + sanitizeFilename(bookName) + format.getExtension();
}
/**
* Parses the name of an export file and returns the date of export
* @param filename Export file name generated by {@link #buildExportFilename(ExportFormat,String)}
* @return Date in milliseconds
*/
public static long getExportTime(String filename){
String[] tokens = filename.split("_");
long timeMillis = 0;
if (tokens.length < 2){
return timeMillis;
}
try {
Date date = EXPORT_FILENAME_DATE_FORMAT.parse(tokens[0] + "_" + tokens[1]);
timeMillis = date.getTime();
} catch (ParseException e) {
Log.e("Exporter", "Error parsing time from file name: " + e.getMessage());
Crashlytics.logException(e);
}
return timeMillis;
}
/**
* Generates the export output
* @throws ExporterException if an error occurs during export
*/
public abstract List<String> generateExport() throws ExporterException;
/**
* Recursively delete all files in a directory
* @param directory File descriptor for directory
*/
private void purgeDirectory(File directory){
for (File file : directory.listFiles()) {
if (file.isDirectory())
purgeDirectory(file);
else
file.delete();
}
}
/**
* Returns the path to the file where the exporter should save the export during generation
* <p>This path is a temporary cache file whose file extension matches the export format.<br>
* This file is deleted every time a new export is started</p>
* @return Absolute path to file
*/
protected String getExportCacheFilePath(){
// The file name contains a timestamp, so ensure it doesn't change with multiple calls to
// avoid issues like #448
if (mExportCacheFilePath == null) {
String cachePath = mCacheDir.getAbsolutePath();
if (!cachePath.endsWith("/"))
cachePath += "/";
String bookName = BooksDbAdapter.getInstance().getAttribute(mBookUID, DatabaseSchema.BookEntry.COLUMN_DISPLAY_NAME);
mExportCacheFilePath = cachePath + buildExportFilename(mExportParams.getExportFormat(), bookName);
}
return mExportCacheFilePath;
}
/**
* Returns that path to the export folder for the book with GUID {@code bookUID}.
* This is the folder where exports like QIF and OFX will be saved for access by external programs
* @param bookUID GUID of the book being exported. Each book has its own export path
* @return Absolute path to export folder for active book
*/
public static String getExportFolderPath(String bookUID){
String path = BASE_FOLDER_PATH + "/" + bookUID + "/exports/";
File file = new File(path);
if (!file.exists())
file.mkdirs();
return path;
}
/**
* Returns the path to the backups folder for the book with GUID {@code bookUID}
* Each book has its own backup path
*
* @return Absolute path to backup folder for the book
*/
public static String getBackupFolderPath(String bookUID){
String path = BASE_FOLDER_PATH + "/" + bookUID + "/backups/";
File file = new File(path);
if (!file.exists())
file.mkdirs();
return path;
}
/**
* Returns the MIME type for this exporter.
* @return MIME type as string
*/
public String getExportMimeType(){
return "text/plain";
}
public static class ExporterException extends RuntimeException{
public ExporterException(ExportParams params){
super("Failed to generate export with parameters: " + params.toString());
}
public ExporterException(@NonNull ExportParams params, @NonNull String msg) {
super("Failed to generate export with parameters: " + params.toString() + " - " + msg);
}
public ExporterException(ExportParams params, Throwable throwable){
super("Failed to generate " + params.getExportFormat().toString() +"-"+ throwable.getMessage(),
throwable);
}
}
}