Java tutorial
/* * Copyright (C) 2013 University of Washington * * 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.opendatakit.common.android.provider.impl; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import org.apache.commons.io.FileUtils; import org.opendatakit.common.android.logic.FormInfo; import org.opendatakit.common.android.provider.FormsColumns; import org.opendatakit.common.android.utilities.ODKFileUtils; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteException; import android.net.Uri; import android.util.Log; public final class FormsDiscoveryRunnable implements Runnable { private static String t = "FormsDiscoveryRunnable"; private static int counter = 0; private static final Map<String, Integer> appInstanceCounterStart = new HashMap<String, Integer>(); private int instanceCounter; private Context context; private Uri formsProviderContentUri; private String appName; private boolean isFramework = false; private String tableDirName; private String formDirName; private static synchronized final int getNextCount() { int newCount = ++counter; return newCount; } public FormsDiscoveryRunnable(FormsProviderImpl impl, String appName, String tableDirName, String formDirName) { context = impl.getContext(); formsProviderContentUri = Uri.parse("content://" + impl.getFormsAuthority()); this.appName = appName; this.tableDirName = tableDirName; this.formDirName = formDirName; this.isFramework = false; this.instanceCounter = getNextCount(); } public FormsDiscoveryRunnable(FormsProviderImpl impl, String appName) { context = impl.getContext(); formsProviderContentUri = Uri.parse("content://" + impl.getFormsAuthority()); this.appName = appName; this.tableDirName = null; this.formDirName = null; this.isFramework = true; this.instanceCounter = getNextCount(); } /** * Remove definitions from the Forms database that are no longer present on * disk. */ private final void removeStaleFormInfo() { Log.i(t, "[" + instanceCounter + "] removeStaleFormInfo " + appName + " begin"); ArrayList<Uri> badEntries = new ArrayList<Uri>(); Cursor c = null; try { c = context.getContentResolver().query(Uri.withAppendedPath(formsProviderContentUri, appName), null, null, null, null); if (c == null) { Log.w(t, "[" + instanceCounter + "] removeStaleFormInfo " + appName + " null cursor returned from query."); return; } if (c.moveToFirst()) { do { String id = c.getString(c.getColumnIndex(FormsColumns.FORM_ID)); Uri otherUri = Uri.withAppendedPath(Uri.withAppendedPath(formsProviderContentUri, appName), id); int appRelativeFormMediaPathIdx = c.getColumnIndex(FormsColumns.APP_RELATIVE_FORM_MEDIA_PATH); if (appRelativeFormMediaPathIdx == -1) { throw new IllegalStateException("Column " + FormsColumns.APP_RELATIVE_FORM_MEDIA_PATH + " missing from database table. Incompatible versions?"); } String appRelativeFormMediaPath = c.getString(appRelativeFormMediaPathIdx); File f = ODKFileUtils.asAppFile(appName, appRelativeFormMediaPath); if (!f.exists() || !f.isDirectory()) { // the form definition does not exist badEntries.add(otherUri); } } while (c.moveToNext()); } } catch (Exception e) { Log.e(t, "[" + instanceCounter + "] removeStaleFormInfo " + appName + " exception: " + e.toString()); e.printStackTrace(); } finally { if (c != null && !c.isClosed()) { c.close(); } } // delete the other entries (and directories) for (Uri badUri : badEntries) { Log.i(t, "[" + instanceCounter + "] removeStaleFormInfo: " + appName + " deleting: " + badUri.toString()); try { context.getContentResolver().delete(badUri, null, null); } catch (Exception e) { Log.e(t, "[" + instanceCounter + "] removeStaleFormInfo " + appName + " exception: " + e.toString()); e.printStackTrace(); // and continue -- don't throw an error } } Log.i(t, "[" + instanceCounter + "] removeStaleFormInfo " + appName + " end"); } /** * Construct a directory name that is unused in the stale path and move * mediaPath there. * * @param mediaPath * @param baseStaleMediaPath * -- the stale directory corresponding to the mediaPath container * @return the directory within the stale directory that the mediaPath was * renamed to. * @throws IOException */ private final File moveToStaleDirectory(File mediaPath, String baseStaleMediaPath) throws IOException { // we have a 'framework' form in the forms directory. // Move it to the stale directory. // Delete all records referring to this directory. int i = 0; File tempMediaPath = new File(baseStaleMediaPath + mediaPath.getName() + "_" + Integer.toString(i)); while (tempMediaPath.exists()) { ++i; tempMediaPath = new File(baseStaleMediaPath + mediaPath.getName() + "_" + Integer.toString(i)); } FileUtils.moveDirectory(mediaPath, tempMediaPath); return tempMediaPath; } /** * Scan the given formDir and update the Forms database. If it is the * formsFolder, then any 'framework' forms should be forbidden. If it is not * the * formsFolder, only 'framework' forms should be allowed * * @param mediaPath * -- full formDir * @param isFormsFolder * @param baseStaleMediaPath * -- path prefix to the stale forms/framework directory. */ private final void updateFormDir(File formDir, boolean isFormsFolder, String baseStaleMediaPath) { String formDirectoryPath = formDir.getAbsolutePath(); Log.i(t, "[" + instanceCounter + "] updateFormDir: " + formDirectoryPath); boolean needUpdate = true; FormInfo fi = null; Uri uri = null; Cursor c = null; try { File formDef = new File(formDir, ODKFileUtils.FORMDEF_JSON_FILENAME); String selection = FormsColumns.APP_RELATIVE_FORM_MEDIA_PATH + "=?"; String[] selectionArgs = { ODKFileUtils.asRelativePath(appName, formDir) }; c = context.getContentResolver().query(Uri.withAppendedPath(formsProviderContentUri, appName), null, selection, selectionArgs, null); if (c == null) { Log.w(t, "[" + instanceCounter + "] updateFormDir: " + formDirectoryPath + " null cursor -- cannot update!"); return; } if (c.getCount() > 1) { c.close(); Log.w(t, "[" + instanceCounter + "] updateFormDir: " + formDirectoryPath + " multiple records from cursor -- delete all and restore!"); // we have multiple records for this one directory. // Rename the directory. Delete the records, and move the // directory back. File tempMediaPath = moveToStaleDirectory(formDir, baseStaleMediaPath); context.getContentResolver().delete(Uri.withAppendedPath(formsProviderContentUri, appName), selection, selectionArgs); FileUtils.moveDirectory(tempMediaPath, formDir); // we don't know which of the above records was correct, so // reparse this to get ground truth... fi = new FormInfo(context, appName, formDef); } else if (c.getCount() == 1) { c.moveToFirst(); String id = c.getString(c.getColumnIndex(FormsColumns.FORM_ID)); uri = Uri.withAppendedPath(Uri.withAppendedPath(formsProviderContentUri, appName), id); Long lastModificationDate = c.getLong(c.getColumnIndex(FormsColumns.DATE)); Long formDefModified = ODKFileUtils.getMostRecentlyModifiedDate(formDir); if (lastModificationDate.compareTo(formDefModified) == 0) { Log.i(t, "[" + instanceCounter + "] updateFormDir: " + formDirectoryPath + " formDef unchanged"); fi = new FormInfo(appName, c, false); needUpdate = false; } else { Log.i(t, "[" + instanceCounter + "] updateFormDir: " + formDirectoryPath + " formDef revised"); fi = new FormInfo(context, appName, formDef); needUpdate = true; } } else if (c.getCount() == 0) { // it should be new, try to parse it... fi = new FormInfo(context, appName, formDef); } // Enforce that a formId == FormsColumns.COMMON_BASE_FORM_ID can only be // in the Framework directory // and that no other formIds can be in that directory. If this is not the // case, ensure that // this record is moved to the stale directory. if (fi.formId.equals(FormsColumns.COMMON_BASE_FORM_ID)) { if (isFormsFolder) { // we have a 'framework' form in the forms directory. // Move it to the stale directory. // Delete all records referring to this directory. moveToStaleDirectory(formDir, baseStaleMediaPath); context.getContentResolver().delete(Uri.withAppendedPath(formsProviderContentUri, appName), selection, selectionArgs); return; } } else { if (!isFormsFolder) { // we have a non-'framework' form in the framework directory. // Move it to the stale directory. // Delete all records referring to this directory. moveToStaleDirectory(formDir, baseStaleMediaPath); context.getContentResolver().delete(Uri.withAppendedPath(formsProviderContentUri, appName), selection, selectionArgs); return; } } } catch (SQLiteException e) { e.printStackTrace(); Log.e(t, "[" + instanceCounter + "] updateFormDir: " + formDirectoryPath + " exception: " + e.toString()); return; } catch (IOException e) { e.printStackTrace(); Log.e(t, "[" + instanceCounter + "] updateFormDir: " + formDirectoryPath + " exception: " + e.toString()); return; } catch (IllegalArgumentException e) { e.printStackTrace(); Log.e(t, "[" + instanceCounter + "] updateFormDir: " + formDirectoryPath + " exception: " + e.toString()); try { FileUtils.deleteDirectory(formDir); Log.i(t, "[" + instanceCounter + "] updateFormDir: " + formDirectoryPath + " Removing -- unable to parse formDef file: " + e.toString()); } catch (IOException e1) { e1.printStackTrace(); Log.i(t, "[" + instanceCounter + "] updateFormDir: " + formDirectoryPath + " Removing -- unable to delete form directory: " + formDir.getName() + " error: " + e.toString()); } return; } finally { if (c != null && !c.isClosed()) { c.close(); } } // Delete any entries matching this FORM_ID, but not the same directory and // which have a version that is equal to or older than this version. String selection; String[] selectionArgs; if (fi.formVersion == null) { selection = FormsColumns.APP_RELATIVE_FORM_MEDIA_PATH + "!=? AND " + FormsColumns.FORM_ID + "=? AND " + FormsColumns.FORM_VERSION + " IS NULL"; String[] temp = { ODKFileUtils.asRelativePath(appName, formDir), fi.formId }; selectionArgs = temp; } else { selection = FormsColumns.APP_RELATIVE_FORM_MEDIA_PATH + "!=? AND " + FormsColumns.FORM_ID + "=? AND " + "( " + FormsColumns.FORM_VERSION + " IS NULL" + " OR " + FormsColumns.FORM_VERSION + " <=?" + " )"; String[] temp = { ODKFileUtils.asRelativePath(appName, formDir), fi.formId, fi.formVersion }; selectionArgs = temp; } try { context.getContentResolver().delete(Uri.withAppendedPath(formsProviderContentUri, appName), selection, selectionArgs); } catch (SQLiteException e) { e.printStackTrace(); Log.e(t, "[" + instanceCounter + "] updateFormDir: " + formDirectoryPath + " exception: " + e.toString()); return; } catch (Exception e) { e.printStackTrace(); Log.e(t, "[" + instanceCounter + "] updateFormDir: " + formDirectoryPath + " exception: " + e.toString()); return; } // See if we have any newer versions already present... if (fi.formVersion == null) { selection = FormsColumns.APP_RELATIVE_FORM_MEDIA_PATH + "!=? AND " + FormsColumns.FORM_ID + "=? AND " + FormsColumns.FORM_VERSION + " IS NOT NULL"; String[] temp = { ODKFileUtils.asRelativePath(appName, formDir), fi.formId }; selectionArgs = temp; } else { selection = FormsColumns.APP_RELATIVE_FORM_MEDIA_PATH + "!=? AND " + FormsColumns.FORM_ID + "=? AND " + FormsColumns.FORM_VERSION + " >?"; String[] temp = { ODKFileUtils.asRelativePath(appName, formDir), fi.formId, fi.formVersion }; selectionArgs = temp; } try { Uri uriApp = Uri.withAppendedPath(formsProviderContentUri, appName); c = context.getContentResolver().query(uriApp, null, selection, selectionArgs, null); if (c == null) { Log.w(t, "[" + instanceCounter + "] updateFormDir: " + uriApp.toString() + " null cursor -- cannot update!"); return; } if (c.moveToFirst()) { // the directory we are processing is stale -- move it to stale // directory moveToStaleDirectory(formDir, baseStaleMediaPath); return; } } catch (SQLiteException e) { e.printStackTrace(); Log.e(t, "[" + instanceCounter + "] updateFormDir: " + formDirectoryPath + " exception: " + e.toString()); return; } catch (IOException e) { e.printStackTrace(); Log.e(t, "[" + instanceCounter + "] updateFormDir: " + formDirectoryPath + " exception: " + e.toString()); return; } finally { if (c != null && !c.isClosed()) { c.close(); } } if (!needUpdate) { // no change... return; } try { // Now insert or update the record... ContentValues v = new ContentValues(); String[] values = fi.asRowValues(FormsColumns.formsDataColumnNames); for (int i = 0; i < values.length; ++i) { v.put(FormsColumns.formsDataColumnNames[i], values[i]); } if (uri != null) { int count = context.getContentResolver().update(uri, v, null, null); Log.i(t, "[" + instanceCounter + "] updateFormDir: " + formDirectoryPath + " " + count + " records successfully updated"); } else { context.getContentResolver().insert(Uri.withAppendedPath(formsProviderContentUri, appName), v); Log.i(t, "[" + instanceCounter + "] updateFormDir: " + formDirectoryPath + " one record successfully inserted"); } } catch (SQLiteException ex) { ex.printStackTrace(); Log.e(t, "[" + instanceCounter + "] updateFormDir: " + formDirectoryPath + " exception: " + ex.toString()); return; } } /** * Scan for new forms directories in both the forms and framework areas and * add them to Forms database. */ private final void updateFormInfo() { Log.i(t, "[" + instanceCounter + "] updateFormInfo: " + appName + " begin"); if (!isFramework) { if (tableDirName != null && formDirName != null) { // specifically target this form... File formDir = new File(ODKFileUtils.getFormFolder(appName, tableDirName, formDirName)); Log.i(t, "[" + instanceCounter + "] updateFormInfo: form: " + formDir.getAbsolutePath()); updateFormDir(formDir, true, ODKFileUtils.getStaleFormsFolder(appName) + File.separator); } } else { File frameworkDir = new File(ODKFileUtils.getFrameworkFolder(appName)); Log.i(t, "[" + instanceCounter + "] updateFormInfo: framework: " + frameworkDir.getAbsolutePath()); updateFormDir(frameworkDir, false, ODKFileUtils.getStaleFrameworkFolder(appName) + File.separator); } Log.i(t, "[" + instanceCounter + "] updateFormInfo: " + appName + " end"); } @Override public void run() { // ensure that there is one and only one scan happening at any one time // should be handled by ExecutorService, but just in case... synchronized (appInstanceCounterStart) { Integer ic = appInstanceCounterStart.get(appName); if (ic == null || ic < instanceCounter) { // this task was created after the start of the last task that searched // and updated the appName tree. So we should execute it. int startCounter = counter; Log.i(t, "[" + instanceCounter + "] doInBackground removeStaleFormInfo() begins! " + appName + " baseCounter: " + ic + " startCounter: " + startCounter); try { removeStaleFormInfo(); } finally { Log.i(t, "[" + instanceCounter + "] doInBackground removeStaleFormInfo() ends! " + appName); appInstanceCounterStart.put(appName, startCounter); } } else { Log.i(t, "[" + instanceCounter + "] doInBackground removeStaleFormInfo() skipped! " + appName + " baseCounter: " + ic); } Log.i(t, "[" + instanceCounter + "] doInBackground updateFormInfo() begins! " + appName + " baseCounter: " + ic); try { updateFormInfo(); } finally { Log.i(t, "[" + instanceCounter + "] doInBackground updateFormInfo() ends! " + appName + " baseCounter: " + ic); } } } }