content: /*
   * Licensed to the Apache Software Foundation (ASF) under one
   * or more contributor license agreements.  See the NOTICE file
   * distributed with this work for additional information
   * regarding copyright ownership.  The ASF licenses this file
   * to you 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.apache.weex.appfram.storage;
  
  import android.content.ContentValues;
  import android.content.Context;
  import android.database.Cursor;
  import android.database.sqlite.SQLiteDatabase;
  import android.database.sqlite.SQLiteFullException;
  import android.database.sqlite.SQLiteStatement;
  import android.support.annotation.Nullable;
  
  import org.apache.weex.common.WXThread;
  import org.apache.weex.utils.WXLogUtils;
  
  import java.util.ArrayList;
  import java.util.Date;
  import java.util.List;
  import java.util.Map;
  import java.util.concurrent.ExecutorService;
  import java.util.concurrent.Executors;
  
  public class DefaultWXStorage implements IWXStorageAdapter {
  
      private WXSQLiteOpenHelper mDatabaseSupplier;
  
      private ExecutorService mExecutorService;
  
      private void execute(@Nullable final Runnable runnable) {
          if (mExecutorService == null) {
              mExecutorService = Executors.newSingleThreadExecutor();
          }
  
          if(runnable != null && !mExecutorService.isShutdown() && !mExecutorService.isTerminated()) {
              mExecutorService.execute(WXThread.secure(runnable));
          }
      }
  
      public DefaultWXStorage(Context context) {
          this.mDatabaseSupplier = new WXSQLiteOpenHelper(context);
      }
  
  
      @Override
      public void setItem(final String key, final String value, final OnResultReceivedListener listener) {
          execute(new Runnable() {
              @Override
              public void run() {
                  Map<String, Object> data = StorageResultHandler.setItemResult(performSetItem(key, value, false, true));
                  if (listener == null) {
                      return;
                  }
                  listener.onReceived(data);
              }
          });
      }
  
      @Override
      public void getItem(final String key, final OnResultReceivedListener listener) {
          execute(new Runnable() {
              @Override
              public void run() {
                  Map<String, Object> data = StorageResultHandler.getItemResult(performGetItem(key));
                  if (listener == null) {
                      return;
                  }
                  listener.onReceived(data);
              }
          });
      }
  
      @Override
      public void removeItem(final String key, final OnResultReceivedListener listener) {
          execute(new Runnable() {
              @Override
              public void run() {
                  Map<String, Object> data = StorageResultHandler.removeItemResult(performRemoveItem(key));
                  if (listener == null) {
                      return;
                  }
                  listener.onReceived(data);
              }
          });
      }
  
      @Override
      public void length(final OnResultReceivedListener listener) {
          execute(new Runnable() {
              @Override
              public void run() {
                  Map<String, Object> data = StorageResultHandler.getLengthResult(performGetLength());
                  if (listener == null) {
                      return;
                  }
                  listener.onReceived(data);
              }
          });
      }
  
      @Override
      public void getAllKeys(final OnResultReceivedListener listener) {
          execute(new Runnable() {
              @Override
              public void run() {
                  Map<String, Object> data = StorageResultHandler.getAllkeysResult(performGetAllKeys());
                  if (listener == null) {
                      return;
                  }
                  listener.onReceived(data);
              }
          });
      }
  
      @Override
      public void setItemPersistent(final String key, final String value, final OnResultReceivedListener listener) {
          execute(new Runnable() {
              @Override
              public void run() {
                  Map<String, Object> data = StorageResultHandler.setItemResult(performSetItem(key, value, true, true));
                  if (listener == null) {
                      return;
                  }
                  listener.onReceived(data);
              }
          });
      }
  
      @Override
      public void close() {
          final ExecutorService needCloseService = mExecutorService;
          execute(new Runnable() {
              @Override
              public void run() {
                  try {
                      mDatabaseSupplier.closeDatabase();
                      if (needCloseService != null) {
                          needCloseService.shutdown();
                      }
                  } catch (Exception e) {
                      WXLogUtils.e(WXSQLiteOpenHelper.TAG_STORAGE, e.getMessage());
                  }
              }
          });
          mExecutorService = null;
      }
  
      private boolean performSetItem(String key, String value, boolean isPersistent, boolean allowRetryWhenFull) {
          SQLiteDatabase database = mDatabaseSupplier.getDatabase();
          if (database == null) {
              return false;
          }
  
          WXLogUtils.d(WXSQLiteOpenHelper.TAG_STORAGE, "set k-v to storage(key:" + key + ",value:" + value + ",isPersistent:" + isPersistent + ",allowRetry:" + allowRetryWhenFull + ")");
          String sql = "INSERT OR REPLACE INTO " + WXSQLiteOpenHelper.TABLE_STORAGE + " VALUES (?,?,?,?);";
          SQLiteStatement statement = null;
          String timeStamp = WXSQLiteOpenHelper.sDateFormatter.format(new Date());
          try {
              statement = database.compileStatement(sql);
              statement.clearBindings();
              statement.bindString(1, key);
              statement.bindString(2, value);
              statement.bindString(3, timeStamp);
              statement.bindLong(4, isPersistent ? 1 : 0);
              statement.execute();
              return true;
          } catch (Exception e) {
              WXLogUtils.e(WXSQLiteOpenHelper.TAG_STORAGE, "DefaultWXStorage occurred an exception when execute setItem :" + e.getMessage());
              if (e instanceof SQLiteFullException) {
                  if (allowRetryWhenFull && trimToSize()) {
                      //try again
                      //setItem/setItemPersistent method only allow try once when occurred a sqliteFullException.
                      WXLogUtils.d(WXSQLiteOpenHelper.TAG_STORAGE, "retry set k-v to storage(key:" + key + ",value:" + value + ")");
                      return performSetItem(key, value, isPersistent, false);
                  }
              }
  
              return false;
          } finally {
              if(statement != null) {
                  statement.close();
              }
          }
      }
  
      /**
       * remove 10% of total record(at most) ordered by timestamp.
       * */
      private boolean trimToSize() {
          SQLiteDatabase database = mDatabaseSupplier.getDatabase();
          if (database == null) {
              return false;
          }
  
          List<String> toEvict = new ArrayList<>();
          int num = 0;
  
          Cursor c = database.query(WXSQLiteOpenHelper.TABLE_STORAGE, new String[]{WXSQLiteOpenHelper.COLUMN_KEY, WXSQLiteOpenHelper.COLUMN_PERSISTENT}, null, null, null, null, WXSQLiteOpenHelper.COLUMN_TIMESTAMP + " ASC");
          try {
              int evictSize = c.getCount() / 10;
              while (c.moveToNext()) {
                  String key = c.getString(c.getColumnIndex(WXSQLiteOpenHelper.COLUMN_KEY));
                  boolean persistent = c.getInt(c.getColumnIndex(WXSQLiteOpenHelper.COLUMN_PERSISTENT)) == 1;
                  if (!persistent && key != null) {
                      num++;
                      toEvict.add(key);
                      if (num == evictSize) {
                          break;
                      }
                  }
              }
          } catch (Exception e) {
              WXLogUtils.e(WXSQLiteOpenHelper.TAG_STORAGE, "DefaultWXStorage occurred an exception when execute trimToSize:" + e.getMessage());
          } finally {
              c.close();
          }
  
          if (num <= 0) {
              return false;
          }
  
          for (String key : toEvict) {
              performRemoveItem(key);
          }
          WXLogUtils.e(WXSQLiteOpenHelper.TAG_STORAGE, "remove " + num + " items by lru");
          return true;
      }
  
      private String performGetItem(String key) {
          SQLiteDatabase database = mDatabaseSupplier.getDatabase();
          if (database == null) {
              return null;
          }
  
          Cursor c = database.query(WXSQLiteOpenHelper.TABLE_STORAGE,
                  new String[]{WXSQLiteOpenHelper.COLUMN_VALUE},
                  WXSQLiteOpenHelper.COLUMN_KEY + "=?",
                  new String[]{key},
                  null, null, null);
          try {
              if (c.moveToNext()) {
                  ContentValues values = new ContentValues();
                  //update timestamp
                  values.put(WXSQLiteOpenHelper.COLUMN_TIMESTAMP, WXSQLiteOpenHelper.sDateFormatter.format(new Date()));
                  int updateResult = mDatabaseSupplier.getDatabase().update(WXSQLiteOpenHelper.TABLE_STORAGE, values, WXSQLiteOpenHelper.COLUMN_KEY + "= ?", new String[]{key});
  
                  WXLogUtils.d(WXSQLiteOpenHelper.TAG_STORAGE, "update timestamp " + (updateResult == 1 ? "success" : "failed") + " for operation [getItem(key = " + key + ")]");
                  return c.getString(c.getColumnIndex(WXSQLiteOpenHelper.COLUMN_VALUE));
              } else {
                  return null;
              }
          } catch (Exception e) {
              WXLogUtils.e(WXSQLiteOpenHelper.TAG_STORAGE, "DefaultWXStorage occurred an exception when execute getItem:" + e.getMessage());
              return null;
          } finally {
              c.close();
          }
      }
  
      private boolean performRemoveItem(String key) {
          SQLiteDatabase database = mDatabaseSupplier.getDatabase();
          if (database == null) {
              return false;
          }
  
          int count = 0;
          try {
              count = database.delete(WXSQLiteOpenHelper.TABLE_STORAGE,
                      WXSQLiteOpenHelper.COLUMN_KEY + "=?",
                      new String[]{key});
          } catch (Exception e) {
              WXLogUtils.e(WXSQLiteOpenHelper.TAG_STORAGE, "DefaultWXStorage occurred an exception when execute removeItem:" + e.getMessage());
              return false;
          }
          return count == 1;
      }
  
      private long performGetLength() {
          SQLiteDatabase database = mDatabaseSupplier.getDatabase();
          if (database == null) {
              return 0;
          }
  
          String sql = "SELECT count(" + WXSQLiteOpenHelper.COLUMN_KEY + ") FROM " + WXSQLiteOpenHelper.TABLE_STORAGE;
          SQLiteStatement statement = null;
          try {
              statement = database.compileStatement(sql);
              return statement.simpleQueryForLong();
          } catch (Exception e) {
              WXLogUtils.e(WXSQLiteOpenHelper.TAG_STORAGE, "DefaultWXStorage occurred an exception when execute getLength:" + e.getMessage());
              return 0;
          } finally {
              if(statement != null) {
                  statement.close();
              }
          }
      }
  
      private List<String> performGetAllKeys() {
          SQLiteDatabase database = mDatabaseSupplier.getDatabase();
          if (database == null) {
              return null;
          }
  
          List<String> result = new ArrayList<>();
          Cursor c = database.query(WXSQLiteOpenHelper.TABLE_STORAGE, new String[]{WXSQLiteOpenHelper.COLUMN_KEY}, null, null, null, null, null);
          try {
              while (c.moveToNext()) {
                  result.add(c.getString(c.getColumnIndex(WXSQLiteOpenHelper.COLUMN_KEY)));
              }
              return result;
          } catch (Exception e) {
              WXLogUtils.e(WXSQLiteOpenHelper.TAG_STORAGE, "DefaultWXStorage occurred an exception when execute getAllKeys:" + e.getMessage());
              return result;
          } finally {
              c.close();
          }
      }
  
  }
  