application.StubCommand.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of specmatic-executable Show documentation
Show all versions of specmatic-executable Show documentation
Command-line standalone executable jar for Specmatic
package application
import `in`.specmatic.core.APPLICATION_NAME_LOWER_CASE
import `in`.specmatic.core.CONTRACT_EXTENSIONS
import `in`.specmatic.core.Configuration
import `in`.specmatic.core.Configuration.Companion.DEFAULT_HTTP_STUB_HOST
import `in`.specmatic.core.Configuration.Companion.DEFAULT_HTTP_STUB_PORT
import `in`.specmatic.core.WorkingDirectory
import `in`.specmatic.core.log.*
import `in`.specmatic.core.utilities.ContractPathData
import `in`.specmatic.core.utilities.exitWithMessage
import `in`.specmatic.stub.ContractStub
import `in`.specmatic.stub.HttpClientFactory
import org.springframework.beans.factory.annotation.Autowired
import picocli.CommandLine.*
import java.io.File
import java.util.concurrent.Callable
@Command(name = "stub",
mixinStandardHelpOptions = true,
description = ["Start a stub server with contract"])
class StubCommand : Callable {
var httpStub: ContractStub? = null
@Autowired
private var httpStubEngine: HTTPStubEngine = HTTPStubEngine()
@Autowired
private var stubLoaderEngine: StubLoaderEngine = StubLoaderEngine()
@Autowired
private var specmaticConfig: SpecmaticConfig = SpecmaticConfig()
@Parameters(arity = "0..*", description = ["Contract file paths"])
var contractPaths: List = mutableListOf()
@Option(names = ["--data"], description = ["Directory in which contract data may be found"], required = false)
var dataDirs: List = mutableListOf()
@Option(names = ["--host"], description = ["Host for the http stub"], defaultValue = DEFAULT_HTTP_STUB_HOST)
lateinit var host: String
@Option(names = ["--port"], description = ["Port for the http stub"], defaultValue = DEFAULT_HTTP_STUB_PORT)
var port: Int = 0
@Option(names = ["--strict"], description = ["Start HTTP stub in strict mode"], required = false)
var strictMode: Boolean = false
@Option(names = ["--passThroughTargetBase"], description = ["All requests that did not match a url in any contract will be forwarded to this service"])
var passThroughTargetBase: String = ""
@Option(names = ["--httpsKeyStore"], description = ["Run the proxy on https using a key in this store"])
var keyStoreFile = ""
@Option(names = ["--httpsKeyStoreDir"], description = ["Run the proxy on https, create a store named $APPLICATION_NAME_LOWER_CASE.jks in this directory"])
var keyStoreDir = ""
@Option(names = ["--httpsKeyStorePassword"], description = ["Run the proxy on https, password for pre-existing key store"])
var keyStorePassword = "forgotten"
@Option(names = ["--httpsKeyAlias"], description = ["Run the proxy on https using a key by this name"])
var keyStoreAlias = "${APPLICATION_NAME_LOWER_CASE}proxy"
@Option(names = ["--httpsPassword"], description = ["Key password if any"])
var keyPassword = "forgotten"
@Option(names = ["--debug"], description = ["Debug logs"])
var verbose = false
@Option(names = ["--config"], description = ["Configuration file name ($APPLICATION_NAME_LOWER_CASE.json by default)"])
var configFileName: String? = null
@Option(names = ["--textLog"], description = ["Directory in which to write a text log"])
var textLog: String? = null
@Option(names = ["--jsonLog"], description = ["Directory in which to write a JSON log"])
var jsonLog: String? = null
@Option(names = ["--jsonConsoleLog"], description = ["Console log should be in JSON format"])
var jsonConsoleLog: Boolean = false
@Option(names = ["--noConsoleLog"], description = ["Don't log to console"])
var noConsoleLog: Boolean = false
@Option(names = ["--logPrefix"], description = ["Prefix of log file"])
var logPrefix: String = "specmatic"
@Autowired
val watchMaker = WatchMaker()
@Autowired
val fileOperations = FileOperations()
@Autowired
val httpClientFactory = HttpClientFactory()
private var contractSources:List = emptyList()
var specmaticConfigPath: String? = null
override fun call() {
val logPrinters = configureLogPrinters()
logger = if(verbose)
Verbose(CompositePrinter(logPrinters))
else
NonVerbose(CompositePrinter(logPrinters))
configFileName?.let {
Configuration.globalConfigFileName = it
}
try {
contractSources = when (contractPaths.isEmpty()) {
true -> {
logger.debug("No contractPaths specified. Using configuration file named $configFileName")
specmaticConfigPath = File(Configuration.globalConfigFileName).canonicalPath
specmaticConfig.contractStubPathData()
}
else -> contractPaths.map {
ContractPathData("", it)
}
}
contractPaths = contractSources.map { it.path }
validateContractFileExtensions(contractPaths, fileOperations)
startServer()
if(httpStub != null) {
addShutdownHook()
val watcher = watchMaker.make(contractPaths.plus(dataDirs))
watcher.watchForChanges {
restartServer()
}
}
} catch (e: Throwable) {
consoleLog(e)
}
}
private fun configureLogPrinters(): List {
val consoleLogPrinter = configureConsoleLogPrinter()
val textLogPrinter = configureTextLogPrinter()
val jsonLogPrinter = configureJSONLogPrinter()
return consoleLogPrinter.plus(textLogPrinter).plus(jsonLogPrinter)
}
private fun configureConsoleLogPrinter(): List {
if (noConsoleLog)
return emptyList()
if (jsonConsoleLog)
return listOf(JSONConsoleLogPrinter)
return listOf(ConsolePrinter)
}
private fun configureJSONLogPrinter(): List = jsonLog?.let {
listOf(JSONFilePrinter(LogDirectory(it, logPrefix, "json", "log")))
} ?: emptyList()
private fun configureTextLogPrinter(): List = textLog?.let {
listOf(TextFilePrinter(LogDirectory(it, logPrefix, "", "log")))
} ?: emptyList()
private fun startServer() {
val workingDirectory = WorkingDirectory()
val stubData = stubLoaderEngine.loadStubs(contractSources, dataDirs)
val certInfo = CertInfo(keyStoreFile, keyStoreDir, keyStorePassword, keyStoreAlias, keyPassword)
port = when (isDefaultPort(port)) {
true -> if (portIsInUse(host, port)) findRandomFreePort() else port
false -> port
}
httpStub = httpStubEngine.runHTTPStub(stubData, host, port, certInfo, strictMode, passThroughTargetBase, specmaticConfigPath, httpClientFactory, workingDirectory)
LogTail.storeSnapshot()
}
private fun isDefaultPort(port:Int): Boolean {
return DEFAULT_HTTP_STUB_PORT == port.toString()
}
private fun restartServer() {
consoleLog(StringLog("Stopping servers..."))
try {
stopServer()
consoleLog(StringLog("Stopped."))
} catch (e: Throwable) {
consoleLog(e,"Error stopping server")
}
try { startServer() } catch (e: Throwable) {
consoleLog(e, "Error starting server")
}
}
private fun stopServer() {
httpStub?.close()
httpStub = null
}
private fun addShutdownHook() {
Runtime.getRuntime().addShutdownHook(object : Thread() {
override fun run() {
try {
consoleLog(StringLog("Shutting down stub servers"))
httpStub?.close()
} catch (e: InterruptedException) {
currentThread().interrupt()
}
}
})
}
}
internal fun validateContractFileExtensions(contractPaths: List, fileOperations: FileOperations) {
contractPaths.filter { fileOperations.isFile(it) && fileOperations.extensionIsNot(it, CONTRACT_EXTENSIONS) }.let {
if (it.isNotEmpty()) {
val files = it.joinToString("\n")
exitWithMessage("The following files do not end with $CONTRACT_EXTENSIONS and cannot be used:\n$files")
}
}
}