blob: d8caf75ac22831ed9656cb525797bc44f1301bd6 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.modules.project.ui;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.GraphicsEnvironment;
import java.awt.event.ActionEvent;
import java.io.CharConversionException;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.Action;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
import org.netbeans.api.actions.Openable;
import org.netbeans.api.project.Project;
import org.netbeans.api.project.ProjectUtils;
import static org.netbeans.modules.project.ui.Bundle.*;
import org.netbeans.modules.project.ui.groups.Group;
import org.netbeans.spi.project.AuxiliaryConfiguration;
import org.netbeans.spi.project.ui.support.ProjectConvertors;
import org.openide.cookies.EditCookie;
import org.openide.cookies.OpenCookie;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.filesystems.URLMapper;
import org.openide.loaders.DataObject;
import org.openide.loaders.DataObjectNotFoundException;
import org.openide.nodes.Node;
import org.openide.util.ContextAwareAction;
import org.openide.util.Exceptions;
import org.openide.util.Mutex;
import org.openide.util.NbBundle.Messages;
import org.openide.windows.Mode;
import org.openide.windows.TopComponent;
import org.openide.windows.WindowManager;
import org.openide.xml.XMLUtil;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
/** The util methods for projectui module.
*
* @author Jiri Rechtacek
*/
public class ProjectUtilities {
static final String OPEN_FILES_NS = "http://www.netbeans.org/ns/projectui-open-files/1"; // NOI18N
static final String OPEN_FILES_NS2 = "http://www.netbeans.org/ns/projectui-open-files/2"; // NOI18N
static final String OPEN_FILES_ELEMENT = "open-files"; // NOI18N
static final String FILE_ELEMENT = "file"; // NOI18N
static final String GROUP_ELEMENT = "group"; // NOI18N
static final String NAME_ATTR = "name";
// support class for xtesting in OpenProjectListTest
static OpenCloseProjectDocument OPEN_CLOSE_PROJECT_DOCUMENT_IMPL = new OpenCloseProjectDocument () {
@Override
public boolean open (FileObject fo) {
DataObject dobj;
try {
dobj = DataObject.find (fo);
} catch (DataObjectNotFoundException donfo) {
assert false : "DataObject must exist for " + fo;
return false;
}
EditCookie ec = dobj.getLookup().lookup(EditCookie.class);
OpenCookie oc = dobj.getLookup().lookup(OpenCookie.class);
Openable o = dobj.getLookup().lookup(Openable.class);
if (ec != null) {
ec.edit();
} else if (oc != null) {
oc.open();
} else if (o != null) {
o.open();
} else {
ERR.log(Level.INFO, "No EditCookie nor OpenCookie nor Openable for {0}", dobj);
return false;
}
return true;
}
@Override
public Map<Project,Set<String>> close(final Project[] projects,
final boolean notifyUI) {
final Wrapper wr = new Wrapper();
wr.urls4project = new HashMap<Project,Set<String>>();
doClose(projects, notifyUI, wr);
return wr.urls4project;
}
private void doClose(Project[] projects, boolean notifyUI, Wrapper wr) {
List<Project> listOfProjects = Arrays.asList(projects);
for (Project p : listOfProjects) { //#232668 all projects need an entry in the map - to handle projects without files correctly
wr.urls4project.put(p, new LinkedHashSet<String>());
}
Set<DataObject> openFiles = new HashSet<DataObject>();
final Set<TopComponent> tc2close = new HashSet<TopComponent>();
ERR.finer("Closing TCs");
final Set<TopComponent> openedTC = getOpenedTCs();
for (TopComponent tc : openedTC) {
DataObject dobj = tc.getLookup().lookup(DataObject.class);
if (dobj != null) {
FileObject fobj = dobj.getPrimaryFile();
Project owner = ProjectConvertors.getNonConvertorOwner(fobj);
ERR.log(Level.FINER, "Found {0} owned by {1} in {2} of {3}", new Object[] {fobj, owner, tc.getName(), tc.getClass()});
if (listOfProjects.contains(owner)) {
if (notifyUI) {
openFiles.add(dobj);
tc2close.add(tc);
} else if (!dobj.isModified()) {
// when not called from UI, only include TCs that arenot modified
tc2close.add(tc);
}
//#235897 split a single line to detect NPE better
final Set<String> pwnr = wr.urls4project.get(owner);
assert pwnr != null : "Owner project for file:" + fobj + " prj:" + owner;
final FileObject pf = fobj;
assert pf != null;
URL u = pf.toURL();
assert u != null;
String uex = u.toExternalForm();
pwnr.add(uex);
}
} else {
ERR.log(Level.FINE, "#194243: no DataObject in lookup of {0} of {1}", new Object[] {tc.getName(), tc.getClass()});
}
}
if (notifyUI) {
for (DataObject dobj : DataObject.getRegistry().getModifiedSet()) {
FileObject fobj = dobj.getPrimaryFile();
Project owner = ProjectConvertors.getNonConvertorOwner(fobj);
if (listOfProjects.contains(owner) &&
!openFiles.contains(dobj)) {
openFiles.add(dobj);
}
}
}
if (!notifyUI ||
(!openFiles.isEmpty() && ExitDialog.showDialog(openFiles))) {
Runnable r = new Runnable() {
@Override
public void run() {
// close documents
for (TopComponent tc : tc2close) {
tc.close();
}
}
};
if(SwingUtilities.isEventDispatchThread()) {
r.run();
} else {
SwingUtilities.invokeLater(r);
}
} else {
// signal that close was vetoed
if (!openFiles.isEmpty()) {
wr.urls4project = null;
}
}
}
private Set<TopComponent> getOpenedTCs() {
final Set<TopComponent> openedTC = new HashSet<TopComponent>();
Runnable r = new Runnable() {
public void run() {
WindowManager wm = WindowManager.getDefault();
for (Mode mode : wm.getModes()) {
//#84546 - this condituon should allow us to close just editor related TCs that are in any imaginable mode.
if (!wm.isEditorMode(mode)) {
continue;
}
ERR.log(Level.FINER, "Closing TCs in mode {0}", mode.getName());
openedTC.addAll(Arrays.asList(wm.getOpenedTopComponents(mode)));
}
}
};
if (SwingUtilities.isEventDispatchThread()) {
r.run();
} else {
try {
SwingUtilities.invokeAndWait(r);
}
catch (Exception ex) {
Exceptions.printStackTrace(ex);
}
}
return openedTC;
}
};
private static class Wrapper {
Map<Project,Set<String>> urls4project;
}
private static final Logger ERR = Logger.getLogger(ProjectUtilities.class.getName());
private ProjectUtilities() {}
public static void selectAndExpandProject( final Project p ) {
// invoke later to select the being opened project if the focus is outside ProjectTab
SwingUtilities.invokeLater (new Runnable () {
@Override
public void run () {
final ProjectTab ptLogial = ProjectTab.findDefault(ProjectTab.ID_LOGICAL);
Node root = ptLogial.getExplorerManager ().getRootContext ();
// Node projNode = root.getChildren ().findChild( p.getProjectDirectory().getName () );
Node projNode = null;
for (Node n : root.getChildren().getNodes()) {
Project prj = n.getLookup().lookup(Project.class);
if (prj != null && prj.getProjectDirectory().equals(p.getProjectDirectory())) {
projNode = n;
break;
}
}
if (projNode == null) {
// fallback..
projNode = root.getChildren ().findChild( ProjectUtils.getInformation( p ).getName() );
}
if ( projNode != null ) {
try {
ptLogial.getExplorerManager ().setSelectedNodes( new Node[] { projNode } );
ptLogial.expandNode( projNode );
// ptLogial.open ();
// ptLogial.requestActive ();
} catch (Exception ignore) {
// may ignore it
}
}
}
});
}
/** Invokes the preferred action on given object and tries to select it in
* corresponding view, e.g. in logical view if possible otherwise
* in physical project's view.
* Note: execution this methods can invokes new threads to assure the action
* is called in EQ.
*
* @param newDo new data object
*/
public static void openAndSelectNewObject (final DataObject newDo) {
// call the preferred action on main class
Mutex.EVENT.writeAccess (new Runnable () {
@Override
public void run () {
final Node node = newDo.getNodeDelegate ();
Action a = node.getPreferredAction();
if (a instanceof ContextAwareAction) {
a = ((ContextAwareAction) a).createContextAwareInstance(node.getLookup ());
}
if (a != null) {
a.actionPerformed(new ActionEvent(node, ActionEvent.ACTION_PERFORMED, "")); // NOI18N
}
// next action -> expand && select main class in package view
final ProjectTab ptLogical = ProjectTab.findDefault(ProjectTab.ID_LOGICAL);
final ProjectTab ptPhysical = ProjectTab.findDefault(ProjectTab.ID_PHYSICAL);
ProjectTab.RP.post(new Runnable() {
public @Override void run() {
ProjectTab tab = ptLogical;
Node n = tab.findNode(newDo.getPrimaryFile());
if (n == null) {
tab = ptPhysical;
n = tab.findNode(newDo.getPrimaryFile());
}
if (n != null) {
tab.selectNode(n);
}
}
});
}
});
}
/** Makes the project tab visible
* @param requestFocus if set to true the project tab will not only become visible but also
* will gain focus
*/
public static void makeProjectTabVisible() {
if (Boolean.getBoolean("project.tab.no.selection")) {
return;
}
ProjectTab ptLogical = ProjectTab.findDefault(ProjectTab.ID_LOGICAL);
ptLogical.open();
ptLogical.requestActive();
}
/** Checks if the given file name can be created in the target folder.
*
* @param targetFolder target folder (e.g. source group)
* @param folderName name of the folder relative to target folder (null or /-separated)
* @param newObjectName name of created file
* @param extension extension of created file
* @param allowFileSeparator if '/' (and possibly other file separator, see {@link FileUtil#createFolder FileUtil#createFolder})
* is allowed in the newObjectName
* @return localized error message (HTML-safe) or null if all right
*/
@Messages({
"# {0} - name of the file", "# {1} - an integer representing the invalid characters:", "# 0: both '/' and '\\' are invalid", "# 1: '\\' is invalid", "MSG_not_valid_filename=The filename {0} is not permitted as it contains {1,choice,0#a slash (/) or a backslash (\\)|1#a backslash (\\)}.",
"# {0} - name of the file", "# {1} - an integer representing the invalid characters:", "# 0: both '/' and '\\' are invalid", "# 1: '\\' is invalid", "MSG_not_valid_folder=The folder name {0} is not permitted as it contains {1,choice,0#a slash (/) or a backslash (\\)|1#a backslash (\\)}.",
"MSG_fs_or_folder_does_not_exist=The target folder does not exist.",
"MSG_fs_is_readonly=The target folder is read-only.",
"# {0} - name of the existing file", "MSG_file_already_exist=The file {0} already exists."
})
public static String canUseFileName (FileObject targetFolder, String folderName, String newObjectName,
String extension, boolean allowFileSeparator, boolean freeFileExtension) {
assert newObjectName != null; // SimpleTargetChooserPanel.isValid returns false if it is... XXX should it use an error label instead?
boolean allowSlash = false;
boolean allowBackslash = false;
int errorVariant = 0;
if (allowFileSeparator) {
if (File.separatorChar == '\\') {
errorVariant = 3;
allowSlash = allowBackslash = true;
} else {
errorVariant = 1;
allowSlash = true;
}
}
if ((!allowSlash && newObjectName.indexOf('/') != -1) || (!allowBackslash && newObjectName.indexOf('\\') != -1)) {
//if errorVariant == 3, the test above should never be true:
assert errorVariant == 0 || errorVariant == 1 : "Invalid error variant: " + errorVariant;
return MSG_not_valid_filename(safeEncode(newObjectName), errorVariant);
}
// test whether the selected folder on selected filesystem already exists
if (targetFolder == null) {
return MSG_fs_or_folder_does_not_exist();
}
// target directory should be writable
// We should not check this via java.io.File - this breaks not only non-file-based file systems,
// but can break versioning as well. See issue #251857 (In Remote Favorites tab user can't create new file)
FileObject targetDir = (folderName != null) ? targetFolder.getFileObject(folderName) : targetFolder;
if (targetDir != null) {
if (targetDir.isValid()&& ! targetDir.canWrite ()) {
return MSG_fs_is_readonly();
}
} else if (! targetFolder.canWrite ()) {
return MSG_fs_is_readonly();
}
// file should not already exist
StringBuilder relFileName = new StringBuilder();
if (folderName != null) {
if (!allowBackslash && folderName.indexOf('\\') != -1) {
return MSG_not_valid_folder(safeEncode(folderName), 1);
}
relFileName.append(folderName);
relFileName.append('/');
}
relFileName.append(newObjectName);
String ext = "";
if (extension != null && extension.length() != 0 && (!freeFileExtension || newObjectName.indexOf('.') == -1)) {
ext = "." + extension;
relFileName.append(ext);
}
if (targetFolder.getFileObject(relFileName.toString()) != null) {
return MSG_file_already_exist(safeEncode(newObjectName + ext));
}
// all ok
return null;
}
private static String safeEncode(String text) { // #208432
if (text.length() > 30) {
text = text.substring(0, 30) + '…';
}
try {
return XMLUtil.toElementContent(text.replaceAll("\\s+", " "));
} catch (CharConversionException ex) {
return text;
}
}
public static class WaitCursor implements Runnable {
private boolean show;
private WaitCursor( boolean show ) {
this.show = show;
}
public static void show() {
invoke( new WaitCursor( true ) );
}
public static void hide() {
invoke( new WaitCursor( false ) );
}
private static void invoke( WaitCursor wc ) {
if (GraphicsEnvironment.isHeadless()) {
return;
}
if ( SwingUtilities.isEventDispatchThread() ) {
wc.run();
}
else {
SwingUtilities.invokeLater( wc );
}
}
@Override
public void run() {
try {
JFrame f = (JFrame)WindowManager.getDefault ().getMainWindow ();
Component c = f.getGlassPane ();
c.setVisible ( show );
c.setCursor (show ? Cursor.getPredefinedCursor (Cursor.WAIT_CURSOR) : null);
}
catch (NullPointerException npe) {
Exceptions.printStackTrace(npe);
}
}
}
/** Closes all documents in editor area which are owned by one of given projects.
* If some documents are modified then an user is notified by Save/Discard/Cancel dialog.
* Dialog is showed only once for all project's documents together.
* URLs of closed documents are stored to <code>private.xml</code>.
*
* @param p project to close
* @return false if the user cancelled the Save/Discard/Cancel dialog, true otherwise
*/
public static boolean closeAllDocuments(Project[] projects, boolean notifyUI, String groupName) {
if (projects == null) {
throw new IllegalArgumentException ("No projects are specified."); // NOI18N
}
if (projects.length == 0) {
// no projects to close, no documents will be closed
return true;
}
Map<Project,Set<String>> urls4project = OPEN_CLOSE_PROJECT_DOCUMENT_IMPL.close(projects, notifyUI);
if (urls4project != null) {
// store project's documents
// loop all project being closed
for (Map.Entry<Project,Set<String>> entry : urls4project.entrySet()) {
storeProjectOpenFiles(entry.getKey(), entry.getValue(), groupName);
}
}
return urls4project != null;
}
public static void storeProjectOpenFiles(Project p, Set<String> urls, String groupName) {
AuxiliaryConfiguration aux = ProjectUtils.getAuxiliaryConfiguration(p);
Set<String> openFileUrls = getOpenFilesUrls(p, groupName);
if(urls.isEmpty() && openFileUrls.isEmpty()) {
// was empty, stays empty, leave
return;
}
if(urls.size() == openFileUrls.size()) {
boolean same = true;
for (String url : openFileUrls) {
if(!urls.contains(url)) {
same = false;
break;
}
}
if(same) {
// nothing changed, leave
return;
}
}
aux.removeConfigurationFragment (OPEN_FILES_ELEMENT, OPEN_FILES_NS, false);
Element openFiles = aux.getConfigurationFragment(OPEN_FILES_ELEMENT, OPEN_FILES_NS2, false);
if (openFiles == null) {
Document xml = XMLUtil.createDocument (OPEN_FILES_ELEMENT, OPEN_FILES_NS2, null, null);
openFiles = xml.createElementNS (OPEN_FILES_NS2, OPEN_FILES_ELEMENT);
}
NodeList groups = openFiles.getElementsByTagNameNS(OPEN_FILES_NS2, GROUP_ELEMENT);
for (int i = 0; i < groups.getLength(); i++) {
Element g = (Element) groups.item(i);
String attr = g.getAttribute(NAME_ATTR);
if (attr.equals(groupName) || (attr.equals("") && groupName == null)) {
openFiles.removeChild(g);
break;
}
}
Element groupEl = openFiles.getOwnerDocument ().createElementNS(OPEN_FILES_NS2, GROUP_ELEMENT);
if (groupName != null) {
groupEl.setAttribute(NAME_ATTR, groupName);
}
openFiles.appendChild(groupEl);
Element fileEl;
// loop all open files of given project
for (String url : urls) {
fileEl = groupEl.getOwnerDocument ().createElementNS(OPEN_FILES_NS2, FILE_ELEMENT);
fileEl.appendChild(fileEl.getOwnerDocument().createTextNode(url));
groupEl.appendChild (fileEl);
}
aux.putConfigurationFragment (openFiles, false);
}
/** Opens the project's files read from the private <code>project.xml</code> file
*
* @param p project
*/
public static Set<FileObject> openProjectFiles (Project p) {
Group grp = Group.getActiveGroup();
return openProjectFiles(p, grp);
}
public static Set<FileObject> openProjectFiles (Project p, Group grp) {
String groupName = grp == null ? null : grp.getName();
ERR.log(Level.FINE, "Trying to open files from {0}...", p);
Set<String> urls = getOpenFilesUrls(p, groupName);
Set<FileObject> toRet = new HashSet<FileObject>();
for (String url : urls) {
ERR.log(Level.FINE, "Will try to open {0}", url);
FileObject fo;
try {
fo = URLMapper.findFileObject (new URL (url));
} catch (MalformedURLException mue) {
assert false : "MalformedURLException in " + url;
continue;
}
if (fo == null || !fo.isValid()) { //check for validity because of issue #238488
ERR.log(Level.FINE, "Could not find {0}", url);
continue;
}
//#109676
if (ProjectConvertors.getNonConvertorOwner(fo) != p) {
ERR.log(Level.FINE, "File {0} doesn''t belong to project at {1}", new Object[] {url, p.getProjectDirectory().getPath()});
continue;
}
OPEN_CLOSE_PROJECT_DOCUMENT_IMPL.open (fo);
toRet.add(fo);
}
// clean-up stored files -
// mkleint: I've commented this out as it makes debugging what went wrong when switching groups or exiting the IDE very difficult
// and now that we have per-group settings stored, removing a single group's values does not pose any real advantage.
//aux.removeConfigurationFragment (OPEN_FILES_ELEMENT, OPEN_FILES_NS, false);
// openFiles.removeChild(groupEl);
// if (openFiles.getElementsByTagNameNS(OPEN_FILES_NS2, GROUP_ELEMENT).getLength() > 0) {
// aux.putConfigurationFragment (openFiles, false);
// } else {
// aux.removeConfigurationFragment (OPEN_FILES_ELEMENT, OPEN_FILES_NS2, false);
// }
return toRet;
}
private static Set<String> getOpenFilesUrls(Project p, String groupName) {
AuxiliaryConfiguration aux = ProjectUtils.getAuxiliaryConfiguration(p);
Element openFiles = aux.getConfigurationFragment (OPEN_FILES_ELEMENT, OPEN_FILES_NS2, false);
if (openFiles == null) {
return Collections.emptySet();
}
Element groupEl = null;
NodeList groups = openFiles.getElementsByTagNameNS(OPEN_FILES_NS2, GROUP_ELEMENT);
for (int i = 0; i < groups.getLength(); i++) {
Element g = (Element) groups.item(i);
String attr = g.getAttribute(NAME_ATTR);
if (attr.equals(groupName) || (attr.equals("") && groupName == null)) {
groupEl = g;
break;
}
}
if (groupEl == null) {
return Collections.emptySet();
}
NodeList list = groupEl.getElementsByTagNameNS(OPEN_FILES_NS2, FILE_ELEMENT);
Set<String> toRet = new HashSet<String>();
for (int i = 0; i < list.getLength (); i++) {
String url = list.item (i).getChildNodes ().item (0).getNodeValue ();
toRet.add(url);
}
return toRet;
}
// interface for handling project's documents stored in project private.xml
// it serves for a unit test of OpenProjectList
interface OpenCloseProjectDocument {
// opens stored document in the document area
boolean open(FileObject fo);
// closes documents of given projects and returns mapped document's urls by project
// it's used as base for storing documents in project private.xml
Map<Project,Set<String>> close(Project[] projects, boolean notifyUI);
}
}