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.tomahawk.application.jsp;
20  
21  import org.apache.commons.logging.Log;
22  import org.apache.commons.logging.LogFactory;
23  import org.apache.tiles.access.TilesAccess;
24  import org.apache.tiles.context.BasicAttributeContext;
25  import org.apache.tiles.context.TilesRequestContext;
26  import org.apache.tiles.definition.NoSuchDefinitionException;
27  import org.apache.tiles.factory.TilesContainerFactory;
28  import org.apache.tiles.impl.BasicTilesContainer;
29  import org.apache.tiles.preparer.NoSuchPreparerException;
30  import org.apache.tiles.preparer.ViewPreparer;
31  import org.apache.tiles.servlet.context.ServletTilesApplicationContext;
32  import org.apache.tiles.servlet.context.ServletTilesRequestContext;
33  import org.apache.tiles.AttributeContext;
34  import org.apache.tiles.Definition;
35  import org.apache.tiles.TilesContainer;
36  import org.apache.tiles.TilesException;
37  import org.apache.myfaces.shared_tomahawk.config.MyfacesConfig;
38  import org.apache.myfaces.shared_tomahawk.renderkit.html.util.JavascriptUtils;
39  import org.apache.myfaces.shared_tomahawk.webapp.webxml.ServletMapping;
40  import org.apache.myfaces.shared_tomahawk.webapp.webxml.WebXml;
41  
42  import javax.faces.application.StateManager;
43  import javax.faces.application.ViewHandler;
44  import javax.faces.context.ExternalContext;
45  import javax.faces.context.FacesContext;
46  import javax.faces.context.ResponseWriter;
47  import javax.faces.FacesException;
48  import javax.faces.FactoryFinder;
49  import javax.faces.component.UIViewRoot;
50  import javax.faces.render.RenderKit;
51  import javax.faces.render.RenderKitFactory;
52  import javax.servlet.http.HttpServletRequest;
53  import javax.servlet.http.HttpServletResponse;
54  import javax.servlet.http.HttpSession;
55  import javax.servlet.ServletResponse;
56  import java.io.IOException;
57  import java.io.StringWriter;
58  import java.io.Writer;
59  import java.util.List;
60  import java.util.Locale;
61  import java.util.StringTokenizer;
62  
63  /**
64   * This ViewHandler should be used with myfaces core 1.2.4 or superior
65   * and it does not work with jsf ri 1.2 
66   * 
67   * @since 1.1.7
68   * @author Martin Marinschek
69   * @version $Revision: 691871 $ $Date: 2008-09-03 23:32:08 -0500 (Wed, 03 Sep 2008) $
70   */
71  public class JspTilesTwoViewHandlerImpl
72          extends ViewHandler {
73      private ViewHandler _viewHandler;
74  
75      public static final String FORM_STATE_MARKER = "<!--@@JSF_FORM_STATE_MARKER@@-->";
76      public static final int FORM_STATE_MARKER_LEN = FORM_STATE_MARKER.length();
77      
78      private static final Log log = LogFactory.getLog(JspTilesTwoViewHandlerImpl.class);
79      private static final String TILES_DEF_EXT = ".tiles";
80      private String tilesExtension = TILES_DEF_EXT;
81      private static final String AFTER_VIEW_TAG_CONTENT_PARAM = JspTilesTwoViewHandlerImpl.class + ".AFTER_VIEW_TAG_CONTENT";
82  
83      public JspTilesTwoViewHandlerImpl(ViewHandler viewHandler)
84      {
85          _viewHandler = viewHandler;
86      }
87  
88      private void initContainer(ExternalContext context) {
89  
90          if(TilesAccess.getContainer(context.getContext())==null) {
91              try
92              {
93                  TilesContainerFactory factory = TilesContainerFactory.getFactory(context.getContext());
94                  TilesContainer container = factory.createTilesContainer(context.getContext());
95                  TilesAccess.setContainer(context.getContext(),container);
96              }
97              catch (Exception e)
98              {
99                  throw new FacesException("Error reading tiles definitions : " + e.getMessage(), e);
100             }
101         }
102     }
103 
104     public void renderView(FacesContext facesContext, UIViewRoot viewToRender) throws IOException, FacesException
105     {
106         if (viewToRender == null)
107         {
108             log.fatal("viewToRender must not be null");
109             throw new NullPointerException("viewToRender must not be null");
110         }
111 
112         ExternalContext externalContext = facesContext.getExternalContext();
113 
114         String viewId = deriveViewId(externalContext, viewToRender.getViewId());
115 
116         if(viewId==null) {
117             //deriving view-id made clear we are not responsible for this view-id - call the delegate
118             _viewHandler.renderView(facesContext, viewToRender);
119             return;
120         }
121 
122 
123         initContainer(externalContext);
124 
125         String tilesId = deriveTileFromViewId(viewId);
126 
127         TilesContainer container = TilesAccess.getContainer(externalContext.getContext());
128 
129         Object[] requestObjects = {externalContext.getRequest(), externalContext.getResponse()};
130 
131         if(container.isValidDefinition(tilesId, requestObjects)) {
132 
133             //propagate our new view-id to wherever it makes sense
134             setViewId(viewToRender, viewId, facesContext);
135             renderTilesView(facesContext, requestObjects, container, viewToRender, viewId, tilesId);
136         } else {
137             //we're not using tiles as no valid definition has been found
138             //just call the delegate view-handler to let it do its thing
139             _viewHandler.renderView(facesContext, viewToRender);
140         }
141     }
142 
143     private void renderTilesView(FacesContext facesContext,
144             Object[] requestObjects, TilesContainer container,
145             UIViewRoot viewToRender, String viewId, String tilesId)
146             throws IOException
147     {
148         ExternalContext externalContext = facesContext.getExternalContext();
149         handleCharacterEncoding(viewId, externalContext, viewToRender);
150         container.startContext(requestObjects);
151         try
152         {
153             //container.render(tilesId,requestObjects);
154             buildTilesViewLikeContainer(externalContext, container, tilesId,
155                     requestObjects);
156         }
157         catch (TilesException e)
158         {
159             throw new FacesException(e);
160         }
161         finally
162         {
163             container.endContext(requestObjects);
164         }
165 
166         handleCharacterEncodingPostDispatch(externalContext);
167 
168         // render the view in this method (since JSF 1.2)
169         RenderKitFactory renderFactory = (RenderKitFactory) FactoryFinder
170                 .getFactory(FactoryFinder.RENDER_KIT_FACTORY);
171         RenderKit renderKit = renderFactory.getRenderKit(facesContext,
172                 viewToRender.getRenderKitId());
173         ServletResponse response = (ServletResponse) requestObjects[1];
174         ResponseWriter responseWriter = facesContext.getResponseWriter();
175         if (responseWriter == null)
176         {
177             responseWriter = renderKit.createResponseWriter(response
178                     .getWriter(), null, ((HttpServletRequest) externalContext
179                     .getRequest()).getCharacterEncoding());
180             facesContext.setResponseWriter(responseWriter);
181         }
182 
183         ResponseWriter oldResponseWriter = responseWriter;
184         StateMarkerAwareWriter stateAwareWriter = null;
185 
186         StateManager stateManager = facesContext.getApplication()
187                 .getStateManager();
188         if (stateManager.isSavingStateInClient(facesContext))
189         {
190             stateAwareWriter = new StateMarkerAwareWriter();
191 
192             // Create a new response-writer using as an underlying writer the stateAwareWriter
193             // Effectively, all output will be buffered in the stateAwareWriter so that later
194             // this writer can replace the state-markers with the actual state.
195             responseWriter = oldResponseWriter
196                     .cloneWithWriter(stateAwareWriter);
197             facesContext.setResponseWriter(responseWriter);
198         }
199 
200         actuallyRenderView(facesContext, viewToRender);
201 
202         //We're done with the document - now we can write all content
203         //to the response, properly replacing the state-markers on the way out
204         //by using the stateAwareWriter
205         if (stateManager.isSavingStateInClient(facesContext))
206         {
207             stateAwareWriter.flushToWriter(response.getWriter());
208         }
209         else
210         {
211             stateManager.saveView(facesContext);
212         }
213 
214         // Final step - we output any content in the wrappedResponse response from above to the response,
215         // removing the wrappedResponse response from the request, we don't need it anymore
216         ViewResponseWrapper afterViewTagResponse = (ViewResponseWrapper) externalContext
217                 .getRequestMap().get(AFTER_VIEW_TAG_CONTENT_PARAM);
218         externalContext.getRequestMap().remove(AFTER_VIEW_TAG_CONTENT_PARAM);
219         if (afterViewTagResponse != null)
220         {
221             afterViewTagResponse.flushToWriter(response.getWriter(),
222                     facesContext.getExternalContext()
223                             .getResponseCharacterEncoding());
224         }
225         response.flushBuffer();
226     }
227     
228     
229     private String deriveTileFromViewId(String viewId) {
230         String tilesId = viewId;
231         int idx = tilesId.lastIndexOf('.');
232         if (idx > 0)
233         {
234             tilesId = tilesId.substring(0, idx) + tilesExtension;
235         }
236         else
237         {
238             tilesId = tilesId  + tilesExtension;
239         }
240         return tilesId;
241     }
242 
243     private String deriveViewId(ExternalContext externalContext, String viewId) {
244         ServletMapping servletMapping = getServletMapping(externalContext);
245 
246         String defaultSuffix = externalContext.getInitParameter(ViewHandler.DEFAULT_SUFFIX_PARAM_NAME);
247         String suffix = defaultSuffix != null ? defaultSuffix : ViewHandler.DEFAULT_SUFFIX;
248         if (servletMapping.isExtensionMapping())
249         {
250             if (!viewId.endsWith(suffix))
251             {
252                 int dot = viewId.lastIndexOf('.');
253                 if (dot == -1)
254                 {
255                     if (log.isTraceEnabled()) log.trace("Current viewId has no extension, appending default suffix " + suffix);
256                     return viewId + suffix;
257                 }
258                 else
259                 {
260                     if (log.isTraceEnabled()) log.trace("Replacing extension of current viewId by suffix " + suffix);
261                     return viewId.substring(0, dot) + suffix;
262                 }
263             }
264 
265             //extension-mapped page ends with proper suffix - all ok
266             return viewId;
267         }
268         else if (!viewId.endsWith(suffix))
269         {
270             // path-mapping used, but suffix is no default-suffix
271             return null;
272         }
273         else {
274             //path-mapping used, suffix is default-suffix - all ok
275             return viewId;
276         }
277     }
278 
279     private void handleCharacterEncodingPostDispatch(ExternalContext externalContext) {
280         // handle character encoding as of section 2.5.2.2 of JSF 1.1
281         if (externalContext.getRequest() instanceof HttpServletRequest) {
282             HttpServletResponse response = (HttpServletResponse) externalContext.getResponse();
283             HttpServletRequest request = (HttpServletRequest) externalContext.getRequest();
284             HttpSession session = request.getSession(false);
285 
286             if (session != null) {
287                 session.setAttribute(ViewHandler.CHARACTER_ENCODING_KEY, response.getCharacterEncoding());
288             }
289         }
290     }
291 
292     private void handleCharacterEncoding(String viewId, ExternalContext externalContext, UIViewRoot viewToRender) {
293         if (log.isTraceEnabled()) log.trace("Dispatching to " + viewId);
294 
295         // handle character encoding as of section 2.5.2.2 of JSF 1.1
296         if (externalContext.getResponse() instanceof ServletResponse) {
297             ServletResponse response = (ServletResponse) externalContext.getResponse();
298             response.setLocale(viewToRender.getLocale());
299         }
300     }
301     
302     private void buildTilesViewLikeContainer(ExternalContext externalContext,
303             TilesContainer container, String definitionName,
304             Object... requestItems) throws TilesException
305     {
306         if (log.isDebugEnabled())
307         {
308             log.debug("Render request recieved for definition '"
309                     + definitionName + "'");
310         }
311         TilesRequestContext tilesRequest = ((BasicTilesContainer) container)
312                 .getContextFactory().createRequestContext(
313                         container.getApplicationContext(), requestItems);
314         Definition definition = ((BasicTilesContainer) container)
315                 .getDefinitionsFactory().getDefinition(definitionName,
316                         tilesRequest);
317         if (definition == null)
318         {
319             if (log.isWarnEnabled())
320             {
321                 String message = "Unable to find the definition '"
322                         + definitionName + "'";
323                 log.warn(message);
324             }
325             throw new NoSuchDefinitionException(definitionName);
326         }
327         if (!isPermitted(tilesRequest, definition.getRole()))
328         {
329             log.info("Access to definition '" + definitionName
330                     + "' denied. User not in role '" + definition.getRole());
331             return;
332         }
333 
334         AttributeContext originalContext = container
335                 .getAttributeContext(requestItems);
336         BasicAttributeContext subContext = new BasicAttributeContext(
337                 originalContext);
338         subContext.addMissing(definition.getAttributes());
339         BasicAttributeContext.pushContext(subContext, tilesRequest);
340 
341         try
342         {
343             if (definition.getPreparer() != null)
344             {
345                 prepare(container, tilesRequest, definition.getPreparer(), true);
346             }
347             String dispatchPath = definition.getTemplate();
348             if (log.isDebugEnabled())
349             {
350                 log.debug("Dispatching to definition path '"
351                         + definition.getTemplate() + " '");
352             }
353 
354             if (!buildView(container, tilesRequest, externalContext,
355                     dispatchPath))
356             {
357                 //building the view was unsuccessful - an exception occurred during rendering
358                 //we need to jump out
359                 return;
360             }
361 
362             // tiles exception so that it doesn't need to be rethrown.
363         }
364         catch (TilesException e)
365         {
366             throw e;
367         }
368         catch (Exception e)
369         {
370             log.error("Error rendering tile", e);
371             throw new TilesException(e.getMessage(), e);
372         }
373         finally
374         {
375             BasicAttributeContext.popContext(tilesRequest);
376         }
377     }
378     
379     /**
380      * Checks if the current user is in one of the comma-separated roles
381      * specified in the <code>role</code> parameter.
382      *
383      * @param request The request context.
384      * @param role The comma-separated list of roles.
385      * @return <code>true</code> if the current user is in one of those roles.
386      */
387     private boolean isPermitted(TilesRequestContext request, String role) {
388         if (role == null) {
389             return true;
390         }
391         StringTokenizer st = new StringTokenizer(role, ",");
392         while (st.hasMoreTokens()) {
393             if (request.isUserInRole(st.nextToken())) {
394                 return true;
395             }
396         }
397         return false;
398     }
399     
400     /**
401      * Execute a preparer.
402      *
403      * @param context The request context.
404      * @param preparerName The name of the preparer.
405      * @param ignoreMissing If <code>true</code> if the preparer is not found,
406      * it ignores the problem.
407      * @throws TilesException If the preparer is not found (and
408      * <code>ignoreMissing</code> is not set) or if the preparer itself threw an
409      * exception.
410      */
411     private void prepare(TilesContainer container, TilesRequestContext context, String preparerName, boolean ignoreMissing) throws TilesException {
412         if (log.isDebugEnabled()) {
413             log.debug("Prepare request received for '" + preparerName);
414         }
415         ViewPreparer preparer = ((BasicTilesContainer)container).getPreparerFactory().getPreparer(preparerName, context);
416         if (preparer == null && ignoreMissing) {
417             return;
418         }
419         if (preparer == null) {
420             throw new NoSuchPreparerException("Preparer '" + preparerName + " not found");
421         }
422         AttributeContext attributeContext = BasicAttributeContext.getContext(context);
423         preparer.execute(context, attributeContext);
424     }
425     
426     /**Build the view-tree before rendering.
427      * This is done by dispatching to the underlying JSP-page, effectively processing it, creating
428      * components out of any text in between JSF components (not rendering the text to the output of course, this
429      * will happen later while rendering), attaching these components
430      * to the component tree, and buffering any content after the view-root.
431      *
432      * @param container The current TilesContainer
433      * @param tilesRequest The current TilesRequestContext - its response will be replaced while the view-building happens (we want the text in the component tree, not on the actual servlet output stream)
434      * @param externalContext The external context where the response will be replaced while building
435      * @param viewId The view-id to dispatch to
436      * @return true if successfull, false if an error occurred during rendering
437      * @throws IOException
438      */
439     private boolean buildView(TilesContainer container,
440             TilesRequestContext tilesRequest, ExternalContext externalContext,
441             String viewId) throws IOException
442     {
443         HttpServletResponse response = (HttpServletResponse) tilesRequest
444                 .getResponse();
445         ViewResponseWrapper wrappedResponse = new ViewResponseWrapper(response);
446         tilesRequest = new ServletTilesRequestContext(
447                 ((ServletTilesApplicationContext) container
448                         .getApplicationContext()).getServletContext(),
449                 (HttpServletRequest) tilesRequest.getRequest(), wrappedResponse);
450         tilesRequest.dispatch(viewId);
451         tilesRequest = new ServletTilesRequestContext(
452                 ((ServletTilesApplicationContext) container
453                         .getApplicationContext()).getServletContext(),
454                 (HttpServletRequest) tilesRequest.getRequest(), response);
455         boolean errorResponse = wrappedResponse.getStatus() < 200
456                 || wrappedResponse.getStatus() > 299;
457         if (errorResponse)
458         {
459             wrappedResponse.flushToWrappedResponse();
460             return false;
461         }
462         // store the wrapped response in the request, so it is thread-safe
463         externalContext.getRequestMap().put(AFTER_VIEW_TAG_CONTENT_PARAM,
464                 wrappedResponse);
465         return true;
466     }
467     
468     /**
469      * Render the view now - properly setting and resetting the response writer
470      */
471     private void actuallyRenderView(FacesContext facesContext, UIViewRoot viewToRender) throws IOException {
472         // Set the new ResponseWriter into the FacesContext, saving the old one aside.
473         ResponseWriter responseWriter = facesContext.getResponseWriter();
474         //Now we actually render the document
475         // Call startDocument() on the ResponseWriter.
476         responseWriter.startDocument();
477         // Call encodeAll() on the UIViewRoot
478         viewToRender.encodeAll(facesContext);
479         // Call endDocument() on the ResponseWriter
480         responseWriter.endDocument();
481         responseWriter.flush();
482     }
483 
484     private void setViewId(UIViewRoot viewToRender, String viewId, FacesContext facesContext) {
485         viewToRender.setViewId(viewId);
486         if(facesContext.getViewRoot()!=null) {
487             facesContext.getViewRoot().setViewId(viewId);
488         }
489     }
490 
491     private static ServletMapping getServletMapping(ExternalContext externalContext)
492     {
493         String servletPath = externalContext.getRequestServletPath();
494         String requestPathInfo = externalContext.getRequestPathInfo();
495 
496         WebXml webxml = WebXml.getWebXml(externalContext);
497         List<?> mappings = webxml.getFacesServletMappings();
498 
499         boolean isExtensionMapping = requestPathInfo == null;
500 
501         for (int i = 0, size = mappings.size(); i < size; i++)
502         {
503             ServletMapping servletMapping = (ServletMapping) mappings.get(i);
504             if (servletMapping.isExtensionMapping() == isExtensionMapping)
505             {
506                 String urlpattern = servletMapping.getUrlPattern();
507                 if (isExtensionMapping)
508                 {
509                     String extension = urlpattern.substring(1, urlpattern.length());
510                     if (servletPath.endsWith(extension))
511                     {
512                         return servletMapping;
513                     }
514                 }
515                 else
516                 {
517                     urlpattern = urlpattern.substring(0, urlpattern.length() - 2);
518                     // servletPath starts with "/" except in the case where the
519                     // request is matched with the "/*" pattern, in which case
520                     // it is the empty string (see Servlet Sepc 2.3 SRV4.4)
521                     if (servletPath.equals(urlpattern))
522                     {
523                         return servletMapping;
524                     }
525                 }
526             }
527         }
528         log.error("could not find pathMapping for servletPath = " + servletPath +
529                   " requestPathInfo = " + requestPathInfo);
530         throw new IllegalArgumentException("could not find pathMapping for servletPath = " + servletPath +
531                   " requestPathInfo = " + requestPathInfo);
532     }
533 
534 
535     public Locale calculateLocale(FacesContext context)
536     {
537         return _viewHandler.calculateLocale(context);
538     }
539 
540     public String calculateRenderKitId(FacesContext context)
541     {
542         return _viewHandler.calculateRenderKitId(context);
543     }
544 
545     public UIViewRoot createView(FacesContext context, String viewId)
546     {
547         return _viewHandler.createView(context, viewId);
548     }
549 
550     public String getActionURL(FacesContext context, String viewId)
551     {
552         return _viewHandler.getActionURL(context, viewId);
553     }
554 
555     public String getResourceURL(FacesContext context, String path)
556     {
557         return _viewHandler.getResourceURL(context, path);
558     }
559 
560     public UIViewRoot restoreView(FacesContext context, String viewId)
561     {
562         return _viewHandler.restoreView(context, viewId);
563     }
564 
565     public void writeState(FacesContext context) throws IOException
566     {
567         _viewHandler.writeState(context);
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             {
598                 throw new IndexOutOfBoundsException();
599             }
600             else if (len == 0)
601             {
602                 return;
603             }
604             buf.append(cbuf, off, len);
605         }
606 
607         public StringBuilder getStringBuilder()
608         {
609             return buf;
610         }
611 
612         public void flushToWriter(Writer writer) throws IOException
613         {
614             FacesContext facesContext = FacesContext.getCurrentInstance();
615             StateManager stateManager = facesContext.getApplication().getStateManager();
616 
617             StringWriter stateWriter = new StringWriter();
618             ResponseWriter realWriter = facesContext.getResponseWriter();
619             facesContext.setResponseWriter(realWriter.cloneWithWriter(stateWriter));
620 
621             Object serializedView = stateManager.saveView(facesContext);
622 
623             stateManager.writeState(facesContext, serializedView);
624             facesContext.setResponseWriter(realWriter);
625 
626             StringBuilder contentBuffer = getStringBuilder();
627             String state = stateWriter.getBuffer().toString();
628 
629             ExternalContext extContext = facesContext.getExternalContext();
630             if (JavascriptUtils.isJavascriptAllowed(extContext) && MyfacesConfig.getCurrentInstance(extContext).isViewStateJavascript()) {
631              // If javascript viewstate is enabled no state markers were written
632              write(contentBuffer, 0, contentBuffer.length(), writer);
633              writer.write(state);
634             } else {
635              // If javascript viewstate is disabled state markers must be replaced
636              int lastFormMarkerPos = 0;
637              int formMarkerPos = 0;
638              // Find all state markers and write out actual state instead
639              while ((formMarkerPos = contentBuffer.indexOf(FORM_STATE_MARKER, formMarkerPos)) > -1)
640              {
641              // Write content before state marker
642              write(contentBuffer, lastFormMarkerPos, formMarkerPos, writer);
643              // Write state and move position in buffer after marker
644              writer.write(state);
645              formMarkerPos += FORM_STATE_MARKER_LEN;
646              lastFormMarkerPos = formMarkerPos;
647              }
648                 // Write content after last state marker
649                 if (lastFormMarkerPos < contentBuffer.length()) {
650                  write(contentBuffer, lastFormMarkerPos, contentBuffer.length(), writer);
651                 }
652             }
653 
654         }
655 
656         /**
657          * Writes the content of the specified StringBuffer from index
658          * <code>beginIndex</code> to index <code>endIndex - 1</code>.
659          *
660          * @param contentBuffer the <code>StringBuffer</code> to copy content from
661          * @param begin the beginning index, inclusive.
662          * @param end the ending index, exclusive
663          * @param writer the <code>Writer</code> to write to
664          * @throws IOException if an error occurs writing to specified <code>Writer</code>
665          */
666         private void write(StringBuilder contentBuffer, int beginIndex, int endIndex, Writer writer) throws IOException {
667             int index = beginIndex;
668             int bufferSize = 2048;
669             char[] bufToWrite = new char[bufferSize];
670 
671             while (index < endIndex)
672             {
673                 int maxSize = Math.min(bufferSize, endIndex - index);
674 
675                 contentBuffer.getChars(index, index + maxSize, bufToWrite, 0);
676                 writer.write(bufToWrite, 0, maxSize);
677 
678                 index += bufferSize;
679             }
680         }
681     }
682 }