Mercurial > cgi-bin > hgweb.cgi > PassMan
annotate src/main/kotlin/name/blackcap/passman/ImportSubcommand.kt @ 13:302d224bbd57
Improve help messages and csv error reportage.
author | David Barts <n5jrn@me.com> |
---|---|
date | Tue, 24 Jan 2023 20:13:13 -0800 |
parents | a38a2a1036c3 |
children | 4dae7a15ee48 |
rev | line source |
---|---|
12 | 1 package name.blackcap.passman |
2 | |
3 import com.opencsv.CSVParserBuilder | |
4 import com.opencsv.CSVReaderBuilder | |
13
302d224bbd57
Improve help messages and csv error reportage.
David Barts <n5jrn@me.com>
parents:
12
diff
changeset
|
5 import com.opencsv.exceptions.CsvException |
12 | 6 import org.apache.commons.cli.* |
7 import java.io.FileReader | |
8 import java.io.IOException | |
9 import java.text.SimpleDateFormat | |
10 import java.util.* | |
11 import kotlin.system.exitProcess | |
12 | |
13 class ImportSubcommand(): Subcommand() { | |
14 private companion object { | |
15 val CSV_DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").apply { | |
16 timeZone = TimeZone.getTimeZone("UTC") | |
17 isLenient = false | |
18 } | |
19 const val ESCAPE = "escape" | |
20 const val FORCE = "force" | |
21 const val HELP = "help" | |
22 const val IGNORE = "ignore" | |
23 const val QUOTE = "quote" | |
24 const val SEPARATOR = "separator" | |
25 const val SKIP = "skip" | |
26 const val NFIELDS = 7 | |
27 | |
28 } | |
29 private lateinit var commandLine: CommandLine | |
30 private lateinit var db: Database | |
31 | |
32 /* default option values */ | |
33 private var escape = '\\' | |
34 private var quote = '"' | |
35 private var separator = ',' | |
36 | |
13
302d224bbd57
Improve help messages and csv error reportage.
David Barts <n5jrn@me.com>
parents:
12
diff
changeset
|
37 private var line = 0 |
302d224bbd57
Improve help messages and csv error reportage.
David Barts <n5jrn@me.com>
parents:
12
diff
changeset
|
38 |
12 | 39 override fun run(args: Array<String>) { |
40 parseArguments(args) | |
41 db = Database.open() | |
42 try { | |
43 doImport() | |
44 } catch (e: IOException) { | |
45 die(e.message ?: "I/O error") | |
13
302d224bbd57
Improve help messages and csv error reportage.
David Barts <n5jrn@me.com>
parents:
12
diff
changeset
|
46 } catch (e: CsvException) { |
302d224bbd57
Improve help messages and csv error reportage.
David Barts <n5jrn@me.com>
parents:
12
diff
changeset
|
47 val message = e.message ?: "CSV error" |
302d224bbd57
Improve help messages and csv error reportage.
David Barts <n5jrn@me.com>
parents:
12
diff
changeset
|
48 die("line $line, $message") |
12 | 49 } |
50 } | |
51 | |
52 private fun parseArguments(args: Array<String>) { | |
53 val options = Options().apply { | |
54 addOption("e", ImportSubcommand.ESCAPE, true, "CSV escape character (default $escape).") | |
55 addOption("f", ImportSubcommand.FORCE, false, "Do not ask before overwriting.") | |
56 addOption("h", ImportSubcommand.HELP, false, "Print this help message.") | |
57 addOption("i", ImportSubcommand.IGNORE, false, "Ignore white space before quoted strings.") | |
58 addOption("q", ImportSubcommand.QUOTE, true, "CSV string-quoting character (default $quote).") | |
59 addOption("s", ImportSubcommand.SEPARATOR, true, "CSV separator character (default $separator).") | |
60 addOption("k", ImportSubcommand.SKIP, false, "Skip first line of input.") | |
61 } | |
62 try { | |
63 commandLine = DefaultParser().parse(options, args) | |
64 } catch (e: ParseException) { | |
65 die(e.message ?: "syntax error", 2) | |
66 } | |
67 if (commandLine.hasOption(ImportSubcommand.HELP)) { | |
68 HelpFormatter().printHelp("$SHORTNAME merge [options] csv_file", options) | |
69 exitProcess(0) | |
70 } | |
71 if (commandLine.args.isEmpty()) { | |
72 die("expecting other CSV file name", 2) | |
73 } | |
74 if (commandLine.args.size > 1) { | |
75 die("unexpected trailing arguments", 2) | |
76 } | |
77 escape = getOptionChar(ImportSubcommand.ESCAPE, escape) | |
78 quote = getOptionChar(ImportSubcommand.QUOTE, quote) | |
79 separator = getOptionChar(ImportSubcommand.SEPARATOR, separator) | |
80 } | |
81 | |
82 private fun getOptionChar(optionName: String, defaultValue: Char): Char { | |
83 val optionValue = commandLine.getOptionValue(optionName) ?: return defaultValue | |
84 val ret = optionValue.firstOrNull() | |
85 if (ret == null) { | |
86 die("--$optionName value must not be empty") | |
87 } | |
88 return ret!! | |
89 } | |
90 | |
91 private fun doImport() { | |
92 val csvParser = CSVParserBuilder() | |
93 .withEscapeChar(escape) | |
94 .withQuoteChar(quote) | |
95 .withSeparator(separator) | |
96 .withIgnoreLeadingWhiteSpace(commandLine.hasOption(ImportSubcommand.IGNORE)) | |
97 .build() | |
98 | |
99 val csvReader = CSVReaderBuilder(FileReader(commandLine.args[0])) | |
100 .withCSVParser(csvParser) | |
101 .build() | |
102 | |
103 csvReader.use { | |
104 if (commandLine.hasOption(ImportSubcommand.SKIP)) { | |
13
302d224bbd57
Improve help messages and csv error reportage.
David Barts <n5jrn@me.com>
parents:
12
diff
changeset
|
105 line++ |
12 | 106 it.skip(1) |
107 } | |
13
302d224bbd57
Improve help messages and csv error reportage.
David Barts <n5jrn@me.com>
parents:
12
diff
changeset
|
108 |
12 | 109 it.iterator().forEach { fields -> |
13
302d224bbd57
Improve help messages and csv error reportage.
David Barts <n5jrn@me.com>
parents:
12
diff
changeset
|
110 line++ |
12 | 111 val importedEntry = fromCsv(fields) |
112 val thisEntry = Entry.fromDatabase(db, importedEntry.name) | |
113 try { | |
114 if (okToChange(thisEntry, importedEntry)) { | |
115 if (thisEntry == null) { | |
116 importedEntry.insert(db) | |
117 } else { | |
118 importedEntry.update(db) | |
119 } | |
120 } | |
121 } finally { | |
122 thisEntry?.password?.clear() | |
123 } | |
124 } | |
125 } | |
126 } | |
127 | |
128 private fun fromCsv(fields: Array<String>): Entry { | |
129 if (fields.size != NFIELDS) { | |
13
302d224bbd57
Improve help messages and csv error reportage.
David Barts <n5jrn@me.com>
parents:
12
diff
changeset
|
130 die("line $line, expected $NFIELDS fields but got ${fields.size}") |
12 | 131 } |
132 return Entry( | |
133 name = fields[0], | |
134 username = fields[1], | |
135 password = fields[2].toCharArray(), | |
136 notes = if (saysNull(fields[3])) null else fields[3], | |
137 created = parseCsvTime(fields[4]) ?: Date(), | |
138 modified = parseCsvTime(fields[5]), | |
139 accessed = parseCsvTime(fields[6]) | |
140 ) | |
141 } | |
142 | |
143 private fun parseCsvTime(unparsed: String): Date? { | |
144 if (saysNull(unparsed)) { | |
145 return null | |
146 } | |
147 try { | |
148 return CSV_DATE_FORMAT.parse(unparsed) | |
149 } catch (e: ParseException) { | |
150 die("${see(unparsed)} - invalid date/time string") | |
151 throw e /* kotlin is too stupid to realize this never happens */ | |
152 } | |
153 } | |
154 | |
155 private fun okToChange(thisEntry: Entry?, otherEntry: Entry): Boolean = | |
156 thisEntry == null || commandLine.hasOption(FORCE) || askUserIfOkToOverwrite(thisEntry, otherEntry) | |
157 | |
158 private fun saysNull(string: String) = string.lowercase() == "null" | |
159 | |
13
302d224bbd57
Improve help messages and csv error reportage.
David Barts <n5jrn@me.com>
parents:
12
diff
changeset
|
160 } |