Mercurial > cgi-bin > hgweb.cgi > JpegWasher
changeset 5:dc1f4359659d
Got it compiling.
author | David Barts <n5jrn@me.com> |
---|---|
date | Thu, 09 Apr 2020 18:20:34 -0700 |
parents | ba5dc14652da |
children | aafc9c127c7b |
files | build.xml src/name/blackcap/exifwasher/Files.kt src/name/blackcap/exifwasher/Main.kt src/name/blackcap/exifwasher/MainFrame.kt src/name/blackcap/exifwasher/Menus.kt src/name/blackcap/exifwasher/Misc.kt src/name/blackcap/exifwasher/SettingsDialog.kt src/name/blackcap/exifwasher/ShowDialog.kt src/name/blackcap/exifwasher/WashDialog.kt src/name/blackcap/exifwasher/Whitelist.kt |
diffstat | 10 files changed, 144 insertions(+), 122 deletions(-) [+] |
line wrap: on
line diff
--- a/build.xml Wed Apr 08 21:31:30 2020 -0700 +++ b/build.xml Thu Apr 09 18:20:34 2020 -0700 @@ -87,7 +87,9 @@ <target name="compile" depends="classpath" description="Compile Java sources to ${work.home}"> <kotlinc src="${src.home}" output="${work.jar}" - classpathref="compile.classpath"/> + classpathref="compile.classpath"> + <compilerarg line="-jvm-target 1.8"/> + </kotlinc> </target> <!-- make .jar file -->
--- a/src/name/blackcap/exifwasher/Files.kt Wed Apr 08 21:31:30 2020 -0700 +++ b/src/name/blackcap/exifwasher/Files.kt Thu Apr 09 18:20:34 2020 -0700 @@ -76,9 +76,9 @@ } } -System.setProperty("java.util.logging.SimpleFormatter.format", - "%1$tFT%1$tT%1$tz %2$s%n%4$s: %5$s%6$s%n") val LOGGER = run { + System.setProperty("java.util.logging.SimpleFormatter.format", + "%1\$tFT%1\$tT%1\$tz %2\$s%n%4\$s: %5\$s%6\$s%n") LF_DIR.makeIfNeeded() Logger.getLogger(LONGNAME).apply { addHandler(FileHandler(LOG_FILE.toString()).apply {
--- a/src/name/blackcap/exifwasher/Main.kt Wed Apr 08 21:31:30 2020 -0700 +++ b/src/name/blackcap/exifwasher/Main.kt Thu Apr 09 18:20:34 2020 -0700 @@ -4,6 +4,8 @@ package name.blackcap.exifwasher import javax.swing.UIManager +import java.util.logging.Level +import java.util.logging.Logger object Application { /* name we call ourselves */
--- a/src/name/blackcap/exifwasher/MainFrame.kt Wed Apr 08 21:31:30 2020 -0700 +++ b/src/name/blackcap/exifwasher/MainFrame.kt Thu Apr 09 18:20:34 2020 -0700 @@ -7,12 +7,13 @@ import java.awt.datatransfer.DataFlavor import java.awt.datatransfer.Transferable import java.awt.datatransfer.UnsupportedFlavorException +import java.awt.event.WindowEvent +import java.awt.event.WindowListener import java.io.File import java.io.IOException -import javax.swing.JFrame -import javax.swing.TransferHandler - -class MainFrame: JFrame { +import java.util.logging.Level +import java.util.logging.Logger +import javax.swing.* /* the main frame itself */ class MainFrame : JFrame(Application.MYNAME) { @@ -38,7 +39,7 @@ } /* acts on dragged files */ - private class MyTransferHandler : TransferHandler { + private class MyTransferHandler : TransferHandler() { override fun canImport(support: TransferHandler.TransferSupport): Boolean { return support.isDataFlavorSupported(DataFlavor.javaFileListFlavor) } @@ -48,7 +49,7 @@ return false } val files = try { - support.transferable.getTransferData(DataFlavor.javaFileListFlavor) as java.util.List<File> + support.transferable.getTransferData(DataFlavor.javaFileListFlavor) as List<File> } catch (e: UnsupportedFlavorException) { return false } catch (e: IOException) {
--- a/src/name/blackcap/exifwasher/Menus.kt Wed Apr 08 21:31:30 2020 -0700 +++ b/src/name/blackcap/exifwasher/Menus.kt Thu Apr 09 18:20:34 2020 -0700 @@ -68,7 +68,7 @@ * Show an About dialog. */ fun showAboutDialog() { - JOptionPane.showMessageDialog(frame.v, + JOptionPane.showMessageDialog(Application.mainFrame, "ExifWasher—Privacy for your photos.\n" + "© MMXX, David W. Barts", "About ExifWasher",
--- a/src/name/blackcap/exifwasher/Misc.kt Wed Apr 08 21:31:30 2020 -0700 +++ b/src/name/blackcap/exifwasher/Misc.kt Thu Apr 09 18:20:34 2020 -0700 @@ -9,7 +9,7 @@ import javax.swing.* import kotlin.annotation.* import kotlin.properties.ReadWriteProperty -import kotlin.reflect.KProperty +import kotlin.reflect.* /** * Delegate that makes a var that can only be set once. This is commonly @@ -17,22 +17,22 @@ * outer levels but initialized in the Swing event dispatch thread. */ class SetOnce<T: Any>: ReadWriteProperty<Any?,T> { - private var value: T? = null + private var setOnceValue: T? = null override operator fun getValue(thisRef: Any?, property: KProperty<*>): T { - if (value == null) { + if (setOnceValue == null) { throw RuntimeException("${property.name} has not been initialized") } else { - return value!! + return setOnceValue!! } } @Synchronized - override operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: T): Unit { - if (value != null) { + override operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T): Unit { + if (setOnceValue != null) { throw RuntimeException("${property.name} has already been initialized") } - value = newValue + setOnceValue = value } } @@ -82,14 +82,14 @@ * Change to the standard wait cursor. */ fun Component.useWaitCursor() { - this.cursor = Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)) + this.cursor = Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR) } /** * Return back to the normal cursor(). */ fun Component.useNormalCursor() { - this.cursor = Cursor.defaultCursor + this.cursor = Cursor.getDefaultCursor() } /** @@ -105,10 +105,10 @@ * @param T Type returned by inBackground (Java doInBackground) task. */ class SwingWorkerBuilder<T>: SwingWorker<T,Unit>() { - private var inBackgroundLambda: (SwingWorkerBuilder.() -> T)? = null - private var whenDoneLambda: (SwingWorkerBuilder.() -> Unit)? = null + private var inBackgroundLambda: (SwingWorkerBuilder<T>.() -> T)? = null + private var whenDoneLambda: (SwingWorkerBuilder<T>.() -> Unit)? = null - private fun setOnce<U>(prop: KMutableProperty<(SwingWorkerBuilder.() -> U)?>, value: SwingWorkerBuilder.() -> U) { + private fun <U> setOnce(prop: KMutableProperty0<(SwingWorkerBuilder<T>.() -> U)?>, value: SwingWorkerBuilder<T>.() -> U) { if (prop.get() != null) { throw SwingWorkerException(prop.name.removeSuffix("Lambda") + " already defined!") } @@ -118,38 +118,40 @@ /** * Define the inBackground task. */ - fun inBackground(lambda: SwingWorkerBuilder.() -> T): Unit { + fun inBackground(lambda: SwingWorkerBuilder<T>.() -> T): Unit { setOnce<T>(::inBackgroundLambda, lambda) } /** * Define the whenDone task. */ - fun whenDone(lambda: SwingWorkerBuilder.() -> Unit): Unit { + fun whenDone(lambda: SwingWorkerBuilder<T>.() -> Unit): Unit { setOnce<Unit>(::whenDoneLambda, lambda) } + /** + * Validates we've been properly initialized. + */ + fun validate(): Unit { + if (inBackgroundLambda == null) { + throw SwingWorkerException("inBackground not defined!") + } + } + /* standard overrides for SwingWorker follow */ - override fun doInBackground(): T = inBackgroundLambda?.invoke(this) - - override fun done(): Unit = whenDoneLambda?.invoke(this) + override fun doInBackground(): T = inBackgroundLambda!!.invoke(this) - override fun execute(): Unit { - if (inBackgroundLambda == null) { - throw SwingWorkerException("inBackground not defined!") - } else { - super.execute() - } - } + override fun done(): Unit = whenDoneLambda?.invoke(this) ?: Unit } /** * Provides for an outer swingWorker block to contain the DSL. */ -fun swingWorker<T>(initializer: SwingWorkerBuilder.() -> Unit): Unit { +fun <T> swingWorker(initializer: SwingWorkerBuilder<T>.() -> Unit): Unit { SwingWorkerBuilder<T>().run { initializer() + validate() execute() } }
--- a/src/name/blackcap/exifwasher/SettingsDialog.kt Wed Apr 08 21:31:30 2020 -0700 +++ b/src/name/blackcap/exifwasher/SettingsDialog.kt Thu Apr 09 18:20:34 2020 -0700 @@ -6,71 +6,79 @@ import java.awt.Toolkit import java.awt.event.ActionEvent import java.awt.event.ActionListener +import java.io.BufferedWriter import java.io.File +import java.io.FileOutputStream import java.io.IOException +import java.io.OutputStreamWriter +import java.util.Properties import java.util.logging.Level import java.util.logging.Logger import javax.swing.* import javax.swing.event.ListDataEvent import javax.swing.event.ListDataListener +import javax.swing.event.ListSelectionListener import kotlin.text.toBoolean import name.blackcap.exifwasher.exiv2.* +/* work around name shadowing */ +private val _PROPS = PROPERTIES + class SettingsDialog : JDialog(Application.mainFrame) { - private val BW = 9 - private val BW2 = BW * 2 + protected val BW = 9 + protected val BW2 = BW * 2 /* where to send output, if not outputToInputDir */ - private var _outputTo = PROPERTIES.getProperty("outputTo") - private var _dOutputTo = DPROPERTIES.getProperty("outputTo") + protected var _outputTo = _PROPS.getProperty("outputTo") + protected var _dOutputTo = DPROPERTIES.getProperty("outputTo") val outputTo: String get() = _outputTo /* make output where input was found */ - private var _outputToInputDir = (PROPERTIES.getProperty("outputToInputDir") ?: "false").toBoolean() - private var _dOutputToInputDir = (DPROPERTIES.getProperty("outputToInputDir") ?: "false").toBoolean() + protected var _outputToInputDir = _PROPS.getProperty("outputToInputDir", "false").toBoolean() + protected var _dOutputToInputDir = DPROPERTIES.getProperty("outputToInputDir", "false").toBoolean() val outputToInputDir: Boolean get() = _outputToInputDir /* the whitelist of allowed Exif tags */ - private var _whitelist = Whitelist.parse(PROPERTIES.getProperty("whitelist") :? "") + protected var _whitelist = Whitelist.parse(_PROPS.getProperty("whitelist", "")) val whitelist: Whitelist get() = _whitelist /* the default whitelist, for factory resets */ - private val _oWhitelist = _whitelist.clone() - private val _dWhitelist = Whitelist.parse(DPROPERTIES.getProperty("whitelist") :? "") + protected val _oWhitelist = _whitelist.clone() + protected val _dWhitelist = Whitelist.parse(DPROPERTIES.getProperty("whitelist", "")) /* radio buttons to choose output directory policy */ - private val outputToButton = JRadioButton("Output to:", !outputToInputDir).apply { + protected val outputToButton = JRadioButton("Output to:", !outputToInputDir).apply { addActionListener(ActionListener { setOutputOpts(isSelected()) }) } - private val outputToInputButton = JRadioButton( + protected val outputToInputButton = JRadioButton( "Output to directory containing input file.", outputToInputDir).apply { addActionListener(ActionListener { setOutputOpts(!isSelected()) }) } - private val buttonGroup = ButtonGroup().apply { + protected val buttonGroup = ButtonGroup().apply { add(outputToButton) add(outputToInputButton) } - private fun setOutputOpts(toSpecific: Boolean) { + protected fun setOutputOpts(toSpecific: Boolean) { _outputToInputDir = !toSpecific changeOutputTo.setEnabled(toSpecific) } /* displays the OutputTo directory */ - private val outputToText = JTextField(outputTo, 50).apply { + protected val outputToText = JTextField(outputTo, 50).apply { setEditable(false) } /* pops up to change the above directory */ - private val outputToChooser = JFileChooser(outputToText.text).apply { - fileSelectionMode = DIRECTORIES_ONLY + protected val outputToChooser = JFileChooser(outputToText.text).apply { + fileSelectionMode = JFileChooser.DIRECTORIES_ONLY } /* requests the OutputTo directory be changed */ - private val changeOutputTo = JButton("Change").also { + protected val changeOutputTo = JButton("Change").also { it.addActionListener(ActionListener { val status = outputToChooser.showOpenDialog(this) if (status == JFileChooser.APPROVE_OPTION) { @@ -82,33 +90,33 @@ } /* bottom buttons to restore defaults, cancel, save */ - private val restoreButton = JButton("Restore All Defaults").also { + protected val restoreButton = JButton("Restore All Defaults").apply { addActionListener(ActionListener { restore(_dOutputToInputDir, _dOutputTo, _dWhitelist) - } + }) } - private val cancelButton = JButton("Cancel").also { + protected val cancelButton = JButton("Cancel").apply { addActionListener(ActionListener { setVisible(false) restore(outputToInputDir, outputTo, whitelist) }) } - private val saveButton = JButton("Save").also { + protected val saveButton = JButton("Save").apply { addActionListener(ActionListener { setVisible(false) writeProperties() }) } - private fun writeProperties() { - PROPERTIES.run { + protected fun writeProperties() { + _PROPS.run { setProperty("outputTo", outputTo) setProperty("outputToInputDir", outputToInputDir.toString()) setProperty("whitelist", whitelist.toString()) } try { BufferedWriter(OutputStreamWriter(FileOutputStream(PROP_FILE), CHARSET)).use { - PROPERTIES.store(it, null) + _PROPS.store(it, null) } } catch (e: IOException) { LOGGER.log(Level.SEVERE, "unable to write settings", e) @@ -119,7 +127,7 @@ } } - private fun restore(outputToInput: Boolean, output: String, wl: Whitelist) { + protected fun restore(outputToInput: Boolean, output: String, wl: Whitelist) { outputToButton.setSelected(!outputToInput) changeOutputTo.setEnabled(!outputToInput) outputToText.text = output @@ -128,7 +136,7 @@ } /* so we can present a list of strings that is always sorted */ - private class WhiteListModel(basedOn: Collection<String>): ListModel<String> { + protected class WhiteListModel(basedOn: Collection<String>): ListModel<String> { private val storage = ArrayList<String>(basedOn).apply { sort() } private val listeners = mutableListOf<ListDataListener>() @@ -140,50 +148,46 @@ index = -(index + 1) } storage.add(index, newItem) - val event = ListDataEvent(this, ListDataEvent.INTERVAL_ADDED, index, index) - listeners.forEach { it.intervalAdded(event) } + notifyAll(ListDataEvent.INTERVAL_ADDED, index, index) } fun removeAt(index: Int): Unit { - if (storage.removeAt(index)) { - val event = ListDataEvent(this, ListDataEvent.INTERVAL_REMOVED, index, index) - listeners.forEach { it.intervalRemoved(event) } - } + storage.removeAt(index) + notifyAll(ListDataEvent.INTERVAL_REMOVED, index, index) } fun remove(oldItem: String): Unit { - val index = basedOn.binarySearch(oldItem) + val index: Int = storage.binarySearch(oldItem) if (index < 0) { return } - var start = index - while (start > 0 && storage[start] == oldItem) { - start -= 1 - } - var end = index - var max = storage.size - 1 - while (end < max && storage[end] == oldItem) { - end += 1 - } - storage.removeRange(start, end+1) - val event = ListDataEvent(this, ListDataEvent.INTERVAL_REMOVED, start, end) - listeners.forEach { it.intervalRemoved(event) } + storage.removeAt(index) + notifyAll(ListDataEvent.INTERVAL_REMOVED, index, index) } fun reset(basedOn: Collection<String>): Unit { - val removeEvent = ListDataEvent(this, ListDataEvent.INTERVAL_REMOVED, 0, storage.size) + val oldSize = storage.size storage.clear() + notifyAll(ListDataEvent.INTERVAL_REMOVED, 0, oldSize) storage.addAll(basedOn) storage.sort() - val addEvent = ListDataEvent(this, ListDataEvent.INTERVAL_ADDED, 0, storage.size) - listeners.forEach { - it.contentsChanged(removeEvent) - it.contentsChanged(addEvent) + notifyAll(ListDataEvent.INTERVAL_ADDED, 0, storage.size) + } + + /* misc. */ + + fun toList(): List<String> = storage + + private fun notifyAll(eType: Int, index0: Int, index1: Int): Unit { + val event = ListDataEvent(this, eType, index0, index1) + when (eType) { + ListDataEvent.CONTENTS_CHANGED -> listeners.forEach { it.contentsChanged(event) } + ListDataEvent.INTERVAL_ADDED -> listeners.forEach { it.intervalAdded(event) } + ListDataEvent.INTERVAL_REMOVED -> listeners.forEach { it.intervalRemoved(event) } + else -> throw RuntimeException("unexpected event type!") } } - fun toList(): List<String> = storage - /* so we are a proper ListModel */ override fun addListDataListener(l: ListDataListener): Unit { @@ -199,30 +203,32 @@ override fun getSize(): Int = storage.size } - private class WLAddDialog(parent: SettingsDialog): JDialog(parent) { - JTextField text = JTextField(40).apply { + protected class WLAddDialog(parent: SettingsDialog): JDialog(parent) { + private val BW = parent.BW + private val BW2 = parent.BW2 + + private val toAdd = JTextField(40).apply { alignmentX = CENTER_ALIGNMENT border = BorderFactory.createEmptyBorder(BW, BW, BW, BW) } - JButton cancelButton = JButton("Cancel").apply { + private val cancelButton = JButton("Cancel").apply { addActionListener(ActionListener { - text.text = "" + toAdd.text = "" setVisible(false) }) } - JButton addButton = JButton("Add").apply { + private val addButton = JButton("Add").apply { addActionListener(ActionListener { - val newItem = text.text?.trim() + val newItem = toAdd.text?.trim() if (newItem.isNullOrEmpty()) { Toolkit.getDefaultToolkit().beep() } else { - wlSelectorModel.add(newItem) + parent.wlSelectorModel.add(newItem) } - text.text = "" + toAdd.text = "" setVisible(false) - } }) } @@ -230,7 +236,7 @@ title = "Add Item to Whitelist" contentPane.apply { layout = BoxLayout(this, BoxLayout.Y_AXIS) - add(text) + add(toAdd) add(Box(BoxLayout.X_AXIS).apply { alignmentX = CENTER_ALIGNMENT border = BorderFactory.createEmptyBorder(BW, BW, BW, BW) @@ -247,33 +253,38 @@ } /* the JList that holds our whitelist */ - private val wlSelectorModel = WhiteListModel(whitelist.toList()) - private val wlSelector = JList().apply { + protected val wlSelectorModel = WhiteListModel(whitelist.toList()) + protected val wlSelector: JList<String> = JList<String>().apply { visibleRowCount = -1 model = wlSelectorModel clearSelection() addListSelectionListener(ListSelectionListener { wlDeleteButton.setEnabled(!isSelectionEmpty()) - } + }) } /* buttons for managing the whitelist */ - private val wlAddButton = JButton("Add").apply { + protected val wlAddButton = JButton("Add").apply { addActionListener(ActionListener { wlAddDialog.setVisible(true) }) } - private val wlDeleteButton = JButton("Delete").apply { + protected val wlDeleteButton = JButton("Delete").apply { addActionListener(ActionListener { wlSelector.selectedIndices.forEach { wlSelectorModel.removeAt(it) } + setEnabled(false) }) setEnabled(false) } /* the dialog that the Add button pops up */ - private val wlAddDialog = WLAddDialog(this) + protected val wlAddDialog = WLAddDialog(this) init { + val home = System.getProperty("user.dir") if (_outputTo.isNullOrEmpty()) { - _outputTo = System.getProperty("user.dir") + _outputTo = home + } + if (_dOutputTo.isNullOrEmpty()) { + _dOutputTo = home } title = "Settings" contentPane.apply {
--- a/src/name/blackcap/exifwasher/ShowDialog.kt Wed Apr 08 21:31:30 2020 -0700 +++ b/src/name/blackcap/exifwasher/ShowDialog.kt Thu Apr 09 18:20:34 2020 -0700 @@ -4,6 +4,7 @@ */ package name.blackcap.exifwasher +import java.awt.Dimension import java.awt.event.ActionEvent import java.awt.event.ActionListener import java.io.File @@ -46,8 +47,8 @@ swingWorker<Array<Array<String>>?> { inBackground { try { - val image = Image(image.absolutePath) - val meta = image.meta + val openedImage = Image(image.absolutePath) + val meta = openedImage.metadata val keys = meta.keys keys.sort() Array<Array<String>>(keys.size) { @@ -79,9 +80,9 @@ } } - private class MyTableModel : DefaultTableModel { + private class MyTableModel(tData: Array<Array<String>>, cNames: Array<String>) : DefaultTableModel(tData, cNames) { override fun isCellEditable(row: Int, col: Int) = false - override fun getColumnClass(col: Int) = java.lang.String::class.java + override fun getColumnClass(col: Int) = String::class.java } init {
--- a/src/name/blackcap/exifwasher/WashDialog.kt Wed Apr 08 21:31:30 2020 -0700 +++ b/src/name/blackcap/exifwasher/WashDialog.kt Thu Apr 09 18:20:34 2020 -0700 @@ -3,6 +3,7 @@ */ package name.blackcap.exifwasher +import java.awt.Dimension import java.awt.event.ActionEvent import java.awt.event.ActionListener import java.io.File @@ -57,7 +58,7 @@ /* initiates the washing of the Exif data */ fun wash(dirty: File) { - title = "Washing: ${image.name}" + title = "Washing: ${dirty.name}" selectAll.setSelected(false) washing = dirty useWaitCursor() @@ -65,13 +66,15 @@ inBackground { try { val image = Image(dirty.canonicalPath) - val meta = image.meta + val meta = image.metadata val keys = meta.keys keys.sort() - Array<Array<String>>(keys.size) { + Array<Array<Any>>(keys.size) { val key = keys[it] val value = meta[key] - arrayOf(!settingsDialog.whitelist.contains(key), key, value.type, value.value) + arrayOf( + !Application.settingsDialog.whitelist.contains(key), + key, value.type, value.value) } } catch (e: Exiv2Exception) { LOGGER.log(Level.SEVERE, "unable to read metadata", e) @@ -97,12 +100,12 @@ } } - private class MyTableModel : DefaultTableModel { + private class MyTableModel(tData: Array<Array<Any>>, cNames: Array<String>) : DefaultTableModel(tData, cNames) { override fun isCellEditable(row: Int, col: Int) = col == 0 override fun getColumnClass(col: Int) = if (col == 0) { - Boolean + Boolean::class.java } else { - String + String::class.java } } @@ -110,7 +113,7 @@ myTable.model.run { for (i in 0 .. rowCount - 1) { val key = getValueAt(i, 1) as String - setValueAt(!settingsDialog.whitelist.contains(key), i, 0) + setValueAt(!Application.settingsDialog.whitelist.contains(key), i, 0) } } myTable.validate() @@ -120,10 +123,10 @@ setVisible(false) /* get path to the directory we create */ - val outDir = if (settingsDialog.outputToInputDir) { + val outDir = if (Application.settingsDialog.outputToInputDir) { washing.canonicalFile.parent } else { - settingsDialog.outputTo + Application.settingsDialog.outputTo } /* get new file name */ @@ -141,9 +144,9 @@ } } val image = Image(newFile.canonicalPath) - val meta = image.meta + val meta = image.metadata meta.keys.forEach { - if (!settingsDialog.whitelist.contains(it)) { + if (!Application.settingsDialog.whitelist.contains(it)) { meta.erase(it) } }
--- a/src/name/blackcap/exifwasher/Whitelist.kt Wed Apr 08 21:31:30 2020 -0700 +++ b/src/name/blackcap/exifwasher/Whitelist.kt Thu Apr 09 18:20:34 2020 -0700 @@ -33,14 +33,14 @@ override fun toString(): String = toList().joinToString(",") - override public fun clone() = this().also { new -> + override public fun clone() = Whitelist().also { new -> entire.forEach { new.addEntire(it) } prefixes.forEach { new.addPrefix(it) } } companion object { private val SPLITTER = Pattern.compile(",\\s*") - fun parse(raw: String) = this().also { + fun parse(raw: String) = Whitelist().also { for (s in raw.split(SPLITTER)) { it.add(s) }