Même si Chiron est loin de ce qu'on entend habituellement par "conteneur d'applications Web" il fournit des primitives pour aiguiller une requête HTTP vers le traitement approprié, en plus du support des WebSockets.
=== À quoi ça ressemble
Imaginons qu'on veuille une console de supervision accessible uniquement à partir de la machine hôte, à partir du chemin ``/console``. Pour que fonctionnent les URI relatives, il faut que le chemin de base soit ``/console/`` (avec une barre oblique à la fin) donc on effectue systématiquement une redirection. Après, pour le chemin ``/console/`` on affiche une page de bienvenue. Pour le chemin ``/console/diagnostic`` on affiche le diagnostic. À part les méthodes ``welcome()`` et ``diagnostic()`` qui sont spécifiques à l'application, tout le reste est fourni en standard.
Voilà comment ça s'écrit avec le ``HttpDispatcher`` de Chiron :
<<<
final HttpDispatcher httpDispatcher = HttpDispatcher.newDispatcher()
.beginPathSegment( "/console" )
.beginCondition( IS_LOCALHOST )
.beginCondition( IS_GET_METHOD )
.response( APPEND_TRAILING_SLASH_IF_CONTEXT_PATH_MATCHES )
.responseIf( relativeMatch( "/" ), welcome() )
.responseIf( relativeMatch( "/diagnostic" ), diagnostic() )
.notFound()
.endCondition()
.endCondition()
.forbidden()
.endPathSegment()
.notFound()
.build()
;
>>>
Le ``HttpDispatcher`` bâtit une liste de fonctions qui prennent en entrée un contexte d'évaluation et une requête HTTP. Il évalue ces fonctions les unes après les autres pour une requête donnée. Si une des fonctions renvoie une valeur non-nulle, c'est que la requête est traitée. La fonction va faire des choses avec le ``ChannelHandlerContext`` issu de Netty, par exemple écrire une réponse HTTP.
Pour un contenu prédéterminé ça ressemble à ça :
<<<
private HttpResponder.Outbound diagnostic() {
return ( evaluationContext, httpRequest ) -> channelHandlerContext -> {
final FullHttpResponse fullHttpResponse = createFullHttpResponseFrom(
httpRequest.channel().alloc(),
CONTENT_TYPE_HTML,
"<html><body>Diagnostics here</body></html>"
) ;
writeFlushAndClose( channelHandlerContext, fullHttpResponse ) ;
} ;
}
>>>
Cet exemple tape dans des primitives d'assez bas niveau, mais c'est facile d'écrire une fonction pour brancher le moteur de patronnage ("templating") de son choix pour obtenir du code plus concis. Le fait de recevoir un ``ChannelHandlerContext`` permet toutes les fantaisies, comme répondre avec du contenu en plusieurs morceaux.
Dans cet exemple on ne voit pas à quoi sert le paramètre ``evaluationContext``. Il contient des informations définies au moment de la construction du ``HttpDispatcher``, notamment le chemin complet définit par les appels imbriqués de ``beginPathSegment(_)``. C'est ce paramètre qu'utilise la fonction ``APPEND_TRAILING_SLASH_IF_CONTEXT_PATH_MATCHES`` pour effectuer une redirection si le chemin de la requête correspond exactement au chemin du contexte mais qu'il n'y a pas de ``/`` à la fin.
Une telle syntaxe est-elle viable avec des définitions plus volumineuses ? On peut éclater la définition d'un ``HttpDispatcher`` à l'aide de la fonction ``include``. Voyons comment l'utiliser dans l'exemple précédent :
<<<
final HttpDispatcher httpDispatcher = HttpDispatcher.newDispatcher()
.beginPathSegment( "/console" )
.include( new HttpConsole().setup() )
.endPathSegment()
.notFound()
.build()
;
>>>
Et la ``HttpConsole`` définit une fonction qui renvoie une fonction qui configure un ``HttpDispatcher`` :
<<<
public Consumer< HttpDispatcher<
?, ?, Void, Void, ? extends HttpDispatcher
> > setup() {
return httpDispatcher -> httpDispatcher
.beginCondition( IS_LOCALHOST )
// ... As above.
;
}
>>>
L'horrible type de retour correspond à une petite ruse pour propager le type d'un Duty dans un contexte donné (si on veut appeler ses méthodes). L'appel d'un ``include`` vérifie que les ``begin*(_)`` sont refermés par les ``end()`` correspondants.
Ce n'est pas montré dans l'exemple, mais il y a une version conditionnelle du ``include`` avec un paramètre supplémentaire qui ne provoque l'inclusion que s'il est évalué à ``true`` au moment de la construction du ``HttpDispatcher``.
=== Comment ça se branche
Nous l'avons vu, Chiron définit une classe ``UpendConnector.Setup`` qui contient toutes les définitions pour accepter des connexions et bâtir le ``ChannelPipeline`` approprié. Il y a un paramètre de type ``HttpRequestRelayer`` qui correspond à la valeur de retour de ``HttpDispatcher#build()``. En gros, un ``HttpRequestRelayer``, c'est quelque chose qui peut manipuler un ``ChannelHandler`` et une ``HttpRequest`` s'il en a envie. S'il n'en a pas envie, l'objet continuera son cheminement dans le ``ChannelPipeline`` de Netty.
La même instance de ``HttpRequestRelayer`` est partagée par tous les ~``ChannelHandler``s donc il faut qu'elle supporte la concurrence d'accès. Mais vu le style fonctionnel imposé par l'interface programmatique, c'est quelque chose qui se fait naturellement.
=== Ressources statiques et cache
Dans le paquet ``com.otcdlink.chiron.upend.http.content.caching`` de **Chiron-upend**, on trouve de quoi conserver des ressources HTTP dans la mémoire native ou le tas de la JVM. Ces ressources doivent être disponibles à l'initialisation du cache, et la durée de vie dans le cache est illimitée. C'est intéressant si on sait d'avance quelles ressources utiliser. Le cas d'utilisation c'est la génération de quelques pages HTML avec des ressources (logos et feuilles de style par exemple) définis comme constantes dans le code Java, ou commes ressources dans le classpath.
Je saute les détails, voici un exemple de code déclarant des ressources avec leur type MIME, utilisées par un ``HttpDispatcher``. Ça devrait parler à quiconque ayant compris qu'une ``ByteSource`` est un objet capable d'ouvrir un ``InputStream``.
<<<
public static final ImmutableMap< String, String > MIME_TYPE_MAP =
ImmutableMap.of(
"html", "text/html",
"css", "text/css",
"js", "application/javascript",
"png", "image/png",
"ico", "image/x-icon"
)
;
public static final ImmutableMap< String, ByteSource > ASSET_MAP =
new StaticContentMapBuilder( PortalAssetStock.class )
.put( "logo.png", Logo.getWebLogo() )
.put( "favicon.ico", Logo.getFavicon() )
.put( "site.css" )
.build()
;
final StaticContentCache homeStaticContentCache =
new StaticContentCacheFactory().sharedCache(
PortalAssetStock.MIME_TYPE_MAP,
PortalAssetStock.HOMEPAGE_MAP
)
;
final HttpRequestRelayer foremostCommandRecognizer =
HttpDispatcher.newDispatcher( timeKit.designatorFactory )
// ...
.resourceMatch( homeStaticContentCache )
;
>>>
Étant donné qu'on peut dériver un ``HttpDispatcher``, et qu'il donne accès à la ``HttpRequest`` et au ``ChannelHandlerContext``, on peut facilement écrire d'autres types de caches, avec les mêmes facilités syntaxiques.
=== Commandes
Jusqu'à maintenant nous n'avons vu que le traitement de requêtes HTTP sans appel à la logique applicative. Mais des fois il y a besoin. Qu'est-ce qu'on voudrait dans ce cas-là ?
- Appeler une interface Duty avec le bon type.
- Que ça crée un objet ``Command`` qui vive sa vie normale.
- Que la logique applicative fournisse un résultat sans lien direct avec la représentation spécifique au Web.
La solution c'est bien sûr de passer une fonction de rappel qui prenne le résultat et le transforme en réponse HTTP. Il y a du code pour ça, le typage des ``UpwardDuty`` est respecté, mais je ne veux pas abuser de la patience du lecteur, s'il y a des gens que ça intéresse vraiment ils n'ont qu'à demander.
=== Authentification
Pour l'instant l'authentification de Chiron ne fonctionne qu'avec les WebSockets, il n'y a rien qui supporte des Cookies HTTP. Mais une telle fonctionnalité devrait rapidement voir le jour. Je devrais me fouetter avec des orties fraîches à chaque fois que je promets que quelque chose doit arriver "rapidement".