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 }