/* * Copyright (c) 2012 Sam Harwell, Tunnel Vision Laboratories LLC * All rights reserved. * * The source code of this document is proprietary work, and is not licensed for * distribution. For information about licensing, contact Sam Harwell at: * sam@tunnelvisionlabs.com */ package org.tvl.goworks.project; import java.awt.EventQueue; import java.awt.Image; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.Transferable; import java.awt.datatransfer.UnsupportedFlavorException; import java.awt.dnd.DnDConstants; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.StringTokenizer; import java.util.TreeMap; import java.util.TreeSet; import java.util.logging.Logger; import javax.swing.Action; import javax.swing.SwingUtilities; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import org.netbeans.api.fileinfo.NonRecursiveFolder; import org.netbeans.api.project.FileOwnerQuery; import org.netbeans.api.project.ProjectManager; import org.netbeans.api.project.SourceGroup; import org.netbeans.api.queries.VisibilityQuery; import org.netbeans.spi.project.ActionProvider; import org.netbeans.spi.project.ui.support.FileSensitiveActions; import org.netbeans.spi.search.SearchInfoDefinitionFactory; import org.openide.DialogDisplayer; import org.openide.NotifyDescriptor; import org.openide.actions.FileSystemAction; import org.openide.filesystems.FileAttributeEvent; import org.openide.filesystems.FileChangeListener; import org.openide.filesystems.FileEvent; import org.openide.filesystems.FileObject; import org.openide.filesystems.FileRenameEvent; import org.openide.filesystems.FileStateInvalidException; import org.openide.filesystems.FileSystem; import org.openide.filesystems.FileUtil; import org.openide.loaders.ChangeableDataFilter; import org.openide.loaders.DataFilter; import org.openide.loaders.DataFolder; import org.openide.loaders.DataObject; import org.openide.nodes.Children; import org.openide.nodes.FilterNode; import org.openide.nodes.Node; import org.openide.nodes.PropertySupport; import org.openide.nodes.Sheet; import org.openide.util.ChangeSupport; import org.openide.util.Exceptions; import org.openide.util.ImageUtilities; import org.openide.util.NbBundle; import org.openide.util.RequestProcessor; import org.openide.util.WeakListeners; import org.openide.util.datatransfer.ExTransferable; import org.openide.util.datatransfer.MultiTransferObject; import org.openide.util.datatransfer.PasteType; import org.openide.util.lookup.Lookups; import org.openide.util.lookup.ProxyLookup; /** * * @author Sam Harwell */ final class PackageViewChildren extends Children.Keys<String> implements FileChangeListener, ChangeListener, Runnable { // -J-Dorg.tvl.goworks.project.PackageViewChildren.level=FINE private static final Logger LOGGER = Logger.getLogger(PackageViewChildren.class.getName()); private static final String NODE_NOT_CREATED = "NNC"; // NOI18N private static final String NODE_NOT_CREATED_EMPTY = "NNC_E"; //NOI18N static final String PRIMARY_TYPE = "application"; //NOI18N static final String SUBTYPE = "x-go-" + GoProject.GO_PROJECT_ID + "-packagenodednd"; //NOI18N static final String MASK = "mask"; //NOI18N private static final MessageFormat PACKAGE_FLAVOR = new MessageFormat(PRIMARY_TYPE + "/" + SUBTYPE + "; class=org.tvl.goworks.project.PackageViewChildren$PackageNode; mask={0}"); //NOI18N private final java.util.Map<String,Object/*NODE_NOT_CREATED|NODE_NOT_CREATED_EMPTY|PackageNode*/> names2nodes; private final FileObject root; private final SourceGroup group; private FileChangeListener wfcl; // Weak listener on the system filesystem private ChangeListener wvqcl; // Weak listener on the VisibilityQuery /** * Creates children based on a single source root. * @param root the folder where sources start (must be a package root) */ public PackageViewChildren(SourceGroup group) { // Sem mas dat cache a bude to uplne nejrychlejsi na svete names2nodes = Collections.synchronizedMap(new TreeMap<String,Object>()); this.root = group.getRootFolder(); this.group = group; } FileObject getRoot() { return root; // Used from PackageRootNode } @Override protected Node[] createNodes(String path) { FileObject fo = root.getFileObject(path); if ( fo != null && fo.isValid() && fo.isFolder()) { Object o = names2nodes.get(path); PackageNode n; DataFolder folder; try { folder = DataFolder.findFolder(fo); } catch (IllegalArgumentException iae) { throw new IllegalStateException( MessageFormat.format( "Root: {0}, Path: {1}, Visible: {2}", //NOI18N FileUtil.getFileDisplayName(root), path, VisibilityQuery.getDefault().isVisible(fo)), iae); } if (folder.isValid()) { if ( o == NODE_NOT_CREATED ) { n = new PackageNode(root, folder, false); } else { // NODE_NOT_CREATED_EMPTY, PackageNode n = new PackageNode(root, folder); } names2nodes.put(path, n); return new Node[] {n}; } } return new Node[0]; } RequestProcessor.Task task = PackageRootNode.PKG_VIEW_RP.create( this ); @Override protected void addNotify() { // System.out.println("ADD NOTIFY" + root + " : " + this ); super.addNotify(); task.schedule( 0 ); } @Override public Node[] getNodes( boolean optimal ) { if ( optimal ) { Node[] garbage = super.getNodes( false ); task.waitFinished(); } return super.getNodes( false ); } @Override public Node findChild (String name) { while (true) { Node n = super.findChild(name); if (n != null) { // If already there, get it quickly. return n; } // In case a project is made on a large existing source root, // which happens to have a file in the root dir (so package node // should exist), try to select the root package node soon; no need // to wait for whole tree. try { if (task.waitFinished(5000)) { return super.findChild(name); } // refreshKeysAsync won't run since we are blocking EQ! refreshKeys(); } catch (InterruptedException x) { Exceptions.printStackTrace(x); } } } @Override public void run() { computeKeys(); refreshKeys(); try { FileSystem fs = root.getFileSystem(); wfcl = FileUtil.weakFileChangeListener(this, fs); fs.addFileChangeListener( wfcl ); } catch ( FileStateInvalidException e ) { Exceptions.printStackTrace(e); } wvqcl = WeakListeners.change( this, VisibilityQuery.getDefault() ); VisibilityQuery.getDefault().addChangeListener( wvqcl ); } @Override protected void removeNotify() { // System.out.println("REMOVE NOTIFY" + root + " : " + this ); VisibilityQuery.getDefault().removeChangeListener( wvqcl ); try { root.getFileSystem().removeFileChangeListener( wfcl ); } catch ( FileStateInvalidException e ) { Exceptions.printStackTrace(e); } setKeys(new String[0]); names2nodes.clear(); super.removeNotify(); } // Private methods --------------------------------------------------------- private void refreshKeys() { Set<String> keys; synchronized (names2nodes) { keys = new TreeSet<>(names2nodes.keySet()); } setKeys(keys); } /* #70097: workaround of a javacore deadlock * See related issue: #61027 */ private void refreshKeysAsync () { EventQueue.invokeLater(new Runnable() { @Override public void run () { refreshKeys(); } }); } private void computeKeys() { // XXX this is not going to perform too well for a huge source root... // However we have to go through the whole hierarchy in order to find // all packages (Hrebejk) names2nodes.clear(); findNonExcludedPackages( root ); } /** * Collect all recursive subfolders, except those which have subfolders * but no files. */ private void findNonExcludedPackages( FileObject fo ) { PackageView.findNonExcludedPackages(this, null, fo, group, true); } /** Finds all empty parents of given package and deletes them */ private void cleanEmptyKeys( FileObject fo ) { FileObject parent = fo.getParent(); // Special case for default package if ( root.equals( parent ) ) { PackageNode n = get( parent ); // the default package is considered empty if it only contains folders, // regardless of the contents of these folders (empty or not) if ( n != null && PackageDisplayUtils.isEmpty( root, false ) ) { remove( root ); } return; } while ( FileUtil.isParentOf( root, parent ) ) { PackageNode n = get( parent ); if ( n != null && n.isLeaf() ) { // System.out.println("Cleaning " + parent); remove( parent ); } parent = parent.getParent(); } } // Non private only to be able to have the findNonExcludedPackages impl // in on place (PackageView) void add(FileObject fo, boolean empty, boolean refreshImmediately) { String path = FileUtil.getRelativePath( root, fo ); assert path != null : "Adding wrong folder " + fo +"(valid="+fo.isValid()+")"+ "under root" + this.root + "(valid="+this.root.isValid()+")"; if ( get( fo ) == null ) { names2nodes.put( path, empty ? NODE_NOT_CREATED_EMPTY : NODE_NOT_CREATED ); if (refreshImmediately) { refreshKeysAsync(); } else { synchronized (this) { if (refreshLazilyTask == null) { refreshLazilyTask = PackageRootNode.PKG_VIEW_RP.post(new Runnable() { @Override public void run() { synchronized (PackageViewChildren.this) { refreshLazilyTask = null; refreshKeysAsync(); } } }, 2500); } } } } } private RequestProcessor.Task refreshLazilyTask; private void remove( FileObject fo ) { String path = FileUtil.getRelativePath( root, fo ); assert path != null : "Removing wrong folder" + fo; names2nodes.remove( path ); } private void removeSubTree (FileObject fo) { String path = FileUtil.getRelativePath( root, fo ); assert path != null : "Removing wrong folder" + fo; synchronized (names2nodes) { Set<String> keys = names2nodes.keySet(); keys.remove(path); path = path + '/'; //NOI18N Iterator<String> it = keys.iterator(); while (it.hasNext()) { if (it.next().startsWith(path)) { it.remove(); } } } } private PackageNode get( FileObject fo ) { String path = FileUtil.getRelativePath( root, fo ); assert path != null : "Asking for wrong folder" + fo; Object o = names2nodes.get( path ); return !isNodeCreated( o ) ? null : (PackageNode)o; } private boolean contains( FileObject fo ) { String path = FileUtil.getRelativePath( root, fo ); assert path != null : "Asking for wrong folder" + fo; Object o = names2nodes.get( path ); return o != null; } private boolean exists( FileObject fo ) { String path = FileUtil.getRelativePath( root, fo ); return names2nodes.get( path ) != null; } private boolean isNodeCreated( Object o ) { return o instanceof Node; } private PackageNode updatePath( String oldPath, String newPath ) { assert newPath != null; Object o = names2nodes.get( oldPath ); if ( o == null ) { return null; } names2nodes.remove( oldPath ); names2nodes.put( newPath, o ); return !isNodeCreated( o ) ? null : (PackageNode)o; } // Implementation of FileChangeListener ------------------------------------ @Override public void fileAttributeChanged( FileAttributeEvent fe ) {} @Override public void fileChanged( FileEvent fe ) {} @Override public void fileFolderCreated(final FileEvent fe ) { FileObject fo = fe.getFile(); if ( FileUtil.isParentOf( root, fo ) && isVisible( root, fo ) ) { if (ProjectManager.mutex().isReadAccess() || ProjectManager.mutex().isWriteAccess()) { PackageRootNode.PKG_VIEW_RP.post(new Runnable() { @Override public void run() { fileFolderCreated(fe); } }); return; } cleanEmptyKeys( fo ); // add( fo, false); findNonExcludedPackages( fo ); refreshKeys(); } } @Override public void fileDataCreated( final FileEvent fe ) { FileObject fo = fe.getFile(); if ( FileUtil.isParentOf( root, fo ) && isVisible( root, fo ) ) { if (ProjectManager.mutex().isReadAccess() || ProjectManager.mutex().isWriteAccess()) { PackageRootNode.PKG_VIEW_RP.post(new Runnable() { @Override public void run() { fileDataCreated(fe); } }); return; } FileObject parent = fo.getParent(); if (!parent.isFolder()) { throw new IllegalStateException(FileUtil.getFileDisplayName(parent) + " is not a folder!"); //NOI18N } // XXX consider using group.contains() here if ( !VisibilityQuery.getDefault().isVisible( parent ) ) { return; // Adding file into ignored directory } PackageNode n = get( parent ); if ( n == null && !contains( parent ) ) { add(parent, false, true); refreshKeysAsync(); } else if ( n != null ) { n.updateChildren(); } } } @Override public void fileDeleted( final FileEvent fe ) { FileObject fo = fe.getFile(); // System.out.println("FILE DELETED " + FileUtil.getRelativePath( root, fo ) ); if ( FileUtil.isParentOf( root, fo ) && isVisible( root, fo ) ) { if (ProjectManager.mutex().isReadAccess() || ProjectManager.mutex().isWriteAccess()) { PackageRootNode.PKG_VIEW_RP.post(new Runnable() { @Override public void run() { // why don't we jave closures in java? :( fileDeleted(fe); } }); return; } // System.out.println("IS FOLDER? " + fo + " : " + fo.isFolder() ); /* Hack for MasterFS see #42464 */ if ( fo.isFolder() || get( fo ) != null ) { // System.out.println("REMOVING FODER " + fo ); removeSubTree( fo ); // Now add the parent if necessary FileObject parent = fo.getParent(); if (!parent.isFolder()) { throw new IllegalStateException(FileUtil.getFileDisplayName(parent) + " is not a folder!"); //NOI18N } if ( ( FileUtil.isParentOf( root, parent ) || root.equals( parent ) ) && get( parent ) == null && parent.isValid() ) { // Candidate for adding if ( !toBeRemoved( parent ) ) { // System.out.println("ADDING PARENT " + parent ); add(parent, true, true); } } refreshKeysAsync(); } else { FileObject parent = fo.getParent(); final PackageNode n = get( parent ); if ( n != null ) { //#61027: workaround to a deadlock when the package is being changed from non-leaf to leaf: boolean leaf = n.isLeaf(); DataFolder df = n.getDataFolder(); boolean empty = isEmpty(df); if (leaf != empty) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { n.updateChildren(); } }); } else { n.updateChildren(); } } // If the parent folder only contains folders remove it if ( toBeRemoved( parent ) ) { remove( parent ); refreshKeysAsync(); } } } // else { // System.out.println("NOT A PARENT " + fo ); // } } /** Returns true if the folder should be removed from the view * i.e. it has some unignored children and the children are folders only */ private boolean toBeRemoved( FileObject folder ) { boolean ignoredOnly = true; boolean foldersOnly = true; for (FileObject kid : folder.getChildren()) { // XXX consider using group.contains() here if (VisibilityQuery.getDefault().isVisible(kid)) { ignoredOnly = false; if (!kid.isFolder()) { foldersOnly = false; break; } } } if ( ignoredOnly ) { return false; // It is either empty or it only contains ignored files // thus is leaf and it means package } else { return foldersOnly; } } @Override public void fileRenamed( final FileRenameEvent fe ) { FileObject fo = fe.getFile(); if ( FileUtil.isParentOf( root, fo ) && fo.isFolder() ) { if (ProjectManager.mutex().isReadAccess() || ProjectManager.mutex().isWriteAccess()) { PackageRootNode.PKG_VIEW_RP.post(new Runnable() { @Override public void run() { // why don't we jave closures in java? :( fileRenamed(fe); } }); return; } String rp = FileUtil.getRelativePath( root, fo.getParent() ); String oldPath = rp + ( rp.length() == 0 ? "" : "/" ) + fe.getName() + fe.getExt(); // NOI18N // XXX consider using group.contains() here boolean visible = VisibilityQuery.getDefault().isVisible( fo ); boolean doUpdate = false; // Find all entries which have to be updated List<String> needsUpdate = new ArrayList<>(); synchronized (names2nodes) { for (Iterator<String> it = names2nodes.keySet().iterator(); it.hasNext(); ) { String p = it.next(); if ( p.startsWith( oldPath ) ) { if ( visible ) { needsUpdate.add( p ); } else { it.remove(); doUpdate = true; } } } } // If the node does not exists then there might have been update // from ignored to non ignored if ( get( fo ) == null && visible ) { cleanEmptyKeys( fo ); findNonExcludedPackages( fo ); doUpdate = true; // force refresh } int oldPathLen = oldPath.length(); String newPath = FileUtil.getRelativePath( root, fo ); for (String p : needsUpdate) { StringBuilder np = new StringBuilder(p); np.replace( 0, oldPathLen, newPath ); PackageNode n = updatePath( p, np.toString() ); // Replace entries in cache if ( n != null ) { n.updateDisplayName(); // Update nodes } } if ( needsUpdate.size() > 1 || doUpdate ) { // Sorting might change refreshKeys(); } } /* else if ( FileUtil.isParentOf( root, fo ) && fo.isFolder() ) { FileObject parent = fo.getParent(); PackageNode n = get( parent ); if ( n != null && VisibilityQuery.getDefault().isVisible( parent ) ) { n.updateChildren(); } } */ } /** Test whether file and all it's parent up to parent paremeter * are visible */ private boolean isVisible( FileObject parent, FileObject file ) { do { // XXX consider using group.contains() here if ( !VisibilityQuery.getDefault().isVisible( file ) ) { return false; } file = file.getParent(); } while ( file != null && file != parent ); return true; } // Implementation of ChangeListener ------------------------------------ @Override public void stateChanged( ChangeEvent e ) { computeKeys(); refreshKeys(); } /* private void debugKeySet() { for( Iterator it = names2nodes.keySet().iterator(); it.hasNext(); ) { String k = (String)it.next(); System.out.println( " " + k + " -> " + names2nodes.get( k ) ); } } */ private final DataFilter NO_FOLDERS_FILTER = new NoFoldersDataFilter(); private static boolean isEmpty(DataFolder dataFolder) { if ( dataFolder == null ) { return true; } return PackageDisplayUtils.isEmpty( dataFolder.getPrimaryFile() ); } final class PackageNode extends FilterNode { private Action actions[]; private final FileObject root; private DataFolder dataFolder; private boolean isDefaultPackage; public PackageNode( FileObject root, DataFolder dataFolder ) { this( root, dataFolder, isEmpty( dataFolder ) ); } public PackageNode( FileObject root, DataFolder dataFolder, boolean empty ) { super( dataFolder.getNodeDelegate(), empty ? FilterNode.Children.LEAF : dataFolder.createNodeChildren( NO_FOLDERS_FILTER ), new ProxyLookup( Lookups.singleton(new GoActionProvider((GoProject)FileOwnerQuery.getOwner(root))), Lookups.singleton(FileOwnerQuery.getOwner(root)), Lookups.singleton(new NoFoldersContainer (dataFolder)), dataFolder.getNodeDelegate().getLookup(), Lookups.singleton(SearchInfoDefinitionFactory.createFlatSearchInfo( dataFolder.getPrimaryFile())))); this.root = root; this.dataFolder = dataFolder; this.isDefaultPackage = root.equals( dataFolder.getPrimaryFile() ); } FileObject getRoot() { return root; // Used from PackageRootNode } @Override public String getName() { String relativePath = FileUtil.getRelativePath(root, dataFolder.getPrimaryFile()); return relativePath == null ? null : relativePath.replace('/', '.'); // NOI18N } @NbBundle.Messages("LBL_CompilePackage_Action=Compile Package") @Override public Action[] getActions( boolean context ) { if ( !context ) { if ( actions == null ) { // Copy actions and leave out the PropertiesAction and FileSystemAction. Action superActions[] = super.getActions( context ); List<Action> actionList = new ArrayList<>(superActions.length); for( int i = 0; i < superActions.length; i++ ) { /*if ( (i <= superActions.length - 2) && superActions[ i ] == null && superActions[i + 1] instanceof PropertiesAction ) { i ++; continue; } else if ( superActions[i] instanceof PropertiesAction ) { continue; } else*/ if ( superActions[i] instanceof FileSystemAction ) { actionList.add (null); // insert separator and new action actionList.add (FileSensitiveActions.fileCommandAction(ActionProvider.COMMAND_COMPILE_SINGLE, Bundle.LBL_CompilePackage_Action(), null)); } actionList.add( superActions[i] ); } actions = new Action[ actionList.size() ]; actionList.toArray( actions ); } return actions; } else { return super.getActions( context ); } } @Override public boolean canRename() { if ( isDefaultPackage ) { return false; } else { return true; } } @Override public boolean canCut () { return !isDefaultPackage; } /** * Copy handling */ @Override public Transferable clipboardCopy () throws IOException { try { return new PackageTransferable (this, DnDConstants.ACTION_COPY); } catch (ClassNotFoundException e) { throw new AssertionError(e); } } @Override public Transferable clipboardCut () throws IOException { try { return new PackageTransferable (this, DnDConstants.ACTION_MOVE); } catch (ClassNotFoundException e) { throw new AssertionError(e); } } @Override public /*@Override*/ Transferable drag () throws IOException { try { return new PackageTransferable (this, DnDConstants.ACTION_NONE); } catch (ClassNotFoundException e) { throw new AssertionError(e); } } @Override public PasteType[] getPasteTypes(Transferable t) { if (t.isDataFlavorSupported(ExTransferable.multiFlavor)) { try { MultiTransferObject mto = (MultiTransferObject) t.getTransferData (ExTransferable.multiFlavor); boolean hasPackageFlavor = false; for (int i=0; i < mto.getCount(); i++) { DataFlavor[] flavors = mto.getTransferDataFlavors(i); if (isPackageFlavor(flavors)) { hasPackageFlavor = true; } } return hasPackageFlavor ? new PasteType[0] : super.getPasteTypes (t); } catch (UnsupportedFlavorException | IOException e) { Exceptions.printStackTrace(e); return new PasteType[0]; } } else { DataFlavor[] flavors = t.getTransferDataFlavors(); if (isPackageFlavor(flavors)) { return new PasteType[0]; } else { return super.getPasteTypes(t); } } } @Override public /*@Override*/ PasteType getDropType (Transferable t, int action, int index) { if (t.isDataFlavorSupported(ExTransferable.multiFlavor)) { try { MultiTransferObject mto = (MultiTransferObject) t.getTransferData (ExTransferable.multiFlavor); boolean hasPackageFlavor = false; for (int i=0; i < mto.getCount(); i++) { DataFlavor[] flavors = mto.getTransferDataFlavors(i); if (isPackageFlavor(flavors)) { hasPackageFlavor = true; } } return hasPackageFlavor ? null : super.getDropType (t, action, index); } catch (UnsupportedFlavorException | IOException e) { Exceptions.printStackTrace(e); return null; } } else { DataFlavor[] flavors = t.getTransferDataFlavors(); if (isPackageFlavor(flavors)) { return null; } else { return super.getDropType (t, action, index); } } } private boolean isPackageFlavor (DataFlavor[] flavors) { for (int i=0; i<flavors.length; i++) { if (SUBTYPE.equals(flavors[i].getSubType ()) && PRIMARY_TYPE.equals(flavors[i].getPrimaryType ())) { //Disable pasting into package, only paste into root is allowed return true; } } return false; } @NbBundle.Messages("MSG_InvalidPackageName=Name is not a valid Java package.") @Override public void setName(String name) { if (isDefaultPackage) { return; } String oldName = getName(); if (oldName.equals(name)) { return; } if (!isValidPackageName (name)) { DialogDisplayer.getDefault().notify(new NotifyDescriptor.Message(Bundle.MSG_InvalidPackageName(), NotifyDescriptor.INFORMATION_MESSAGE)); return; } name = name.replace('.','/')+'/'; //NOI18N oldName = oldName.replace('.','/')+'/'; //NOI18N int i; for (i=0; i<oldName.length() && i< name.length(); i++) { if (oldName.charAt(i) != name.charAt(i)) { break; } } i--; int index = oldName.lastIndexOf('/',i); //NOI18N String commonPrefix = index == -1 ? null : oldName.substring(0,index); String toCreate = (index+1 == name.length()) ? "" : name.substring(index+1); //NOI18N try { FileObject commonFolder = commonPrefix == null ? this.root : this.root.getFileObject(commonPrefix); FileObject destination = commonFolder; StringTokenizer dtk = new StringTokenizer(toCreate,"/"); //NOI18N while (dtk.hasMoreTokens()) { String pathElement = dtk.nextToken(); FileObject tmp = destination.getFileObject(pathElement); if (tmp == null) { tmp = destination.createFolder (pathElement); } destination = tmp; } FileObject source = this.dataFolder.getPrimaryFile(); DataFolder sourceFolder = DataFolder.findFolder (source); DataFolder destinationFolder = DataFolder.findFolder (destination); DataObject[] children = sourceFolder.getChildren(); for (int j=0; j<children.length; j++) { if (children[j].getPrimaryFile().isData()) { children[j].move(destinationFolder); } } while (!commonFolder.equals(source)) { if (source.getChildren().length==0) { FileObject tmp = source; source = source.getParent(); tmp.delete(); } else { break; } } } catch (IOException ioe) { Exceptions.printStackTrace(ioe); } } @Override public boolean canDestroy() { if ( isDefaultPackage ) { return false; } else { return true; } } @Override public void destroy() throws IOException { FileObject parent = dataFolder.getPrimaryFile().getParent(); // First; delete all files except packages DataObject ch[] = dataFolder.getChildren(); boolean empty = true; for( int i = 0; ch != null && i < ch.length; i++ ) { if ( !ch[i].getPrimaryFile().isFolder() ) { ch[i].delete(); } else { empty = false; } } // If empty delete itself if ( empty ) { super.destroy(); } // Second; delete empty super packages while( !parent.equals( root ) && parent.getChildren().length == 0 ) { FileObject newParent = parent.getParent(); parent.delete(); parent = newParent; } } /** * Initially overridden to support CVS status labels in package nodes. * * @return annotated display name */ @Override public String getHtmlDisplayName() { String name = getDisplayName(); try { FileObject fo = dataFolder.getPrimaryFile(); Set<FileObject> set = new NonRecursiveFolderSet(fo); FileSystem.Status status = fo.getFileSystem().getStatus(); if (status instanceof FileSystem.HtmlStatus) { name = ((FileSystem.HtmlStatus) status).annotateNameHtml(name, set); } else { // #89138: return null if the name starts with '<' and status is not HtmlStatus if (name.startsWith("<")) { name = null; } else { name = status.annotateName(name, set); } } } catch (FileStateInvalidException e) { // no fs, do nothing } return name; } @Override public String getDisplayName() { FileObject folder = dataFolder.getPrimaryFile(); String path = FileUtil.getRelativePath(root, folder); if (path == null) { // ??? return ""; } return PackageDisplayUtils.getDisplayLabel( path.replace('/', '.')); } @Override public String getShortDescription() { FileObject folder = dataFolder.getPrimaryFile(); String path = FileUtil.getRelativePath(root, folder); if (path == null) { // ??? return ""; } return PackageDisplayUtils.getToolTip(folder, path.replace('/', '.')); } @Override public Image getIcon (int type) { Image img = getMyIcon (type); try { FileObject fo = dataFolder.getPrimaryFile(); Set<FileObject> set = new NonRecursiveFolderSet(fo); img = fo.getFileSystem ().getStatus ().annotateIcon (img, type, set); } catch (FileStateInvalidException e) { // no fs, do nothing } return img; } @Override public Image getOpenedIcon (int type) { Image img = getMyOpenedIcon(type); try { FileObject fo = dataFolder.getPrimaryFile(); Set<FileObject> set = new NonRecursiveFolderSet(fo); img = fo.getFileSystem ().getStatus ().annotateIcon (img, type, set); } catch (FileStateInvalidException e) { // no fs, do nothing } return img; } private Image getMyIcon(int type) { FileObject folder = dataFolder.getPrimaryFile(); String path = FileUtil.getRelativePath(root, folder); if (path == null) { // ??? - #103711: null cannot be returned because the icon // must be annotated; general package icon is returned instead return ImageUtilities.loadImage(PackageDisplayUtils.PACKAGE); } return PackageDisplayUtils.getIcon(folder, isLeaf()); } private Image getMyOpenedIcon(int type) { return getMyIcon(type); } public void update() { fireIconChange(); fireOpenedIconChange(); } public void updateDisplayName() { fireNameChange(null, null); fireDisplayNameChange(null, null); fireShortDescriptionChange(null, null); } public void updateChildren() { boolean leaf = isLeaf(); DataFolder df = getDataFolder(); boolean empty = isEmpty( df ); if ( leaf != empty ) { setChildren( empty ? FilterNode.Children.LEAF: df.createNodeChildren( NO_FOLDERS_FILTER ) ); update(); } } @Override public Node.PropertySet[] getPropertySets () { final Node.PropertySet[] properties = super.getPropertySets (); final List<Node.PropertySet> result = new ArrayList<>(properties.length); for (Node.PropertySet set : properties) { if (set == null) { continue; } if (Sheet.PROPERTIES.equals(set.getName())) { //Replace the Sheet.PROPERTIES by the new one //having only the name property which does refactoring set = Sheet.createPropertiesSet(); ((Sheet.Set)set).put(new PropertySupport.ReadWrite<String>(DataObject.PROP_NAME, String.class, Bundle.PROP_name(), Bundle.HINT_name()) { @Override public String getValue() { return PackageViewChildren.PackageNode.this.getName(); } @Override public void setValue(String n) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { if (!canRename()) { throw new IllegalAccessException(); } PackageViewChildren.PackageNode.this.setName(n); } @Override public boolean canWrite() { return PackageViewChildren.PackageNode.this.canRename(); } }); } result.add(set); } return result.toArray(new Node.PropertySet[result.size()]); } private DataFolder getDataFolder() { return getCookie(DataFolder.class); } } static boolean isValidPackageName(String name) { if (name.length() == 0) { //Fast check of default pkg return true; } StringTokenizer tk = new StringTokenizer(name,".",true); //NOI18N boolean delimExpected = false; while (tk.hasMoreTokens()) { String namePart = tk.nextToken(); if (!delimExpected) { if (namePart.equals(".")) { //NOI18N return false; } for (int i=0; i< namePart.length(); i++) { char c = namePart.charAt(i); if (i == 0) { if (!Character.isJavaIdentifierStart (c)) { return false; } } else { if (!Character.isJavaIdentifierPart(c)) { return false; } } } } else { if (!namePart.equals(".")) { //NOI18N return false; } } delimExpected = !delimExpected; } return delimExpected; } private static final class NoFoldersContainer implements DataObject.Container, PropertyChangeListener, NonRecursiveFolder { private DataFolder folder; private PropertyChangeSupport prop = new PropertyChangeSupport (this); public NoFoldersContainer (DataFolder folder) { this.folder = folder; } @Override public FileObject getFolder() { return folder.getPrimaryFile(); } @Override public DataObject[] getChildren () { DataObject[] arr = folder.getChildren (); List<DataObject> list = new ArrayList<>(arr.length); for (int i = 0; i < arr.length; i++) { if (arr[i] instanceof DataFolder) continue; list.add (arr[i]); } return list.size() == arr.length ? arr : list.toArray(new DataObject[0]); } @Override public void addPropertyChangeListener(PropertyChangeListener l) { prop.addPropertyChangeListener (l); } @Override public void removePropertyChangeListener(PropertyChangeListener l) { prop.removePropertyChangeListener (l); } @Override public void propertyChange(PropertyChangeEvent evt) { if (DataObject.Container.PROP_CHILDREN.equals (evt.getPropertyName ())) { prop.firePropertyChange (PROP_CHILDREN, null, null); } } } final class NoFoldersDataFilter implements ChangeListener, ChangeableDataFilter, DataFilter.FileBased { private final ChangeSupport cs = new ChangeSupport(this); public NoFoldersDataFilter() { VisibilityQuery.getDefault().addChangeListener(WeakListeners.change(this, VisibilityQuery.getDefault())); } @Override public boolean acceptDataObject(DataObject obj) { return acceptFileObject(obj.getPrimaryFile()); } @Override public void stateChanged( ChangeEvent e) { cs.fireChange(); } @Override public void addChangeListener( ChangeListener listener ) { cs.addChangeListener(listener); } @Override public void removeChangeListener( ChangeListener listener ) { cs.removeChangeListener(listener); } @Override public boolean acceptFileObject(FileObject fo) { return fo.isValid() && VisibilityQuery.getDefault().isVisible(fo) && fo.isData() && group.contains(fo); } } static class PackageTransferable extends ExTransferable.Single { private PackageNode node; public PackageTransferable (PackageNode node, int operation) throws ClassNotFoundException { super(new DataFlavor(PACKAGE_FLAVOR.format(new Object[] {operation}), null, PackageNode.class.getClassLoader())); this.node = node; } @Override protected Object getData() throws IOException, UnsupportedFlavorException { return this.node; } } /** * FileObject set that represents package. It means * that it's content must not be processed recursively. */ private static class NonRecursiveFolderSet extends HashSet<FileObject> implements NonRecursiveFolder { private final FileObject folder; /** * Creates set with one element, the folder. */ public NonRecursiveFolderSet(FileObject folder) { this.folder = folder; add(folder); } @Override public FileObject getFolder() { return folder; } } }