Repository for the HealthTool which enables Apple users to analyse their health data from the Apple health app and prepares the data for contributing it for future studies on wearable data.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

624 lines
19 KiB

3 years ago
  1. package application.parsing;
  2. import java.io.BufferedWriter;
  3. import java.io.File;
  4. import java.io.IOException;
  5. import java.io.Writer;
  6. import java.text.ParseException;
  7. import java.util.Calendar;
  8. import java.util.GregorianCalendar;
  9. import java.util.HashMap;
  10. import org.xml.sax.Attributes;
  11. import org.xml.sax.SAXException;
  12. import org.xml.sax.helpers.DefaultHandler;
  13. import application.enums.BiologicalSex;
  14. import application.enums.BloodType;
  15. import application.enums.EntryType;
  16. import application.enums.SkinType;
  17. import application.helpers.Utils;
  18. import application.helpers.wrappers.AttributeDataInfoTuple;
  19. import application.helpers.wrappers.GeneralSaveInfo;
  20. import application.helpers.wrappers.MeSaveInfo;
  21. import application.helpers.wrappers.Occupation;
  22. import application.helpers.wrappers.WrappedException;
  23. import application.helpers.wrappers.WriteSelection;
  24. import application.res.Text;
  25. public class WriteHandler extends DefaultHandler
  26. {
  27. /**
  28. * Contains the StringBuilders for the nested attributes which need to be added
  29. * to the complete Row. Key is the tag name of the nested attribute
  30. */
  31. private HashMap<String, AttributeDataInfoTuple> nestedAttributes;
  32. /** The text to be written as one row into the files **/
  33. private StringBuilder completeRow;
  34. /** the selection of the user **/
  35. private WriteSelection selection;
  36. /** all files which will be written. Key is the readable name **/
  37. private HashMap<String, FileWrapper> allFiles;
  38. /**
  39. * In case of records and workouts there will be sub elements. The name will be
  40. * saved here.
  41. **/
  42. private String subElementName = null;
  43. /**
  44. * All error messages will be appended here as {@link DefaultHandler} does not
  45. * permit other exceptions in the startElement and endElement methods
  46. **/
  47. private StringBuilder errorMessages;
  48. /**
  49. * Initializes the variables and creates all files needed for the selected data
  50. *
  51. * @param selection The selection of the user
  52. * @param job The job given by the user
  53. */
  54. public WriteHandler(WriteSelection selection, Occupation job)
  55. {
  56. this.selection = selection;
  57. this.errorMessages = new StringBuilder();
  58. resetSubAttributes();
  59. // figure out what is selected
  60. // create files for the selected
  61. allFiles = new HashMap<>();
  62. try
  63. {
  64. boolean me = false;
  65. for (EntryType meElem : EntryType.ALL_ME_ONES)
  66. {
  67. if (this.selection.entrySelected(meElem))
  68. {
  69. me = true;
  70. }
  71. }
  72. if (!job.equals(Text.NOT_SET))
  73. { // job given
  74. me = true;
  75. }
  76. if (me)
  77. {
  78. FileWrapper fw = new FileWrapper("me_info.txt", "MainInfo");
  79. allFiles.put(fw.getReadableFilename(), fw);
  80. if (!job.getLevelOne().equals(Text.NOT_SET))
  81. {
  82. writeToMeFile(new MeSaveInfo(EntryType.JOB, Text.OCCUPATION_LEVEL_ONE, job.getLevelOne()));
  83. if (!job.getLevelTwo().equals(Text.NOT_SET))
  84. {
  85. writeToMeFile(new MeSaveInfo(EntryType.JOB, Text.OCCUPATION_LEVEL_TWO, job.getLevelTwo()));
  86. if (!job.getLevelThree().equals(Text.NOT_SET))
  87. {
  88. writeToMeFile(new MeSaveInfo(EntryType.JOB, Text.OCCUPATION_LEVEL_THREE, job.getLevelThree()));
  89. if (!job.getLevelFour().equals(Text.NOT_SET))
  90. {
  91. writeToMeFile(new MeSaveInfo(EntryType.JOB, Text.OCCUPATION_LEVEL_FOUR, job.getLevelFour()));
  92. }
  93. }
  94. }
  95. }
  96. }
  97. if (this.selection.entrySelected(EntryType.ACTIVITY_SUMMARY))
  98. {
  99. FileWrapper fw = new FileWrapper("activity_summary.csv", EntryType.ACTIVITY_SUMMARY.getValue(), null,
  100. Utils.ACTIV_SUMM_ATTR);
  101. allFiles.put(fw.getReadableFilename(), fw);
  102. }
  103. if (this.selection.entrySelected(EntryType.WORKOUT))
  104. {
  105. for (String selectedType : selection.getWorkouts())
  106. {
  107. FileWrapper fw = new FileWrapper("workout_" + selectedType + ".csv", EntryType.WORKOUT.getValue(),
  108. Utils.shortenHKStrings(selectedType), Utils.workoutAttributes);
  109. allFiles.put(fw.getReadableFilename(), fw);
  110. }
  111. }
  112. if (this.selection.entrySelected(EntryType.RECORD))
  113. {
  114. for (String selectedType : selection.getRecords())
  115. {
  116. FileWrapper fw = new FileWrapper("record_" + selectedType + ".csv", EntryType.RECORD.getValue(),
  117. Utils.shortenHKStrings(selectedType), Utils.recordAttributes);
  118. allFiles.put(fw.getReadableFilename(), fw);
  119. }
  120. }
  121. }
  122. catch (WrappedException e)
  123. {
  124. errorMessages.append(e.getReason());
  125. errorMessages.append("\n");
  126. }
  127. catch (Exception e)
  128. {
  129. errorMessages.append(Text.E_CREATE_TEMP_FILES);
  130. errorMessages.append("\n");
  131. }
  132. }
  133. @Override
  134. public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException
  135. {
  136. try
  137. {
  138. // ME Entries not of the me tag
  139. if (selection.entrySelected(EntryType.REGION_CODE) && qName.equalsIgnoreCase(Text.TAG_NAME_HEALTH_DATA))
  140. {
  141. writeToMeFile(new MeSaveInfo(EntryType.REGION_CODE, attributes.getValue(Text.TAG_ATTR_REGION_CODE)));
  142. }
  143. else if (selection.entrySelected(EntryType.EXPORT_DATE)
  144. && qName.equalsIgnoreCase(EntryType.EXPORT_DATE.getValue()))
  145. {
  146. writeToMeFile(new MeSaveInfo(EntryType.EXPORT_DATE, attributes.getValue(Text.TAG_ATTR_EXPORT_DATE)));
  147. }
  148. // ME Tag
  149. else if (qName.equalsIgnoreCase(Text.TAG_NAME_ME_INFO))
  150. {
  151. for (EntryType me : EntryType.ALL_ORIGINAL_ME_ONES)
  152. {
  153. if (selection.entrySelected(me))
  154. {
  155. String dateOfBirthString = attributes.getValue(Text.TAG_ATTR_DATE_OF_BIRTH);
  156. // first the simple ones
  157. if (me != EntryType.DATE_OF_BIRTH)
  158. {
  159. String value = "";
  160. if (me == EntryType.BIOLOGICAL_SEX)
  161. {
  162. value = BiologicalSex.getValue(attributes.getValue(Text.TAG_ATTR_BIOLOGICAL_SEX))
  163. .getReadableValue();
  164. }
  165. else if (me == EntryType.BLOOD_TYPE)
  166. {
  167. value = BloodType.getValue(attributes.getValue(Text.TAG_ATTR_BLOOD_TYPE))
  168. .getReadableValue();
  169. }
  170. else if (me == EntryType.SKIN_TYPE)
  171. {
  172. value = SkinType.getValue(attributes.getValue(Text.TAG_ATTR_SKIN_TYPE))
  173. .getReadableValue();
  174. }
  175. writeToMeFile(new MeSaveInfo(me, value));
  176. }
  177. else if (dateOfBirthString != null && !dateOfBirthString.equals(""))
  178. {// now the date of birth with individually selected parts
  179. try
  180. {
  181. Calendar dateOfBirth = new GregorianCalendar();
  182. dateOfBirth.setTime(Utils.formatDateOfBirth.parse(dateOfBirthString));
  183. if (selection.birthPartSelected(Text.YEAR))
  184. {
  185. writeToMeFile(new MeSaveInfo(EntryType.DATE_OF_BIRTH, Text.YEAR,
  186. dateOfBirth.get(Calendar.YEAR) + ""));
  187. }
  188. if (selection.birthPartSelected(Text.MONTH))
  189. {
  190. writeToMeFile(new MeSaveInfo(EntryType.DATE_OF_BIRTH, Text.MONTH,
  191. "" + (dateOfBirth.get(Calendar.MONTH) + 1)));
  192. }
  193. if (selection.birthPartSelected(Text.DAY))
  194. {
  195. writeToMeFile(new MeSaveInfo(EntryType.DATE_OF_BIRTH, Text.DAY,
  196. dateOfBirth.get(Calendar.DAY_OF_MONTH) + ""));
  197. }
  198. }
  199. catch (ParseException e)
  200. {
  201. // bad luck. Do nothing if parsing error
  202. }
  203. }
  204. }
  205. }
  206. }
  207. // activity summaries: only one file and no special cases
  208. else if (selection.entrySelected(EntryType.ACTIVITY_SUMMARY)
  209. && qName.equalsIgnoreCase(EntryType.ACTIVITY_SUMMARY.getValue()))
  210. {
  211. writeToFile(new GeneralSaveInfo(getValuesToAttributes(Utils.ACTIV_SUMM_ATTR, attributes),
  212. EntryType.ACTIVITY_SUMMARY));
  213. }
  214. // The following ones might need completion in the endElement-method
  215. else if (qName.equalsIgnoreCase(EntryType.WORKOUT.getValue()) && selection.workoutSelected(attributes))
  216. {
  217. subElementName = Utils.shortenHKStrings(attributes.getValue(Text.TAG_ATTR_WORKOUT_TYPE));
  218. completeRow.append(getNonNestedAttributeValues(
  219. getValuesToAttributes(Utils.workoutAttributes, attributes), Utils.workoutAttributes, true));
  220. }
  221. else if (qName.equalsIgnoreCase(EntryType.RECORD.getValue()) && selection.recordSelected(attributes))
  222. {
  223. subElementName = Utils.shortenHKStrings(attributes.getValue(Text.TAG_ATTR_RECORD_TYPE));
  224. completeRow.append(getNonNestedAttributeValues(
  225. getValuesToAttributes(Utils.recordAttributes, attributes), Utils.recordAttributes, true));
  226. }
  227. // These are now nested elements with nested attributes
  228. else if (subElementName != null && isNestedAttribute(qName))
  229. {
  230. for (String tagName : nestedAttributes.keySet())
  231. {
  232. if (qName.equals(tagName))
  233. { // all entries in attributes
  234. AttributeDataInfoTuple tuple = nestedAttributes.get(tagName);
  235. if (tagName.equals(Text.TAG_NAME_HR_LIST))
  236. { // this one is completely nested
  237. tuple.getBuilder().append("[");
  238. }
  239. else
  240. {
  241. tuple.getBuilder()
  242. .append(getNonNestedAttributeValues(
  243. getValuesToAttributes(tuple.getSubAttributes(), attributes),
  244. tuple.getSubAttributes(), false));
  245. if (!tagName.equals(Text.TAG_NAME_WORKOUT_ROUTE))
  246. { // workout route is nested again and isn't complete yet
  247. tuple.getBuilder().append(",");
  248. }
  249. }
  250. }
  251. }
  252. }
  253. }
  254. catch (WrappedException e)
  255. {
  256. errorMessages.append(e.getReason());
  257. errorMessages.append("\n");
  258. }
  259. }
  260. /**
  261. * Writes all attributes of an element up to the first nested attribute
  262. *
  263. * @param values attribute values to be written
  264. * @param names attribute names to check for nesting
  265. * @param top indicates whether this is already a nested attribute
  266. * ({@code False}) or whether we are at top level ({@code True}
  267. * @return The complete string for the element if there are no nested values,
  268. * otherwise the incomplete beginning which needs to be finished with
  269. * {@code addAllNestedAttributes}
  270. */
  271. private String getNonNestedAttributeValues(String[] values, String[] names, boolean top)
  272. {
  273. StringBuilder res = new StringBuilder();
  274. // Start element with a bracket
  275. res.append("[");
  276. for (int i = 0; i < names.length; i++)
  277. {
  278. if (isNestedAttribute(names[i]))
  279. { // stop if attribute is nested, then we need to finish at another point
  280. // here is no bracket at the end!
  281. return res.toString();
  282. }
  283. res.append(values[i]);
  284. if (top)
  285. {
  286. res.append(";");
  287. }
  288. else
  289. {
  290. res.append(",");
  291. }
  292. }
  293. // replace last char (, or ;) with bracket
  294. res = res.replace(res.length() - 1, res.length(), "]");
  295. return res.toString();
  296. }
  297. /**
  298. * Determines for an attribute name whether it is a nested attribute
  299. *
  300. * @param attr the name to check
  301. * @return {@code True} if it is a nested attribute, {@code False} otherwise.
  302. */
  303. private boolean isNestedAttribute(String attr)
  304. {
  305. for (String nestedAttribute : nestedAttributes.keySet())
  306. {
  307. if (attr.equals(nestedAttribute))
  308. {
  309. return true;
  310. }
  311. }
  312. return false;
  313. }
  314. @Override
  315. public void endElement(String uri, String localName, String qName) throws SAXException
  316. {
  317. if (subElementName != null)
  318. { // we have a selected value which is nested / has nested attributes
  319. if (isNestedAttribute(qName))
  320. { // nested case: either hr_list or workout_route
  321. if (qName.equals(Text.TAG_NAME_HR_LIST))
  322. { // all entries in IBperMinutes
  323. appendSubEntry(Text.TAG_NAME_HR_LIST, Text.TAG_NAME_IB_PER_MINUTES);
  324. nestedAttributes.put(Text.TAG_NAME_IB_PER_MINUTES,
  325. new AttributeDataInfoTuple(Utils.IB_PER_MINUTES_ATTR));
  326. nestedAttributes.get(Text.TAG_NAME_HR_LIST).getBuilder().append("],");
  327. }
  328. if (qName.equals(Text.TAG_NAME_WORKOUT_ROUTE))
  329. { // last entry not in attributes! (MetaDataEntry)
  330. appendSubEntry(Text.TAG_NAME_WORKOUT_ROUTE, Text.TAG_NAME_META_DATA_ENTRY);
  331. nestedAttributes.put(Text.TAG_NAME_META_DATA_ENTRY,
  332. new AttributeDataInfoTuple(Utils.META_DATA_ATTR));
  333. nestedAttributes.get(Text.TAG_NAME_WORKOUT_ROUTE).getBuilder().append("],");
  334. }
  335. }
  336. else
  337. { // this can only be a record or workout
  338. // non nested ones were written in the start method, so only add the nested ones
  339. // now
  340. if (qName.equalsIgnoreCase(EntryType.WORKOUT.getValue()))
  341. {
  342. addAllNestedAttributes(completeRow, Utils.workoutAttributes);
  343. }
  344. else
  345. {
  346. addAllNestedAttributes(completeRow, Utils.recordAttributes);
  347. }
  348. completeRow = removeOuterBrackets(completeRow);
  349. FileWrapper fw = allFiles.get(FileWrapper.createReadableFileName(qName, subElementName));
  350. if (fw != null)
  351. {
  352. try
  353. {
  354. fw.getBufferedWriter().append(completeRow);
  355. fw.getBufferedWriter().newLine();
  356. }
  357. catch (IOException e)
  358. {
  359. errorMessages.append(Text.E_WRITE_TO_TEMP_FILES);
  360. errorMessages.append("\n");
  361. }
  362. }
  363. resetSubAttributes();
  364. }
  365. }
  366. }
  367. /**
  368. * Used for removing the outer Brackets in a StringBuilder, but in truth just
  369. * removes the first and last char of it.... So be careful, it doesn't check for
  370. * brackets!
  371. *
  372. * @param sb The StringBuilder to be shortened
  373. * @return The shortened StringBuilder
  374. */
  375. private StringBuilder removeOuterBrackets(StringBuilder sb)
  376. {
  377. sb = sb.replace(0, 1, "");
  378. return sb.replace(sb.length() - 1, sb.length(), "");
  379. }
  380. /**
  381. * Nested Attributes (which are ALLWAYS at the end of attribute lists) are here
  382. * appended at the end of the current row
  383. *
  384. * @param toAppend Contains the current row which should be appended
  385. * @param attributes The whole attribute list of the current row. But only the
  386. * nested ones will be appended thus the non nested ones need
  387. * to already be in the StringBuilder
  388. */
  389. private void addAllNestedAttributes(StringBuilder toAppend, String[] attributes)
  390. {
  391. for (int i = 0; i < attributes.length; i++)
  392. {
  393. for (String tagName : nestedAttributes.keySet())
  394. {
  395. if (attributes[i].equals(tagName))
  396. {
  397. appendSubEntry(toAppend, tagName);
  398. toAppend.append(";");
  399. }
  400. }
  401. }
  402. toAppend = toAppend.replace(toAppend.length() - 1, toAppend.length(), "]");
  403. }
  404. /**
  405. * Appends the text of a sub entry to its containing row. All sub entries (even
  406. * if they are empty) will be surrounded by brackets [].
  407. *
  408. * @param toAppendTo The containing row which should be appended
  409. * @param nestedInfo The name of the attribute containing the sub entry
  410. * information
  411. */
  412. private void appendSubEntry(StringBuilder toAppendTo, String nestedInfo)
  413. {
  414. toAppendTo.append("[");
  415. StringBuilder toAppendWith = nestedAttributes.get(nestedInfo).getBuilder();
  416. if (toAppendWith.length() > 0) // there is an element here, so there is also a ',' too much
  417. {
  418. toAppendTo.append(toAppendWith.substring(0, toAppendWith.length() - 1));
  419. }
  420. toAppendTo.append("]");
  421. }
  422. /**
  423. * Appends the text of a sub entry to its containing row. All sub entries (even
  424. * if they are empty) will be surrounded by brackets [].
  425. *
  426. * @param topLevelInfo The name of the attribute which should be appended with a
  427. * sub attribute
  428. * @param nestedInfo The name of the attribute containing the sub entry
  429. * information
  430. */
  431. private void appendSubEntry(String topLevelInfo, String nestedInfo)
  432. {
  433. appendSubEntry(nestedAttributes.get(topLevelInfo).getBuilder(), nestedInfo);
  434. }
  435. /**
  436. * /** Writes one line of information to a file (but not the ME File)
  437. *
  438. * @param info The information about what to write.
  439. * @throws WrappedException if the file could not be written
  440. */
  441. private void writeToFile(GeneralSaveInfo info) throws WrappedException
  442. {
  443. FileWrapper fw = allFiles.get(info.getReadableFilename());
  444. if (fw != null)
  445. {
  446. BufferedWriter bout = fw.getBufferedWriter();
  447. try
  448. {
  449. writeNormalRow(bout, info.getValues(), true);
  450. bout.newLine();
  451. }
  452. catch (IOException e)
  453. {
  454. throw new WrappedException(e, Text.E_WRITE_TO_TEMP_FILES);
  455. }
  456. }
  457. }
  458. /**
  459. * Writes one line of information to the me file
  460. *
  461. * @param info The information about what to write.
  462. * @throws WrappedException if the file could not be written
  463. */
  464. private void writeToMeFile(MeSaveInfo info) throws WrappedException
  465. {
  466. FileWrapper fw = allFiles.get(FileWrapper.createReadableFileName(Text.ME_FILE_DESC));
  467. if (fw != null)
  468. {
  469. BufferedWriter bout = fw.getBufferedWriter();
  470. try
  471. {
  472. bout.write(String.format(Text.F_ME_SAVE, info.getSaveKey(), info.getSaveValue()));
  473. bout.newLine();
  474. }
  475. catch (IOException e)
  476. {
  477. throw new WrappedException(e, Text.E_WRITE_TO_TEMP_FILES);
  478. }
  479. }
  480. }
  481. /**
  482. * Initializes the HashMap containing the nested attributes info, as well as the
  483. * completeRow and sets the subElementname to {@code null}
  484. */
  485. private void resetSubAttributes()
  486. {
  487. nestedAttributes = new HashMap<>();
  488. nestedAttributes.put(Text.TAG_NAME_META_DATA_ENTRY, new AttributeDataInfoTuple(Utils.META_DATA_ATTR));
  489. nestedAttributes.put(Text.TAG_NAME_WORKOUT_EVENT, new AttributeDataInfoTuple(Utils.WORKOUT_EVENT_ATTR));
  490. nestedAttributes.put(Text.TAG_NAME_WORKOUT_ROUTE, new AttributeDataInfoTuple(Utils.WORKOUT_ROUTE_ATTR));
  491. nestedAttributes.put(Text.TAG_NAME_HR_LIST, new AttributeDataInfoTuple(Utils.HR_LIST_ATTR));
  492. nestedAttributes.put(Text.TAG_NAME_IB_PER_MINUTES, new AttributeDataInfoTuple(Utils.IB_PER_MINUTES_ATTR));
  493. completeRow = new StringBuilder();
  494. subElementName = null;
  495. }
  496. /**
  497. * Gets the values for the given attributes and returns them in an array where
  498. * the values are saved at the index corresponding to their keys.
  499. *
  500. * @param attributeName Keys / names of the attributes.
  501. * @param values The object containing the values
  502. * @return The described array
  503. */
  504. private String[] getValuesToAttributes(String[] attributeName, Attributes values)
  505. {
  506. String[] res = new String[attributeName.length];
  507. for (int i = 0; i < res.length; i++)
  508. {
  509. String v = values.getValue(attributeName[i]);
  510. if(attributeName[i].equals("device")&&v!=null)
  511. {
  512. //Entry left out due to privacy
  513. String device="<HKDevice:";
  514. String actualDeviceSubstring=v.substring(v.indexOf(device),v.indexOf(">")+1);
  515. v=v.replace(actualDeviceSubstring, "device_anonymized");
  516. }
  517. if (v == null)
  518. {
  519. res[i] = "";
  520. }
  521. else if (v.contains(",") || v.contains(";"))
  522. {
  523. res[i] = "\"" + v + "\"";
  524. }
  525. else
  526. {
  527. res[i] = v;
  528. }
  529. }
  530. return res;
  531. }
  532. /**
  533. * Writes a non nested row into a file
  534. *
  535. * @param bout The writer of the file
  536. * @param values the values in correct order needed to fit with the header row
  537. * which should be written
  538. * @param top shows whether this is a "nested row" / nested attribute entry
  539. * @throws IOException if there was an error writing
  540. */
  541. private void writeNormalRow(Writer bout, String[] values, boolean top) throws IOException
  542. {
  543. for (int i = 0; i < values.length; i++) // for every attribute value
  544. {
  545. bout.write(values[i]);
  546. if (i < values.length - 1)
  547. {
  548. if (top)
  549. {
  550. bout.write(";");
  551. }
  552. else
  553. {
  554. bout.write(",");
  555. }
  556. }
  557. }
  558. }
  559. /**
  560. * Returns all files written in temp directory with the readable name as key and
  561. * the {@link File} as value. It also closes the writers and thus can only be
  562. * used after the end of writing.
  563. *
  564. * @return The described value.
  565. */
  566. public HashMap<String, File> getAllFiles()
  567. {
  568. HashMap<String, File> res = new HashMap<>();
  569. for (String key : allFiles.keySet())
  570. {
  571. FileWrapper fw = allFiles.get(key);
  572. try
  573. {
  574. fw.getBufferedWriter().close();
  575. }
  576. catch (IOException e)
  577. {
  578. // Stream couldn't be closed. Bad luck.
  579. }
  580. res.put(key, fw.getFile());
  581. }
  582. return res;
  583. }
  584. /**
  585. * Returns all accumulated error messages.
  586. *
  587. * @return The described value or {@code null} if there is no error message.
  588. */
  589. public String getErrorMessages()
  590. {
  591. if (errorMessages.length()==0)
  592. {
  593. return null;
  594. }
  595. return errorMessages.toString();
  596. }
  597. }