Java tutorial
/* * Copyright (C) 2012 The Android Open Source Project * Copyright (C) 2009-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.utilities; import android.os.Build; import android.os.Environment; import android.support.annotation.CheckResult; import android.util.Log; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.io.Charsets; import org.apache.commons.io.FileUtils; import org.apache.commons.io.filefilter.IOFileFilter; import org.apache.commons.lang3.CharEncoding; import org.opendatakit.logging.WebLogger; import org.opendatakit.provider.FormsColumns; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import java.io.*; import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Collection; import java.util.regex.Pattern; /** * Static methods used for common file operations. * * @author Carl Hartung (carlhartung@gmail.com) */ public final class ODKFileUtils { public static final ObjectMapper mapper = new ObjectMapper(); // 2nd level -- directories // special filename public static final String FORMDEF_JSON_FILENAME = "formDef.json"; // 1st level -- appId private static final String MD5_COLON_PREFIX = "md5:"; // Used for logging private static final String TAG = ODKFileUtils.class.getSimpleName(); // Default app name when unspecified private static final String ODK_DEFAULT_APP_NAME = "default"; // base path private static final String ODK_FOLDER_NAME = "opendatakit"; // 3rd level -- directories // under config private static final String CONFIG_FOLDER_NAME = "config"; // under config and data private static final String DATA_FOLDER_NAME = "data"; // under data private static final String OUTPUT_FOLDER_NAME = "output"; private static final String SYSTEM_FOLDER_NAME = "system"; private static final String PERMANENT_FOLDER_NAME = "permanent"; // under output and config/assets private static final String ASSETS_FOLDER_NAME = "assets"; // under output private static final String TABLES_FOLDER_NAME = "tables"; private static final String WEB_DB_FOLDER_NAME = "webDb"; // under system private static final String GEO_CACHE_FOLDER_NAME = "geoCache"; private static final String APP_CACHE_FOLDER_NAME = "appCache"; // 4th level // under the config/tables directory... private static final String CSV_FOLDER_NAME = "csv"; private static final String LOGGING_FOLDER_NAME = "logging"; /** * The name of the folder where the debug objects are written. */ private static final String DEBUG_FOLDER_NAME = "debug"; private static final String STALE_TABLES_FOLDER_NAME = "tables.deleting"; /** * Miscellaneous well-known file names */ private static final String PENDING_TABLES_FOLDER_NAME = "tables.pending"; private static final String FORMS_FOLDER_NAME = "forms"; // under the data/tables directory... // and under the output/csv/tableId.qual/ and config/assets/csv/tableId.qual/ directories private static final String INSTANCES_FOLDER_NAME = "instances"; // under data/webDb private static final String DATABASE_NAME = "sqlite.db"; // under data/webDb private static final String DATABASE_LOCK_FILE_NAME = "db.lock"; /** * Filename holding app-wide definitions (just translations for now). */ private static final String COMMON_DEFINITIONS_JS = "commonDefinitions.js"; /** * Filename for the top-level configuration file (in assets) */ private static final String ODK_TABLES_INIT_FILENAME = "tables.init"; /** * Filename for the ODK Tables home screen (in assets) */ private static final String ODK_TABLES_HOME_SCREEN_FILE_NAME = "index.html"; /** * Filename of the config/tables/tableId/properties.csv file * that holds all kvs properties for this tableId. */ private static final String PROPERTIES_CSV = "properties.csv"; /** * Filename of the config/tables/tableId/definition.csv file * that holds the table schema for this tableId. */ private static final String DEFINITION_CSV = "definition.csv"; /** * Filename holding table-specific definitions (just translations for now). */ private static final String TABLE_SPECIFIC_DEFINITIONS_JS = "tableSpecificDefinitions.js"; private static final Pattern VALID_INSTANCE_ID_FOLDER_NAME_PATTERN = Pattern.compile("(\\p{P}|\\p{Z})"); private static final Pattern VALID_FOLDER_PATTERN = Pattern .compile("^\\p{L}\\p{M}*(\\p{L}\\p{M}*|\\p{Nd}|_)+$"); /** * Do not instantiate this class */ private ODKFileUtils() { } /** * uri on web server begins with appName. * construct the full file. * <p> * return null if the file does not exist or is not an * accessible uri thru the WebServer. (i.e., the getAsFile() API). * used in services/fi.iki.elonen.SimpleWebServer */ @SuppressWarnings("unused") public static File fileFromUriOnWebServer(String uri) { String appName; String uriFragment; int idxAppName = uri.indexOf('/'); if (idxAppName == -1) { return null; } if (idxAppName == 0) { idxAppName = uri.indexOf('/', idxAppName + 1); if (idxAppName == -1) { return null; } appName = uri.substring(1, idxAppName); uriFragment = uri.substring(idxAppName + 1); } else { appName = uri.substring(0, idxAppName); uriFragment = uri.substring(idxAppName + 1); } File filename = getAsFile(appName, uriFragment); if (!filename.exists()) { return null; } String[] parts = uriFragment.split("/"); if (parts.length > 1) { switch (parts[0]) { case CONFIG_FOLDER_NAME: case SYSTEM_FOLDER_NAME: case PERMANENT_FOLDER_NAME: return filename; case DATA_FOLDER_NAME: if (parts.length > 2 && parts[1].equals(TABLES_FOLDER_NAME)) { return filename; } break; default: // ignore; } } return null; } /** * Used in AndroidOdkConnection and OdkConnectionFactoryAbstractClass, both in services.database * * @return sqlite.db */ @SuppressWarnings("unused") public static String getNameOfSQLiteDatabase() { return DATABASE_NAME; } /** * Used in services.database.OdkConnectionFactoryAbstractClass * * @return db.lock */ @SuppressWarnings("unused") public static String getNameOfSQLiteDatabaseLockFile() { return DATABASE_LOCK_FILE_NAME; } public static void verifyExternalStorageAvailability() { String cardstatus = Environment.getExternalStorageState(); if (cardstatus.equals(Environment.MEDIA_REMOVED) || cardstatus.equals(Environment.MEDIA_UNMOUNTABLE) || cardstatus.equals(Environment.MEDIA_UNMOUNTED) || cardstatus.equals(Environment.MEDIA_MOUNTED_READ_ONLY) || cardstatus.equals(Environment.MEDIA_SHARED)) { throw new RuntimeException("ODK reports :: SDCard error: " + Environment.getExternalStorageState()); } } /** * Used all over the place * * @return /sdcard/opendatakit */ @SuppressWarnings("WeakerAccess") public static String getOdkFolder() { return Environment.getExternalStorageDirectory() + File.separator + ODK_FOLDER_NAME; } /** * Used in AggregateSynchronizer and ProcessManifestContentAndFileChanges * * @param path the path of the folder to create * @return whether it was created successfully or not */ @CheckResult @SuppressWarnings("unused") public static boolean createFolder(String path) { File dir = new File(path); //noinspection SimplifiableIfStatement if (dir.exists()) { return true; } return dir.mkdirs(); } /** * Used all over the place * * @return default */ @SuppressWarnings("unused") public static String getOdkDefaultAppName() { return ODK_DEFAULT_APP_NAME; } /** * Used in AndroidShortcuts * * @return a list of all the folders in /sdcard/opendatakit */ @SuppressWarnings("unused") public static File[] getAppFolders() { return new File(getOdkFolder()).listFiles(new FileFilter() { @Override public boolean accept(File pathname) { return pathname.isDirectory(); } }); } public static void assertDirectoryStructure(String appName) { String[] dirs = { getAppFolder(appName), getConfigFolder(appName), getDataFolder(appName), getOutputFolder(appName), getSystemFolder(appName), getPermanentFolder(appName), // under Config getAssetsFolder(appName), getTablesFolder(appName), // under Data getAppCacheFolder(appName), getGeoCacheFolder(appName), getWebDbFolder(appName), getTableDataFolder(appName), // under Output getLoggingFolder(appName), getOutputCsvFolder(appName), getTablesDebugObjectFolder(appName), // under System getPendingDeletionTablesFolder(appName), getPendingInsertionTablesFolder(appName) }; for (String dirName : dirs) { File dir = new File(dirName); File badDir = new File(dir.getParent(), dir.getName() + ".bad"); if (badDir.exists()) { if (!badDir.delete()) { throw new RuntimeException("Cannot remove bad directory " + badDir); } } if (!dir.exists()) { if (!dir.mkdirs()) { throw new RuntimeException("Cannot create directory: " + dirName); } } else { if (!dir.isDirectory()) { File retryDir = new File(dir.getParent(), dir.getName()); if (dir.renameTo(badDir)) { if (!retryDir.mkdirs()) { throw new RuntimeException("Cannot create directory: " + dirName); } } else { throw new RuntimeException(dirName + " exists, but is not a directory"); } } } } // and create an empty .nomedia file File nomedia = new File(getAppFolder(appName), ".nomedia"); try { if (!nomedia.exists() && !nomedia.createNewFile()) { throw new IOException(); } } catch (IOException ex) { // DO NOT CHANGE THIS TO WebLogger // WebLogger.getLogger calls assertDirectoryStructure, and we will end up in an infinite // loop of failures Log.e(TAG, "Cannot create .nomedia in app directory: " + ex); // Also, don't throw an exception or the tests will fail } } /** * This routine clears all the marker files used by the various tools to * avoid re-running the initialization task. * <p> * Used in services.preferences.activities.ClearAppPropertiesActivity * * @param appName the app name */ @SuppressWarnings("unused") public static void clearConfiguredToolFiles(String appName) { File dataDir = new File(ODKFileUtils.getDataFolder(appName)); File[] filesToDelete = dataDir.listFiles(new FileFilter() { @Override public boolean accept(File pathname) { if (pathname.isDirectory()) { return false; } String name = pathname.getName(); int idx = name.lastIndexOf('.'); if (idx == -1) { return false; } String type = name.substring(idx + 1); return "version".equals(type); } }); for (File f : filesToDelete) { if (!f.delete()) { throw new RuntimeException("Could not delete file " + f.getAbsolutePath()); } } } public static void assertConfiguredToolApp(String appName, String toolName, String apkVersion) { writeConfiguredOdkAppVersion(appName, toolName + ".version", apkVersion); } /** * TODO this is almost identical to checkOdkAppVersion * * @param appName the app name * @param odkAppVersionFile the file that contains the installed version * @param apkVersion the version to overwrite odkAppVerisonFile with */ private static void writeConfiguredOdkAppVersion(String appName, String odkAppVersionFile, String apkVersion) { File versionFile = new File(getDataFolder(appName), odkAppVersionFile); if (!versionFile.exists()) { if (!versionFile.getParentFile().mkdirs()) { //throw new RuntimeException("Failed mkdirs on " + versionFile.getPath()); WebLogger.getLogger(appName).e(TAG, "Failed mkdirs on " + versionFile.getParentFile().getPath()); } } FileOutputStream fs = null; OutputStreamWriter w = null; BufferedWriter bw = null; try { fs = new FileOutputStream(versionFile, false); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { w = new OutputStreamWriter(fs, StandardCharsets.UTF_8); } else { //noinspection deprecation w = new OutputStreamWriter(fs, Charsets.UTF_8); } bw = new BufferedWriter(w); bw.write(apkVersion); bw.write("\n"); } catch (IOException e) { WebLogger.getLogger(appName).printStackTrace(e); } finally { if (bw != null) { try { bw.flush(); bw.close(); } catch (IOException e) { WebLogger.getLogger(appName).printStackTrace(e); } } if (w != null) { try { w.close(); } catch (IOException e) { WebLogger.getLogger(appName).printStackTrace(e); } } if (fs != null) { try { fs.close(); } catch (IOException e) { WebLogger.getLogger(appName).printStackTrace(e); } } } } /** * Used in InitializationUtil * * @param appName the app name * @param toolName the name of the app we want to check the version of * @param apkVersion the version we want to match * @return whether the passed apk version matches the version in the file */ public static boolean isConfiguredToolApp(String appName, String toolName, String apkVersion) { return checkOdkAppVersion(appName, toolName + ".version", apkVersion); } /** * TODO this is almost identical to writeConfiguredOdkAppVersion * * @param appName the app name * @param odkAppVersionFile the file that contains the installed app version * @param apkVersion the version to match against * @return whether the passed apk version matches the version in the file */ private static boolean checkOdkAppVersion(String appName, String odkAppVersionFile, String apkVersion) { File versionFile = new File(getDataFolder(appName), odkAppVersionFile); if (!versionFile.exists()) { return false; } String versionLine = null; FileInputStream fs = null; InputStreamReader r = null; BufferedReader br = null; try { fs = new FileInputStream(versionFile); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { r = new InputStreamReader(fs, StandardCharsets.UTF_8); } else { //noinspection deprecation r = new InputStreamReader(fs, Charsets.UTF_8); } br = new BufferedReader(r); versionLine = br.readLine(); } catch (IOException e) { WebLogger.getLogger(appName).printStackTrace(e); return false; } finally { if (br != null) { try { br.close(); } catch (IOException e) { WebLogger.getLogger(appName).printStackTrace(e); } } if (r != null) { try { r.close(); } catch (IOException e) { WebLogger.getLogger(appName).printStackTrace(e); } } try { if (fs != null) { fs.close(); } } catch (IOException e) { WebLogger.getLogger(appName).printStackTrace(e); } } String[] versionRange = versionLine.split(";"); for (String version : versionRange) { if (version.trim().equals(apkVersion)) { return true; } } return false; } private static File fromAppPath(String appPath) { String[] terms = appPath.split(File.separator); if (terms.length < 1) { return null; } return new File(new File(getOdkFolder()), appPath); } public static String getAppFolder(String appName) { return getOdkFolder() + File.separator + appName; } // 1st level folders /** * Used in ScanUtils, ProcessManifestContentAndFileChanges * * @param appName the app name * @return :app_name/config */ @SuppressWarnings("WeakerAccess") public static String getConfigFolder(String appName) { return getAppFolder(appName) + File.separator + CONFIG_FOLDER_NAME; } /** * Used in GainPropertiesLock and PropertiesSingleton * * @param appName the app name * @return :app_name/data */ public static String getDataFolder(String appName) { return getAppFolder(appName) + File.separator + DATA_FOLDER_NAME; } /** * Used only in NanoHTTPD * * @param appName the app name * @return :app_name/output */ @SuppressWarnings("WeakerAccess") public static String getOutputFolder(String appName) { return getAppFolder(appName) + File.separator + OUTPUT_FOLDER_NAME; } /** * Used in MainMenuActivity and scan.utils.ScanUtils * * @param appName the app name * @return :app_name/system */ @SuppressWarnings("WeakerAccess") public static String getSystemFolder(String appName) { return getAppFolder(appName) + File.separator + SYSTEM_FOLDER_NAME; } private static String getPermanentFolder(String appName) { return getAppFolder(appName) + File.separator + PERMANENT_FOLDER_NAME; } ////////////////////////////////////////////////////////// // Everything under config folder public static String getAssetsFolder(String appName) { return getConfigFolder(appName) + File.separator + ASSETS_FOLDER_NAME; } public static String getAssetsCsvFolder(String appName) { return getAssetsFolder(appName) + File.separator + CSV_FOLDER_NAME; } /** * Used in CsvUtil * * @param appName the app name * @param tableId the id of the table to get the csv folder for * @return :app_name/assets/csv/:table_id/instances */ public static String getAssetsCsvInstancesFolder(String appName, String tableId) { return getAssetsCsvFolder(appName) + File.separator + tableId + File.separator + INSTANCES_FOLDER_NAME; } /** * Used in CsvUtil, and AbstractPermissionsTestCase in services * * @param appName the app name * @param tableId the id of the table to get the instance for * @param instanceId the id of the instance to find * @return :app_name/assets/csv/:table_id/instances/:instance_id */ public static String getAssetsCsvInstanceFolder(String appName, String tableId, String instanceId) { return getAssetsCsvInstancesFolder(appName, tableId) + File.separator + safeInstanceIdFolderName(instanceId); } /** * Get the path to the tables initialization file for the given app. * Used in services.sync.service.logic.ProcessManifestContentAndFileChanges, InitializationUtil * * @param appName the app name * @return :app_name/assets/tables.init */ public static String getTablesInitializationFile(String appName) { return getAssetsFolder(appName) + File.separator + ODK_TABLES_INIT_FILENAME; } /** * Get the path to the common definitions file for the given app. * Used in LocalizationUtils * * @param appName the app name * @return :app_name/assets/commonDefinitions.js */ @SuppressWarnings("WeakerAccess") public static String getCommonDefinitionsFile(String appName) { return getAssetsFolder(appName) + File.separator + COMMON_DEFINITIONS_JS; } /** * Get the path to the user-defined home screen file. * Used in tables.activities.MainActivity * * @param appName the app name * @return :app_name/assets/index.html */ @SuppressWarnings("unused") public static String getTablesHomeScreenFile(String appName) { return getAssetsFolder(appName) + File.separator + ODK_TABLES_HOME_SCREEN_FILE_NAME; } /** * Used in InitializationUtil, services.sync.service.logic.ProcessManifestContentAndFileChanges * * @param appName the app name * @return :app_name/config/tables */ public static String getTablesFolder(String appName) { return getConfigFolder(appName) + File.separator + TABLES_FOLDER_NAME; } /** * Used in AggregateSynchronizer, services.database.utilities.ODKDatabaseImplUtils, * services.sync.service.logic.ProcessManifestContentAndFileChanges * * @param appName the app name * @param tableId the id of the table to get the folder for * @return :app_name/config/tables/:table_id */ @SuppressWarnings("WeakerAccess") public static String getTablesFolder(String appName, String tableId) { String path; if (tableId == null || tableId.isEmpty()) { throw new IllegalArgumentException("getTablesFolder: tableId is null or the empty string!"); } else { if (!VALID_FOLDER_PATTERN.matcher(tableId).matches()) { throw new IllegalArgumentException( "getFormFolder: tableId does not begin with a letter and contain only letters, digits or underscores!"); } if (FormsColumns.COMMON_BASE_FORM_ID.equals(tableId)) { path = getAssetsFolder(appName) + File.separator + FormsColumns.COMMON_BASE_FORM_ID; } else { path = getTablesFolder(appName) + File.separator + tableId; } } File f = new File(path); if (!f.exists() && !f.mkdirs()) { throw new RuntimeException("Could not mkdirs on " + f.getPath()); } return f.getAbsolutePath(); } // files under that /** * @param appName the app name * @param tableId the table id * @return :app_name/config/tables/:table_id/definition.csv */ public static String getTableDefinitionCsvFile(String appName, String tableId) { return getTablesFolder(appName, tableId) + File.separator + DEFINITION_CSV; } /** * @param appName the app name * @param tableId the table id * @return :app_name/config/tables/:table_id/properties.csv */ public static String getTablePropertiesCsvFile(String appName, String tableId) { return getTablesFolder(appName, tableId) + File.separator + PROPERTIES_CSV; } /** * Used in LocalizationUtils * * @param appName the app name * @param tableId the id of the table to find the definitions file for * @return :app_name/config/tables/:table_id/tableSpecificDefinitions.js */ static String getTableSpecificDefinitionsFile(String appName, String tableId) { return getTablesFolder(appName, tableId) + File.separator + TABLE_SPECIFIC_DEFINITIONS_JS; } /** * Used in InitializationUtil * * @param appName the app name * @param tableId the table id to find the forms for * @return :app_name/config/tables/:table_id/forms */ public static String getFormsFolder(String appName, String tableId) { return getTablesFolder(appName, tableId) + File.separator + FORMS_FOLDER_NAME; } public static String getFormFolder(String appName, String tableId, String formId) { if (formId == null || formId.isEmpty()) { throw new IllegalArgumentException("getFormFolder: formId is null or the empty string!"); } else { if (!VALID_FOLDER_PATTERN.matcher(formId).matches()) { throw new IllegalArgumentException( "getFormFolder: formId does not begin with a letter and contain only letters, digits or underscores!"); } return getFormsFolder(appName, tableId) + File.separator + formId; } } ///////////////////////////////////////////////////////// // Everything under data folder public static String getTablesInitializationCompleteMarkerFile(String appName) { return getDataFolder(appName) + File.separator + ODK_TABLES_INIT_FILENAME; } /** * Used in androidCommon.views.ODKWebView * * @param appName the app name * @return :app_name/data/appCache */ @SuppressWarnings("WeakerAccess") public static String getAppCacheFolder(String appName) { return getDataFolder(appName) + File.separator + APP_CACHE_FOLDER_NAME; } /** * Used in survey.activities.DrawActivity * * @param appName the app name * @return :app_name/data/appCache/tmpDraw.jpg */ @SuppressWarnings("unused") public static String getTempDrawFile(String appName) { return getAppCacheFolder(appName) + File.separator + "tmpDraw.jpg"; } /** * Used in survey.activities.DrawActivity * * @param appName the app name * @return :app_name/data/appCache/tmp.jpg */ @SuppressWarnings("unused") public static String getTempFile(String appName) { return getAppCacheFolder(appName) + File.separator + "tmp.jpg"; } /** * Used in ODKWebView * * @param appName the app name * @return :app_name/data/geoCache */ @SuppressWarnings("WeakerAccess") public static String getGeoCacheFolder(String appName) { return getDataFolder(appName) + File.separator + GEO_CACHE_FOLDER_NAME; } /** * Used in AndroidOdkConnection, OdkConnectionFactoryAbstractClass, SQLiteConnection (all in * services) * * @param appName the app name * @return :app_name/data/webDb */ @SuppressWarnings("WeakerAccess") public static String getWebDbFolder(String appName) { return getDataFolder(appName) + File.separator + WEB_DB_FOLDER_NAME; } /** * used only locally, but feel free to make it public if you need to use it * * @param appName the app name * @return :app_name/data/tables */ private static String getTableDataFolder(String appName) { return getDataFolder(appName) + File.separator + TABLES_FOLDER_NAME; } /** * Used in CsvUtil, scan.utils.ScanUtil * * @param appName the app name * @param tableId the id of the table to find the instances folder for * @return :app_name/data/tables/:table_id/instances */ public static String getInstancesFolder(String appName, String tableId) { String path; path = getTableDataFolder(appName) + File.separator + tableId + File.separator + INSTANCES_FOLDER_NAME; File f = new File(path); if (!f.exists() && !f.mkdirs()) { throw new RuntimeException("Could not mkdirs on " + f.getPath()); } return f.getAbsolutePath(); } private static String safeInstanceIdFolderName(String instanceId) { if (instanceId == null || instanceId.isEmpty()) { throw new IllegalArgumentException("getInstanceFolder: instanceId is null or the empty string!"); } else { return VALID_INSTANCE_ID_FOLDER_NAME_PATTERN.matcher(instanceId).replaceAll("_"); } } /** * @param appName the app name * @param tableId the table id * @param instanceId the instance id * @return :app_name/data/tables/:table_id/instances/:instance_id */ public static String getInstanceFolder(String appName, String tableId, String instanceId) { String path; String instanceFolder = safeInstanceIdFolderName(instanceId); path = getInstancesFolder(appName, tableId) + File.separator + instanceFolder; File f = new File(path); if (!f.exists() && !f.mkdirs()) { throw new RuntimeException("Could not mkdirs on " + f.getPath()); } return f.getAbsolutePath(); } /** * Used mostly in Media activities in survey, but also in AndroidSynchronizer * TODO I really can't document this without knowing what a rowpath is * * @param appName the app name * @param tableId the table id * @param instanceId the instance id * @param rowpathUri The URI to a rowpath, used to get the filename * @return the filename of the rowpath */ public static File getRowpathFile(String appName, String tableId, String instanceId, String rowpathUri) { // clean up the value... if (rowpathUri.startsWith("/")) { rowpathUri = rowpathUri.substring(1); } String instanceFolder = ODKFileUtils.getInstanceFolder(appName, tableId, instanceId); String instanceUri = ODKFileUtils.asUriFragment(appName, new File(instanceFolder)); String fileUri; if (rowpathUri.startsWith(instanceUri)) { // legacy construction WebLogger.getLogger(appName).e(TAG, "table [" + tableId + "] contains old-style rowpath constructs!"); fileUri = rowpathUri; } else { fileUri = instanceUri + "/" + rowpathUri; } return ODKFileUtils.getAsFile(appName, fileUri); } /** * Used in AggregateSynchronizer, scan.activities.JSON2SurveyJSONActivity and several of * survey's Media activities * * @param appName the app name * @param tableId the table id * @param instanceId the instance id * @param rowFile the file that the rowpath uri will point to * @return a uri that can be used for an intent and later dereferenced to get a rowpath file */ @SuppressWarnings("unused") public static String asRowpathUri(String appName, String tableId, String instanceId, File rowFile) { String instanceFolder = ODKFileUtils.getInstanceFolder(appName, tableId, instanceId); String instanceUri = ODKFileUtils.asUriFragment(appName, new File(instanceFolder)); String rowpathUri = ODKFileUtils.asUriFragment(appName, rowFile); if (!rowpathUri.startsWith(instanceUri)) { throw new IllegalArgumentException("asRowpathUri -- rowFile is not in a valid rowpath location!"); } String relativeUri = rowpathUri.substring(instanceUri.length()); if (relativeUri.startsWith("/")) { relativeUri = relativeUri.substring(1); } return relativeUri; } /////////////////////////////////////////////// // Everything under output folder /** * Used only in WebLoggerImpl * * @param appName the app name * @return :app_name/output/logging */ public static String getLoggingFolder(String appName) { return getOutputFolder(appName) + File.separator + LOGGING_FOLDER_NAME; } /** * Used in SimpleWebServer, tables.utils.OutputUtil * * @param appName the app name * @return :app_name/output/debug */ @SuppressWarnings("WeakerAccess") public static String getTablesDebugObjectFolder(String appName) { String outputFolder = getOutputFolder(appName); return outputFolder + File.separator + DEBUG_FOLDER_NAME; } /** * Used only in CSVUtil * * @param appName the app name * @return :app_name/output/csv */ public static String getOutputCsvFolder(String appName) { return getOutputFolder(appName) + File.separator + CSV_FOLDER_NAME; } /** * Used only in CsvUtil * * @param appName the app name * @param tableId the table id of the instance * @param instanceId the instance id of the form submission * @return :app_name/output/csv/:table_id/instances/:instance_id */ public static String getOutputCsvInstanceFolder(String appName, String tableId, String instanceId) { return getOutputCsvFolder(appName) + File.separator + tableId + File.separator + INSTANCES_FOLDER_NAME + File.separator + safeInstanceIdFolderName(instanceId); } /** * @param appName the app name * @param tableId the id of the table to export * @param fileQualifier An optional tag that is appended to the end of the four exported files * with a dot * @return :app_name/output/csv/:table_id(.:file_qualifier).csv */ public static String getOutputTableCsvFile(String appName, String tableId, String fileQualifier) { return getOutputCsvFolder(appName) + File.separator + tableId + (fileQualifier != null && !fileQualifier.isEmpty() ? "." + fileQualifier : "") + ".csv"; } /** * @param appName the app name * @param tableId the id of the table to export * @param fileQualifier An optional tag that is appended to the end of the four exported files * with a dot * @return :app_name/output/csv/:table_id(.:file_qualifier).definition.csv */ public static String getOutputTableDefinitionCsvFile(String appName, String tableId, String fileQualifier) { return getOutputCsvFolder(appName) + File.separator + tableId + (fileQualifier != null && !fileQualifier.isEmpty() ? "." + fileQualifier : "") + "." + DEFINITION_CSV; } /** * @param appName the app name * @param tableId the id of the table to export * @param fileQualifier An optional tag that is appended to the end of the four exported files * with a dot * @return :app_name/output/csv/:table_id(.:file_qualifier).properties.csv */ public static String getOutputTablePropertiesCsvFile(String appName, String tableId, String fileQualifier) { return getOutputCsvFolder(appName) + File.separator + tableId + (fileQualifier != null && !fileQualifier.isEmpty() ? "." + fileQualifier : "") + "." + PROPERTIES_CSV; } //////////////////////////////////////// // Everything under system folder /** * Used in FormsProvider, InitializationUtil * * @param appName the table id * @return :app_name/system/tables.deleting */ public static String getPendingDeletionTablesFolder(String appName) { return getSystemFolder(appName) + File.separator + STALE_TABLES_FOLDER_NAME; } /** * used only locally, by assertDirectoryStructure * * @param appName the table id * @return :app_name/system/tables.pending */ private static String getPendingInsertionTablesFolder(String appName) { return getSystemFolder(appName) + File.separator + PENDING_TABLES_FOLDER_NAME; } // 4th level config tables tableId folder // 3rd level output /** * Used in services.preferences.fragments.DeviceSettingsFragment * * @param appName the app name * @param path the path to check against * @return whether the path is under :app_name/ */ @SuppressWarnings("unused") public static boolean isPathUnderAppName(String appName, File path) { File parentDir = new File(getAppFolder(appName)); while (path != null && !path.equals(parentDir)) { path = path.getParentFile(); } return path != null; } /** * Used in AggregateSynchronizer, services.sync.logic.ProcessManifestContentAndFileChanges * * @param path the path that's under an app * @return the app name */ @SuppressWarnings("unused") public static String extractAppNameFromPath(File path) { if (path == null) { return null; } File parent = path.getParentFile(); File odkDir = new File(getOdkFolder()); while (parent != null && !parent.equals(odkDir)) { path = parent; parent = path.getParentFile(); } if (parent == null) { return null; } return path.getName(); } /** * Returns the relative path beginning after the getAppFolder(appName) directory. * The relative path does not start or end with a '/' * * @param appName the app name * @param fileUnderAppName a file that's under :app_name/ * @return a relative path to that file */ public static String asRelativePath(String appName, File fileUnderAppName) { // convert fileUnderAppName to a relative path such that if // we just append it to the AppFolder, we have a full path. File parentDir = new File(getAppFolder(appName)); ArrayList<String> pathElements = new ArrayList<>(); File f = fileUnderAppName; while (f != null && !f.equals(parentDir)) { pathElements.add(f.getName()); f = f.getParentFile(); } if (f == null) { throw new IllegalArgumentException("file is not located under this appName (" + appName + ")!"); } StringBuilder b = new StringBuilder(); for (int i = pathElements.size() - 1; i >= 0; --i) { String element = pathElements.get(i); b.append(element); if (i != 0) { b.append(File.separator); } } return b.toString(); } public static String asUriFragment(String appName, File fileUnderAppName) { String relativePath = asRelativePath(appName, fileUnderAppName); String separatorString; if (File.separatorChar == '\\') { // Windows Robolectric separatorString = File.separator + File.separator; } else { separatorString = File.separator; } String[] segments = relativePath.split(separatorString); StringBuilder b = new StringBuilder(); boolean first = true; for (String s : segments) { if (!first) { b.append("/"); // uris have forward slashes } first = false; b.append(s); } return b.toString(); } /** * Reconstructs the full path from the appName and uriFragment * Used in AggregateSynchronizer, FileSet and services.submissions.provider.SubmissionProvider * * @param appName the app name * @param uriFragment a uri that represents a fully qualified path to a file * @return a File object to the represented path */ @SuppressWarnings("WeakerAccess") public static File getAsFile(String appName, String uriFragment) { // forward slash always... if (uriFragment == null || uriFragment.isEmpty()) { throw new IllegalArgumentException("Not a valid uriFragment: " + appName + "/" + uriFragment + " application or subdirectory not specified."); } File f = fromAppPath(appName); if (f == null || !f.exists() || !f.isDirectory()) { throw new IllegalArgumentException( "Not a valid uriFragment: " + appName + "/" + uriFragment + " invalid application."); } String[] segments = uriFragment.split("/"); for (String s : segments) { f = new File(f, s); } return f; } /** * Convert a relative path into an application filename. Used all over the place * * @param appName the app name * @param relativePath the relative path to a file * @return A file object to :app_name/*:relative_path */ @SuppressWarnings("unused") public static File asAppFile(String appName, String relativePath) { return new File(getAppFolder(appName) + File.separator + relativePath); } /** * Used in services.preferences.fragments.DeviceSettingsFragment, * services.sync.logic.ProcessManifestContentAndFileChanges * * @param appName the app name * @param relativePath the relative path to a config file * @return a file object to :app_name/config/*:relative_path */ @SuppressWarnings("unused") public static File asConfigFile(String appName, String relativePath) { return new File(getConfigFolder(appName) + File.separator + relativePath); } /** * Used in AggregateSynchronizer * * @param appName the app name * @param fileUnderAppConfigName a file object to a file under :app_name/config/ * @return the relative path to that file */ @SuppressWarnings("unused") public static String asConfigRelativePath(String appName, File fileUnderAppConfigName) { String relativePath = asRelativePath(appName, fileUnderAppConfigName); if (!relativePath.startsWith(CONFIG_FOLDER_NAME + File.separator)) { throw new IllegalArgumentException("File is not located under config folder"); } relativePath = relativePath.substring(CONFIG_FOLDER_NAME.length() + File.separator.length()); if (relativePath.contains(File.separator + "..")) { throw new IllegalArgumentException("File contains " + File.separator + ".."); } return relativePath; } /** * The formPath is relative to the framework directory and is passed into * the WebKit to specify the form to display. * Used in (survey) FormIdStruct, MainMenuActivity, (services) FormInfo * * @param appName the app name * @param formDefFile a file object to a formDef.json * @return a relative path to that formDef file */ @SuppressWarnings("unused") public static String getRelativeFormPath(String appName, File formDefFile) { // compute FORM_PATH... // we need to do this relative to the AppFolder, as the // common index.html is under the ./system folder. String relativePath = asRelativePath(appName, formDefFile.getParentFile()); // adjust for relative path from ./system... relativePath = ".." + File.separator + relativePath + File.separator; return relativePath; } /** * Used as the baseUrl in the webserver, exposed to the javascript interface via getBaseUrl in * OdkCommon * * @return ../system */ @SuppressWarnings("unused") public static String getRelativeSystemPath() { return ".." + File.separator + ODKFileUtils.SYSTEM_FOLDER_NAME; } public static String getMd5Hash(String appName, File file) { return MD5_COLON_PREFIX + getNakedMd5Hash(appName, file); } /** * MD5's a file. Used in ODKDatabaseImplUtils and EncryptionUtils * * @param appName the app name * @param file the file to hash * @return the md5sum of that file */ @SuppressWarnings("WeakerAccess") public static String getNakedMd5Hash(String appName, Object file) { InputStream is = null; try { // CTS (6/15/2010) : stream file through digest instead of handing // it the byte[] MessageDigest md = MessageDigest.getInstance("MD5"); int chunkSize = 8192; byte[] chunk = new byte[chunkSize]; // Get the size of the file long lLength; if (file instanceof File) { lLength = ((File) file).length(); } else if (file instanceof String) { lLength = ((String) file).length(); } else { throw new IllegalArgumentException("Bad object to md5"); } if (lLength > Integer.MAX_VALUE) { if (file instanceof File) { WebLogger.getLogger(appName).e(TAG, "File " + ((File) file).getName() + " is too large"); } else { WebLogger.getLogger(appName).e(TAG, "String is too large to md5"); } return null; } if (lLength > Integer.MAX_VALUE) { throw new RuntimeException("Refusing to cast from long to int with loss of precision"); } //noinspection NumericCastThatLosesPrecision int length = (int) lLength; if (file instanceof File) { is = new FileInputStream((File) file); } else { is = new ByteArrayInputStream(((String) file).getBytes(CharEncoding.UTF_8)); } int l; for (l = 0; l + chunkSize < length; l += chunkSize) { // TODO double check that this still works after the change if (is.read(chunk, 0, chunkSize) == -1) break; md.update(chunk, 0, chunkSize); } int remaining = length - l; if (remaining > 0) { // TODO double check that this still works after the change if (is.read(chunk, 0, remaining) != -1) { md.update(chunk, 0, remaining); } } byte[] messageDigest = md.digest(); BigInteger number = new BigInteger(1, messageDigest); String md5 = number.toString(16); while (md5.length() < 32) md5 = "0" + md5; is.close(); return md5; } catch (NoSuchAlgorithmException e) { WebLogger.getLogger(appName).e("MD5", e.getMessage()); return null; } catch (FileNotFoundException e) { WebLogger.getLogger(appName).e("No Cache File", e.getMessage()); return null; } catch (IOException e) { WebLogger.getLogger(appName).e("Problem reading from file", e.getMessage()); return null; } finally { if (is != null) { try { is.close(); } catch (IOException e) { WebLogger.getLogger(appName).printStackTrace(e); } } } } /** * Used in WebCursorUtils * * @param n the node to read text from * @param trim whether to trim extra whitespace from the node * @return the text in the node */ @SuppressWarnings("unused") public static String getXMLText(Node n, boolean trim) { NodeList nl = n.getChildNodes(); return nl.getLength() == 0 ? null : getXMLText(nl, 0, trim); } /** * reads all subsequent text nodes and returns the combined string needed * because escape sequences are parsed into consecutive text nodes e.g. * "abc&123" --> (abc)(&)(123) **/ private static String getXMLText(NodeList nl, int i, boolean trim) { StringBuffer strBuff = null; String text = nl.item(i).getTextContent(); if (text == null) return null; for (i++; i < nl.getLength() && nl.item(i).getNodeType() == Node.TEXT_NODE; i++) { if (strBuff == null) strBuff = new StringBuffer(text); strBuff.append(nl.item(i).getTextContent()); } if (strBuff != null) text = strBuff.toString(); if (trim) text = text.trim(); return text; } public static void copyDirectory(File sourceFolder, File destinationFolder) throws IOException { ContextClassLoaderWrapper wrapper = new ContextClassLoaderWrapper(); try { FileUtils.copyDirectory(sourceFolder, destinationFolder); } finally { wrapper.release(); } } public static void moveDirectory(File sourceFolder, File destinationFolder) throws IOException { ContextClassLoaderWrapper wrapper = new ContextClassLoaderWrapper(); try { FileUtils.moveDirectory(sourceFolder, destinationFolder); } finally { wrapper.release(); } } public static boolean directoryContains(File folder, File file) throws IOException { ContextClassLoaderWrapper wrapper = new ContextClassLoaderWrapper(); try { return FileUtils.directoryContains(folder, file); } finally { wrapper.release(); } } public static void deleteDirectory(File folder) throws IOException { ContextClassLoaderWrapper wrapper = new ContextClassLoaderWrapper(); try { FileUtils.deleteDirectory(folder); } finally { wrapper.release(); } } public static void copyFile(File sourceFile, File destinationFile) throws IOException { ContextClassLoaderWrapper wrapper = new ContextClassLoaderWrapper(); try { FileUtils.copyFile(sourceFile, destinationFile); } finally { wrapper.release(); } } /** * Used in ODKDatabaseImplUtils * * @param directory a directory to scan through * @param fileFileFilter a filter used to strip unwanted files from the result * @param directoryFilter a filter used to strip unwanted directories from the result * @return a list of the files that matched the file filter and directory filter */ @SuppressWarnings("unused") public static Collection<File> listFiles(File directory, IOFileFilter fileFileFilter, IOFileFilter directoryFilter) { ContextClassLoaderWrapper wrapper = new ContextClassLoaderWrapper(); try { return FileUtils.listFiles(directory, fileFileFilter, directoryFilter); } finally { wrapper.release(); } } /** * Used in ODKDatabaseImplUtils * * @param file the file to delete */ @SuppressWarnings("unused") public static void deleteQuietly(File file) { ContextClassLoaderWrapper wrapper = new ContextClassLoaderWrapper(); try { FileUtils.deleteQuietly(file); } finally { wrapper.release(); } } /** * This ensures that the current thread's class loader is set, because if it isn't, things in * FileUtils won't work. FileUtils uses some fancy Java 7 optimizations, but it will fallback * to the slow way if you're on java 6. However the way it detects if these optimizations are * present uses the current thread's class loader, and if the class loader is null, it can't * tell and just dies. */ private static class ContextClassLoaderWrapper { boolean wrapped = false; ContextClassLoaderWrapper() { ClassLoader loader = Thread.currentThread().getContextClassLoader(); if (loader == null) { wrapped = true; Thread.currentThread().setContextClassLoader(ODKFileUtils.class.getClassLoader()); } } void release() { if (wrapped) { Thread.currentThread().setContextClassLoader(null); } } } }