Programatically render template area in Magnolia CMS

…衆ロ難τιáo~ 提交于 2019-12-24 14:02:06

问题


I am using Magnolia CMS 5.4 and I want to build a module that will render some content of a page and expose it over REST API. The task is simple but not sure how to approach it and/or where to start.

I want my module to generate partial template or an area of a template for a given reference, let's say that is "header". I need to render the header template/area get the HTML and return that as a response to another system.

So questions are: is this possible at all and where to start?


回答1:


OK after asking here and on Magnolia forum couldn't get answer I dug in the source code and found a way to do it.

First thing the rendering works based on different renderers and those could be JCR, plain text or Freemarker renderer. In Magnolia those are decided and used in RenderingEngine and the implementation: DefaultRenderingEngine. The rendering engine will allow you to render a whole page node which is one step closer to what I am trying to achieve. So let's see how could this be done:

I'll skip some steps but I've added command and made that work over REST so I could see what's happening when I send a request to the endpoint. The command extends BaseRepositoryCommand to allow access to the JCR repositories.

@Inject
public setDefaultRenderingEngine(
    final RendererRegistry rendererRegistry,
    final TemplateDefinitionAssignment templateDefinitionAssignment,
    final RenderableVariationResolver variationResolver,
    final Provider<RenderingContext> renderingContextProvider
) {
    renderingEngine = new DefaultRenderingEngine(rendererRegistry, templateDefinitionAssignment,
            variationResolver, renderingContextProvider);
}

This creates your rendering engine and from here you can start rendering nodes with few small gotchas. I've tried injecting the rendering engine directly but that didn't work as all of the internals were empty/null so decided to grab all construct properties and initialise my own version.

Next step is we want to render a page node. First of all the rendering engine works based on the idea it's rendering for a HttpServletResponse and ties to the request/response flow really well, though we need to put the generated markup in a variable so I've added a new implementation of the FilteringResponseOutputProvider:

public class AppendableFilteringResponseOutputProvider extends FilteringResponseOutputProvider {

    private final FilteringAppendableWrapper appendable;

    private OutputStream outputStream = new ByteArrayOutputStream();

    public AppendableFilteringResponseOutputProvider(HttpServletResponse aResponse) {
        super(aResponse);

        OutputStreamWriter writer = new OutputStreamWriter(outputStream);
        appendable = Components.newInstance(FilteringAppendableWrapper.class);
        appendable.setWrappedAppendable(writer);
    }

    @Override
    public Appendable getAppendable() throws IOException {
        return appendable;
    }

    @Override
    public OutputStream getOutputStream() throws IOException {
        ((Writer) appendable.getWrappedAppendable()).flush();

        return outputStream;
    }

    @Override
    public void setWriteEnabled(boolean writeEnabled) {
        super.setWriteEnabled(writeEnabled);
        appendable.setWriteEnabled(writeEnabled);
    }
}

So idea of the class is to expose the output stream and still preserve the FilteringAppendableWrapper that will allow us the filter the content we want to write. This is not needed in the general case, you can stick to using AppendableOnlyOutputProvider with StringBuilder appendable and easily retrieve the entire page markup.

// here I needed to create a fake HttpServletResponse
OutputProvider outputProvider = new AppendableFilteringResponseOutputProvider(new FakeResponse());

Once you have the output provider you need a page node and since you are faking it you need to set the Magnolia global env to be able to retrieve the JCR node:

// populate repository and root node as those are not set for commands
super.setRepository(RepositoryConstants.WEBSITE);
super.setPath(nodePath); // this can be any existing path like: "/home/page"
Node pageNode = getJCRNode(context);

Now we have the content provider and the node we want to render next thing is actually running the rendering engine:

renderingEngine.render(pageNode, outputProvider);
outputProvider.getOutputStream().toString();

And that's it, you should have your content rendered and you can use it as you wish.

Now we come to my special case where I want to render just an area of the whole page in this case this is the Header of the page. This is all handled by same renderingEngine though you need to add a rendering listener that overrides the writing process. First inject it in the command:

@Inject
public void setAreaFilteringListener(final AreaFilteringListener aAreaFilteringListener) {
    areaFilteringListener = aAreaFilteringListener;
}

This is where the magic happens, the AreaFilteringListener will check if you are currently rendering the requested area and if you do it enables the output provider for writing otherwise keeps it locked and skips all unrelated areas. You need to add the listener to the rendering engine like so:

// add the area filtering listener that generates specific area HTML only
LinkedList<AbstractRenderingListener> listeners = new LinkedList<>();
listeners.add(areaFilteringListener);
renderingEngine.setListeners(listeners);

// we need to provide the exact same Response instance that the WebContext is using
// otherwise the voters against the AreaFilteringListener will skip the execution
renderingEngine.initListeners(outputProvider, MgnlContext.getWebContext().getResponse());

I hear you ask: "But where do we specify the area to be rendered?", aha here is comes:

// enable the area filtering listener through a global flag
MgnlContext.setAttribute(AreaFilteringListener.MGNL_AREA_PARAMETER, areaName);
MgnlContext.getAggregationState().setMainContentNode(pageNode);

The area filtering listener is checking for a specific Magnolia context property to be set: "mgnlArea" if that's found it will read its value and use it as an area name, check if that area exists in the node and then enable writing once we hit the area. This could be also used through URLs like: https://demopublic.magnolia-cms.com/~mgnlArea=footer~.html and this will give you just the footer area generated as an HTML page.

here is the full solution: http://yysource.com/2016/03/programatically-render-template-area-in-magnolia-cms/




回答2:


Just use the path of the area and make a http request using that url, e.g. http://localhost:9080/magnoliaAuthor/travel/main/0.html As far as I can see there is no need to go through everything programmatically as you did. Direct component rendering



来源:https://stackoverflow.com/questions/33346351/programatically-render-template-area-in-magnolia-cms

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!