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