View Javadoc

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