| /* |
| * 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.openide.loaders; |
| |
| |
| import java.awt.Component; |
| import java.awt.Graphics; |
| import java.beans.*; |
| import java.io.*; |
| import java.lang.annotation.ElementType; |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.lang.annotation.Target; |
| import java.text.DateFormat; |
| import java.util.*; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; |
| import java.util.logging.Level; |
| import java.util.logging.Logger; |
| import javax.swing.Icon; |
| import javax.swing.event.*; |
| import org.netbeans.api.actions.Savable; |
| import org.netbeans.api.progress.ProgressHandle; |
| import org.netbeans.api.progress.ProgressHandleFactory; |
| import org.netbeans.modules.openide.loaders.DataObjectAccessor; |
| import org.netbeans.modules.openide.loaders.DataObjectEncodingQueryImplementation; |
| import org.netbeans.modules.openide.loaders.Unmodify; |
| import org.netbeans.spi.actions.AbstractSavable; |
| import org.openide.cookies.SaveCookie; |
| import org.openide.filesystems.*; |
| import org.openide.nodes.*; |
| import org.openide.util.*; |
| |
| /** Object that represents one or more file objects, with added behavior |
| * accessible though {@link #getLookup} lookup pattern. Since version 6.0 |
| * this class implements {@link org.openide.util.Lookup.Provider}. |
| * |
| * @author Jaroslav Tulach, Petr Hamernik, Jan Jancura, Ian Formanek |
| */ |
| public abstract class DataObject extends Object |
| implements Node.Cookie, Serializable, HelpCtx.Provider, Lookup.Provider { |
| /** generated Serialized Version UID */ |
| private static final long serialVersionUID = 3328227388376142699L; |
| |
| /** Name of the template property. */ |
| public static final String PROP_TEMPLATE = "template"; // NOI18N |
| |
| /** Name of the name property. */ |
| public static final String PROP_NAME = "name"; // NOI18N |
| |
| /** Name of the help context property. */ |
| public static final String PROP_HELP = "helpCtx"; // NOI18N |
| |
| /** Name of the modified property. */ |
| public static final String PROP_MODIFIED = "modified"; // NOI18N |
| |
| /** Name of the property used during notification of changes in the set of cookies attached to this object. */ |
| public static final String PROP_COOKIE = Node.PROP_COOKIE; |
| |
| /** Name of valid property. Allows listening to deletion or disposal of the data object. */ |
| public static final String PROP_VALID = "valid"; // NOI18N |
| |
| /** Name of primary file property. Primary file is changed when the object is moved */ |
| public static final String PROP_PRIMARY_FILE = "primaryFile"; // NOI18N |
| /** Name of files property. Allows listening to set of files handled by this object. */ |
| public static final String PROP_FILES = "files"; // NOI18N |
| |
| private static ThreadLocal<ProgressInfo> PROGRESS_INFO_TL |
| = new ThreadLocal<ProgressInfo>(); |
| |
| /** Extended attribute for holding the class of the loader that should |
| * be used to recognize a file object before the normal processing takes |
| * place. |
| */ |
| static final String EA_ASSIGNED_LOADER = "NetBeansAttrAssignedLoader"; // NOI18N |
| /** Extended attribute which may be used in addition to EA_ASSIGNED_LOADER |
| * which indicates the code name base of the module that installed that preferred |
| * loader. If the indicated module is not installed, ignore the loader request. |
| * See #13816. |
| */ |
| static final String EA_ASSIGNED_LOADER_MODULE = "NetBeansAttrAssignedLoaderModule"; // NOI18N |
| |
| private static final Logger OBJ_LOG = Logger.getLogger(DataObject.class.getName()); |
| |
| /** |
| * CAS for {@link DataObject#changeSupport}. |
| */ |
| private static final AtomicReferenceFieldUpdater<DataObject, PropertyChangeSupport> changeSupportUpdater = |
| AtomicReferenceFieldUpdater.newUpdater( |
| DataObject.class, |
| PropertyChangeSupport.class, |
| "changeSupport"); //NOI18N |
| |
| /** all modified data objects contains DataObjects. |
| * ! Use syncModified for modifications instead !*/ |
| private static final ModifiedRegistry modified = new ModifiedRegistry(); |
| /** sync modified data (for modification operations) */ |
| private static final Set<DataObject> syncModified = Collections.synchronizedSet(modified); |
| |
| /** Modified flag |
| * @GuardedBy(LOCK) |
| */ |
| private boolean modif = false; |
| |
| /** the node delegate for this data object |
| * @GuardedBy(LOCK) |
| */ |
| private transient Node nodeDelegate; |
| private static final Node BEING_CREATED = Node.EMPTY.cloneNode(); |
| |
| /** item with info about this data object |
| * @GuardedBy(DataObjectPool.getPOOL()) |
| */ |
| private transient DataObjectPool.Item item; |
| |
| /** the loader for this data object */ |
| private final DataLoader loader; |
| |
| /** property change listener support. |
| * Threading: lock free, changes HAS to go through {@link DataObject#changeSupportUpdater}. |
| */ |
| private volatile PropertyChangeSupport changeSupport; |
| |
| /** vetoable property change listener support |
| * @GuardedBy(LOCK) |
| */ |
| private VetoableChangeSupport vetoableChangeSupport; |
| |
| /** Lock used for ensuring there will be just one node delegate |
| * and also to synchronize on other changed aspects inside of a single DataObject |
| */ |
| private static final Object LOCK = new Object(); |
| |
| /** Lock for copy/move/rename/etc. operations */ |
| private static Object synchObject = new Object (); |
| |
| |
| /** default logger for whole package */ |
| static final Logger LOG = Logger.getLogger("org.openide.loaders"); // NOI18N |
| |
| static { |
| DataObjectAccessor.DEFAULT = new DataObjectAccessorImpl(); |
| } |
| |
| /** Create a new data object. |
| * |
| * @param pf primary file object for this data object |
| * @param loader loader that created the data object |
| * @exception DataObjectExistsException if there is already a data object |
| * for this primary file |
| */ |
| public DataObject (FileObject pf, DataLoader loader) throws DataObjectExistsException { |
| // By registering we'll also get notifications about file changes. |
| this (pf, DataObjectPool.getPOOL().register (pf, loader), loader); |
| } |
| |
| /** Private constructor. At this time the constructor receives |
| * the primary file and pool item where it should register itself. |
| * |
| * @param pf primary file |
| * @param item the item to register into |
| * @param loader loader that created the data object |
| */ |
| private DataObject (FileObject pf, DataObjectPool.Item item, DataLoader loader) { |
| OBJ_LOG.log(Level.FINE, "created {0}", pf); // NOI18N |
| this.item = item; |
| this.loader = loader; |
| item.setDataObject (this); |
| } |
| |
| // This method first unregisters the object, then calls method unreferenced. |
| // After that it asks the parent folder to regenerate its list of children, |
| // so different object is usually created for primary file of this object. |
| /** Allows subclasses to discard the object. When an object is discarded, |
| * it is released from the list of objects registered in the system. |
| * Then the contents of the parent folder (if it still exists) are rescanned, which |
| * may result in the creation of a new data object for the primary file. |
| * <P> |
| * The normal use of this method is to change the type of a data object. |
| * Because this would usually only be invoked from |
| * the original data object, it is protected. |
| */ |
| protected void dispose () { |
| DataObjectPool.Item i = item(); |
| |
| if (i != null) { |
| DataObjectPool.getPOOL().countRegistration(i.primaryFile); |
| i.deregister (true); |
| i.setDataObject(null); |
| firePropertyChange (PROP_VALID, Boolean.TRUE, Boolean.FALSE); |
| } |
| } |
| |
| // |
| // Item accessors |
| // |
| final DataObjectPool.Item item() { |
| synchronized (DataObjectPool.getPOOL()) { |
| return item; |
| } |
| } |
| private void changeItem(DataObjectPool.Item item) { |
| synchronized (DataObjectPool.getPOOL()) { |
| this.item = item; |
| } |
| } |
| final void changeItemByFolder(DataObjectPool.Item item) { |
| assert this instanceof DataFolder; |
| this.changeItem(item); |
| } |
| |
| /** Setter that allows to destroy this data object. Because such |
| * operation can be dangerous and not always possible (if the data object |
| * is opened in editor) it can be vetoed. Either by this data object |
| * or by any vetoable listener attached to this object (like editor support) |
| * |
| * @param valid should be false |
| * @exception PropertyVetoException if the invalidation has been vetoed |
| */ |
| public void setValid (boolean valid) throws PropertyVetoException { |
| if (!valid && isValid ()) { |
| markInvalid0 (); |
| } |
| } |
| |
| /** Tries to mark the object invalid. Called from setValid or from |
| * MultiDataObject.notifyDeleted |
| */ |
| final void markInvalid0 () throws PropertyVetoException { |
| fireVetoableChange (PROP_VALID, Boolean.TRUE, Boolean.FALSE); |
| dispose (); |
| setModified(false); |
| } |
| |
| /** Test whether the data object is still valid and usable. |
| * <P> |
| * The object can become invalid when it is deleted, its files are deleted, or |
| * {@link #dispose} is called. |
| * <P> |
| * When the validity of the object changes a property change event is fired, so |
| * anyone can listen and be notified when the object is deleted/disposed. |
| */ |
| public final boolean isValid () { |
| return item().isValid (); |
| } |
| |
| |
| |
| /** Get the loader that created this data object. |
| * @return the data loader |
| */ |
| public final DataLoader getLoader () { |
| return loader; |
| } |
| |
| /** Mark all contained files as belonging to this loader. |
| * If the files are rescanned (e.g. after a disposal), the current data loader will be given preference. |
| */ |
| protected final void markFiles () throws IOException { |
| Iterator<FileObject> en = files ().iterator (); |
| while (en.hasNext ()) { |
| FileObject fo = en.next (); |
| loader.markFile (fo); |
| } |
| } |
| |
| /** Get all contained files. |
| * These file objects should ideally have had the {@linkplain FileObject#setImportant important flag} set appropriately. |
| * <P> |
| * The default implementation returns a set consisting only of the primary file. |
| * |
| * @return set of files |
| */ |
| public Set<FileObject> files() { |
| return java.util.Collections.singleton (getPrimaryFile ()); |
| } |
| |
| |
| /** Get the node delegate. Either {@link #createNodeDelegate creates it} (if it does not |
| * already exist) or |
| * returns a previously created instance of it. |
| * @return the node delegate (without parent) for this data object |
| * @see <a href="doc-files/api.html#delegate">Datasystems API - Node Delegates</a> |
| */ |
| public final Node getNodeDelegate () { |
| if (! isValid()) { |
| String debugMessage = "this=" + this + " id=" + System.identityHashCode(this) + " primaryFileId=" + System.identityHashCode(this.getPrimaryFile()) + " valid=" + this.getPrimaryFile().isValid() + "\n"; //NOI18N |
| DataObject dob = DataObjectPool.getPOOL().find(getPrimaryFile()); |
| debugMessage += "pool=" + dob; //NOI18N |
| if (dob != null) { |
| debugMessage += " id=" + System.identityHashCode(dob); //NOI18N |
| if (dob.getPrimaryFile() != null) { |
| debugMessage += " primaryFileId=" + System.identityHashCode(dob.getPrimaryFile()) + " valid=" + dob.getPrimaryFile().isValid(); //NOI18N |
| } |
| } |
| |
| Exception e = new IllegalStateException("The data object " + getPrimaryFile() + " is invalid; you may not call getNodeDelegate on it any more; see #17020 and please fix your code.\n" + debugMessage); // NOI18N |
| Logger.getLogger(DataObject.class.getName()).log(Level.INFO, null, e); |
| } |
| return getNodeDelegateImpl(); |
| } |
| |
| private final Node getNodeDelegateImpl() { |
| for (;;) { |
| synchronized (LOCK) { |
| if (nodeDelegate != null && nodeDelegate != BEING_CREATED) { |
| return nodeDelegate; |
| } |
| } |
| // synchronize on something private, so only one delegate can be created |
| // do not synchronize on this, because we could deadlock with |
| // subclasses could synchronize too. |
| Children.MUTEX.readAccess (new Runnable() { |
| @Override |
| public void run() { |
| synchronized(LOCK) { |
| if (nodeDelegate == null) { |
| nodeDelegate = BEING_CREATED; |
| } else { |
| if (nodeDelegate == BEING_CREATED) { |
| try { |
| LOCK.wait(); |
| } catch (InterruptedException ex) { |
| LOG.log(Level.FINE, null, ex); |
| } |
| } |
| return; |
| } |
| } |
| Node newNode = createNodeDelegate(); |
| synchronized (LOCK) { |
| if (nodeDelegate == BEING_CREATED) { |
| nodeDelegate = newNode; |
| } |
| LOCK.notifyAll(); |
| } |
| } |
| }); |
| |
| synchronized (LOCK) { |
| if (nodeDelegate == null) { |
| throw new IllegalStateException("DataObject " + this + " has null node delegate"); // NOI18N |
| } |
| } |
| } |
| } |
| |
| /** This method allows DataFolder to filter its nodes. |
| * |
| * @param filter filter for subdata objects |
| * @return the node delegate (without parent) the node is new instance |
| * of node and can be inserted to any place in the hierarchy |
| */ |
| Node getClonedNodeDelegate (DataFilter filter) { |
| return getNodeDelegate ().cloneNode (); |
| } |
| |
| /** Access method for node delagate. |
| * @return node delegate or null |
| */ |
| final Node getNodeDelegateOrNull () { |
| synchronized (LOCK) { |
| return nodeDelegate; |
| } |
| } |
| |
| final void setNodeDelegate(Node n) { |
| synchronized (LOCK) { |
| nodeDelegate = n; |
| } |
| } |
| |
| /** Provides node that should represent this data object. |
| * <p>The default implementation creates an instance of {@link DataNode}. |
| * Most subclasses will override this method to provide a <code>DataNode</code> |
| * (usually subclassed). |
| * <P> |
| * This method is called only once per data object. |
| * <p>It is strongly recommended that the resulting node will, when asked for |
| * the cookie <samp>DataObject.class</samp>, return this same data object. |
| * <p>It is also recommended that the node: |
| * <ol> |
| * <li>Base its name on {@link #getName}. |
| * <li>Base its display name additionally on {@link DataNode#getShowFileExtensions}. |
| * <li>Tune its display name and icon according to {@link org.openide.filesystems.FileSystem.Status}. |
| * </ol> |
| * @return the node delegate (without parent) for this data object |
| * @see <a href="doc-files/api.html#create-delegate">Datasystems API - Creating a node delegate</a> |
| */ |
| protected Node createNodeDelegate () { |
| return new DataNode (this, Children.LEAF); |
| } |
| |
| /** Obtains lock for primary file. |
| * |
| * @return the lock |
| * @exception IOException if taking the lock fails |
| */ |
| protected FileLock takePrimaryFileLock () throws IOException { |
| return getPrimaryFile ().lock (); |
| } |
| |
| /** Package private method to assign template attribute to a file. |
| * Used also from FileEntry. |
| * |
| * @param fo the file |
| * @param newTempl is template or not |
| * @return true if the value change/false otherwise |
| */ |
| static boolean setTemplate (FileObject fo, boolean newTempl) throws IOException { |
| boolean oldTempl = false; |
| |
| Object o = fo.getAttribute(DataObject.PROP_TEMPLATE); |
| if ((o instanceof Boolean) && ((Boolean)o).booleanValue()) |
| oldTempl = true; |
| if (oldTempl == newTempl) |
| return false; |
| |
| fo.setAttribute(DataObject.PROP_TEMPLATE, (newTempl ? Boolean.TRUE : null)); |
| |
| return true; |
| } |
| |
| /** Set the template status of this data object. |
| * @param newTempl <code>true</code> if the object should be a template |
| * @exception IOException if setting the template state fails |
| */ |
| public final void setTemplate (boolean newTempl) throws IOException { |
| if (!setTemplate (getPrimaryFile(), newTempl)) { |
| // no change in state |
| return; |
| } |
| |
| firePropertyChange(DataObject.PROP_TEMPLATE, |
| !newTempl ? Boolean.TRUE : Boolean.FALSE, |
| newTempl ? Boolean.TRUE : Boolean.FALSE); |
| } |
| |
| /** Get the template status of this data object. |
| * @return <code>true</code> if it is a template |
| */ |
| public final boolean isTemplate () { |
| Object o = getPrimaryFile().getAttribute(PROP_TEMPLATE); |
| boolean ret = false; |
| if (o instanceof Boolean) |
| ret = ((Boolean) o).booleanValue(); |
| return ret; |
| } |
| |
| |
| /** Test whether the object may be deleted. |
| * @return <code>true</code> if it may |
| */ |
| public abstract boolean isDeleteAllowed (); |
| |
| /** Test whether the object may be copied. |
| * @return <code>true</code> if it may |
| */ |
| public abstract boolean isCopyAllowed (); |
| |
| /** Test whether the object may be moved. |
| * @return <code>true</code> if it may |
| */ |
| public abstract boolean isMoveAllowed (); |
| |
| /** Test whether the object may create shadows. |
| * <p>The default implementation returns <code>true</code>. |
| * @return <code>true</code> if it may |
| */ |
| public boolean isShadowAllowed () { |
| return true; |
| } |
| |
| /** Test whether the object may be renamed. |
| * @return <code>true</code> if it may |
| */ |
| public abstract boolean isRenameAllowed (); |
| |
| |
| /** Test whether the object is modified. |
| * @return <code>true</code> if it is modified |
| */ |
| public boolean isModified() { |
| synchronized (LOCK) { |
| return modif; |
| } |
| } |
| |
| /** Set whether the object is considered modified. |
| * Also fires a change event. |
| * If the new value is <code>true</code>, the data object is added into a {@link #getRegistry registry} of opened data objects. |
| * If the new value is <code>false</code>, |
| * the data object is removed from the registry. |
| */ |
| public void setModified(boolean modif) { |
| boolean log = OBJ_LOG.isLoggable(Level.FINE); |
| synchronized (LOCK) { |
| if (log) { |
| String msg = "setModified(): modif=" + modif + ", original-modif=" + this.modif; // NOI18N |
| if (OBJ_LOG.isLoggable(Level.FINEST)) { |
| OBJ_LOG.log(Level.FINEST, msg, new Exception()); |
| } else { |
| OBJ_LOG.log(Level.FINE, msg); |
| } |
| } |
| if (this.modif == modif) { |
| return; |
| } |
| this.modif = modif; |
| } |
| Savable present = getLookup().lookup(AbstractSavable.class); |
| if (log) { |
| OBJ_LOG.log(Level.FINE, "setModified(): present={0}", new Object[]{present}); // NOI18N |
| } |
| if (modif) { |
| syncModified.add (this); |
| if (present == null) { |
| new DOSavable(this).add(); |
| } |
| } else { |
| syncModified.remove (this); |
| if (present == null) { |
| new DOSavable(this).remove(); |
| } |
| Unmodify un = getLookup().lookup(Unmodify.class); |
| if (un != null) { |
| un.unmodify(); |
| } |
| } |
| firePropertyChange(DataObject.PROP_MODIFIED, |
| !modif ? Boolean.TRUE : Boolean.FALSE, |
| modif ? Boolean.TRUE : Boolean.FALSE); |
| } |
| |
| /** Get help context for this object. |
| * @return the help context |
| */ |
| public abstract HelpCtx getHelpCtx (); |
| |
| /** Get the primary file for this data object. |
| * For example, |
| * Java source uses <code>*.java</code> and <code>*.class</code> files but the primary one is |
| * always <code>*.java</code>. Please note that two data objects are {@link #equals equivalent} if |
| * they use the same primary file. |
| * <p><em>Warning:</em> do not call {@link Node#getHandle} or {@link DefaultHandle#createHandle} in this method. |
| * |
| * @return the primary file |
| */ |
| public final FileObject getPrimaryFile () { |
| return item().primaryFile; |
| } |
| |
| /** Finds the data object for a specified file object. |
| * @param fo file object |
| * @return the data object for that file |
| * @exception DataObjectNotFoundException if the file does not have a |
| * data object |
| */ |
| @NbBundle.Messages({ |
| "# {0} - the path", |
| "EXC_FIND_4_INVALID=The file {0} seems no longer valid!" |
| }) |
| public static DataObject find (FileObject fo) |
| throws DataObjectNotFoundException { |
| if (fo == null) |
| throw new IllegalArgumentException("Called DataObject.find on null"); // NOI18N |
| |
| try { |
| if (!fo.isValid()) { |
| FileStateInvalidException ex = new FileStateInvalidException(fo.toString()); |
| Exceptions.attachLocalizedMessage(ex, Bundle.EXC_FIND_4_INVALID(fo.getPath())); |
| throw ex; |
| } |
| |
| // try to scan directly the pool (holds only primary files) |
| DataObject obj = DataObjectPool.getPOOL().find (fo); |
| if (obj != null) { |
| return obj; |
| } |
| |
| // try to use the loaders machinery |
| DataLoaderPool p = DataLoaderPool.getDefault(); |
| assert p != null : "No DataLoaderPool found in " + Lookup.getDefault(); |
| obj = p.findDataObject (fo); |
| if (obj != null) { |
| return obj; |
| } |
| |
| throw new DataObjectNotFoundException (fo); |
| } catch (DataObjectExistsException ex) { |
| return ex.getDataObject (); |
| } catch (IOException ex) { |
| throw (DataObjectNotFoundException) new DataObjectNotFoundException(fo).initCause(ex); |
| } |
| } |
| |
| /** the only instance */ |
| private static Registry REGISTRY_INSTANCE = new Registry(); |
| |
| /** Get the registry containing all modified objects. |
| * |
| * @return the registry |
| */ |
| public static Registry getRegistry () { |
| return REGISTRY_INSTANCE; |
| } |
| |
| /** Get the name of the data object. |
| * <p>The default implementation uses the name of the primary file. |
| * @return the name |
| */ |
| public String getName () { |
| return getPrimaryFile ().getName (); |
| } |
| |
| @Override |
| public String toString () { |
| return super.toString () + '[' + getPrimaryFile () + ']'; |
| } |
| |
| /** Get the folder this data object is stored in. |
| * @return the folder; <CODE>null</CODE> if the primary file |
| * is the {@link FileObject#isRoot root} of its filesystem |
| */ |
| public final DataFolder getFolder () { |
| FileObject fo = getPrimaryFile ().getParent (); |
| // could throw IllegalArgumentException but only if fo is not folder |
| // => then there is a bug in filesystem implementation |
| return fo == null ? null : DataFolder.findFolder (fo); |
| } |
| |
| /** Copy this object to a folder. The copy of the object is required to |
| * be deletable and movable. |
| * <p>An event is fired, and atomicity is implemented. |
| * @param f the folder to copy the object to |
| * @exception IOException if something went wrong |
| * @return the new object |
| */ |
| @NbBundle.Messages({ |
| "# {0} - File name", |
| "LBL_Copying=Copying {0}" |
| }) |
| public final DataObject copy (final DataFolder f) throws IOException { |
| ProgressInfo pi = getProgressInfo(); |
| if (pi == null) { |
| pi = initProgressInfo(Bundle.LBL_Copying(this.getName()), this); |
| } else if (pi.isTerminated()) { |
| return null; |
| } |
| try { |
| pi.updateProgress(this); |
| final DataObject[] result = new DataObject[1]; |
| invokeAtomicAction(f.getPrimaryFile(), new FileSystem.AtomicAction() { |
| public void run () throws IOException { |
| result[0] = handleCopy (f); |
| } |
| }, null); |
| fireOperationEvent( |
| new OperationEvent.Copy(result[0], this), OperationEvent.COPY); |
| return result[0]; |
| } finally { |
| finishProgressInfoIfDone(pi, this); |
| } |
| } |
| |
| /** Copy this object to a folder (implemented by subclasses). |
| * @param f target folder |
| * @return the new data object |
| * @exception IOException if an error occures |
| */ |
| protected abstract DataObject handleCopy (DataFolder f) throws IOException; |
| |
| /** Copy this object to a folder under a different name and file extension. |
| * The copy of the object is required to be deletable and movable. |
| * <p>An event is fired, and atomicity is implemented. |
| * @param f the folder to copy the object to |
| * @exception IOException if something went wrong |
| * @return the new object |
| * @since 6.3 |
| */ |
| final DataObject copyRename (final DataFolder f, final String name, final String ext) throws IOException { |
| final DataObject[] result = new DataObject[1]; |
| invokeAtomicAction (f.getPrimaryFile (), new FileSystem.AtomicAction () { |
| public void run () throws IOException { |
| result[0] = handleCopyRename (f, name, ext); |
| } |
| }, null); |
| fireOperationEvent ( |
| new OperationEvent(result[0]), OperationEvent.CREATE |
| ); |
| return result[0]; |
| } |
| /** |
| * Copy and rename this object to a folder (implemented by subclasses). |
| * @param f target folder |
| * @param name new file name |
| * @param ext new file extension |
| * @return the new data object |
| * @exception IOException if an error occures or the file cannot be copied/renamed |
| * @since 6.3 |
| */ |
| protected DataObject handleCopyRename (DataFolder f, String name, String ext) throws IOException { |
| throw new IOException( "Unsupported operation" ); //NOI18N |
| } |
| |
| /** Delete this object. |
| * <p>Events are fired and atomicity is implemented. |
| * @exception IOException if an error occures |
| */ |
| @NbBundle.Messages({ |
| "# {0} - Deleted file or folder", |
| "LBL_Deleting=Deleting {0}"}) |
| public final void delete () throws IOException { |
| ProgressInfo pi = getProgressInfo(); |
| if (pi == null) { |
| pi = initProgressInfo(Bundle.LBL_Deleting(this.getName()), this); |
| } else if (pi.isTerminated()) { |
| return; |
| } |
| try { |
| pi.updateProgress(this); |
| // the object is ready to be closed |
| invokeAtomicAction(getPrimaryFile(), new FileSystem.AtomicAction() { |
| public void run () throws IOException { |
| handleDelete (); |
| if (isCurrentActionTerminated() && isValid()) { |
| return; |
| } |
| DataObjectPool.getPOOL().countRegistration(item().primaryFile); |
| item().deregister(false); |
| item().setDataObject(null); |
| } |
| }, synchObject()); |
| if (pi.isTerminated() && isValid()) { |
| return; |
| } |
| firePropertyChange(PROP_VALID, Boolean.TRUE, Boolean.FALSE); |
| fireOperationEvent(new OperationEvent(this), OperationEvent.DELETE); |
| } finally { |
| finishProgressInfoIfDone(pi, this); |
| } |
| } |
| |
| /** Delete this object (implemented by subclasses). |
| * @exception IOException if an error occures |
| */ |
| protected abstract void handleDelete () throws IOException; |
| |
| |
| /** Rename this object. |
| * <p>Events are fired and atomicity is implemented. |
| * |
| * @param name the new name |
| * |
| * @exception IOException if an error occurs |
| */ |
| public final void rename (String name) throws IOException { |
| if (name == null || name.trim ().length ()==0) { |
| IllegalArgumentException iae = new IllegalArgumentException (this.getName ()); |
| String msg = NbBundle.getMessage (DataObject.class, |
| "MSG_NotValidName", getName ()); // NOI18N |
| Exceptions.attachLocalizedMessage(iae, msg); |
| throw iae; |
| } |
| |
| |
| class Op implements FileSystem.AtomicAction { |
| FileObject oldPf; |
| FileObject newPf; |
| |
| String oldName; |
| String newName; |
| public void run() throws IOException { |
| oldName = getName (); |
| |
| if (oldName.equals (newName)) return; // the new name is the same as the old one |
| |
| oldPf = getPrimaryFile (); |
| newPf = handleRename (newName); |
| if (oldPf != newPf) { |
| changeItem(item().changePrimaryFile(newPf)); |
| } |
| newName = getName (); |
| } |
| } |
| |
| // executes atomic action with renaming |
| Op op = new Op(); |
| op.newName = name; |
| FileObject target = getPrimaryFile().getParent(); |
| if (target == null) { |
| target = getPrimaryFile(); |
| } |
| invokeAtomicAction (target, op, synchObject()); |
| |
| if (op.oldName.equals (op.newName)) { |
| return; // the new name is the same as the old one |
| } |
| |
| if (op.oldPf != op.newPf) { |
| firePropertyChange (PROP_PRIMARY_FILE, op.oldPf, op.newPf); |
| } |
| firePropertyChange (PROP_NAME, op.oldName, op.newName); |
| firePropertyChange (PROP_FILES, null, null); |
| |
| fireOperationEvent (new OperationEvent.Rename (this, op.oldName), OperationEvent.RENAME); |
| } |
| |
| /** Rename this object (implemented in subclasses). |
| * |
| * @param name name to rename the object to |
| * @return new primary file of the object |
| * @exception IOException if an error occures |
| */ |
| protected abstract FileObject handleRename (String name) throws IOException; |
| |
| /** Move this object to another folder. |
| * <p>An event is fired and atomicity is implemented. |
| * @param df folder to move object to |
| * @exception IOException if an error occurs |
| */ |
| @NbBundle.Messages({ |
| "# {0} - File name", |
| "LBL_Moving=Moving {0}"}) |
| public final void move (final DataFolder df) throws IOException { |
| class Op implements FileSystem.AtomicAction { |
| FileObject old; |
| public void run () throws IOException { |
| if ((getFolder () == null)) return; // cannot move filesystem root |
| if (df.equals (getFolder ())) return; // if the destination folder is the same as the current one ==>> do nothing |
| |
| // executes atomic action for moving |
| old = getPrimaryFile (); |
| FileObject mf = handleMove (df); |
| changeItem(item().changePrimaryFile (mf)); |
| } |
| } |
| Op op = new Op(); |
| ProgressInfo pi = getProgressInfo(); |
| if (pi == null) { |
| pi = initProgressInfo(Bundle.LBL_Moving(this.getName()), this); |
| } else if (pi.isTerminated()) { |
| return; |
| } |
| try { |
| pi.updateProgress(this); |
| invokeAtomicAction(df.getPrimaryFile(), op, synchObject()); |
| |
| firePropertyChange(PROP_PRIMARY_FILE, op.old, getPrimaryFile()); |
| fireOperationEvent( |
| new OperationEvent.Move(this, op.old), OperationEvent.MOVE); |
| } finally { |
| finishProgressInfoIfDone(pi, this); |
| } |
| } |
| |
| /** Move this object to another folder (implemented in subclasses). |
| * |
| * @param df target data folder |
| * @return new primary file of the object |
| * @exception IOException if an error occures |
| */ |
| protected abstract FileObject handleMove (DataFolder df) throws IOException; |
| |
| /** Creates shadow for this object in specified folder (overridable in subclasses). |
| * <p>The default |
| * implementation creates a reference data shadow and pastes it into |
| * the specified folder. |
| * |
| * @param f the folder to create a shortcut in |
| * @return the shadow |
| */ |
| protected DataShadow handleCreateShadow (DataFolder f) throws IOException { |
| return DataShadow.create (f, this); |
| } |
| |
| /** Creates shadow for this object in specified folder. |
| * <p>An event is fired and atomicity is implemented. |
| * |
| * @param f the folder to create shortcut in |
| * @return the shadow |
| */ |
| public final DataShadow createShadow (final DataFolder f) throws IOException { |
| final DataShadow[] result = new DataShadow[1]; |
| |
| invokeAtomicAction (f.getPrimaryFile (), new FileSystem.AtomicAction () { |
| public void run () throws IOException { |
| result[0] = handleCreateShadow (f); |
| } |
| }, null); |
| fireOperationEvent ( |
| new OperationEvent.Copy (result[0], this), OperationEvent.SHADOW |
| ); |
| return result[0]; |
| } |
| |
| /** Create a new object from template (with a name depending on the template). |
| * |
| * @param f folder to create object in |
| * @return new data object based on this one |
| * @exception IOException if an error occured |
| * @see #createFromTemplate(DataFolder,String) |
| */ |
| public final DataObject createFromTemplate (DataFolder f) |
| throws IOException { |
| return createFromTemplate (f, null); |
| } |
| |
| /** Create a new object from template. |
| * Asks {@link #handleCreateFromTemplate}. |
| * |
| * @param f folder to create object in |
| * @param name name of object that should be created, or <CODE>null</CODE> if the |
| * name should be same as that of the template (or otherwise mechanically generated) |
| * @return the new data object |
| * @exception IOException if an error occured |
| */ |
| public final DataObject createFromTemplate ( |
| final DataFolder f, final String name |
| ) throws IOException { |
| return createFromTemplate(f, name, Collections.<String,Object>emptyMap()); |
| } |
| |
| /** More generic way how to instantiate a {@link DataObject}. One can |
| * not only specify its name, but also pass a map of parameters that |
| * can influence the copying of the stream. |
| * |
| * @param f folder to create object in |
| * @param name name of object that should be created, or <CODE>null</CODE> if the |
| * name should be same as that of the template (or otherwise mechanically generated) |
| * @param parameters map of named objects that are going to be used when |
| * creating the new object |
| * @return the new data object |
| * @exception IOException if an error occured |
| * @since 6.1 |
| */ |
| public final DataObject createFromTemplate( |
| final DataFolder f, final String name, final Map<String,? extends Object> parameters |
| ) throws IOException { |
| CreateAction create = new CreateAction(this, f, name, parameters); |
| invokeAtomicAction (f.getPrimaryFile (), create, null); |
| fireOperationEvent ( |
| new OperationEvent.Copy (create.result, this), OperationEvent.TEMPL |
| ); |
| return create.result; |
| } |
| |
| /** Create a new data object from template (implemented in subclasses). |
| * This method should |
| * copy the content of the template to the destination folder and assign a new name |
| * to the new object. |
| * |
| * @param df data folder to create object in |
| * @param name name to give to the new object (or <CODE>null</CODE> |
| * if the name should be chosen according to the template) |
| * @return the new data object |
| * @exception IOException if an error occured |
| */ |
| protected abstract DataObject handleCreateFromTemplate ( |
| DataFolder df, String name |
| ) throws IOException; |
| |
| |
| /** Fires operation event to data loader pool. |
| * @param ev the event |
| * @param type OperationEvent.XXXX constant |
| */ |
| private static void fireOperationEvent (OperationEvent ev, int type) { |
| DataLoaderPool.getDefault().fireOperationEvent (ev, type); |
| } |
| |
| /** Provide object used for synchronization. |
| * @return <CODE>this</CODE> in DataObject implementation. Other DataObjects |
| * (MultiDataObject) can rewrite this method and return own synch object. |
| */ |
| Object synchObject() { |
| return synchObject; |
| } |
| |
| /** Invokes atomic action. |
| */ |
| private void invokeAtomicAction (FileObject target, final FileSystem.AtomicAction action, final Object lockTheSession) throws IOException { |
| FileSystem.AtomicAction toRun; |
| |
| if (lockTheSession != null) { |
| class WrapRun implements FileSystem.AtomicAction { |
| public void run() throws IOException { |
| synchronized (lockTheSession) { |
| action.run(); |
| } |
| } |
| } |
| toRun = new WrapRun(); |
| } else { |
| toRun = action; |
| } |
| |
| if (Boolean.getBoolean ("netbeans.dataobject.insecure.operation")) { |
| DataObjectPool.getPOOL ().runAtomicActionSimple (target, toRun); |
| return; |
| } |
| |
| |
| if (this instanceof DataFolder) { |
| // action is slow |
| DataObjectPool.getPOOL ().runAtomicActionSimple (target, toRun); |
| } else { |
| // it is quick, make it block DataObject recognition |
| DataObjectPool.getPOOL ().runAtomicAction (target, toRun); |
| } |
| } |
| |
| |
| // |
| // Property change support |
| // |
| |
| /** Add a property change listener. |
| * @param l the listener to add |
| */ |
| public void addPropertyChangeListener (PropertyChangeListener l) { |
| PropertyChangeSupport sup = changeSupport; |
| if (sup == null) { |
| sup = new PropertyChangeSupport(this); |
| if (!changeSupportUpdater.compareAndSet(this, null, sup)) { |
| sup = changeSupport; |
| } |
| } |
| assert sup != null; |
| sup.addPropertyChangeListener(l); |
| } |
| |
| /** Remove a property change listener. |
| * @param l the listener to remove |
| */ |
| public void removePropertyChangeListener (PropertyChangeListener l) { |
| final PropertyChangeSupport sup = changeSupport; |
| if (sup != null) { |
| sup.removePropertyChangeListener(l); |
| } |
| } |
| |
| /** Fires property change notification to all listeners registered via |
| * {@link #addPropertyChangeListener}. |
| * |
| * @param name of property |
| * @param oldValue old value |
| * @param newValue new value |
| */ |
| protected final void firePropertyChange (String name, Object oldValue, Object newValue) { |
| PropertyChangeSupport ch = changeSupport; |
| if (ch != null) { |
| ch.firePropertyChange(name, oldValue, newValue); |
| } |
| } |
| |
| // |
| // Property change support |
| // |
| |
| /** Add a listener to vetoable changes. |
| * @param l the listener to add |
| * @see #PROP_VALID |
| */ |
| public void addVetoableChangeListener (VetoableChangeListener l) { |
| synchronized (LOCK) { |
| if (vetoableChangeSupport == null) { |
| vetoableChangeSupport = new VetoableChangeSupport(this); |
| } |
| vetoableChangeSupport.addVetoableChangeListener(l); |
| } |
| } |
| |
| /** Add a listener to vetoable changes. |
| * @param l the listener to remove |
| * @see #PROP_VALID |
| */ |
| public void removeVetoableChangeListener (VetoableChangeListener l) { |
| synchronized (LOCK) { |
| if (vetoableChangeSupport != null) { |
| vetoableChangeSupport.removeVetoableChangeListener(l); |
| } |
| } |
| } |
| |
| /** Fires vetoable change notification. |
| * |
| * @param name of property |
| * @param oldValue old value |
| * @param newValue new value |
| * @exception PropertyVetoException if the change has been vetoed |
| */ |
| protected final void fireVetoableChange (String name, Object oldValue, Object newValue) |
| throws PropertyVetoException |
| { |
| VetoableChangeSupport ch; |
| synchronized (LOCK) { |
| ch = vetoableChangeSupport; |
| if (ch == null) { |
| return; |
| } |
| } |
| ch.fireVetoableChange(name, oldValue, newValue); |
| } |
| |
| // |
| // Cookie |
| // |
| |
| /** Obtain a cookie from the data object. |
| * May be overridden by subclasses to extend the behaviour of |
| * data objects. |
| * <P> |
| * The default implementation tests if this object is of the requested class and |
| * if so, returns it. |
| * <p> |
| * <b>Warning:</b> the {@link #getCookie} method and {@link #getLookup} |
| * method are ment to be interchangable - e.g. if you override one of them |
| * be sure to override also the other and try as much as possible to |
| * keep the same content in each of them. The default implementation tries |
| * to do that as much as possible. |
| * |
| * @param c class of requested cookie |
| * @return a cookie or <code>null</code> if such cookies are not supported |
| */ |
| public <T extends Node.Cookie> T getCookie(Class<T> c) { |
| if (c.isInstance (this)) { |
| return c.cast(this); |
| } |
| return null; |
| } |
| |
| /** Represents a context of the data object. This method is a more |
| * general replacement for {@link #getCookie} and should preferably |
| * be used instead of the old method. The default implementation |
| * inside a data object |
| * returns the <code>getNodeDelegate().getLookup()</code> - which is |
| * the most compatible behaviour with previous versions. However |
| * this code has significant potential to deadlocks. That is why the |
| * preferred advice is to override the method to: |
| * <pre> |
| * class MyDataObject extends {@link MultiDataObject} { |
| * public @Override Lookup getLookup() { |
| * return getCookieSet().getLookup(); |
| * } |
| * } |
| * </pre> |
| * <p> |
| * <b>Warning:</b> the {@link #getCookie} method and {@link #getLookup} |
| * method are ment to be interchangable - e.g. if you override one of them |
| * be sure to override also the other and try as much as possible to |
| * keep the same content in each of them. The default implementation tries |
| * to do that as much as possible. |
| * |
| * @return lookup representing this data object and its content |
| * @since 6.0 |
| */ |
| public Lookup getLookup() { |
| Class<?> c = getClass(); |
| if (warnedClasses.add(c)) { |
| LOG.warning("Should override getLookup() in " + c + ", e.g.: [MultiDataObject.this.]getCookieSet().getLookup()"); |
| } |
| if (isValid()) { |
| return getNodeDelegateImpl().getLookup(); |
| } else { |
| // Fallback for invalid DO; at least provide something reasonable. |
| return createNodeDelegate().getLookup(); |
| } |
| } |
| private static final Set<Class<?>> warnedClasses = Collections.synchronizedSet(new WeakSet<Class<?>>()); |
| |
| /** When a request for a cookie is done on a DataShadow of this DataObject |
| * this methods gets called (by default) so the DataObject knows which |
| * DataShadow is asking and extract some information from the shadow itself. |
| * <P> |
| * Subclasses can override this method with better logic, but the default |
| * implementation just delegates to <code>getCookie (Class)</code>. |
| * |
| * @param clazz class to search for |
| * @param shadow the shadow for which is asking |
| * @return the cookie or <code>null</code> |
| * |
| * @since 1.16 |
| */ |
| protected <T extends Node.Cookie> T getCookie(DataShadow shadow, Class<T> clazz) { |
| return getCookie (clazz); |
| } |
| |
| // ======================= |
| // Serialization methods |
| // |
| |
| /** The Serialization replacement for this object stores the primary file instead. |
| * @return a replacement |
| */ |
| public Object writeReplace () { |
| return new Replace (this); |
| } |
| |
| /** The default replace for the data object |
| */ |
| private static final class Replace extends Object implements Serializable { |
| /** the primary file */ |
| private FileObject fo; |
| /** the object to return */ |
| private transient DataObject obj; |
| |
| private static final long serialVersionUID =-627843044348243058L; |
| /** Constructor. |
| * @param obj the object to use |
| */ |
| public Replace (DataObject obj) { |
| this.obj = obj; |
| this.fo = obj.getPrimaryFile (); |
| } |
| |
| public Object readResolve () { |
| return obj; |
| } |
| |
| /** Read method */ |
| private void readObject (ObjectInputStream ois) |
| throws IOException, ClassNotFoundException { |
| ois.defaultReadObject (); |
| if (fo == null) { |
| throw new java.io.FileNotFoundException (); |
| } |
| // DataObjectNotFoundException extends IOException: |
| obj = DataObject.find(fo); |
| } |
| } |
| |
| /** Getter for a text from resource bundle. |
| */ |
| static String getString (String name) { |
| return NbBundle.getMessage (DataObject.class, name); |
| } |
| |
| /** Factory interface for converting file object to data objects. Read |
| * more about the layer based registrations in |
| * <a href="@TOP@/org/openide/loaders/doc-files/api.html#register"/>separate document</a>. |
| * @since 7.0 |
| */ |
| public static interface Factory { |
| /** Find a data object appropriate to the given file object--the meat of this class. |
| * The loader can add all files it has recognized into the <CODE>recognized</CODE> |
| * buffer. Then all these files will be excluded from further processing. |
| * |
| * @param fo file object to recognize |
| * @param recognized recognized file buffer |
| * @exception DataObjectExistsException if the data object for the |
| * primary file already exists |
| * @exception IOException if the object is recognized but cannot be created |
| * @exception InvalidClassException if the class is not instance of |
| * {@link #getRepresentationClass} |
| * |
| * @return suitable data object or <CODE>null</CODE> if the handler cannot |
| * recognize this object (or its group) |
| * @see DataLoader |
| */ |
| public DataObject findDataObject(FileObject fo, Set<? super FileObject> recognized) |
| throws IOException; |
| } |
| |
| /** Interface for objects that can contain other data objects. |
| * For example DataFolder and DataShadow implement this interface |
| * to allow others to access the contained objects in uniform maner |
| */ |
| public static interface Container extends Node.Cookie { |
| /** Name of property that holds children of this container. */ |
| public static final String PROP_CHILDREN = "children"; // NOI18N |
| |
| /** @return the array of contained objects |
| */ |
| public DataObject[] getChildren (); |
| |
| /** Adds a listener. |
| * @param l the listener |
| */ |
| public void addPropertyChangeListener (PropertyChangeListener l); |
| |
| /** Removes property change listener. |
| * @param l the listener |
| */ |
| public void removePropertyChangeListener (PropertyChangeListener l); |
| } |
| |
| /** Registers new file type into the system. |
| * Apply this annotation to a class that extends either |
| * (@link org.openide.loaders.DataObject.Factory) or |
| *(@link org.openide.loaders.DataObject). This methods generates |
| * a layer registration as described by {@link DataLoaderPool#factory(java.lang.Class, java.lang.String, java.awt.Image)}. |
| * |
| * @see Registrations |
| * @since 7.36 |
| */ |
| @Retention(RetentionPolicy.SOURCE) |
| @Target({ElementType.TYPE}) |
| public static @interface Registration { |
| /** |
| * Mime type to recognize. Use |
| * {@link MIMEResolver.ExtensionRegistration} and co. to assign |
| * a mime types to {@link FileObject files} in the system. |
| */ |
| String mimeType(); |
| |
| /** |
| * Display name for the file type created by this registration. |
| */ |
| String displayName() default ""; |
| |
| /** |
| * Path to icon to be used by default for nodes created by |
| * this registration. |
| */ |
| String iconBase() default ""; |
| |
| /** |
| * Position of the registration among other {@link DataObject.Factory |
| * factories} registered for the given |
| * {@link #mimeType() mime type}. |
| */ |
| int position() default Integer.MAX_VALUE; |
| } |
| |
| /** |
| * May be used to allow multiple {@link DataObject.Registration DataObject.Registration} at one place. |
| * @since 7.36 |
| */ |
| @Retention(RetentionPolicy.SOURCE) |
| @Target({ElementType.TYPE}) |
| public static @interface Registrations { |
| |
| Registration[] value(); |
| } |
| |
| |
| /** Registry of modified data objects. |
| * The registry permits attaching of a change listener |
| * to be informed when the count of modified objects changes. |
| */ |
| public static final class Registry extends Object { |
| |
| /** Private constructor */ |
| private Registry () { |
| } |
| |
| /** Add new listener to changes in the set of modified objects. |
| * @param chl listener to add |
| */ |
| public void addChangeListener (final ChangeListener chl) { |
| modified.addChangeListener(chl); |
| } |
| |
| /** Remove a listener to changes in the set of modified objects. |
| * @param chl listener to remove |
| */ |
| public void removeChangeListener (final ChangeListener chl) { |
| modified.removeChangeListener(chl); |
| } |
| |
| /** Get a set of modified data objects. |
| * @return an unmodifiable set of data objects |
| */ |
| public Set<DataObject> getModifiedSet() { |
| synchronized (syncModified) { |
| HashSet<DataObject> set = new HashSet<DataObject>(syncModified); |
| return set; |
| } |
| } |
| |
| /** Get modified objects. |
| * @return array of objects |
| */ |
| public DataObject[] getModified () { |
| return getModifiedSet().toArray(new DataObject[0]); |
| } |
| } |
| |
| private static final class ModifiedRegistry extends HashSet<DataObject> { |
| static final long serialVersionUID =-2861723614638919680L; |
| private static final Logger REGLOG = Logger.getLogger("org.openide.loaders.DataObject.Registry"); // NOI18N |
| |
| private final ChangeSupport cs = new ChangeSupport(this); |
| |
| ModifiedRegistry() {} |
| |
| /** Adds new listener. |
| * @param chl new listener |
| */ |
| public final void addChangeListener(final ChangeListener chl) { |
| cs.addChangeListener(chl); |
| } |
| |
| /** Removes listener from the listener list. |
| * @param chl listener to remove |
| */ |
| public final void removeChangeListener(final ChangeListener chl) { |
| cs.removeChangeListener(chl); |
| } |
| |
| /***** overriding of methods which change content in order to notify |
| * listeners about the content change */ |
| @Override |
| public boolean add (DataObject o) { |
| boolean result = super.add(o); |
| REGLOG.log(Level.FINER, "Data Object {0} modified, change {1}", new Object[] { o, result }); // NOI18N |
| if (result) { |
| cs.fireChange(); |
| } |
| return result; |
| } |
| |
| @Override |
| public boolean remove (Object o) { |
| boolean result = super.remove(o); |
| REGLOG.log(Level.FINER, "Data Object {0} unmodified, change {1}", new Object[] { o, result }); // NOI18N |
| if (result) { |
| cs.fireChange(); |
| } |
| return result; |
| } |
| |
| } // end of ModifiedRegistry inner class |
| |
| private static final class DOSavable extends AbstractSavable |
| implements Icon { |
| final DataObject obj; |
| |
| public DOSavable(DataObject obj) { |
| this.obj = obj; |
| } |
| |
| @Override |
| public String findDisplayName() { |
| return obj.getNodeDelegate().getDisplayName(); |
| } |
| |
| @Override |
| protected void handleSave() throws IOException { |
| SaveCookie sc = obj.getCookie(SaveCookie.class); |
| if (sc != null) { |
| sc.save(); |
| } |
| } |
| |
| @Override |
| public boolean equals(Object other) { |
| if (other instanceof DOSavable) { |
| DOSavable dos = (DOSavable)other; |
| return obj.equals(dos.obj); |
| } |
| return false; |
| } |
| |
| @Override |
| public int hashCode() { |
| return obj.hashCode(); |
| } |
| |
| final void remove() { |
| unregister(); |
| } |
| |
| final void add() { |
| register(); |
| } |
| |
| @Override |
| public void paintIcon(Component c, Graphics g, int x, int y) { |
| icon().paintIcon(c, g, x, y); |
| } |
| |
| @Override |
| public int getIconWidth() { |
| return icon().getIconWidth(); |
| } |
| |
| @Override |
| public int getIconHeight() { |
| return icon().getIconHeight(); |
| } |
| |
| private Icon icon() { |
| return ImageUtilities.image2Icon(obj.getNodeDelegate().getIcon(BeanInfo.ICON_COLOR_16x16)); |
| } |
| } |
| |
| /** A.N. - profiling shows that MultiLoader.checkFiles() is called too often |
| * This method is part of the fix - empty for DataObject. |
| */ |
| void recognizedByFolder() { |
| } |
| |
| // This methods are called by DataObjectPool whenever the primary file |
| // gets changed. The Pool listens on the whole FS thus reducing |
| // the number of individual listeners created/registered. |
| void notifyFileRenamed(FileRenameEvent fe) { |
| if (fe.getFile ().equals (getPrimaryFile ())) { |
| firePropertyChange(PROP_NAME, fe.getName(), getName()); |
| } |
| } |
| |
| void notifyFileDeleted(FileEvent fe) { |
| } |
| |
| void notifyFileChanged(FileEvent fe) { |
| } |
| |
| void notifyFileDataCreated(FileEvent fe) { |
| } |
| |
| void notifyAttributeChanged(FileAttributeEvent fae) { |
| if (! EA_ASSIGNED_LOADER.equals(fae.getName())) { |
| // We are interested only in assigned loader |
| return; |
| } |
| FileObject f = fae.getFile(); |
| if (f != null) { |
| String attrFromFO = (String)f.getAttribute(EA_ASSIGNED_LOADER); |
| if (attrFromFO == null || (! attrFromFO.equals(getLoader().getClass().getName()))) { |
| Set<FileObject> single = new HashSet<FileObject>(); // Collections.singleton is r/o, this must be writable |
| single.add(f); |
| if (!DataObjectPool.getPOOL().revalidate(single).isEmpty()) { |
| LOG.info("It was not possible to invalidate data object: " + this); // NOI18N |
| } else { |
| // we need to refresh parent folder if it is there |
| // this should be covered by DataLoaderPoolTest.testChangeIsAlsoReflectedInNodes |
| FolderList.changedDataSystem (f.getParent()); |
| } |
| } |
| } |
| } |
| static final class CreateAction implements FileSystem.AtomicAction { |
| public DataObject result; |
| private String name; |
| private DataFolder f; |
| private DataObject orig; |
| private Map<String, ? extends Object> param; |
| |
| private static ThreadLocal<CreateAction> CURRENT = new ThreadLocal<CreateAction>(); |
| |
| public CreateAction(DataObject orig, DataFolder f, String name, Map<String, ? extends Object> param) { |
| this.orig = orig; |
| this.f = f; |
| this.name = name; |
| this.param = param; |
| } |
| |
| public void run () throws IOException { |
| DataFolder prevFold = DataObjectEncodingQueryImplementation.enterIgnoreTargetFolder(f); |
| CreateAction prev = CURRENT.get(); |
| try { |
| CURRENT.set(this); |
| result = orig.handleCreateFromTemplate(f, name); |
| } finally { |
| DataObjectEncodingQueryImplementation.exitIgnoreTargetFolder(prevFold); |
| CURRENT.set(prev); |
| } |
| } |
| |
| public static Map<String, Object> getCallParameters(String name) { |
| CreateAction c = CURRENT.get(); |
| if (c == null || c.param == null) { |
| return Collections.emptyMap(); |
| } |
| return Collections.unmodifiableMap(c.param); |
| } |
| |
| static String getOrigName() { |
| CreateAction c = CURRENT.get(); |
| return c == null ? null : c.name; |
| } |
| |
| public static Map<String,Object> findParameters(String name) { |
| CreateAction c = CURRENT.get(); |
| if (c == null) { |
| return Collections.emptyMap(); |
| } |
| HashMap<String,Object> all = new HashMap<String,Object>(); |
| for (CreateFromTemplateAttributesProvider provider : Lookup.getDefault().lookupAll(CreateFromTemplateAttributesProvider.class)) { |
| Map<String,? extends Object> map = provider.attributesFor(c.orig, c.f, c.name); |
| if (map != null) { |
| for (Map.Entry<String,? extends Object> e : map.entrySet()) { |
| all.put(e.getKey(), e.getValue()); |
| } |
| } |
| } |
| if (c.param != null) { |
| for (Map.Entry<String,? extends Object> e : c.param.entrySet()) { |
| all.put(e.getKey(), e.getValue()); |
| } |
| } |
| |
| if (!all.containsKey("name") && name != null) { // NOI18N |
| if (Boolean.TRUE.equals(all.get(CreateFromTemplateHandler.FREE_FILE_EXTENSION))) { |
| name = name.replaceFirst("[.].*", ""); |
| } |
| all.put("name", name); // NOI18N |
| } |
| if (!all.containsKey("user")) { // NOI18N |
| all.put("user", System.getProperty("user.name")); // NOI18N |
| } |
| Date d = new Date(); |
| if (!all.containsKey("date")) { // NOI18N |
| all.put("date", DateFormat.getDateInstance().format(d)); // NOI18N |
| } |
| if (!all.containsKey("time")) { // NOI18N |
| all.put("time", DateFormat.getTimeInstance().format(d)); // NOI18N |
| } |
| if (!all.containsKey("dateTime")) { // NOI18N |
| all.put("dateTime", d); // NOI18N |
| } |
| |
| return Collections.unmodifiableMap(all); |
| } |
| |
| public static Map<String,Object> enhanceParameters(Map<String,Object> old, String name, String ext) { |
| HashMap<String,Object> all = new HashMap<String,Object>(old); |
| if (!all.containsKey("nameAndExt") && name != null) { // NOI18N |
| if (ext != null && ext.length() > 0 && |
| (!Boolean.TRUE.equals(old.get(CreateFromTemplateHandler.FREE_FILE_EXTENSION)) || name.indexOf('.') == -1)) { |
| all.put("nameAndExt", name + '.' + ext); // NOI18N |
| } else { |
| all.put("nameAndExt", name); // NOI18N |
| } |
| } |
| return Collections.unmodifiableMap(all); |
| } |
| |
| } // end of CreateAction |
| |
| /** |
| * Get existing thread-local ProgressInfo instance. |
| * |
| * @return The thread-local instance, or null if not yet initialized. |
| */ |
| static ProgressInfo getProgressInfo() { |
| return PROGRESS_INFO_TL.get(); |
| } |
| |
| /** |
| * Initialize a thread-local ProgressInfo instance. The instance mustn't be |
| * already initialized. |
| * |
| * @return The new ProgressInfo instance. |
| */ |
| static ProgressInfo initProgressInfo(String name, DataObject root) { |
| assert PROGRESS_INFO_TL.get() == null; |
| ProgressInfo pi = new ProgressInfo(name, root); |
| PROGRESS_INFO_TL.set(pi); |
| OBJ_LOG.log(Level.FINEST, "ProgressInfo init: {0}", name); //NOI18N |
| return pi; |
| } |
| |
| /** |
| * Finish the progress bar and remove the thread-local ProgressInfo |
| * instance, but only if the root object of the operation has just been |
| * processed. |
| */ |
| static void finishProgressInfoIfDone(ProgressInfo pi, |
| DataObject dob) { |
| assert PROGRESS_INFO_TL.get() == null || PROGRESS_INFO_TL.get() == pi; |
| if (pi.finishIfDone(dob)) { |
| PROGRESS_INFO_TL.remove(); |
| } |
| } |
| |
| /** |
| * Check whether the current delete, move or copy action has been terminated |
| * by the user. |
| * |
| * @return True if the action has been terminatad, false otherwise. |
| */ |
| static boolean isCurrentActionTerminated() { |
| ProgressInfo pi = getProgressInfo(); |
| return pi != null && pi.isTerminated(); |
| } |
| |
| /** |
| * Object holding information about a move, delete or copy operation. It |
| * should be stored in a thread-local variable, see methods |
| * {@link #getProgressInfo()}, {@link #initProgressInfo(DataObject) |
| * and {@link #finishProgressInfoIfDone(ProgressInfo, DataObject)}}. |
| */ |
| static class ProgressInfo { |
| |
| private final int NAME_LEN_LIMIT = 128; |
| |
| private final ProgressHandle progressHandle; |
| private final AtomicBoolean terminated = new AtomicBoolean(); |
| private final DataObject root; |
| |
| public ProgressInfo(String name, DataObject root) { |
| final Cancellable can; |
| if (root instanceof DataFolder) { |
| can = new Cancellable() { |
| |
| @Override |
| public boolean cancel() { |
| terminated.set(true); |
| return true; |
| } |
| }; |
| } else { |
| can = null; |
| } |
| ProgressHandle ph = ProgressHandleFactory.createHandle(name, can); |
| ph.setInitialDelay(500); |
| ph.start(); |
| this.progressHandle = ph; |
| this.root = root; |
| } |
| |
| public void updateProgress(DataObject dob) { |
| OBJ_LOG.log(Level.FINEST, "Update ProgressInfo: {0}", dob); //NOI18N |
| String displayName; |
| if (dob.getPrimaryFile() == null) { |
| displayName = dob.getName(); |
| } else { |
| displayName = dob.getPrimaryFile().getPath(); |
| } |
| if (displayName != null && displayName.length() > NAME_LEN_LIMIT) { |
| displayName = "..." + displayName.substring( //NOI18N |
| displayName.length() - NAME_LEN_LIMIT + 3, |
| displayName.length()); |
| } |
| progressHandle.progress(displayName); |
| } |
| |
| /** |
| * Terminate the current action. |
| */ |
| public void terminate() { |
| terminated.set(true); |
| } |
| |
| public boolean isTerminated() { |
| return terminated.get(); |
| } |
| |
| /** |
| * If the passed data object is the root object for the operation, |
| * finish the progress bar and return true, otherwise do nothing and |
| * return false. |
| */ |
| public boolean finishIfDone(DataObject currentFile) { |
| if (currentFile == root) { |
| progressHandle.finish(); |
| return true; |
| } else { |
| return false; |
| } |
| } |
| } |
| } |