blob: ac64c569a0a34df0449c4fbc12886b9144b71446 [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.web.jsf;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.SwingUtilities;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.StyledDocument;
import org.netbeans.core.api.multiview.MultiViewHandler;
import org.netbeans.core.api.multiview.MultiViews;
import org.netbeans.modules.web.jsf.api.ConfigurationUtils;
import org.netbeans.modules.web.jsf.api.facesmodel.JSFConfigModel;
import org.netbeans.modules.web.jsf.impl.facesmodel.JSFConfigModelUtilities;
import org.netbeans.modules.xml.api.EncodingUtil;
import org.openide.DialogDescriptor;
import org.openide.DialogDisplayer;
import org.openide.NotifyDescriptor;
import org.openide.awt.UndoRedo;
import org.openide.filesystems.FileLock;
import org.openide.filesystems.FileObject;
import org.openide.nodes.Node.Cookie;
import org.openide.text.CloneableEditor;
import org.openide.text.CloneableEditorSupport.Pane;
import org.openide.text.DataEditorSupport;
import org.openide.cookies.*;
import org.openide.text.CloneableEditorSupport;
import org.openide.text.NbDocument;
import org.openide.util.Exceptions;
import org.openide.util.NbBundle;
import org.openide.util.RequestProcessor;
import org.openide.windows.CloneableTopComponent;
import org.openide.windows.TopComponent;
/**
*
* @author Petr Pisl
*/
public class JSFConfigEditorSupport extends DataEditorSupport
implements OpenCookie, EditCookie, EditorCookie.Observable, PrintCookie, CloseCookie {
private static final RequestProcessor RP = new RequestProcessor(JSFConfigEditorSupport.class);
/** SaveCookie for this support instance. The cookie is adding/removing
* data object's cookie set depending on if modification flag was set/unset. */
private final SaveCookie saveCookie = new SaveCookie() {
/** Implements <code>SaveCookie</code> interface. */
public void save() throws java.io.IOException {
JSFConfigDataObject obj = (JSFConfigDataObject) getDataObject();
// invoke parsing before save
restartTimer();
obj.parsingDocument();
if (obj.isDocumentValid()) {
saveDocument();
}else {
DialogDescriptor dialog = new DialogDescriptor(
NbBundle.getMessage(JSFConfigEditorSupport.class, "MSG_invalidXmlWarning"),
NbBundle.getMessage(JSFConfigEditorSupport.class, "TTL_invalidXmlWarning"));
java.awt.Dialog d = org.openide.DialogDisplayer.getDefault().createDialog(dialog);
d.setVisible(true);
if (dialog.getValue() == org.openide.DialogDescriptor.OK_OPTION) {
saveDocument();
}
}
}
};
private JSFConfigDataObject dataObject;
private RequestProcessor.Task parsingDocumentTask;
private TopComponent mvtc;
/** Delay for automatic parsing - in miliseconds */
private static final int AUTO_PARSING_DELAY = 2000;
public JSFConfigEditorSupport(JSFConfigDataObject dobj) {
super(dobj, null, new XmlEnv(dobj));
dataObject = dobj;
setMIMEType(JSFConfigLoader.MIME_TYPE); //NOI18N
//initialize the listeners on the document
initialize();
}
@Override
protected boolean asynchronousOpen() {
return false;
}
@Override
protected Pane createPane() {
return (CloneableEditorSupport.Pane) MultiViews.createCloneableMultiView(JSFConfigLoader.MIME_TYPE, getDataObject());
}
@Override
protected void initializeCloneableEditor(CloneableEditor editor) {
super.initializeCloneableEditor(editor);
}
@Override
protected CloneableTopComponent createCloneableTopComponent() {
CloneableTopComponent tc = super.createCloneableTopComponent();
this.mvtc = tc;
updateDisplayName ();
return tc;
}
public UndoRedo.Manager getUndoRedoManager() {
return super.getUndoRedo();
}
protected void setMVTC(TopComponent mvtc) {
this.mvtc = mvtc;
updateDisplayName();
}
private int click = 0;
public void updateDisplayName() {
final TopComponent tc = mvtc;
if (tc == null)
return;
SwingUtilities.invokeLater(new Runnable() {
public void run() {
String displayName = messageHtmlName();
if (! displayName.equals(tc.getDisplayName())){
tc.setHtmlDisplayName(displayName);
}
// XXX should probably set htmlDisplayName too, from messageHtmlName
// XXX should probably use messageToolTip instead
tc.setToolTipText(dataObject.getPrimaryFile().getPath());
}
});
}
private void initialize() {
// Create DocumentListener
final DocumentListener docListener = new DocumentListener() {
public void insertUpdate(DocumentEvent e) { change(e); }
public void changedUpdate(DocumentEvent e) { }
public void removeUpdate(DocumentEvent e) { change(e); }
private void change(DocumentEvent e) {
if (!dataObject.isNodeDirty()) restartTimer();
}
};
// the listener add only when the document is move to memory
addPropertyChangeListener(new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent evt) {
StyledDocument doc = getDocument();
if (EditorCookie.Observable.PROP_DOCUMENT.equals(evt.getPropertyName())
&& isDocumentLoaded() && doc != null) {
doc.addDocumentListener(docListener);
}
}
});
}
/*
* Save document using encoding declared in XML prolog if possible otherwise
* at UTF-8 (in such case it updates the prolog).
*/
@Override
public void saveDocument() throws java.io.IOException {
final javax.swing.text.StyledDocument doc = getDocument();
String defaultEncoding = "UTF-8"; // NOI18N
// dependency on xml/core
String enc = EncodingUtil.detectEncoding(doc);
boolean changeEncodingToDefault = false;
if (enc == null) {
enc = defaultEncoding;
}
//test encoding
if (!isSupportedEncoding(enc)) {
NotifyDescriptor nd = new NotifyDescriptor.Confirmation(
NbBundle.getMessage(JSFConfigEditorSupport.class, "MSG_BadEncodingDuringSave", //NOI18N
new Object[]{getDataObject().getPrimaryFile().getNameExt(),
enc,
defaultEncoding}),
NotifyDescriptor.YES_NO_OPTION,
NotifyDescriptor.WARNING_MESSAGE);
nd.setValue(NotifyDescriptor.NO_OPTION);
DialogDisplayer.getDefault().notify(nd);
if (nd.getValue() != NotifyDescriptor.YES_OPTION) {
return;
}
changeEncodingToDefault = true;
}
if (!changeEncodingToDefault) {
// is it possible to save the document in the encoding?
try {
java.nio.charset.CharsetEncoder coder = java.nio.charset.Charset.forName(enc).newEncoder();
if (!coder.canEncode(doc.getText(0, doc.getLength()))) {
NotifyDescriptor nd = new NotifyDescriptor.Confirmation(
NbBundle.getMessage(JSFConfigEditorSupport.class, "MSG_BadCharConversion", //NOI18N
new Object[]{getDataObject().getPrimaryFile().getNameExt(),
enc}),
NotifyDescriptor.YES_NO_OPTION,
NotifyDescriptor.WARNING_MESSAGE);
nd.setValue(NotifyDescriptor.NO_OPTION);
DialogDisplayer.getDefault().notify(nd);
if (nd.getValue() != NotifyDescriptor.YES_OPTION) {
return;
}
}
} catch (javax.swing.text.BadLocationException e) {
Logger.getLogger("global").log(Level.INFO, null, e);
}
super.saveDocument();
} else {
// update prolog to new valid encoding
try {
final int MAX_PROLOG = 1000;
int maxPrologLen = Math.min(MAX_PROLOG, doc.getLength());
final char prolog[] = doc.getText(0, maxPrologLen).toCharArray();
int prologLen = 0; // actual prolog length
//parse prolog and get prolog end
if (prolog[0] == '<' && prolog[1] == '?' && prolog[2] == 'x') {
// look for delimitting ?>
for (int i = 3; i < maxPrologLen; i++) {
if (prolog[i] == '?' && prolog[i + 1] == '>') {
prologLen = i + 1;
break;
}
}
}
final int passPrologLen = prologLen;
Runnable edit = new Runnable() {
@Override
public void run() {
try {
doc.remove(0, passPrologLen + 1); // +1 it removes exclusive
doc.insertString(0, "<?xml version='1.0' encoding='UTF-8' ?> \n<!-- was: " + new String(prolog, 0, passPrologLen + 1) + " -->", null); // NOI18N
} catch (BadLocationException e) {
if (System.getProperty("netbeans.debug.exceptions") != null) { // NOI18N
Exceptions.printStackTrace(e);
}
}
}
};
NbDocument.runAtomic(doc, edit);
super.saveDocument();
} catch (javax.swing.text.BadLocationException e) {
Logger.getLogger("global").log(Level.INFO, null, e);
}
}
}
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;
}
/** Restart the timer which starts the parser after the specified delay.
* @param onlyIfRunning Restarts the timer only if it is already running
*/
public void restartTimer() {
if (parsingDocumentTask==null || parsingDocumentTask.isFinished() ||
parsingDocumentTask.cancel()) {
dataObject.setDocumentDirty(true);
Runnable r = new Runnable() {
public void run() {
dataObject.parsingDocument();
}
};
if (parsingDocumentTask != null)
parsingDocumentTask = RP.post(r, AUTO_PARSING_DELAY);
else
parsingDocumentTask = RP.post(r, 100);
}
}
/**
* 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() {
boolean notif = super.notifyModified();
if (!notif){
return false;
}
updateDisplayName();
addSaveCookie();
return true;
}
@Override
protected void notifyClosed() {
mvtc = null;
super.notifyClosed();
syncModel();
}
private void syncModel() {
final JSFConfigModel configModel = ConfigurationUtils.getConfigModel(dataObject.getPrimaryFile(), true);
RP.post(new Runnable() {
@Override
public void run() {
long time = System.currentTimeMillis();
try {
// synchronize the model with the document. See issue #116315
if (configModel != null) {
// the model can be null, if the file wasn't opened.
configModel.sync();
}
} catch (IOException ex) {
// Logger.getLogger("global").log(Level.INFO, null, ex);
}
Logger.getLogger(this.getClass().getName()).log(Level.FINE, "Sync Config Model took: "+ (System.currentTimeMillis() - time) + " ms"); //NOI18N
}
});
}
/** Overrides superclass method. Adds removing of save cookie. */
@Override
protected void notifyUnmodified() {
super.notifyUnmodified();
updateDisplayName();
removeSaveCookie();
}
/** Helper method. Adds save cookie to the data object. */
private void addSaveCookie() {
// Adds save cookie to the data object.
if(dataObject.getCookie(SaveCookie.class) == null) {
dataObject.getCookieSet0().add(saveCookie);
dataObject.setModified(true);
}
}
/** Helper method. Removes save cookie from the data object. */
private void removeSaveCookie() {
JSFConfigDataObject obj = (JSFConfigDataObject)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);
}
}
@Override
public void open() {
super.open();
// parse once after opening the document
restartTimer();
updateDisplayName();
}
@Override
public void edit(){
// open the top component
open();
// ask for opening the last (source) editor
runInAwtDispatchThread(new Runnable() {
public void run() {
MultiViewHandler handler = MultiViews.findMultiViewHandler(mvtc);
// The handler can be null, when user uninstall a module, which
// provides view that was opened last time
if (handler != null) {
handler.requestVisible(handler.getPerspectives()[handler.getPerspectives().length - 1]);
mvtc.requestActive();
}
}
});
}
private static void runInAwtDispatchThread(Runnable runnable) {
if (SwingUtilities.isEventDispatchThread()) {
runnable.run();
} else {
SwingUtilities.invokeLater(runnable);
}
}
// /** Implementation of CloseOperationHandler for multiview.
// */
// @MimeRegistration(mimeType=JSFConfigLoader.MIME_TYPE, service=CloseOperationHandler.class)
// public static class CloseHandler implements CloseOperationHandler, Serializable {
// private static final long serialVersionUID = 1L;
//
// private JSFConfigDataObject dataObject;
//
// private CloseHandler() {
// }
//
// public CloseHandler(JSFConfigDataObject facesConfig) {
// dataObject = facesConfig;
// }
//
// @Override
// public boolean resolveCloseOperation(CloseOperationState[] elements) {
// boolean can = dataObject.getEditorSupport().canClose();
// if (can) {
// dataObject.getEditorSupport().notifyClosed();
// }
// return can;
// }
// }
private static class XmlEnv extends DataEditorSupport.Env {
private static final long serialVersionUID = -800036748848958489L;
//private static final long serialVersionUID = ...L;
/** Create a new environment based on the data object.
* @param obj the data object to edit
*/
public XmlEnv(JSFConfigDataObject obj) {
super(obj);
}
/** Get the file to edit.
* @return the primary file normally
*/
protected FileObject getFile() {
return getDataObject().getPrimaryFile();
}
/** Lock the file to edit.
* Should be taken from the file entry if possible, helpful during
* e.g. deletion of the file.
* @return a lock on the primary file normally
* @throws IOException if the lock could not be taken
*/
protected FileLock takeLock() throws java.io.IOException {
return ((JSFConfigDataObject) getDataObject()).getPrimaryEntry().takeLock();
}
/** Find the editor support this environment represents.
* Note that we have to look it up, as keeping a direct
* reference would not permit this environment to be serialized.
* @return the editor support
*/
@Override
public org.openide.windows.CloneableOpenSupport findCloneableOpenSupport() {
return getDataObject().getCookie(JSFConfigEditorSupport.class);
}
}
}