options for connecting templating middleware

classic Classic list List threaded Threaded
6 messages Options
Reply | Threaded
Open this post in threaded view
|

options for connecting templating middleware

hash
I'll start my question with a description of what I actually what to get done, so I don't go off the rails immediately overfitting what I think the solution should look like :)

I'm assembling a website that pulls data from several different sources.  I'm doing this in several handlers: for example one asset handler that drags in from the filesystem, and another that renders markdown from another source.  I want to have one uniform set of headers and footers wrapping all the html pages in this site; I have a templater that will do what I want, but where do I inject it?

The templater needs to see the full body of the page so it can see any directives embedded in it.  It may add bytes to the beginning and end of the stream, but also add/remove bytes from the stream.

The challenge here is that if I'm using the built-in asset handler in ratpack, iiuc it commits to being the end of the chain when it calls context.getResponse().send(*).  I seem to want to pick up the content *after* that.  (I also probably want to get involved only after response mime type is picked out, since I want to process any page with a response mime type of 'text/html'; 'application/x-iso9660-image' not so much; but I think I see how to do that.)

Option 1: Could I use the mechanisms for passing data between handlers to have the handler loading the resource (that is, the markdown render or the asset loader) save response bodies for a later templater handler to pick up, process, and send?  Well, sort of, but it doesn't seem very composable -- I'd have to fork the asset handler, for starters.  And after that, sort of agree on a name for passing around the psuedo-response.  Having handlers opt-in to and have cross-cutting knowledge of the templater doesn't seem like the right path -- for the sake of ability to both gain from and contribute back to the community, I really want to avoid forking all the built in handlers.

Option 2: Could I insert a handler into my flow that wraps the Context on its way through with another delegating context that replaces the getResponse() method with another delegating Response implementation that overrides all the send(*) methods?  Maybe!  This approach seems like it would work fine for composing other existing handlers.  But boy did I use the heck out of eclipse code generation to do it; it feels like this might be fragile in the face of ratpack API updates (and incidentally, is there perhaps some kind of ContextDelegateAdapter already in the code base I should use?).

I also had trouble in practice with my delegated context gets cut back out of the chain somehow (specifically, I think by the filesystem handler in the middle of the handler set produced by the Handlers.assets() method, though I haven't debugged this all the way yet), so I'm not sure if Context wrapping is a good/supported idea.  And it's also a shame there's no single method in Response I can override to take part in all of the send flows (it seems like DefaultResponse.commit(ByteBuf) is almost exactly what I want, but it's a private implementation detail).

Option 3: Suggestions?  This is my first go at ratpack; there's almost certainly things I've missed. :)
Reply | Threaded
Open this post in threaded view
|

Re: options for connecting templating middleware

Luke Daley
Administrator
hash wrote
Option 1: Could I use the mechanisms for passing data between handlers to have the handler loading the resource…
This is a dead end I think.

hash wrote
Option 2: Could I insert a handler into my flow that wraps the Context on its way through
This won't work because each handler gets a different Context instance. There's intentionally no avenue for supplying your own instance. The closest you can do is wrap the context for a given handler (see GroovyHandler), but this won't help you.


I'm not sure that doing something completely cross cutting is the right thing to do here. There are potential issues with conditional requests and last modified headers. It would also negate the use of zero copy sending of files from the file system and introduce a performance overhead.

I'd be inclined to implement this in a more focussed way. I'd put a handler early in the chain that inspected the request path. If the extension matches something that you want to extend, use Context.file() to read the file and do the processing, similarly for the markdown stuff.
Reply | Threaded
Open this post in threaded view
|

Re: options for connecting templating middleware

hash
I agree, Option 1 is the least pleasant option.  It just came to mind first because other discussions of middleware here used that sort of thing to good effect for i.e. logins.

I found Option 2 works if I'm setting up the next handler myself so that I'm immediately in the path.  But if it's intention that this won't work when chaining other handlers from the core set, then yes, it's limited in viability.

Maybe a code sample is in order.  Here's a rough outline of what I wish I could do:

```
return chain(launchConfig, (Action<? super Chain>) chain -> {
        chain.handler(new TemplateSwaddlingHandler(chain(launchConfig, (Action<? super Chain>) chain2 -> {
                // all these handlers should have the same template engine pass on their output
                chain2.handler(fileSystem(launchConfig, "www/", new MarkdownHandler()));
                // not all of these handlers should "know" about the template engine
                chain2.handler(assets(launchConfig, "www/", new ArrayList<String>(){{ add("index.html"); }}));
                // ... numerous other handlers pulling in various sources
                chain.handler("foobar", context -> context.render("several custom renders pulling from other apis"));
        })));
        // handlers outside the chain with the template engine shouldn't pass through it
        chain.handler(assets(launchConfig, "plain/", new ArrayList<String>(){{ add("index.html"); }}));
});
```

The template handler in this example would definitely need to be careful not to get in the way of zero-copy file transfers, I agree.  I do literally have ISO files I want to get served out from under that second handler and yes I fear OOM :)  But, I think content type for example can provide a good hint for that.  If it's not 'text/html', the handler would just pass that ByteBuf straight on through without interference.  If it *is* the 'text/html' I'm filtering for, yes, that will require spooling the whole page into memory; but I'm already way past having accepted that as a cost of doing business with this particular templater.

I don't see a way to do this with going directly to Context.file().  How can I use that without ending up with one megahandler that knows all the extensions and patterns at once?

Hm, I haven't thought about the last modified headers yet.  Good point.  I think that's addressable though: a well designed TemplateSwaddlingHandler could take the last-modified from the handler inside that resolved the request, or the last-modified time from the header template files, whichever is more recent.
Reply | Threaded
Open this post in threaded view
|

Re: options for connecting templating middleware

Luke Daley
Administrator
I’d probably approach this by implementing the templating logic as a Renderer. I’d then have different handlers that pull data from wherever, wrap the content up into the value object and then use render() to send it on. This seems straightforward to me except for the file system case.

In that case, I’d write a handler that is before a standard asset handler in the chain that looks for requests for HTML files (i.e. looks at the request path). You’d use file() to then get a Path for the file object. You’d be then responsible for loading up the file into memory and into a value object for your renderer to use with the render() method.
Reply | Threaded
Open this post in threaded view
|

Re: options for connecting templating middleware

hash
Ooh, I haven't looked enough at handlers yet.  I'll see how much I can do with those next.

Docs seem a little sparse but I'll see some references to the jackson/json renderer, so I'll start there.

Thanks for the suggestion!
Reply | Threaded
Open this post in threaded view
|

Re: options for connecting templating middleware

Luke Daley
Administrator