001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.wicket.core.util.string;
018
019import java.io.Serializable;
020import java.util.List;
021import java.util.Set;
022import java.util.function.Supplier;
023
024import org.apache.wicket.Application;
025import org.apache.wicket.Component;
026import org.apache.wicket.MarkupContainer;
027import org.apache.wicket.Page;
028import org.apache.wicket.RuntimeConfigurationType;
029import org.apache.wicket.Session;
030import org.apache.wicket.ThreadContext;
031import org.apache.wicket.core.request.handler.PageProvider;
032import org.apache.wicket.markup.IMarkupCacheKeyProvider;
033import org.apache.wicket.markup.IMarkupResourceStreamProvider;
034import org.apache.wicket.markup.MarkupNotFoundException;
035import org.apache.wicket.markup.html.WebPage;
036import org.apache.wicket.mock.MockApplication;
037import org.apache.wicket.mock.MockWebRequest;
038import org.apache.wicket.protocol.http.BufferedWebResponse;
039import org.apache.wicket.protocol.http.WebApplication;
040import org.apache.wicket.protocol.http.mock.MockServletContext;
041import org.apache.wicket.request.Request;
042import org.apache.wicket.request.Response;
043import org.apache.wicket.request.Url;
044import org.apache.wicket.request.cycle.RequestCycle;
045import org.apache.wicket.serialize.ISerializer;
046import org.apache.wicket.session.ISessionStore;
047import org.apache.wicket.util.resource.IResourceStream;
048import org.apache.wicket.util.resource.StringResourceStream;
049import org.slf4j.Logger;
050import org.slf4j.LoggerFactory;
051
052/**
053 * A helper class for rendering components and pages.
054 * <p>
055 * With the static methods of this class components and pages can be rendered on a thread already
056 * processing an {@link Application}.
057 * <p>
058 * If you want to render independently from any web request processing (e.g. generating an email
059 * body on a worker thread), you can create an instance of this class.<br/>
060 * You may use an existing application, create a fresh one or just use the automatically created
061 * mocked application with sensible defaults.
062 * <p>
063 * Note: For performance reasons instances can and should be reused, be sure to call {@link #destroy()} when
064 * they are no longer needed.
065 */
066public class ComponentRenderer
067{
068        private static final Logger LOGGER = LoggerFactory.getLogger(ComponentRenderer.class);
069
070        private Application application;
071
072        /**
073         * A renderer using a default mocked application, which
074         * <ul>
075         * <li>never shares anything in a session</li>
076         * <li>never serializes anything</li>
077         * </ul>
078         */
079        public ComponentRenderer()
080        {
081                this(new MockApplication()
082                {
083                        @Override
084                        public RuntimeConfigurationType getConfigurationType()
085                        {
086                                return RuntimeConfigurationType.DEPLOYMENT;
087                        }
088
089                        @Override
090                        protected void init()
091                        {
092                                super.init();
093
094                                setSessionStoreProvider(() -> new NeverSessionStore());
095                                getFrameworkSettings().setSerializer(new NeverSerializer());
096                        }
097                });
098        }
099
100        /**
101         * A renderer using the given application.
102         * <p>
103         * If the application was not yet initialized - e.g. it is not reused from an already running
104         * web container - it will be initialized.
105         * 
106         * @param application the application to render components in
107         * 
108         * @see Application#initApplication()
109         */
110        public ComponentRenderer(Application application)
111        {
112                this.application = application;
113
114                if (application.getName() == null)
115                {
116                        // not yet initialized
117                        inThreadContext(this::initApplication);
118                }
119        }
120
121        private void initApplication()
122        {
123                if (application instanceof WebApplication) {
124                        WebApplication webApplication = (WebApplication)application;
125                        
126                        // WebApplication requires a servlet context
127                        webApplication.setServletContext(new MockServletContext(application, null));
128                }
129                
130                application.setName("ComponentRenderer[" + System.identityHashCode(ComponentRenderer.this) + "]");
131                application.initApplication();
132        }
133
134        /**
135         * Destroy this renderer.
136         */
137        public void destroy()
138        {
139                inThreadContext(() -> {
140                        application.internalDestroy();
141                        application = null;
142                });
143        }
144
145        /**
146         * 
147         * Collects the Html generated by rendering a component.
148         * 
149         * @param component
150         *            supplier of the component
151         * @return html rendered by the panel
152         */
153        public CharSequence renderComponent(final Supplier<Component> component)
154        {
155                return renderPage(() -> new RenderPage(component.get()));
156        }
157
158        /**
159         * Collects the html generated by rendering a page.
160         *
161         * @param page
162         *            supplier of the page
163         * @return the html rendered by the panel
164         */
165        public CharSequence renderPage(final Supplier<? extends Page> page)
166        {
167                return inThreadContext(() -> {
168                        Request request = newRequest();
169
170                        BufferedWebResponse response = new BufferedWebResponse(null);
171
172                        RequestCycle cycle = application.createRequestCycle(request, response);
173
174                        ThreadContext.setRequestCycle(cycle);
175
176                        page.get().renderPage();
177
178                        return response.getText();
179                });
180        }
181
182        /**
183         * Run the given runnable inside a bound {@link ThreadContext}.
184         * 
185         * @param runnable
186         *            runnable
187         */
188        private void inThreadContext(Runnable runnable)
189        {
190                inThreadContext(() -> {
191                        runnable.run();
192                        return null;
193                });
194        }
195
196        /**
197         * Get the result from the given supplier inside a bound {@link ThreadContext}.
198         * 
199         * @param supplier
200         *            supplier
201         * @return result of {@link Supplier#get()}
202         */
203        private <T> T inThreadContext(Supplier<T> supplier)
204        {
205                ThreadContext oldContext = ThreadContext.detach();
206
207                try
208                {
209                        ThreadContext.setApplication(application);
210
211                        return supplier.get();
212                }
213                finally
214                {
215
216                        ThreadContext.restore(oldContext);
217                }
218        }
219
220        /**
221         * Create a new request, by default a {@link MockWebRequest}.
222         */
223        protected Request newRequest()
224        {
225                return new MockWebRequest(Url.parse("/"));
226        }
227
228        /**
229         * Never serialize.
230         */
231        private static final class NeverSerializer implements ISerializer
232        {
233                @Override
234                public byte[] serialize(Object object)
235                {
236                        return null;
237                }
238
239                @Override
240                public Object deserialize(byte[] data)
241                {
242                        return null;
243                }
244        }
245
246        /**
247         * Never share anything.
248         */
249        private static class NeverSessionStore implements ISessionStore
250        {
251
252                @Override
253                public Serializable getAttribute(Request request, String name)
254                {
255                        return null;
256                }
257
258                @Override
259                public List<String> getAttributeNames(Request request)
260                {
261                        return null;
262                }
263
264                @Override
265                public void setAttribute(Request request, String name, Serializable value)
266                {
267                }
268
269                @Override
270                public void removeAttribute(Request request, String name)
271                {
272                }
273
274                @Override
275                public void invalidate(Request request)
276                {
277                }
278
279                @Override
280                public String getSessionId(Request request, boolean create)
281                {
282                        return null;
283                }
284
285                @Override
286                public Session lookup(Request request)
287                {
288                        return null;
289                }
290
291                @Override
292                public void bind(Request request, Session newSession)
293                {
294                }
295
296                @Override
297                public void flushSession(Request request, Session session)
298                {
299                }
300
301                @Override
302                public void destroy()
303                {
304                }
305
306                @Override
307                public void registerUnboundListener(UnboundListener listener)
308                {
309                }
310
311                @Override
312                public void unregisterUnboundListener(UnboundListener listener)
313                {
314                }
315
316                @Override
317                public Set<UnboundListener> getUnboundListener()
318                {
319                        return null;
320                }
321
322                @Override
323                public void registerBindListener(BindListener listener)
324                {
325                }
326
327                @Override
328                public void unregisterBindListener(BindListener listener)
329                {
330                }
331
332
333                @Override
334
335                public Set<BindListener> getBindListeners()
336                {
337                        return null;
338                }
339        }
340
341        /**
342         * Collects the Html generated by the rendering a page.
343         * <p>
344         * Important note: Must be called on a thread bound to an application's {@link ThreadContext}!
345         *
346         * @param pageProvider
347         *            the provider of the page class/instance and its parameters
348         * @return the html rendered by a page
349         * 
350         * @see ThreadContext
351         */
352        public static CharSequence renderPage(final PageProvider pageProvider)
353        {
354                Application application = Application.get();
355
356                RequestCycle originalRequestCycle = RequestCycle.get();
357
358                BufferedWebResponse tempResponse = new BufferedWebResponse(null);
359
360                RequestCycle tempRequestCycle = application
361                        .createRequestCycle(originalRequestCycle.getRequest(), tempResponse);
362
363                try
364                {
365                        ThreadContext.setRequestCycle(tempRequestCycle);
366                        pageProvider.getPageInstance().renderPage();
367                }
368                finally
369                {
370                        ThreadContext.setRequestCycle(originalRequestCycle);
371                }
372
373                return tempResponse.getText();
374        }
375
376        /**
377         * Collects the Html generated by rendering a component.
378         * <p>
379         * Important notes:
380         * <ul>
381         * <li>this method is meant to render fresh component instances that are disposed after the html
382         * has been generate. To avoid unwanted side effects do not use it with components that are from
383         * an existing hierarchy.</li>
384         * <li>does <strong>not</strong> support rendering
385         * {@link org.apache.wicket.markup.html.panel.Fragment} instances</li>
386         * <li>must be called on a thread bound to an application's {@link ThreadContext}!</li>
387         * </ul>
388         *
389         * @param component
390         *            the component to render.
391         * @return the html rendered by the component
392         * 
393         * @see ThreadContext
394         */
395        public static CharSequence renderComponent(final Component component)
396        {
397                RequestCycle requestCycle = RequestCycle.get();
398
399                final Response originalResponse = requestCycle.getResponse();
400                BufferedWebResponse tempResponse = new BufferedWebResponse(null);
401
402                MarkupContainer oldParent = component.getParent();
403
404                if (oldParent != null && LOGGER.isWarnEnabled())
405                {
406                        LOGGER.warn("Component '{}' with a parent '{}' is passed for standalone rendering. "
407                                + "It is recommended to render only orphan components because they are not cleaned up/detached"
408                                + " after the rendering.", component, oldParent);
409                }
410
411                try
412                {
413                        requestCycle.setResponse(tempResponse);
414
415                        // add the component to a dummy page just for the rendering
416                        RenderPage page = new RenderPage(component);
417                        page.internalInitialize();
418
419                        component.beforeRender();
420                        component.renderPart();
421                }
422                finally
423                {
424                        if (oldParent != null)
425                        {
426                                oldParent.add(component); // re-add the child to its old parent
427                        }
428
429                        requestCycle.setResponse(originalResponse);
430                }
431
432                return tempResponse.getText();
433        }
434
435        /**
436         * A page used as a parent for the component based rendering.
437         */
438        private static class RenderPage extends WebPage
439                implements
440                        IMarkupResourceStreamProvider,
441                        IMarkupCacheKeyProvider
442        {
443                /**
444                 * Markup to use when the component to render is not already added to a MarkupContainer
445                 */
446                private static final String DEFAULT_MARKUP = "<wicket:container wicket:id='%s'></wicket:container>";
447
448                /**
449                 * The markup of the component to render
450                 */
451                private final String markup;
452
453                private RenderPage(Component component)
454                {
455                        // WICKET-5422 do not store the page in IPageStore 
456                        setStatelessHint(true);
457
458                        String componentMarkup;
459                        try
460                        {
461                                componentMarkup = component.getMarkup().toString(true);
462                        }
463                        catch (MarkupNotFoundException mnfx)
464                        {
465                                componentMarkup = String.format(DEFAULT_MARKUP, component.getId());
466                        }
467                        this.markup = componentMarkup;
468                        add(component); // this changes the component's parent
469                }
470
471                @Override
472                public IResourceStream getMarkupResourceStream(MarkupContainer container,
473                        Class<?> containerClass)
474                {
475                        return new StringResourceStream(markup);
476                }
477
478                @Override
479                public String getCacheKey(MarkupContainer container, Class<?> containerClass)
480                {
481                        // no caching for this page
482                        return null;
483                }
484
485                @Override
486                public boolean isBookmarkable()
487                {
488                        // pretend the page is bookmarkable to make it stateless. WICKET-5422
489                        return true;
490                }
491        }
492
493}