1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19 package org.apache.myfaces.application.jsp;
20
21 import java.io.IOException;
22 import java.io.StringWriter;
23 import java.io.Writer;
24 import java.util.Iterator;
25 import java.util.Locale;
26 import java.util.logging.Level;
27 import java.util.logging.Logger;
28
29 import javax.faces.FacesException;
30 import javax.faces.FactoryFinder;
31 import javax.faces.application.Application;
32 import javax.faces.application.StateManager;
33 import javax.faces.application.ViewHandler;
34 import javax.faces.component.UIViewRoot;
35 import javax.faces.context.ExternalContext;
36 import javax.faces.context.FacesContext;
37 import javax.faces.context.ResponseWriter;
38 import javax.faces.render.RenderKit;
39 import javax.faces.render.RenderKitFactory;
40 import javax.faces.render.ResponseStateManager;
41 import javax.servlet.ServletRequest;
42 import javax.servlet.ServletResponse;
43 import javax.servlet.http.HttpServletRequest;
44 import javax.servlet.http.HttpServletResponse;
45 import javax.servlet.http.HttpSession;
46 import javax.servlet.jsp.jstl.core.Config;
47
48 import org.apache.myfaces.shared.application.DefaultViewHandlerSupport;
49 import org.apache.myfaces.shared.application.InvalidViewIdException;
50 import org.apache.myfaces.shared.application.ViewHandlerSupport;
51 import org.apache.myfaces.shared.config.MyfacesConfig;
52 import org.apache.myfaces.shared.renderkit.html.util.JavascriptUtils;
53
54 /**
55 * Implementation of the ViewHandler interface that knows how to use JSP pages
56 * as the view templating mechanism.
57 * <p>
58 * This implementation works tightly together with the various JSP TagHandler classes
59 * to implement the behaviour mandated by the ViewHandler specification.
60 * <p>
61 * Rendering of a view is done in two parts: first a jsp-generated servlet is invoked
62 * to create or refresh a jsf component tree, then the component tree is walked to generate
63 * the output to send to the user.
64 * <p>
65 * The invoked servlet is the one generated from the jsp file which corresponds to the
66 * viewId of the view being rendered. As is normal for jsp, this servlet alternates between
67 * writing literal text to the response output stream and invoking "tag handler" classes
68 * representing the jsp tags that were present in the page. This servlet is not aware of
69 * JSF at all.
70 * <p>
71 * On the first visit to a view, when each JSF taghandler is invoked, the corresponding
72 * JSF component will not yet exist so it is created and added to the current view tree.
73 * Each JSF taghandler also marks itself as having "buffered body content", which means that
74 * after the start-tag is executed a temporary output stream is installed for the response.
75 * Any output generated by the jsp-servlet therefore gets written into a memory buffer
76 * rather than sent via the network socket to the sender of the request. When the end
77 * of the JSF tag is encountered, the JSF tag checks whether any such body text did exist,
78 * and if so it creates a transient f:verbatim component and inserts it into the component
79 * tree. The final result is that after this "first pass" a component tree exists which has
80 * all the JSF components in it, plus a bunch of auto-generated f:verbatim components that
81 * hold all plain text, or output generated by non-jsf jsp tags. Absolutely NO output has
82 * yet been sent to the real response stream.
83 * <p>
84 * On later visits to the same view, the component tree already exists (has been restored).
85 * However the "verbatim" components holding static text are not present as they were
86 * marked "transient" (not keeping them reduces the amount of memory required to "save state").
87 * Note that these components are not needed for any phase prior to RENDER because they
88 * are not "input" components. When the jsp-generated servlet is executed, JSF taghandlers
89 * that are invoked will simply verify that a corresponding component already exists in the
90 * view-tree rather than creating a new one. However the "body buffering" occurs again, so
91 * that the appropriate transient verbatim components are once again created and inserted into
92 * the tree.
93 * <p>
94 * Regardless of whether the view is new or restored, the rendered output can now be generated
95 * simply by walking the component tree and invoking the encodeBegin/encodeChildren/encodeEnd
96 * methods on each component. The static components simply output their contained text.
97 * <p>
98 * Notes for JSF1.1 users: the new two-phase approach that uses "output buffering" to capture
99 * non-JSF output is rather like wrapping all non-jsf components in an invisible f:verbatim tag.
100 * Although that doesn't sound like a big change, it allows processing to occur in two passes
101 * rather than one. And that means that before any component is rendered the entire component
102 * tree already exists. This solves a number of JSF1.1 problems, including output-ordering
103 * problems between text and jsf components, and errors when using the "for" attribute of a
104 * label to reference a component later in the page. It does introduce a performance penalty;
105 * non-JSF-generated output now gets buffered rather than being streamed directly to the
106 * user.
107 * <p>
108 * @author Thomas Spiegl (latest modification by $Author: struberg $)
109 * @author Bruno Aranda
110 * @version $Revision: 1188643 $ $Date: 2011-10-25 08:13:09 -0500 (Tue, 25 Oct 2011) $
111 */
112 public class JspViewHandlerImpl extends ViewHandler
113 {
114 //private static final Log log = LogFactory.getLog(JspViewHandlerImpl.class);
115 private static final Logger log = Logger.getLogger(JspViewHandlerImpl.class.getName());
116 public static final String FORM_STATE_MARKER = "<!--@@JSF_FORM_STATE_MARKER@@-->";
117 public static final int FORM_STATE_MARKER_LEN = FORM_STATE_MARKER.length();
118
119 private static final String AFTER_VIEW_TAG_CONTENT_PARAM = JspViewHandlerImpl.class + ".AFTER_VIEW_TAG_CONTENT";
120
121 private ViewHandlerSupport _viewHandlerSupport;
122
123 public JspViewHandlerImpl()
124 {
125 if (log.isLoggable(Level.FINEST))
126 {
127 log.finest("New ViewHandler instance created");
128 }
129 }
130
131 /**
132 * @param viewHandlerSupport
133 * the viewHandlerSupport to set
134 */
135 public void setViewHandlerSupport(ViewHandlerSupport viewHandlerSupport)
136 {
137 _viewHandlerSupport = viewHandlerSupport;
138 }
139
140 /**
141 * @return the viewHandlerSupport
142 */
143 protected ViewHandlerSupport getViewHandlerSupport()
144 {
145 if (_viewHandlerSupport == null)
146 {
147 _viewHandlerSupport = new DefaultViewHandlerSupport();
148 }
149 return _viewHandlerSupport;
150 }
151
152 /**
153 * Get the locales specified as acceptable by the original request, compare them to the
154 * locales supported by this Application and return the best match.
155 */
156 @Override
157 public Locale calculateLocale(FacesContext facesContext)
158 {
159 Application application = facesContext.getApplication();
160 for (Iterator<Locale> requestLocales = facesContext.getExternalContext().getRequestLocales(); requestLocales
161 .hasNext();)
162 {
163 Locale requestLocale = requestLocales.next();
164 for (Iterator<Locale> supportedLocales = application.getSupportedLocales(); supportedLocales.hasNext();)
165 {
166 Locale supportedLocale = supportedLocales.next();
167 // higher priority to a language match over an exact match
168 // that occures further down (see Jstl Reference 1.0 8.3.1)
169 if (requestLocale.getLanguage().equals(supportedLocale.getLanguage())
170 && (supportedLocale.getCountry() == null || supportedLocale.getCountry().length() == 0))
171 {
172 return supportedLocale;
173 }
174 else if (supportedLocale.equals(requestLocale))
175 {
176 return supportedLocale;
177 }
178 }
179 }
180
181 Locale defaultLocale = application.getDefaultLocale();
182 return defaultLocale != null ? defaultLocale : Locale.getDefault();
183 }
184
185 @Override
186 public String calculateRenderKitId(FacesContext facesContext)
187 {
188 Object renderKitId = facesContext.getExternalContext().getRequestMap().get(
189 ResponseStateManager.RENDER_KIT_ID_PARAM);
190 if (renderKitId == null)
191 {
192 renderKitId = facesContext.getApplication().getDefaultRenderKitId();
193 }
194 if (renderKitId == null)
195 {
196 renderKitId = RenderKitFactory.HTML_BASIC_RENDER_KIT;
197 }
198 return renderKitId.toString();
199 }
200
201 /**
202 * Create a UIViewRoot object and return it; the returned object has no children.
203 * <p>
204 * As required by the spec, the returned object inherits locale and renderkit settings from
205 * the viewRoot currently configured for the facesContext (if any). This means that on navigation
206 * from one view to another these settings are "inherited".
207 */
208 @Override
209 public UIViewRoot createView(FacesContext facesContext, String viewId)
210 {
211 String calculatedViewId = viewId;
212 try
213 {
214 calculatedViewId = getViewHandlerSupport().calculateViewId(facesContext, viewId);
215 }
216 catch (InvalidViewIdException e)
217 {
218 sendSourceNotFound(facesContext, e.getMessage());
219 }
220
221 Application application = facesContext.getApplication();
222 ViewHandler applicationViewHandler = application.getViewHandler();
223
224 Locale currentLocale = null;
225 String currentRenderKitId = null;
226 UIViewRoot uiViewRoot = facesContext.getViewRoot();
227 if (uiViewRoot != null)
228 {
229 // Remember current locale and renderKitId
230 currentLocale = uiViewRoot.getLocale();
231 currentRenderKitId = uiViewRoot.getRenderKitId();
232 }
233
234 uiViewRoot = (UIViewRoot) application.createComponent(UIViewRoot.COMPONENT_TYPE);
235
236 uiViewRoot.setViewId(calculatedViewId);
237
238 if (currentLocale != null)
239 {
240 // set old locale
241 uiViewRoot.setLocale(currentLocale);
242 }
243 else
244 {
245 // calculate locale
246 uiViewRoot.setLocale(applicationViewHandler.calculateLocale(facesContext));
247 }
248
249 if (currentRenderKitId != null)
250 {
251 // set old renderKit
252 uiViewRoot.setRenderKitId(currentRenderKitId);
253 }
254 else
255 {
256 // calculate renderKit
257 uiViewRoot.setRenderKitId(applicationViewHandler.calculateRenderKitId(facesContext));
258 }
259
260 if (log.isLoggable(Level.FINEST))
261 {
262 log.finest("Created view " + viewId);
263 }
264 return uiViewRoot;
265 }
266
267 private void sendSourceNotFound(FacesContext context, String message)
268 {
269 HttpServletResponse response = (HttpServletResponse) context.getExternalContext().getResponse();
270 try
271 {
272 context.responseComplete();
273 response.sendError(HttpServletResponse.SC_NOT_FOUND, message);
274 }
275 catch (IOException ioe)
276 {
277 throw new FacesException(ioe);
278 }
279 }
280
281 /**
282 * Return a string containing a webapp-relative URL that the user can invoke
283 * to render the specified view.
284 * <p>
285 * URLs and ViewIds are not quite the same; for example a url of "/foo.jsf"
286 * or "/faces/foo.jsp" may be needed to access the view "/foo.jsp".
287 * <p>
288 * This method simply delegates to ViewHandlerSupport.calculateActionURL.
289 */
290 @Override
291 public String getActionURL(FacesContext facesContext, String viewId)
292 {
293 return getViewHandlerSupport().calculateActionURL(facesContext, viewId);
294 }
295
296 @Override
297 public String getResourceURL(FacesContext facesContext, String path)
298 {
299 if (path.length() > 0 && path.charAt(0) == '/')
300 {
301 return facesContext.getExternalContext().getRequestContextPath() + path;
302 }
303
304 return path;
305
306 }
307
308 /**
309 * Generate output to the user by combining the data in the jsp-page specified by viewToRender
310 * with the existing JSF component tree (if any).
311 * <p>
312 * As described in the class documentation, this first runs the jsp-generated servlet to
313 * create or enhance the JSF component tree - including verbatim nodes for any non-jsf
314 * data in that page.
315 * <p>
316 * The component tree is then walked to generate the appropriate output for each component.
317 */
318 @Override
319 public void renderView(FacesContext facesContext, UIViewRoot viewToRender) throws IOException, FacesException
320 {
321 if (viewToRender == null)
322 {
323 log.severe("viewToRender must not be null");
324 throw new NullPointerException("viewToRender must not be null");
325 }
326
327 // do not render the view if the rendered attribute for the view is false
328 if (!viewToRender.isRendered())
329 {
330 if (log.isLoggable(Level.FINEST))
331 {
332 log.finest("View is not rendered");
333 }
334 return;
335 }
336
337 ExternalContext externalContext = facesContext.getExternalContext();
338
339 String viewId = facesContext.getViewRoot().getViewId();
340
341 if (log.isLoggable(Level.FINEST))
342 {
343 log.finest("Rendering JSP view: " + viewId);
344 }
345
346 ServletResponse response = (ServletResponse) externalContext.getResponse();
347 ServletRequest request = (ServletRequest) externalContext.getRequest();
348
349 Locale locale = viewToRender.getLocale();
350 response.setLocale(locale);
351 Config.set(request, Config.FMT_LOCALE, facesContext.getViewRoot().getLocale());
352
353 if(!buildView(response, externalContext, viewId))
354 {
355 //building the view was unsuccessful - an exception occurred during rendering
356 //we need to jump out
357 return;
358 }
359
360 // handle character encoding as of section 2.5.2.2 of JSF 1.1
361 if (externalContext.getRequest() instanceof HttpServletRequest)
362 {
363 HttpServletRequest httpServletRequest = (HttpServletRequest) externalContext.getRequest();
364 HttpSession session = httpServletRequest.getSession(false);
365
366 if (session != null)
367 {
368 session.setAttribute(ViewHandler.CHARACTER_ENCODING_KEY, response.getCharacterEncoding());
369 }
370 }
371
372 // render the view in this method (since JSF 1.2)
373 RenderKitFactory renderFactory = (RenderKitFactory) FactoryFinder.getFactory(FactoryFinder.RENDER_KIT_FACTORY);
374 RenderKit renderKit = renderFactory.getRenderKit(facesContext, viewToRender.getRenderKitId());
375
376 ResponseWriter responseWriter = facesContext.getResponseWriter();
377 if (responseWriter == null)
378 {
379 responseWriter = renderKit.createResponseWriter(response.getWriter(), null,
380 ((HttpServletRequest) externalContext.getRequest()).getCharacterEncoding());
381 facesContext.setResponseWriter(responseWriter);
382 }
383
384 ResponseWriter oldResponseWriter = responseWriter;
385 StateMarkerAwareWriter stateAwareWriter = null;
386
387 StateManager stateManager = facesContext.getApplication().getStateManager();
388 if (stateManager.isSavingStateInClient(facesContext))
389 {
390 stateAwareWriter = new StateMarkerAwareWriter();
391
392 // Create a new response-writer using as an underlying writer the stateAwareWriter
393 // Effectively, all output will be buffered in the stateAwareWriter so that later
394 // this writer can replace the state-markers with the actual state.
395 responseWriter = hookInStateAwareWriter(
396 oldResponseWriter, stateAwareWriter, renderKit, externalContext);
397 facesContext.setResponseWriter(responseWriter);
398 }
399
400 actuallyRenderView(facesContext, viewToRender);
401
402 facesContext.setResponseWriter(oldResponseWriter);
403
404 //We're done with the document - now we can write all content
405 //to the response, properly replacing the state-markers on the way out
406 //by using the stateAwareWriter
407 if (stateManager.isSavingStateInClient(facesContext))
408 {
409 stateAwareWriter.flushToWriter(response.getWriter());
410 }
411 else
412 {
413 stateManager.saveView(facesContext);
414 }
415
416 // Final step - we output any content in the wrappedResponse response from above to the response,
417 // removing the wrappedResponse response from the request, we don't need it anymore
418 ServletViewResponseWrapper afterViewTagResponse
419 = (ServletViewResponseWrapper) externalContext.getRequestMap().get(AFTER_VIEW_TAG_CONTENT_PARAM);
420 externalContext.getRequestMap().remove(AFTER_VIEW_TAG_CONTENT_PARAM);
421
422 if (afterViewTagResponse != null)
423 {
424 afterViewTagResponse.flushToWriter(response.getWriter(),
425 facesContext.getExternalContext().getResponseCharacterEncoding());
426 }
427
428 response.flushBuffer();
429 }
430
431 /**
432 * Render the view now - properly setting and resetting the response writer
433 */
434 private void actuallyRenderView(FacesContext facesContext, UIViewRoot viewToRender) throws IOException
435 {
436 // Set the new ResponseWriter into the FacesContext, saving the old one aside.
437 ResponseWriter responseWriter = facesContext.getResponseWriter();
438
439 //Now we actually render the document
440 // Call startDocument() on the ResponseWriter.
441 responseWriter.startDocument();
442
443 // Call encodeAll() on the UIViewRoot
444 viewToRender.encodeAll(facesContext);
445
446 // Call endDocument() on the ResponseWriter
447 responseWriter.endDocument();
448
449 responseWriter.flush();
450 }
451
452 /**Create a new response-writer using as an underlying writer the stateAwareWriter
453 * Effectively, all output will be buffered in the stateAwareWriter so that later
454 * this writer can replace the state-markers with the actual state.
455 *
456 * If the FacesContext has a non-null ResponseWriter create a new writer using its
457 * cloneWithWriter() method, passing the response's Writer as the argument.
458 * Otherwise, use the current RenderKit to create a new ResponseWriter.
459 *
460 * @param oldResponseWriter
461 * @param stateAwareWriter
462 * @param renderKit
463 * @param externalContext
464 * @return
465 */
466 private ResponseWriter hookInStateAwareWriter(ResponseWriter oldResponseWriter,
467 StateMarkerAwareWriter stateAwareWriter,
468 RenderKit renderKit,
469 ExternalContext externalContext)
470 {
471 return oldResponseWriter.cloneWithWriter(stateAwareWriter);
472 }
473
474 /**Build the view-tree before rendering.
475 * This is done by dispatching to the underlying JSP-page, effectively processing it, creating
476 * components out of any text in between JSF components (not rendering the text to the output of course, this
477 * will happen later while rendering), attaching these components
478 * to the component tree, and buffering any content after the view-root.
479 *
480 * @param response The current response - it will be replaced while the view-building happens
481 * (we want the text in the component tree, not on the actual servlet output stream)
482 * @param externalContext The external context where the response will be replaced while building
483 * @param viewId The view-id to dispatch to
484 * @return true if successfull, false if an error occurred during rendering
485 * @throws IOException
486 */
487 private boolean buildView(ServletResponse response, ExternalContext externalContext, String viewId)
488 throws IOException
489 {
490 ServletViewResponseWrapper wrappedResponse = new ServletViewResponseWrapper((HttpServletResponse) response);
491
492 externalContext.setResponse(wrappedResponse);
493 try
494 {
495 externalContext.dispatch(viewId);
496 }
497 finally
498 {
499 externalContext.setResponse(response);
500 }
501
502 boolean errorResponse = wrappedResponse.getStatus() < 200 || wrappedResponse.getStatus() > 299;
503 if (errorResponse)
504 {
505 wrappedResponse.flushToWrappedResponse();
506 return false;
507 }
508
509 // store the wrapped response in the request, so it is thread-safe
510 externalContext.getRequestMap().put(AFTER_VIEW_TAG_CONTENT_PARAM, wrappedResponse);
511
512 return true;
513 }
514
515 /**
516 * Just invoke StateManager.restoreView.
517 */
518 @Override
519 public UIViewRoot restoreView(FacesContext facesContext, String viewId)
520 {
521 Application application = facesContext.getApplication();
522 ViewHandler applicationViewHandler = application.getViewHandler();
523 String renderKitId = applicationViewHandler.calculateRenderKitId(facesContext);
524 String calculatedViewId = getViewHandlerSupport().calculateViewId(facesContext, viewId);
525 UIViewRoot viewRoot = application.getStateManager().restoreView(facesContext, calculatedViewId, renderKitId);
526 return viewRoot;
527 }
528
529 /**
530 * Writes a state marker that is replaced later by one or more hidden form
531 * inputs.
532 * <p>
533 * The problem with html is that the only place to encode client-side state is
534 * in a hidden html input field. However when a form is submitted, only the fields
535 * within a particular form are sent; fields in other forms are not sent. Therefore
536 * the view tree state must be written into every form in the page. This method
537 * is therefore invoked at the end of every form.
538 * <p>
539 * Theoretically the state of a component tree will not change after rendering
540 * starts. Therefore it is possible to create a serialized representation of that
541 * state at the start of the rendering phase (or when first needed) and output it
542 * whenever needed as described above. However this is not currently implemented;
543 * instead the entire page being generated is buffered, and a "marker" string is
544 * output instead of the tree state. After the rendering pass is complete the component
545 * final tree state is computed and the buffer is then post-processed to replace the
546 * "marker" strings with the real data.
547 * <p>
548 * This method also supports "javascript viewstate". TODO: document this.
549 *
550 * @param facesContext
551 * @throws IOException
552 */
553 @Override
554 public void writeState(FacesContext facesContext) throws IOException
555 {
556 StateManager stateManager = facesContext.getApplication().getStateManager();
557 if (stateManager.isSavingStateInClient(facesContext))
558 {
559 // Only write state marker if javascript view state is disabled
560 ExternalContext extContext = facesContext.getExternalContext();
561 if (!(JavascriptUtils.isJavascriptAllowed(extContext) &&
562 MyfacesConfig.getCurrentInstance(extContext).isViewStateJavascript()))
563 {
564 facesContext.getResponseWriter().write(FORM_STATE_MARKER);
565 }
566 }
567 else
568 {
569 stateManager.writeState(facesContext, new Object[2]);
570 }
571 }
572
573 /**
574 * Writes the response and replaces the state marker tags with the state information for the current context
575 */
576 private static class StateMarkerAwareWriter extends Writer
577 {
578 private StringBuilder buf;
579
580 public StateMarkerAwareWriter()
581 {
582 this.buf = new StringBuilder();
583 }
584
585 @Override
586 public void close() throws IOException
587 {
588 }
589
590 @Override
591 public void flush() throws IOException
592 {
593 }
594
595 @Override
596 public void write(char[] cbuf, int off, int len) throws IOException
597 {
598 if ((off < 0) || (off > cbuf.length) || (len < 0) ||
599 ((off + len) > cbuf.length) || ((off + len) < 0))
600 {
601 throw new IndexOutOfBoundsException();
602 }
603 else if (len == 0)
604 {
605 return;
606 }
607 buf.append(cbuf, off, len);
608 }
609
610 public StringBuilder getStringBuilder()
611 {
612 return buf;
613 }
614
615 public void flushToWriter(Writer writer) throws IOException
616 {
617 FacesContext facesContext = FacesContext.getCurrentInstance();
618 StateManager stateManager = facesContext.getApplication().getStateManager();
619
620 StringWriter stateWriter = new StringWriter();
621 ResponseWriter realWriter = facesContext.getResponseWriter();
622 facesContext.setResponseWriter(realWriter.cloneWithWriter(stateWriter));
623
624 Object serializedView = stateManager.saveView(facesContext);
625
626 stateManager.writeState(facesContext, serializedView);
627 facesContext.setResponseWriter(realWriter);
628
629 StringBuilder contentBuffer = getStringBuilder();
630 String state = stateWriter.getBuffer().toString();
631
632 ExternalContext extContext = facesContext.getExternalContext();
633 if (JavascriptUtils.isJavascriptAllowed(extContext) &&
634 MyfacesConfig.getCurrentInstance(extContext).isViewStateJavascript())
635 {
636 // If javascript viewstate is enabled no state markers were written
637 write(contentBuffer, 0, contentBuffer.length(), writer);
638 writer.write(state);
639 }
640 else
641 {
642 // If javascript viewstate is disabled state markers must be replaced
643 int lastFormMarkerPos = 0;
644 int formMarkerPos = 0;
645 // Find all state markers and write out actual state instead
646 while ((formMarkerPos = contentBuffer.indexOf(FORM_STATE_MARKER, formMarkerPos)) > -1)
647 {
648 // Write content before state marker
649 write(contentBuffer, lastFormMarkerPos, formMarkerPos, writer);
650 // Write state and move position in buffer after marker
651 writer.write(state);
652 formMarkerPos += FORM_STATE_MARKER_LEN;
653 lastFormMarkerPos = formMarkerPos;
654 }
655 // Write content after last state marker
656 if (lastFormMarkerPos < contentBuffer.length())
657 {
658 write(contentBuffer, lastFormMarkerPos, contentBuffer.length(), writer);
659 }
660 }
661
662 }
663
664 /**
665 * Writes the content of the specified StringBuffer from index
666 * <code>beginIndex</code> to index <code>endIndex - 1</code>.
667 *
668 * @param contentBuffer the <code>StringBuffer</code> to copy content from
669 * @param beginIndex the beginning index, inclusive.
670 * @param endIndex the ending index, exclusive
671 * @param writer the <code>Writer</code> to write to
672 * @throws IOException if an error occurs writing to specified <code>Writer</code>
673 */
674 private void write(StringBuilder contentBuffer, int beginIndex, int endIndex, Writer writer) throws IOException
675 {
676 int index = beginIndex;
677 int bufferSize = 2048;
678 char[] bufToWrite = new char[bufferSize];
679
680 while (index < endIndex)
681 {
682 int maxSize = Math.min(bufferSize, endIndex - index);
683
684 contentBuffer.getChars(index, index + maxSize, bufToWrite, 0);
685 writer.write(bufToWrite, 0, maxSize);
686
687 index += bufferSize;
688 }
689 }
690 }
691 }