Refactor + // img download
This commit is contained in:
parent
b1e2efa75f
commit
d3bcff470c
16
pom.xml
16
pom.xml
@ -9,7 +9,7 @@
|
|||||||
<maven.compiler.target>${java.version}</maven.compiler.target>
|
<maven.compiler.target>${java.version}</maven.compiler.target>
|
||||||
<maven.compiler.source>${java.version}</maven.compiler.source>
|
<maven.compiler.source>${java.version}</maven.compiler.source>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<main.class>be.simplenotes.c2c/C2cKt</main.class>
|
<main.class>be.simplenotes.c2c/Camp2CampKt</main.class>
|
||||||
<openhtml.version>1.0.4</openhtml.version>
|
<openhtml.version>1.0.4</openhtml.version>
|
||||||
</properties>
|
</properties>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
@ -204,20 +204,6 @@
|
|||||||
<mainClass>${main.class}</mainClass>
|
<mainClass>${main.class}</mainClass>
|
||||||
</transformer>
|
</transformer>
|
||||||
</transformers>
|
</transformers>
|
||||||
<filters>
|
|
||||||
<filter>
|
|
||||||
<artifact>com.github.ben-manes.caffeine:caffeine</artifact>
|
|
||||||
<includes>
|
|
||||||
<include>**</include>
|
|
||||||
</includes>
|
|
||||||
</filter>
|
|
||||||
<filter>
|
|
||||||
<artifact>org.jetbrains.kotlin:kotlin-reflect</artifact>
|
|
||||||
<includes>
|
|
||||||
<include>**</include>
|
|
||||||
</includes>
|
|
||||||
</filter>
|
|
||||||
</filters>
|
|
||||||
</configuration>
|
</configuration>
|
||||||
</execution>
|
</execution>
|
||||||
</executions>
|
</executions>
|
||||||
|
|||||||
@ -1,102 +0,0 @@
|
|||||||
package be.simplenotes.c2c
|
|
||||||
|
|
||||||
import be.simplenotes.c2c.api.Api
|
|
||||||
import be.simplenotes.c2c.api.ApiUrls
|
|
||||||
import be.simplenotes.c2c.pdf.PdfCreator
|
|
||||||
import be.simplenotes.c2c.pdf.StreamFactory
|
|
||||||
import be.simplenotes.c2c.routes.IndexRoute
|
|
||||||
import be.simplenotes.c2c.routes.PdfRoute
|
|
||||||
import be.simplenotes.c2c.routes.RouteRoute
|
|
||||||
import be.simplenotes.c2c.routes.SearchRoute
|
|
||||||
import com.mitchellbosecke.pebble.PebbleEngine
|
|
||||||
import com.mitchellbosecke.pebble.loader.ClasspathLoader
|
|
||||||
import org.apache.hc.client5.http.impl.DefaultRedirectStrategy
|
|
||||||
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder
|
|
||||||
import org.http4k.client.ApacheClient
|
|
||||||
import org.http4k.contract.ContractRoute
|
|
||||||
import org.http4k.contract.contract
|
|
||||||
import org.http4k.contract.openapi.ApiInfo
|
|
||||||
import org.http4k.contract.openapi.v3.OpenApi3
|
|
||||||
import org.http4k.core.Filter
|
|
||||||
import org.http4k.core.Response
|
|
||||||
import org.http4k.core.Status
|
|
||||||
import org.http4k.core.then
|
|
||||||
import org.http4k.filter.RequestFilters
|
|
||||||
import org.http4k.filter.ServerFilters
|
|
||||||
import org.http4k.format.Jackson
|
|
||||||
import org.http4k.server.asServer
|
|
||||||
import org.koin.core.context.startKoin
|
|
||||||
import org.koin.core.qualifier.named
|
|
||||||
import org.koin.dsl.bind
|
|
||||||
import org.koin.dsl.module
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import java.io.PrintWriter
|
|
||||||
import java.io.StringWriter
|
|
||||||
|
|
||||||
val module = module {
|
|
||||||
single {
|
|
||||||
ApacheClient(
|
|
||||||
HttpClientBuilder
|
|
||||||
.create()
|
|
||||||
.setRedirectStrategy(DefaultRedirectStrategy())
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
single { ApiUrls(get()) }
|
|
||||||
single { Jackson.mapper }
|
|
||||||
single { Api(get(), get()) }
|
|
||||||
single { PdfCreator(get()) }
|
|
||||||
single { StreamFactory(get()) }
|
|
||||||
single {
|
|
||||||
PebbleEngine
|
|
||||||
.Builder()
|
|
||||||
.loader(ClasspathLoader().apply {
|
|
||||||
prefix = "views/"
|
|
||||||
suffix = ".twig"
|
|
||||||
})
|
|
||||||
.cacheActive(false)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val routes = module {
|
|
||||||
single(named<SearchRoute>()) { SearchRoute(get()).searchRoute() } bind ContractRoute::class
|
|
||||||
single(named<RouteRoute>()) { RouteRoute(get()).routeRoute() } bind ContractRoute::class
|
|
||||||
single(named<PdfRoute>()) { PdfRoute(get(), get()).route() } bind ContractRoute::class
|
|
||||||
single(named<IndexRoute>()) { IndexRoute(get()).route() } bind ContractRoute::class
|
|
||||||
}
|
|
||||||
|
|
||||||
fun main() {
|
|
||||||
val koin = startKoin {
|
|
||||||
modules(module, routes)
|
|
||||||
}.koin
|
|
||||||
|
|
||||||
val appRoutes = koin.getAll<ContractRoute>()
|
|
||||||
val app = contract {
|
|
||||||
renderer = OpenApi3(ApiInfo("Camp2Camp", "1.0-SNAPSHOT"), Jackson)
|
|
||||||
descriptionPath = "/api/swagger.json"
|
|
||||||
routes.all.addAll(appRoutes)
|
|
||||||
}
|
|
||||||
|
|
||||||
val logger = LoggerFactory.getLogger("Camp2Camp")
|
|
||||||
|
|
||||||
val loggingFilter = RequestFilters.Tap {
|
|
||||||
logger.info("${it.method} ${it.uri}")
|
|
||||||
}
|
|
||||||
|
|
||||||
val catchAll = Filter { next ->
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
next(it)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
val sw = StringWriter()
|
|
||||||
e.printStackTrace(PrintWriter(sw))
|
|
||||||
logger.error(sw.toString())
|
|
||||||
Response(Status.INTERNAL_SERVER_ERROR)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
catchAll.then(ServerFilters.GZip()).then(loggingFilter).then(app).asServer(CustomApacheServer(4000)).start()
|
|
||||||
logger.info("Listening on http://localhost:4000")
|
|
||||||
}
|
|
||||||
53
src/main/kotlin/Camp2Camp.kt
Normal file
53
src/main/kotlin/Camp2Camp.kt
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package be.simplenotes.c2c
|
||||||
|
|
||||||
|
import be.simplenotes.c2c.filters.catchAllFilter
|
||||||
|
import be.simplenotes.c2c.filters.loggingFilter
|
||||||
|
import be.simplenotes.c2c.routes.RouteProvider
|
||||||
|
import org.http4k.contract.contract
|
||||||
|
import org.http4k.contract.openapi.ApiInfo
|
||||||
|
import org.http4k.contract.openapi.v3.OpenApi3
|
||||||
|
import org.http4k.core.then
|
||||||
|
import org.http4k.filter.ServerFilters
|
||||||
|
import org.http4k.format.Jackson
|
||||||
|
import org.http4k.server.ServerConfig
|
||||||
|
import org.http4k.server.asServer
|
||||||
|
import org.koin.core.context.startKoin
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
|
||||||
|
fun main() {
|
||||||
|
val koin = startKoin {
|
||||||
|
modules(
|
||||||
|
serverModule,
|
||||||
|
routes,
|
||||||
|
clientModule,
|
||||||
|
apiModule,
|
||||||
|
utilsModule,
|
||||||
|
pdfModule,
|
||||||
|
)
|
||||||
|
properties(mapOf(
|
||||||
|
"port" to System.getenv().getOrDefault("PORT", "4000"),
|
||||||
|
"host" to System.getenv().getOrDefault("HOST", "localhost"),
|
||||||
|
))
|
||||||
|
}.koin
|
||||||
|
|
||||||
|
val appRoutes = koin.getAll<RouteProvider>().map(RouteProvider::route)
|
||||||
|
val app = contract {
|
||||||
|
renderer = OpenApi3(ApiInfo("Camp2Camp", "1.0-SNAPSHOT"), Jackson)
|
||||||
|
descriptionPath = "/api/swagger.json"
|
||||||
|
routes.all.addAll(appRoutes)
|
||||||
|
}
|
||||||
|
|
||||||
|
val logger = LoggerFactory.getLogger("Camp2Camp")
|
||||||
|
val server = koin.get<ServerConfig>()
|
||||||
|
val address = koin.get<InetSocketAddress>()
|
||||||
|
|
||||||
|
catchAllFilter(logger)
|
||||||
|
.then(ServerFilters.GZip())
|
||||||
|
.then(loggingFilter(logger))
|
||||||
|
.then(app)
|
||||||
|
.asServer(server)
|
||||||
|
.start()
|
||||||
|
|
||||||
|
logger.info("Listening on http://${address.hostName}:${address.port}")
|
||||||
|
}
|
||||||
53
src/main/kotlin/Modules.kt
Normal file
53
src/main/kotlin/Modules.kt
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package be.simplenotes.c2c
|
||||||
|
|
||||||
|
import be.simplenotes.c2c.api.Api
|
||||||
|
import be.simplenotes.c2c.api.ApiUrls
|
||||||
|
import be.simplenotes.c2c.api.ImageDownloader
|
||||||
|
import be.simplenotes.c2c.pdf.PdfCreator
|
||||||
|
import be.simplenotes.c2c.routes.*
|
||||||
|
import be.simplenotes.c2c.server.CustomApacheServer
|
||||||
|
import be.simplenotes.c2c.templates.PebbleEngine
|
||||||
|
import com.mitchellbosecke.pebble.PebbleEngine
|
||||||
|
import org.apache.hc.client5.http.impl.DefaultRedirectStrategy
|
||||||
|
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder
|
||||||
|
import org.http4k.client.ApacheClient
|
||||||
|
import org.http4k.format.Jackson
|
||||||
|
import org.http4k.server.ServerConfig
|
||||||
|
import org.koin.core.qualifier.named
|
||||||
|
import org.koin.dsl.bind
|
||||||
|
import org.koin.dsl.module
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
|
||||||
|
val routes = module {
|
||||||
|
single { SearchRouteProvider(get()) } bind RouteProvider::class
|
||||||
|
single { RouteRouteProvider(get()) } bind RouteProvider::class
|
||||||
|
single { PdfRouteProvider(get(), get(), get()) } bind RouteProvider::class
|
||||||
|
single { IndexRouteProvider(get()) } bind RouteProvider::class
|
||||||
|
}
|
||||||
|
|
||||||
|
val serverModule = module {
|
||||||
|
single { InetSocketAddress(getProperty("host"), getProperty("port").toInt()) }
|
||||||
|
single<ServerConfig> { CustomApacheServer(get()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
val clientModule = module {
|
||||||
|
single { ApacheClient(get()) }
|
||||||
|
single { HttpClientBuilder.create().setRedirectStrategy(DefaultRedirectStrategy()).build() }
|
||||||
|
}
|
||||||
|
|
||||||
|
val apiModule = module {
|
||||||
|
single { ApiUrls(get()) }
|
||||||
|
single { Api(get(), get()) }
|
||||||
|
single { ImageDownloader(get()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
val utilsModule = module {
|
||||||
|
single { Jackson.mapper }
|
||||||
|
single { PebbleEngine(cache = false, prefix = "templates/") }
|
||||||
|
}
|
||||||
|
|
||||||
|
val pdfModule = module {
|
||||||
|
val pdf = named("pdf")
|
||||||
|
single { PdfCreator(get(pdf)) }
|
||||||
|
single(pdf) { get<PebbleEngine>().getTemplate("pdf") }
|
||||||
|
}
|
||||||
24
src/main/kotlin/api/ImageDownloader.kt
Normal file
24
src/main/kotlin/api/ImageDownloader.kt
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package be.simplenotes.c2c.api
|
||||||
|
|
||||||
|
import org.http4k.core.HttpHandler
|
||||||
|
import org.http4k.core.Method
|
||||||
|
import org.http4k.core.Request
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.util.stream.Collectors
|
||||||
|
|
||||||
|
class ImageDownloader(private val client: HttpHandler) {
|
||||||
|
private val logger = LoggerFactory.getLogger(javaClass)
|
||||||
|
|
||||||
|
fun download(images: List<Image>): Map<String, ByteArray?> =
|
||||||
|
images.map(Image::medium)
|
||||||
|
.parallelStream()
|
||||||
|
.collect(Collectors.toConcurrentMap(
|
||||||
|
{ it },
|
||||||
|
{
|
||||||
|
val res = client(Request(Method.GET, it))
|
||||||
|
logger.info("GET ${res.status} $it")
|
||||||
|
if (res.status.successful) res.body.stream.readAllBytes()
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
26
src/main/kotlin/filters/Filters.kt
Normal file
26
src/main/kotlin/filters/Filters.kt
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package be.simplenotes.c2c.filters
|
||||||
|
|
||||||
|
import org.http4k.core.Filter
|
||||||
|
import org.http4k.core.Response
|
||||||
|
import org.http4k.core.Status
|
||||||
|
import org.http4k.filter.RequestFilters
|
||||||
|
import org.slf4j.Logger
|
||||||
|
import java.io.PrintWriter
|
||||||
|
import java.io.StringWriter
|
||||||
|
|
||||||
|
fun loggingFilter(logger: Logger) = RequestFilters.Tap {
|
||||||
|
logger.info("${it.method} ${it.uri}")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun catchAllFilter(logger: Logger) = Filter { next ->
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
next(it)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
val sw = StringWriter()
|
||||||
|
e.printStackTrace(PrintWriter(sw))
|
||||||
|
logger.error(sw.toString())
|
||||||
|
Response(Status.INTERNAL_SERVER_ERROR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,59 +2,19 @@ package be.simplenotes.c2c.pdf
|
|||||||
|
|
||||||
import be.simplenotes.c2c.api.Route
|
import be.simplenotes.c2c.api.Route
|
||||||
import be.simplenotes.c2c.templates.render
|
import be.simplenotes.c2c.templates.render
|
||||||
import com.github.benmanes.caffeine.cache.Caffeine
|
import com.mitchellbosecke.pebble.template.PebbleTemplate
|
||||||
import com.mitchellbosecke.pebble.PebbleEngine
|
|
||||||
import com.mitchellbosecke.pebble.loader.ClasspathLoader
|
|
||||||
import com.openhtmltopdf.extend.FSStream
|
|
||||||
import com.openhtmltopdf.extend.FSStreamFactory
|
import com.openhtmltopdf.extend.FSStreamFactory
|
||||||
import com.openhtmltopdf.pdfboxout.PdfRendererBuilder
|
import com.openhtmltopdf.pdfboxout.PdfRendererBuilder
|
||||||
import com.openhtmltopdf.util.XRLog
|
import com.openhtmltopdf.util.XRLog
|
||||||
import org.http4k.core.HttpHandler
|
|
||||||
import org.http4k.core.Method
|
|
||||||
import org.http4k.core.Request
|
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import org.jsoup.helper.W3CDom
|
import org.jsoup.helper.W3CDom
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.InputStream
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
class StreamFactory(private val client: HttpHandler) : FSStreamFactory {
|
class PdfCreator(private val template: PebbleTemplate) {
|
||||||
private val logger = LoggerFactory.getLogger(javaClass)
|
|
||||||
|
|
||||||
private val imgCache = Caffeine.newBuilder()
|
fun create(route: Route, streamFactory: FSStreamFactory): ByteArrayInputStream {
|
||||||
.maximumSize(15)
|
val html = template.render(mapOf("route" to route))
|
||||||
.expireAfterWrite(15, TimeUnit.MINUTES)
|
|
||||||
.build<String, ByteArray>()
|
|
||||||
|
|
||||||
override fun getUrl(url: String) = object : FSStream {
|
|
||||||
override fun getStream(): InputStream? {
|
|
||||||
val bytes = imgCache.get(url) {
|
|
||||||
logger.info("Downloading $url")
|
|
||||||
val res = client(Request(Method.GET, url))
|
|
||||||
if (res.status.successful) res.body.stream.readAllBytes()
|
|
||||||
else null
|
|
||||||
}
|
|
||||||
return bytes?.inputStream()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getReader() = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class PdfCreator(private val streamFactory: StreamFactory) {
|
|
||||||
private val engine = PebbleEngine
|
|
||||||
.Builder()
|
|
||||||
.loader(ClasspathLoader().apply {
|
|
||||||
prefix = "templates/"
|
|
||||||
suffix = ".twig"
|
|
||||||
})
|
|
||||||
.cacheActive(true)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
fun create(route: Route): ByteArrayInputStream {
|
|
||||||
val html = engine.render("index", mapOf("route" to route))
|
|
||||||
|
|
||||||
XRLog.setLoggingEnabled(false)
|
XRLog.setLoggingEnabled(false)
|
||||||
|
|
||||||
|
|||||||
12
src/main/kotlin/pdf/PreloadedStreamFactory.kt
Normal file
12
src/main/kotlin/pdf/PreloadedStreamFactory.kt
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package be.simplenotes.c2c.pdf
|
||||||
|
|
||||||
|
import com.openhtmltopdf.extend.FSStream
|
||||||
|
import com.openhtmltopdf.extend.FSStreamFactory
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
class PreloadedStreamFactory(private val images: Map<String, ByteArray?>) : FSStreamFactory {
|
||||||
|
override fun getUrl(url: String) = object : FSStream {
|
||||||
|
override fun getStream(): InputStream? = images[url]?.inputStream()
|
||||||
|
override fun getReader() = null
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,20 +4,18 @@ import be.simplenotes.c2c.templates.render
|
|||||||
import com.mitchellbosecke.pebble.PebbleEngine
|
import com.mitchellbosecke.pebble.PebbleEngine
|
||||||
import org.http4k.contract.ContractRoute
|
import org.http4k.contract.ContractRoute
|
||||||
import org.http4k.contract.meta
|
import org.http4k.contract.meta
|
||||||
import org.http4k.core.HttpHandler
|
import org.http4k.core.*
|
||||||
import org.http4k.core.Method
|
|
||||||
import org.http4k.core.Response
|
|
||||||
import org.http4k.core.Status
|
|
||||||
|
|
||||||
class IndexRoute(private val engine: PebbleEngine) {
|
class IndexRouteProvider(private val engine: PebbleEngine) : RouteProvider {
|
||||||
|
|
||||||
fun route(): ContractRoute {
|
override fun route(): ContractRoute {
|
||||||
val spec = "/" meta {
|
val spec = "/" meta {
|
||||||
summary = "Redoc ui"
|
summary = "Redoc ui"
|
||||||
|
produces += ContentType.TEXT_HTML
|
||||||
} bindContract Method.GET
|
} bindContract Method.GET
|
||||||
|
|
||||||
val route: HttpHandler = {
|
val route: HttpHandler = {
|
||||||
Response(Status.OK).body(engine.render("index"))
|
Response(Status.OK).body(engine.render("index")).header("Content-Type", "text/html; charset=utf-8")
|
||||||
}
|
}
|
||||||
|
|
||||||
return spec to route
|
return spec to route
|
||||||
@ -1,38 +0,0 @@
|
|||||||
package be.simplenotes.c2c.routes
|
|
||||||
|
|
||||||
import be.simplenotes.c2c.api.Api
|
|
||||||
import be.simplenotes.c2c.pdf.PdfCreator
|
|
||||||
import org.http4k.contract.ContractRoute
|
|
||||||
import org.http4k.contract.div
|
|
||||||
import org.http4k.contract.meta
|
|
||||||
import org.http4k.core.HttpHandler
|
|
||||||
import org.http4k.core.Method
|
|
||||||
import org.http4k.core.Response
|
|
||||||
import org.http4k.core.Status
|
|
||||||
import org.http4k.lens.Path
|
|
||||||
|
|
||||||
// TODO: rename..
|
|
||||||
class PdfRoute(private val api: Api, private val pdfCreator: PdfCreator) {
|
|
||||||
|
|
||||||
fun route(): ContractRoute {
|
|
||||||
val spec = "/route" / Path.of("id", "Route's ID") / "pdf" meta {
|
|
||||||
summary = "Generate pdf from routes"
|
|
||||||
} bindContract Method.GET
|
|
||||||
|
|
||||||
fun route(id: String, pdf: String): HttpHandler = {
|
|
||||||
when (val route = api.getRoute(id)) {
|
|
||||||
null -> Response(Status.NOT_FOUND)
|
|
||||||
else -> {
|
|
||||||
val bytes = pdfCreator.create(route)
|
|
||||||
Response(Status.OK)
|
|
||||||
.body(bytes)
|
|
||||||
.header("Content-Type", "application/pdf")
|
|
||||||
.header("Content-Disposition", "attachment; filename=\"out.pdf\"")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return spec to ::route
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
39
src/main/kotlin/routes/PdfRouteProvider.kt
Normal file
39
src/main/kotlin/routes/PdfRouteProvider.kt
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package be.simplenotes.c2c.routes
|
||||||
|
|
||||||
|
import be.simplenotes.c2c.api.Api
|
||||||
|
import be.simplenotes.c2c.api.ImageDownloader
|
||||||
|
import be.simplenotes.c2c.pdf.PdfCreator
|
||||||
|
import be.simplenotes.c2c.pdf.PreloadedStreamFactory
|
||||||
|
import org.http4k.contract.ContractRoute
|
||||||
|
import org.http4k.contract.div
|
||||||
|
import org.http4k.contract.meta
|
||||||
|
import org.http4k.core.*
|
||||||
|
import org.http4k.lens.Path
|
||||||
|
|
||||||
|
class PdfRouteProvider(
|
||||||
|
private val api: Api,
|
||||||
|
private val pdfCreator: PdfCreator,
|
||||||
|
private val imageDownloader: ImageDownloader,
|
||||||
|
) : RouteProvider {
|
||||||
|
|
||||||
|
override fun route(): ContractRoute {
|
||||||
|
val spec = "/route" / Path.of("id", "Route's ID") / "pdf" meta {
|
||||||
|
summary = "Generate pdf from routes"
|
||||||
|
produces += ContentType("application/pdf")
|
||||||
|
} bindContract Method.GET
|
||||||
|
|
||||||
|
fun route(id: String, pdf: String): HttpHandler = {
|
||||||
|
api.getRoute(id)?.let {
|
||||||
|
val imageMap = imageDownloader.download(it.associations.images)
|
||||||
|
|
||||||
|
Response(Status.OK)
|
||||||
|
.body(pdfCreator.create(it, PreloadedStreamFactory(imageMap)))
|
||||||
|
.header("Content-Type", "application/pdf")
|
||||||
|
.header("Content-Disposition", "attachment; filename=\"out.pdf\"")
|
||||||
|
} ?: Response(Status.NOT_FOUND)
|
||||||
|
}
|
||||||
|
|
||||||
|
return spec to ::route
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
7
src/main/kotlin/routes/RouteProvider.kt
Normal file
7
src/main/kotlin/routes/RouteProvider.kt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package be.simplenotes.c2c.routes
|
||||||
|
|
||||||
|
import org.http4k.contract.ContractRoute
|
||||||
|
|
||||||
|
interface RouteProvider {
|
||||||
|
fun route(): ContractRoute
|
||||||
|
}
|
||||||
@ -10,13 +10,14 @@ import org.http4k.format.Jackson.auto
|
|||||||
import org.http4k.lens.Path
|
import org.http4k.lens.Path
|
||||||
|
|
||||||
// TODO: rename..
|
// TODO: rename..
|
||||||
class RouteRoute(private val api: Api) {
|
class RouteRouteProvider(private val api: Api) : RouteProvider {
|
||||||
|
|
||||||
fun routeRoute(): ContractRoute {
|
override fun route(): ContractRoute {
|
||||||
val responseLens = Body.auto<Route>("Route").toLens()
|
val responseLens = Body.auto<Route>("Route").toLens()
|
||||||
|
|
||||||
val spec = "/route" / Path.of("id", "Route's ID") meta {
|
val spec = "/route" / Path.of("id", "Route's ID") meta {
|
||||||
summary = "Route details"
|
summary = "Route details"
|
||||||
|
produces += ContentType.APPLICATION_JSON
|
||||||
} bindContract Method.GET
|
} bindContract Method.GET
|
||||||
|
|
||||||
fun route(id: String): HttpHandler = {
|
fun route(id: String): HttpHandler = {
|
||||||
@ -5,21 +5,22 @@ import be.simplenotes.c2c.api.Api
|
|||||||
import be.simplenotes.c2c.api.RouteResult
|
import be.simplenotes.c2c.api.RouteResult
|
||||||
import be.simplenotes.c2c.api.SearchResults
|
import be.simplenotes.c2c.api.SearchResults
|
||||||
import org.http4k.contract.ContractRoute
|
import org.http4k.contract.ContractRoute
|
||||||
import org.http4k.contract.Tag
|
import org.http4k.contract.RequestMeta
|
||||||
import org.http4k.contract.meta
|
import org.http4k.contract.meta
|
||||||
import org.http4k.core.*
|
import org.http4k.core.*
|
||||||
import org.http4k.format.Jackson.auto
|
import org.http4k.format.Jackson.auto
|
||||||
import org.http4k.lens.Query
|
import org.http4k.lens.Query
|
||||||
|
|
||||||
class SearchRoute(private val api: Api) {
|
class SearchRouteProvider(private val api: Api) : RouteProvider {
|
||||||
|
|
||||||
fun searchRoute(): ContractRoute {
|
override fun route(): ContractRoute {
|
||||||
val query = Query.required("q", "Search terms")
|
val query = Query.required("q", "Search terms")
|
||||||
val responseLens = Body.auto<SearchResults>("Search results").toLens()
|
val responseLens = Body.auto<SearchResults>("Search results").toLens()
|
||||||
|
|
||||||
val spec = "/search" meta {
|
val spec = "/search" meta {
|
||||||
summary = "Search routes and waypoints"
|
summary = "Search routes and waypoints"
|
||||||
queries += query
|
queries += query
|
||||||
|
receiving(RequestMeta(Request(Method.GET, "/search").query("q", "tour de bavon")))
|
||||||
returning(Status.OK, responseLens to SearchResults(listOf(RouteResult(
|
returning(Status.OK, responseLens to SearchResults(listOf(RouteResult(
|
||||||
id = "57103",
|
id = "57103",
|
||||||
title = "Tour de Bavon : Thor",
|
title = "Tour de Bavon : Thor",
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package be.simplenotes.c2c
|
package be.simplenotes.c2c.server
|
||||||
|
|
||||||
import org.apache.hc.core5.http.impl.bootstrap.ServerBootstrap
|
import org.apache.hc.core5.http.impl.bootstrap.ServerBootstrap
|
||||||
import org.apache.hc.core5.http.io.SocketConfig
|
import org.apache.hc.core5.http.io.SocketConfig
|
||||||
@ -6,13 +6,16 @@ import org.http4k.core.HttpHandler
|
|||||||
import org.http4k.server.Http4kRequestHandler
|
import org.http4k.server.Http4kRequestHandler
|
||||||
import org.http4k.server.Http4kServer
|
import org.http4k.server.Http4kServer
|
||||||
import org.http4k.server.ServerConfig
|
import org.http4k.server.ServerConfig
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
|
||||||
|
class CustomApacheServer(private val socketAddress: InetSocketAddress) : ServerConfig {
|
||||||
|
|
||||||
class CustomApacheServer(val port: Int) : ServerConfig {
|
|
||||||
override fun toServer(httpHandler: HttpHandler): Http4kServer = object : Http4kServer {
|
override fun toServer(httpHandler: HttpHandler): Http4kServer = object : Http4kServer {
|
||||||
val handler = Http4kRequestHandler(httpHandler)
|
val handler = Http4kRequestHandler(httpHandler)
|
||||||
|
|
||||||
val server = ServerBootstrap.bootstrap()
|
val server = ServerBootstrap.bootstrap()
|
||||||
.setListenerPort(port)
|
.setListenerPort(socketAddress.port)
|
||||||
|
.setLocalAddress(socketAddress.address)
|
||||||
.setSocketConfig(SocketConfig.custom()
|
.setSocketConfig(SocketConfig.custom()
|
||||||
.setTcpNoDelay(true)
|
.setTcpNoDelay(true)
|
||||||
.setSoKeepAlive(true)
|
.setSoKeepAlive(true)
|
||||||
@ -28,6 +31,6 @@ class CustomApacheServer(val port: Int) : ServerConfig {
|
|||||||
|
|
||||||
override fun stop() = apply { server.stop() }
|
override fun stop() = apply { server.stop() }
|
||||||
|
|
||||||
override fun port(): Int = port
|
override fun port(): Int = socketAddress.port
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,11 +1,21 @@
|
|||||||
package be.simplenotes.c2c.templates
|
package be.simplenotes.c2c.templates
|
||||||
|
|
||||||
import com.mitchellbosecke.pebble.PebbleEngine
|
import com.mitchellbosecke.pebble.PebbleEngine
|
||||||
|
import com.mitchellbosecke.pebble.loader.ClasspathLoader
|
||||||
|
import com.mitchellbosecke.pebble.template.PebbleTemplate
|
||||||
import java.io.StringWriter
|
import java.io.StringWriter
|
||||||
|
|
||||||
fun PebbleEngine.render(name: String, args: Map<String, Any?> = mapOf()): String {
|
fun PebbleEngine.render(name: String, args: Map<String, Any?> = mapOf()) = getTemplate(name).render(args)
|
||||||
val template = getTemplate(name)
|
|
||||||
|
fun PebbleTemplate.render(args: Map<String, Any?> = mapOf()): String {
|
||||||
val writer = StringWriter()
|
val writer = StringWriter()
|
||||||
template.evaluate(writer, args)
|
evaluate(writer, args)
|
||||||
return writer.toString()
|
return writer.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("FunctionName")
|
||||||
|
fun PebbleEngine(cache: Boolean, prefix: String? = null, suffix: String? = ".twig"): PebbleEngine =
|
||||||
|
PebbleEngine.Builder().loader(ClasspathLoader().also { loader ->
|
||||||
|
prefix?.let { loader.prefix = it }
|
||||||
|
suffix?.let { loader.suffix = it }
|
||||||
|
}).cacheActive(cache).build()
|
||||||
|
|||||||
@ -1,10 +1,19 @@
|
|||||||
<h1>{{ route.title }}</h1>
|
<!DOCTYPE html>
|
||||||
<p>{{ route.summary }}</p>
|
<html lang="en">
|
||||||
<p>{{ route.description }}</p>
|
<head>
|
||||||
{% for img in route.associations.images %}
|
<title>ReDoc</title>
|
||||||
<figure>
|
<meta charset="utf-8"/>
|
||||||
<img src="{{ img.medium }}"
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
alt="{{ img.title }}">
|
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
|
||||||
<figcaption>{{ img.title }}</figcaption>
|
<style>
|
||||||
</figure>
|
body {
|
||||||
{% endfor %}
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<redoc spec-url='/api/swagger.json'></redoc>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|||||||
10
src/main/resources/templates/pdf.twig
Normal file
10
src/main/resources/templates/pdf.twig
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<h1>{{ route.title }}</h1>
|
||||||
|
<p>{{ route.summary }}</p>
|
||||||
|
<p>{{ route.description }}</p>
|
||||||
|
{% for img in route.associations.images %}
|
||||||
|
<figure>
|
||||||
|
<img src="{{ img.medium }}"
|
||||||
|
alt="{{ img.title }}">
|
||||||
|
<figcaption>{{ img.title }}</figcaption>
|
||||||
|
</figure>
|
||||||
|
{% endfor %}
|
||||||
@ -1,19 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<title>ReDoc</title>
|
|
||||||
<meta charset="utf-8"/>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<redoc spec-url='/api/swagger.json'></redoc>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
Loading…
x
Reference in New Issue
Block a user