001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2007 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041:
042: package org.netbeans.modules.visualweb.insync.action;
043:
044: import org.netbeans.modules.visualweb.api.designerapi.DesignerServiceHack;
045: import org.netbeans.modules.visualweb.insync.InSyncServiceProvider;
046: import org.netbeans.modules.visualweb.insync.models.FacesModel;
047: import java.io.BufferedInputStream;
048: import java.io.File;
049: import java.io.FileInputStream;
050: import java.io.FileOutputStream;
051: import java.io.FileWriter;
052: import java.io.IOException;
053: import java.io.InputStream;
054: import java.io.OutputStreamWriter;
055: import java.io.UnsupportedEncodingException;
056: import java.io.Writer;
057: import java.net.MalformedURLException;
058: import java.net.URI;
059: import java.net.URL;
060: import java.util.HashMap;
061: import java.util.Map;
062:
063: import org.openide.ErrorManager;
064: import org.openide.LifecycleManager;
065: import org.openide.awt.HtmlBrowser.URLDisplayer;
066: import org.openide.awt.StatusDisplayer;
067: import org.openide.filesystems.FileObject;
068: import org.openide.filesystems.FileUtil;
069: import org.openide.loaders.DataObject;
070: import org.openide.loaders.DataObjectNotFoundException;
071: import org.openide.util.NbBundle;
072: import org.w3c.dom.DocumentFragment;
073: import org.w3c.dom.Element;
074: import org.w3c.dom.Node;
075: import org.w3c.dom.NodeList;
076:
077: import org.netbeans.modules.visualweb.project.jsf.api.JsfProjectUtils;
078: import org.netbeans.modules.visualweb.insync.Util;
079: import org.netbeans.modules.visualweb.designer.html.HtmlAttribute;
080: import org.netbeans.modules.visualweb.designer.html.HtmlTag;
081: import org.netbeans.modules.visualweb.insync.markup.JspxSerializer;
082: import org.netbeans.modules.visualweb.insync.markup.MarkupUnit;
083:
084: // XXX Moved from designer.
085: /**
086: * Preview a webform in the browser
087: * @todo Rewrite to align more with box tree. I'm already depending on
088: * this for the jsp-include stuff.
089: * @todo "Escape" characters into entities! The DOM parser may take
090: * e.g.   and convert it into char 160; I need to undo that
091: * transformation when going back into html!
092: * @todo Handle attributes correctly; might possible share some code
093: * with the PrettyJspWriter in jsfsupport which has similar needs.
094: * @author Tor Norbye
095: */
096: // TODO Make package private when removed old outline impl.
097: public class BrowserPreview {
098: private static Map jarCache;
099: // private WebForm webform;
100: private FacesModel facesModel;
101:
102: public BrowserPreview(/*WebForm webform*/FacesModel facesModel) {
103: // this.webform = webform;
104: this .facesModel = facesModel;
105: }
106:
107: /** Preview the current document in the browser */
108: public void preview() {
109: // We should first force a save all, like execute project does,
110: // to ensure that for example referenced stylesheets that have been
111: // edited will be reflected correctly
112: LifecycleManager.getDefault().saveAll();
113:
114: // Dump out html content in a temporary file
115: File file = null;
116:
117: try {
118: // On the Mac at least, .xml will open some .xml editor,
119: // .xhtml opens IE instead of Safari... so for now stay with
120: // the .html extension even if that doesn't force the right
121: // xml processing mode on say Mozilla
122: //file = File.createTempFile("browserpreview", ".xml"); // NOI18N
123: file = File.createTempFile("browserpreview", ".html"); // NOI18N
124: file.deleteOnExit();
125:
126: Writer writer;
127:
128: try {
129: // String encoding = null;
130: // if (webform.getMarkup() != null) {
131: // encoding = webform.getMarkup().getEncoding();
132: // }
133: MarkupUnit markupUnit = facesModel.getMarkupUnit();
134: String encoding = markupUnit == null ? null
135: : markupUnit.getEncoding();
136:
137: if (encoding != null) {
138: writer = new OutputStreamWriter(
139: new FileOutputStream(file), encoding);
140: } else {
141: writer = new OutputStreamWriter(
142: new FileOutputStream(file));
143: }
144: } catch (UnsupportedEncodingException ue) {
145: writer = new FileWriter(file);
146: }
147:
148: // if (DesignerUtils.isBraveheartPage(webform.getDom())) {
149: // if (DesignerServiceHack.getDefault().isBraveheartPage(facesModel.getJspDom())) {
150: if (InSyncServiceProvider.get().isBraveheartPage(
151: facesModel.getJspDom())) {
152: // Braveheart pages already write out DOCTYPEs from the page component, but I've
153: // suppressed that in my own internal DOM. Emit it here.
154: // Regular (Reef) pages already have <jsp:text> nodes that should take
155: // care of this (but verify that)
156: writer
157: .write("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"\n"
158: + // NOI18N
159: "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n"); // NOI18N
160: }
161:
162: boolean fragment = false;
163:
164: try {
165: // TODO - handle page previewing here
166: // Element bodyElement = webform.getBody();
167: Element bodyElement = facesModel.getHtmlBody();
168: String bodyTag = null;
169:
170: if (bodyElement != null) {
171: bodyTag = bodyElement.getTagName();
172: }
173:
174: if ((bodyTag != null)
175: && !bodyTag.equals(HtmlTag.BODY.name)
176: && !bodyTag.equals(HtmlTag.FRAMESET.name)) {
177: fragment = true;
178:
179: // what about \r\n's etc?
180: writer.write("<html>\n"); // NOI18N
181: injectHeadStart(writer);
182:
183: // Write in theme links: fetch them from the context page
184: // WebForm page = webform.getContextPage();
185: FileObject contextFile = DesignerServiceHack
186: .getDefault()
187: .getContextFileForFragmentFile(
188: facesModel.getMarkupFile());
189: FacesModel page = contextFile == null ? null
190: : FacesModel.getInstance(contextFile);
191:
192: if (page != null) {
193: DocumentFragment df = page.getHtmlDomFragment();
194:
195: if (df != null) {
196: Node head = Util.findDescendant(
197: HtmlTag.HEAD.name, df);
198:
199: if (head != null) {
200: NodeList nl = head.getChildNodes();
201:
202: for (int i = 0, n = nl.getLength(); i < n; i++) {
203: Node node = nl.item(i);
204:
205: if (node.getNodeName().equals(
206: HtmlTag.SCRIPT.name)
207: || node
208: .getNodeName()
209: .equals(
210: HtmlTag.LINK.name)) {
211: dump(writer, node, 0);
212: }
213: }
214: }
215: }
216: }
217:
218: // writer.write("<title>" + webform.getMarkup().getFileObject().getNameExt() +
219: // "</title></head>\n<body>\n"); // NOI18N
220: writer.write("<title>"
221: + facesModel.getMarkupUnit()
222: .getFileObject().getNameExt()
223: + "</title></head>\n<body>\n"); // NOI18N
224: }
225: } catch (Exception e) {
226: e.printStackTrace();
227: }
228:
229: // Skip head etc.
230: // <removing set/getRoot from RaveDocument>
231: // dump(writer, webform.getDom().getRoot(), 0);
232: // ====
233: DocumentFragment html = facesModel.getHtmlDomFragment();
234: if (html == null) {
235: // XXX #6469774 NPE.
236: ErrorManager.getDefault().notify(
237: ErrorManager.INFORMATIONAL,
238: new NullPointerException(
239: "Null Html Dom Fragment from FacesModel, it is probably invalid, facesModel="
240: + facesModel)); // NOI18N
241: return;
242: }
243:
244: Node effectiveRoot = null;
245: NodeList nl = html.getChildNodes();
246: for (int i = 0, n = nl.getLength(); i < n; i++) {
247: Node node = nl.item(i);
248: if (node.getNodeType() == Node.ELEMENT_NODE) {
249: effectiveRoot = node;
250: break;
251: }
252: }
253: dump(writer, effectiveRoot, 0);
254: // <removing set/getRoot from RaveDocument>
255:
256: if (fragment) {
257: writer.write("</body>\n</html>\n"); // NOI18N
258: }
259:
260: writer.close();
261: } catch (IOException ex) {
262: ErrorManager.getDefault().notify(ex);
263:
264: return;
265: }
266:
267: // Show in Browser
268: URL url = null;
269:
270: try {
271: // TODO what about inserting a / infront of the C:\ in paths
272: // etc?? See MarkupUnit.getBase() - why does this work?
273: // and what about calling MarkupUnit.toURL here to escape
274: // \'s etc.?
275: url = new URL("file:" + file.getPath()); // NOI18N
276: } catch (MalformedURLException e) {
277: // Can't show URL
278: ErrorManager.getDefault().notify(e);
279:
280: return;
281: }
282:
283: URLDisplayer.getDefault().showURL(url);
284:
285: // TODO -- delete OTHER existing/old browser preview files in the
286: // same directory? Or just record the new file name and stash
287: // it in a to-be-deleted list somewhere?
288: }
289:
290: /** Dump out the DOM to the given writer, skipping whitespace
291: * and comments if it feels like it.
292: * @todo Unhackify.
293: */
294: private void dump(Writer writer, Node n, int depth)
295: throws IOException {
296: String close = null;
297:
298: if (n.getNodeType() != Node.ATTRIBUTE_NODE) {
299: if (n.getNodeType() == Node.ELEMENT_NODE) {
300: Element element = (Element) n;
301:
302: String tagName = element.getTagName();
303:
304: FileObject markupFile = facesModel.getMarkupFile();
305: DataObject dob;
306: try {
307: dob = DataObject.find(markupFile);
308: } catch (DataObjectNotFoundException e) {
309: ErrorManager.getDefault().notify(
310: ErrorManager.INFORMATIONAL, e);
311: dob = null;
312: }
313: // if (HtmlTag.HEAD.name.equals(tagName) && (webform.getDataObject() != null)) {
314: if (HtmlTag.HEAD.name.equals(tagName) && (dob != null)) {
315:
316: injectHeadStart(writer);
317: close = HtmlTag.HEAD.name;
318: } else if (HtmlTag.BASE.name.equals(tagName)) {
319: // We've put a <base> tag into the head already
320: return;
321: } else if (HtmlTag.JSPINCLUDE.name.equals(tagName)
322: || HtmlTag.JSPINCLUDEX.name.equals(tagName)) {
323: // CssBox includeBox = CssBox.getBox(element);
324: //
325: // if ((includeBox != null) && includeBox instanceof JspIncludeBox) {
326: // WebForm frameForm = ((JspIncludeBox)includeBox).getExternalForm();
327: //
328: // if ((frameForm != null) && (frameForm != WebForm.EXTERNAL)) {
329: // // Recurse
330: // Element body = frameForm.getBody();
331: // dump(writer, body, depth + 1);
332: // }
333: // }
334: FileObject externalFormFile = DesignerServiceHack
335: .getDefault()
336: .getExternalFormFileForElement(element);
337: FacesModel frameForm = externalFormFile == null ? null
338: : FacesModel.getInstance(externalFormFile);
339: if (frameForm != null) {
340: Element body = frameForm.getHtmlBody();
341: dump(writer, body, depth + 1);
342: }
343: } else if (tagName.equals(HtmlTag.FSUBVIEW)) { // NOI18N
344:
345: // Skip
346: } else if (tagName.equals("f:view")) { // NOI18N
347:
348: // Skip
349: } else if (!tagName.startsWith("jsp:")) { // NOI18N // Skip meta stuff
350: writer.write('<');
351: writer.write(tagName);
352:
353: // Later, if we use class="jsferror" instead of inlining styles
354: // in the mock container, we need to insert the styles here
355: //boolean fixStyle = false;
356: //boolean processed = false;
357: //if (element.getAttribute("class").equals("jsferror")) {
358: // fixStyle = true;
359: //}
360: int num = element.getAttributes().getLength();
361:
362: for (int i = 0; i < num; i++) {
363: Node a = element.getAttributes().item(i); // XXX move element.getAttributes out of loop
364: writer.write(' ');
365:
366: String name = a.getNodeName();
367: writer.write(name);
368: writer.write('=');
369: writer.write('"');
370:
371: //if (fixStyle && a.getNodeName().equals("style")) {
372: // writer.write(a.getNodeValue()); // XXX TODO: escape "'s, &'s, etc.
373: // writer.write(';');
374: // writer.write(getErrorCss());
375: // processed = true;
376: //} else {
377: //writer.write(a.getNodeValue()); // XXX TODO: escape "'s, &'s, etc.
378: String val = a.getNodeValue();
379:
380: if (name.equals(HtmlAttribute.SRC)
381: || name.equals(HtmlAttribute.HREF)) {
382: if (val.startsWith("jar:file:")) { // NOI18N
383:
384: // We have to translate jar urls because
385: // some browsers like IE and Safari don't
386: // understand them
387: val = translateJarUrl(val);
388: } else if (val.startsWith("/")) {
389: // Context relative path - just strip it and rely
390: // on base tag
391: val = val.substring(1);
392: }
393: }
394:
395: for (int j = 0, m = val.length(); j < m; j++) {
396: char c = val.charAt(j);
397:
398: switch (c) {
399: case '"':
400: writer.write(""");
401:
402: break; // NOI18N
403:
404: /* Don't escape '. It doesn't seem to be
405: necessary - the XML parser will not be confused
406: by an apostrophy in the middle of a quote;
407: and more importantly translating these to
408: ' will break javascript attributes
409: (like onmouseout) on Explorer. Mozilla handles
410: these correctly.
411: case '\'': writer.write("'"); break; // NOI18N
412: */
413: case '<':
414: writer.write("<");
415:
416: break; // NOI18N
417:
418: case '>':
419: writer.write(">");
420:
421: break; // NOI18N
422:
423: case '&':
424: writer.write("&");
425:
426: break; // NOI18N
427:
428: default:
429: writer.write(c);
430:
431: break;
432: }
433: }
434:
435: //}
436: writer.write('"');
437: }
438:
439: //if (fixStyle && !processed) { // Still haven't inserted the jsferror stuff
440: // writer.write(" style=\"");
441: // writer.write(getErrorCss()); // look up CSS string for class=jsferror
442: // writer.write('"');
443: //}
444: boolean closeImmediately = JspxSerializer
445: .canMinimizeTag(tagName);
446:
447: if (closeImmediately) {
448: writer.write(' ');
449: writer.write('/');
450: }
451:
452: writer.write('>');
453: close = tagName;
454:
455: if (closeImmediately) {
456: close = null;
457: }
458: }
459: } else if (n.getNodeType() == Node.TEXT_NODE) {
460: String str = n.getNodeValue();
461:
462: // We don't want to strip spaces
463: //if (!Utilities.onlyWhitespace(str)) {
464: //
465: boolean windows = org.openide.util.Utilities
466: .isWindows();
467:
468: for (int i = 0, max = str.length(); i < max; i++) {
469: char c = str.charAt(i);
470:
471: if (windows && (c == '\n')) {
472: // On Windows? - fix newline issue so View
473: // Source looks okay Change \n's into \r\n -
474: // unless we already have \r's in there.
475: writer.write('\n');
476: writer.write('\r');
477: } else if (c == '<') {
478: writer.write("<"); // NOI18N
479: } else if (c == '>') {
480: writer.write(">"); // NOI18N
481:
482: // XXX should we also change & -> &, " -> " and ' -> ' ??
483: } else {
484: writer.write(c);
485: }
486: }
487:
488: //}
489: } else if (n.getNodeType() == Node.CDATA_SECTION_NODE) {
490: writer.write(n.getNodeValue()); // TODO Windows \r\n handling??
491: } else if (n.getNodeType() == Node.COMMENT_NODE) {
492: // Needed at browser preview for components which emit
493: // script tags for example with comments embedded for
494: // browser compatibility
495: writer.write("<!--"); // NOI18N
496: writer.write(n.getNodeValue()); // TODO Windows \r\n handling??
497: writer.write("-->"); // NOI18N
498: }
499:
500: // else {
501: // //System.out.println("Skipping node " + n);
502: // }
503: }
504:
505: if (n.hasChildNodes()) {
506: NodeList list = n.getChildNodes();
507: int len = list.getLength();
508:
509: for (int i = 0; i < len; i++) {
510: dump(writer, list.item(i), depth + 1);
511: }
512: }
513:
514: if (close != null) {
515: writer.write('<');
516: writer.write('/');
517: writer.write(close);
518: writer.write('>');
519: }
520: }
521:
522: /** Given a JAR URL (jar:file:/foo/bar/baz.jar!/foo/bar/baz.css)
523: * translate it to a file in the userdir's cache directory, and
524: * return a URL pointing to this new file. This enables browsers that
525: * don't support the jar: url (which means pretty much everybody except
526: * Firefox/Mozilla) to view the preview contents.
527: *
528: * The jars are cached such that they only are extracted when a cached copy
529: * is not found.
530: *
531: * @todo Figure out if we need to refresh this copy occasionally.
532: *
533: * @param A jar-syntax url
534: * @return A file url, or if parsing the jar url fails (or extracting the file
535: * fails) the original url.
536: */
537: private String translateJarUrl(String jarUrl) {
538: URL source = null;
539:
540: try {
541: source = new URL(jarUrl);
542: } catch (MalformedURLException mfue) {
543: ErrorManager.getDefault().notify(mfue);
544:
545: return jarUrl;
546: }
547:
548: if (jarCache == null) {
549: jarCache = new HashMap();
550: }
551:
552: String s = source.getFile();
553:
554: // Decode %20's etc.
555: // <markup_separation>
556: // s = MarkupUnit.fromURL(s);
557: // ====
558: s = InSyncServiceProvider.get().fromURL(s);
559: // </markup_separation>
560:
561: if (s.startsWith("file:")) {
562: s = s.substring(5);
563: }
564:
565: int bang = s.indexOf('!');
566:
567: if (bang == -1) {
568: return jarUrl;
569: }
570:
571: String jar = s.substring(0, bang);
572: String file = s.substring(bang + 2); // skip !, and skip first /
573:
574: String dirName = jar.substring(jar.lastIndexOf('/'));
575: int n = dirName.length();
576: StringBuffer sb = new StringBuffer(n);
577:
578: for (int i = 0; i < n; i++) {
579: char c = dirName.charAt(i);
580:
581: if (Character.isLetterOrDigit(c)) {
582: sb.append(c);
583: }
584: }
585:
586: if (sb.length() == 0) {
587: return jarUrl;
588: }
589:
590: // TODO Should I extract the version number too and put that on the dir?
591: dirName = sb.toString();
592:
593: String cacheDir = (String) jarCache.get(dirName);
594:
595: if (cacheDir == null) {
596: cacheDir = extract(jar, dirName);
597:
598: if (cacheDir == null) { // IO error extracting jar
599:
600: return jarUrl;
601: }
602:
603: jarCache.put(dirName, cacheDir);
604: }
605:
606: try {
607: return new File(cacheDir, file).toURI().toURL()
608: .toExternalForm();
609: } catch (MalformedURLException mue) {
610: return jarUrl;
611: }
612: }
613:
614: /**
615: * Given a jar file and a dir name extract the jar file into the dirname in
616: * the cache directory
617: */
618: private String extract(String jar, String dirname) {
619: String cacheDir = System.getProperty("netbeans.user")
620: + "/var/cache/" + dirname;
621:
622: try {
623: File f = new File(cacheDir);
624:
625: if (f.exists()) {
626: // Cache already exists. Assume current. Perhaps we should look
627: // at the time and occasionally regenerate - or is there a way to
628: // look at the checksum of the jar perhaps?
629: return cacheDir;
630: }
631:
632: String message = NbBundle.getMessage(BrowserPreview.class,
633: "ExtractingJars");
634: StatusDisplayer.getDefault().setStatusText(message);
635:
636: f.mkdirs();
637:
638: FileObject fo = FileUtil.toFileObject(f);
639: InputStream is = new BufferedInputStream(
640: new FileInputStream(new File(jar)));
641: FileUtil.extractJar(fo, is);
642:
643: return cacheDir;
644: } catch (Exception e) {
645: ErrorManager.getDefault().notify(e);
646:
647: return null;
648: } finally {
649: StatusDisplayer.getDefault().setStatusText("");
650: }
651: }
652:
653: private void injectHeadStart(Writer writer) throws IOException {
654: // Inject a base to make urls work okay
655: writer.write('<');
656: writer.write(HtmlTag.HEAD.name);
657: writer.write('>');
658:
659: //URI uri = webform.getMarkup().getBaseURI();
660: // The components seem to render project-relative paths (/resources/foo.gif becomes resources/foo.gif)
661: // so I should use the project as the base!
662: // FileObject webroot = JsfProjectUtils.getDocumentRoot(webform.getProject());
663: FileObject webroot = JsfProjectUtils.getDocumentRoot(facesModel
664: .getProject());
665:
666: URI uri = FileUtil.toFile(webroot).toURI();
667:
668: if (uri != null) {
669: writer.write("<base href=\""); // NOI18N
670: writer.write(uri.toASCIIString());
671: writer.write("\" />\n"); // NOI18N
672:
673: // Set up encoding
674: String content = getContentType();
675: writer
676: .write("<meta http-equiv=\"Content-Type\" content=\""); // NOI18N
677: writer.write(content);
678: writer.write("\"/>\n"); // NOI18N
679: }
680: }
681:
682: private String getContentType() {
683: String content = "text/html;charset=UTF-8"; // NOI18N
684:
685: // Look for the jsp directive
686: // Element root = webform.getDom().getDocumentElement();
687: Element root = facesModel.getJspDom().getDocumentElement();
688:
689: Element e = MarkupUnit.getFirstDescendantElement(root,
690: "jsp:directive.page"); // NOI18N
691:
692: if ((e != null) && e.hasAttribute("contentType")) { // NOI18N
693: content = e.getAttribute("contentType"); // NOI18N
694: }
695:
696: return content;
697: }
698: }
|