/**
* Copyright (c) 2002-2013 "Neo Technology,"
* Network Engine for Objects in Lund AB [http://neotechnology.com]
*
* This file is part of Neo4j.
*
* Neo4j 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.neo4j.android.service;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.locks.ReentrantLock;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.neo4j.android.Version;
import org.neo4j.android.common.IGraphDatabase;
import org.neo4j.android.common.INeo4jService;
import org.neo4j.android.common.ParcelableError;
import org.neo4j.kernel.EmbeddedGraphDatabase;
import android.app.Service;
import android.content.Intent;
import android.content.res.Resources.NotFoundException;
import android.os.Environment;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
/**
* Service wrapper for Neo4j.
*/
public class Neo4jServiceImpl extends Service {
private static final String TAG = Neo4jServiceImpl.class.getSimpleName();
private static final String EXTENSION = ".zip";
private static final String PROPERTY_PRELOAD_ALL = "preloadAll";
private static final int BLOCKSIZE = 8192;
// Transaction reaper interval
private static final long TRX_REAPER_INTERVAL_MILLIS = 1000;
private static final long TRX_MAX_LIFETIME_MILLIS = 120 * 1000;
/**
* The databases that are currently loaded. Weak referenced, so they may be
* garbage collected if no clients use them (we can't detect a single client
* disconnecting)
*/
private Map<String, WeakReference<EmbeddedGraphDatabase>> mDatabases = new HashMap<String, WeakReference<EmbeddedGraphDatabase>>();
// this lock protects the above data structure
private ReentrantLock mDatabaseLock = new ReentrantLock(true);
/**
* The core of our transaction association mechanism. It will associate
* IBinder instances (DbWrapper) with transactions, so that multiple bound
* clients may perform independent transactions.
*/
private TrxManager mTrxManager;
/**
* Background cleanup of zombie transactions. We don't get notified if a
* client unbinds (weirdly, only wenn all unbind), so we don't really know
* if the client has died and trx is going to stay here forever.
*/
private Reaper mReaper;
private IBinder mBinder = new INeo4jService.Stub() {
@Override
public IGraphDatabase openOrCreateDatabase(String name, ParcelableError err) throws RemoteException {
if (name.isEmpty()) {
String message = "given database name is empty.";
err.setError(Errors.MISSING_DATABASE_NAME, message);
Log.e(TAG, message);
return null;
}
try {
return doOpenOrCreateDatabase(name);
} catch (Exception e) {
err.setError(Errors.OPEN_CREATE_DATABASE, e.getMessage());
Log.e(TAG, "Exception while opening/creating database '" + name + "'", e);
return null;
}
}
@Override
public String getNeo4jVersion() throws RemoteException {
return Version.getVersion();
}
@Override
public List<String> listAvailableDatabases() throws RemoteException {
return doListAvailableDatabases();
}
@Override
public boolean deleteDatabase(String name, ParcelableError err) throws RemoteException {
if (name.isEmpty()) {
String message = "given database name is empty.";
err.setError(Errors.MISSING_DATABASE_NAME, message);
Log.e(TAG, message);
return false;
}
boolean doDelete = false;
// check if DB is loaded, if it is, terminate it
mDatabaseLock.lock();
try {
WeakReference<EmbeddedGraphDatabase> dbRef = mDatabases.get(name);
if (dbRef == null) {
doDelete = true;
} else {
if (dbRef.get() != null) {
// DB is still in memory and strongly referenced?
Log.w(TAG, "Delete requested for database that is still being referenced (HINT: misbehaving clients?)");
} else {
// object has been cleared, remove the mapping and
// delete the DB
mDatabases.remove(name);
doDelete = true;
}
}
if (doDelete) {
deleteDatabaseDir(name); // I/O in a lock, not good, but
// it's not critical here
}
return doDelete;
} finally {
mDatabaseLock.unlock();
}
}
@Override
public boolean exportDatabase(String name, ParcelableError err) throws RemoteException {
if (name.isEmpty()) {
String message = "given database name is empty.";
err.setError(Errors.MISSING_DATABASE_NAME, message);
Log.e(TAG, message);
return false;
}
boolean doExport = false;
// check if DB is loaded, if it is, terminate it
mDatabaseLock.lock();
try {
WeakReference<EmbeddedGraphDatabase> dbRef = mDatabases.get(name);
if (dbRef == null) {
doExport = true;
} else {
if (dbRef.get() != null) {
// DB is still in memory and strongly referenced?
Log.w(TAG, "Export requested for database that is still being referenced (HINT: misbehaving clients?)");
} else {
// object has been cleared, remove the mapping and
// delete the DB
mDatabases.remove(name);
doExport = true;
}
}
if (doExport) {
exportDatabaseDir(name); // I/O in a lock, not good, but
// it's not critical here
}
return doExport;
} finally {
mDatabaseLock.unlock();
}
}
@Override
public void shutdownDatabase(String name, ParcelableError err) throws RemoteException {
if (name.isEmpty()) {
String message = "given database name is empty.";
err.setError(Errors.MISSING_DATABASE_NAME, message);
Log.e(TAG, message);
return;
}
mDatabaseLock.lock();
try {
WeakReference<EmbeddedGraphDatabase> dbRef = mDatabases.get(name);
if (dbRef != null) {
if (dbRef.get() != null) {
EmbeddedGraphDatabase db = dbRef.get();
db.shutdown();
}
mDatabases.remove(name);
} else {
Log.w(TAG, "no database found with name '" + name + "'. available databases '" + mDatabases.keySet() + "'.");
}
} finally {
mDatabaseLock.unlock();
}
}
@Override
public boolean databaseExists(String name) throws RemoteException {
return databaseDirExists(name);
}
public boolean isDatabaseOpen(String name) throws RemoteException {
if (name == null) {
return false;
} else {
return (mDatabases.containsKey(name) && (mDatabases.get(name).get() != null));
}
}
};
/**
* Check if the named DB exists. Under the hood we just check if the
* database directory exists and assume that it is non empty and filled with
* valid data.
*
* @param name
*/
private boolean databaseDirExists(String name) {
// input checks, no exceptions!
if (name == null) {
return false;
}
File dbDir = new File(getApplicationContext().getFilesDir(), name);
return dbDir.exists();
}
private void deleteDatabaseDir(String name) {
File dbDir = new File(getApplicationContext().getFilesDir(), name);
if (!dbDir.exists()) {
return;
}
// delete all files in directory. Let's hope Neo4j doesn't introduce
// subdirectories...
for (File file : dbDir.listFiles()) {
boolean deleted = file.delete();
Log.i(TAG, "Deleted file " + file + ": " + deleted);
}
boolean dirDeleted = dbDir.delete();
Log.i(TAG, "Deleting DB directory at " + dbDir + ": " + dirDeleted);
return;
}
private void exportDatabaseDir(String name) {
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
File externalDir = Environment.getExternalStorageDirectory();
File dbDir = new File(getApplicationContext().getFilesDir(), name);
if (!dbDir.exists()) {
Log.i(TAG, "database with name '" + name + "' does not exist. doing nothing.");
return;
}
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.US);
String zipName = externalDir.getAbsolutePath() + "/" + name + "_" + dateFormat.format(new Date()) + EXTENSION;
try {
ZipOutputStream zippingOutputStream = new ZipOutputStream(new FileOutputStream(zipName, false));
for (File file : dbDir.listFiles()) {
zippingOutputStream.putNextEntry(new ZipEntry(file.getName()));
zippingOutputStream.setLevel(ZipOutputStream.DEFLATED);
BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(file));
byte[] data = new byte[BLOCKSIZE];
int byteCount;
while ((byteCount = bufferedInputStream.read(data, 0, BLOCKSIZE)) > -1) {
zippingOutputStream.write(data, 0, byteCount);
}
bufferedInputStream.close();
zippingOutputStream.closeEntry();
}
zippingOutputStream.close();
} catch (Exception e) {
Log.e(TAG, "zipping of '" + dbDir.getAbsolutePath() + "' failed.", e);
}
Log.i(TAG, "created zip file '" + zipName + "'");
} else {
Log.i(TAG, "external storage not mounted. doing nothing.");
}
}
private Properties loadNeo4jProperties() {
Properties properties = new Properties();
try {
InputStream inputStream = getApplicationContext().getResources().openRawResource(R.raw.neo4j);
properties.load(inputStream);
inputStream.close();
} catch (NotFoundException e) {
Log.e(TAG, "unable to load neo4j configuration", e);
} catch (IOException e) {
Log.e(TAG, "unable to load neo4j configuration", e);
}
return properties;
}
private List<String> doListAvailableDatabases() {
List<String> databaseDirs = new ArrayList<String>();
for (File file : getApplicationContext().getFilesDir().listFiles()) {
databaseDirs.add(file.getName());
}
return databaseDirs;
}
private IGraphDatabase doOpenOrCreateDatabase(String name) {
mDatabaseLock.lock();
try {
// TODO: [eRiC] what happens if another application tries to
// open the same database?
WeakReference<EmbeddedGraphDatabase> dbRef = mDatabases.get(name);
// check if the DB is already loaded, or if has been
// previously loaded but unloaded by the GC
EmbeddedGraphDatabase db = null;
if (dbRef != null) {
db = dbRef.get();
if (db == null) {
Log.d(TAG, "database '" + name + "' was previously loaded, and garbage collected.");
// delete the mapping, we will reload and remap it
// later
mDatabases.remove(name);
}
}
// if we haven't found a DB yet, it wasn't loaded, or has
// been unloaded by the GC
if (db == null) {
File dbDir = new File(getApplicationContext().getFilesDir(), name);
db = new EmbeddedGraphDatabase(getApplicationContext(), dbDir.getAbsolutePath());
mDatabases.put(name, new WeakReference<EmbeddedGraphDatabase>(db));
Log.d(TAG, "database '" + name + "' loaded.");
}
DbWrapper dbWrapper = new DbWrapper(db, mTrxManager, getApplicationContext());
Log.i(TAG, "Returning database '" + name + "' (binder hashcode '" + dbWrapper.asBinder().hashCode() + "'): " + db);
return dbWrapper;
} finally {
mDatabaseLock.unlock();
}
}
private void preloadAllDatabases() {
List<String> databaseNames = doListAvailableDatabases();
try {
for (String databaseName : databaseNames) {
doOpenOrCreateDatabase(databaseName);
Log.i(TAG, "preloaded database '" + databaseName + "'");
}
} catch (Exception e) {
Log.e(TAG, "preloading databases aborted.", e);
}
}
@Override
public IBinder onBind(Intent intent) {
// Note: Android caches the object returned here, unless there is a
// variation in the intent.
Log.i(TAG, "Bound to some client. intent: " + intent);
return mBinder;
}
@Override
public void onCreate() {
super.onCreate();
// supporting infrastructure
mTrxManager = new TrxManager();
mReaper = new Reaper();
mReaper.start();
Properties properties = loadNeo4jProperties();
if (properties.containsKey(PROPERTY_PRELOAD_ALL) && Boolean.parseBoolean(properties.getProperty(PROPERTY_PRELOAD_ALL))) {
preloadAllDatabases();
}
Log.i(TAG, "Service created, neo4j version: " + Version.getVersion());
}
@Override
public void onDestroy() {
super.onDestroy();
mReaper.alive = false;
try {
mReaper.join();
} catch (InterruptedException e) {
Log.e(TAG, "Interrupted while join reaper", e);
}
Log.i(TAG, "Service destroyed");
}
private class Reaper extends Thread {
public Reaper() {
super("TrxReaper");
alive = true;
}
/* package */volatile boolean alive;
@Override
public void run() {
try {
while (alive) {
Thread.sleep(TRX_REAPER_INTERVAL_MILLIS);
mDatabaseLock.lock();
try {
mTrxManager.rollbackZombies(TRX_MAX_LIFETIME_MILLIS);
} finally {
mDatabaseLock.unlock();
}
}
} catch (InterruptedException ex) {
Log.i(TAG, "Reaper was interrupted, probably because service died.");
}
Log.i(TAG, "Reaper terminated");
}
}
}