package com.lmit.jenkins.android.addon; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.List; import android.content.ContentValues; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.util.Base64; import com.google.gson.GsonBuilder; import com.lmit.jenkins.android.logger.Logger; import com.lmit.jenkins.android.networking.DBAdapter; import com.lmit.jenkinscloud.commons.JenkinsCloudDataNode; import com.lmit.jenkinscloud.commons.JenkinsCloudNode; import com.lmit.jenkinscloud.commons.JenkinsCloudPage; public class LocalStorage { private static final Logger log = Logger.getInstance(); private static final boolean IMAGE_EVICTION_DISABLED = true; private static LocalStorage instance; public static LocalStorage getInstance() { if (instance == null) { instance = new LocalStorage(); } return instance; } private LocalStorage() { ; } public JenkinsCloudNode getNode(String path) { JenkinsCloudNode node = getNode(path, JenkinsCloudNode.class); if(node != null) { try { if(node.className == null) { node.className = JenkinsCloudDataNode.class.getName(); } return (JenkinsCloudNode) getNode(path, Class.forName(node.className)); } catch (ClassNotFoundException e) { log.error("Unsupported cached node " + node.className); return null; } } else { return null; } } public <T> T getNode(String path, Class<T> nodeClass) { T root = null; DBAdapter helper = new DBAdapter(); if(path.endsWith("/")) { path = path.substring(0, path.length()-1); } try { SQLiteDatabase db = tryGettingReadableDatabase(helper); if (db == null) { return null; } Cursor cursor = db.query(DBAdapter.JSON_TABLE_NAME, new String[] {"data, tag"}, "id=?", new String[] {path}, null, null, null); try { if (cursor.moveToFirst()) { String jsonText = cursor.getString(0); try { GsonBuilder gbuilder = new GsonBuilder(); gbuilder.disableHtmlEscaping(); root = gbuilder.create().fromJson(jsonText, nodeClass); } catch(Exception e) { log.error("Malformed JSON detected in input stream\n" + jsonText, e); return null; } if(JenkinsCloudNode.class.isAssignableFrom(nodeClass)) { JenkinsCloudNode jenkinsNode = (JenkinsCloudNode) root; jenkinsNode.setEtag(cursor.getString(1)); jenkinsNode.setCached(true); } } } finally { cursor.close(); helper.closeConnection(); } } catch (Exception e) { Logger.getInstance().error("Error during getNode", e); } log.debug("DISK-CACHE " + path + " "+ (root == null ? "MISS":"HIT - " + root.getClass())); return root; } public Bitmap getIcon(String path) { DBAdapter helper = new DBAdapter(); SQLiteDatabase db = tryGettingReadableDatabase(helper); if (db == null) { return null; } Cursor cursor = db.query(DBAdapter.ICONS_TABLE_NAME, new String[] {"data"}, "id=?", new String[] {path}, null, null, null); try { Bitmap result = null; if (cursor.moveToFirst()) { String base64 = cursor.getString(0); db.close(); byte[] decoded = Base64.decode(base64, Base64.DEFAULT); ByteArrayInputStream bis = new ByteArrayInputStream(decoded); result = BitmapFactory.decodeStream(bis); } return result; } finally { cursor.close(); helper.closeConnection(); } } private SQLiteDatabase tryGettingReadableDatabase( DBAdapter helper) { int retries = 5; SQLiteDatabase db = null; while (retries > 0) { try { db = helper.getConnection(); break; } catch (SQLiteException e) { retries--; try { Thread.sleep(50); } catch (InterruptedException e1) { ; } } } return db; } private SQLiteDatabase tryGettingWritableDatabase( DBAdapter helper) { int retries = 5; SQLiteDatabase db = null; while (retries > 0) { try { db = helper.getConnection(); break; } catch (SQLiteException e) { retries--; try { Thread.sleep(50); } catch (InterruptedException e1) { ; } } } return db; } public void putNode(String path, JenkinsCloudNode node) { if (node.isCached()) { return; } if (node.className == null) { node.className = node.getClass().getName(); } if (path.endsWith("/")) { path = path.substring(0, path.length() - 1); } cleanPath(path); DBAdapter helper = new DBAdapter(); try { SQLiteDatabase db = tryGettingWritableDatabase(helper); if (db == null) { return; } ContentValues values = new ContentValues(); if (!path.startsWith("/") && !path.startsWith("http")) { path = "/" + path; } values.put("id", path); values.put("data", node.toJson()); values.put("tag", node.getEtag()); values.put("pluginid", ""); db.beginTransaction(); try { long insertResult = db.insertWithOnConflict(DBAdapter.JSON_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE); Logger.getInstance().debug("InsertData result=" + insertResult); db.setTransactionSuccessful(); } finally { db.endTransaction(); helper.closeConnection(); } } catch (Exception e) { Logger.getInstance().error("Error during putNode", e); } log.debug("DISK-CACHE PUT " + path + " " + node.getClass()); } public void replaceNode(String path, JenkinsCloudNode node) { if(node.isCached()) { return; } putNode(path, node); } public void putIcon(String path, Bitmap icon) { DBAdapter helper = new DBAdapter(); try { SQLiteDatabase db = tryGettingWritableDatabase(helper); if (db == null) { return; } ByteArrayOutputStream stream = new ByteArrayOutputStream(); icon.compress(Bitmap.CompressFormat.PNG, 100, stream); byte[] byteArray = stream.toByteArray(); String base64 = Base64.encodeToString(byteArray, Base64.DEFAULT); ContentValues values = new ContentValues(); values.put("id", path); values.put("data", base64); values.put("pluginid", ""); db.beginTransaction(); try { long insertResult = db.insertWithOnConflict(DBAdapter.ICONS_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE); Logger.getInstance().debug("InsertData result=" + insertResult); db.setTransactionSuccessful(); } finally { db.endTransaction(); helper.closeConnection(); } } catch (Exception e) { Logger.getInstance().error("Error during putIcon", e); } } public void cleanAll() { DBAdapter helper = new DBAdapter(); SQLiteDatabase db = helper.getConnection(); try { db.delete(DBAdapter.JSON_TABLE_NAME, null, null); db.delete(DBAdapter.ICONS_TABLE_NAME, null, null); } finally { helper.closeConnection(); } } public void cleanPath(String path) { DBAdapter helper = new DBAdapter(); SQLiteDatabase db = helper.getConnection(); try { db.delete(DBAdapter.JSON_TABLE_NAME, "id=?", new String[] {path + "%"}); db.delete(DBAdapter.ICONS_TABLE_NAME, "id=?", new String[] {path + "%"}); } finally { helper.closeConnection(); } log.debug("DISK-CACHE EVICT " + path); } private SQLiteDatabase transactionDB; private DBAdapter transactionHelper; public void startTransaction() throws IOException { transactionHelper = new DBAdapter(); transactionDB = transactionHelper.swap(); transactionDB.beginTransaction(); transactionDB.setLockingEnabled(false); } // make sure to call this method after startTransaction in case of success public void commitTransaction() throws IOException { transactionDB.setTransactionSuccessful(); transactionDB.endTransaction(); transactionDB.close(); transactionHelper = new DBAdapter(); transactionHelper.swapBack(); transactionHelper = null; transactionDB = null; } // make sure to call this method after startTransaction in case of error public void abortTransaction() throws IOException { if(transactionDB == null) { return; } transactionHelper = new DBAdapter(); transactionDB.endTransaction(); transactionDB.close(); transactionHelper.swapBack(); transactionHelper = null; transactionDB = null; } public void evictNode(String path, JenkinsCloudNode result) { cleanPath(path); if (result instanceof JenkinsCloudDataNode) { evictImages((JenkinsCloudDataNode) result); } } public void evictImages(JenkinsCloudDataNode result) { cleanImage(result.getIcon()); List<JenkinsCloudDataNode> payload = result.getPayload(); if(payload == null) { return; } for (JenkinsCloudDataNode subNode : payload) { evictImages(subNode); } } private void cleanImage(String icon) { if(IMAGE_EVICTION_DISABLED) { return; } if(icon == null) { return; } DBAdapter helper = new DBAdapter(); SQLiteDatabase db = helper.getConnection(); try { String imageId = icon.substring(icon.lastIndexOf('/')+1); log.debug("Cleanup Image " + imageId); db.delete(DBAdapter.ICONS_TABLE_NAME, "id=?", new String[] { imageId }); ImageCache.evict(imageId); } finally { helper.closeConnection(); } } public JenkinsCloudPage getPage(String url) { return getNode(url, JenkinsCloudPage.class); } public boolean isCached(String path) { return getNode(path) != null; } }