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.

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