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