001: /*
002: * Version: MPL 1.1/GPL 2.0/LGPL 2.1
003: *
004: * "The contents of this file are subject to the Mozilla Public License
005: * Version 1.1 (the "License"); you may not use this file except in
006: * compliance with the License. You may obtain a copy of the License at
007: * http://www.mozilla.org/MPL/
008: *
009: * Software distributed under the License is distributed on an "AS IS"
010: * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
011: * License for the specific language governing rights and limitations under
012: * the License.
013: *
014: * The Original Code is ICEfaces 1.5 open source software code, released
015: * November 5, 2006. The Initial Developer of the Original Code is ICEsoft
016: * Technologies Canada, Corp. Portions created by ICEsoft are Copyright (C)
017: * 2004-2006 ICEsoft Technologies Canada, Corp. All Rights Reserved.
018: *
019: * Contributor(s): _____________________.
020: *
021: * Alternatively, the contents of this file may be used under the terms of
022: * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"
023: * License), in which case the provisions of the LGPL License are
024: * applicable instead of those above. If you wish to allow use of your
025: * version of this file only under the terms of the LGPL License and not to
026: * allow others to use your version of this file under the MPL, indicate
027: * your decision by deleting the provisions above and replace them with
028: * the notice and other provisions required by the LGPL License. If you do
029: * not delete the provisions above, a recipient may use your version of
030: * this file under either the MPL or the LGPL License."
031: *
032: */
033:
034: package com.icesoft.faces.context;
035:
036: import com.icesoft.faces.application.D2DViewHandler;
037: import com.icesoft.faces.application.StartupTime;
038: import com.icesoft.faces.context.effects.JavascriptContext;
039: import com.icesoft.faces.util.CoreUtils;
040: import com.icesoft.faces.util.DOMUtils;
041: import com.icesoft.faces.webapp.http.common.Configuration;
042: import com.icesoft.jasper.Constants;
043: import org.apache.commons.logging.Log;
044: import org.apache.commons.logging.LogFactory;
045: import org.w3c.dom.Attr;
046: import org.w3c.dom.DOMException;
047: import org.w3c.dom.Document;
048: import org.w3c.dom.Element;
049: import org.w3c.dom.Node;
050: import org.w3c.dom.NodeList;
051:
052: import javax.faces.FacesException;
053: import javax.faces.application.ViewHandler;
054: import javax.faces.component.UIComponent;
055: import javax.faces.context.FacesContext;
056: import javax.faces.context.ResponseWriter;
057: import javax.servlet.http.HttpServletRequest;
058: import javax.xml.parsers.DocumentBuilder;
059: import javax.xml.parsers.DocumentBuilderFactory;
060: import javax.xml.parsers.ParserConfigurationException;
061: import java.beans.Beans;
062: import java.io.IOException;
063: import java.io.Writer;
064: import java.util.ArrayList;
065: import java.util.Collection;
066: import java.util.HashMap;
067: import java.util.Iterator;
068: import java.util.Locale;
069: import java.util.Map;
070:
071: /**
072: * <p><strong>DOMResponseWriter</strong> is a DOM specific implementation of
073: * <code>javax.faces.context.ResponseWriter</code>.
074: */
075: public class DOMResponseWriter extends ResponseWriter {
076: private static final Log log = LogFactory
077: .getLog(DOMResponseWriter.class);
078: public static final String STREAM_WRITING = "com.icesoft.faces.streamWriting";
079: //DOM and current node being written to for this ResponseWriter
080: public static final String DOCTYPE_PUBLIC = "com.icesoft.doctype.public";
081: public static final String DOCTYPE_SYSTEM = "com.icesoft.doctype.system";
082: public static final String DOCTYPE_ROOT = "com.icesoft.doctype.root";
083: public static final String DOCTYPE_OUTPUT = "com.icesoft.doctype.output";
084: public static final String DOCTYPE_PRETTY_PRINTING = "com.icesoft.doctype.prettyprinting";
085:
086: public static final String RESPONSE_DOM = "com.icesoft.domResponseDocument";
087: public static final String RESPONSE_DOM_ID = "com.icesoft.domResponseDocumentID";
088: public static final String OLD_DOM = "com.icesoft.oldDocument";
089: public static final String RESPONSE_VIEWROOT = "com.icesoft.domResponseViewRoot";
090: //Hashtable of DOMContext objects associated with each component
091: public static final String RESPONSE_CONTEXTS_TABLE = "com.icesoft.domResponseContexts";
092: private static DocumentBuilder DOCUMENT_BUILDER;
093:
094: static {
095: try {
096: DOCUMENT_BUILDER = DocumentBuilderFactory.newInstance()
097: .newDocumentBuilder();
098: } catch (ParserConfigurationException e) {
099: log.error("Cannot acquire a DocumentBuilder", e);
100: }
101: }
102:
103: private static boolean isStreamWritingFlag = false;
104: private Document document;
105: private Node cursor;
106: private Map domResponseContexts;
107: private Map contextServletTable;
108: private BridgeFacesContext context;
109: private DOMSerializer serializer;
110: private Configuration configuration;
111:
112: public DOMResponseWriter(FacesContext context,
113: DOMSerializer serializer, Configuration configuration) {
114: this .serializer = serializer;
115: this .configuration = configuration;
116: try {
117: this .context = (BridgeFacesContext) context;
118: } catch (ClassCastException e) {
119: throw new IllegalStateException(
120: "ICEfaces requires the PersistentFacesServlet. "
121: + "Please check your web.xml servlet mappings");
122: }
123: this .initialize();
124: }
125:
126: Map getDomResponseContexts() {
127: return domResponseContexts;
128: }
129:
130: public Node getCursorParent() {
131: return cursor;
132: }
133:
134: public Document getDocument() {
135: return document;
136: }
137:
138: public String getContentType() {
139: return "text/html; charset=UTF-8";
140: }
141:
142: public String getCharacterEncoding() {
143: return "UTF-8";
144: }
145:
146: public void startDocument() throws IOException {
147: }
148:
149: private void initialize() {
150: contextServletTable = D2DViewHandler
151: .getContextServletTable(context);
152: // contexts for each component
153: if (contextServletTable
154: .containsKey(DOMResponseWriter.RESPONSE_CONTEXTS_TABLE)) {
155: domResponseContexts = (Map) contextServletTable
156: .get(DOMResponseWriter.RESPONSE_CONTEXTS_TABLE);
157: }
158: if (null == domResponseContexts) {
159: domResponseContexts = new HashMap();
160: contextServletTable.put(
161: DOMResponseWriter.RESPONSE_CONTEXTS_TABLE,
162: domResponseContexts);
163: }
164: // viewroot, application
165: contextServletTable.put(DOMResponseWriter.RESPONSE_VIEWROOT,
166: context.getViewRoot());
167: cursor = document = DOCUMENT_BUILDER.newDocument();
168: contextServletTable.put(DOMResponseWriter.RESPONSE_DOM,
169: document);
170: boolean streamWritingParam = "true".equalsIgnoreCase(context
171: .getExternalContext().getInitParameter(
172: DOMResponseWriter.STREAM_WRITING));
173: DOMResponseWriter.isStreamWritingFlag = Beans.isDesignTime()
174: || streamWritingParam;
175: }
176:
177: public void endDocument() throws IOException {
178: if (!isStreamWriting()) {
179: enhanceAndFixDocument();
180: serializer.serialize(document);
181: }
182: }
183:
184: public void flush() throws IOException {
185: }
186:
187: public void startElement(String name,
188: UIComponent componentForElement) throws IOException {
189: moveCursorOn(appendToCursor(document.createElement(name)));
190: }
191:
192: public void endElement(String name) throws IOException {
193: moveCursorOn(cursor.getParentNode());
194: }
195:
196: public void writeAttribute(String name, Object value,
197: String componentPropertyName) throws IOException {
198: //name.trim() because cardemo had a leading space in an attribute name
199: //which made the DOM processor choke
200: Attr attribute = document.createAttribute(name.trim());
201: attribute.setValue(String.valueOf(value));
202: appendToCursor(attribute);
203: }
204:
205: public void writeURIAttribute(String name, Object value,
206: String componentPropertyName) throws IOException {
207: String stringValue = String.valueOf(value);
208: if (stringValue.startsWith("javascript:")) {
209: writeAttribute(name, stringValue, componentPropertyName);
210: } else {
211: writeAttribute(name, stringValue.replace(' ', '+'),
212: componentPropertyName);
213: }
214: }
215:
216: public void writeComment(Object comment) throws IOException {
217: appendToCursor(document.createComment(String.valueOf(comment)));
218: }
219:
220: public void writeText(Object text, String componentPropertyName)
221: throws IOException {
222: appendToCursor(document.createTextNode(String.valueOf(text)));
223: }
224:
225: public void writeText(char text[], int off, int len)
226: throws IOException {
227: appendToCursor(document.createTextNode(new String(text, off,
228: len)));
229: }
230:
231: public ResponseWriter cloneWithWriter(Writer writer) {
232: //FIXME: This is a hack for DOM rendering but JSF currently clones the writer
233: //just as the components are complete
234: if (null != document) {
235: try {
236: endDocument();
237: } catch (IOException e) {
238: throw new IllegalStateException(e.toString());
239: }
240: }
241: try {
242: return new DOMResponseWriter(context, serializer,
243: configuration);
244: } catch (FacesException e) {
245: throw new IllegalStateException();
246: }
247: }
248:
249: public void close() throws IOException {
250: }
251:
252: public void write(char[] cbuf, int off, int len) throws IOException {
253: appendToCursor(document.createTextNode(new String(cbuf, off,
254: len)));
255: }
256:
257: public void write(int c) throws IOException {
258: appendToCursor(document
259: .createTextNode(String.valueOf((char) c)));
260: }
261:
262: public void write(String str) throws IOException {
263: appendToCursor(document.createTextNode(str));
264: }
265:
266: public void write(String str, int off, int len) throws IOException {
267: appendToCursor(document.createTextNode(str.substring(off, len)));
268: }
269:
270: private void enhanceAndFixDocument() {
271: Element html = (Element) document.getDocumentElement();
272: enhanceHtml(html = "html".equals(html.getTagName()) ? html
273: : fixHtml());
274:
275: Element head = (Element) document.getElementsByTagName("head")
276: .item(0);
277: enhanceHead(head == null ? fixHead() : head);
278:
279: Element body = (Element) document.getElementsByTagName("body")
280: .item(0);
281: enhanceBody(body == null ? fixBody() : body);
282: }
283:
284: private void enhanceHtml(Element html) {
285: //add lang attribute
286: Locale locale = context.getApplication().getViewHandler()
287: .calculateLocale(context);
288: html.setAttribute("lang", locale.getLanguage());
289: }
290:
291: private void enhanceBody(Element body) {
292: //id required for forwarded (server-side) redirects
293: body.setAttribute("id", "body");
294: Element iframe = document.createElement("iframe");
295: body.insertBefore(iframe, body.getFirstChild());
296: iframe.setAttribute("id", "history-frame");
297: Object request = context.getExternalContext().getRequest();
298:
299: final String frameURI;
300: //another "workaround" to resolve the iframe URI
301: if (request instanceof HttpServletRequest) {
302: HttpServletRequest httpRequest = (HttpServletRequest) request;
303: if (httpRequest.getRequestURI() == null) {
304: frameURI = "about:blank";
305: } else {
306: frameURI = CoreUtils.resolveResourceURL(FacesContext
307: .getCurrentInstance(), "/xmlhttp/blank");
308: }
309: } else {
310: frameURI = "about:blank";
311: }
312: iframe.setAttribute("title", "Icefaces Redirect");
313: iframe.setAttribute("src", frameURI);
314: iframe.setAttribute("frameborder", "0");
315: iframe
316: .setAttribute(
317: "style",
318: "z-index: 10000; visibility: hidden; width: 0; height: 0; position: absolute; opacity: 0.22; filter: alpha(opacity=22);");
319:
320: // TODO This is only meant to be a transitional focus retention(management) solution.
321: String focusId = context.getFocusId();
322: if (focusId != null && !focusId.equals("null")) {
323: JavascriptContext.focus(context, focusId);
324: }
325:
326: Element script = (Element) body.appendChild(document
327: .createElement("script"));
328: script.setAttribute("id", JavascriptContext.DYNAMIC_CODE_ID);
329: script.setAttribute("language", "javascript");
330: String calls = JavascriptContext.getJavascriptCalls(context);
331: script.appendChild(document.createTextNode(calls));
332:
333: Map session = context.getExternalContext().getSessionMap();
334: ElementController.from(session).addInto(body);
335:
336: String sessionIDScript = "window.session='"
337: + context.getIceFacesId() + "';\n";
338: //add viewIdentifier property to the container element ("body" for servlet env., any element for the portlet env.)
339: String viewIDScript = "document.getElementById('configuration-script').parentNode.viewIdentifier="
340: + context.getViewNumber() + ";\n";
341: String viewsIDScript = "if (!window.views) window.views = []; window.views.push("
342: + context.getViewNumber() + ");\n";
343:
344: String configurationScript = "window.configuration = {"
345: + "synchronous: "
346: + configuration.getAttribute("synchronousUpdate",
347: "false")
348: + ","
349: + "redirectURI: "
350: + configuration.getAttribute(
351: "connectionLostRedirectURI", "null")
352: + ","
353: + "connection: {"
354: + "context: '"
355: + context.getApplication().getViewHandler()
356: .getResourceURL(context, "/")
357: + "',"
358: + "timeout: "
359: + configuration.getAttributeAsLong("connectionTimeout",
360: 30000)
361: + ","
362: + "heartbeat: {"
363: + "interval: "
364: + configuration.getAttributeAsLong("heartbeatInterval",
365: 20000)
366: + ","
367: + "timeout: "
368: + configuration.getAttributeAsLong("heartbeatTimeout",
369: 3000)
370: + ","
371: + "retries: "
372: + configuration.getAttributeAsLong("heartbeatRetries",
373: 3) + "}" + "}" + "};\n";
374:
375: Element configurationElement = (Element) body
376: .appendChild(document.createElement("script"));
377: configurationElement.setAttribute("id", "configuration-script");
378: configurationElement.setAttribute("language", "javascript");
379: configurationElement.appendChild(document
380: .createTextNode(sessionIDScript + viewIDScript
381: + viewsIDScript + configurationScript));
382: body.appendChild(configurationElement);
383: }
384:
385: private void enhanceHead(Element head) {
386: ViewHandler handler = context.getApplication().getViewHandler();
387: //id required for forwarded (server-side) redirects
388: head.setAttribute("id", "head");
389: Element meta = (Element) head.appendChild(document
390: .createElement("meta"));
391: meta.setAttribute("name", "icefaces");
392: meta.setAttribute("content", "Rendered by ICEFaces D2D");
393:
394: Element noscript = (Element) head.appendChild(document
395: .createElement("noscript"));
396: Element noscriptMeta = (Element) noscript.appendChild(document
397: .createElement("meta"));
398: noscriptMeta.setAttribute("http-equiv", "refresh");
399: noscriptMeta.setAttribute("content", "0;url="
400: + handler.getResourceURL(context,
401: "/xmlhttp/javascript-blocked"));
402:
403: //load libraries
404: Collection libs = new ArrayList();
405: if (context.getExternalContext().getInitParameter(
406: D2DViewHandler.INCLUDE_OPEN_AJAX_HUB) != null) {
407: libs.add("/xmlhttp/openajax.js");
408: }
409: libs.add("/xmlhttp" + StartupTime.getStartupInc()
410: + "icefaces-d2d.js");
411: //todo: refactor how extral libraries are loaded into the bridge; always include extra libraries for now
412: libs.add("/xmlhttp" + StartupTime.getStartupInc()
413: + "ice-extras.js");
414: if (context.getExternalContext().getRequestMap().get(
415: Constants.INC_SERVLET_PATH) == null) {
416: String[] componentLibs = JavascriptContext
417: .getIncludedLibs(context);
418: for (int i = 0; i < componentLibs.length; i++) {
419: String componentLib = componentLibs[i];
420: if (!libs.contains(componentLib)) {
421: libs.add(componentLib);
422: }
423: }
424: }
425:
426: Iterator iterator = libs.iterator();
427: while (iterator.hasNext()) {
428: String lib = (String) iterator.next();
429: Element script = (Element) head.appendChild(document
430: .createElement("script"));
431: script.setAttribute("language", "javascript");
432: script.setAttribute("src", handler.getResourceURL(context,
433: lib));
434: }
435:
436: String sessionIdentifier = context.getIceFacesId();
437: Element viewAndSessionScript = (Element) head
438: .appendChild(document.createElement("script"));
439: viewAndSessionScript.setAttribute("language", "javascript");
440: viewAndSessionScript.appendChild(document
441: .createTextNode("window.session = '"
442: + sessionIdentifier + "';"));
443: }
444:
445: private Element fixHtml() {
446: Element root = document.getDocumentElement();
447: Element html = document.createElement("html");
448: document.replaceChild(html, root);
449: html.appendChild(root);
450:
451: return html;
452: }
453:
454: private Element fixBody() {
455: Element html = document.getDocumentElement();
456: Element body = document.createElement("body");
457: NodeList children = html.getChildNodes();
458: int length = children.getLength();
459: Node[] nodes = new Node[length];
460: //copy the children first, since NodeList is live
461: for (int i = 0; i < nodes.length; i++)
462: nodes[i] = children.item(i);
463: for (int i = 0; i < nodes.length; i++) {
464: Node node = nodes[i];
465: if (!(node instanceof Element && "head"
466: .equals(((Element) node).getTagName())))
467: body.appendChild(node);
468: }
469: html.appendChild(body);
470:
471: return body;
472: }
473:
474: private Element fixHead() {
475: Element html = document.getDocumentElement();
476: Element head = document.createElement("head");
477: html.insertBefore(head, html.getFirstChild());
478:
479: return head;
480: }
481:
482: /**
483: * This method sets the write cursor for DOM modifications. Subsequent DOM
484: * modifications will take place below the cursor element.
485: *
486: * @param cursorParent parent node for subsequent modifications to the DOM
487: */
488: protected void setCursorParent(Node cursorParent) {
489: this .cursor = cursorParent;
490: }
491:
492: public static boolean isStreamWriting() {
493: return isStreamWritingFlag;
494: }
495:
496: private void moveCursorOn(Node node) {
497: if (log.isTraceEnabled()) {
498: log.trace("moving cursor on "
499: + DOMUtils.toDebugString(node));
500: }
501: cursor = node;
502: }
503:
504: private Node appendToCursor(Node node) {
505: try {
506: if (log.isTraceEnabled()) {
507: log.trace("Appending " + DOMUtils.toDebugString(node)
508: + " into " + DOMUtils.toDebugString(cursor));
509: }
510: return cursor.appendChild(node);
511: } catch (DOMException e) {
512: String message = "Failed to append "
513: + DOMUtils.toDebugString(node) + " into "
514: + DOMUtils.toDebugString(cursor);
515: log.error(message);
516: throw new RuntimeException(message, e);
517: }
518: }
519:
520: private Node appendToCursor(Attr node) {
521: try {
522: if (log.isTraceEnabled()) {
523: log.trace("Appending " + DOMUtils.toDebugString(node)
524: + " into " + DOMUtils.toDebugString(cursor));
525: }
526: return ((Element) cursor).setAttributeNode(node);
527: } catch (DOMException e) {
528: String message = "Failed to append "
529: + DOMUtils.toDebugString(node) + " into "
530: + DOMUtils.toDebugString(cursor);
531: log.error(message);
532: throw new RuntimeException(message, e);
533: } catch (ClassCastException e) {
534: String message = "The cursor is not an element: "
535: + DOMUtils.toDebugString(cursor);
536: log.error(message);
537: throw new RuntimeException(message, e);
538: }
539: }
540: }
|