ImportCommandHandler.kt
package de.pflugradts.passbird.application.commandhandling.handler
import de.pflugradts.passbird.application.UserInterfaceAdapterPort
import de.pflugradts.passbird.application.commandhandling.CommandExecutionTracker
import de.pflugradts.passbird.application.commandhandling.command.ImportCommand
import de.pflugradts.passbird.application.configuration.ReadableConfiguration
import de.pflugradts.passbird.application.exchange.ImportExportService
import de.pflugradts.passbird.application.exchange.ImportNestPreview
import de.pflugradts.passbird.application.exchange.ShellMap
import de.pflugradts.passbird.domain.model.shell.Shell
import de.pflugradts.passbird.domain.model.shell.Shell.Companion.shellOf
import de.pflugradts.passbird.domain.model.slot.Slot
import de.pflugradts.passbird.domain.model.slot.Slot.Companion.slotAt
import de.pflugradts.passbird.domain.model.slot.Slot.DEFAULT
import de.pflugradts.passbird.domain.model.transfer.Output.Companion.outputOf
import de.pflugradts.passbird.domain.model.transfer.OutputFormatting.HIGHLIGHT
import de.pflugradts.passbird.domain.model.transfer.OutputFormatting.OPERATION_ABORTED
import de.pflugradts.passbird.domain.service.nest.NestService
import de.pflugradts.passbird.domain.service.password.PasswordService
class ImportCommandHandler constructor(
private val configuration: ReadableConfiguration,
private val importExportService: ImportExportService,
private val nestService: NestService,
private val passwordService: PasswordService,
private val userInterfaceAdapterPort: UserInterfaceAdapterPort,
private val commandExecutionTracker: CommandExecutionTracker,
) : TypedCommandHandler<ImportCommand>(ImportCommand::class.java) {
override fun handleCommand(command: ImportCommand) {
when (if (command.selective) selectiveCommandConfirmed() else commandConfirmed()) {
ImportCommandConfirmation.CONFIRMED -> if (command.selective) {
val selectedNest = selectedNest ?: return
importExportService.importEggs(selectedNest.slot, selectedNest.targetSlot)
} else {
importExportService.importEggs()
}
ImportCommandConfirmation.ABORTED -> {
commandExecutionTracker.markAborted()
userInterfaceAdapterPort.send(outputOf(shellOf("Operation aborted."), OPERATION_ABORTED))
}
ImportCommandConfirmation.FAILED -> commandExecutionTracker.markFailure()
}
userInterfaceAdapterPort.sendLineBreak()
}
private var selectedNest: SelectedNest? = null
private fun commandConfirmed(): ImportCommandConfirmation {
selectedNest = null
if (configuration.application.password.promptOnRemoval) {
val importedEggIds = importExportService.peekImportEggIdShells()
if (importedEggIds.failure) {
return ImportCommandConfirmation.FAILED
}
return importedEggIds.getOrNull()!!.useScrambled {
val overlaps = it
.map { (nestSlot, eggIdShell) -> eggIdShell.map { Triple(nestSlot, it, passwordService.eggExists(it, nestSlot)) } }
.flatten()
.filter { it.third }
.map { Pair(it.first, it.second) }
if (overlaps.isNotEmpty()) {
confirmImport(overlaps)
} else {
ImportCommandConfirmation.CONFIRMED
}
}
}
return ImportCommandConfirmation.CONFIRMED
}
private fun selectiveCommandConfirmed(): ImportCommandConfirmation {
selectedNest = null
val importedNests = importExportService.peekImportNests()
if (importedNests.failure) {
return ImportCommandConfirmation.FAILED
}
val previews = importedNests.getOrNull()!!.takeIf(List<ImportNestPreview>::isNotEmpty)
?: return ImportCommandConfirmation.ABORTED
return previews.useScrambled {
userInterfaceAdapterPort.send(outputOf(shellOf("\nAvailable Nests in import file:\n"), HIGHLIGHT))
userInterfaceAdapterPort.send(outputOf(shellOf(it.joinToString("\n") { "\t${it.slot.index()}: ${it.nestId.asString()}" })))
val sourceSlot = receiveSourceSlot(it) ?: return@useScrambled ImportCommandConfirmation.ABORTED
val targetSlot = receiveTargetSlot() ?: return@useScrambled ImportCommandConfirmation.ABORTED
val preview = it.first { it.slot == sourceSlot }
if (targetSlot == DEFAULT && sourceSlot != DEFAULT) {
return@useScrambled ImportCommandConfirmation.ABORTED
}
val targetNest = nestService.atNestSlot(targetSlot)
if (targetNest.isPresent && targetNest.get().viewNestId() != preview.nestId) {
return@useScrambled ImportCommandConfirmation.ABORTED
}
val overlaps = preview.eggIds
.filter { eggId -> passwordService.eggExists(eggId, targetSlot) }
.map { eggId -> Pair(targetSlot, eggId) }
if (overlaps.isNotEmpty()) {
val confirmation = confirmImport(overlaps)
if (confirmation != ImportCommandConfirmation.CONFIRMED) {
return@useScrambled confirmation
}
}
selectedNest = SelectedNest(sourceSlot, targetSlot)
ImportCommandConfirmation.CONFIRMED
}
}
private fun receiveSourceSlot(previews: List<ImportNestPreview>) = receiveNestSlot(
prompt = "\nSpecify a Nest Slot 0-9 to import or anything else to abort: ",
availableSlots = previews.map { it.slot }.toSet(),
)
private fun receiveTargetSlot() = receiveNestSlot(
prompt = "Specify a target Nest Slot 0-9 or anything else to abort: ",
availableSlots = Slot.entries.toSet(),
)
private fun receiveNestSlot(prompt: String, availableSlots: Set<Slot>) = userInterfaceAdapterPort.receive(
outputOf(shellOf(prompt)),
).shell.asString()
.takeIf { input -> input.length == 1 && input[0].isDigit() }
?.let(::slotAt)
?.takeIf(availableSlots::contains)
private fun confirmImport(overlaps: List<Pair<Slot, Shell>>) = if (
userInterfaceAdapterPort.receiveConfirmation(
outputOf(
shellOf(
"By importing this file ${overlaps.size} existing Passwords " +
"will be irrevocably overwritten.\n" +
"The following Eggs will be affected: " +
"${overlaps.joinToString { "${it.second.asString()} (${it.first})" }}\n" +
"Input 'c' to confirm or anything else to abort.\nYour input: ",
),
),
)
) {
ImportCommandConfirmation.CONFIRMED
} else {
ImportCommandConfirmation.ABORTED
}
}
private enum class ImportCommandConfirmation { CONFIRMED, ABORTED, FAILED }
private data class SelectedNest(val slot: Slot, val targetSlot: Slot)
private inline fun <T> ShellMap.useScrambled(block: (ShellMap) -> T): T = try {
block(this)
} finally {
values.flatten().scrambleShells()
}
private inline fun <T> List<ImportNestPreview>.useScrambled(block: (List<ImportNestPreview>) -> T): T = try {
block(this)
} finally {
forEach {
it.nestId.scramble()
it.eggIds.scrambleShells()
}
}
private fun Iterable<Shell>.scrambleShells() = forEach(Shell::scramble)