Java tutorial
/* * Copyright (C) 2011 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.briefcase.util; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.text.DateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Logger; import javax.crypto.Cipher; import javax.crypto.CipherInputStream; import javax.crypto.NoSuchPaddingException; import org.apache.commons.codec.binary.Base64; import org.apache.commons.io.FileUtils; import org.bushe.swing.event.EventBus; import org.javarosa.core.model.instance.AbstractTreeElement; import org.javarosa.core.model.instance.TreeElement; import org.kxml2.kdom.Document; import org.kxml2.kdom.Element; import org.kxml2.kdom.Node; import org.opendatakit.briefcase.model.BriefcaseFormDefinition; import org.opendatakit.briefcase.model.CryptoException; import org.opendatakit.briefcase.model.ExportProgressEvent; import org.opendatakit.briefcase.model.FileSystemException; import org.opendatakit.briefcase.model.ParsingException; import org.opendatakit.briefcase.model.TerminationFuture; import org.opendatakit.briefcase.util.XmlManipulationUtils.FormInstanceMetadata; public class ExportToCsv implements ITransformFormAction { private static final String MEDIA_DIR = "media"; static final Logger log = Logger.getLogger(ExportToCsv.class.getName()); File outputDir; File outputMediaDir; String baseFilename; BriefcaseFormDefinition briefcaseLfd; TerminationFuture terminationFuture; Map<TreeElement, OutputStreamWriter> fileMap = new HashMap<TreeElement, OutputStreamWriter>(); boolean exportMedia = true; Date startDate; Date endDate; boolean overwrite = false; // Default briefcase constructor public ExportToCsv(File outputDir, BriefcaseFormDefinition lfd, TerminationFuture terminationFuture) { this(outputDir, lfd, terminationFuture, lfd.getFormName(), true, false, null, null); } public ExportToCsv(File outputDir, BriefcaseFormDefinition lfd, TerminationFuture terminationFuture, String filename, boolean exportMedia, Boolean overwrite, Date start, Date end) { this.outputDir = outputDir; this.outputMediaDir = new File(outputDir, MEDIA_DIR); this.briefcaseLfd = lfd; this.terminationFuture = terminationFuture; // Strip .csv, it gets added later if (filename.endsWith(".csv")) { filename = filename.substring(0, filename.length() - 4); } this.baseFilename = filename; this.exportMedia = exportMedia; this.overwrite = overwrite; this.startDate = start; this.endDate = end; } @Override public boolean doAction() { boolean allSuccessful = true; File instancesDir; try { instancesDir = FileSystemUtils.getFormInstancesDirectory(briefcaseLfd.getFormDirectory()); } catch (FileSystemException e) { // emit status change... EventBus.publish(new ExportProgressEvent("Unable to access instances directory of form")); e.printStackTrace(); return false; } if (!outputDir.exists()) { if (!outputDir.mkdir()) { EventBus.publish(new ExportProgressEvent("Unable to create destination directory")); return false; } } if (exportMedia) { if (!outputMediaDir.exists()) { if (!outputMediaDir.mkdir()) { EventBus.publish(new ExportProgressEvent("Unable to create destination media directory")); return false; } } } if (!processFormDefinition()) { // weren't able to initialize the csv file... return false; } File[] instances = instancesDir.listFiles(); for (File instanceDir : instances) { if (terminationFuture.isCancelled()) { EventBus.publish(new ExportProgressEvent("ABORTED")); allSuccessful = false; break; } if (instanceDir.getName().startsWith(".")) continue; // Mac OSX allSuccessful = allSuccessful && processInstance(instanceDir); } for (OutputStreamWriter w : fileMap.values()) { try { w.flush(); w.close(); } catch (IOException e) { e.printStackTrace(); EventBus.publish(new ExportProgressEvent("Error flushing csv file")); allSuccessful = false; } } return allSuccessful; } private void emitString(OutputStreamWriter osw, boolean first, String string) throws IOException { osw.append(first ? "" : ","); if (string == null) return; if (string.length() == 0 || string.contains("\n") || string.contains("\"") || string.contains(",")) { string = string.replace("\"", "\"\""); string = "\"" + string + "\""; } osw.append(string); } private String getFullName(AbstractTreeElement e, TreeElement group) { List<String> names = new ArrayList<String>(); while (e != null && e != group) { names.add(e.getName()); e = e.getParent(); } StringBuilder b = new StringBuilder(); Collections.reverse(names); boolean first = true; for (String s : names) { if (!first) { b.append("-"); } first = false; b.append(s); } return b.toString(); } private Element findElement(Element submissionElement, String name) { if (submissionElement == null) { return null; } int maxChildren = submissionElement.getChildCount(); for (int i = 0; i < maxChildren; i++) { if (submissionElement.getType(i) == Node.ELEMENT) { Element e = submissionElement.getElement(i); if (name.equals(e.getName())) { return e; } } } return null; } private List<Element> findElementList(Element submissionElement, String name) { List<Element> ecl = new ArrayList<Element>(); int maxChildren = submissionElement.getChildCount(); for (int i = 0; i < maxChildren; i++) { if (submissionElement.getType(i) == Node.ELEMENT) { Element e = submissionElement.getElement(i); if (name.equals(e.getName())) { ecl.add(e); } } } return ecl; } private String getSubmissionValue(EncryptionInformation ei, TreeElement model, Element element) { // could not find element, return null if (element == null) { return null; } StringBuilder b = new StringBuilder(); int maxChildren = element.getChildCount(); for (int i = 0; i < maxChildren; i++) { if (element.getType(i) == Node.TEXT) { b.append(element.getText(i)); } } String rawElement = b.toString(); // Field-level encryption support -- experimental if (JavaRosaParserWrapper.isEncryptedField(model)) { InputStreamReader isr = null; try { Cipher c = ei.getCipher("field:" + model.getName(), model.getName()); isr = new InputStreamReader( new CipherInputStream(new ByteArrayInputStream(Base64.decodeBase64(rawElement)), c), "UTF-8"); b.setLength(0); int ch; while ((ch = isr.read()) != -1) { char theChar = (char) ch; b.append(theChar); } return b.toString(); } catch (IOException e) { e.printStackTrace(); log.severe(" element name: " + model.getName() + " exception: " + e.toString()); } catch (InvalidKeyException e) { e.printStackTrace(); log.severe(" element name: " + model.getName() + " exception: " + e.toString()); } catch (InvalidAlgorithmParameterException e) { e.printStackTrace(); log.severe(" element name: " + model.getName() + " exception: " + e.toString()); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); log.severe(" element name: " + model.getName() + " exception: " + e.toString()); } catch (NoSuchPaddingException e) { e.printStackTrace(); log.severe(" element name: " + model.getName() + " exception: " + e.toString()); } finally { if (isr != null) { try { isr.close(); } catch (IOException e) { e.printStackTrace(); } } } } return rawElement; } private boolean emitSubmissionCsv(OutputStreamWriter osw, EncryptionInformation ei, Element submissionElement, TreeElement primarySet, TreeElement treeElement, boolean first, String uniquePath, File instanceDir) throws IOException { // OK -- group with at least one element -- assume no value... // TreeElement list has the begin and end tags for the nested groups. // Swallow the end tag by looking to see if the prior and current // field names are the same. TreeElement prior = null; for (int i = 0; i < treeElement.getNumChildren(); ++i) { TreeElement current = (TreeElement) treeElement.getChildAt(i); log.fine(" element name: " + current.getName()); if ((prior != null) && (prior.getName().equals(current.getName()))) { // it is the end-group tag... seems to happen with two adjacent repeat // groups log.info("repeating tag at " + i + " skipping " + current.getName()); prior = current; } else { Element ec = findElement(submissionElement, current.getName()); switch (current.getDataType()) { case org.javarosa.core.model.Constants.DATATYPE_TEXT:/** * Text question * type. */ case org.javarosa.core.model.Constants.DATATYPE_INTEGER:/** * Numeric * question type. These are numbers without decimal points */ case org.javarosa.core.model.Constants.DATATYPE_DECIMAL:/** * Decimal * question type. These are numbers with decimals */ case org.javarosa.core.model.Constants.DATATYPE_CHOICE:/** * This is a * question with alist of options where not more than one option can * be selected at a time. */ case org.javarosa.core.model.Constants.DATATYPE_CHOICE_LIST:/** * This is a * question with alist of options where more than one option can be * selected at a time. */ case org.javarosa.core.model.Constants.DATATYPE_BOOLEAN:/** * Question with * true and false answers. */ case org.javarosa.core.model.Constants.DATATYPE_BARCODE:/** * Question with * barcode string answer. */ default: case org.javarosa.core.model.Constants.DATATYPE_UNSUPPORTED: if (ec == null) { emitString(osw, first, null); } else { emitString(osw, first, getSubmissionValue(ei, current, ec)); } first = false; break; case org.javarosa.core.model.Constants.DATATYPE_DATE: /** * Date question type. This has only date component without time. */ if (ec == null) { emitString(osw, first, null); } else { String value = getSubmissionValue(ei, current, ec); if (value == null || value.length() == 0) { emitString(osw, first, null); } else { Date date = WebUtils.parseDate(value); DateFormat formatter = DateFormat.getDateInstance(); emitString(osw, first, formatter.format(date)); } } first = false; break; case org.javarosa.core.model.Constants.DATATYPE_TIME: /** * Time question type. This has only time element without date */ if (ec == null) { emitString(osw, first, null); } else { String value = getSubmissionValue(ei, current, ec); if (value == null || value.length() == 0) { emitString(osw, first, null); } else { Date date = WebUtils.parseDate(value); DateFormat formatter = DateFormat.getTimeInstance(); emitString(osw, first, formatter.format(date)); } } first = false; break; case org.javarosa.core.model.Constants.DATATYPE_DATE_TIME: /** * Date and Time question type. This has both the date and time * components */ if (ec == null) { emitString(osw, first, null); } else { String value = getSubmissionValue(ei, current, ec); if (value == null || value.length() == 0) { emitString(osw, first, null); } else { Date date = WebUtils.parseDate(value); DateFormat formatter = DateFormat.getDateTimeInstance(); emitString(osw, first, formatter.format(date)); } } first = false; break; case org.javarosa.core.model.Constants.DATATYPE_GEOPOINT: /** * Question with location answer. */ String compositeValue = (ec == null) ? null : getSubmissionValue(ei, current, ec); compositeValue = (compositeValue == null) ? null : compositeValue.trim(); // emit separate lat, long, alt, acc columns... if (compositeValue == null || compositeValue.length() == 0) { for (int count = 0; count < 4; ++count) { emitString(osw, first, null); first = false; } } else { String[] values = compositeValue.split(" "); for (String value : values) { emitString(osw, first, value); first = false; } for (int count = values.length; count < 4; ++count) { emitString(osw, first, null); first = false; } } break; case org.javarosa.core.model.Constants.DATATYPE_BINARY: /** * Question with external binary answer. */ String binaryFilename = getSubmissionValue(ei, current, ec); if (binaryFilename == null || binaryFilename.length() == 0) { emitString(osw, first, null); first = false; } else { if (exportMedia) { int dotIndex = binaryFilename.lastIndexOf("."); String namePart = (dotIndex == -1) ? binaryFilename : binaryFilename.substring(0, dotIndex); String extPart = (dotIndex == -1) ? "" : binaryFilename.substring(dotIndex); File binaryFile = new File(instanceDir, binaryFilename); String destBinaryFilename = binaryFilename; int version = 1; File destFile = new File(outputMediaDir, destBinaryFilename); while (destFile.exists()) { destBinaryFilename = namePart + "-" + (++version) + extPart; destFile = new File(outputMediaDir, destBinaryFilename); } if (binaryFile.exists()) { FileUtils.copyFile(binaryFile, destFile); } emitString(osw, first, MEDIA_DIR + File.separator + destFile.getName()); } else { emitString(osw, first, binaryFilename); } first = false; } break; case org.javarosa.core.model.Constants.DATATYPE_NULL: /* * for nodes that * have no data, * or data type * otherwise * unknown */ if (current.isRepeatable()) { if (prior == null || !current.getName().equals(prior.getName())) { // repeatable group... if (ec == null) { emitString(osw, first, null); first = false; } else { String uniqueGroupPath = uniquePath + "/" + getFullName(current, primarySet); emitString(osw, first, uniqueGroupPath); first = false; // first time processing this repeat group (ignore templates) List<Element> ecl = findElementList(submissionElement, current.getName()); emitRepeatingGroupCsv(ei, ecl, current, uniquePath, uniqueGroupPath, instanceDir); } } } else if (current.getNumChildren() == 0 && current != briefcaseLfd.getSubmissionElement()) { // assume fields that don't have children are string fields. if (ec == null) { emitString(osw, first, null); first = false; } else { emitString(osw, first, getSubmissionValue(ei, current, ec)); first = false; } } else { /* one or more children -- this is a non-repeating group */ first = emitSubmissionCsv(osw, ei, ec, primarySet, current, first, uniquePath, instanceDir); } break; } prior = current; } } return first; } private void emitRepeatingGroupCsv(EncryptionInformation ei, List<Element> groupElementList, TreeElement group, String uniqueParentPath, String uniqueGroupPath, File instanceDir) throws IOException { OutputStreamWriter osw = fileMap.get(group); int trueOrdinal = 1; for (Element groupElement : groupElementList) { String uniqueGroupInstancePath = uniqueGroupPath + "[" + trueOrdinal + "]"; boolean first = true; first = emitSubmissionCsv(osw, ei, groupElement, group, group, first, uniqueGroupInstancePath, instanceDir); emitString(osw, first, uniqueParentPath); emitString(osw, false, uniqueGroupInstancePath); emitString(osw, false, uniqueGroupPath); osw.append("\n"); ++trueOrdinal; } } private boolean emitCsvHeaders(OutputStreamWriter osw, TreeElement primarySet, TreeElement treeElement, boolean first) throws IOException { // OK -- group with at least one element -- assume no value... // TreeElement list has the begin and end tags for the nested groups. // Swallow the end tag by looking to see if the prior and current // field names are the same. TreeElement prior = null; for (int i = 0; i < treeElement.getNumChildren(); ++i) { TreeElement current = (TreeElement) treeElement.getChildAt(i); if ((prior != null) && (prior.getName().equals(current.getName()))) { // it is the end-group tag... seems to happen with two adjacent repeat // groups log.info("repeating tag at " + i + " skipping " + current.getName()); prior = current; } else { switch (current.getDataType()) { case org.javarosa.core.model.Constants.DATATYPE_TEXT:/** * Text question * type. */ case org.javarosa.core.model.Constants.DATATYPE_INTEGER:/** * Numeric * question type. These are numbers without decimal points */ case org.javarosa.core.model.Constants.DATATYPE_DECIMAL:/** * Decimal * question type. These are numbers with decimals */ case org.javarosa.core.model.Constants.DATATYPE_DATE:/** * Date question * type. This has only date component without time. */ case org.javarosa.core.model.Constants.DATATYPE_TIME:/** * Time question * type. This has only time element without date */ case org.javarosa.core.model.Constants.DATATYPE_DATE_TIME:/** * Date and * Time question type. This has both the date and time components */ case org.javarosa.core.model.Constants.DATATYPE_CHOICE:/** * This is a * question with alist of options where not more than one option can * be selected at a time. */ case org.javarosa.core.model.Constants.DATATYPE_CHOICE_LIST:/** * This is a * question with alist of options where more than one option can be * selected at a time. */ case org.javarosa.core.model.Constants.DATATYPE_BOOLEAN:/** * Question with * true and false answers. */ case org.javarosa.core.model.Constants.DATATYPE_BARCODE:/** * Question with * barcode string answer. */ case org.javarosa.core.model.Constants.DATATYPE_BINARY:/** * Question with * external binary answer. */ default: case org.javarosa.core.model.Constants.DATATYPE_UNSUPPORTED: emitString(osw, first, getFullName(current, primarySet)); first = false; break; case org.javarosa.core.model.Constants.DATATYPE_GEOPOINT: /** * Question with location answer. */ emitString(osw, first, getFullName(current, primarySet) + "-Latitude"); emitString(osw, false, getFullName(current, primarySet) + "-Longitude"); emitString(osw, false, getFullName(current, primarySet) + "-Altitude"); emitString(osw, false, getFullName(current, primarySet) + "-Accuracy"); first = false; break; case org.javarosa.core.model.Constants.DATATYPE_NULL: /* * for nodes that * have no data, * or data type * otherwise * unknown */ if (current.isRepeatable()) { // repeatable group... emitString(osw, first, "SET-OF-" + getFullName(current, primarySet)); first = false; processRepeatingGroupDefinition(current, primarySet, true); } else if (current.getNumChildren() == 0 && current != briefcaseLfd.getSubmissionElement()) { // assume fields that don't have children are string fields. emitString(osw, first, getFullName(current, primarySet)); first = false; } else { /* one or more children -- this is a non-repeating group */ first = emitCsvHeaders(osw, primarySet, current, first); } break; } prior = current; } } return first; } private void populateRepeatGroupsIntoFileMap(TreeElement primarySet, TreeElement treeElement) throws IOException { // OK -- group with at least one element -- assume no value... // TreeElement list has the begin and end tags for the nested groups. // Swallow the end tag by looking to see if the prior and current // field names are the same. TreeElement prior = null; for (int i = 0; i < treeElement.getNumChildren(); ++i) { TreeElement current = (TreeElement) treeElement.getChildAt(i); if ((prior != null) && (prior.getName().equals(current.getName()))) { // it is the end-group tag... seems to happen with two adjacent repeat // groups log.info("repeating tag at " + i + " skipping " + current.getName()); prior = current; } else { switch (current.getDataType()) { default: break; case org.javarosa.core.model.Constants.DATATYPE_NULL: /* for nodes that have no data, or data type otherwise unknown */ if (current.isRepeatable()) { processRepeatingGroupDefinition(current, primarySet, false); } else if (current.getNumChildren() == 0 && current != briefcaseLfd.getSubmissionElement()) { // ignore - string type } else { /* one or more children -- this is a non-repeating group */ populateRepeatGroupsIntoFileMap(primarySet, current); } break; } prior = current; } } } private void processRepeatingGroupDefinition(TreeElement group, TreeElement primarySet, boolean emitCsvHeaders) throws IOException { String formName = baseFilename + "-" + getFullName(group, primarySet); File topLevelCsv = new File(outputDir, safeFilename(formName) + ".csv"); FileOutputStream os = new FileOutputStream(topLevelCsv, !overwrite); OutputStreamWriter osw = new OutputStreamWriter(os, "UTF-8"); fileMap.put(group, osw); if (emitCsvHeaders) { boolean first = true; first = emitCsvHeaders(osw, group, group, first); emitString(osw, first, "PARENT_KEY"); emitString(osw, false, "KEY"); emitString(osw, false, "SET-OF-" + group.getName()); osw.append("\n"); } else { populateRepeatGroupsIntoFileMap(group, group); } } private String safeFilename(String name) { return name.replaceAll("\\p{Punct}", "_").replace("\\p{Space}", " "); } private boolean processFormDefinition() { TreeElement submission = briefcaseLfd.getSubmissionElement(); String formName = baseFilename; File topLevelCsv = new File(outputDir, safeFilename(formName) + ".csv"); boolean exists = topLevelCsv.exists(); FileOutputStream os; try { os = new FileOutputStream(topLevelCsv, !overwrite); OutputStreamWriter osw = new OutputStreamWriter(os, "UTF-8"); fileMap.put(submission, osw); // only write headers if overwrite is set, or creating file for the first time if (overwrite || !exists) { emitString(osw, true, "SubmissionDate"); emitCsvHeaders(osw, submission, submission, false); emitString(osw, false, "KEY"); if (briefcaseLfd.isFileEncryptedForm()) { emitString(osw, false, "isValidated"); } osw.append("\n"); } else { populateRepeatGroupsIntoFileMap(submission, submission); } } catch (FileNotFoundException e) { e.printStackTrace(); EventBus.publish(new ExportProgressEvent("Unable to create csv file: " + topLevelCsv.getPath())); for (OutputStreamWriter w : fileMap.values()) { try { w.close(); } catch (IOException e1) { e1.printStackTrace(); } } fileMap.clear(); return false; } catch (UnsupportedEncodingException e) { e.printStackTrace(); EventBus.publish(new ExportProgressEvent("Unable to create csv file: " + topLevelCsv.getPath())); for (OutputStreamWriter w : fileMap.values()) { try { w.close(); } catch (IOException e1) { e1.printStackTrace(); } } fileMap.clear(); return false; } catch (IOException e) { e.printStackTrace(); EventBus.publish(new ExportProgressEvent("Unable to create csv file: " + topLevelCsv.getPath())); for (OutputStreamWriter w : fileMap.values()) { try { w.close(); } catch (IOException e1) { e1.printStackTrace(); } } fileMap.clear(); return false; } return true; } private boolean processInstance(File instanceDir) { File submission = new File(instanceDir, "submission.xml"); if (!submission.exists() || !submission.isFile()) { EventBus.publish(new ExportProgressEvent( "Submission not found for instance directory: " + instanceDir.getPath())); return false; } EventBus.publish(new ExportProgressEvent("Processing instance: " + instanceDir.getName())); // If we are encrypted, be sure the temporary directory // that will hold the unencrypted files is created and empty. // If we aren't encrypted, the temporary directory // is the same as the instance directory. File unEncryptedDir; if (briefcaseLfd.isFileEncryptedForm()) { // create or clean-up the temp directory that will hold the unencrypted // files. Do this in the outputDir so that the briefcase storage location // can be a read-only network mount. issue 676. unEncryptedDir = new File(outputDir, ".temp"); if (unEncryptedDir.exists()) { // silently delete it... try { FileUtils.deleteDirectory(unEncryptedDir); } catch (IOException e) { e.printStackTrace(); EventBus.publish(new ExportProgressEvent( "Unable to delete stale temp directory: " + unEncryptedDir.getAbsolutePath())); return false; } } if (!unEncryptedDir.mkdirs()) { EventBus.publish(new ExportProgressEvent( "Unable to create temp directory: " + unEncryptedDir.getAbsolutePath())); return false; } } else { unEncryptedDir = instanceDir; } // parse the xml document (this is the manifest if encrypted)... Document doc; boolean isValidated = false; try { doc = XmlManipulationUtils.parseXml(submission); } catch (ParsingException e) { e.printStackTrace(); EventBus.publish(new ExportProgressEvent( "Error parsing submission " + instanceDir.getName() + " Cause: " + e.toString())); return false; } catch (FileSystemException e) { e.printStackTrace(); EventBus.publish(new ExportProgressEvent( "Error parsing submission " + instanceDir.getName() + " Cause: " + e.toString())); return false; } String submissionDate = null; // extract the submissionDate, if present, from the attributes // of the root element of the submission or submission manifest (if encrypted). submissionDate = doc.getRootElement().getAttributeValue(null, "submissionDate"); if (submissionDate == null || submissionDate.length() == 0) { submissionDate = null; } else { Date theDate = WebUtils.parseDate(submissionDate); DateFormat formatter = DateFormat.getDateTimeInstance(); submissionDate = formatter.format(theDate); // just return true to skip records out of range if (startDate != null && theDate.before(startDate)) { log.info("Submission date is before specified, skipping: " + instanceDir.getName()); return true; } if (endDate != null && theDate.after(endDate)) { log.info("Submission date is after specified, skipping: " + instanceDir.getName()); return true; } // don't export records without dates if either date is set if ((startDate != null || endDate != null) && submissionDate == null) { log.info("No submission date found, skipping: " + instanceDir.getName()); return true; } } // Beyond this point, we need to have a finally block that // will clean up any decrypted files whenever there is any // failure. try { if (briefcaseLfd.isFileEncryptedForm()) { // Decrypt the form and all its media files into the // unEncryptedDir and validate the contents of all // those files. // NOTE: this changes the value of 'doc' try { FileSystemUtils.DecryptOutcome outcome = FileSystemUtils.decryptAndValidateSubmission(doc, briefcaseLfd.getPrivateKey(), instanceDir, unEncryptedDir); doc = outcome.submission; isValidated = outcome.isValidated; } catch (ParsingException e) { e.printStackTrace(); EventBus.publish(new ExportProgressEvent( "Error decrypting submission " + instanceDir.getName() + " Cause: " + e.toString())); return false; } catch (FileSystemException e) { e.printStackTrace(); EventBus.publish(new ExportProgressEvent( "Error decrypting submission " + instanceDir.getName() + " Cause: " + e.toString())); return false; } catch (CryptoException e) { e.printStackTrace(); EventBus.publish(new ExportProgressEvent( "Error decrypting submission " + instanceDir.getName() + " Cause: " + e.toString())); return false; } } String instanceId = null; String base64EncryptedFieldKey = null; // find an instanceId to use... try { FormInstanceMetadata sim = XmlManipulationUtils.getFormInstanceMetadata(doc.getRootElement()); instanceId = sim.instanceId; base64EncryptedFieldKey = sim.base64EncryptedFieldKey; } catch (ParsingException e) { e.printStackTrace(); EventBus.publish(new ExportProgressEvent("Could not extract metadata from submission: " + submission.getAbsolutePath() + " Cause: " + e.toString())); return false; } if (instanceId == null || instanceId.length() == 0) { // if we have no instanceID, and there isn't any in the file, // use the checksum as the id. // NOTE: encrypted submissions always have instanceIDs. // This is for legacy non-OpenRosa forms. long checksum; try { checksum = FileUtils.checksumCRC32(submission); } catch (IOException e1) { e1.printStackTrace(); EventBus.publish(new ExportProgressEvent("Failed during computing of crc: " + e1.getMessage())); return false; } instanceId = "crc32:" + Long.toString(checksum); } if (terminationFuture.isCancelled()) { EventBus.publish(new ExportProgressEvent("ABORTED")); return false; } EncryptionInformation ei = null; if (base64EncryptedFieldKey != null) { try { ei = new EncryptionInformation(base64EncryptedFieldKey, instanceId, briefcaseLfd.getPrivateKey()); } catch (CryptoException e) { e.printStackTrace(); EventBus.publish(new ExportProgressEvent("Error establishing field decryption for submission " + instanceDir.getName() + " Cause: " + e.toString())); return false; } } // emit the csv record... try { OutputStreamWriter osw = fileMap.get(briefcaseLfd.getSubmissionElement()); emitString(osw, true, submissionDate); emitSubmissionCsv(osw, ei, doc.getRootElement(), briefcaseLfd.getSubmissionElement(), briefcaseLfd.getSubmissionElement(), false, instanceId, unEncryptedDir); emitString(osw, false, instanceId); if (briefcaseLfd.isFileEncryptedForm()) { emitString(osw, false, Boolean.toString(isValidated)); if (!isValidated) { EventBus.publish(new ExportProgressEvent("Decrypted submission " + instanceDir.getName() + " may be missing attachments and could not be validated.")); } } osw.append("\n"); return true; } catch (IOException e) { e.printStackTrace(); EventBus.publish(new ExportProgressEvent("Failed writing csv: " + e.getMessage())); return false; } } finally { if (briefcaseLfd.isFileEncryptedForm()) { // destroy the temp directory and its contents... try { FileUtils.deleteDirectory(unEncryptedDir); } catch (IOException e) { e.printStackTrace(); EventBus.publish( new ExportProgressEvent("Unable to remove decrypted files: " + e.getMessage())); return false; } } } } @Override public BriefcaseFormDefinition getFormDefinition() { return briefcaseLfd; } }