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