blob: bb4762764d0095684eb19534c4cdbb96da94c1e2 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.modules.html;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CodingErrorAction;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.EditorKit;
import javax.swing.text.StyledDocument;
import org.netbeans.api.queries.FileEncodingQuery;
import org.netbeans.core.api.multiview.MultiViews;
import org.netbeans.modules.html.api.HtmlDataNode;
import org.openide.cookies.EditCookie;
import org.openide.cookies.EditorCookie;
import org.openide.cookies.OpenCookie;
import org.openide.cookies.PrintCookie;
import org.openide.cookies.SaveCookie;
import org.openide.filesystems.FileLock;
import org.openide.filesystems.FileObject;
import org.openide.loaders.DataObject;
import org.openide.nodes.Node.Cookie;
import org.openide.text.CloneableEditorSupport;
import org.openide.text.DataEditorSupport;
import org.openide.util.NbBundle;
import org.openide.util.UserCancelException;
import org.openide.util.UserQuestionException;
import org.openide.windows.CloneableOpenSupport;
/**
* Editor support for HTML data objects.
*
* @author Radim Kubacki
* @author Marek Fukala
*
* @see org.openide.text.DataEditorSupport
*/
public final class HtmlEditorSupport extends DataEditorSupport implements OpenCookie,
EditCookie, EditorCookie.Observable, PrintCookie {
private static final String DOCUMENT_SAVE_ENCODING = "Document_Save_Encoding";
private static final String UTF_8_ENCODING = "UTF-8";
/**
* SaveCookie for this support instance. The cookie is adding/removing data
* object's cookie set depending on if modification flag was set/unset. It
* also invokes beforeSave() method on the HtmlDataObject to give it a
* chance to eg. reflect changes in 'charset' attribute
*
*/
private final SaveCookie saveCookie = new SaveCookie() {
/**
* Implements
* <code>SaveCookie</code> interface.
*/
@Override
public void save() throws IOException {
try {
saveDocument();
} catch (UserCancelException uce) {
//just ignore
}
}
@Override
public String toString() {
return getDataObject().getPrimaryFile().getNameExt();
}
};
HtmlEditorSupport(HtmlDataObject obj) {
super(obj, null, new Environment(obj));
setMIMEType(getDataObject().getPrimaryFile().getMIMEType());
}
@Override
protected boolean close(boolean ask) {
boolean closed = super.close(ask);
DataObject dobj = getDataObject();
if(closed && dobj.isValid()) {
//set the original property sets
HtmlDataNode nodeDelegate = (HtmlDataNode)dobj.getNodeDelegate();
nodeDelegate.setPropertySets(null);
}
return closed;
}
@Override
protected boolean asynchronousOpen() {
return true;
}
@Override
protected Pane createPane() {
return (CloneableEditorSupport.Pane) MultiViews.createCloneableMultiView(HtmlLoader.HTML_MIMETYPE, getDataObject());
}
@Override
public void saveDocument() throws IOException {
updateEncoding();
super.saveDocument();
}
void updateEncoding() throws UserQuestionException {
//try to find encoding specification in the editor content
String documentContent = getDocumentText();
String encoding = HtmlDataObject.findEncoding(documentContent);
String feqEncoding = FileEncodingQuery.getEncoding(getDataObject().getPrimaryFile()).name();
if (encoding != null) {
//found encoding specified in the file content by meta tag
if (!isSupportedEncoding(encoding) || !canEncode(documentContent, encoding)) {
//test if the file can be saved by the original encoding or if it needs to be saved using utf-8
final String defaultEncoding = canEncode(documentContent, feqEncoding) ? feqEncoding : UTF_8_ENCODING;
String msg = NbBundle.getMessage(
HtmlEditorSupport.class,
"MSG_unsupportedEncodingSave",
new Object[]{
getDataObject().getPrimaryFile().getNameExt(),
encoding,
defaultEncoding,
defaultEncoding.equals(UTF_8_ENCODING) ? "" : " the original"}
);
throw new UserQuestionException(msg) {
@Override
public void confirmed() throws IOException {
setEncodingProperty(defaultEncoding);
HtmlEditorSupport.super.saveDocument();
}
};
} else {
setEncodingProperty(encoding);
}
} else {
//no encoding specified in the file, use FEQ value
if (!canEncode(documentContent, feqEncoding)) {
String msg = NbBundle.getMessage(
HtmlEditorSupport.class,
"MSG_badCharConversionSave",
new Object[]{getDataObject().getPrimaryFile().getNameExt(), feqEncoding});
throw new UserQuestionException(msg) {
@Override
public void confirmed() throws IOException {
setEncodingProperty(UTF_8_ENCODING);
HtmlEditorSupport.super.saveDocument();
}
};
} else {
setEncodingProperty(feqEncoding);
}
}
}
private void setEncodingProperty(String encoding) {
//FEQ cannot be run in saveFromKitToStream since document is locked for writing,
//so setting the FEQ result to document property
Document doc = getDocument();
//the document is already loaded so getDocument() should normally return
//no null value, but if a CES redirector returns null from the redirected
//CES.getDocument() then we are not able to set the found encoding
if (doc != null) {
doc.putProperty(DOCUMENT_SAVE_ENCODING, encoding);
}
}
/**
* @inheritDoc
*/
@Override
protected void saveFromKitToStream(StyledDocument doc, EditorKit kit, OutputStream stream) throws IOException, BadLocationException {
String foundEncoding = (String) doc.getProperty(DOCUMENT_SAVE_ENCODING);
String usedEncoding = foundEncoding != null ? foundEncoding : UTF_8_ENCODING;
final Charset c = Charset.forName(usedEncoding);
final Writer w = new OutputStreamWriter(stream, c);
try {
kit.write(w, doc, 0, doc.getLength());
} finally {
w.close();
}
}
/**
* Overrides superclass method. Adds adding of save cookie if the document
* has been marked modified.
*
* @return true if the environment accepted being marked as modified or
* false if it has refused and the document should remain unmodified
*/
@Override
protected boolean notifyModified() {
if (!super.notifyModified()) {
return false;
}
addSaveCookie();
return true;
}
/**
* Overrides superclass method. Adds removing of save cookie.
*/
@Override
protected void notifyUnmodified() {
super.notifyUnmodified();
removeSaveCookie();
}
/**
* Helper method. Adds save cookie to the data object.
*/
private void addSaveCookie() {
HtmlDataObject obj = (HtmlDataObject) getDataObject();
// Adds save cookie to the data object.
if (obj.getCookie(SaveCookie.class) == null) {
obj.getCookieSet0().add(saveCookie);
obj.setModified(true);
}
}
/**
* Helper method. Removes save cookie from the data object.
*/
void removeSaveCookie() {
HtmlDataObject obj = (HtmlDataObject) getDataObject();
// Remove save cookie from the data object.
Cookie cookie = obj.getCookie(SaveCookie.class);
if (cookie != null && cookie.equals(saveCookie)) {
obj.getCookieSet0().remove(saveCookie);
obj.setModified(false);
}
}
private String getDocumentText() {
String text = "";
try {
StyledDocument doc = getDocument();
if (doc != null) {
text = doc.getText(doc.getStartPosition().getOffset(), doc.getLength());
}
} catch (BadLocationException e) {
Logger.getLogger("global").log(Level.WARNING, null, e);
}
return text;
}
private boolean canDecodeFile(FileObject fo, String encoding) {
CharsetDecoder decoder = Charset.forName(encoding).newDecoder().onUnmappableCharacter(CodingErrorAction.REPORT).onMalformedInput(CodingErrorAction.REPORT);
try {
BufferedInputStream bis = new BufferedInputStream(fo.getInputStream());
//I probably have to create such big buffer since I am not sure
//how to cut the file to smaller byte arrays so it cannot happen
//that an encoded character is divided by the arrays border.
//In such case it might happen that the method woult return
//incorrect value.
byte[] buffer = new byte[(int) fo.getSize()];
bis.read(buffer);
bis.close();
decoder.decode(ByteBuffer.wrap(buffer));
return true;
} catch (CharacterCodingException ex) {
//return false
} catch (IOException ioe) {
Logger.getLogger("global").log(Level.WARNING, "Error during charset verification", ioe);
}
return false;
}
private boolean canEncode(String docText, String encoding) {
CharsetEncoder encoder = Charset.forName(encoding).newEncoder();
return encoder.canEncode(docText);
}
private boolean isSupportedEncoding(String encoding) {
boolean supported;
try {
supported = java.nio.charset.Charset.isSupported(encoding);
} catch (java.nio.charset.IllegalCharsetNameException e) {
supported = false;
}
return supported;
}
/**
* Nested class. Environment for this support. Extends
* <code>DataEditorSupport.Env</code> abstract class.
*/
private static class Environment extends DataEditorSupport.Env {
private static final long serialVersionUID = 3035543168452715818L;
/**
* Constructor.
*/
public Environment(HtmlDataObject obj) {
super(obj);
}
/**
* Implements abstract superclass method.
*/
@Override
protected FileObject getFile() {
return getDataObject().getPrimaryFile();
}
/**
* Implements abstract superclass method.
*/
@Override
protected FileLock takeLock() throws IOException {
return ((HtmlDataObject) getDataObject()).getPrimaryEntry().takeLock();
}
/**
* Overrides superclass method.
*
* @return text editor support (instance of enclosing class)
*/
@Override
public CloneableOpenSupport findCloneableOpenSupport() {
return (HtmlEditorSupport) getDataObject().getCookie(HtmlEditorSupport.class);
}
} // End of nested Environment class.
}