Java tutorial
/* The MIT License (MIT) * * Copyright (c) 2015 PMA2020 * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.odk.collect.android.logic; import android.content.ContentValues; import android.database.Cursor; import android.net.Uri; import android.util.Log; import org.apache.commons.io.IOUtils; import org.javarosa.core.model.FormDef; import org.javarosa.core.model.instance.InstanceInitializationFactory; import org.javarosa.core.model.instance.TreeElement; import org.javarosa.core.services.transport.payload.ByteArrayPayload; import org.javarosa.form.api.FormEntryController; import org.javarosa.form.api.FormEntryModel; import org.javarosa.xform.parse.XFormParseException; import org.javarosa.xform.util.XFormUtils; import org.odk.collect.android.application.Collect; import org.odk.collect.android.database.FormRelationsDb; import org.odk.collect.android.database.FormRelationsDb.MappingData; import org.odk.collect.android.exception.FormRelationsException; import org.odk.collect.android.preferences.AdminPreferencesActivity; import org.odk.collect.android.provider.FormsProviderAPI.FormsColumns; import org.odk.collect.android.provider.InstanceProviderAPI; import org.odk.collect.android.provider.InstanceProviderAPI.InstanceColumns; import org.odk.collect.android.tasks.FormLoaderTask; import org.odk.collect.android.tasks.SaveToDiskTask; import org.odk.collect.android.tasks.UseLog; import org.odk.collect.android.tasks.UseLogContract; import org.odk.collect.android.utilities.FileUtils; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.io.UnsupportedEncodingException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Set; import java.util.TreeSet; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.Result; import javax.xml.transform.Source; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpression; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; /** * Defines important functions for working with Parent/Child forms. * * This class is the workhorse of the form relations logic. It contains * routines that are run when users save, delete, and upload forms. General * uses are the following: * * 1. When saving, initialize `FormRelationsManager` with static factory * method. Test if anything will be deleted. Then `manageFormRelations()`. * 2. When deleting, use `removeAllReferences`. * 3. When uploading, use `getRelatedFormsFinalized`. * * Creator: James K. Pringle * E-mail: jpringle@jhu.edu * Created: 20 August 2015 * Last modified: 23 August 2016 */ public class FormRelationsManager { private static final String TAG = "FormRelationsManager"; private static final boolean LOCAL_LOG = true; // Return codes for what to delete public static final int NO_DELETE = -1; public static final int DELETE_THIS = 0; public static final int DELETE_CHILD = 1; // Return codes for what related forms are finalized public static final int NO_RELATIONS = -1; public static final int ALL_FINALIZED = 0; public static final int CHILD_UNFINALIZED = 1; public static final int PARENT_UNFINALIZED = 2; public static final int SIBLING_UNFINALIZED = 3; // Error codes for FormRelationsException private static final int NO_ERROR_CODE = 0; private static final int PROVIDER_NO_FORM = 1; private static final int NO_INSTANCE_NO_FORM = 2; private static final int NO_REPEAT_NUMBER = 3; private static final int PROVIDER_NO_INSTANCE = 4; private static final int BAD_XPATH_INSTANCE = 5; // Important strings to search for in XForms private static final String SAVE_INSTANCE = "saveInstance"; private static final String SAVE_FORM = "saveForm"; private static final String DELETE_FORM = "deleteForm"; // Public error codes public static final int CODE_NO_SUBFORM = -1; public static final int CODE_NO_XPATH = -2; private long mInstanceId; private ArrayList<TraverseData> mAllTraverseData; private int mMaxRepeatIndex; private ArrayList<TraverseData> mNonRelevantSaveForm; private boolean mHasDeleteForm; private static UseLog mUseLog; public FormRelationsManager() { mInstanceId = -1; mAllTraverseData = new ArrayList<TraverseData>(); mMaxRepeatIndex = 0; mNonRelevantSaveForm = new ArrayList<TraverseData>(); mHasDeleteForm = false; } public FormRelationsManager(long instanceId) { mInstanceId = instanceId; mAllTraverseData = new ArrayList<TraverseData>(); mMaxRepeatIndex = 0; mNonRelevantSaveForm = new ArrayList<TraverseData>(); mHasDeleteForm = false; } /** * Performs all necessary routines for maintaining form relations. * * When saving, the following tasks are always done in this order: * * 1. The parent form is updated * 2. Children forms are created and updated * 3. Relevant deletions are performed * * Of course, these subroutines are no-ops if nothing needs to be done or * if there is no parent/child form extent. * * This method is the entry point for the `SaveToDiskTask`. By the time * this method is called, the instance should already be saved to disk, * i.e. not stored as a temporary save file. * * After this method finishes, the form relations database should be * up-to-date, all paired nodes between parent and child forms should be * synced, and all relevant deletions should have taken place. * * @param uri Uri of the current survey, be it a form or instance uri. * @param instanceRoot Root of the JavaRosa tree built during the survey. * @return Returns number of other forms updated, or error code if linking * error */ public static int manageFormRelations(Uri uri, TreeElement instanceRoot) { long instanceId = getIdFromSingleUri(uri); int parentCode = manageParentForm(instanceId); FormRelationsManager frm = getFormRelationsManager(instanceId, instanceRoot); int childCode = frm.outputOrUpdateChildForms(); int deleteCode = frm.manageDeletions(); int returnCode = 0; if (parentCode >= 0) { returnCode += parentCode; } if (childCode >= 0) { returnCode += childCode; } if (deleteCode >= 0) { returnCode += deleteCode; } // Currently (May 2016), childCode is the only thing that can have an error if (childCode < 0) { returnCode = childCode; } return returnCode; } /** * Modifies the parent form if a paired node with child form is changed. * * When this method is called, both child and parent instance are saved to * disk. After opening each file, this method gets all node pairs, or * mappings, and loops through them. For each mapping, the instance value * is obtained in both files. If there is a difference, then the parent * is modified in memory. If there is any change in the parent, then the * file is rewritten to disk. If there is an exception while evaulating * xpaths or doing anything else, updating the parent form is aborted. * * If the parent form is changed, then its status is changed to * incomplete. * * @param childId Instance id * @return Returns -1 if no parent, 0 if has parent, but no updates, 1 if * has parent and updates made. */ private static int manageParentForm(long childId) { Long parentId = FormRelationsDb.getParent(childId); if (LOCAL_LOG) { Log.d(TAG, "Inside manageParentForm. Parent instance id is \'" + parentId + "\'"); } if (parentId < 0) { // No parent form to manage return -1; } int returnCode = 0; try { String parentInstancePath = getInstancePath(getInstanceUriFromId(parentId)); String childInstancePath = getInstancePath(getInstanceUriFromId(childId)); Document parentDocument = getDocument(parentInstancePath); Document childDocument = getDocument(childInstancePath); XPath xpath = XPathFactory.newInstance().newXPath(); ArrayList<MappingData> mappings = FormRelationsDb.getMappingsToParent(childId); boolean editedParentForm = false; mUseLog = new UseLog(parentInstancePath, true); for (MappingData mapping : mappings) { XPathExpression parentExpression = xpath.compile(mapping.parentNode); XPathExpression childExpression = xpath.compile(mapping.childNode); Node parentNode = (Node) parentExpression.evaluate(parentDocument, XPathConstants.NODE); Node childNode = (Node) childExpression.evaluate(childDocument, XPathConstants.NODE); if (null == parentNode || null == childNode) { throw new FormRelationsException(BAD_XPATH_INSTANCE, "Child: " + mapping.childNode + ", Parent: " + mapping.parentNode); } if (!childNode.getTextContent().equals(parentNode.getTextContent())) { mUseLog.log(UseLogContract.RELATION_CHANGE_VALUE, parentId, mapping.parentNode, childNode.getTextContent()); Log.i(TAG, "Found difference updating parent form @ parent node \'" + parentNode.getNodeName() + "\'. Child: \'" + childNode.getTextContent() + "\' <> Parent: \'" + parentNode.getTextContent() + "\'"); parentNode.setTextContent(childNode.getTextContent()); editedParentForm = true; } } if (editedParentForm) { writeDocumentToFile(parentDocument, parentInstancePath); mUseLog.writeBackLogAndClose(); ContentValues cv = new ContentValues(); cv.put(InstanceColumns.STATUS, InstanceProviderAPI.STATUS_INCOMPLETE); Collect.getInstance().getContentResolver().update(getInstanceUriFromId(parentId), cv, null, null); returnCode = 1; } } catch (FormRelationsException e) { if (e.getErrorCode() == PROVIDER_NO_INSTANCE) { Log.w(TAG, "Unable to find the instance path for either this form (id=" + childId + ") or its parent (id=" + parentId + ")"); } else if (e.getErrorCode() == BAD_XPATH_INSTANCE) { Log.w(TAG, "Bad XPath from one of child or parent. " + e.getInfo()); } } catch (FileNotFoundException e) { e.printStackTrace(); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } catch (ParserConfigurationException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (SAXException e) { e.printStackTrace(); } catch (XPathExpressionException e) { e.printStackTrace(); } catch (TransformerException e) { e.printStackTrace(); } return returnCode; } /** * Initializes a `FormRelationsManager` with form relations information. * * A FormRelationsManager is initialized, and then the JavaRosa tree for * the current instance is traversed. All attributes and values are * analyzed to determine what relates to form relations. * * The `FormRelationsManager` is then used to determine what to create, * update, or delete. * * @param parentId The instance id of the current form * @param instanceRoot Root of the JavaRosa tree built during the survey. * @return Returns a properly initialized FormRelationsManager, containing * the information needed for subsequent CRUD. */ public static FormRelationsManager getFormRelationsManager(long parentId, TreeElement instanceRoot) { FormRelationsManager frm = new FormRelationsManager(parentId); try { traverseInstance(instanceRoot, frm, null); } catch (FormRelationsException e) { if (DELETE_FORM.equals(e.getMessage())) { if (LOCAL_LOG) { Log.d(TAG, "Interrupt traverse to delete instance"); } frm.setDeleteForm(true); } } return frm; } /** * Initializes a `FormRelationsManager` with form relations information. * * This is called in the UI thread in order to determine if any deletions * have become relevant. This information is used to determine what * warning dialogs, if any, to display. Note that the instance may not * be in the InstanceProvider if the form has not yet been saved. * * This method traverses the JavaRosa tree analyzing all attributes and * values. The only difference with the other version of this overloaded * method is that the object member `mInstanceId` is not set. * * @param instanceRoot Root of the JavaRosa tree built during the survey. * @return Returns a properly initialized FormRelationsManager, containing * the information needed to determine if a delete is necessary. */ public static FormRelationsManager getFormRelationsManager(TreeElement instanceRoot) { FormRelationsManager frm = new FormRelationsManager(); try { traverseInstance(instanceRoot, frm, null); } catch (FormRelationsException e) { if (DELETE_FORM.equals(e.getMessage())) { if (LOCAL_LOG) { Log.d(TAG, "Interrupt traverse to delete instance"); } frm.setDeleteForm(true); } } return frm; } /** * Initializes a `FormRelationsManager` with form relations information. * * This is called in the UI thread from `FormEntryActivity` in order to * determine if any deletions have become relevant. This information is * used to determine what warning dialogs, if any, to display. Note that * the instance may not be in the InstanceProvider if the form has not yet * been saved. That is why the intent data that starts FormEntryActivity * is passed here. * * This method traverses the JavaRosa tree analyzing all attributes and * values. * * @param formUri The Uri passed in the intent to start FormEntryActivity. * @param instanceRoot Root of the JavaRosa tree built during the survey. * @return Returns a properly initialized FormRelationsManager, containing * the information needed to determine if a delete is necessary. */ public static FormRelationsManager getFormRelationsManager(Uri formUri, TreeElement instanceRoot) { if (LOCAL_LOG) { Log.d(TAG, "Inside getFormRelationsManager with uri \'" + formUri.toString() + "\'"); } long instanceId = getIdFromSingleUri(formUri); if (LOCAL_LOG) { Log.d(TAG, "Determined id to be \'" + instanceId + "\'"); } FormRelationsManager frm = new FormRelationsManager(instanceId); try { traverseInstance(instanceRoot, frm, null); } catch (FormRelationsException e) { if (DELETE_FORM.equals(e.getMessage())) { if (LOCAL_LOG) { Log.d(TAG, "Interrupt traverse to delete instance"); } frm.setDeleteForm(true); } } return frm; } /** * Deletes current instance or children, as necessary. * * Pre-condition: The FormRelationsManager object should have been * initialized by one of the getFormRelationsManager methods. Thus all * form relations information in the current instance is collected. * Furthermore, the current instance should already be saved to disk. * * Post-condition: If a relevant deleteForm is discovered, then the * current instance (and its children) are deleted from the * InstanceProvider and from the form relations database. If an irrelevant * saveForm is discovered and it is associated with a child, then that * child is deleted. In this second case, the parent form is unmodified. * * @return The number of forms deleted is returned. */ private int manageDeletions() { int nDeletions = 0; int deleteWhat = getWhatToDelete(); if (deleteWhat == DELETE_THIS) { // PMA-Logging BEGIN mUseLog.log(UseLogContract.RELATION_SELF_DESTRUCT, mInstanceId, null, null); mUseLog.writeBackLogAndClose(); // try { // // possible racing? writing for value differences, then writing for deletion // long thisParentId = FormRelationsDb.getParent(mInstanceId); // String thisParent = getInstancePath(getInstanceUriFromId(thisParentId)); // String repeatable = FormRelationsDb.getRepeatable(thisParentId, mInstanceId); // int repeatIndex = FormRelationsDb.getRepeatIndex(thisParentId, mInstanceId); // FormRelationsUseLog frul = new FormRelationsUseLog(thisParent); // frul.log(UseLogContract.RELATION_SELF_DESTRUCT, repeatable, String.valueOf(repeatIndex)); // frul.writeBackLog(true); // frul.close(); // } catch (FormRelationsException e) { // Log.w(TAG, "Failed to log self-deletion", e); // } // PMA-Logging END nDeletions = deleteInstance(mInstanceId); } else if (deleteWhat == DELETE_CHILD) { TreeSet<Integer> allRepeatIndices = new TreeSet<Integer>(); for (TraverseData td : mNonRelevantSaveForm) { allRepeatIndices.add(td.repeatIndex); } TreeSet<Long> allWaywardChildren = new TreeSet<Long>(); for (Integer i : allRepeatIndices) { Long childInstanceId = FormRelationsDb.getChild(mInstanceId, i); if (LOCAL_LOG) { Log.d(TAG, "ParentId(" + mInstanceId + ") + RepeatIndex(" + i + ") + ChildIdFound(" + childInstanceId + ")"); } if (childInstanceId != -1) { allWaywardChildren.add(childInstanceId); } } for (Long childInstanceId : allWaywardChildren) { // PMA-Logging BEGIN // probably not good to keep track here. log file already being written to in formentry // // Get true mInstanceId, write rD to parent log.txt // PMA-Logging END deleteInstance(childInstanceId); } nDeletions = allWaywardChildren.size(); } return nDeletions; } /** * Deletes a child when a repeat group in the parent form is removed. * * When a repeat is removed from a parent form, then the deletion process * must take place here. It is different from a normal child deletion * because siblings are possibly affected. * * FormRelationsDb handles fixing the database to account for the * deletion. * * Unfortunately, this is currently called in the UI thread. * * @param parentId The instance id of the current form * @param repeatIndex The repeat index in the parent instance, where a * repeatIndex is greater than zero. */ public static void manageRepeatDelete(long parentId, int repeatIndex) { if (parentId >= 0 && repeatIndex >= 0) { Long childInstanceId = FormRelationsDb.getChild(parentId, repeatIndex); Uri childInstance = getInstanceUriFromId(childInstanceId); Collect.getInstance().getContentResolver().delete(childInstance, null, null); FormRelationsDb.deleteChild(parentId, repeatIndex); } } /** * Using the data from traversal, creates/updates children forms. * * During traversal, the largest repeat index is stored. From 1 up to and * including the largest repeat index, the saveForm and saveInstance * information is collected. If this information is not empty, then the * Uri or the child instance is obtained (perhaps creating the child * first). Everything is sent to the subroutine `insertAllIntoChild` to * finish off transferring parent information to the child. Raised * exceptions abort the process. * * @return Returns number of child forms that are modified ( >= 0) or an * error code. */ private int outputOrUpdateChildForms() { if (mHasDeleteForm) { // Children to be deleted. Just return. // Counting happens later. return 0; } int returnCode = 0; int nModifiedChildren = 0; for (int i = 1; i <= mMaxRepeatIndex; i++) { ArrayList<TraverseData> saveFormMapping = new ArrayList<TraverseData>(); ArrayList<TraverseData> saveInstanceMapping = new ArrayList<TraverseData>(); // Build up `saveFormMapping` and `saveInstanceMapping` for repeat index `i` for (Iterator<TraverseData> it = mAllTraverseData.iterator(); it.hasNext();) { TraverseData td = it.next(); if (td.repeatIndex == i) { if (SAVE_FORM.equals(td.attr)) { saveFormMapping.add(td); } else if (SAVE_INSTANCE.equals(td.attr)) { saveInstanceMapping.add(td); } else { String m = "Trying to output or update child form. Unexpected attr=\'" + td.attr + "\' @" + td.instanceXpath; Log.w(TAG, m); } } } if (saveFormMapping.isEmpty() && saveInstanceMapping.isEmpty()) { Log.i(TAG, "No form relations information for repeat node (" + i + "). Moving on..."); continue; } try { if (LOCAL_LOG) { Log.d(TAG, "Calling `getOrCreateChildForm` for index (" + i + ")"); } Uri childInstance = getOrCreateChildForm(saveFormMapping, saveInstanceMapping); // Now that we have the URI, the child instance definitely exists. Need to // transfer over values that are different. mUseLog already initialized for this // child in `getOrCreateChildForm`. boolean isChildModified = insertAllIntoChild(saveFormMapping, saveInstanceMapping, childInstance); if (isChildModified) { nModifiedChildren++; } } catch (IOException e) { Log.w(TAG, e.getMessage()); e.printStackTrace(); continue; } catch (FormRelationsException e) { String msg = "Exception raised when getting or creating child form for repeat (" + i + ")"; switch (e.getErrorCode()) { case NO_ERROR_CODE: break; case PROVIDER_NO_FORM: returnCode = CODE_NO_SUBFORM; msg = "No form with id \'" + e.getInfo() + "\' in FormProvider for " + "repeat (" + i + ")"; break; case BAD_XPATH_INSTANCE: long instanceId = Long.parseLong(e.getInfo()); deleteInstance(instanceId); returnCode = CODE_NO_XPATH; break; case NO_INSTANCE_NO_FORM: msg = "No child form exists, impossible to create one, no saveForm " + "information in repeat node (" + i + ")!"; break; case NO_REPEAT_NUMBER: // This should never happen msg = "No information from form relations to indicate which repeat (" + i + ")"; break; case PROVIDER_NO_INSTANCE: // This should never happen msg = "InstanceProvider does not have record of child for repeat (" + i + ")"; } Log.w(TAG, msg); continue; } } return returnCode < 0 ? returnCode : nModifiedChildren; } /** * Gets the Uri of the child instance, creating it first if necessary. * * @param saveFormMapping All `saveForm` information gathered from * traversal, filtered for this child. * @param saveInstanceMapping All `saveInstance` information gathered from * traversal, filtered for this child. * @return Returns the Uri of an instance in the InstanceProvider. * @throws FormRelationsException Process aborted if child must be created * and no appropriate form is in the FormProvider. * @throws IOException Process aborted if there is an IO error. */ private Uri getOrCreateChildForm(ArrayList<TraverseData> saveFormMapping, ArrayList<TraverseData> saveInstanceMapping) throws FormRelationsException, IOException { Uri childInstance; int repeatIndex = getRepeatIndex(saveFormMapping, saveInstanceMapping); long childId = FormRelationsDb.getChild(mInstanceId, repeatIndex); if (LOCAL_LOG) { Log.d(TAG, "From relations database, child id is: " + childId); } if (childId < 0) { // There was no child for the given parent form and associated index, so create one. String childFormId = getChildFormId(saveFormMapping); String[] columns = { FormsColumns._ID }; String selection = FormsColumns.JR_FORM_ID + "=?"; String[] selectionArgs = { childFormId }; Cursor cursor = Collect.getInstance().getContentResolver().query(FormsColumns.CONTENT_URI, columns, selection, selectionArgs, null); long formId = -1; if (cursor != null) { if (cursor.getCount() > 0) { cursor.moveToFirst(); formId = cursor.getLong(cursor.getColumnIndex(FormsColumns._ID)); } cursor.close(); } if (formId == -1) { throw new FormRelationsException(PROVIDER_NO_FORM, childFormId); } Uri formUri = getFormUriFromId(formId); // mUseLog initialized later in `createInstance` childInstance = createInstance(formUri); } else { // Get old instance childInstance = getInstanceUriFromId(childId); mUseLog = new UseLog(getInstancePath(childInstance), true); } return childInstance; } /** * Creates an instance from a form definition. * * In order to get the proper instance, we must first load the child form, * then save it to disk and only *then* can we edit the .xml file with the * appropriate values from the parent form. Much of this is similar to * what happens in FormLoaderTask, but we're already in a thread here. * * Unfortunately, we must violate DRY (don't repeat yourself). Normally, * creating an instance from a Uri is done at the end of onCreate in * FormEntryActivity and in doInBackground of FormLoaderTask, not in a * method that can be called. This is a lot of copy and paste. * * @param formUri A Uri of a form in the FormProvider. * @return Returns the Uri of the newly created instance. * @throws FormRelationsException A catch-all for various errors, any of * which aborts creation of an instance. * @throws IOException Routine aborted if there is an IO error. */ public static Uri createInstance(Uri formUri) throws FormRelationsException, IOException { if (LOCAL_LOG) { Log.d(TAG, "Inside createInstance(" + formUri.toString() + ")"); } FormDef fd = null; FileInputStream fis = null; String errorMsg = null; String formPath = ""; Cursor c = Collect.getInstance().getContentResolver().query(formUri, null, null, null, null); if (c != null) { if (c.getCount() == 1) { c.moveToFirst(); formPath = c.getString(c.getColumnIndex(FormsColumns.FORM_FILE_PATH)); } c.close(); } if (formPath.equals("")) { throw new FormRelationsException(); } FormDef.EvalBehavior mode = AdminPreferencesActivity .getConfiguredFormProcessingLogic(Collect.getInstance()); FormDef.setEvalBehavior(mode); File formXml = new File(formPath); String formHash = FileUtils.getMd5Hash(formXml); File formBin = new File(Collect.CACHE_PATH + File.separator + formHash + ".formdef"); if (formBin.exists()) { // if we have binary, deserialize binary Log.i(TAG, "Attempting to load " + formXml.getName() + " from cached file: " + formBin.getAbsolutePath()); fd = FormLoaderTask.deserializeFormDef(formBin); if (fd == null) { // some error occured with deserialization. Remove the file, and make a // new .formdef // from xml Log.w(TAG, "Deserialization FAILED! Deleting cache file: " + formBin.getAbsolutePath()); formBin.delete(); } } if (fd == null) { // no binary, read from xml try { Log.i(TAG, "Attempting to load from: " + formXml.getAbsolutePath()); fis = new FileInputStream(formXml); fd = XFormUtils.getFormFromInputStream(fis); if (fd == null) { errorMsg = "Error reading XForm file"; } else { FormLoaderTask.serializeFormDef(fd, formPath); } } catch (FileNotFoundException e) { e.printStackTrace(); errorMsg = e.getMessage(); } catch (XFormParseException e) { errorMsg = e.getMessage(); e.printStackTrace(); } catch (Exception e) { errorMsg = e.getMessage(); e.printStackTrace(); } finally { IOUtils.closeQuietly(fis); } } if (errorMsg != null || fd == null) { throw new FormRelationsException(); } // set paths to /sdcard/odk/forms/formfilename-media/ String formFileName = formXml.getName().substring(0, formXml.getName().lastIndexOf(".")); File formMediaDir = new File(formXml.getParent(), formFileName + "-media"); // Skip ExternalDataManaging // create FormEntryController from formdef FormEntryModel fem = new FormEntryModel(fd); FormEntryController fec = new FormEntryController(fem); FormController formController = new FormController(formMediaDir, fec, null); // this is for preloaders, but gets rid of non-relevant variables. (the // boolean is new form). fd.initialize(true, new InstanceInitializationFactory()); String instancePath = ""; if (formController.getInstancePath() == null) { // Create new answer folder. String time = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss-SS", Locale.ENGLISH) .format(Calendar.getInstance().getTime()); String file = formPath.substring(formPath.lastIndexOf('/') + 1, formPath.lastIndexOf('.')); String path = Collect.INSTANCES_PATH + File.separator + file + "_" + time; if (FileUtils.createFolder(path)) { formController.setInstancePath(new File(path + File.separator + file + "_" + time + ".xml")); instancePath = path + File.separator + file + "_" + time + ".xml"; } } exportData(formController); Uri createdInstance = updateInstanceDatabase(formUri, instancePath); mUseLog = new UseLog(getInstancePath(createdInstance), true); mUseLog.log(UseLogContract.RELATION_CREATE_FORM, getIdFromSingleUri(createdInstance), null, null); mUseLog.writeBackLogAndClose(); return createdInstance; } /** * Writes data in an instance to disk. * * @param formController The form controller for an instance * @return Returns true if and only if no exception is raised * @throws IOException An IO error aborts the routine. */ private static boolean exportData(FormController formController) throws IOException { ByteArrayPayload payload; payload = formController.getFilledInFormXml(); // write out xml String instancePath = formController.getInstancePath().getAbsolutePath(); SaveToDiskTask.exportXmlFile(payload, instancePath); return true; } /** * Adds to InstanceProvider using information from the form definition. * * After an instance is created and written to disk, it must be added to * the InstanceProvider. That happens here. This is mostly copypasta from * the private method inside the SaveToDiskTask. * * @param formUri The Uri for the form template for the instance. * @param instancePath The path to disk where the instance has been saved. * @return Returns the Uri of the record that was created in the * InstanceProvider. */ private static Uri updateInstanceDatabase(Uri formUri, String instancePath) { ContentValues values = new ContentValues(); values.put(InstanceColumns.STATUS, InstanceProviderAPI.STATUS_INCOMPLETE); values.put(InstanceColumns.CAN_EDIT_WHEN_COMPLETE, Boolean.toString(true)); // Entry didn't exist, so create it. Cursor c = null; try { // retrieve the form definition... c = Collect.getInstance().getContentResolver().query(formUri, null, null, null, null); c.moveToFirst(); String jrformid = c.getString(c.getColumnIndex(FormsColumns.JR_FORM_ID)); String jrversion = c.getString(c.getColumnIndex(FormsColumns.JR_VERSION)); String formname = c.getString(c.getColumnIndex(FormsColumns.DISPLAY_NAME)); String submissionUri = null; if (!c.isNull(c.getColumnIndex(FormsColumns.SUBMISSION_URI))) { submissionUri = c.getString(c.getColumnIndex(FormsColumns.SUBMISSION_URI)); } // add missing fields into values values.put(InstanceColumns.INSTANCE_FILE_PATH, instancePath); values.put(InstanceColumns.SUBMISSION_URI, submissionUri); values.put(InstanceColumns.DISPLAY_NAME, formname); values.put(InstanceColumns.JR_FORM_ID, jrformid); values.put(InstanceColumns.JR_VERSION, jrversion); } finally { if (c != null) { c.close(); } } Uri insertedInstance = Collect.getInstance().getContentResolver().insert(InstanceColumns.CONTENT_URI, values); if (LOCAL_LOG) { Log.d(TAG, "Successfully placed instance \'" + insertedInstance.toString() + "\' into InstanceProvider"); } return insertedInstance; } /** * Deletes an instance from form relations database and InstanceProvider. * * Deleting an instance should delete all descendant sub-forms. This * method assumes that there are no more than two generations, i.e. that * there are no grandparents or grandchildren or beyond for any given * form. * * First, all reference to the instance is removed from the form relations * database. Second, the instance is removed from the InstanceProvider. * Third, all children are removed from the InstanceProvider. These steps * are no-ops if there are no children, no form relations, not in the * InstanceProvider, etc. * * @param instanceId The id of the instance to be deleted. * @return The number of instances that are deleted, excluding this one. */ public static int deleteInstance(long instanceId) { if (LOCAL_LOG) { Log.d(TAG, "### deleteInstance(" + instanceId + ")"); } long[] childrenIds = FormRelationsDb.getChildren(instanceId); // Delete from relations.db FormRelationsDb.deleteAsParent(instanceId); FormRelationsDb.deleteAsChild(instanceId); // Delete from instance provider Uri thisInstance = getInstanceUriFromId(instanceId); Collect.getInstance().getContentResolver().delete(thisInstance, null, null); for (int i = 0; i < childrenIds.length; i++) { Uri childInstance = getInstanceUriFromId(instanceId); Collect.getInstance().getContentResolver().delete(childInstance, null, null); } return childrenIds.length; } /** * Iterates through the traversal data for insertion into a child form. * * First, the InstanceProvider is updated to show the correct * instanceName. Then for each item in saveInstanceMapping, * insertIntoChild copies the new information if necessary. Binary data is * copied if necessary. If saveInstanceMapping returns true, i.e. if the * child form is changed, then the child form is written to disk and its * status is set to incomplete. * * If an error is raised inside insertIntoChild, then that morsel of * traversal data is skipped and the next is processed. Other errors abort * the routine. * * @param saveFormMapping All `saveForm` information gathered from * traversal, filtered for this child. * @param saveInstanceMapping All `saveInstance` information gathered from * traversal, filtered for this child. * @param childInstance The Uri for the child instance * @return Returns true if and only if a child is updated. * @throws FormRelationsException This exception is propagated to the * calling method. */ private boolean insertAllIntoChild(ArrayList<TraverseData> saveFormMapping, ArrayList<TraverseData> saveInstanceMapping, Uri childInstance) throws FormRelationsException { boolean isInstanceModified = false; String childInstancePath = getInstancePath(childInstance); try { Document document = getDocument(childInstancePath); // here we should have a good xml document in DOM if (saveFormMapping.size() > 0) { TraverseData td = saveFormMapping.get(0); ContentValues values = new ContentValues(); values.put(InstanceColumns.DISPLAY_NAME, td.instanceValue); Collect.getInstance().getContentResolver().update(childInstance, values, null, null); if (LOCAL_LOG) { Log.d(TAG, "Updated InstanceProvider to show correct instanceName: " + td.instanceValue); } } int repeatIndex = getRepeatIndex(saveFormMapping, saveInstanceMapping); long childId = getIdFromSingleUri(childInstance); for (TraverseData td : saveInstanceMapping) { try { boolean isThisModified = insertIntoChild(td, document); if (isThisModified) { // PMA-Logging BEGIN if (null == mUseLog) { Log.w(TAG, "Null mUseLog when should be initialized for child(" + childId + ")"); } else { mUseLog.log(UseLogContract.RELATION_CHANGE_VALUE, childId, td.attrValue, td.instanceValue); } // PMA-Logging END checkCopyBinaryFile(td, childInstancePath); } isInstanceModified = isInstanceModified || isThisModified; } catch (FormRelationsException e) { if (e.getErrorCode() == BAD_XPATH_INSTANCE) { Log.w(TAG, "Unable to insert value \'" + td.instanceValue + "\' into child at " + e.getInfo()); e.setInfo(String.valueOf(childId)); throw e; } } updateRelationsDatabase(mInstanceId, td.instanceXpath, repeatIndex, childId, td.attrValue, td.repeatableNode); } if (isInstanceModified) { // only need to update xml if something changed writeDocumentToFile(document, childInstancePath); // PMA-Logging BEGIN mUseLog.writeBackLogAndClose(); // PMA-Logging END // Set status to incomplete ContentValues values = new ContentValues(); values.put(InstanceColumns.STATUS, InstanceProviderAPI.STATUS_INCOMPLETE); Collect.getInstance().getContentResolver().update(childInstance, values, null, null); if (LOCAL_LOG) { Log.d(TAG, "Rewrote child instance because of changes at " + childInstancePath); } } } catch (FormRelationsException e) { throw e; } catch (FileNotFoundException e) { e.printStackTrace(); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } catch (XPathExpressionException e) { e.printStackTrace(); } catch (SAXException e) { e.printStackTrace(); } catch (TransformerException e) { e.printStackTrace(); } catch (ParserConfigurationException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return isInstanceModified; } /** * Inserts data into child form from one morsel of traversal data. * * @param td A `saveInstance` morsel of traversal data. * @param document The child form represented as a document. * @return Returns true if and only if the child instance is modified. * @throws XPathExpressionException Another possible error that should * abort this routine. * @throws FormRelationsException Thrown if the xpath data for the child * form is bad. */ private static boolean insertIntoChild(TraverseData td, Document document) throws XPathExpressionException, FormRelationsException { boolean isModified = false; String childInstanceValue = td.instanceValue; if (null != childInstanceValue) { // extract nodes using xpath XPath xpath = XPathFactory.newInstance().newXPath(); XPathExpression expression; String childInstanceXpath = td.attrValue; expression = xpath.compile(childInstanceXpath); Node node = (Node) expression.evaluate(document, XPathConstants.NODE); if (null == node) { throw new FormRelationsException(BAD_XPATH_INSTANCE, childInstanceXpath); } if (!node.getTextContent().equals(childInstanceValue)) { Log.v(TAG, "Found difference saving child form @ child node \'" + node.getNodeName() + "\'. Child: \'" + node.getTextContent() + "\' <> Parent: \'" + childInstanceValue + "\'"); node.setTextContent(childInstanceValue); isModified = true; } } return isModified; } /** * Removes a node identified by xpath from a document (instance). * * @param xpathStr The xpath for the node to remove. * @param document The document to mutate. * @return Returns true if and only if a node is removed. * @throws XPathExpressionException Another possible error that should * abort this routine. * @throws FormRelationsException Thrown if the xpath data for the * supplied document is bad. */ private static boolean removeFromDocument(String xpathStr, Document document) throws XPathExpressionException, FormRelationsException { boolean isModified = false; if (null != xpathStr) { // extract nodes using xpath XPath xpath = XPathFactory.newInstance().newXPath(); XPathExpression expression; expression = xpath.compile(xpathStr); Node node = (Node) expression.evaluate(document, XPathConstants.NODE); if (null == node) { throw new FormRelationsException(BAD_XPATH_INSTANCE, xpathStr); } if (LOCAL_LOG) { Log.i(TAG, "removeFromDocument -- attempting to delete: " + xpathStr); } Node removeNode = node.getParentNode(); removeNode.removeChild(node); isModified = true; } return isModified; } /** * Deletes a repeatable in parent and fixes affected siblings. * * This method is called from within `DeleteInstancesTask`. The instance * goes through the meat-grinder there. This method wipes up the trail of * blood. The repeat group in the parent form associated with this child * is removed. Also, the sibling information from the child is corrected * from in the relations database. * * If the form is instead a parent, the form relations database is * cleared of that parent id. * * @param instanceId The id of the instance to remove. */ public static void removeAllReferences(long instanceId) { boolean isParentModified = false; long parentId = FormRelationsDb.getParent(instanceId); if (parentId != -1) { try { String parentPath = getInstancePath(getInstanceUriFromId(parentId)); Document parentDocument = getDocument(parentPath); String repeatXpathToRemove = FormRelationsDb.getRepeatable(parentId, instanceId); isParentModified = removeFromDocument(repeatXpathToRemove, parentDocument); if (isParentModified) { writeDocumentToFile(parentDocument, parentPath); } } catch (FormRelationsException e) { if (e.getErrorCode() == PROVIDER_NO_INSTANCE) { Log.w(TAG, "Removing all references for " + instanceId + ". Parent in relations.db, but no parent in InstanceProvider"); } else if (e.getErrorCode() == BAD_XPATH_INSTANCE) { Log.w(TAG, "Unable to remove node @" + e.getInfo() + " from parent of instance (" + instanceId + ")"); } else { Log.w(TAG, "OTHER FORMRELATIONSEXCEPTION in removeAllReferences: " + e.getErrorCode()); } } catch (FileNotFoundException e) { e.printStackTrace(); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } catch (XPathExpressionException e) { e.printStackTrace(); } catch (SAXException e) { e.printStackTrace(); } catch (TransformerException e) { e.printStackTrace(); } catch (ParserConfigurationException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { if (isParentModified) { int repeatIndex = FormRelationsDb.getRepeatIndex(parentId, instanceId); FormRelationsDb.deleteChild(parentId, repeatIndex); } else { FormRelationsDb.deleteAsChild(instanceId); } } } FormRelationsDb.deleteAsParent(instanceId); } /** * Writes a Document object to the supplied path. * * @param document The document object * @param path The output path * @throws FileNotFoundException An exception that aborts writing to disk * @throws TransformerException An exception that aborts writing to disk */ private static void writeDocumentToFile(Document document, String path) throws FileNotFoundException, TransformerException { // there's a bug in streamresult that replaces spaces in the // filename with %20 // so we use a fileoutput stream // http://stackoverflow.com/questions/10301674/save-file-in-android-with-spaces-in-file-name File outputFile = new File(path); FileOutputStream fos = new FileOutputStream(outputFile); Result output = new StreamResult(fos); Source input = new DOMSource(document); Transformer transformer = TransformerFactory.newInstance().newTransformer(); transformer.transform(input, output); } /** * Ensures the given information exists as a record in the relations db. * * First the method checks if there already is a row with this * information. If there is not, then it is added to the form relations * database. * * @param parentId Parent instance id. * @param parentXpath Xpath to the paired node in the parent. * @param repeatIndex The repeat index, parsed out of the xpath. * @param childId Child instance id. * @param childXpath Xpath to the paired node in the child. * @param repeatableNode The root of the repeat group that contains child * information. * @return Returns true if and only if the row exists in the form * relations database */ private static boolean updateRelationsDatabase(long parentId, String parentXpath, int repeatIndex, long childId, String childXpath, String repeatableNode) { // First check if row exists. boolean rowExists = FormRelationsDb.isRowExists(String.valueOf(parentId), parentXpath, String.valueOf(repeatIndex), String.valueOf(childId), childXpath, repeatableNode); if (!rowExists) { if (LOCAL_LOG) { Log.v(TAG, "Inserting (parentId=" + parentId + ", parentNode=" + parentXpath + ", index=" + repeatIndex + ", childId=" + childId + ", childNode=" + childXpath + ", repeatableNode=" + repeatableNode + ") into relations database"); } // Otherwise add FormRelationsDb.insert(String.valueOf(parentId), parentXpath, String.valueOf(repeatIndex), String.valueOf(childId), childXpath, repeatableNode); rowExists = true; } return rowExists; } /** * Checks the value of a node, and if binary, the file is copied to child * * This check is performed on all data that is copied from parent to * child. * * @param td Traversal data for the current node * @param childInstancePath Path to the child instance save on disk * @return Returns true if and only if `copyBinaryFile` returns true. * @throws FormRelationsException An exception allowed to propagate from * subroutines in order to abort checking/copying. */ private boolean checkCopyBinaryFile(TraverseData td, String childInstancePath) throws FormRelationsException { boolean toReturn = false; String childInstanceValue = td.instanceValue; if (childInstanceValue.endsWith(".jpg") || childInstanceValue.endsWith(".jpeg") || childInstanceValue.endsWith(".3gpp") || childInstanceValue.endsWith(".3gp") || childInstanceValue.endsWith(".mp4") || childInstanceValue.endsWith(".png")) { // check for more extensions? Uri parentInstance = getInstanceUriFromId(mInstanceId); String parentInstancePath = getInstancePath(parentInstance); toReturn = copyBinaryFile(parentInstancePath, childInstancePath, childInstanceValue); } return toReturn; } /** * Copies a binary file from one instance to another. * * This method accepts paths to instances. The enclosing directories are * determined, from which appropriate source and destination paths are * generated for the file to be copied. * * @param parentInstancePath The path to the instance of the parent. * @param childInstancePath The path to the instance of the child. * @param filename The file name of the file to be copied. * @return Returns true if everything happens without a hitch. */ private static boolean copyBinaryFile(String parentInstancePath, String childInstancePath, String filename) { File parentFile = new File(parentInstancePath); File childFile = new File(childInstancePath); File parentImage = new File(parentFile.getParent() + "/" + filename); File childImage = new File(childFile.getParent() + "/" + filename); if (LOCAL_LOG) { Log.d(TAG, "copyBinaryFile \'" + filename + "\' from " + parentFile.getParent() + " to " + childFile.getParent()); } FileUtils.copyFile(parentImage, childImage); return true; } /** * Gets the id in the InstanceProvider for a given instance. * * @param instance The Uri for * @return Returns the id in the InstanceProvider for a given instance. * Returns -1 if no corresponding id is found. */ private static long getIdFromSingleUri(Uri instance) { long id = -1; if (Collect.getInstance().getContentResolver().getType(instance) .equals(InstanceColumns.CONTENT_ITEM_TYPE)) { String idStr = instance.getLastPathSegment(); id = Long.parseLong(idStr); } else if (Collect.getInstance().getContentResolver().getType(instance) .equals(FormsColumns.CONTENT_ITEM_TYPE)) { // if uri is for a form // first try to find by looking up absolute path FormController formController = Collect.getInstance().getFormController(); String[] projection = { InstanceColumns._ID }; String selection = InstanceColumns.INSTANCE_FILE_PATH + "=?"; String instancePath = formController.getInstancePath().getAbsolutePath(); String[] selectionArgs = { instancePath }; Cursor c = Collect.getInstance().getContentResolver().query(InstanceColumns.CONTENT_URI, projection, selection, selectionArgs, null); if (c != null) { if (c.getCount() > 0) { c.moveToFirst(); id = c.getLong(c.getColumnIndex(InstanceColumns._ID)); } c.close(); } } return id; } /** * Converts an id number to Uri for an instance. * * @param id Id number * @return Returns the corresponding InstanceProvider Uri. */ private static Uri getInstanceUriFromId(long id) { return Uri.withAppendedPath(InstanceColumns.CONTENT_URI, String.valueOf(id)); } /** * Converts an id number to Uri for a form. * * @param id Id number * @return Returns the corresponding FormProvider Uri. */ private static Uri getFormUriFromId(long id) { return Uri.withAppendedPath(FormsColumns.CONTENT_URI, String.valueOf(id)); } /** * Gets repeat index based off of traversal data * * Both saveFormMapping and saveInstanceMapping should have been filtered * so that they only have the same repeat index at this point. * * @param saveFormMapping All `saveForm` information gathered from * traversal, filtered for this child. * @param saveInstanceMapping All `saveInstance` information gathered from * traversal, filtered for this child. * @return Returns the repeat index that is found. * @throws FormRelationsException If no repeat index is found, then an * exception is thrown. */ private int getRepeatIndex(ArrayList<TraverseData> saveFormMapping, ArrayList<TraverseData> saveInstanceMapping) throws FormRelationsException { int repeatIndex = 0; if (!saveFormMapping.isEmpty()) { repeatIndex = saveFormMapping.get(0).repeatIndex; } else if (!saveInstanceMapping.isEmpty()) { repeatIndex = saveInstanceMapping.get(0).repeatIndex; } if (repeatIndex == 0) { throw new FormRelationsException(NO_REPEAT_NUMBER); } return repeatIndex; } /** * Gets the form id defined in a `saveForm`. * * @param saveFormMapping All `saveForm` information gathered from * traversal, filtered for this child. * @return Returns the found form id. * @throws FormRelationsException Raised only if `saveFormMapping` is * empty. */ private String getChildFormId(ArrayList<TraverseData> saveFormMapping) throws FormRelationsException { if (saveFormMapping.isEmpty()) { throw new FormRelationsException(NO_INSTANCE_NO_FORM); } String childFormId = saveFormMapping.get(0).attrValue; return childFormId; } /** * Gets the instance path from a Uri for one instance * * @param oneInstance A Uri for a single instance (has primary key _ID) * @return Returns the path found in the InstanceProvider. * @throws FormRelationsException If the InstanceProvider does not have * the required information, this exception is thrown. */ private static String getInstancePath(Uri oneInstance) throws FormRelationsException { String[] projection = { InstanceColumns.INSTANCE_FILE_PATH }; Cursor childCursor = Collect.getInstance().getContentResolver().query(oneInstance, projection, null, null, null); if (null == childCursor || childCursor.getCount() < 1) { throw new FormRelationsException(PROVIDER_NO_INSTANCE); } // If URI is for table, not for row, potential error (only getting the first row). childCursor.moveToFirst(); String instancePath = childCursor.getString(childCursor.getColumnIndex(InstanceColumns.INSTANCE_FILE_PATH)); childCursor.close(); return instancePath; } /** * Gets the instance path from the id number of an instance in the instance provider * * A public static method for other classes to access this functionality. * * @param id Id number of an instance. * @return Returns the path found in the InstanceProvider. If no path is * found, then null is returned */ public static String getInstancePath(long id) { String instancePath = null; try { Uri instanceUri = getInstanceUriFromId(id); instancePath = getInstancePath(instanceUri); } catch (FormRelationsException e) { Log.w(TAG, "No instance path found for instanceId(" + id + ")"); } return instancePath; } /** * A container for saving information discovered during traversal. */ private class TraverseData { String attr; String attrValue; String instanceXpath; String instanceValue; int repeatIndex; String repeatableNode; } // Cleans the input somewhat /** * Accepts raw information from traversal, cleans it, and stores it. * * The data is placed into a `TraverseData` object. Before storage, some * information is cleaned or parsed. The xpath is stripped of the initial * instance name information so that it starts with '/'. The repeat index * is parsed from the xpath. * * Depending on `isRelevant` the information goes into either the object * member `mAllTraverseData` or the object member `mNonRelevantSaveForm`. * * @param attr The tag attribute * @param attrValue The value associated with the attribute * @param instanceXpath The xpath to the tag (node) * @param instanceValue The value (text) stored inside the node * @param repeatableNode The nearest ancestor repeatable root * @param isRelevant Boolean, true if the node is relevant. * @throws FormRelationsException This exception is thrown if a * `deleteForm` is discovered. No more information is needed because this * form is going to be deleted. */ private void addTraverseData(String attr, String attrValue, String instanceXpath, String instanceValue, String repeatableNode, boolean isRelevant) throws FormRelationsException { TraverseData td = new TraverseData(); td.attr = attr; td.attrValue = attrValue; td.instanceXpath = cleanInstanceXpath(instanceXpath); td.instanceValue = instanceValue; td.repeatIndex = parseInstanceXpath(td.instanceXpath); td.repeatableNode = cleanInstanceXpath(repeatableNode); if (isRelevant) { if (DELETE_FORM.equals(attr)) { throw new FormRelationsException(DELETE_FORM); } mAllTraverseData.add(td); } else if (SAVE_FORM.equals(td.attr)) { mNonRelevantSaveForm.add(td); } } /** * Cleans an instance xpath when adding traverse data. * * @param instanceXpath The xpath of a given node. * @return Returns the instance path starting with the first '/'. */ private String cleanInstanceXpath(String instanceXpath) { String toReturn = null; if (instanceXpath != null) { int firstSlash = instanceXpath.indexOf("/"); if (firstSlash < 0) { toReturn = instanceXpath; } else { toReturn = instanceXpath.substring(firstSlash); } } return toReturn; } /** * Gets the largest child selector number in an xpath. * * From examination, xpaths have child selectors at each step of xpath * after root, i.e. /root/path[1]/to[1]/node[1]... etc. If any of the * child selectors are greater than one, then it must be a repeat group. * Usually children are created with the information from a repeat group. * * This method picks out the greatest child selector in the supplied * xpath. * * If there is more than one non-"1" child selector, then a warning is * logged. Perhaps an exception should be thrown? * * This method taught me how hard it is to debug Java code using an * Android device. * * @param instanceXpath Xpath to the node under examination * @return Returns a number greater than zero. */ private int parseInstanceXpath(String instanceXpath) { int repeatIndex = 1; int numNonOne = 0; int leftBracket = instanceXpath.indexOf("["); int safety = 100; while (leftBracket >= 0 && safety > 0) { int rightBracket = instanceXpath.indexOf("]", leftBracket); if (rightBracket < 0) { break; } try { String repeat = instanceXpath.substring(leftBracket + 1, rightBracket); int potentialRepeat = Integer.parseInt(repeat); if (potentialRepeat > 1) { repeatIndex = potentialRepeat; numNonOne += 1; } mMaxRepeatIndex = Math.max(mMaxRepeatIndex, repeatIndex); } catch (NumberFormatException e) { Log.w(TAG, "Error parsing repeat index to int: \'" + instanceXpath + "\'"); } leftBracket = instanceXpath.indexOf("[", rightBracket); safety--; } if (numNonOne > 1) { Log.w(TAG, "Multiple repeats detected in this XPath: \'" + instanceXpath + "\'"); } return repeatIndex; } /** * Traverses an instance and collects all form relations information. * * This is a recursive method that traverses a tree in a depth-first * search. It keeps a record of the nearest repeatable node as it goes. It * scans all attributes for all nodes in this search. * * @param te The current tree element * @param frm The `FormRelationsManager` object that stores traverse data. * @param repeatableNode The most recent repeatable node. It is null if * there is no repeatable ancestor node. * @throws FormRelationsException Thrown if a `deleteForm` is found to be * relevant, propagated from checkAttrs. */ private static void traverseInstance(TreeElement te, FormRelationsManager frm, String repeatableNode) throws FormRelationsException { for (int i = 0; i < te.getNumChildren(); i++) { TreeElement teChild = te.getChildAt(i); String ref = teChild.getRef().toString(true); if (ref.contains("@template")) { // skip template nodes (from Nafundi) continue; } if (teChild.isRepeatable()) { if (LOCAL_LOG) { Log.d(TAG, "In repeatable node @" + ref + " with index [" + teChild.getMult() + "]"); } repeatableNode = ref; } checkAttrs(teChild, frm, repeatableNode); // recurse if (teChild.getNumChildren() > 0) { traverseInstance(teChild, frm, repeatableNode); } } } /** * Checks attributes of a node for form relation material. * * @param te The current tree element * @param frm The `FormRelationsManager` object that stores traverse data. * @param repeatableNode The most recent repeatable node. * @throws FormRelationsException Thrown if a `deleteForm` is found to be * relevant, propagated from addTraverseData. */ private static void checkAttrs(TreeElement te, FormRelationsManager frm, String repeatableNode) throws FormRelationsException { List<TreeElement> attrs = te.getBindAttributes(); for (TreeElement attr : attrs) { boolean isFormRelationMaterial = SAVE_INSTANCE.equals(attr.getName()) || SAVE_FORM.equals(attr.getName()) || DELETE_FORM.equals(attr.getName()); if (isFormRelationMaterial) { String thisAttr = attr.getName(); String attrValue = attr.getAttributeValue(); String instanceXpath = te.getRef().toString(true); String instanceValue = null; if (te.getValue() != null) { instanceValue = te.getValue().getDisplayText(); } frm.addTraverseData(thisAttr, attrValue, instanceXpath, instanceValue, repeatableNode, te.isRelevant()); } } } /** * Gets a Document object for a given instance path. * * @param path The path to the instance * @return Returns a Document object for the file at the supplied path. * @throws ParserConfigurationException One of various exceptions that * abort the routine. * @throws SAXException One of various exceptions that abort the routine. * @throws IOException One of various exceptions that abort the routine. */ private static Document getDocument(String path) throws ParserConfigurationException, SAXException, IOException { File outputFile = new File(path); InputStream inputStream = new FileInputStream(outputFile); Reader reader = new InputStreamReader(inputStream, "UTF-8"); InputSource inputSource = new InputSource(reader); Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputSource); inputStream.close(); reader.close(); return document; } // Assumes one generation span max. /** * Get the set of instanceIds of all related forms * * @param instanceId The instance id of the form's family to check * @return Returns a set with the instance ids of all family members */ public static Set<Long> getRelatedForms(long instanceId) { Set<Long> relatedForms = new HashSet<Long>(); long[] children = FormRelationsDb.getChildren(instanceId); for (int i = 0; i < children.length; i++) { relatedForms.add(children[i]); } long parent = FormRelationsDb.getParent(instanceId); if (parent != -1) { relatedForms.add(parent); long[] siblings = FormRelationsDb.getChildren(parent); for (int i = 0; i < siblings.length; i++) { relatedForms.add(siblings[i]); } } return relatedForms; } /** * Checks parents, children, and siblings for finalized-ness. * * Checks if there are relations. Checks if parent is finalized. Checks if * siblings are finalized. Checks if children are finalized. * * Does not check if self is finalized. * * @param instanceId The instance id of the form's family to check * @return Returns an integer code for one of five outcomes. */ public static int getRelatedFormsFinalized(long instanceId) { int toReturn = NO_RELATIONS; long parent = FormRelationsDb.getParent(instanceId); long[] children = FormRelationsDb.getChildren(instanceId); if (parent != -1) { toReturn = PARENT_UNFINALIZED; try { boolean isParentFinalized = isInstanceFinalized(parent); if (isParentFinalized) { toReturn = ALL_FINALIZED; } } catch (FormRelationsException e) { Log.w(TAG, "Error searching for parent (" + parent + ") when trying to determine finalized-ness"); } long[] parentChildren = FormRelationsDb.getChildren(parent); if (toReturn != PARENT_UNFINALIZED) { try { for (int i = 0; i < parentChildren.length; i++) { boolean isParentChildFinalized = isInstanceFinalized(parentChildren[i]); if (!isParentChildFinalized) { toReturn = SIBLING_UNFINALIZED; break; } } } catch (FormRelationsException e) { toReturn = SIBLING_UNFINALIZED; Log.w(TAG, "Error searching for child (" + e.getInfo() + ") when trying to determine finalized-ness"); } } } if (toReturn != PARENT_UNFINALIZED && toReturn != SIBLING_UNFINALIZED) { if (children.length > 0) { toReturn = ALL_FINALIZED; } try { for (int i = 0; i < children.length; i++) { boolean isChildFinalized = isInstanceFinalized(children[i]); if (!isChildFinalized) { toReturn = CHILD_UNFINALIZED; break; } } } catch (FormRelationsException e) { toReturn = CHILD_UNFINALIZED; Log.w(TAG, "Error searching for child (" + e.getInfo() + ") when trying to determine finalized-ness"); } } return toReturn; } /** * Checks if an instance is finalized * * Being finalized is defined as not having STATUS_INCOMPLETE. That means * being sent / having problems being sent / being finalized counts. * * It is assumed that at the family of forms spans at most two * generations, i.e. no grandparents or grandchildren or beyond. * * @param instanceId The instance id * @return Returns true if and only if the instance is proven to be finalized. * @throws FormRelationsException This exception is raised if */ public static boolean isInstanceFinalized(long instanceId) throws FormRelationsException { boolean isFinalized = false; Uri instance = getInstanceUriFromId(instanceId); String[] projection = { InstanceColumns.STATUS }; Cursor cursor = Collect.getInstance().getContentResolver().query(instance, projection, null, null, null); if (cursor != null) { if (cursor.getCount() > 0) { cursor.moveToFirst(); String thisStatus = cursor.getString(cursor.getColumnIndex(InstanceColumns.STATUS)); if (!thisStatus.equals(InstanceProviderAPI.STATUS_INCOMPLETE)) { isFinalized = true; } } else { cursor.close(); throw new FormRelationsException(PROVIDER_NO_INSTANCE, String.valueOf(instanceId)); } cursor.close(); } else { throw new FormRelationsException(PROVIDER_NO_INSTANCE, String.valueOf(instanceId)); } return isFinalized; } /** * Gets how many forms to delete * * Pre-condition: a `FormRelationsManager` object has been initialized and * traverse data has been collected for a form. * * @return An integer representing how many forms to delete. */ public int getHowManyToDelete() { int howMany = 0; if (mHasDeleteForm) { howMany++; howMany += FormRelationsDb.getChildren(mInstanceId).length; } else { TreeSet<Integer> allRepeatIndices = new TreeSet<Integer>(); for (TraverseData td : mNonRelevantSaveForm) { allRepeatIndices.add(td.repeatIndex); } for (Integer i : allRepeatIndices) { if (FormRelationsDb.getChild(mInstanceId, i) != -1) { howMany++; } } } return howMany; } /** * Gets how many forms to delete * * This version of the method is called when removing a repeat. * * Pre-condition: a `FormRelationsManager` object has been initialized and * traverse data has been collected for a form. * * @param repeatIndex The index of the repeat to be removed * @return An integer representing how many forms to delete. */ public int getHowManyToDelete(int repeatIndex) { int howMany = 0; if (mHasDeleteForm) { howMany++; howMany += FormRelationsDb.getChildren(mInstanceId).length; } else { TreeSet<Integer> allRepeatIndices = new TreeSet<Integer>(); allRepeatIndices.add(repeatIndex); for (TraverseData td : mNonRelevantSaveForm) { allRepeatIndices.add(td.repeatIndex); } for (Integer i : allRepeatIndices) { if (FormRelationsDb.getChild(mInstanceId, i) != -1) { howMany++; } } } return howMany; } /** * Gets information to say what is scheduled for deletion * * Gets all repeat indices from non-relevant saveForm attributes and * checks if there are children associated with those indices. * * @return Returns one of three codes to say no deletions, delete this * form, or delete at least one child */ public int getWhatToDelete() { int returnCode = NO_DELETE; if (mHasDeleteForm) { returnCode = DELETE_THIS; } else if (mInstanceId != -1) { TreeSet<Integer> allRepeatIndices = new TreeSet<Integer>(); for (TraverseData td : mNonRelevantSaveForm) { allRepeatIndices.add(td.repeatIndex); } for (Integer i : allRepeatIndices) { if (FormRelationsDb.getChild(mInstanceId, i) != -1) { returnCode = DELETE_CHILD; break; } } } return returnCode; } /** * Gets information to say what is scheduled for deletion * * Gets all repeat indices from non-relevant saveForm attributes and * checks if there are children associated with those indices. This * version of the method is called when removing a repeat, so that index * is checked as well. * * @param repeatIndex The index of the repeat to be removed * @return Returns one of three codes to say no deletions, delete this * form, or delete at least one child */ public int getWhatToDelete(int repeatIndex) { int returnCode = NO_DELETE; if (mHasDeleteForm) { returnCode = DELETE_THIS; } else if (mInstanceId != -1) { TreeSet<Integer> allRepeatIndices = new TreeSet<Integer>(); allRepeatIndices.add(repeatIndex); for (TraverseData td : mNonRelevantSaveForm) { allRepeatIndices.add(td.repeatIndex); } for (Integer i : allRepeatIndices) { if (FormRelationsDb.getChild(mInstanceId, i) != -1) { returnCode = DELETE_CHILD; break; } } } return returnCode; } public void setDeleteForm(boolean val) { mHasDeleteForm = val; } public void setInstanceId(long id) { mInstanceId = id; } public long getInstanceId() { return mInstanceId; } public boolean getDeleteForm() { return mHasDeleteForm; } }