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  
20  package org.apache.myfaces.tobago.renderkit.html.scarborough.standard.tag;
21  
22  import org.apache.commons.lang.StringUtils;
23  import org.apache.myfaces.tobago.application.ProjectStage;
24  import org.apache.myfaces.tobago.component.Attributes;
25  import org.apache.myfaces.tobago.component.Facets;
26  import org.apache.myfaces.tobago.component.UIMenuBar;
27  import org.apache.myfaces.tobago.component.UIPage;
28  import org.apache.myfaces.tobago.component.UIPopup;
29  import org.apache.myfaces.tobago.config.Configurable;
30  import org.apache.myfaces.tobago.config.TobagoConfig;
31  import org.apache.myfaces.tobago.context.ClientProperties;
32  import org.apache.myfaces.tobago.context.Markup;
33  import org.apache.myfaces.tobago.context.ResourceManagerUtils;
34  import org.apache.myfaces.tobago.context.Theme;
35  import org.apache.myfaces.tobago.internal.ajax.AjaxInternalUtils;
36  import org.apache.myfaces.tobago.internal.component.AbstractUIPage;
37  import org.apache.myfaces.tobago.internal.layout.LayoutContext;
38  import org.apache.myfaces.tobago.internal.util.AccessKeyMap;
39  import org.apache.myfaces.tobago.internal.util.FacesContextUtils;
40  import org.apache.myfaces.tobago.internal.util.MimeTypeUtils;
41  import org.apache.myfaces.tobago.internal.util.ResponseUtils;
42  import org.apache.myfaces.tobago.layout.Measure;
43  import org.apache.myfaces.tobago.renderkit.PageRendererBase;
44  import org.apache.myfaces.tobago.renderkit.css.Classes;
45  import org.apache.myfaces.tobago.renderkit.css.Style;
46  import org.apache.myfaces.tobago.renderkit.html.HtmlAttributes;
47  import org.apache.myfaces.tobago.renderkit.html.HtmlElements;
48  import org.apache.myfaces.tobago.renderkit.html.HtmlInputTypes;
49  import org.apache.myfaces.tobago.renderkit.html.util.HtmlRendererUtils;
50  import org.apache.myfaces.tobago.renderkit.util.RenderUtils;
51  import org.apache.myfaces.tobago.util.ComponentUtils;
52  import org.apache.myfaces.tobago.util.VariableResolverUtils;
53  import org.apache.myfaces.tobago.webapp.Secret;
54  import org.apache.myfaces.tobago.webapp.TobagoResponseWriter;
55  import org.slf4j.Logger;
56  import org.slf4j.LoggerFactory;
57  
58  import javax.faces.application.Application;
59  import javax.faces.application.FacesMessage;
60  import javax.faces.application.ViewHandler;
61  import javax.faces.component.UIComponent;
62  import javax.faces.context.ExternalContext;
63  import javax.faces.context.FacesContext;
64  import javax.faces.context.ResponseWriter;
65  import java.io.IOException;
66  import java.util.ArrayList;
67  import java.util.Collection;
68  import java.util.Iterator;
69  import java.util.List;
70  import java.util.Map;
71  import java.util.Set;
72  import java.util.StringTokenizer;
73  
74  public class PageRenderer extends PageRendererBase {
75  
76    private static final Logger LOG = LoggerFactory.getLogger(PageRenderer.class);
77  
78    private static final String CLIENT_DEBUG_SEVERITY = "clientDebugSeverity";
79    private static final String LAST_FOCUS_ID = "lastFocusId";
80  
81    @Override
82    public void decode(FacesContext facesContext, UIComponent component) {
83      super.decode(facesContext, component);
84      String clientId = component.getClientId(facesContext);
85      ExternalContext externalContext = facesContext.getExternalContext();
86  
87      // severity
88      String severity = (String)
89          externalContext.getRequestParameterMap().get(clientId + ComponentUtils.SUB_SEPARATOR + "clientSeverity");
90      if (severity != null) {
91        externalContext.getRequestMap().put(CLIENT_DEBUG_SEVERITY, severity);
92      }
93  
94      // last focus
95      String lastFocusId = (String) 
96          externalContext.getRequestParameterMap().get(clientId + ComponentUtils.SUB_SEPARATOR + LAST_FOCUS_ID);
97      if (lastFocusId != null) {
98        FacesContextUtils.setFocusId(facesContext, lastFocusId);
99      }
100 
101     // scrollbar weight
102     String name = clientId + ComponentUtils.SUB_SEPARATOR + "scrollbarWeight";
103     String value = null;
104     try {
105       value = (String) facesContext.getExternalContext().getRequestParameterMap().get(name);
106       if (StringUtils.isNotBlank(value)) {
107         StringTokenizer tokenizer = new StringTokenizer(value, ";");
108         Measure vertical = Measure.valueOf(tokenizer.nextToken());
109         Measure horizontal = Measure.valueOf(tokenizer.nextToken());
110         if (vertical.greaterThan(Measure.valueOf(30)) || vertical.lessThan(Measure.valueOf(3))
111            || horizontal.greaterThan(Measure.valueOf(30)) || horizontal.lessThan(Measure.valueOf(3))) {
112           LOG.error("Ignoring strange values: vertical=" + vertical + " horizontal=" + horizontal);
113         } else {
114           ClientProperties client = VariableResolverUtils.resolveClientProperties(facesContext);
115           client.setVerticalScrollbarWeight(vertical);
116           client.setHorizontalScrollbarWeight(horizontal);
117         }
118       }
119     } catch (Exception e) {
120       LOG.error("Error in decoding '" + name + "': value='" + value + "'", e);
121     }
122   }
123 
124   @Override
125   public void encodeBegin(FacesContext facesContext, UIComponent component) throws IOException {
126 
127     final UIPage page = (UIPage) component;
128     final TobagoConfig tobagoConfig = TobagoConfig.getInstance(facesContext);
129 
130     // invoke prepareRender
131     RenderUtils.prepareRendererAll(facesContext, page);
132 
133     LayoutContext layoutContext = new LayoutContext(page);
134     layoutContext.layout();
135     if (FacesContextUtils.getFocusId(facesContext) == null && !StringUtils.isBlank(page.getFocusId())) {
136       FacesContextUtils.setFocusId(facesContext, page.getFocusId());
137     }
138     TobagoResponseWriter writer = HtmlRendererUtils.getTobagoResponseWriter(facesContext);
139 
140     // reset responseWriter and render page
141     facesContext.setResponseWriter(writer);
142 
143     ResponseUtils.ensureNoCacheHeader(facesContext);
144 
145     ResponseUtils.ensureContentSecurityPolicyHeader(facesContext, tobagoConfig.getContentSecurityPolicy());
146 
147     if (LOG.isDebugEnabled()) {
148       for (Object o : page.getAttributes().entrySet()) {
149         Map.Entry entry = (Map.Entry) o;
150         LOG.debug("*** '" + entry.getKey() + "' -> '" + entry.getValue() + "'");
151       }
152     }
153 
154     Application application = facesContext.getApplication();
155     ViewHandler viewHandler = application.getViewHandler();
156     String viewId = facesContext.getViewRoot().getViewId();
157     String formAction = viewHandler.getActionURL(facesContext, viewId);
158     formAction = facesContext.getExternalContext().encodeActionURL(formAction);
159     String contentType = writer.getContentTypeWithCharSet();
160     ResponseUtils.ensureContentTypeHeader(facesContext, contentType);
161     String clientId = page.getClientId(facesContext);
162     final ClientProperties client = VariableResolverUtils.resolveClientProperties(facesContext);
163     final ProjectStage projectStage = tobagoConfig.getProjectStage();
164     final boolean developmentMode =  projectStage == ProjectStage.Development;
165     final boolean debugMode = client.isDebugMode() || developmentMode;
166     final boolean productionMode = !debugMode && projectStage == ProjectStage.Production;
167     int clientLogSeverity = 2;
168     if (debugMode) {
169       String severity = (String) facesContext.getExternalContext().getRequestMap().get(CLIENT_DEBUG_SEVERITY);
170       if (LOG.isDebugEnabled()) {
171         LOG.debug("get " + CLIENT_DEBUG_SEVERITY + " = " + severity);
172       }
173       if (severity != null) {
174         try {
175           int index = severity.indexOf(';');
176           if (index == -1) {
177             index = severity.length();
178           }
179             clientLogSeverity = Integer.parseInt(severity.substring(0, index));
180         } catch (NumberFormatException e) {/* ignore; use default*/ }
181       }
182     }
183     boolean frameKiller = tobagoConfig.isPreventFrameAttacks();
184 
185     if (!FacesContextUtils.isAjax(facesContext)) {
186       HtmlRendererUtils.renderDojoDndSource(facesContext, component);
187 
188       String title = (String) page.getAttributes().get(Attributes.LABEL);
189 
190       writer.startElement(HtmlElements.HEAD, null);
191 
192       // meta tags
193 
194       // this is needed, because websphere 6.0? ignores the setting of the content type on the response
195       writer.startElement(HtmlElements.META, null);
196       writer.writeAttribute(HtmlAttributes.HTTP_EQUIV, "Content-Type", false);
197       writer.writeAttribute(HtmlAttributes.CONTENT, contentType, false);
198       writer.endElement(HtmlElements.META);
199 
200       // title
201       writer.startElement(HtmlElements.TITLE, null);
202       writer.writeText(title != null ? title : "");
203       writer.endElement(HtmlElements.TITLE);
204       final Theme theme = client.getTheme();
205 
206       if (debugMode) {
207         // This tag must not be earlier, because the
208         // IE doesn't accept some META tags, when they are not the first ones.
209         writer.writeJavascript("var TbgHeadStart = new Date();");
210       }
211 
212       // style files
213       for (String styleFile : theme.getStyleResources(productionMode)) {
214         writeStyle(facesContext, writer, styleFile);
215       }
216 
217       for (String styleFile : FacesContextUtils.getStyleFiles(facesContext)) {
218         writeStyle(facesContext, writer, styleFile);
219       }
220 
221       String icon = page.getApplicationIcon();
222       if (icon != null) {
223         // XXX unify with image renderer
224         if (ResourceManagerUtils.isAbsoluteResource(icon)) {
225           // absolute Path to image : nothing to do
226         } else {
227           icon = ResourceManagerUtils.getImageWithPath(facesContext, icon);
228         }
229 
230         writer.startElement(HtmlElements.LINK, null);
231         if (icon.endsWith(".ico")) {
232           writer.writeAttribute(HtmlAttributes.REL, "shortcut icon", false);
233           writer.writeAttribute(HtmlAttributes.HREF, icon, false);
234         } else {
235           // XXX IE only supports ICO files for favicons
236           writer.writeAttribute(HtmlAttributes.REL, "icon", false);
237           writer.writeAttribute(HtmlAttributes.TYPE, MimeTypeUtils.getMimeTypeForFile(icon), false);
238           writer.writeAttribute(HtmlAttributes.HREF, icon, false);
239         }
240         writer.endElement(HtmlElements.LINK);
241       }
242 
243       // style sniplets
244       Set<String> styleBlocks = FacesContextUtils.getStyleBlocks(facesContext);
245       if (styleBlocks.size() > 0) {
246         writer.startElement(HtmlElements.STYLE, null);
247         writer.flush(); // is needed in some cases, e. g. TOBAGO-1094
248         for (String cssBlock : styleBlocks) {
249           writer.write(cssBlock);
250         }
251         writer.endElement(HtmlElements.STYLE);
252       }
253 
254       if (debugMode) {
255         boolean hideClientLogging = true;
256         String severity = (String) facesContext.getExternalContext().getRequestMap().get(CLIENT_DEBUG_SEVERITY);
257         if (LOG.isDebugEnabled()) {
258           LOG.debug("get " + CLIENT_DEBUG_SEVERITY + " = " + severity);
259         }
260         if (severity != null) {
261           try {
262             int index = severity.indexOf(';');
263             if (index == -1) {
264               index = severity.length();
265             }
266             clientLogSeverity = Integer.parseInt(severity.substring(0, index));
267           } catch (NumberFormatException e) {/* ignore; use default*/ }
268           hideClientLogging = !severity.contains("show");
269         }
270         // the jquery ui is used in moment only for the logging area...
271         //FacesContextUtils.addOnloadScript(facesContext, 0, "new LOG.LogArea({hide: " + hideClientLogging + "});");
272       }
273 
274       // render remaining script tags
275       for (String scriptFile: theme.getScriptResources(productionMode)) {
276         encodeScript(facesContext, writer, scriptFile);
277       }
278 
279       for (String scriptFile : FacesContextUtils.getScriptFiles(facesContext)) {
280         encodeScript(facesContext, writer, scriptFile);
281       }
282 
283 
284       writer.startJavascript();
285       // onload script
286       writeEventFunction(writer, FacesContextUtils.getOnloadScripts(facesContext), "load", false);
287 
288       // onunload script
289       writeEventFunction(writer, FacesContextUtils.getOnunloadScripts(facesContext), "unload", false);
290 
291       // onexit script
292       writeEventFunction(writer, FacesContextUtils.getOnexitScripts(facesContext), "exit", false);
293 
294       writeEventFunction(writer, FacesContextUtils.getOnsubmitScripts(facesContext), "submit", true);
295 
296       int debugCounter = 0;
297       for (String scriptBlock : FacesContextUtils.getScriptBlocks(facesContext)) {
298 
299         if (LOG.isDebugEnabled()) {
300           LOG.debug("write scriptblock " + ++debugCounter + " :\n" + scriptBlock);
301         }
302         writer.write(scriptBlock);
303         writer.write('\n');
304       }
305       writer.endJavascript();
306       writer.endElement(HtmlElements.HEAD);
307     }
308 
309     writer.startElement(HtmlElements.BODY, page);
310     writer.writeIdAttribute(clientId);
311     writer.writeClassAttribute(Classes.create(page));
312     HtmlRendererUtils.writeDataAttributes(facesContext, writer, page);
313     HtmlRendererUtils.renderCommandFacet(page, facesContext, writer);
314 
315     if (debugMode) {
316       writer.writeJavascript("TbgTimer.startBody = new Date();");
317     }
318 
319     writer.startElement(HtmlElements.FORM, page);
320     if (frameKiller && !FacesContextUtils.isAjax(facesContext)) {
321       writer.writeAttribute(HtmlAttributes.STYLE, "display:none", false);
322     }
323     writer.writeAttribute(HtmlAttributes.ACTION, formAction, true);
324     writer.writeIdAttribute(page.getFormId(facesContext));
325     writer.writeAttribute(HtmlAttributes.METHOD, getMethod(page), false);
326     String enctype = FacesContextUtils.getEnctype(facesContext);
327     if (enctype != null) {
328       writer.writeAttribute(HtmlAttributes.ENCTYPE, enctype, false);
329     }
330     // TODO: enable configuration of  'accept-charset'
331     writer.writeAttribute(HtmlAttributes.ACCEPT_CHARSET, AbstractUIPage.FORM_ACCEPT_CHARSET, false);
332     // TODO evaluate 'accept' attribute usage
333     //writer.writeAttribute(HtmlAttributes.ACCEPT, );
334     writer.startElement(HtmlElements.INPUT, null);
335     writer.writeAttribute(HtmlAttributes.TYPE, HtmlInputTypes.HIDDEN, false);
336     writer.writeNameAttribute(clientId + ComponentUtils.SUB_SEPARATOR + "form-action");
337     writer.writeIdAttribute(clientId + ComponentUtils.SUB_SEPARATOR + "form-action");
338     writer.endElement(HtmlElements.INPUT);
339 
340     writer.startElement(HtmlElements.INPUT, null);
341     writer.writeAttribute(HtmlAttributes.TYPE, HtmlInputTypes.HIDDEN, false);
342     writer.writeNameAttribute(clientId + ComponentUtils.SUB_SEPARATOR + "context-path");
343     writer.writeIdAttribute(clientId + ComponentUtils.SUB_SEPARATOR + "context-path");
344     writer.writeAttribute(HtmlAttributes.VALUE, facesContext.getExternalContext().getRequestContextPath(), true);
345     writer.endElement(HtmlElements.INPUT);
346 
347     writer.startElement(HtmlElements.INPUT, null);
348     writer.writeAttribute(HtmlAttributes.TYPE, HtmlInputTypes.HIDDEN, false);
349     writer.writeNameAttribute(clientId + ComponentUtils.SUB_SEPARATOR + "action-position");
350     writer.writeIdAttribute(clientId + ComponentUtils.SUB_SEPARATOR + "action-position");
351     writer.endElement(HtmlElements.INPUT);
352 
353     boolean calculateScrollbarWeight =
354         client.getVerticalScrollbarWeight() == null || client.getHorizontalScrollbarWeight() == null;
355 
356     if (calculateScrollbarWeight) {
357       writer.startElement(HtmlElements.DIV, null);
358       writer.writeClassAttribute(Classes.create(page, "scrollbarWeight", Markup.NULL));
359       writer.startElement(HtmlElements.DIV, null);
360       writer.endElement(HtmlElements.DIV);
361       writer.endElement(HtmlElements.DIV);
362     }
363 
364     writer.startElement(HtmlElements.INPUT, null);
365     writer.writeAttribute(HtmlAttributes.TYPE, HtmlInputTypes.HIDDEN, false);
366     writer.writeNameAttribute(clientId + ComponentUtils.SUB_SEPARATOR + "scrollbarWeight");
367     writer.writeIdAttribute(clientId + ComponentUtils.SUB_SEPARATOR + "scrollbarWeight");
368     if (client.getVerticalScrollbarWeight() != null && client.getHorizontalScrollbarWeight() != null) {
369       StringBuilder buf = new StringBuilder();
370       buf.append(client.getVerticalScrollbarWeight().getPixel());
371       buf.append(";");
372       buf.append(client.getHorizontalScrollbarWeight().getPixel());
373       writer.writeAttribute(HtmlAttributes.VALUE, buf.toString(), false);
374     }
375     writer.endElement(HtmlElements.INPUT);
376 
377     if (TobagoConfig.getInstance(FacesContext.getCurrentInstance()).isCreateSessionSecret()) {
378       Secret.encode(facesContext, writer);
379     }
380 
381     if (debugMode) {
382       writer.startElement(HtmlElements.INPUT, null);
383       writer.writeAttribute(HtmlAttributes.VALUE, clientLogSeverity);
384       writer.writeAttribute(HtmlAttributes.ID, clientId + ComponentUtils.SUB_SEPARATOR + "clientSeverity", false);
385       writer.writeAttribute(HtmlAttributes.NAME, clientId + ComponentUtils.SUB_SEPARATOR + "clientSeverity", false);
386       writer.writeAttribute(HtmlAttributes.TYPE, HtmlInputTypes.HIDDEN, false);
387       writer.endElement(HtmlElements.INPUT);
388     }
389 
390     if (component.getFacet("backButtonDetector") != null) {
391       UIComponent hidden = component.getFacet("backButtonDetector");
392       RenderUtils.encode(facesContext, hidden);
393     }
394 
395     //checkForCommandFacet(component, facesContext, writer);
396 
397 // TODO: this is needed for the "BACK-BUTTON-PROBLEM"
398 // but may no longer needed
399 /*
400     if (ViewHandlerImpl.USE_VIEW_MAP) {
401       writer.startElement(HtmlElements.INPUT, null);
402       writer.writeAttribute(HtmlAttributes.TYPE, "hidden", null);
403       writer.writeNameAttribute(ViewHandlerImpl.PAGE_ID);
404       writer.writeIdAttribute(ViewHandlerImpl.PAGE_ID);
405       Object value = facesContext.getViewRoot().getAttributes().get(
406           ViewHandlerImpl.PAGE_ID);
407       writer.writeAttribute(HtmlAttributes.VALUE, (value != null ? value : ""), null);
408       writer.endElement(HtmlElements.INPUT);
409     }
410 */
411 
412     UIMenuBar menuBar = (UIMenuBar) page.getFacet(Facets.MENUBAR);
413     if (menuBar != null) {
414       menuBar.getAttributes().put(Attributes.PAGE_MENU, Boolean.TRUE);
415       RenderUtils.encode(facesContext, menuBar);
416     }
417     // write the previously rendered page content
418 //    AbstractUILayoutBase.getLayout(component).encodeChildrenOfComponent(facesContext, component);
419 
420 //    page.encodeLayoutBegin(facesContext);
421     
422     writer.startElement(HtmlElements.DIV, page);
423     writer.writeClassAttribute(Classes.create(page, "content"));
424     writer.writeIdAttribute(clientId + ComponentUtils.SUB_SEPARATOR + "content");
425     Style style = new Style(facesContext, page);
426     // XXX position the div, so that the scrollable area is correct.
427     // XXX better to take this fact into layout management.
428     // XXX is also useful in boxes, etc.
429     Measure border = getBorderBottom(facesContext, page);
430     style.setHeight(page.getCurrentHeight().subtract(border));
431     style.setTop(border);
432     writer.writeStyleAttribute(style);
433   }
434 
435   private void writeStyle(FacesContext facesContext, TobagoResponseWriter writer, String styleFile)
436       throws IOException {
437     List<String> styles = ResourceManagerUtils.getStyles(facesContext, styleFile);
438     for (String styleString : styles) {
439       if (styleString.length() > 0) {
440         writer.startElement(HtmlElements.LINK, null);
441         writer.writeAttribute(HtmlAttributes.REL, "stylesheet", false);
442         writer.writeAttribute(HtmlAttributes.HREF, styleString, false);
443 //          writer.writeAttribute(HtmlAttributes.MEDIA, "screen", false);
444         writer.writeAttribute(HtmlAttributes.TYPE, "text/css", false);
445         writer.endElement(HtmlElements.LINK);
446       }
447     }
448   }
449 
450 //  @Override
451 //  public void encodeChildren(FacesContext facesContext, UIComponent component) throws IOException {
452 //    UIPage page = (UIPage) component;
453 //    page.encodeLayoutChildren(facesContext);
454 //  }
455 
456   @Override
457   public void encodeEnd(FacesContext facesContext, UIComponent component) throws IOException {
458 
459 
460     UIPage page = (UIPage) component;
461     TobagoResponseWriter writer = HtmlRendererUtils.getTobagoResponseWriter(facesContext);
462 
463     writer.endElement(HtmlElements.DIV);
464 
465     // write popup components
466     // beware of ConcurrentModificationException in cascading popups!
467     // no foreach
468 
469     UIPopup[] popupArray = FacesContextUtils.getPopups(facesContext).toArray(
470         new UIPopup[FacesContextUtils.getPopups(facesContext).size()]);
471     for (UIPopup popup : popupArray) {
472       RenderUtils.encode(facesContext, popup);
473     }
474 
475     String clientId = page.getClientId(facesContext);
476     final boolean debugMode = VariableResolverUtils.resolveClientProperties(facesContext).isDebugMode();
477 
478 
479     // avoid submit page in ie if the form contains only one input and you press the enter key in the input
480     if (VariableResolverUtils.resolveClientProperties(facesContext).getUserAgent().isMsie()) {
481       writer.startElement(HtmlElements.INPUT, null);
482       writer.writeAttribute(HtmlAttributes.TYPE, HtmlInputTypes.TEXT, false);
483       writer.writeAttribute(HtmlAttributes.NAME, "tobago.dummy", false);
484       writer.writeAttribute(HtmlAttributes.TABINDEX, "-1", false);
485       writer.writeAttribute(HtmlAttributes.STYLE, "visibility:hidden;display:none;", false);
486       writer.endElement(HtmlElements.INPUT);
487     }
488 
489     List<String> messageClientIds = AjaxInternalUtils.getMessagesClientIds(facesContext);
490     if (messageClientIds != null) {
491       writer.startElement(HtmlElements.INPUT, null);
492       writer.writeAttribute(HtmlAttributes.VALUE, StringUtils.join(messageClientIds, ','), true);
493       writer.writeAttribute(HtmlAttributes.ID, clientId + ComponentUtils.SUB_SEPARATOR + "messagesClientIds", false);
494       writer.writeAttribute(HtmlAttributes.NAME, clientId + ComponentUtils.SUB_SEPARATOR + "messagesClientIds", false);
495       writer.writeAttribute(HtmlAttributes.TYPE, HtmlInputTypes.HIDDEN, false);
496       writer.endElement(HtmlElements.INPUT);
497     }
498 
499     // placeholder for menus
500     writer.startElement(HtmlElements.DIV, page);
501     writer.writeClassAttribute(Classes.create(page, "menuStore"));
502     writer.endElement(HtmlElements.DIV);
503 
504     Application application = facesContext.getApplication();
505     ViewHandler viewHandler = application.getViewHandler();
506 
507     writer.startElement(HtmlElements.SPAN, null);
508     writer.writeIdAttribute(clientId + ComponentUtils.SUB_SEPARATOR + "jsf-state-container");
509     writer.flush();
510     if (!FacesContextUtils.isAjax(facesContext)) {
511         viewHandler.writeState(facesContext);
512     }
513     writer.endElement(HtmlElements.SPAN);
514 
515 
516     writer.endElement(HtmlElements.FORM);
517 
518     // The waiting for the next page image
519     // Warning: The image must be loaded before the submit, otherwise this feature will not work with webkit
520     // browsers. This is the reason, why this code has moved from JavaScript to the renderer here.
521     writer.startElement(HtmlElements.IMG, null);
522     writer.writeClassAttribute(Classes.create(page, "overlayWaitPreloadedImage"));
523     final String wait = ResourceManagerUtils.getImageWithPath(facesContext, "image/tobago-overlay-wait.gif");
524     writer.writeAttribute(HtmlAttributes.SRC, wait, false);
525     writer.endElement(HtmlElements.IMG);
526 
527     writer.startElement(HtmlElements.IMG, null);
528     writer.writeClassAttribute(Classes.create(page, "overlayErrorPreloadedImage"));
529     final String error = ClientProperties.getInstance(facesContext).getUserAgent().isMsie6()
530         ? ResourceManagerUtils.getImageWithPath(facesContext, "image/remove.gif") // XXX why png doesn't work in ie6?
531         : ResourceManagerUtils.getImageWithPath(facesContext, "image/dialog-error.png");
532     writer.writeAttribute(HtmlAttributes.SRC, error, false);
533     writer.endElement(HtmlElements.IMG);
534 
535     writer.startElement(HtmlElements.IMG, null);
536     writer.writeClassAttribute(Classes.create(page, "pngFixBlankImage"));
537     final String pngFixBlankImage = ResourceManagerUtils.getImageWithPath(facesContext, "image/blank.gif");
538     writer.writeAttribute(HtmlAttributes.SRC, pngFixBlankImage, false);
539     writer.endElement(HtmlElements.IMG);
540 
541     writer.startElement(HtmlElements.IMG, null);
542     writer.writeClassAttribute(Classes.create(page, "overlayBackgroundImage"));
543     final String overlayBackgroundImage = ResourceManagerUtils.getImageWithPath(facesContext,
544         "image/tobago-overlay-background.png");
545     writer.writeAttribute(HtmlAttributes.SRC, overlayBackgroundImage, false);
546     writer.endElement(HtmlElements.IMG);
547 
548     // debugging...
549     if (debugMode) {
550       List<String> logMessages = new ArrayList<String>();
551       for (Iterator ids = facesContext.getClientIdsWithMessages();
552            ids.hasNext();) {
553         String id = (String) ids.next();
554         for (Iterator messages = facesContext.getMessages(id);
555              messages.hasNext();) {
556           FacesMessage message = (FacesMessage) messages.next();
557           logMessages.add(errorMessageForDebugging(id, message));
558         }
559         
560       }
561       if (!logMessages.isEmpty()) {
562         logMessages.add(0, "LOG.show();");
563       }
564 
565       HtmlRendererUtils.writeScriptLoader(facesContext, null,
566           logMessages.toArray(new String[logMessages.size()]));
567     }
568 
569     if (debugMode) {
570       writer.writeJavascript("TbgTimer.endBody = new Date();");
571     }
572 
573 //    writer.writeJavascript("setTimeout(\"Tobago.init('" + clientId + "')\", 1000)");
574 
575     writer.startElement(HtmlElements.NOSCRIPT, null);
576     writer.startElement(HtmlElements.DIV, null);
577     writer.writeClassAttribute(Classes.create(page, "noscript"));
578     writer.writeText(ResourceManagerUtils.getPropertyNotNull(facesContext, "tobago", "pageNoscript"));
579     writer.endElement(HtmlElements.DIV);
580     writer.endElement(HtmlElements.NOSCRIPT);
581 
582     writer.endElement(HtmlElements.BODY);
583 
584     if (LOG.isDebugEnabled()) {
585       LOG.debug("unused AccessKeys    : "
586           + AccessKeyMap.getUnusedKeys(facesContext));
587       LOG.debug("duplicated AccessKeys: "
588           + AccessKeyMap.getDublicatedKeys(facesContext));
589     }
590 
591     if (facesContext.getExternalContext().getRequestParameterMap().get("X") != null) {
592       throw new RuntimeException("Debugging activated via X parameter");
593     }
594   }
595 
596   private void writeEventFunction(TobagoResponseWriter writer, Collection<String> eventFunctions,
597       String event, boolean returnBoolean) throws IOException {
598     if (!eventFunctions.isEmpty()) {
599       writer.write("Tobago.applicationOn");
600       writer.write(event);
601       writer.write(" = function() {\n");
602       if (returnBoolean) {
603         writer.write("  var result;\n");
604       }
605       for (String function : eventFunctions) {
606         if (returnBoolean) {
607           writer.write("  result = ");
608         } else {
609           writer.write("  ");
610         }
611         writer.write(function);
612         if (!function.trim().endsWith(";")) {
613           writer.write(";\n");
614         } else {
615           writer.write("\n");
616         }
617         if (returnBoolean) {
618           writer.write("  if (typeof result == \"boolean\" && ! result) {\n");
619           writer.write("    return false;\n");
620           writer.write("  }\n");
621         }
622       }
623       writer.write("\n  return true;\n}\n");
624     }
625   }
626 
627   private void encodeScript(FacesContext facesContext, TobagoResponseWriter writer, String script) throws IOException {
628     List<String> list;
629     if (ResourceManagerUtils.isAbsoluteResource(script)) {
630       list = new ArrayList<String>();
631       list.add(script);
632     } else {
633       list = ResourceManagerUtils.getScripts(facesContext, script);
634     }
635     for (String src : list) {
636       if (StringUtils.isNotBlank(src)) {
637         writer.startElement(HtmlElements.SCRIPT, null);
638         writer.writeAttribute(HtmlAttributes.SRC, src, true);
639         // TODO test defer attribute
640         //writer.writeAttribute(HtmlAttributes.DEFER, true);
641         writer.writeAttribute(HtmlAttributes.TYPE, "text/javascript", false);
642         writer.endElement(HtmlElements.SCRIPT);
643       }
644     }
645   }
646 
647   private void errorMessageForDebugging(String id, FacesMessage message,
648       ResponseWriter writer) throws IOException {
649     writer.startElement(HtmlElements.DIV, null);
650     writer.writeAttribute(HtmlAttributes.STYLE, "color: red", null);
651     writer.flush(); // is needed in some cases, e. g. TOBAGO-1094
652     writer.write("[");
653     writer.write(id != null ? id : "null");
654     writer.write("]");
655     writer.write("[");
656     writer.write(message.getSummary() == null ? "null" : message.getSummary());
657     writer.write("/");
658     writer.write(message.getDetail() == null ? "null" : message.getDetail());
659     writer.write("]");
660     writer.endElement(HtmlElements.DIV);
661     writer.startElement(HtmlElements.BR, null);
662     writer.endElement(HtmlElements.BR);
663   }
664 
665   private String errorMessageForDebugging(String id, FacesMessage message) {
666     StringBuilder sb = new StringBuilder("LOG.info(\"FacesMessage: [");
667     sb.append(id != null ? id : "null");
668     sb.append("][");
669     sb.append(message.getSummary() == null ? "null" : escape(message.getSummary()));
670     sb.append("/");
671     sb.append(message.getDetail() == null ? "null" : escape(message.getDetail()));
672     sb.append("]\");");
673     return sb.toString();
674   }
675 
676   private String escape(String s) {
677     return StringUtils.replace(StringUtils.replace(s, "\\", "\\\\"), "\"", "\\\"");
678   }
679 
680   private String getMethod(UIPage page) {
681     String method = (String) page.getAttributes().get(Attributes.METHOD);
682     return method == null ? "post" : method;
683   }
684 
685   @Override
686   public boolean getRendersChildren() {
687     return true;
688   }
689 
690   @Override
691   public Measure getBorderBottom(FacesContext facesContext, Configurable component) {
692     // XXX this is a hack. correct would be the top-border, but this would shift the content, because of the
693     // XXX hack before the code: writer.writeStyleAttribute(style)
694     UIPage page = (UIPage) component;
695     UIMenuBar menuBar = (UIMenuBar) page.getFacet(Facets.MENUBAR);
696     if (menuBar != null) {
697       return getResourceManager().getThemeMeasure(facesContext, page, "custom.menuBar-height");
698     } else {
699       return Measure.ZERO;
700     }
701   }
702 
703   @Override
704   public Measure getWidth(FacesContext facesContext, Configurable component) {
705     // width of the actual browser window
706     Measure width = (Measure) FacesContext.getCurrentInstance().getExternalContext()
707         .getRequestMap().get("tobago-page-clientDimension-width");
708     if (width != null) {
709       return width;
710     } else {
711       return super.getWidth(facesContext, component);
712     }
713   }
714 
715   @Override
716   public Measure getHeight(FacesContext facesContext, Configurable component) {
717     // height of the actual browser window
718     Measure height = (Measure) FacesContext.getCurrentInstance().getExternalContext()
719         .getRequestMap().get("tobago-page-clientDimension-height");
720     if (height != null) {
721       return height;
722     } else {
723       return super.getHeight(facesContext, component);
724     }
725   }
726 }