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

package application.parsing;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.Writer;
import java.text.ParseException;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.HashMap;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import application.enums.BiologicalSex;
import application.enums.BloodType;
import application.enums.EntryType;
import application.enums.SkinType;
import application.helpers.Utils;
import application.helpers.wrappers.AttributeDataInfoTuple;
import application.helpers.wrappers.GeneralSaveInfo;
import application.helpers.wrappers.MeSaveInfo;
import application.helpers.wrappers.Occupation;
import application.helpers.wrappers.WrappedException;
import application.helpers.wrappers.WriteSelection;
import application.res.Text;
public class WriteHandler extends DefaultHandler
{
/**
* Contains the StringBuilders for the nested attributes which need to be added
* to the complete Row. Key is the tag name of the nested attribute
*/
private HashMap<String, AttributeDataInfoTuple> nestedAttributes;
/** The text to be written as one row into the files **/
private StringBuilder completeRow;
/** the selection of the user **/
private WriteSelection selection;
/** all files which will be written. Key is the readable name **/
private HashMap<String, FileWrapper> allFiles;
/**
* In case of records and workouts there will be sub elements. The name will be
* saved here.
**/
private String subElementName = null;
/**
* All error messages will be appended here as {@link DefaultHandler} does not
* permit other exceptions in the startElement and endElement methods
**/
private StringBuilder errorMessages;
/**
* Initializes the variables and creates all files needed for the selected data
*
* @param selection The selection of the user
* @param job The job given by the user
*/
public WriteHandler(WriteSelection selection, Occupation job)
{
this.selection = selection;
this.errorMessages = new StringBuilder();
resetSubAttributes();
// figure out what is selected
// create files for the selected
allFiles = new HashMap<>();
try
{
boolean me = false;
for (EntryType meElem : EntryType.ALL_ME_ONES)
{
if (this.selection.entrySelected(meElem))
{
me = true;
}
}
if (!job.equals(Text.NOT_SET))
{ // job given
me = true;
}
if (me)
{
FileWrapper fw = new FileWrapper("me_info.txt", "MainInfo");
allFiles.put(fw.getReadableFilename(), fw);
if (!job.getLevelOne().equals(Text.NOT_SET))
{
writeToMeFile(new MeSaveInfo(EntryType.JOB, Text.OCCUPATION_LEVEL_ONE, job.getLevelOne()));
if (!job.getLevelTwo().equals(Text.NOT_SET))
{
writeToMeFile(new MeSaveInfo(EntryType.JOB, Text.OCCUPATION_LEVEL_TWO, job.getLevelTwo()));
if (!job.getLevelThree().equals(Text.NOT_SET))
{
writeToMeFile(new MeSaveInfo(EntryType.JOB, Text.OCCUPATION_LEVEL_THREE, job.getLevelThree()));
if (!job.getLevelFour().equals(Text.NOT_SET))
{
writeToMeFile(new MeSaveInfo(EntryType.JOB, Text.OCCUPATION_LEVEL_FOUR, job.getLevelFour()));
}
}
}
}
}
if (this.selection.entrySelected(EntryType.ACTIVITY_SUMMARY))
{
FileWrapper fw = new FileWrapper("activity_summary.csv", EntryType.ACTIVITY_SUMMARY.getValue(), null,
Utils.ACTIV_SUMM_ATTR);
allFiles.put(fw.getReadableFilename(), fw);
}
if (this.selection.entrySelected(EntryType.WORKOUT))
{
for (String selectedType : selection.getWorkouts())
{
FileWrapper fw = new FileWrapper("workout_" + selectedType + ".csv", EntryType.WORKOUT.getValue(),
Utils.shortenHKStrings(selectedType), Utils.workoutAttributes);
allFiles.put(fw.getReadableFilename(), fw);
}
}
if (this.selection.entrySelected(EntryType.RECORD))
{
for (String selectedType : selection.getRecords())
{
FileWrapper fw = new FileWrapper("record_" + selectedType + ".csv", EntryType.RECORD.getValue(),
Utils.shortenHKStrings(selectedType), Utils.recordAttributes);
allFiles.put(fw.getReadableFilename(), fw);
}
}
}
catch (WrappedException e)
{
errorMessages.append(e.getReason());
errorMessages.append("\n");
}
catch (Exception e)
{
errorMessages.append(Text.E_CREATE_TEMP_FILES);
errorMessages.append("\n");
}
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException
{
try
{
// ME Entries not of the me tag
if (selection.entrySelected(EntryType.REGION_CODE) && qName.equalsIgnoreCase(Text.TAG_NAME_HEALTH_DATA))
{
writeToMeFile(new MeSaveInfo(EntryType.REGION_CODE, attributes.getValue(Text.TAG_ATTR_REGION_CODE)));
}
else if (selection.entrySelected(EntryType.EXPORT_DATE)
&& qName.equalsIgnoreCase(EntryType.EXPORT_DATE.getValue()))
{
writeToMeFile(new MeSaveInfo(EntryType.EXPORT_DATE, attributes.getValue(Text.TAG_ATTR_EXPORT_DATE)));
}
// ME Tag
else if (qName.equalsIgnoreCase(Text.TAG_NAME_ME_INFO))
{
for (EntryType me : EntryType.ALL_ORIGINAL_ME_ONES)
{
if (selection.entrySelected(me))
{
String dateOfBirthString = attributes.getValue(Text.TAG_ATTR_DATE_OF_BIRTH);
// first the simple ones
if (me != EntryType.DATE_OF_BIRTH)
{
String value = "";
if (me == EntryType.BIOLOGICAL_SEX)
{
value = BiologicalSex.getValue(attributes.getValue(Text.TAG_ATTR_BIOLOGICAL_SEX))
.getReadableValue();
}
else if (me == EntryType.BLOOD_TYPE)
{
value = BloodType.getValue(attributes.getValue(Text.TAG_ATTR_BLOOD_TYPE))
.getReadableValue();
}
else if (me == EntryType.SKIN_TYPE)
{
value = SkinType.getValue(attributes.getValue(Text.TAG_ATTR_SKIN_TYPE))
.getReadableValue();
}
writeToMeFile(new MeSaveInfo(me, value));
}
else if (dateOfBirthString != null && !dateOfBirthString.equals(""))
{// now the date of birth with individually selected parts
try
{
Calendar dateOfBirth = new GregorianCalendar();
dateOfBirth.setTime(Utils.formatDateOfBirth.parse(dateOfBirthString));
if (selection.birthPartSelected(Text.YEAR))
{
writeToMeFile(new MeSaveInfo(EntryType.DATE_OF_BIRTH, Text.YEAR,
dateOfBirth.get(Calendar.YEAR) + ""));
}
if (selection.birthPartSelected(Text.MONTH))
{
writeToMeFile(new MeSaveInfo(EntryType.DATE_OF_BIRTH, Text.MONTH,
"" + (dateOfBirth.get(Calendar.MONTH) + 1)));
}
if (selection.birthPartSelected(Text.DAY))
{
writeToMeFile(new MeSaveInfo(EntryType.DATE_OF_BIRTH, Text.DAY,
dateOfBirth.get(Calendar.DAY_OF_MONTH) + ""));
}
}
catch (ParseException e)
{
// bad luck. Do nothing if parsing error
}
}
}
}
}
// activity summaries: only one file and no special cases
else if (selection.entrySelected(EntryType.ACTIVITY_SUMMARY)
&& qName.equalsIgnoreCase(EntryType.ACTIVITY_SUMMARY.getValue()))
{
writeToFile(new GeneralSaveInfo(getValuesToAttributes(Utils.ACTIV_SUMM_ATTR, attributes),
EntryType.ACTIVITY_SUMMARY));
}
// The following ones might need completion in the endElement-method
else if (qName.equalsIgnoreCase(EntryType.WORKOUT.getValue()) && selection.workoutSelected(attributes))
{
subElementName = Utils.shortenHKStrings(attributes.getValue(Text.TAG_ATTR_WORKOUT_TYPE));
completeRow.append(getNonNestedAttributeValues(
getValuesToAttributes(Utils.workoutAttributes, attributes), Utils.workoutAttributes, true));
}
else if (qName.equalsIgnoreCase(EntryType.RECORD.getValue()) && selection.recordSelected(attributes))
{
subElementName = Utils.shortenHKStrings(attributes.getValue(Text.TAG_ATTR_RECORD_TYPE));
completeRow.append(getNonNestedAttributeValues(
getValuesToAttributes(Utils.recordAttributes, attributes), Utils.recordAttributes, true));
}
// These are now nested elements with nested attributes
else if (subElementName != null && isNestedAttribute(qName))
{
for (String tagName : nestedAttributes.keySet())
{
if (qName.equals(tagName))
{ // all entries in attributes
AttributeDataInfoTuple tuple = nestedAttributes.get(tagName);
if (tagName.equals(Text.TAG_NAME_HR_LIST))
{ // this one is completely nested
tuple.getBuilder().append("[");
}
else
{
tuple.getBuilder()
.append(getNonNestedAttributeValues(
getValuesToAttributes(tuple.getSubAttributes(), attributes),
tuple.getSubAttributes(), false));
if (!tagName.equals(Text.TAG_NAME_WORKOUT_ROUTE))
{ // workout route is nested again and isn't complete yet
tuple.getBuilder().append(",");
}
}
}
}
}
}
catch (WrappedException e)
{
errorMessages.append(e.getReason());
errorMessages.append("\n");
}
}
/**
* Writes all attributes of an element up to the first nested attribute
*
* @param values attribute values to be written
* @param names attribute names to check for nesting
* @param top indicates whether this is already a nested attribute
* ({@code False}) or whether we are at top level ({@code True}
* @return The complete string for the element if there are no nested values,
* otherwise the incomplete beginning which needs to be finished with
* {@code addAllNestedAttributes}
*/
private String getNonNestedAttributeValues(String[] values, String[] names, boolean top)
{
StringBuilder res = new StringBuilder();
// Start element with a bracket
res.append("[");
for (int i = 0; i < names.length; i++)
{
if (isNestedAttribute(names[i]))
{ // stop if attribute is nested, then we need to finish at another point
// here is no bracket at the end!
return res.toString();
}
res.append(values[i]);
if (top)
{
res.append(";");
}
else
{
res.append(",");
}
}
// replace last char (, or ;) with bracket
res = res.replace(res.length() - 1, res.length(), "]");
return res.toString();
}
/**
* Determines for an attribute name whether it is a nested attribute
*
* @param attr the name to check
* @return {@code True} if it is a nested attribute, {@code False} otherwise.
*/
private boolean isNestedAttribute(String attr)
{
for (String nestedAttribute : nestedAttributes.keySet())
{
if (attr.equals(nestedAttribute))
{
return true;
}
}
return false;
}
@Override
public void endElement(String uri, String localName, String qName) throws SAXException
{
if (subElementName != null)
{ // we have a selected value which is nested / has nested attributes
if (isNestedAttribute(qName))
{ // nested case: either hr_list or workout_route
if (qName.equals(Text.TAG_NAME_HR_LIST))
{ // all entries in IBperMinutes
appendSubEntry(Text.TAG_NAME_HR_LIST, Text.TAG_NAME_IB_PER_MINUTES);
nestedAttributes.put(Text.TAG_NAME_IB_PER_MINUTES,
new AttributeDataInfoTuple(Utils.IB_PER_MINUTES_ATTR));
nestedAttributes.get(Text.TAG_NAME_HR_LIST).getBuilder().append("],");
}
if (qName.equals(Text.TAG_NAME_WORKOUT_ROUTE))
{ // last entry not in attributes! (MetaDataEntry)
appendSubEntry(Text.TAG_NAME_WORKOUT_ROUTE, Text.TAG_NAME_META_DATA_ENTRY);
nestedAttributes.put(Text.TAG_NAME_META_DATA_ENTRY,
new AttributeDataInfoTuple(Utils.META_DATA_ATTR));
nestedAttributes.get(Text.TAG_NAME_WORKOUT_ROUTE).getBuilder().append("],");
}
}
else
{ // this can only be a record or workout
// non nested ones were written in the start method, so only add the nested ones
// now
if (qName.equalsIgnoreCase(EntryType.WORKOUT.getValue()))
{
addAllNestedAttributes(completeRow, Utils.workoutAttributes);
}
else
{
addAllNestedAttributes(completeRow, Utils.recordAttributes);
}
completeRow = removeOuterBrackets(completeRow);
FileWrapper fw = allFiles.get(FileWrapper.createReadableFileName(qName, subElementName));
if (fw != null)
{
try
{
fw.getBufferedWriter().append(completeRow);
fw.getBufferedWriter().newLine();
}
catch (IOException e)
{
errorMessages.append(Text.E_WRITE_TO_TEMP_FILES);
errorMessages.append("\n");
}
}
resetSubAttributes();
}
}
}
/**
* Used for removing the outer Brackets in a StringBuilder, but in truth just
* removes the first and last char of it.... So be careful, it doesn't check for
* brackets!
*
* @param sb The StringBuilder to be shortened
* @return The shortened StringBuilder
*/
private StringBuilder removeOuterBrackets(StringBuilder sb)
{
sb = sb.replace(0, 1, "");
return sb.replace(sb.length() - 1, sb.length(), "");
}
/**
* Nested Attributes (which are ALLWAYS at the end of attribute lists) are here
* appended at the end of the current row
*
* @param toAppend Contains the current row which should be appended
* @param attributes The whole attribute list of the current row. But only the
* nested ones will be appended thus the non nested ones need
* to already be in the StringBuilder
*/
private void addAllNestedAttributes(StringBuilder toAppend, String[] attributes)
{
for (int i = 0; i < attributes.length; i++)
{
for (String tagName : nestedAttributes.keySet())
{
if (attributes[i].equals(tagName))
{
appendSubEntry(toAppend, tagName);
toAppend.append(";");
}
}
}
toAppend = toAppend.replace(toAppend.length() - 1, toAppend.length(), "]");
}
/**
* Appends the text of a sub entry to its containing row. All sub entries (even
* if they are empty) will be surrounded by brackets [].
*
* @param toAppendTo The containing row which should be appended
* @param nestedInfo The name of the attribute containing the sub entry
* information
*/
private void appendSubEntry(StringBuilder toAppendTo, String nestedInfo)
{
toAppendTo.append("[");
StringBuilder toAppendWith = nestedAttributes.get(nestedInfo).getBuilder();
if (toAppendWith.length() > 0) // there is an element here, so there is also a ',' too much
{
toAppendTo.append(toAppendWith.substring(0, toAppendWith.length() - 1));
}
toAppendTo.append("]");
}
/**
* Appends the text of a sub entry to its containing row. All sub entries (even
* if they are empty) will be surrounded by brackets [].
*
* @param topLevelInfo The name of the attribute which should be appended with a
* sub attribute
* @param nestedInfo The name of the attribute containing the sub entry
* information
*/
private void appendSubEntry(String topLevelInfo, String nestedInfo)
{
appendSubEntry(nestedAttributes.get(topLevelInfo).getBuilder(), nestedInfo);
}
/**
* /** Writes one line of information to a file (but not the ME File)
*
* @param info The information about what to write.
* @throws WrappedException if the file could not be written
*/
private void writeToFile(GeneralSaveInfo info) throws WrappedException
{
FileWrapper fw = allFiles.get(info.getReadableFilename());
if (fw != null)
{
BufferedWriter bout = fw.getBufferedWriter();
try
{
writeNormalRow(bout, info.getValues(), true);
bout.newLine();
}
catch (IOException e)
{
throw new WrappedException(e, Text.E_WRITE_TO_TEMP_FILES);
}
}
}
/**
* Writes one line of information to the me file
*
* @param info The information about what to write.
* @throws WrappedException if the file could not be written
*/
private void writeToMeFile(MeSaveInfo info) throws WrappedException
{
FileWrapper fw = allFiles.get(FileWrapper.createReadableFileName(Text.ME_FILE_DESC));
if (fw != null)
{
BufferedWriter bout = fw.getBufferedWriter();
try
{
bout.write(String.format(Text.F_ME_SAVE, info.getSaveKey(), info.getSaveValue()));
bout.newLine();
}
catch (IOException e)
{
throw new WrappedException(e, Text.E_WRITE_TO_TEMP_FILES);
}
}
}
/**
* Initializes the HashMap containing the nested attributes info, as well as the
* completeRow and sets the subElementname to {@code null}
*/
private void resetSubAttributes()
{
nestedAttributes = new HashMap<>();
nestedAttributes.put(Text.TAG_NAME_META_DATA_ENTRY, new AttributeDataInfoTuple(Utils.META_DATA_ATTR));
nestedAttributes.put(Text.TAG_NAME_WORKOUT_EVENT, new AttributeDataInfoTuple(Utils.WORKOUT_EVENT_ATTR));
nestedAttributes.put(Text.TAG_NAME_WORKOUT_ROUTE, new AttributeDataInfoTuple(Utils.WORKOUT_ROUTE_ATTR));
nestedAttributes.put(Text.TAG_NAME_HR_LIST, new AttributeDataInfoTuple(Utils.HR_LIST_ATTR));
nestedAttributes.put(Text.TAG_NAME_IB_PER_MINUTES, new AttributeDataInfoTuple(Utils.IB_PER_MINUTES_ATTR));
completeRow = new StringBuilder();
subElementName = null;
}
/**
* Gets the values for the given attributes and returns them in an array where
* the values are saved at the index corresponding to their keys.
*
* @param attributeName Keys / names of the attributes.
* @param values The object containing the values
* @return The described array
*/
private String[] getValuesToAttributes(String[] attributeName, Attributes values)
{
String[] res = new String[attributeName.length];
for (int i = 0; i < res.length; i++)
{
String v = values.getValue(attributeName[i]);
if(attributeName[i].equals("device")&&v!=null)
{
//Entry left out due to privacy
String device="<HKDevice:";
String actualDeviceSubstring=v.substring(v.indexOf(device),v.indexOf(">")+1);
v=v.replace(actualDeviceSubstring, "device_anonymized");
}
if (v == null)
{
res[i] = "";
}
else if (v.contains(",") || v.contains(";"))
{
res[i] = "\"" + v + "\"";
}
else
{
res[i] = v;
}
}
return res;
}
/**
* Writes a non nested row into a file
*
* @param bout The writer of the file
* @param values the values in correct order needed to fit with the header row
* which should be written
* @param top shows whether this is a "nested row" / nested attribute entry
* @throws IOException if there was an error writing
*/
private void writeNormalRow(Writer bout, String[] values, boolean top) throws IOException
{
for (int i = 0; i < values.length; i++) // for every attribute value
{
bout.write(values[i]);
if (i < values.length - 1)
{
if (top)
{
bout.write(";");
}
else
{
bout.write(",");
}
}
}
}
/**
* Returns all files written in temp directory with the readable name as key and
* the {@link File} as value. It also closes the writers and thus can only be
* used after the end of writing.
*
* @return The described value.
*/
public HashMap<String, File> getAllFiles()
{
HashMap<String, File> res = new HashMap<>();
for (String key : allFiles.keySet())
{
FileWrapper fw = allFiles.get(key);
try
{
fw.getBufferedWriter().close();
}
catch (IOException e)
{
// Stream couldn't be closed. Bad luck.
}
res.put(key, fw.getFile());
}
return res;
}
/**
* Returns all accumulated error messages.
*
* @return The described value or {@code null} if there is no error message.
*/
public String getErrorMessages()
{
if (errorMessages.length()==0)
{
return null;
}
return errorMessages.toString();
}
}