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 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 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="")+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 getAllFiles() { HashMap 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(); } }