Использование контекста запроса в виде #
Проблема #
В виде необходимо иметь доступ к контексту:
- Информации о текущем пользователе для отображения внутри HTML-документа.
- Информации о разрешениях пользователя для отображения или сокрытия элементов на HTML-странице.
Эта информация нужна для каждой страницы, в результате:
- в каждом обработчике добавляются линзы для получения контекста из запроса, даже если они не учитываются при обработке самого запроса;
- в каждой модели добавляются поля для передачи соответствующих данных в вид.
К сожалению библиотека http4k не предлагает встроенного решения для данной проблемы.
Вариант решения #
Одним из возможных вариантов является добавление следующих классов в приложение. Они относятся к подсистеме шаблонизатора, могут быть размещены в пакете templates
:
package ru.ac.uniyar.web.templates
import com.mitchellbosecke.pebble.PebbleEngine
import com.mitchellbosecke.pebble.error.LoaderException
import com.mitchellbosecke.pebble.loader.ClasspathLoader
import com.mitchellbosecke.pebble.loader.FileLoader
import org.http4k.template.ViewModel
import org.http4k.template.ViewNotFound
import java.io.StringWriter
typealias ContextAwareTemplateRenderer = (Map<String, Any?>, ViewModel) -> String
class ContextAwarePebbleTemplates(
private val configure: (PebbleEngine.Builder) -> PebbleEngine.Builder = { it },
private val classLoader: ClassLoader = ClassLoader.getSystemClassLoader()
) {
private class ContextAwarePebbleTemplateRenderer(
private val engine: PebbleEngine,
) : ContextAwareTemplateRenderer {
override fun invoke(context: Map<String, Any?>, viewModel: ViewModel): String = try {
val writer = StringWriter()
val wholeContext = context + mapOf("model" to viewModel)
engine.getTemplate(viewModel.template() + ".peb").evaluate(writer, wholeContext)
writer.toString()
} catch (_: LoaderException) {
throw ViewNotFound(viewModel)
}
}
fun CachingClasspath(baseClasspathPackage: String): ContextAwareTemplateRenderer {
val loader = ClasspathLoader(classLoader)
loader.prefix = if (baseClasspathPackage.isEmpty()) null else baseClasspathPackage.replace('.', '/')
return ContextAwarePebbleTemplateRenderer(
configure(
PebbleEngine.Builder().loader(loader),
).build()
)
}
fun Caching(baseTemplateDir: String): ContextAwareTemplateRenderer {
val loader = FileLoader()
loader.prefix = baseTemplateDir
return ContextAwarePebbleTemplateRenderer(
configure(
PebbleEngine.Builder().cacheActive(true).loader(loader),
).build()
)
}
fun HotReload(baseTemplateDir: String): ContextAwareTemplateRenderer {
val loader = FileLoader()
loader.prefix = baseTemplateDir
return ContextAwarePebbleTemplateRenderer(
configure(
PebbleEngine.Builder().cacheActive(false).loader(loader),
).build()
)
}
}
package ru.ac.uniyar.web.templates
import org.http4k.core.Body
import org.http4k.core.ContentType
import org.http4k.core.Request
import org.http4k.lens.BiDiBodyLens
import org.http4k.lens.BiDiBodyLensSpec
import org.http4k.lens.RequestContextLens
import org.http4k.lens.string
import org.http4k.template.ViewModel
data class ContextAwareViewRender(
private val templateRenderer: ContextAwareTemplateRenderer,
private val baseBodyLensSpec: BiDiBodyLensSpec<String>,
private val contextLenses: Map<String, RequestContextLens<*>> = emptyMap(),
) {
companion object {
fun withContentType(
templateRenderer: ContextAwareTemplateRenderer,
contentType: ContentType,
): ContextAwareViewRender =
ContextAwareViewRender(
templateRenderer,
Body.string(contentType),
)
}
fun associateContextLens(key: String, lens: RequestContextLens<*>): ContextAwareViewRender {
val newContextLenses = contextLenses + mapOf(key to lens)
return this.copy(contextLenses = newContextLenses)
}
fun associateContextLenses(lenses: Map<String, RequestContextLens<*>>): ContextAwareViewRender {
val newContextLenses = contextLenses + lenses
return this.copy(contextLenses = newContextLenses)
}
operator fun invoke(request: Request): BiDiBodyLens<ViewModel> {
return baseBodyLensSpec.map<ViewModel>(
{
throw UnsupportedOperationException("Cannot parse a ViewModel")
},
{ viewModel: ViewModel ->
templateRenderer(extractContext(request), viewModel)
}
).toLens()
}
private fun extractContext(request: Request): Map<String, Any?> =
contextLenses.mapValues {
it.value(request)
}
}
Класс ContextAwarePebbleTemplates
является аналогом класса PebbleTemplates
, и он обеспечивает создание HTML-документов из Pebble-шаблонов. Ключевое отличие состоит в том, что пользователю предоставляется возможность задания контекста для формирования шаблона. Т.е. внутри Pebble-шаблона помимо переменной model
могут быть доступны другие данные, получаемые из ассоциированных с объектом линз.
Его использование не отличается от использования PebbleTemplates
:
val renderer = ContextAwarePebbleTemplates().HotReload("src/main/resources")
Для создания HTML-документа данному объекту необходимо передать контекст и модель:
data class SomeModel(val info: text) : ViewModel
val model = SomeModel("сообщение")
val document = renderer(mapOf("extra" to "Экстра", "num" to 42), model)
Внутри шаблона SomeModel.peb
будут доступны 3 переменные:
extra
со значениемЭкстра
;num
со значением42
;model
с содержимым объектаmodel
.
Для того, чтобы была возможность обеспечить удобную работу с контекстом предоставляется второй класс: ContextAwareViewRender
. В задачи данного класса входит:
- Сохранение ссылки на шаблонизатор.
- Сохранение линз для доступа к контексту запроса.
- Создание линзы для записи HTML-документа в тело ответа.
При инициализации данного класса ему необходимо передать ссылки на шаблонизатор и линзы для получения данных контекста:
val htmlView = ContextAwareViewRender.withContentType(renderer, ContentType.TEXT_HTML)
val contexts = RequestContexts()
val currentWorkerLens: RequestContextLens<Worker?> = RequestContextKey.optional(contexts, "worker")
val permissionsLens: RequestContextLens<RolePermissions> =
RequestContextKey.required(contexts, name = "permissions")
val htmlViewWithContext = htmlView
.associateContextLens("currentWorker", currentWorkerLens)
.associateContextLens("permissions", permissionsLens)
Ссылку на данный объект можно передать вместо линзы для использования шаблонизатора. Внутри обработчика запроса с помощью данного объекта можно создать линзу и применить её для формирования ответа:
class ShowWorkersListHandler(
private val listWorkersPerPageQuery: ListWorkersPerPageQuery,
private val permissionsLens: RequestContextLens<RolePermissions>,
private val htmlView: ContextAwareViewRender,
) {
companion object {
private val pageLens = Query.int().defaulted("page", 1)
}
override fun invoke(Request: request) {
val permissions = permissionsLens(request)
if (!permissions.listWorkers) {
return Response(Status.UNAUTHORIZED)
}
val page = lensOrDefault(pageLens, request, 1)
val pagedResult = listWorkersPerPageOperation.listOnPage(page)
val paginator = Paginator(pagedResult.pageCount, page, request.uri)
val model = WorkersListVM(pagedResult.values, paginator)
return Response(Status.OK).with(htmlView(request) of model)
}
}
Стоит отметить, что если HTTP-обработчику необходимы данные из контекста запроса, то при его создании необходимо явно передать соответствующую линзу.
Задача #
Интегрируйте данные классы в приложение по управлению теугольниками, убрав
- передачу линз для получения контекста в обработчики HTTP-запросов;
- лишние поля из моделей, содержащих информацию о пользователе и его разрешениях.