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-2006 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.html;
043:
044: import java.io.BufferedInputStream;
045: import java.io.IOException;
046: import java.io.ObjectInput;
047: import java.io.OutputStream;
048: import java.io.OutputStreamWriter;
049: import java.io.Writer;
050: import java.nio.ByteBuffer;
051: import java.nio.charset.CharacterCodingException;
052: import java.nio.charset.Charset;
053: import java.nio.charset.CharsetDecoder;
054: import java.nio.charset.CharsetEncoder;
055: import java.nio.charset.CodingErrorAction;
056: import java.util.logging.Level;
057: import java.util.logging.Logger;
058: import javax.swing.text.EditorKit;
059: import javax.swing.text.StyledDocument;
060: import javax.swing.text.BadLocationException;
061: import org.netbeans.api.queries.FileEncodingQuery;
062: import org.netbeans.modules.html.palette.HTMLPaletteFactory;
063: import org.netbeans.spi.palette.PaletteController;
064: import org.openide.DialogDisplayer;
065: import org.openide.NotifyDescriptor;
066: import org.openide.NotifyDescriptor;
067: import org.openide.cookies.EditCookie;
068: import org.openide.cookies.EditorCookie;
069: import org.openide.cookies.OpenCookie;
070: import org.openide.cookies.PrintCookie;
071: import org.openide.cookies.SaveCookie;
072: import org.openide.filesystems.FileObject;
073: import org.openide.filesystems.FileLock;
074: import org.openide.loaders.DataObject;
075: import org.openide.nodes.Node;
076: import org.openide.nodes.Node.Cookie;
077: import org.openide.text.CloneableEditor;
078: import org.openide.text.DataEditorSupport;
079: import org.openide.util.Exceptions;
080: import org.openide.util.Exceptions;
081: import org.openide.util.Lookup;
082: import org.openide.util.NbBundle;
083: import org.openide.util.UserCancelException;
084: import org.openide.util.lookup.AbstractLookup;
085: import org.openide.util.lookup.InstanceContent;
086: import org.openide.util.lookup.ProxyLookup;
087: import org.openide.windows.CloneableOpenSupport;
088:
089: /**
090: * Editor support for HTML data objects.
091: *
092: * @author Radim Kubacki
093: * @author Marek Fukala
094: *
095: * @see org.openide.text.DataEditorSupportH
096: */
097: public final class HtmlEditorSupport extends DataEditorSupport
098: implements OpenCookie, EditCookie, EditorCookie.Observable,
099: PrintCookie {
100:
101: private static final String DOCUMENT_SAVE_ENCODING = "Document_Save_Encoding";
102: private static final String UTF_8_ENCODING = "UTF-8";
103:
104: /** SaveCookie for this support instance. The cookie is adding/removing
105: * data object's cookie set depending on if modification flag was set/unset.
106: * It also invokes beforeSave() method on the HtmlDataObject to give it
107: * a chance to eg. reflect changes in 'charset' attribute
108: * */
109:
110: private final SaveCookie saveCookie = new SaveCookie() {
111:
112: /** Implements <code>SaveCookie</code> interface. */
113: public void save() throws IOException {
114: try {
115: saveDocument();
116: } catch (UserCancelException uce) {
117: //just ignore
118: }
119: }
120: };
121:
122: /** Constructor. */
123: HtmlEditorSupport(HtmlDataObject obj) {
124: super (obj, new Environment(obj));
125:
126: setMIMEType("text/html"); // NOI18N
127: }
128:
129: @Override
130: public void saveDocument() throws IOException {
131: //try to find encoding specification in the editor content
132: String documentContent = getDocumentText();
133: String encoding = HtmlDataObject.findEncoding(documentContent);
134: String feqEncoding = FileEncodingQuery.getEncoding(
135: getDataObject().getPrimaryFile()).name();
136: String finalEncoding = null;
137: if (encoding != null) {
138: //found encoding specified in the file content by meta tag
139: if (!isSupportedEncoding(encoding)
140: || !canEncode(documentContent, encoding)) {
141: //test if the file can be saved by the original encoding or if it needs to be saved using utf-8
142: finalEncoding = canEncode(documentContent, feqEncoding) ? feqEncoding
143: : UTF_8_ENCODING;
144: NotifyDescriptor nd = new NotifyDescriptor.Confirmation(
145: NbBundle
146: .getMessage(
147: HtmlEditorSupport.class,
148: "MSG_unsupportedEncodingSave",
149: new Object[] {
150: getDataObject()
151: .getPrimaryFile()
152: .getNameExt(),
153: encoding,
154: finalEncoding,
155: finalEncoding
156: .equals(UTF_8_ENCODING) ? ""
157: : " the original" }),
158: NotifyDescriptor.YES_NO_OPTION,
159: NotifyDescriptor.WARNING_MESSAGE);
160: nd.setValue(NotifyDescriptor.NO_OPTION);
161: DialogDisplayer.getDefault().notify(nd);
162: if (nd.getValue() != NotifyDescriptor.YES_OPTION) {
163: throw new UserCancelException();
164: }
165: } else {
166: finalEncoding = encoding;
167: }
168: } else {
169: //no encoding specified in the file, use FEQ value
170: if (!canEncode(documentContent, feqEncoding)) {
171: NotifyDescriptor nd = new NotifyDescriptor.Confirmation(
172: NbBundle.getMessage(HtmlEditorSupport.class,
173: "MSG_badCharConversionSave",
174: new Object[] {
175: getDataObject()
176: .getPrimaryFile()
177: .getNameExt(),
178: feqEncoding }),
179: NotifyDescriptor.YES_NO_OPTION,
180: NotifyDescriptor.WARNING_MESSAGE);
181: nd.setValue(NotifyDescriptor.NO_OPTION);
182: DialogDisplayer.getDefault().notify(nd);
183: if (nd.getValue() != NotifyDescriptor.YES_OPTION) {
184: throw new UserCancelException();
185: } else {
186: finalEncoding = UTF_8_ENCODING;
187: }
188: } else {
189: finalEncoding = feqEncoding;
190: }
191: }
192:
193: //FEQ cannot be run in saveFromKitToStream since document is locked for writing,
194: //so setting the FEQ result to document property
195: getDocument()
196: .putProperty(DOCUMENT_SAVE_ENCODING, finalEncoding);
197:
198: super .saveDocument();
199: HtmlEditorSupport.this .getDataObject().setModified(false);
200: }
201:
202: @Override
203: public void open() {
204: String encoding = ((HtmlDataObject) getDataObject())
205: .getFileEncoding();
206: String feqEncoding = FileEncodingQuery.getEncoding(
207: getDataObject().getPrimaryFile()).name();
208: if (encoding != null && !isSupportedEncoding(encoding)) {
209: // if(!canDecodeFile(getDataObject().getPrimaryFile(), feqEncoding)) {
210: // feqEncoding = UTF_8_ENCODING;
211: // }
212: NotifyDescriptor nd = new NotifyDescriptor.Confirmation(
213: NbBundle.getMessage(HtmlEditorSupport.class,
214: "MSG_unsupportedEncodingLoad", //NOI18N
215: new Object[] {
216: getDataObject().getPrimaryFile()
217: .getNameExt(), encoding,
218: feqEncoding }),
219: NotifyDescriptor.YES_NO_OPTION,
220: NotifyDescriptor.WARNING_MESSAGE);
221: DialogDisplayer.getDefault().notify(nd);
222: if (nd.getValue() != NotifyDescriptor.YES_OPTION) {
223: return; // do not open the file
224: }
225: }
226:
227: // if(!canDecodeFile(getDataObject().getPrimaryFile(), feqEncoding)) {
228: // feqEncoding = UTF_8_ENCODING;
229: // }
230:
231: super .open();
232: }
233:
234: /**
235: * @inheritDoc
236: */
237: @Override
238: protected void saveFromKitToStream(StyledDocument doc,
239: EditorKit kit, OutputStream stream) throws IOException,
240: BadLocationException {
241: final Charset c = Charset.forName((String) doc
242: .getProperty(DOCUMENT_SAVE_ENCODING));
243: final Writer w = new OutputStreamWriter(stream, c);
244: try {
245: kit.write(w, doc, 0, doc.getLength());
246: } finally {
247: w.close();
248: }
249: }
250:
251: /**
252: * Overrides superclass method. Adds adding of save cookie if the document has been marked modified.
253: * @return true if the environment accepted being marked as modified
254: * or false if it has refused and the document should remain unmodified
255: */
256: protected boolean notifyModified() {
257: if (!super .notifyModified())
258: return false;
259:
260: addSaveCookie();
261:
262: return true;
263: }
264:
265: /** Overrides superclass method. Adds removing of save cookie. */
266: protected void notifyUnmodified() {
267: super .notifyUnmodified();
268:
269: removeSaveCookie();
270: }
271:
272: /** Helper method. Adds save cookie to the data object. */
273: private void addSaveCookie() {
274: HtmlDataObject obj = (HtmlDataObject) getDataObject();
275:
276: // Adds save cookie to the data object.
277: if (obj.getCookie(SaveCookie.class) == null) {
278: obj.getCookieSet0().add(saveCookie);
279: obj.setModified(true);
280: }
281: }
282:
283: /** Helper method. Removes save cookie from the data object. */
284: private void removeSaveCookie() {
285: HtmlDataObject obj = (HtmlDataObject) getDataObject();
286:
287: // Remove save cookie from the data object.
288: Cookie cookie = obj.getCookie(SaveCookie.class);
289:
290: if (cookie != null && cookie.equals(saveCookie)) {
291: obj.getCookieSet0().remove(saveCookie);
292: obj.setModified(false);
293: }
294: }
295:
296: private String getDocumentText() {
297: String text = "";
298: try {
299: StyledDocument doc = getDocument();
300: if (doc != null) {
301: text = doc.getText(doc.getStartPosition().getOffset(),
302: doc.getLength());
303: }
304: } catch (BadLocationException e) {
305: Logger.getLogger("global").log(Level.WARNING, null, e);
306: }
307: return text;
308: }
309:
310: private boolean canDecodeFile(FileObject fo, String encoding) {
311: CharsetDecoder decoder = Charset.forName(encoding).newDecoder()
312: .onUnmappableCharacter(CodingErrorAction.REPORT)
313: .onMalformedInput(CodingErrorAction.REPORT);
314: try {
315: BufferedInputStream bis = new BufferedInputStream(fo
316: .getInputStream());
317: //I probably have to create such big buffer since I am not sure
318: //how to cut the file to smaller byte arrays so it cannot happen
319: //that an encoded character is divided by the arrays border.
320: //In such case it might happen that the method woult return
321: //incorrect value.
322: byte[] buffer = new byte[(int) fo.getSize()];
323: bis.read(buffer);
324: bis.close();
325: decoder.decode(ByteBuffer.wrap(buffer));
326: return true;
327: } catch (CharacterCodingException ex) {
328: //return false
329: } catch (IOException ioe) {
330: Logger.getLogger("global").log(Level.WARNING,
331: "Error during charset verification", ioe);
332: }
333: return false;
334: }
335:
336: private boolean canEncode(String docText, String encoding) {
337: CharsetEncoder encoder = Charset.forName(encoding).newEncoder();
338: return encoder.canEncode(docText);
339: }
340:
341: private boolean isSupportedEncoding(String encoding) {
342: boolean supported;
343: try {
344: supported = java.nio.charset.Charset.isSupported(encoding);
345: } catch (java.nio.charset.IllegalCharsetNameException e) {
346: supported = false;
347: }
348: return supported;
349: }
350:
351: /** Nested class. Environment for this support. Extends <code>DataEditorSupport.Env</code> abstract class. */
352: private static class Environment extends DataEditorSupport.Env {
353:
354: private static final long serialVersionUID = 3035543168452715818L;
355:
356: /** Constructor. */
357: public Environment(HtmlDataObject obj) {
358: super (obj);
359: }
360:
361: /** Implements abstract superclass method. */
362: protected FileObject getFile() {
363: return getDataObject().getPrimaryFile();
364: }
365:
366: /** Implements abstract superclass method.*/
367: protected FileLock takeLock() throws IOException {
368: return ((HtmlDataObject) getDataObject()).getPrimaryEntry()
369: .takeLock();
370: }
371:
372: /**
373: * Overrides superclass method.
374: * @return text editor support (instance of enclosing class)
375: */
376: public CloneableOpenSupport findCloneableOpenSupport() {
377: return (HtmlEditorSupport) getDataObject().getCookie(
378: HtmlEditorSupport.class);
379: }
380: } // End of nested Environment class.
381:
382: /** A method to create a new component. Overridden in subclasses.
383: * @return the {@link HtmlEditor} for this support
384: */
385: protected CloneableEditor createCloneableEditor() {
386: return new HtmlEditor(this );
387: }
388:
389: public static class HtmlEditor extends CloneableEditor {
390:
391: public HtmlEditor() {
392: }
393:
394: void associatePalette(HtmlEditorSupport s) {
395:
396: Node nodes[] = { s.getDataObject().getNodeDelegate() };
397: InstanceContent instanceContent = new InstanceContent();
398: associateLookup(new ProxyLookup(new Lookup[] {
399: new AbstractLookup(instanceContent),
400: nodes[0].getLookup() }));
401: instanceContent.add(getActionMap());
402:
403: setActivatedNodes(nodes);
404:
405: DataObject dataObject = s.getDataObject();
406: if (dataObject instanceof HtmlDataObject) {
407: try {
408: PaletteController pc = HTMLPaletteFactory
409: .getPalette();
410: instanceContent.add(pc);
411: } catch (IOException ioe) {
412: //TODO exception handling
413: ioe.printStackTrace();
414: }
415: }
416: }
417:
418: /** Creates new editor */
419: public HtmlEditor(HtmlEditorSupport s) {
420: super (s);
421: initialize();
422: }
423:
424: private void initialize() {
425: associatePalette((HtmlEditorSupport) cloneableEditorSupport());
426: }
427:
428: public void readExternal(ObjectInput in) throws IOException,
429: ClassNotFoundException {
430: super.readExternal(in);
431: initialize();
432: }
433:
434: }
435:
436: }
|