/* * Copyright (C) 2009 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 com.radicaldynamic.groupinform.tasks; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.util.HashMap; import java.util.Map.Entry; import org.ektorp.Attachment; import org.ektorp.AttachmentInputStream; import org.ektorp.DocumentNotFoundException; import org.javarosa.core.model.FormDef; import org.javarosa.core.model.condition.EvaluationContext; import org.javarosa.core.model.instance.TreeElement; import org.javarosa.core.model.instance.TreeReference; import org.javarosa.core.reference.ReferenceManager; import org.javarosa.core.reference.RootTranslator; import org.javarosa.core.services.PrototypeManager; import org.javarosa.core.util.externalizable.DeserializationException; import org.javarosa.core.util.externalizable.ExtUtil; import org.javarosa.form.api.FormEntryController; import org.javarosa.form.api.FormEntryModel; import org.javarosa.xform.parse.XFormParseException; import org.javarosa.xform.parse.XFormParser; import org.javarosa.xform.util.XFormUtils; import org.odk.collect.android.logic.FileReferenceFactory; import org.odk.collect.android.logic.FormController; import org.odk.collect.android.utilities.FileUtils; import android.os.AsyncTask; import android.util.Log; import com.radicaldynamic.groupinform.activities.FormEntryActivity; import com.radicaldynamic.groupinform.application.Collect; import com.radicaldynamic.groupinform.documents.FormDefinition; import com.radicaldynamic.groupinform.documents.FormInstance; import com.radicaldynamic.groupinform.listeners.FormLoaderListener; import com.radicaldynamic.groupinform.utilities.FileUtilsExtended; /** * Background task for loading a form. * * @author Carl Hartung (carlhartung@gmail.com) * @author Yaw Anokwa (yanokwa@gmail.com) */ public class FormLoaderTask extends AsyncTask<String, String, FormLoaderTask.FECWrapper> { private final static String t = "FormLoaderTask"; /** * Classes needed to serialize objects. Need to put anything from JR in here. */ public final static String[] SERIALIABLE_CLASSES = { "org.javarosa.core.model.FormDef", "org.javarosa.core.model.GroupDef", "org.javarosa.core.model.QuestionDef", "org.javarosa.core.model.data.DateData", "org.javarosa.core.model.data.DateTimeData", "org.javarosa.core.model.data.DecimalData", "org.javarosa.core.model.data.GeoPointData", "org.javarosa.core.model.data.helper.BasicDataPointer", "org.javarosa.core.model.data.IntegerData", "org.javarosa.core.model.data.MultiPointerAnswerData", "org.javarosa.core.model.data.PointerAnswerData", "org.javarosa.core.model.data.SelectMultiData", "org.javarosa.core.model.data.SelectOneData", "org.javarosa.core.model.data.StringData", "org.javarosa.core.model.data.TimeData", "org.javarosa.core.services.locale.TableLocaleSource", "org.javarosa.xpath.expr.XPathArithExpr", "org.javarosa.xpath.expr.XPathBoolExpr", "org.javarosa.xpath.expr.XPathCmpExpr", "org.javarosa.xpath.expr.XPathEqExpr", "org.javarosa.xpath.expr.XPathFilterExpr", "org.javarosa.xpath.expr.XPathFuncExpr", "org.javarosa.xpath.expr.XPathNumericLiteral", "org.javarosa.xpath.expr.XPathNumNegExpr", "org.javarosa.xpath.expr.XPathPathExpr", "org.javarosa.xpath.expr.XPathStringLiteral", "org.javarosa.xpath.expr.XPathUnionExpr", "org.javarosa.xpath.expr.XPathVariableReference" }; private FormLoaderListener mStateListener; private String mErrorMsg; protected class FECWrapper { FormController controller; protected FECWrapper(FormController controller) { this.controller = controller; } protected FormController getController() { return controller; } protected void free() { controller = null; } } FECWrapper data; // BEGIN custom FormDefinition mFormDefinition = null; FormInstance mFormInstance = null; // END custom /** * Initialize {@link FormEntryController} with {@link FormDef} from binary or from XML. If given * an instance, it will be used to fill the {@link FormDef}. */ @Override protected FECWrapper doInBackground(String... path) { FormEntryController fec = null; FormDef fd = null; FileInputStream fis = null; mErrorMsg = null; // BEGIN custom File formDefinitionFile = new File(path[0]); String formId = formDefinitionFile.getName().substring(0, formDefinitionFile.getName().lastIndexOf(".")); // Retrieve form definition from database try { if (Collect.Log.DEBUG) Log.d(Collect.LOGTAG, t + ": retrieving form definition document " + formId); FileUtils.createFolder(formDefinitionFile.getParent()); FileUtils.createFolder(formDefinitionFile.getParent() + File.separator + FileUtilsExtended.MEDIA_DIR); mFormDefinition = Collect.getInstance().getDbService().getDb().get(FormDefinition.class, formId); HashMap<String, Attachment> attachments = (HashMap<String, Attachment>) mFormDefinition.getAttachments(); // Download attachments (form definition XML & other media) for (Entry<String, Attachment> entry : attachments.entrySet()) { // TODO: Skip this attachment if a cache file exists and is newer than the document create date/update date AttachmentInputStream ais = Collect.getInstance().getDbService().getDb().getAttachment(formId, entry.getKey()); FileOutputStream file; if (entry.getKey().equals("xml")) { file = new FileOutputStream(formDefinitionFile); } else { file = new FileOutputStream(formDefinitionFile.getParent() + File.separator + FileUtilsExtended.MEDIA_DIR + File.separator + entry.getKey()); } byte [] buffer = new byte[8192]; int bytesRead = 0; while ((bytesRead = ais.read(buffer)) != -1) { file.write(buffer, 0, bytesRead); } file.close(); ais.close(); } } catch (DocumentNotFoundException e) { if (Collect.Log.WARN) Log.w(Collect.LOGTAG, t + ": " + e.toString()); mErrorMsg = "The form that you requested could not be found. It may have been removed by one of your team members.\n\nSelect OK to refresh the screen and try again."; return null; } catch (Exception e) { if (Collect.Log.ERROR) Log.e(Collect.LOGTAG, t + ": unexpected exception while retrieving form definition: " + e.toString()); mErrorMsg = e.getMessage(); e.printStackTrace(); return null; } // END custom String formPath = path[0]; File formXml = new File(formPath); String formHash = FileUtils.getMd5Hash(formXml); // BEGIN custom // File formBin = new File(Collect.CACHE_PATH + "/" + formHash + ".formdef"); File formBin = new File(formDefinitionFile.getParent() + File.separator + formHash + ".formdef"); // END custom if (formBin.exists()) { // if we have binary, deserialize binary Log.i( t, "Attempting to load " + formXml.getName() + " from cached file: " + formBin.getAbsolutePath()); fd = deserializeFormDef(formBin); if (fd == null) { // some error occured with deserialization. Remove the file, and make a new .formdef // from xml Log.w(t, "Deserialization FAILED! Deleting cache file: " + formBin.getAbsolutePath()); formBin.delete(); } } if (fd == null) { // no binary, read from xml try { Log.i(t, "Attempting to load from: " + formXml.getAbsolutePath()); fis = new FileInputStream(formXml); fd = XFormUtils.getFormFromInputStream(fis); if (fd == null) { mErrorMsg = "Error reading XForm file"; } else { serializeFormDef(fd, formPath); } } catch (FileNotFoundException e) { e.printStackTrace(); mErrorMsg = e.getMessage(); } catch (XFormParseException e) { mErrorMsg = e.getMessage(); e.printStackTrace(); } catch (Exception e) { mErrorMsg = e.getMessage(); e.printStackTrace(); } } if (mErrorMsg != null) { return null; } // new evaluation context for function handlers EvaluationContext ec = new EvaluationContext(); fd.setEvaluationContext(ec); // create FormEntryController from formdef FormEntryModel fem = new FormEntryModel(fd); fec = new FormEntryController(fem); try { // import existing data into formdef if (FormEntryActivity.mInstancePath != null) { // This order is important. Import data, then initialize. // BEGIN custom // importData(FormEntryActivity.mInstancePath, fec); try { String instanceId = FormEntryActivity.mInstancePath.substring(FormEntryActivity.mInstancePath.lastIndexOf("/") + 1, FormEntryActivity.mInstancePath.lastIndexOf(".")); if (Collect.Log.DEBUG) Log.d(Collect.LOGTAG, t + ": retrieving form instance document " + instanceId); String instanceFolder = FormEntryActivity.mInstancePath.substring(0, FormEntryActivity.mInstancePath.lastIndexOf("/")); FileUtils.createFolder(instanceFolder); mFormInstance = Collect.getInstance().getDbService().getDb().get(FormInstance.class, instanceId); HashMap<String, Attachment> attachments = (HashMap<String, Attachment>) mFormInstance.getAttachments(); // Download attachments (form instance XML & other media) for (Entry<String, Attachment> entry : attachments.entrySet()) { AttachmentInputStream ais = Collect.getInstance().getDbService().getDb().getAttachment(instanceId, entry.getKey()); FileOutputStream file; if (entry.getKey().equals("xml")) { file = new FileOutputStream(FormEntryActivity.mInstancePath); } else { file = new FileOutputStream(instanceFolder + File.separator + entry.getKey()); } byte [] buffer = new byte[8192]; int bytesRead = 0; while ((bytesRead = ais.read(buffer)) != -1) { file.write(buffer, 0, bytesRead); } file.close(); ais.close(); } importData(FormEntryActivity.mInstancePath, fec); } catch (Exception e) { if (Collect.Log.ERROR) Log.e(Collect.LOGTAG, t + ": unexpected exception while retrieving form instance: " + e.toString()); mErrorMsg = e.getMessage(); e.printStackTrace(); } // END custom fd.initialize(false); } else { fd.initialize(true); } } catch (RuntimeException e) { mErrorMsg = e.getMessage(); return null; } // set paths to /sdcard/odk/forms/formfilename-media/ // BEGIN custom // String formFileName = formXml.getName().substring(0, formXml.getName().lastIndexOf(".")); // END custom // Remove previous forms ReferenceManager._().clearSession(); // This should get moved to the Application Class if (ReferenceManager._().getFactories().length == 0) { // this is /sdcard/odk // BEGIN custom // ReferenceManager._().addReferenceFactory( // new FileReferenceFactory(Environment.getExternalStorageDirectory() + "/odk")); ReferenceManager._().addReferenceFactory( new FileReferenceFactory(FileUtilsExtended.FORMS_PATH)); // END custom } // Set jr://... to point to /sdcard/odk/forms/filename-media/ // BEGIN custom // ReferenceManager._().addSessionRootTranslator( // new RootTranslator("jr://images/", "jr://file/forms/" + formFileName + "-media/")); // ReferenceManager._().addSessionRootTranslator( // new RootTranslator("jr://audio/", "jr://file/forms/" + formFileName + "-media/")); // ReferenceManager._().addSessionRootTranslator( // new RootTranslator("jr://video/", "jr://file/forms/" + formFileName + "-media/")); ReferenceManager._().addSessionRootTranslator( new RootTranslator("jr://images/", "jr://file/" + formId + "/media/")); ReferenceManager._().addSessionRootTranslator( new RootTranslator("jr://audio/", "jr://file/" + formId + "/media/")); ReferenceManager._().addSessionRootTranslator( new RootTranslator("jr://video/", "jr://file/" + formId + "/media/")); // END custom // clean up vars fis = null; fd = null; formBin = null; formXml = null; formPath = null; FormController fc = new FormController(fec); data = new FECWrapper(fc); return data; } public boolean importData(String filePath, FormEntryController fec) { // convert files into a byte array byte[] fileBytes = FileUtils.getFileAsBytes(new File(filePath)); // get the root of the saved and template instances TreeElement savedRoot = XFormParser.restoreDataModel(fileBytes, null).getRoot(); TreeElement templateRoot = fec.getModel().getForm().getInstance().getRoot().deepCopy(true); // weak check for matching forms if (!savedRoot.getName().equals(templateRoot.getName()) || savedRoot.getMult() != 0) { Log.e(t, "Saved form instance does not match template form definition"); return false; } else { // populate the data model TreeReference tr = TreeReference.rootRef(); tr.add(templateRoot.getName(), TreeReference.INDEX_UNBOUND); templateRoot.populate(savedRoot, fec.getModel().getForm()); // populated model to current form fec.getModel().getForm().getInstance().setRoot(templateRoot); // fix any language issues // : http://bitbucket.org/javarosa/main/issue/5/itext-n-appearing-in-restored-instances if (fec.getModel().getLanguages() != null) { fec.getModel() .getForm() .localeChanged(fec.getModel().getLanguage(), fec.getModel().getForm().getLocalizer()); } return true; } } /** * Read serialized {@link FormDef} from file and recreate as object. * * @param formDef serialized FormDef file * @return {@link FormDef} object */ public FormDef deserializeFormDef(File formDef) { // TODO: any way to remove reliance on jrsp? // need a list of classes that formdef uses PrototypeManager.registerPrototypes(SERIALIABLE_CLASSES); FileInputStream fis = null; FormDef fd = null; try { // create new form def fd = new FormDef(); fis = new FileInputStream(formDef); DataInputStream dis = new DataInputStream(fis); // read serialized formdef into new formdef fd.readExternal(dis, ExtUtil.defaultPrototypes()); dis.close(); } catch (FileNotFoundException e) { e.printStackTrace(); fd = null; } catch (IOException e) { e.printStackTrace(); fd = null; } catch (DeserializationException e) { e.printStackTrace(); fd = null; } catch (Exception e) { e.printStackTrace(); fd = null; } return fd; } /** * Write the FormDef to the file system as a binary blog. * * @param filepath path to the form file */ public void serializeFormDef(FormDef fd, String filepath) { // calculate unique md5 identifier String hash = FileUtils.getMd5Hash(new File(filepath)); // BEGIN custom // File formDef = new File(Collect.CACHE_PATH + "/" + hash + ".formdef"); String formId = filepath.substring(filepath.lastIndexOf("/") + 1, filepath.lastIndexOf(".")); File formDef = new File(FileUtilsExtended.FORMS_PATH + File.separator + formId + File.separator + hash + ".formdef"); // END custom // formdef does not exist, create one. if (!formDef.exists()) { FileOutputStream fos; try { fos = new FileOutputStream(formDef); DataOutputStream dos = new DataOutputStream(fos); fd.writeExternal(dos); dos.flush(); dos.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } } @Override protected void onPostExecute(FECWrapper wrapper) { synchronized (this) { if (mStateListener != null) { if (wrapper == null) { mStateListener.loadingError(mErrorMsg); } else { // BEGIN custom // mStateListener.loadingComplete(wrapper.getController()); mStateListener.loadingComplete(wrapper.getController(), mFormDefinition, mFormInstance); // END custom } } } } public void setFormLoaderListener(FormLoaderListener sl) { synchronized (this) { mStateListener = sl; } } public void destroy() { if (data != null) { data.free(); data = null; } } }