public void toCSV( String format, String locale, Character separator, Iterator<? extends MailItem> contacts, StringBuilder sb) throws ParseException { if (knownFormats == null) { return; } CsvFormat fmt = getFormat(format, locale); if (separator != null) { fieldSeparator = separator; } else { String delimKey = fmt.key(); Character formatDefaultDelim = delimiterInfo.get(delimKey); if (formatDefaultDelim != null) { LOG.debug("toCSV choosing %c from <delimiter> matching %s", formatDefaultDelim, delimKey); fieldSeparator = formatDefaultDelim; } } LOG.debug( "toCSV Requested=[format=\"%s\" locale=\"%s\" delim=\"%c\"] Actual=[%s delim=\"%c\"]", format, locale, separator, fmt, fieldSeparator); if (fmt == null) { return; } if (fmt.allFields()) { ArrayList<Map<String, String>> allContacts = new ArrayList<Map<String, String>>(); HashSet<String> fields = new HashSet<String>(); while (contacts.hasNext()) { Object obj = contacts.next(); if (obj instanceof Contact) { Contact c = (Contact) obj; allContacts.add(c.getFields()); fields.addAll(c.getFields().keySet()); } } ArrayList<String> allFields = new ArrayList<String>(); allFields.addAll(fields); Collections.sort(allFields); addFieldDef(allFields, sb); for (Map<String, String> contactMap : allContacts) { toCSVContact(allFields, contactMap, sb); } return; } if (!fmt.hasNoHeaders()) { addFieldDef(fmt, sb); } while (contacts.hasNext()) { Object c = contacts.next(); if (c instanceof Contact) { toCSVContact(fmt, (Contact) c, sb); } } }
/** * @param csv is the list of fields in a record from a CSV file * @param formats is the list of CsvFormats to be considered applicable * @return a map from field to value * @throws ParseException */ private Map<String, String> toContact(List<String> csv, CsvFormat format) throws ParseException { ContactMap contactMap = new ContactMap(); // NOTE: If there isn't a mapping for a field name defined in "format" // NOTE: a user defined attribute with that field name will be used. if (csv == null) { return contactMap.getContacts(); } if (format.allFields()) { int end = csv.size(); end = (end > fieldNames.size()) ? fieldNames.size() : end; for (int i = 0; i < end; i++) { contactMap.put(fieldNames.get(i), csv.get(i)); } } else if (format.hasNoHeaders()) { int end = csv.size(); end = (end > format.columns.size()) ? format.columns.size() : end; for (int i = 0; i < end; i++) { contactMap.put(format.columns.get(i).field, csv.get(i)); } } else { /* Many CSV formats are output in a specific order and sometimes * contain duplicate field names with mappings to different * Zimbra contact fields. */ Map<CsvColumn, Map<String, String>> pendMV = new HashMap<CsvColumn, Map<String, String>>(); List<CsvColumn> unseenColumns = new ArrayList<CsvColumn>(); unseenColumns.addAll(format.columns); for (int ndx = 0; ndx < fieldNames.size(); ndx++) { String csvFieldName = fieldNames.get(ndx); String fieldValue = (ndx >= csv.size()) ? null : csv.get(ndx); CsvColumn matchingCol = null; String matchingFieldLc = null; for (CsvColumn unseenC : unseenColumns) { matchingFieldLc = unseenC.matchingLcCsvFieldName(csvFieldName); if (matchingFieldLc == null) { continue; } if (unseenC.colType == ColType.MULTIVALUE) { Map<String, String> currMV = pendMV.get(matchingCol); if ((currMV != null) && currMV.get(matchingFieldLc) != null) { // already have field with this name that matches this column continue; } } matchingCol = unseenC; break; } if (matchingCol == null) { // unknown field - setup for adding as a user defined attribute LOG.debug( "Adding CSV contact attribute [%s=%s] - assuming is user defined.", csvFieldName, fieldValue); contactMap.put(csvFieldName, fieldValue); continue; } switch (matchingCol.colType) { case NAME: addNameField(fieldValue, matchingCol.field, contactMap); unseenColumns.remove(matchingCol); break; case DATE: addDateField(fieldValue, matchingCol.field, format.key(), contactMap); unseenColumns.remove(matchingCol); break; case TAG: contactMap.put(TAG, fieldValue); break; case MULTIVALUE: for (String cname : matchingCol.names) { if (cname.toLowerCase().equals(matchingFieldLc)) { Map<String, String> currMV = pendMV.get(matchingCol); if (currMV == null) { currMV = new HashMap<String, String>(); pendMV.put(matchingCol, currMV); } currMV.put(matchingFieldLc, fieldValue); if (currMV.size() >= matchingCol.names.size()) { addMultiValueField(matchingCol, currMV, contactMap); pendMV.remove(currMV); unseenColumns.remove(matchingCol); } } } break; default: contactMap.put(matchingCol.field, fieldValue); unseenColumns.remove(matchingCol); } } // Process multi-value fields where only some constituent fields were present for (Map.Entry<CsvColumn, Map<String, String>> entry : pendMV.entrySet()) { addMultiValueField(entry.getKey(), entry.getValue(), contactMap); } } Map<String, String> contact = contactMap.getContacts(); // Bug 50069 - Lines with single blank in them got imported as a blank contact // Initial fix idea was for parseField to return the trimmed version of the string // However, this from rfc4180 - Common Format and MIME Type for Comma-Separated // Values (CSV) Files : // "Spaces are considered part of a field and should not be ignored." // suggests that might be an invalid thing to do, so now just reject the contact // if the whole line would collapse to an empty string with trim. if (contact.size() == 1) { boolean onlyBlank = true; for (String val : contact.values()) { if (!val.trim().equals("")) { onlyBlank = false; break; } } if (onlyBlank) contact = new HashMap<String, String>(); } return contact; }