blob: eccd54f227e2d32378615b2d7ae121bd8572991d [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.properties;
import java.awt.event.ActionEvent;
import javax.swing.AbstractAction;
import javax.swing.Action;
import org.netbeans.core.spi.multiview.MultiViewFactory;
import org.openide.util.Exceptions;
import org.openide.util.lookup.Lookups;
import org.openide.util.Lookup;
import javax.swing.JComponent;
import javax.swing.JToolBar;
import org.netbeans.core.spi.multiview.CloseOperationState;
import org.netbeans.core.spi.multiview.MultiViewElementCallback;
import org.openide.text.NbDocument;
import org.netbeans.core.spi.multiview.MultiViewElement;
import org.openide.util.NbBundle.Messages;
import org.netbeans.core.api.multiview.MultiViews;
import java.awt.EventQueue;
import java.awt.Image;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.beans.PropertyVetoException;
import java.beans.VetoableChangeListener;
import java.beans.VetoableChangeSupport;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Serializable;
import java.io.Writer;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JEditorPane;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.EditorKit;
import javax.swing.text.StyledDocument;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.CannotUndoException;
import javax.swing.undo.UndoableEdit;
import org.netbeans.api.queries.FileEncodingQuery;
import org.netbeans.modules.properties.PropertiesEncoding.PropCharset;
import org.netbeans.modules.properties.PropertiesEncoding.PropCharsetEncoder;
import org.openide.ErrorManager;
import org.openide.awt.UndoRedo;
import org.openide.cookies.CloseCookie;
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.FileChangeAdapter;
import org.openide.filesystems.FileEvent;
import org.openide.filesystems.FileLock;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileRenameEvent;
import org.openide.filesystems.FileStatusEvent;
import org.openide.filesystems.FileStatusListener;
import org.openide.filesystems.FileSystem;
import org.openide.filesystems.FileStateInvalidException;
import org.openide.filesystems.FileSystem.AtomicAction;
import org.openide.filesystems.FileUtil;
import org.openide.loaders.DataFolder;
import org.openide.loaders.DataObject;
import org.openide.loaders.SaveAsCapable;
import org.openide.nodes.Node;
import org.openide.text.CloneableEditor;
import org.openide.text.CloneableEditorSupport;
import org.openide.text.DataEditorSupport;
import org.openide.util.HelpCtx;
import org.openide.util.ImageUtilities;
import org.openide.util.Mutex;
import org.openide.util.NbBundle;
import org.openide.util.WeakListeners;
import org.openide.windows.CloneableOpenSupport;
import org.openide.util.Task;
import org.openide.util.TaskListener;
import org.openide.util.lookup.ProxyLookup;
import org.openide.windows.CloneableTopComponent;
import org.openide.windows.TopComponent;
import static java.util.logging.Level.FINER;
import javax.swing.*;
import org.openide.filesystems.StatusDecorator;
/**
* Support for viewing .properties files (EditCookie) by opening them in a text editor.
*
* @author Petr Jiricka, Peter Zavadsky
* @see org.openide.text.CloneableEditorSupport
*/
public class PropertiesEditorSupport extends CloneableEditorSupport
implements EditCookie, EditorCookie.Observable, PrintCookie, CloseCookie, Serializable, SaveAsCapable {
/** error manager for CloneableEditorSupport logging and error reporting */
static final Logger LOG = Logger.getLogger("org.netbeans.modules.properties.PropertiesEditorSupport"); // NOI18N
/** */
private FileStatusListener fsStatusListener;
/** Visible view of underlying file entry */
transient PropertiesFileEntry myEntry;
/** Generated serial version UID. */
static final long serialVersionUID =1787354011149868490L;
private Charset charset;
/** Constructor. */
public PropertiesEditorSupport(PropertiesFileEntry entry) {
super(new Environment(entry), new ProxyLookup(Lookups.singleton(entry.getDataObject()), entry.getDataObject().getLookup()));
this.myEntry = entry;
}
@Override
protected Pane createPane() {
return (Pane) MultiViews.createCloneableMultiView(PropertiesDataLoader.PROPERTIES_MIME_TYPE, getDataObject());
}
/** Getter for the environment that was provided in the constructor.
* @return the environment
*/
final CloneableEditorSupport.Env desEnv() {
return (CloneableEditorSupport.Env) env;
}
/**
* Overrides superclass method.
* Should test whether all data is saved, and if not, prompt the user
* to save. Called by my topcomponent when it wants to close its last topcomponent, but the table editor may still be open
* @return <code>true</code> if everything can be closed
*/
@Override
protected boolean canClose () {
// if the table is open, can close without worries, don't remove the save cookie
if (hasOpenedTableComponent()){
return true;
}else{
DataObject propDO = myEntry.getDataObject();
if ((propDO == null) || !propDO.isModified()) {
return true;
}
return super.canClose();
}
}
/** Getter of the data object that this support is associated with.
* @return data object passed in constructor
*/
public final DataObject getDataObject () {
return myEntry.getDataObject();
}
private boolean isEnvReadOnly() {
CloneableEditorSupport.Env myEnv = desEnv();
return myEnv instanceof Environment && !((Environment) myEnv).getFileImpl().canWrite();
}
/**
*
*/
final class FsStatusListener implements FileStatusListener, Runnable {
/**
*/
public void annotationChanged(FileStatusEvent ev) {
if (ev.isNameChange() && ev.hasChanged(myEntry.getFile())) {
Mutex.EVENT.writeAccess(this);
}
}
/**
*/
public void run() {
updateEditorDisplayNames();
}
}
/**
*/
private void attachStatusListener() {
if (fsStatusListener != null) {
return; //already attached
}
FileSystem fs;
try {
fs = myEntry.getFile().getFileSystem();
} catch (FileStateInvalidException ex) {
ErrorManager.getDefault().notify(ErrorManager.ERROR, ex);
return;
}
fsStatusListener = new FsStatusListener();
fs.addFileStatusListener(
FileUtil.weakFileStatusListener(fsStatusListener, fs));
}
/**
*/
private void updateEditorDisplayNames() {
assert EventQueue.isDispatchThread();
final String title = messageName();
final String htmlTitle = messageHtmlName();
final String toolTip = messageToolTip();
Enumeration en = allEditors.getComponents();
while (en.hasMoreElements()) {
TopComponent tc = (TopComponent) en.nextElement();
tc.setDisplayName(title);
tc.setHtmlDisplayName(htmlTitle);
tc.setToolTipText(toolTip);
}
}
/**
*/
@Override
protected void initializeCloneableEditor(CloneableEditor editor) {
((PropertiesEditor) editor).initialize(myEntry);
}
/**
* Overrides superclass method.
* Let's the super method create the document and also annotates it
* with Title and StreamDescription properities.
* @param kit kit to user to create the document
* @return the document annotated by the properties
*/
@Override
protected StyledDocument createStyledDocument(EditorKit kit) {
StyledDocument document = super.createStyledDocument(kit);
// Set additional proerties to document.
// Set document name property. Used in CloneableEditorSupport.
document.putProperty(Document.TitleProperty, myEntry.getFile().toString());
// Set dataobject to stream desc property.
document.putProperty(Document.StreamDescriptionProperty, myEntry.getDataObject());
// hook the document to listen for any changes to update changes by
// reparsing the document
document.addDocumentListener(new javax.swing.event.DocumentListener() {
public void insertUpdate(javax.swing.event.DocumentEvent e) { changed();}
public void changedUpdate(javax.swing.event.DocumentEvent e) { changed();}
public void removeUpdate(javax.swing.event.DocumentEvent e) { changed();}
private void changed() {
myEntry.getHandler().autoParse();
}
});
return document;
}
/**
* Reads the file from the stream, filter the guarded section
* comments, and mark the sections in the editor. Overrides superclass method.
* @param document the document to read into
* @param inputStream the open stream to read from
* @param editorKit the associated editor kit
* @throws <code>IOException</code> if there was a problem reading the file
* @throws <code>BadLocationException</code> should not normally be thrown
* @see #saveFromKitToStream
*/
@Override
protected void loadFromStreamToKit(StyledDocument document, InputStream inputStream, EditorKit editorKit) throws IOException, BadLocationException {
final Charset c = getCharset();
final Reader reader = new BufferedReader(new InputStreamReader(inputStream, c));
try {
editorKit.read(reader, document, 0);
} finally {
reader.close();
}
}
/**
* Adds new lines according actual value of <code>newLineType</code> variable.
* Overrides superclass method.
* @param document the document to write from
* @param editorKit the associated editor kit
* @param outputStream the open stream to write to
* @throws IOException if there was a problem writing the file
* @throws BadLocationException should not normally be thrown
* @see #loadFromStreamToKit
*/
@Override
protected void saveFromKitToStream(StyledDocument document, EditorKit editorKit, OutputStream outputStream) throws IOException, BadLocationException {
final Writer writer;
final Charset c = getCharset();
if (c.name().equals(PropertiesEncoding.PROP_CHARSET_NAME)) {
final PropCharsetEncoder encoder = new PropCharsetEncoder();
writer = new BufferedWriter(new OutputStreamWriter(outputStream, encoder));
} else {
writer = new BufferedWriter(new OutputStreamWriter(outputStream, c));
}
try {
editorKit.write(writer, document, 0, document.getLength());
} finally {
writer.flush();
writer.close();
}
}
private Charset getCharset() {
if (charset == null) {
charset = FileEncodingQuery.getEncoding(myEntry.getDataObject().getPrimaryFile());
}
return charset;
}
void resetCharset() {
charset = null;
}
/**
* Adds a save cookie if the document has been marked modified. Overrides superclass method.
* @return <code>true</code> if the environment accepted being marked as modified
* or <code>false</code> if it refused it and the document should still be unmodified
*/
@Override
protected boolean notifyModified () {
// Reparse file.
myEntry.getHandler().autoParse();
if (!super.notifyModified()) {
return false; // Will cause the log message:
// INFO [org.openide.text.ClonableEditorSupport]: ...
}
// See #89029, #175275, #186876 for info about the implementation
((Environment)env).addSaveCookie();
return true;
}
@Override
protected Task reloadDocument(){
Task tsk = super.reloadDocument();
tsk.addTaskListener(new TaskListener(){
public void taskFinished(Task task){
myEntry.getHandler().autoParse();
}
});
return tsk;
}
/** Overrides superclass method. Adds checking for opened Table component. */
@Override
protected void notifyUnmodified () {
super.notifyUnmodified();
((Environment)env).removeSaveCookie();
}
/**
*/
@Override
public void open() {
super.open();
attachStatusListener();
}
/** Overrides superclass method. Adds checking for opened Table panel. */
@Override
protected void notifyClosed() {
// Close document only in case there is not open table editor.
if(!hasOpenedTableComponent()) {
boolean wasModified = isModified();
super.notifyClosed();
if (wasModified) {
// #21850. Don't reparse invalid or virtual file.
if(myEntry.getFile().isValid() && !myEntry.getFile().isVirtual()) {
myEntry.getHandler().reparseNowBlocking();
}
}
}
}
/**
* Overrides superclass abstract method.
* Message to display when an object is being opened.
* @return the message or null if nothing should be displayed
*/
protected String messageOpening() {
return NbBundle.getMessage(
PropertiesEditorSupport.class,
"LBL_ObjectOpen", // NOI18N
getFileLabel()
);
}
/**
* Overrides superclass abstract method.
* Message to display when an object has been opened.
* @return the message or null if nothing should be displayed
*/
protected String messageOpened() {
return NbBundle.getMessage(
PropertiesEditorSupport.class,
"LBL_ObjectOpened", // NOI18N
getFileLabel()
);
}
private String getFileLabel() {
PropertiesDataObject propDO = (PropertiesDataObject) myEntry.getDataObject();
return propDO.getPrimaryFile().getNameExt();
}
/**
* Overrides superclass abstract method.
* Constructs message that should be used to name the editor component.
* @return name of the editor
*/
protected String messageName () {
if (!myEntry.getDataObject().isValid()) {
return ""; //NOI18N
}
return DataEditorSupport.annotateName(getFileLabel(), false, isModified(), !myEntry.getFile().canWrite());
}
/** */
@Override
protected String messageHtmlName () {
if (!myEntry.getDataObject().isValid()) {
return null;
}
String rawName = getFileLabel();
String annotatedName = null;
final FileObject entry = myEntry.getFile();
try {
StatusDecorator status = entry.getFileSystem().getDecorator();
if (status != null) {
Set<FileObject> files = Collections.singleton(entry);
annotatedName = status.annotateNameHtml(rawName, files);
if (rawName.equals(annotatedName)) {
annotatedName = null;
}
if ((annotatedName != null)
&& (!annotatedName.startsWith("<html>"))) { //NOI18N
annotatedName = "<html>" + annotatedName; //NOI18N
}
if (annotatedName == null) {
annotatedName = status.annotateName(rawName, files);
}
}
} catch (FileStateInvalidException ex) {
//do nothing and fall through
}
String name = (annotatedName != null) ? annotatedName : /*XXX escape HTML content*/rawName;
return DataEditorSupport.annotateName(name, true, isModified(), !myEntry.getFile().canWrite());
}
/**
* Overrides superclass abstract method.
* Is modified and is being closed.
* @return text to show to the user
*/
protected String messageSave () {
return NbBundle.getMessage (
PropertiesEditorSupport.class,
"MSG_SaveFile", // NOI18N
getFileLabel()
);
}
/**
* Overrides superclass abstract method.
* Text to use as tooltip for component.
* @return text to show to the user
*/
protected String messageToolTip () {
// copied from DataEditorSupport, more or less
FileObject fo = myEntry.getFile();
return DataEditorSupport.toolTip(fo, isModified(), !myEntry.getFile().canWrite());
}
/** Overrides superclass method. Gets <code>UndoRedo</code> manager which maps
* <code>UndoalbleEdit</code>'s to <code>StampFlag</code>'s. */
@Override
protected UndoRedo.Manager createUndoRedoManager () {
return new UndoRedoStampFlagManager();
}
/**
* Helper method. Hack on superclass <code>getUndoRedo()</code> method, to widen its protected modifier.
* Needs to be accessed from outside this class (in <code>PropertiesOpen</code>).
* @see PropertiesOpen
*/
UndoRedo.Manager getUndoRedoManager() {
return super.getUndoRedo();
}
/**
* Helper method. Used only by <code>PropertiesOpen</code> support when closing last Table component.
* Note: It's quite ugly by-pass of <code>notifyClosed()</code> method. Should be revised.
*/
void forceNotifyClosed() {
super.notifyClosed();
}
/** Helper method. Saves this entry. */
private void saveThisEntry() throws IOException {
FileSystem.AtomicAction aa = new SaveImpl(this);
FileUtil.runAtomicAction(aa);
// super.saveDocument();
// #32777 - it can happen that save operation was interrupted
// and file is still modified. Mark it unmodified only when it is really
// not modified.
if (!env.isModified()) {
myEntry.setModified(false);
}
}
final void superSaveDoc() throws IOException {
super.saveDocument();
}
/**
* Save the document under a new file name and/or extension.
* @param folder New folder to save the DataObject to.
* @param fileName New file name to save the DataObject to.
* @throws java.io.IOException If the operation failed
* @since 6.3
*/
public void saveAs( FileObject folder, String fileName ) throws IOException {
//ask the user for a new file name to save to
String newExtension = FileUtil.getExtension( fileName );
DataObject newDob = null;
DataObject currentDob = myEntry.getDataObject();
if( !currentDob.isModified() || null == getDocument() ) {
//the document is not modified on disk, we copy/rename the file
DataFolder df = DataFolder.findFolder( folder );
FileObject newFile = folder.getFileObject(fileName);
if( null != newFile ) {
//remove the target file if it already exists
newFile.delete();
}
newFile = myEntry.copyRename(df.getPrimaryFile(), getFileNameNoExtension(fileName), newExtension);
if (null != newFile) {
newDob = DataObject.find(newFile);
}
} else {
//the document is modified in editor, we need to save the editor kit instead
FileObject newFile = FileUtil.createData( folder, fileName );
saveDocumentAs( newFile.getOutputStream() );
currentDob.setModified( false );
newDob = DataObject.find( newFile );
}
if( null != newDob ) {
OpenCookie c = newDob.getCookie( OpenCookie.class );
if( null != c ) {
//close the original document
close( false );
//open the new one
c.open();
}
}
}
private String getFileNameNoExtension(String fileName) {
int index = fileName.lastIndexOf("."); // NOI18N
if (index == -1) {
return fileName;
} else {
return fileName.substring(0, index);
}
}
/**
* Save the document to a new file.
* @param output
* @exception IOException on I/O error
* @since 6.3
*/
private void saveDocumentAs( final OutputStream output ) throws IOException {
final StyledDocument myDoc = getDocument();
// save the document as a reader
class SaveAsWriter implements Runnable {
private IOException ex;
public void run() {
try {
OutputStream os = null;
try {
os = new BufferedOutputStream( output );
EditorKit kit = createEditorKit();
saveFromKitToStream( myDoc, kit, os );
os.close(); // performs firing
os = null;
} catch (BadLocationException ex2) {
LOG.log(Level.INFO, null, ex2);
} finally {
if (os != null) { // try to close if not yet done
os.close();
}
}
} catch (IOException e) {
this.ex = e;
}
}
public void after() throws IOException {
if (ex != null) {
throw ex;
}
}
}
SaveAsWriter saveAsWriter = new SaveAsWriter();
myDoc.render(saveAsWriter);
saveAsWriter.after();
}
/** Helper method.
* @return whether there is an table view opened */
public synchronized boolean hasOpenedTableComponent() {
PropertiesDataObject dataObject = (PropertiesDataObject) myEntry.getDataObject();
if (dataObject.getBundleStructureOrNull() == null || dataObject.getBundleStructure().getEntryCount()==0) {
return false;
}
return dataObject.getOpenSupport().hasOpenedTableComponent();
}
/**
* Helper method.
* @return whether there is an open editor component. */
public synchronized boolean hasOpenedEditorComponent() {
Enumeration en = allEditors.getComponents ();
return en.hasMoreElements ();
}
/** Class which exist only due comaptibility with version 3.0. */
private static final class Env extends Environment {
/** Generated Serialized Version UID. */
static final long serialVersionUID = -9218186467757330339L;
/** Used for deserialization. */
private PropertiesFileEntry entry;
/** */
public Env(PropertiesFileEntry entry) {
super(entry);
}
/** Adds passing entry field to superclass. */
private void readObject(java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException {
in.defaultReadObject();
if (this.entry != null) {
super.entry = this.entry;
}
}
}
/** Nested class. Implementation of <code>ClonableEditorSupport.Env</code> interface. */
private static class Environment implements CloneableEditorSupport.Env,
PropertyChangeListener, SaveCookie {
/** generated Serialized Version UID */
static final long serialVersionUID = 354528097109874355L;
/** Entry on which is support build. */
private PropertiesFileEntry entry;
/** Lock acquired after the first modification and used in <code>save</code> method. */
private transient FileLock fileLock;
/** The file object this environment is associated to.
* This file object can be changed by a call to refresh file.
*/
private transient FileObject fileObject;
/** Spport for firing of property changes. */
private transient PropertyChangeSupport propSupp;
/** Support for firing of vetoable changes. */
private transient VetoableChangeSupport vetoSupp;
private transient EnvironmentListener envListener;
/** Constructor.
* @param obj this support should be associated with
*/
public Environment (PropertiesFileEntry entry) {
LOG.finer("PropertiesEditorSupport(<PropertiesFileEntry>)");//NOI18N
LOG.finer(" - new Environment(<PropertiesFileEntry>)"); //NOI18N
this.entry = entry;
envListener = new EnvironmentListener(this);
entry.getFile().addFileChangeListener(envListener);
entry.addPropertyChangeListener(this);
}
/** Getter for the file to work on.
* @return the file
*/
private FileObject getFileImpl () {
// updates the file if there was a change
changeFile();
return fileObject;
}
protected final DataObject getDataObject() {
return entry.getDataObject();
}
/** Implements <code>CloneableEditorSupport.Env</code> inetrface. Adds property listener. */
public void addPropertyChangeListener(PropertyChangeListener l) {
LOG.finer("Environment.addPropertyChangeListener(...)"); //NOI18N
prop().addPropertyChangeListener (l);
}
/** Accepts property changes from entry and fires them to own listeners. */
public void propertyChange(PropertyChangeEvent evt) {
if (LOG.isLoggable(FINER)) {
LOG.finer("Environment.propertyChange(" //NOI18N
+ evt.getPropertyName()
+ ", " + evt.getOldValue() //NOI18N
+ ", " + evt.getNewValue() //NOI18N
+ ')');
}
// We will handle the object invalidation here.
if(DataObject.PROP_VALID.equals(evt.getPropertyName ())) {
// do not check it if old value is not true
if (Boolean.FALSE.equals(evt.getOldValue())) {
return;
}
// loosing validity
PropertiesEditorSupport support = (PropertiesEditorSupport)findCloneableOpenSupport();
if(support != null) {
// mark the object as not being modified, so nobody
// will ask for save
unmarkModified();
support.close(false);
}
} else {
firePropertyChange (
evt.getPropertyName(),
evt.getOldValue(),
evt.getNewValue()
);
}
}
/** Implements <code>CloneableEditorSupport.Env</code> inetrface. Removes property listener. */
public void removePropertyChangeListener(PropertyChangeListener l) {
LOG.finer("Environment.removePropertyChangeListener(...)"); //NOI18N
prop().removePropertyChangeListener (l);
}
/** Implements <code>CloneableEditorSupport.Env</code> inetrface. Adds veto listener. */
public void addVetoableChangeListener(VetoableChangeListener l) {
LOG.finer("Environment.addVetoableChangeListener(...)"); //NOI18N
veto().addVetoableChangeListener (l);
}
/** Implements <code>CloneableEditorSupport.Env</code> inetrface. Removes veto listener. */
public void removeVetoableChangeListener(VetoableChangeListener l) {
LOG.finer("Environment.removeVetoableChangeListener(...)"); //NOI18N
veto().removeVetoableChangeListener (l);
}
/** Overrides superclass method.
* Note: in fact it returns <code>CloneableEditorSupport</code> instance.
* @return the support or null if the environemnt is not in valid
* state and the CloneableOpenSupport cannot be found for associated
* entry object
*/
public CloneableOpenSupport findCloneableOpenSupport() {
return (PropertiesEditorSupport)entry.getCookieSet().getCookie(EditCookie.class);
}
/**
* Implements <code>CloneableEditorSupport.Env</code> interface.
* Test whether the support is in valid state or not.
* It could be invalid after deserialization when the object it
* referenced to does not exist anymore.
* @return true or false depending on its state
*/
public boolean isValid() {
return entry.getDataObject().isValid();
}
/**
* Implements <code>CloneableEditorSupport.Env</code> interface.
* Test whether the object is modified or not.
* @return true if the object is modified
*/
public boolean isModified() {
return entry.isModified();
}
/**
* Implements <code>CloneableEditorSupport.Env</code> interface.
* First of all tries to lock the primary file and
* if it succeeds it marks the data object modified.
* @exception IOException if the environment cannot be marked modified
* (for example when the file is readonly), when such exception
* is the support should discard all previous changes
*/
public void markModified() throws java.io.IOException {
LOG.finer("Environment.markModified()"); //NOI18N
if (fileLock == null || !fileLock.isValid()) {
fileLock = entry.takeLock();
}
entry.setModified(true);
}
/**
* Implements <code>CloneableEditorSupport.Env</code> interface.
* Reverse method that can be called to make the environment
* unmodified.
*/
public void unmarkModified() {
LOG.finer("Environment.unmarkModified()"); //NOI18N
if (fileLock != null && fileLock.isValid()) {
fileLock.releaseLock();
}
entry.setModified(false);
}
/**
* Called from the <code>EnvironmentListener</code>.
*/
final void updateDocumentProperty () {
//Update document TitleProperty
EditorCookie ec = getDataObject().getCookie(EditorCookie.class);
if (ec != null) {
StyledDocument doc = ec.getDocument();
if (doc != null) {
doc.putProperty(Document.TitleProperty,
FileUtil.getFileDisplayName(getDataObject().getPrimaryFile()));
}
}
}
/**
* Implements <code>CloneableEditorSupport.Env</code> interface.
* Mime type of the document.
* @return the mime type to use for the document
*/
public String getMimeType() {
return getFileImpl().getMIMEType();
}
/**
* Implements <code>CloneableEditorSupport.Env</code> interface.
* The time when the data has been modified. */
public Date getTime() {
// #32777 - refresh file object and return always the actual time
getFileImpl().refresh();
return getFileImpl().lastModified();
}
/** Method that allows subclasses to notify this environment that
* the file associated with this support has changed and that
* the environment should listen on modifications of different
* file object.
*/
protected final void changeFile () {
FileObject newFile = entry.getFile ();
if (newFile.equals (fileObject)) {
// the file has not been updated
return;
}
boolean lockAgain;
if (fileLock != null) {
// <> NB #61818 In case the lock was not active (isValid() == false), the new lock was taken,
// which seems to be incorrect. There is taken a lock on new file, while it there wasn't on the old one.
// fileLock.releaseLock ();
// lockAgain = true;
// =====
if(fileLock.isValid()) {
LOG.fine("changeFile releaseLock: " + fileLock + " for " + fileObject); // NOI18N
fileLock.releaseLock ();
lockAgain = true;
} else {
fileLock = null;
lockAgain = false;
}
// </>
} else {
lockAgain = false;
}
boolean wasNull = fileObject == null;
fileObject = newFile;
LOG.fine("changeFile: " + newFile + " for " + fileObject); // NOI18N
if (envListener != null)
fileObject.removeFileChangeListener(envListener);
envListener = new EnvironmentListener(this);
fileObject.addFileChangeListener (envListener);
if (lockAgain) { // refresh lock
try {
fileLock = entry.takeLock ();
LOG.fine("changeFile takeLock: " + fileLock + " for " + fileObject); // NOI18N
} catch (IOException e) {
Logger.getLogger(PropertiesEditorSupport.class.getName()).log(Level.WARNING, null, e);
}
}
if (!wasNull) {
firePropertyChange("expectedTime", null, getTime()); // NOI18N
}
}
/**
* Implements <code>CloneableEditorSupport.Env</code> interface.
* Obtains the input stream.
* @exception IOException if an I/O error occures
*/
public InputStream inputStream() throws IOException {
LOG.finer("Environment.inputStream()"); //NOI18N
return getFileImpl().getInputStream();
}
/**
* Implements <code>CloneableEditorSupport.Env</code> interface.
* Obtains the output stream.
* @exception IOException if an I/O error occures
*/
public OutputStream outputStream() throws IOException {
LOG.finer("Environment.outputStream()"); //NOI18N
if (fileLock == null || !fileLock.isValid()) {
fileLock = entry.takeLock ();
}
LOG.fine("outputStream after takeLock: " + fileLock + " for " + fileObject); // NOI18N
try {
return getFileImpl ().getOutputStream (fileLock);
} catch (IOException fse) {
// [pnejedly] just retry once.
// Ugly workaround for #40552
if (fileLock == null || !fileLock.isValid()) {
fileLock = entry.takeLock ();
}
LOG.fine("ugly workaround for #40552: " + fileLock + " for " + fileObject); // NOI18N
return getFileImpl ().getOutputStream (fileLock);
}
// return entry.getFile().getOutputStream(fileLock);
}
/**
* Implements <code>SaveCookie</code> interface.
* Invoke the save operation.
* @throws IOException if the object could not be saved
*/
public void save() throws IOException {
LOG.finer("Environment.save()"); //NOI18N
// Do saving job. Note it gets editor support, not open support.
((PropertiesEditorSupport)findCloneableOpenSupport()).saveThisEntry();
}
/** Fires property change.
* @param name the name of property that changed
* @param oldValue old value
* @param newValue new value
*/
private void firePropertyChange (String name, Object oldValue, Object newValue) {
prop().firePropertyChange (name, oldValue, newValue);
}
/** Fires vetoable change.
* @param name the name of property that changed
* @param oldValue old value
* @param newValue new value
*/
private void fireVetoableChange (String name, Object oldValue, Object newValue) throws PropertyVetoException {
veto ().fireVetoableChange (name, oldValue, newValue);
}
/** Lazy getter for property change support. */
private PropertyChangeSupport prop() {
synchronized (this) {
if (propSupp == null) {
propSupp = new PropertyChangeSupport (this);
}
}
return propSupp;
}
/** Lazy getter for vetoable support. */
private VetoableChangeSupport veto() {
synchronized (this) {
if (vetoSupp == null) {
vetoSupp = new VetoableChangeSupport (this);
}
}
return vetoSupp;
}
/** Helper method. Adds save cookie to the entry. */
private void addSaveCookie() {
LOG.finer("Environment.addSaveCookie(...)"); //NOI18N
if (entry.getCookie(SaveCookie.class) == null) {
entry.getCookieSet().add(this);
}
//Need to add cookie to DataObject since saveAll use it, and
//OpenCookie may not be initialized
PropertiesDataObject dataObject = (PropertiesDataObject) getDataObject();
dataObject.updateModificationStatus();
if (dataObject.getCookie(SaveCookie.class) == null){
dataObject.getCookieSet0().add(this);
}
}
/** Helper method. Removes save cookie from the entry. */
private void removeSaveCookie() {
LOG.finer("Environment.removeSaveCookie(...)"); //NOI18N
// remove Save cookie from the entry
SaveCookie sc = entry.getCookie(SaveCookie.class);
if (sc != null && sc.equals(this)) {
entry.getCookieSet().remove(this);
}
final SaveCookie cookie = this;
PropertiesRequestProcessor.getInstance().post(new Runnable() {
public void run() {
PropertiesDataObject dataObject = (PropertiesDataObject) getDataObject();
dataObject.updateModificationStatus();
dataObject.getCookieSet0().remove(cookie);
}
});
}
/** Called from the <code>EnvironmnetListener</code>
* @param expected is the change expected
* @param time of the change
*/
private void fileChanged(boolean expected, long time) {
LOG.finer("Environment.fileChanged(...)"); //NOI18N
if (expected) {
// newValue = null means do not ask user whether to reload
firePropertyChange (PROP_TIME, null, null);
} else {
firePropertyChange (PROP_TIME, null, new Date (time));
}
}
/** Called from the <code>EnvironmentListener</code>.
*/
final void fileRenamed () {
//#151787: Sync timestamp when svn client changes timestamp externally during rename.
firePropertyChange("expectedTime", null, getTime()); // NOI18N
}
/** Called from the <code>EnvironmentListener</code>.
* The components are going to be closed anyway and in case of
* modified document its asked before if to save the change. */
private void fileRemoved() {
LOG.finer("Environment.fileRemoved() ... "); //NOI18N
try {
fireVetoableChange(PROP_VALID, Boolean.TRUE, Boolean.FALSE);
} catch(PropertyVetoException pve) {
// Ignore it and close anyway. File doesn't exist anymore.
}
firePropertyChange(PROP_VALID, Boolean.TRUE, Boolean.FALSE);
}
} // End of nested class Environment.
/** Weak listener on file object that notifies the <code>Environment</code> object
* that a file has been modified. */
private static final class EnvironmentListener extends FileChangeAdapter {
/** Reference of <code>Environment</code> */
private Reference<Environment> reference;
/** @param environment <code>Environment<code> to use
*/
public EnvironmentListener(Environment environment) {
LOG.finer("new EnvironmentListener(<Environment>)"); //NOI18N
reference = new WeakReference<Environment>(environment);
}
@Override
public void fileDeleted(FileEvent fe) {
Environment myEnv = this.reference.get();
if (myEnv != null) {
myEnv.updateDocumentProperty();
myEnv.fileRemoved();
}
// super.fileDeleted(fe);
}
/** Fired when a file is changed.
* @param fe the event describing context where action has taken place
*/
@Override
public void fileChanged(FileEvent evt) {
if (LOG.isLoggable(FINER)) {
LOG.finer("EnviromentListener.fileChanged(...)"); //NOI18N
LOG.finer(" - original file: " //NOI18N
+ FileUtil.getFileDisplayName(evt.getFile()));
LOG.finer(" - current file: " //NOI18N
+ FileUtil.getFileDisplayName((FileObject) evt.getSource()));
}
//see #160338
if (evt.firedFrom(SaveImpl.DEFAULT)) {
return;
}
Environment environment = reference.get();
if (environment != null) {
if(!environment.getFileImpl().equals(evt.getFile()) ) {
// If the FileObject was changed.
// Remove old listener from old FileObject.
evt.getFile().removeFileChangeListener(this);
// Add new listener to new FileObject.
environment.getFileImpl().addFileChangeListener(new EnvironmentListener(environment));
return;
}
// #16403. See DataEditorSupport.EnvListener.
if(evt.getFile().isVirtual()) {
environment.entry.getFile().removeFileChangeListener(this);
// File doesn't exist on disk -> simulate env is invalid,
// even the fileObject could be valid, see VCS FS.
environment.fileRemoved();
environment.entry.getFile().addFileChangeListener(this);
} else {
environment.fileChanged(evt.isExpected(), evt.getTime());
}
}
}
@Override
public void fileRenamed(FileRenameEvent fe) {
Environment myEnv = this.reference.get();
if (myEnv != null) {
myEnv.updateDocumentProperty();
myEnv.fileRenamed();
}
}
} // End of nested class EnvironmentListener.
/** Inner class for opening editor view at a given key. */
public class PropertiesEditAt implements EditCookie {
/** Key at which should be pane opened. (Cursor will be at the position of that key). */
private String key;
/** Constructor. */
PropertiesEditAt(String key) {
this.key = key;
}
/** Setter for <code>key</code>. */
public void setKey(String key) {
this.key = key;
}
/** Implementation of <code>EditCookie</code> interface. */
public void edit() {
CloneableTopComponent ctc = PropertiesEditorSupport.super.openCloneableTopComponent();
ctc.requestActive();
PropertiesEditor editor = ctc.getLookup().lookup(PropertiesEditor.class);
Element.ItemElem item = myEntry.getHandler().getStructure().getItem(key);
if (item != null) {
int offset = item.getKeyElem().getBounds().getBegin().getOffset();
if ((editor.getPane() != null) && (editor.getPane().getCaret() != null)) {
editor.getPane().getCaret().setDot(offset);
}
}
}
} // End of inner class PropertiesEditAt.
/** Cloneable top component to hold the editor kit. */
@Messages("CTL_SourceTabCaption=&Source")
@MultiViewElement.Registration(
displayName="#CTL_SourceTabCaption",
iconBase="org/netbeans/modules/properties/propertiesObject.png",
persistenceType=TopComponent.PERSISTENCE_ONLY_OPENED,
preferredID="properties.source",
mimeType=PropertiesDataLoader.PROPERTIES_MIME_TYPE,
position=1
)
public static class PropertiesEditor extends CloneableEditor implements MultiViewElement {
/** Holds the file being edited. */
protected transient PropertiesFileEntry entry;
/** Listener for entry's save cookie changes. */
private transient PropertyChangeListener saveCookieLNode;
/** Generated serial version UID. */
static final long serialVersionUID =-2702087884943509637L;
private MultiViewElementCallback callback;
private transient JToolBar bar;
private transient PropertiesEditorLookup peLookup;
private transient Lookup originalLookup;
/** Constructor for deserialization */
public PropertiesEditor() {
super();
}
/** Creates new editor */
public PropertiesEditor(Lookup lookup) {
super(lookup.lookup(PropertiesEditorSupport.class));
PropertiesEditorSupport support = lookup.lookup(PropertiesEditorSupport.class);
setActivatedNodes(new Node[] {support.getDataObject().getNodeDelegate()});
}
/** Initializes object, used in construction and deserialization. */
private void initialize(PropertiesFileEntry entry) {
this.entry = entry;
Node n = entry.getNodeDelegate ();
setActivatedNodes (new Node[] { n });
updateName();
// entry to the set of listeners
saveCookieLNode = new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent evt) {
if (Node.PROP_COOKIE.equals(evt.getPropertyName()) ||
DataObject.PROP_NAME.equals(evt.getPropertyName()))
{
updateName();
}
}
};
this.entry.addPropertyChangeListener(WeakListeners.propertyChange(saveCookieLNode, this.entry));
}
/**
* Overrides superclass method.
* When closing last view, also close the document.
* @return <code>true</code> if close succeeded
*/
@Override
protected boolean closeLast () {
return super.closeLast(false);
}
/** Overrides superclass method. Gets <code>Icon</code>. */
@Override
public Image getIcon () {
PropertiesDataObject propDO = (PropertiesDataObject) getDataObject();
return ImageUtilities.loadImage(
propDO.isMultiLocale()
? "org/netbeans/modules/properties/propertiesLocale.gif" // NOI18N
: "org/netbeans/modules/properties/propertiesObject.png"); // NOI18N
}
/** Overrides superclass method. Gets help context. */
@Override
public HelpCtx getHelpCtx() {
return new HelpCtx(Util.HELP_ID_EDITLOCALE);
}
/** Getter for pane. */
private JEditorPane getPane() {
return pane;
}
@Override
public Lookup getLookup() {
Lookup currentLookup = super.getLookup();
if (currentLookup != originalLookup || null == peLookup) {
originalLookup = currentLookup;
if(peLookup == null) {
peLookup = new PropertiesEditorLookup(Lookups.singleton(PropertiesEditor.this));
}
peLookup.updateLookups(originalLookup);
}
return peLookup;
}
@Override
public JComponent getVisualRepresentation() {
return this;
}
@Override
public void componentDeactivated() {
super.componentDeactivated();
}
@Override
public void componentClosed() {
super.componentClosed();
}
@Override
public JComponent getToolbarRepresentation() {
JToolBar toolBar = bar;
if (toolBar == null) {
JEditorPane lPane = getEditorPane();
if (lPane != null) {
Document doc = lPane.getDocument();
if (doc instanceof NbDocument.CustomToolbar) {
toolBar = ((NbDocument.CustomToolbar)doc).createToolbar(lPane);
}
}
if (toolBar == null) {
toolBar = new JToolBar();
}
bar = toolBar;
}
return toolBar;
}
@Override
public void setMultiViewCallback(MultiViewElementCallback callback) {
this.callback = callback;
PropertiesEditorSupport editor = (PropertiesEditorSupport) cloneableEditorSupport();
editor.attachStatusListener();
}
@Messages({
"MSG_SaveModified=File {0} is modified. Save?"
})
@Override
public CloseOperationState canCloseElement() {
final CloneableEditorSupport sup = getLookup().lookup(CloneableEditorSupport.class);
Enumeration en = getReference().getComponents();
if (en.hasMoreElements()) {
en.nextElement();
if (en.hasMoreElements()) {
// at least two is OK
return CloseOperationState.STATE_OK;
}
}
PropertiesDataObject dataObject = getDataObject();
if (dataObject.isModified()) {
AbstractAction save = new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
try {
sup.saveDocument();
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
}
};
save.putValue(Action.LONG_DESCRIPTION, Bundle.MSG_SaveModified(FileUtil.getFileDisplayName(dataObject.getPrimaryFile())));
return MultiViewFactory.createUnsafeCloseState("editor", save, null);
}
return CloseOperationState.STATE_OK;
}
@Override
public void componentActivated() {
super.componentActivated();
}
@Override
public void componentHidden() {
super.componentHidden();
}
@Override
public void componentOpened() {
super.componentOpened();
}
@Override
public void componentShowing() {
if (callback != null) {
updateName();
}
super.componentShowing();
}
@Override
public void requestVisible() {
if (callback != null) {
callback.requestVisible();
} else {
super.requestVisible();
}
}
@Override
public void requestActive() {
if (callback != null) {
callback.requestActive();
} else {
super.requestActive();
}
}
@Override
public void updateName() {
super.updateName();
Mutex.EVENT.writeAccess(
new Runnable() {
@Override
public void run() {
if (callback != null) {
TopComponent tc = callback.getTopComponent();
tc.setHtmlDisplayName(getHtmlDisplayName());
tc.setDisplayName(getDisplayName());
tc.setName(getName());
tc.setToolTipText(getToolTipText());
}
}
}
);
}
@Override
public void open() {
if (callback != null) {
callback.requestVisible();
} else {
super.open();
}
}
private PropertiesDataObject getDataObject() {
return (PropertiesDataObject) ((PropertiesEditorSupport) cloneableEditorSupport()).getDataObject();
}
} // End of nested class PropertiesEditor.
/** Inner class. UndoRedo manager which saves a StampFlag
* for each UndoAbleEdit.
*/
class UndoRedoStampFlagManager extends UndoRedo.Manager {
/** Hash map of weak reference keys (UndoableEdit's) to their StampFlag's. */
WeakHashMap<UndoableEdit,StampFlag> stampFlags
= new WeakHashMap<UndoableEdit,StampFlag>(5);
/** Overrides superclass method. Adds StampFlag to UndoableEdit. */
@Override
public synchronized boolean addEdit(UndoableEdit anEdit) {
stampFlags.put(anEdit, new StampFlag(System.currentTimeMillis(),
// ((PropertiesDataObject)PropertiesEditorSupport.this.myEntry.getDataObject()).getOpenSupport().atomicUndoRedoFlag ));
PropertiesEditorSupport.this.myEntry.atomicUndoRedoFlag ));
return super.addEdit(anEdit);
}
/** Overrides superclass method. Adds StampFlag to UndoableEdit. */
@Override
public boolean replaceEdit(UndoableEdit anEdit) {
stampFlags.put(anEdit, new StampFlag(System.currentTimeMillis(),
// ((PropertiesDataObject)PropertiesEditorSupport.this.myEntry.getDataObject()).getOpenSupport().atomicUndoRedoFlag ));
PropertiesEditorSupport.this.myEntry.atomicUndoRedoFlag ));
return super.replaceEdit(anEdit);
}
/** Overrides superclass method. Updates time stamp for the edit. */
@Override
public synchronized void undo() throws CannotUndoException {
UndoableEdit anEdit = editToBeUndone();
if(anEdit != null) {
Object atomicFlag = stampFlags.get(anEdit).getAtomicFlag(); // atomic flag remains
super.undo();
stampFlags.put(anEdit, new StampFlag(System.currentTimeMillis(), atomicFlag));
}
}
/** Overrides superclass method. Updates time stamp for that edit. */
@Override
public synchronized void redo() throws CannotRedoException {
UndoableEdit anEdit = editToBeRedone();
if(anEdit != null) {
Object atomicFlag = stampFlags.get(anEdit).getAtomicFlag(); // atomic flag remains
super.redo();
stampFlags.put(anEdit, new StampFlag(System.currentTimeMillis(), atomicFlag));
}
}
/** Method which gets time stamp of next Undoable edit to be undone.
* @ return time stamp in milliseconds or 0 (if don't exit edit to be undone). */
public long getTimeStampOfEditToBeUndone() {
UndoableEdit nextUndo = editToBeUndone();
if (nextUndo == null) {
return 0L;
} else {
return stampFlags.get(nextUndo).getTimeStamp();
}
}
/** Method which gets time stamp of next Undoable edit to be redone.
* @ return time stamp in milliseconds or 0 (if don't exit edit to be redone). */
public long getTimeStampOfEditToBeRedone() {
UndoableEdit nextRedo = editToBeRedone();
if (nextRedo == null) {
return 0L;
} else {
return stampFlags.get(nextRedo).getTimeStamp();
}
}
/** Method which gets atomic flag of next Undoable edit to be undone.
* @ return atomic flag in milliseconds or 0 (if don't exit edit to be undone). */
public Object getAtomicFlagOfEditToBeUndone() {
UndoableEdit nextUndo = editToBeUndone();
if (nextUndo == null) {
return null;
} else {
return (stampFlags.get(nextUndo)).getAtomicFlag();
}
}
/** Method which gets atomic flag of next Undoable edit to be redone.
* @ return time stamp in milliseconds or 0 (if don't exit edit to be redone). */
public Object getAtomicFlagOfEditToBeRedone() {
UndoableEdit nextRedo = editToBeRedone();
if (nextRedo == null) {
return null;
} else {
return (stampFlags.get(nextRedo)).getAtomicFlag();
}
}
} // End of inner class UndoRedoTimeStampManager.
/** Simple nested class for storing time stamp and atomic flag used
* in <code>UndoRedoStampFlagManager</code>.
*/
static class StampFlag {
/** Time stamp when was an UndoableEdit (to which is this class mapped via
* UndoRedoStampFlagManager,) was created, replaced, undone, or redone. */
private long timeStamp;
/** Atomic flag. If this object is not null it means that an UndoableEdit ( to which
* is this class mapped via UndoRedoStampFlagManager,) was created as part of one
* action which could consist from more UndoableEdits in differrent editor supports.
* These Undoable edits are marked with this (i.e. same) object. */
private Object atomicFlag;
/** Consructor. */
public StampFlag(long timeStamp, Object atomicFlag) {
this.timeStamp = timeStamp;
this.atomicFlag = atomicFlag;
}
/** Getter for time stamp. */
public long getTimeStamp() {
return timeStamp;
}
/** Setter for time stamp. */
public void setTimeStamp(long timeStamp) {
this.timeStamp = timeStamp;
}
/** Getter for atomic flag.
@ return Returns null if is not linked with more Undoable edits.*/
public Object getAtomicFlag() {
return atomicFlag;
}
} // End of nested class TimeStamp.
private static class SaveImpl implements AtomicAction {
private static final SaveImpl DEFAULT = new SaveImpl(null);
private final PropertiesEditorSupport des;
public SaveImpl(PropertiesEditorSupport des) {
this.des = des;
}
public void run() throws IOException {
if (des.desEnv().isModified() && des.isEnvReadOnly()) {
IOException e = new IOException("File is read-only: " + ((Environment) des.env).getFileImpl()); // NOI18N
// UIException.annotateUser(e, null, org.openide.util.NbBundle.getMessage(org.openide.loaders.DataObject.class, "MSG_FileReadOnlySaving", new java.lang.Object[]{((org.netbeans.modules.properties.PropertiesEditorSupport.Environment) des.env).getFileImpl().getNameExt()}), null, null);
throw e;
}
des.superSaveDoc();
}
@Override
public int hashCode() {
return getClass().hashCode();
}
@Override
public boolean equals(Object obj) {
return obj != null && getClass() == obj.getClass();
}
}
private static final class PropertiesEditorLookup extends ProxyLookup {
private Lookup initialLookup;
public PropertiesEditorLookup(Lookup lookup) {
super(lookup);
this.initialLookup = lookup;
}
public void updateLookups(Lookup additionalLookup) {
setLookups(new Lookup[] {initialLookup, additionalLookup});
}
}
}