/* * Sun Public License Notice * * The contents of this file are subject to the Sun Public License * Version 1.0 (the "License"). You may not use this file except in * compliance with the License. A copy of the License is available at * http://www.sun.com/ * * The Original Code is NetBeans. The Initial Developer of the Original * Code is Sun Microsystems, Inc. Portions Copyright 1997-2001 Sun * Microsystems, Inc. All Rights Reserved. */ package org.openide.loaders; import java.awt.datatransfer.Transferable; import java.awt.Image; import java.beans.*; import java.io.*; import java.text.MessageFormat; import java.lang.reflect.*; import java.lang.ref.*; import java.util.*; import org.openide.filesystems.*; import org.openide.filesystems.FileSystem; import org.openide.util.NbBundle; import org.openide.nodes.*; import org.openide.util.HelpCtx; import org.openide.ErrorManager; import org.openide.util.datatransfer.ExTransferable; import org.openide.util.Mutex; import org.openide.util.MutexException; /** Default implementation of a shortcut to another data object. * Since 1.13 it extends MultiDataObject. * @author Jan Jancura, Jaroslav Tulach */ public class DataShadow extends MultiDataObject implements DataObject.Container { /** generated Serialized Version UID */ static final long serialVersionUID = 6305590675982925167L; /** original data object */ private DataObject original; /** Listener attached to original DataObject. */ private OrigL origL = null; /** List of nodes created for the DataShadow. */ private LinkedList nodes = new LinkedList (); /** Extension name. */ static final String SHADOW_EXTENSION = "shadow"; // NOI18N /** Set of all DataShadows */ private static Set allDataShadows; /** ReferenceQueue for collected DataShadows */ private static ReferenceQueue rqueue; private static Mutex MUTEX = new Mutex (); private static final int IDX_FS = 0; private static final int IDX_PATH = 1; /** Getter for the Set that contains all DataShadows. */ private static Set getDataShadowsSet() { if (allDataShadows == null) { synchronized (DataShadow.class) { if (allDataShadows == null) { allDataShadows = new HashSet(); } } } return allDataShadows; } /** Getter for the ReferenceQueue that contains WeakReferences * for discarded DataShadows */ private static ReferenceQueue getRqueue() { if (rqueue == null) { synchronized (DataShadow.class) { if (rqueue == null) { rqueue = new ReferenceQueue(); } } } return rqueue; } /** Removes WeakReference of collected DataShadows. */ private static void checkQueue() { if (rqueue == null) { return; } Reference ref = rqueue.poll(); while (ref != null) { getDataShadowsSet().remove(ref); ref = rqueue.poll(); } } /** Creates WeakReference for given DataShadow */ static Reference createReference(Object ds, ReferenceQueue q) { return new DSWeakReference(ds, q); } private static synchronized void enqueueDataShadow(DataShadow ds) { checkQueue(); getDataShadowsSet().add(createReference(ds, getRqueue())); } /** @return all active DataShadows or null */ private static synchronized List getAllDataShadows() { Set allShadows = allDataShadows; if ((allShadows == null) || allShadows.isEmpty()) { return null; } List ret = new ArrayList(allShadows.size()); Iterator it = allShadows.iterator(); while (it.hasNext()) { Reference ref = (Reference) it.next(); Object shadow = ref.get(); if (shadow != null) { ret.add(shadow); } } return ret; } /** Checks whether a change of the given dataObject * does not hurt validity of a DataShadow */ static void checkValidity(EventObject ev) { List all = getAllDataShadows(); if (all == null) { return; } boolean moved = false; if ((ev instanceof OperationEvent.Rename) || (ev instanceof OperationEvent.Move)) moved = true; int size = all.size(); for (int i = 0; i < size; i++) { Object obj = all.get(i); ((DataShadow) obj).refresh(moved); } } /** Constructs new data shadow for given primary file and referenced original. * Method to allow subclasses of data shadow. * * @param fo the primary file * @param original original data object * @param loader the loader that created the object */ protected DataShadow ( FileObject fo, DataObject original, MultiFileLoader loader ) throws DataObjectExistsException { super (fo, loader); init(original); } /** Constructs new data shadow for given primary file and referenced original. * Method to allow subclasses of data shadow. * * @param fo the primary file * @param original original data object * @param loader the loader that created the object * @deprecated Since 1.13 do not use this constructor, it is for backward compatibility only */ protected DataShadow ( FileObject fo, DataObject original, DataLoader loader ) throws DataObjectExistsException { super (fo, loader); init(original); } /** Perform initialization after construction. * @param original original data object */ private void init(DataObject original) { if (original == null) throw new IllegalArgumentException(); setOriginal (original); enqueueDataShadow(this); } /** Constructs new data shadow for given primary file and referenced original. * @param fo the primary file * @param original original data object */ private DataShadow (FileObject fo, DataObject original) throws DataObjectExistsException { this (fo, original, (MultiFileLoader)DataLoaderPool.getShadowLoader ()); } /** Method that creates new data shadow in a folder. The name chosen is based * on the name of the original object. * * @param folder target folder to create data in * @param original orignal object that should be represented by the shadow */ public static DataShadow create (DataFolder folder, DataObject original) throws IOException { return create (folder, null, original, SHADOW_EXTENSION); } /** Method that creates new data shadow in a folder. The default extension * is used. * * @param folder target folder to create data in * @param name name to give to the shadow * @param original object that should be represented by the shadow */ public static DataShadow create ( DataFolder folder, final String name, final DataObject original ) throws IOException { return create (folder, name, original, SHADOW_EXTENSION); } /** Method that creates new data shadow in a folder. All modifications are * done atomicly using {@link FileSystem#runAtomicAction}. * * @param folder target folder to create data in * @param name name to give to the shadow * @param original orignal object that should be represented by the shadow */ public static DataShadow create ( DataFolder folder, final String name, final DataObject original, final String ext ) throws IOException { final FileObject fo = folder.getPrimaryFile (); final DataShadow[] arr = new DataShadow[1]; fo.getFileSystem ().runAtomicAction (new FileSystem.AtomicAction () { public void run () throws IOException { FileObject file = writeOriginal (name, ext, fo, original); DataObject obj = DataObject.find (file); if (obj instanceof DataShadow) { arr[0] = (DataShadow)obj; } else { // wrong instance => shadow was not found DataObjectNotFoundException dnfe = new DataObjectNotFoundException (obj.getPrimaryFile ()); ErrorManager errMan = ErrorManager.getDefault (); errMan.annotate( dnfe, obj == null ? null : obj.getClass().toString()); errMan.annotate( dnfe, file == null ? null : file.getPath()); throw dnfe; } } }); return arr[0]; } /** Writes the original DataObject into file of given name and extension. * Both parameters {@link name} and {@link ext} are ignored when the data file * is passed in as a {@link trg} parameter, in that case name and link can be <code>null</code>. * @param name name of the file to write original DataObject in * @param ext extension of the file to write original DataObject in * @param trg folder where FileObject of given name and ext will be created or * file which content is replaced * @param obj DataObject which link is stored into * @return the file with link * @exception IOException on I/O error */ private static FileObject writeOriginal ( final String name, final String ext, final FileObject trg, final DataObject obj ) throws IOException { try { return (FileObject) MUTEX.writeAccess (new Mutex.ExceptionAction () { public Object run () throws IOException { FileObject fo; if (trg.isData ()) { fo = trg; } else { String n; if (name == null) { n = FileUtil.findFreeFileName (trg, obj.getName (), ext); } else { n = name; } fo = trg.createData (n, ext); } FileLock lock = fo.lock (); Writer os = new OutputStreamWriter (fo.getOutputStream (lock), "UTF-8"); try { FileObject pf = obj.getPrimaryFile (); os.write (pf.getPath()); os.write ('\n'); os.write (pf.getFileSystem ().getSystemName ()); os.write ('\n'); } finally { os.close (); lock.releaseLock (); } return fo; } }); } catch (MutexException e) { throw (IOException) e.getException (); } } /** Loads proper dataShadow from the file fileObject. * * @param fileObject The file to deserialize shadow from. * @return the original <code>DataObject</code> referenced by the shadow * @exception IOException error during load */ protected static DataObject deserialize (FileObject fileObject) throws java.io.IOException { String result [] = read (fileObject); FileObject fo = checkOriginal (result [IDX_PATH], result [IDX_FS], fileObject.getFileSystem()); return DataObject.find (fo); } private static String [] read (final FileObject f) throws IOException { if ( f.getSize() == 0 ) { Object fileName = f.getAttribute ("originalFile"); // NOI18N if ( fileName instanceof String ) { Object fileSystemName = f.getAttribute( "originalFileSystem" ); // NOI18N if (!(fileSystemName instanceof String )) { /* fileSystemName = f.getFileSystem().getSystemName(); */ fileSystemName = null; } return new String [] { (String)fileSystemName, (String)fileName }; } else { throw new java.io.FileNotFoundException (f.getPath()); } } try { return (String []) MUTEX.readAccess (new Mutex.ExceptionAction () { public Object run () throws IOException { BufferedReader ois = new BufferedReader (new InputStreamReader (f.getInputStream (), "UTF-8")); try { String s = ois.readLine (); String fs = ois.readLine (); if (s == null) { // not found throw new java.io.FileNotFoundException (f.getPath()); } return new String [] { fs, s }; } finally { ois.close (); } } }); } catch (MutexException e) { throw (IOException) e.getException (); } } private FileObject checkOriginal (DataObject orig) throws java.io.IOException { if (orig == null) return null; return deserialize(getPrimaryFile()).getPrimaryFile(); } /* static FileObject checkOriginal (String strFile, String strFS) throws java.io.IOException { return checkOriginal(strFile, strFS, null); } */ static FileObject checkOriginal (String strFile, String strFS, FileSystem origSystem) throws java.io.IOException { Repository rep = Repository.getDefault(); FileSystem fileSystem; if (strFS != null) { // try to locate the fs fileSystem = rep.findFileSystem (strFS); } else { fileSystem = origSystem; } FileObject fo; if (fileSystem != null) { // first of all try to locate the shadow by filesystem fo = fileSystem.findResource (strFile); } else { fo = null; } /* if (fo == null) { fo = rep.findResource (s); } */ if (fo == null) { throw new java.io.FileNotFoundException (strFile); } return fo; } /** Return the original shadowed object. * @return the data object */ public DataObject getOriginal () { return original; } /** Implementation of Container interface. * @return array of one element, the original */ public DataObject[] getChildren () { return new DataObject[] { getOriginal () }; } /* Creates node delegate. */ protected Node createNodeDelegate () { return new ShadowNode (this); } /* Getter for delete action. * @return true if the object can be deleted */ public boolean isDeleteAllowed () { return !getPrimaryFile ().isReadOnly (); } /* Getter for copy action. * @return true if the object can be copied */ public boolean isCopyAllowed () { return true; } /* Getter for move action. * @return true if the object can be moved */ public boolean isMoveAllowed () { return !getPrimaryFile ().isReadOnly (); } /* Getter for rename action. * @return true if the object can be renamed */ public boolean isRenameAllowed () { return !getPrimaryFile ().isReadOnly (); } /* Help context for this object. * @return help context */ public HelpCtx getHelpCtx () { return getOriginal ().getHelpCtx (); } /* Creates shadow for this object in specified folder. The current * implementation creates reference data shadow and pastes it into * specified folder. * * @param f the folder to create shortcut in * @return the shadow */ protected DataShadow handleCreateShadow (DataFolder f) throws IOException { return original.handleCreateShadow (f); } /* Scans the orginal bundle */ public Node.Cookie getCookie (Class c) { if (c.isInstance (this)) { return this; } return original.getCookie (this, c); } /* Try to refresh link to original file */ public void refresh() { refresh(false); } private void refresh(boolean moved) { try { /* Link isn't broken */ if (moved) tryUpdate(); if (checkOriginal(original) != null) return; } catch (IOException e) { } try { /* Link is broken */ this.setValid(false); } catch (java.beans.PropertyVetoException e) { } } private void tryUpdate() throws IOException { String result [] = read (getPrimaryFile ()); FileObject pf = original.getPrimaryFile (); if (result [IDX_PATH].equals (pf.getPath())) { if (result[IDX_FS] == null) { if (getPrimaryFile().getFileSystem() == pf.getFileSystem ()) return; } else { if (result [IDX_FS].equals (pf.getFileSystem ().getSystemName ())) return; } } writeOriginal (null, null, getPrimaryFile (), original); } private void setOriginal (DataObject o) { if (origL == null) { origL = new OrigL (this); } // set new original if (original != null) { original.removePropertyChangeListener (origL); } DataObject oldOriginal = original; o.addPropertyChangeListener (origL); original = o; // update nodes ShadowNode n [] = null; synchronized (nodes) { n = (ShadowNode [])nodes.toArray (new ShadowNode [nodes.size ()]); } try { for (int i = 0; i < n.length; i++) { n[i].originalChanged (); } } catch (IllegalStateException e) { System.out.println("Please reopen the bug #18998 if you see this message."); // NOI18N System.out.println("Old:"+oldOriginal + // NOI18N ((oldOriginal == null) ? "" : (" / " + oldOriginal.isValid() + " / " + System.identityHashCode(oldOriginal)))); // NOI18N System.out.println("New:"+original + // NOI18N ((original == null) ? "" : (" / " + original.isValid() + " / " + System.identityHashCode(original)))); // NOI18N throw e; } } private static void updateShadowOriginal(final DataShadow shadow) { final FileObject primary = shadow.original.getPrimaryFile (); org.openide.util.RequestProcessor.postRequest (new Runnable () { public void run () { DataObject newOrig; try { newOrig = DataObject.find (primary); } catch (DataObjectNotFoundException e) { newOrig = null; } if (newOrig != null) { shadow.setOriginal (newOrig); } } }, 100); } private static class OrigL implements PropertyChangeListener { WeakReference shadow = null; public OrigL (DataShadow shadow) { this.shadow = new WeakReference (shadow); } public void propertyChange (PropertyChangeEvent evt) { final DataShadow shadow = (DataShadow) this.shadow.get (); if (shadow != null && DataObject.PROP_VALID.equals (evt.getPropertyName ())) { updateShadowOriginal(shadow); } } } /** Node for a shadow object. */ protected static class ShadowNode extends FilterNode { /** message to create name of node */ private static MessageFormat format; /** message to create short description of node */ private static MessageFormat descriptionFormat; /** if true, the DataShadow name is used instead of original's name, * affects DataShadows of filesystem roots only */ private static final String ATTR_USEOWNNAME = "UseOwnName"; //NOI18N /** shadow */ private DataShadow obj; /** the sheet computed for this node or null */ private Sheet sheet; /** filesystem name property of original */ private String originalFS; /** Create a shadowing node. * @param shadow the shadow */ public ShadowNode (DataShadow shadow) { this (shadow, shadow.getOriginal ().getNodeDelegate ()); } /** Initializes it */ private ShadowNode (DataShadow shadow, Node node) { super (node); this.obj = shadow; synchronized (this.obj.nodes) { this.obj.nodes.add (this); } } /* Clones the node */ public Node cloneNode () { ShadowNode sn = new ShadowNode (obj); return sn; } /* Renames the shadow data object. * @param name new name for the object * @exception IllegalArgumentException if the rename failed */ public void setName (String name) { try { if (!name.equals (obj.getName ())) { obj.rename (name); if (obj.original.getPrimaryFile ().isRoot ()) { obj.getPrimaryFile ().setAttribute (ATTR_USEOWNNAME, Boolean.TRUE); } fireDisplayNameChange (null, null); fireNameChange (null, null); } } catch (IOException ex) { throw new IllegalArgumentException (ex.getMessage ()); } } /** The name of the shadow. * @return the name */ public String getName () { return obj.getName (); } /** Lazy getter for filesystem name property of original data object * @return the filesystem display name of original */ private String getOriginalFileSystemName () { if ( originalFS != null ) return originalFS; else { try { originalFS = obj.getOriginal().getPrimaryFile().getFileSystem().getDisplayName(); } catch (FileStateInvalidException ex) { originalFS = ""; // NOI18N } } return originalFS; } /* Creates name based on the original one. */ public String getDisplayName () { if (format == null) { format = new MessageFormat (NbBundle.getBundle (DataShadow.class).getString ("FMT_shadowName")); } String n = format.format (createArguments ()); try { obj.getPrimaryFile().getFileSystem().getStatus().annotateName(n, obj.files()); } catch (FileStateInvalidException fsie) { // ignore } return n; } /** Creates arguments for given shadow node */ private Object[] createArguments () { String origDisp; String shadowName = obj.getName (); if (obj.original.isValid()) { origDisp = obj.original.getNodeDelegate().getDisplayName(); } else { // We will soon be a broken data shadow, in the meantime... origDisp = ""; // NOI18N } Boolean useOwnName = (Boolean)obj.getPrimaryFile ().getAttribute (ATTR_USEOWNNAME); if (obj.original.getPrimaryFile ().isRoot () && (useOwnName == null || !useOwnName.booleanValue ())) { try { shadowName = obj.original.getPrimaryFile ().getFileSystem ().getDisplayName (); } catch (FileStateInvalidException e) { // ignore } } return new Object[] { shadowName, // name of the shadow super.getDisplayName (), // name of original systemNameOrFileName (obj.getPrimaryFile ()), // full name of file for shadow systemNameOrFileName (obj.getOriginal ().getPrimaryFile ()), // full name of original file origDisp, // display name of original }; } /** System name of file name */ private static String systemNameOrFileName (FileObject fo) { if (fo.isRoot ()) { try { return fo.getFileSystem ().getDisplayName (); } catch (FileStateInvalidException ex) { } } return fo.getPath(); } /* Creates description based on the original one. */ public String getShortDescription () { if (descriptionFormat == null) { descriptionFormat = new MessageFormat ( NbBundle.getBundle (DataShadow.class).getString ("FMT_shadowHint") ); } return descriptionFormat.format (createArguments ()); } /* Show filesystem icon if it is a root. */ public Image getIcon(int type) { Image i = rootIcon(type); if (i != null) { return i; } else { return super.getIcon(type); } } public Image getOpenedIcon(int type) { Image i = rootIcon(type); if (i != null) { return i; } else { return super.getOpenedIcon(type); } } private Image rootIcon(int type) { FileObject orig = obj.getOriginal().getPrimaryFile(); if (orig.isRoot()) { try { FileSystem fs = orig.getFileSystem(); try { Image i = Introspector.getBeanInfo(fs.getClass()).getIcon(type); return fs.getStatus().annotateIcon(i, type, obj.files()); } catch (IntrospectionException ie) { ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, ie); // ignore } } catch (FileStateInvalidException fsie) { // ignore } } return null; } /* @return obj.isDeleteAllowed () */ public boolean canDestroy () { return obj.isDeleteAllowed (); } /* Destroyes the node */ public void destroy () throws IOException { synchronized (obj.nodes) { obj.nodes.remove (this); } obj.delete (); // super.destroy (); } /** @return true if shadow can be renamed */ public final boolean canRename () { return obj.isRenameAllowed (); } /* Returns true if this object allows copying. * @returns true if so */ public final boolean canCopy () { return obj.isCopyAllowed (); } /* Returns true if this object allows cutting. * @returns true if so */ public final boolean canCut () { return obj.isMoveAllowed (); } /* First of all the DataObject.getCookie method is * called. If it produces non-null result, it is returned. * Otherwise the value returned from super.getCookie * method is returned. * * @return the cookie or null */ public Node.Cookie getCookie (Class cl) { Node.Cookie c = obj.getCookie (cl); if (c != null) { return c; } else { return super.getCookie (cl); } } /** Returns modified properties of the original node. * @return property sets */ public PropertySet[] getPropertySets () { Sheet s = sheet; if (s == null) { s = sheet = cloneSheet (); } return s.toArray (); } /** Copy this node to the clipboard. * * @return {@link org.openide.util.datatransfer.ExTransferable.Single} with one copy flavor * @throws IOException if it could not copy * @see NodeTransfer */ public Transferable clipboardCopy () throws IOException { ExTransferable t = ExTransferable.create (super.clipboardCopy ()); t.put (LoaderTransfer.transferable ( obj, LoaderTransfer.CLIPBOARD_COPY) ); return t; } /** Cut this node to the clipboard. * * @return {@link org.openide.util.datatransfer.ExTransferable.Single} with one cut flavor * @throws IOException if it could not cut * @see NodeTransfer */ public Transferable clipboardCut () throws IOException { ExTransferable t = ExTransferable.create (super.clipboardCut ()); t.put (LoaderTransfer.transferable ( obj, LoaderTransfer.CLIPBOARD_CUT) ); return t; } /** * This implementation only calls clipboardCopy supposing that * copy to clipboard and copy by d'n'd are similar. * * @return transferable to represent this node during a drag * @exception IOException when the * cut cannot be performed */ public Transferable drag () throws IOException { return clipboardCopy (); } /** Creates a node listener that allows listening on the * original node and propagating events to the proxy. * <p>Intended for overriding by subclasses, as with {@link #createPropertyChangeListener}. * * @return a {@link org.openide.nodes.FilterNode.NodeAdapter} in the default implementation */ protected org.openide.nodes.NodeListener createNodeListener () { return new PropL (this); } /** Equal if the o is ShadowNode to the same shadow object. */ public boolean equals (Object o) { if (o instanceof ShadowNode) { ShadowNode sn = (ShadowNode)o; return sn.obj == obj; } return false; } /** Hashcode is computed by the represented shadow. */ public int hashCode () { return obj.hashCode (); } /** Clones the property sheet of original node. */ private Sheet cloneSheet () { PropertySet[] sets = this.getOriginal ().getPropertySets (); Sheet s = new Sheet (); for (int i = 0; i < sets.length; i++) { Sheet.Set ss = new Sheet.Set (); ss.put (sets[i].getProperties ()); ss.setName (sets[i].getName ()); ss.setDisplayName (sets[i].getDisplayName ()); ss.setShortDescription (sets[i].getShortDescription ()); // modifies the set if it contains name of object property modifySheetSet (ss); s.put (ss); } return s; } /** Modifies the sheet set to contain name of property and name of * original object. */ private void modifySheetSet (Sheet.Set ss) { Property p = ss.remove (DataObject.PROP_NAME); if (p != null) { p = new PropertySupport.Name (this); ss.put (p); p = new Name (); ss.put (p); p = new FileSystemProperty (); ss.put (p); } } private void originalChanged () { DataObject ori = obj.getOriginal(); if (ori.isValid()) { changeOriginal (ori.getNodeDelegate(), true); } else { updateShadowOriginal(obj); } } /** Class that renames the orginal object and also updates * the link */ private final class Name extends PropertySupport.ReadWrite { public Name () { super ( "OriginalName", // NOI18N String.class, DataObject.getString ("PROP_ShadowOriginalName"), DataObject.getString ("HINT_ShadowOriginalName") ); } public Object getValue () { return obj.getOriginal ().getName(); } public void setValue (Object val) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { if (!canWrite()) throw new IllegalAccessException(); if (!(val instanceof String)) throw new IllegalArgumentException(); try { DataObject orig = obj.getOriginal (); orig.rename ((String)val); writeOriginal (null, null, obj.getPrimaryFile (), orig); } catch (IOException ex) { throw new InvocationTargetException (ex); } } public boolean canWrite () { return obj.getOriginal ().isRenameAllowed(); } } /** Class for original filesystem name property of broken link */ private final class FileSystemProperty extends PropertySupport.ReadOnly { public FileSystemProperty () { super ( "OriginalFileSystem", // NOI18N String.class, DataObject.getString ("PROP_ShadowOriginalFileSystem"), DataObject.getString ("HINT_ShadowOriginalFileSystem") ); } /* Getter */ public Object getValue () { return getOriginalFileSystemName(); } } /** Property listener on data object that delegates all changes of * properties to this node. */ private static class PropL extends FilterNode.NodeAdapter { public PropL (ShadowNode sn) { super (sn); } protected void propertyChange (FilterNode fn, PropertyChangeEvent ev) { if (Node.PROP_PROPERTY_SETS.equals(ev.getPropertyName ())) { // clear the sheet ShadowNode sn = (ShadowNode)fn; sn.sheet = null; } super.propertyChange (fn, ev); } } } static final class DSWeakReference extends WeakReference { private int hash; DSWeakReference(Object o, ReferenceQueue rqueue) { super(o, rqueue); hash = o.hashCode(); } public int hashCode() { return hash; } public boolean equals(Object o) { Object mine = get(); if (mine == null) { return false; } if (o instanceof DSWeakReference) { DSWeakReference him = (DSWeakReference) o; return mine.equals(him.get()); } return false; } } }