/**
* @author Dirk Bergstrom
*
* Keyring for webOS - Easy password management on your phone.
* Copyright (C) 2009-2010, Dirk Bergstrom, keyring@otisbean.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.otisbean.keyring.converters;
import java.io.BufferedReader;
import java.io.FileReader;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import com.Ostermiller.util.CSVParser;
import com.Ostermiller.util.ExcelCSVParser;
import com.Ostermiller.util.LabeledCSVParser;
import com.otisbean.keyring.Item;
import com.otisbean.keyring.Ring;
/**
* Export CSV format as Keyring for webOS.
*
* @author Matt Williams, Dirk Bergstrom
*/
public class CSVConverter extends Converter {
public CSVConverter() {
needsInputFilePassword = false;
}
public LabeledCSVParser determineFileFormat(String csvFile)
throws Exception {
FileReader fr = new FileReader(csvFile);
BufferedReader br = new BufferedReader(fr);
// Look thru the file and see if it's Excel format or plain CSV
boolean isExcelFormat = false;
String thisLine = br.readLine();
while (thisLine != null) {
// Excel uses doubled quotes to escape a quote character
if (thisLine.indexOf("\"\"") >= 0) {
isExcelFormat = true;
break;
}
thisLine = br.readLine();
}
br.close();
fr.close();
// Now reopen the file and return the parser.
LabeledCSVParser lcsvp = null;
fr = new FileReader(csvFile);
br = new BufferedReader(fr);
if (isExcelFormat) {
lcsvp = new LabeledCSVParser(new ExcelCSVParser(br));
} else {
lcsvp = new LabeledCSVParser(new CSVParser(br));
}
return lcsvp;
}
@Override
public Ring convert(String csvFile, String unused, String outPassword)
throws Exception {
LabeledCSVParser lcsvp = determineFileFormat(csvFile);
String[] labels = lcsvp.getLabels();
String[][] entries = lcsvp.getAllValues();
Ring ring = new Ring(outPassword);
// Determine column indexes
int titleidx = -1;
int categoryidx = -1;
int accountidx = -1;
int passwordidx = -1;
int notesidx = -1;
int urlidx = -1;
int createdidx = -1;
int changedidx = -1;
int viewedidx = -1;
String[] entry = labels; // entries[0];
for (int ii = 0; ii < entry.length; ii++) {
if (entry[ii].equalsIgnoreCase("name") || entry[ii].equalsIgnoreCase("title"))
titleidx = ii;
if (entry[ii].equalsIgnoreCase("category"))
categoryidx = ii;
if (entry[ii].equalsIgnoreCase("account") || entry[ii].equalsIgnoreCase("username"))
accountidx = ii;
if (entry[ii].equalsIgnoreCase("password"))
passwordidx = ii;
if (entry[ii].toLowerCase().startsWith("note"))
notesidx = ii;
if (entry[ii].equalsIgnoreCase("url"))
urlidx = ii;
if (entry[ii].equalsIgnoreCase("created"))
createdidx = ii;
if (entry[ii].equalsIgnoreCase("viewed"))
viewedidx = ii;
if (entry[ii].equalsIgnoreCase("changed"))
changedidx = ii;
}
if (titleidx == -1) {
throw new Exception("Input file format is invalid. Allowed column " +
"headers are: name or title, category, account or username, password, " +
"url, note, viewed, changed, created. Any other labels will be ignored. The title " +
"field is mandatory, others are optional.");
}
long now = System.currentTimeMillis();
int exported = 0;
for (int ii = 0; ii < entries.length; ii++) {
entry = entries[ii];
try {
String title = entry[titleidx];
if (ring.getItem(title) != null) {
error("Duplicate entry, skipping.", title, ii);
} else {
String category = categoryidx == -1 || categoryidx >= entry.length ? "Unfiled"
: entry[categoryidx];
String account = accountidx == -1 || accountidx >= entry.length ? ""
: entry[accountidx];
String password = passwordidx == -1 || passwordidx >= entry.length ? ""
: entry[passwordidx];
String notes = notesidx == -1 || notesidx >= entry.length ? ""
: entry[notesidx];
String url = urlidx == -1 || urlidx >= entry.length ? ""
: entry[urlidx];
/* Dates default to time of import if they're not provided. */
long changed = changedidx == -1 || changedidx >= entry.length ? now
: parseDate(entry[changedidx].trim(), title, ii);
long viewed = viewedidx == -1 || viewedidx >= entry.length ? now
: parseDate(entry[viewedidx].trim(), title, ii);
long created = createdidx == -1 || createdidx >= entry.length ? now
: parseDate(entry[createdidx].trim(), title, ii);
ring.addItem(new Item(ring, account, password, url, notes,
title, category, created, viewed, changed));
exported++;
}
}
catch(ArrayIndexOutOfBoundsException e) {
error("Wrong number of columns.", "UNKNOWN", ii);
}
}
return ring;
}
private final String FULL_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
/**
* Attempt to parse a string into an epoch time, accepts either ISO dates
* or raw epoch times.
*
* Makes an effort, but it's not very robust.
*
* @return Epoch time, or System.currentTimeMillis() if the input
* can't be parsed.
*/
private long parseDate(String dateVal, String title, int index) {
if (dateVal.contains("-")) {
// ISO date?
String format;
if (dateVal.length() > FULL_DATE_FORMAT.length() ) {
// Has time zone
format = FULL_DATE_FORMAT + " Z";
} else {
// Use as much of the format as we have date to match
format = FULL_DATE_FORMAT.substring(0, dateVal.length());
}
try {
return new SimpleDateFormat(format).parse(dateVal).getTime();
} catch (ParseException e) {
// Fall through to return below.
}
} else {
try {
return Long.parseLong(dateVal);
}
catch(NumberFormatException nfe) {
// Fall through to return below.
}
}
error("Unparseable date '" + dateVal + "'", title, index);
return System.currentTimeMillis();
}
private void error(String msg, String title, int index) {
System.err.println("WARNING: Entry #" + (index + 1) + " (\"" + title + "\") is invalid:" +
msg);
}
}