Repository: wicket Updated Branches: refs/heads/master 693dad38a -> e7597f03f
easier rendering of components, closes #249 setup ThreadLocal independently from filter or tester Project: http://git-wip-us.apache.org/repos/asf/wicket/repo Commit: http://git-wip-us.apache.org/repos/asf/wicket/commit/a148e088 Tree: http://git-wip-us.apache.org/repos/asf/wicket/tree/a148e088 Diff: http://git-wip-us.apache.org/repos/asf/wicket/diff/a148e088 Branch: refs/heads/master Commit: a148e088f13a4ec1cebfc938e24d6215e19576c8 Parents: 693dad3 Author: Sven Meier <[email protected]> Authored: Thu Dec 7 09:24:12 2017 +0100 Committer: Sven Meier <[email protected]> Committed: Thu Dec 7 20:40:36 2017 +0100 ---------------------------------------------------------------------- .../core/util/string/ComponentRenderer.java | 346 ++++++++++++++++++- .../ComponentRendererInstanceTest.java | 51 +++ 2 files changed, 379 insertions(+), 18 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/wicket/blob/a148e088/wicket-core/src/main/java/org/apache/wicket/core/util/string/ComponentRenderer.java ---------------------------------------------------------------------- diff --git a/wicket-core/src/main/java/org/apache/wicket/core/util/string/ComponentRenderer.java b/wicket-core/src/main/java/org/apache/wicket/core/util/string/ComponentRenderer.java index 0cf63bf..d583057 100644 --- a/wicket-core/src/main/java/org/apache/wicket/core/util/string/ComponentRenderer.java +++ b/wicket-core/src/main/java/org/apache/wicket/core/util/string/ComponentRenderer.java @@ -16,18 +16,34 @@ */ package org.apache.wicket.core.util.string; +import java.io.Serializable; +import java.util.List; +import java.util.Set; +import java.util.function.Supplier; + import org.apache.wicket.Application; import org.apache.wicket.Component; import org.apache.wicket.MarkupContainer; +import org.apache.wicket.Page; +import org.apache.wicket.RuntimeConfigurationType; +import org.apache.wicket.Session; import org.apache.wicket.ThreadContext; import org.apache.wicket.core.request.handler.PageProvider; import org.apache.wicket.markup.IMarkupCacheKeyProvider; import org.apache.wicket.markup.IMarkupResourceStreamProvider; import org.apache.wicket.markup.MarkupNotFoundException; import org.apache.wicket.markup.html.WebPage; +import org.apache.wicket.mock.MockApplication; +import org.apache.wicket.mock.MockWebRequest; import org.apache.wicket.protocol.http.BufferedWebResponse; +import org.apache.wicket.protocol.http.WebApplication; +import org.apache.wicket.protocol.http.mock.MockServletContext; +import org.apache.wicket.request.Request; import org.apache.wicket.request.Response; +import org.apache.wicket.request.Url; import org.apache.wicket.request.cycle.RequestCycle; +import org.apache.wicket.serialize.ISerializer; +import org.apache.wicket.session.ISessionStore; import org.apache.wicket.util.resource.IResourceStream; import org.apache.wicket.util.resource.StringResourceStream; import org.slf4j.Logger; @@ -35,20 +51,303 @@ import org.slf4j.LoggerFactory; /** * A helper class for rendering components and pages. - * - * <p><strong>Note</strong>: {@link #renderComponent(Component)} does <strong>not</strong> - * support rendering {@link org.apache.wicket.markup.html.panel.Fragment} instances!</p> + * <p> + * With the static methods of this class components and pages can be rendered on a thread already + * processing an {@link Application}. + * <p> + * If you want to render independently from any web request processing (e.g. generating an email + * body on a worker thread), you can create an instance of this class.<br/> + * You may use an existing application, create a fresh one or just use the automatically created + * mocked application with sensible defaults. + * <p> + * Note: For performance reasons instances can and should be reused, be sure to call {@link #destroy()} when + * they are no longer needed. */ public class ComponentRenderer { private static final Logger LOGGER = LoggerFactory.getLogger(ComponentRenderer.class); + private Application application; + + /** + * A renderer using a default mocked application, which + * <ul> + * <li>never shares anything in a session</li> + * <li>never serializes anything</li> + * </ul> + */ + public ComponentRenderer() + { + this(new MockApplication() + { + @Override + public RuntimeConfigurationType getConfigurationType() + { + return RuntimeConfigurationType.DEPLOYMENT; + } + + @Override + protected void init() + { + super.init(); + + setSessionStoreProvider(() -> new NeverSessionStore()); + getFrameworkSettings().setSerializer(new NeverSerializer()); + } + }); + } + /** - * Collects the html generated by the rendering of a page. + * A renderer using the given application. + * <p> + * If the application was not yet initialized - e.g. it is not reused from an already running + * web container - it will be initialized. + * + * @param application the application to render components in + * + * @see Application#initApplication() + */ + public ComponentRenderer(Application application) + { + this.application = application; + + if (application.getName() == null) + { + // not yet initialized + inThreadContext(this::initApplication); + } + } + + private void initApplication() + { + if (application instanceof WebApplication) { + WebApplication webApplication = (WebApplication)application; + + // WebApplication requires a servlet context + webApplication.setServletContext(new MockServletContext(application, null)); + } + + application.setName("ComponentRenderer[" + System.identityHashCode(ComponentRenderer.this) + "]"); + application.initApplication(); + } + + /** + * Destroy this renderer. + */ + public void destroy() + { + inThreadContext(() -> { + application.internalDestroy(); + application = null; + }); + } + + /** + * + * Collects the Html generated by rendering a component. + * + * @param component + * supplier of the component + * @return html rendered by the panel + */ + public CharSequence renderComponent(final Supplier<Component> component) + { + return renderPage(() -> new RenderPage(component.get())); + } + + /** + * Collects the html generated by rendering a page. + * + * @param page + * supplier of the page + * @return the html rendered by the panel + */ + public CharSequence renderPage(final Supplier<? extends Page> page) + { + return inThreadContext(() -> { + Request request = newRequest(); + + BufferedWebResponse response = new BufferedWebResponse(null); + + RequestCycle cycle = application.createRequestCycle(request, response); + + ThreadContext.setRequestCycle(cycle); + + page.get().renderPage(); + + return response.getText(); + }); + } + + /** + * Run the given runnable inside a bound {@link ThreadContext}. + * + * @param runnable + * runnable + */ + private void inThreadContext(Runnable runnable) + { + inThreadContext(() -> { + runnable.run(); + return null; + }); + } + + /** + * Get the result from the given supplier inside a bound {@link ThreadContext}. + * + * @param supplier + * supplier + * @return result of {@link Supplier#get()} + */ + private <T> T inThreadContext(Supplier<T> supplier) + { + ThreadContext oldContext = ThreadContext.detach(); + + try + { + ThreadContext.setApplication(application); + + return supplier.get(); + } + finally + { + + ThreadContext.restore(oldContext); + } + } + + /** + * Create a new request, by default a {@link MockWebRequest}. + */ + protected Request newRequest() + { + return new MockWebRequest(Url.parse("/")); + } + + /** + * Never serialize. + */ + private static final class NeverSerializer implements ISerializer + { + @Override + public byte[] serialize(Object object) + { + return null; + } + + @Override + public Object deserialize(byte[] data) + { + return null; + } + } + + /** + * Never share anything. + */ + private static class NeverSessionStore implements ISessionStore + { + + @Override + public Serializable getAttribute(Request request, String name) + { + return null; + } + + @Override + public List<String> getAttributeNames(Request request) + { + return null; + } + + @Override + public void setAttribute(Request request, String name, Serializable value) + { + } + + @Override + public void removeAttribute(Request request, String name) + { + } + + @Override + public void invalidate(Request request) + { + } + + @Override + public String getSessionId(Request request, boolean create) + { + return null; + } + + @Override + public Session lookup(Request request) + { + return null; + } + + @Override + public void bind(Request request, Session newSession) + { + } + + @Override + public void flushSession(Request request, Session session) + { + } + + @Override + public void destroy() + { + } + + @Override + public void registerUnboundListener(UnboundListener listener) + { + } + + @Override + public void unregisterUnboundListener(UnboundListener listener) + { + } + + @Override + public Set<UnboundListener> getUnboundListener() + { + return null; + } + + @Override + public void registerBindListener(BindListener listener) + { + } + + @Override + public void unregisterBindListener(BindListener listener) + { + } + + + @Override + + public Set<BindListener> getBindListeners() + { + return null; + } + } + + /** + * Collects the Html generated by the rendering a page. + * <p> + * Important note: Must be called on a thread bound to an application's {@link ThreadContext}! * * @param pageProvider * the provider of the page class/instance and its parameters * @return the html rendered by a page + * + * @see ThreadContext */ public static CharSequence renderPage(final PageProvider pageProvider) { @@ -58,7 +357,8 @@ public class ComponentRenderer BufferedWebResponse tempResponse = new BufferedWebResponse(null); - RequestCycle tempRequestCycle = application.createRequestCycle(originalRequestCycle.getRequest(), tempResponse); + RequestCycle tempRequestCycle = application + .createRequestCycle(originalRequestCycle.getRequest(), tempResponse); try { @@ -74,17 +374,23 @@ public class ComponentRenderer } /** - * Collects the html generated by the rendering of a component. - * + * Collects the Html generated by rendering a component. * <p> - * NOTE: this method is meant to render fresh component instances that are disposed after the - * html has been generate. To avoid unwanted side effects do not use it with components that - * are from an existing hierarchy. - * </p> + * Important notes: + * <ul> + * <li>this method is meant to render fresh component instances that are disposed after the html + * has been generate. To avoid unwanted side effects do not use it with components that are from + * an existing hierarchy.</li> + * <li>does <strong>not</strong> support rendering + * {@link org.apache.wicket.markup.html.panel.Fragment} instances</li> + * <li>must be called on a thread bound to an application's {@link ThreadContext}!</li> + * </ul> * * @param component * the component to render. * @return the html rendered by the component + * + * @see ThreadContext */ public static CharSequence renderComponent(final Component component) { @@ -97,10 +403,9 @@ public class ComponentRenderer if (oldParent != null && LOGGER.isWarnEnabled()) { - LOGGER.warn("Component '{}' with a parent '{}' is passed for standalone rendering. " + - "It is recommended to render only orphan components because they are not cleaned up/detached" + - " after the rendering.", - component, oldParent); + LOGGER.warn("Component '{}' with a parent '{}' is passed for standalone rendering. " + + "It is recommended to render only orphan components because they are not cleaned up/detached" + + " after the rendering.", component, oldParent); } try @@ -129,7 +434,10 @@ public class ComponentRenderer /** * A page used as a parent for the component based rendering. */ - private static class RenderPage extends WebPage implements IMarkupResourceStreamProvider, IMarkupCacheKeyProvider + private static class RenderPage extends WebPage + implements + IMarkupResourceStreamProvider, + IMarkupCacheKeyProvider { /** * Markup to use when the component to render is not already added to a MarkupContainer @@ -150,7 +458,8 @@ public class ComponentRenderer try { componentMarkup = component.getMarkup().toString(true); - } catch (MarkupNotFoundException mnfx) + } + catch (MarkupNotFoundException mnfx) { componentMarkup = String.format(DEFAULT_MARKUP, component.getId()); } @@ -159,7 +468,8 @@ public class ComponentRenderer } @Override - public IResourceStream getMarkupResourceStream(MarkupContainer container, Class<?> containerClass) + public IResourceStream getMarkupResourceStream(MarkupContainer container, + Class<?> containerClass) { return new StringResourceStream(markup); } http://git-wip-us.apache.org/repos/asf/wicket/blob/a148e088/wicket-core/src/test/java/org/apache/wicket/core/util/string/componentrenderer/ComponentRendererInstanceTest.java ---------------------------------------------------------------------- diff --git a/wicket-core/src/test/java/org/apache/wicket/core/util/string/componentrenderer/ComponentRendererInstanceTest.java b/wicket-core/src/test/java/org/apache/wicket/core/util/string/componentrenderer/ComponentRendererInstanceTest.java new file mode 100644 index 0000000..fe5aafb --- /dev/null +++ b/wicket-core/src/test/java/org/apache/wicket/core/util/string/componentrenderer/ComponentRendererInstanceTest.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.wicket.core.util.string.componentrenderer; + +import static org.junit.Assert.assertEquals; + +import org.apache.wicket.core.util.string.ComponentRenderer; +import org.apache.wicket.markup.html.basic.Label; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * Tests for {@link ComponentRenderer} + */ +public class ComponentRendererInstanceTest +{ + private ComponentRenderer renderer; + + @Before + public void setup() { + renderer = new ComponentRenderer(); + } + + @After + public void destroy() { + renderer.destroy(); + } + + @Test + public void render() + { + CharSequence html = renderer.renderComponent(() -> new Label("id", "Hello renderer")); + + assertEquals("Hello renderer", html.toString()); + } +} \ No newline at end of file
