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}