001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041:
042: package org.netbeans.modules.project.ui;
043:
044: import java.awt.Component;
045: import java.awt.Cursor;
046: import java.awt.event.ActionEvent;
047: import java.io.File;
048: import java.net.MalformedURLException;
049: import java.net.URL;
050: import java.util.Arrays;
051: import java.util.HashMap;
052: import java.util.HashSet;
053: import java.util.List;
054: import java.util.Map;
055: import java.util.Set;
056: import java.util.SortedSet;
057: import java.util.TreeSet;
058: import java.util.logging.Level;
059: import java.util.logging.Logger;
060: import javax.swing.Action;
061: import javax.swing.JFrame;
062: import javax.swing.SwingUtilities;
063: import org.netbeans.api.project.FileOwnerQuery;
064: import org.netbeans.api.project.Project;
065: import org.netbeans.api.project.ProjectUtils;
066: import org.netbeans.spi.project.AuxiliaryConfiguration;
067: import org.openide.cookies.EditCookie;
068: import org.openide.cookies.OpenCookie;
069: import org.openide.filesystems.FileObject;
070: import org.openide.filesystems.FileStateInvalidException;
071: import org.openide.filesystems.URLMapper;
072: import org.openide.loaders.DataObject;
073: import org.openide.loaders.DataObjectNotFoundException;
074: import org.openide.nodes.Node;
075: import org.openide.util.ContextAwareAction;
076: import org.openide.util.Exceptions;
077: import org.openide.util.Mutex;
078: import org.openide.util.NbBundle;
079: import org.openide.windows.TopComponent;
080: import org.openide.windows.WindowManager;
081: import org.openide.xml.XMLUtil;
082: import org.w3c.dom.Document;
083: import org.w3c.dom.Element;
084: import org.w3c.dom.NodeList;
085:
086: /** The util methods for projectui module.
087: *
088: * @author Jiri Rechtacek
089: */
090: public class ProjectUtilities {
091:
092: static final String OPEN_FILES_NS = "http://www.netbeans.org/ns/projectui-open-files/1"; // NOI18N
093: static final String OPEN_FILES_ELEMENT = "open-files"; // NOI18N
094: static final String FILE_ELEMENT = "file"; // NOI18N
095:
096: // support class for xtesting in OpenProjectListTest
097: static OpenCloseProjectDocument OPEN_CLOSE_PROJECT_DOCUMENT_IMPL = new OpenCloseProjectDocument() {
098: public boolean open(FileObject fo) {
099: DataObject dobj;
100: try {
101: dobj = DataObject.find(fo);
102: } catch (DataObjectNotFoundException donfo) {
103: assert false : "DataObject must exist for " + fo;
104: return false;
105: }
106: EditCookie ec = dobj.getCookie(EditCookie.class);
107: OpenCookie oc = dobj.getCookie(OpenCookie.class);
108: if (ec != null) {
109: ec.edit();
110: } else if (oc != null) {
111: oc.open();
112: } else {
113: ERR.log(Level.INFO,
114: "No EditCookie nor OpenCookie for {0}", dobj);
115: return false;
116: }
117: return true;
118: }
119:
120: public Map<Project, SortedSet<String>> close(
121: final Project[] projects, final boolean notifyUI) {
122: final Wrapper wr = new Wrapper();
123:
124: wr.urls4project = new HashMap<Project, SortedSet<String>>();
125: if (SwingUtilities.isEventDispatchThread()) {
126: doClose(projects, notifyUI, wr);
127: } else {
128: try {
129: SwingUtilities.invokeAndWait(new Runnable() {
130: public void run() {
131: doClose(projects, notifyUI, wr);
132: }
133: });
134: } catch (Exception ex) {
135: Exceptions.printStackTrace(ex);
136: }
137: }
138: return wr.urls4project;
139: }
140:
141: private void doClose(Project[] projects, boolean notifyUI,
142: Wrapper wr) {
143: List<Project> listOfProjects = Arrays.asList(projects);
144: Set<DataObject> openFiles = new HashSet<DataObject>();
145: final Set<TopComponent> tc2close = new HashSet<TopComponent>();
146: WindowManager wm = WindowManager.getDefault();
147: for (TopComponent tc : wm.getRegistry().getOpened()) {
148: //#84546 - this condituon should allow us to close just editor related TCs that are in any imaginable mode.
149: if (!wm.isOpenedEditorTopComponent(tc)) {
150: continue;
151: }
152: DataObject dobj = tc.getLookup().lookup(
153: DataObject.class);
154:
155: if (dobj != null) {
156: FileObject fobj = dobj.getPrimaryFile();
157: Project owner = FileOwnerQuery.getOwner(fobj);
158:
159: if (listOfProjects.contains(owner)) {
160: if (notifyUI) {
161: openFiles.add(dobj);
162: tc2close.add(tc);
163: } else if (!dobj.isModified()) {
164: // when not called from UI, only include TCs that arenot modified
165: tc2close.add(tc);
166: }
167: if (!wr.urls4project.containsKey(owner)) {
168: // add project
169: wr.urls4project.put(owner,
170: new TreeSet<String>());
171: }
172: URL url = null;
173:
174: try {
175: url = dobj.getPrimaryFile().getURL();
176: wr.urls4project.get(owner).add(
177: url.toExternalForm());
178: } catch (FileStateInvalidException fsie) {
179: assert false : "FileStateInvalidException in "
180: + dobj.getPrimaryFile();
181: }
182: }
183: }
184: }
185: if (notifyUI) {
186: for (DataObject dobj : DataObject.getRegistry()
187: .getModifiedSet()) {
188: FileObject fobj = dobj.getPrimaryFile();
189: Project owner = FileOwnerQuery.getOwner(fobj);
190:
191: if (listOfProjects.contains(owner)
192: && !openFiles.contains(dobj)) {
193: openFiles.add(dobj);
194: }
195: }
196: }
197: if (!notifyUI
198: || (!openFiles.isEmpty() && ExitDialog
199: .showDialog(openFiles))) {
200: // close documents
201: for (TopComponent tc : tc2close) {
202: tc.close();
203: }
204: } else {
205: // signal that close was vetoed
206: if (!openFiles.isEmpty()) {
207: wr.urls4project = null;
208: }
209: }
210: }
211: };
212:
213: private static class Wrapper {
214: Map<Project, SortedSet<String>> urls4project;
215: }
216:
217: private static final Logger ERR = Logger
218: .getLogger(ProjectUtilities.class.getName());
219:
220: private ProjectUtilities() {
221: }
222:
223: public static void selectAndExpandProject(final Project p) {
224:
225: // invoke later to select the being opened project if the focus is outside ProjectTab
226: SwingUtilities.invokeLater(new Runnable() {
227:
228: final ProjectTab ptLogial = ProjectTab
229: .findDefault(ProjectTab.ID_LOGICAL);
230:
231: public void run() {
232: Node root = ptLogial.getExplorerManager()
233: .getRootContext();
234: // Node projNode = root.getChildren ().findChild( p.getProjectDirectory().getName () );
235: Node projNode = null;
236: for (Node n : root.getChildren().getNodes()) {
237: Project prj = n.getLookup().lookup(Project.class);
238: if (prj != null
239: && prj.getProjectDirectory().equals(
240: p.getProjectDirectory())) {
241: projNode = n;
242: break;
243: }
244: }
245: if (projNode == null) {
246: // fallback..
247: projNode = root.getChildren().findChild(
248: ProjectUtils.getInformation(p).getName());
249: }
250:
251: if (projNode != null) {
252: try {
253: ptLogial.getExplorerManager().setSelectedNodes(
254: new Node[] { projNode });
255: ptLogial.expandNode(projNode);
256: // ptLogial.open ();
257: // ptLogial.requestActive ();
258: } catch (Exception ignore) {
259: // may ignore it
260: }
261: }
262: }
263: });
264:
265: }
266:
267: /** Invokes the preferred action on given object and tries to select it in
268: * corresponding view, e.g. in logical view if possible otherwise
269: * in physical project's view.
270: * Note: execution this methods can invokes new threads to assure the action
271: * is called in EQ.
272: *
273: * @param newDo new data object
274: */
275: public static void openAndSelectNewObject(final DataObject newDo) {
276: // call the preferred action on main class
277: Mutex.EVENT.writeAccess(new Runnable() {
278: public void run() {
279: final Node node = newDo.getNodeDelegate();
280: Action a = node.getPreferredAction();
281: if (a instanceof ContextAwareAction) {
282: a = ((ContextAwareAction) a)
283: .createContextAwareInstance(node
284: .getLookup());
285: }
286: if (a != null) {
287: a.actionPerformed(new ActionEvent(node,
288: ActionEvent.ACTION_PERFORMED, "")); // NOI18N
289: }
290:
291: // next action -> expand && select main class in package view
292: final ProjectTab ptLogical = ProjectTab
293: .findDefault(ProjectTab.ID_LOGICAL);
294: final ProjectTab ptPhysical = ProjectTab
295: .findDefault(ProjectTab.ID_PHYSICAL);
296: // invoke later, Mutex.EVENT.writeAccess isn't suffice to
297: // select && expand if the focus is outside ProjectTab
298: SwingUtilities.invokeLater(new Runnable() {
299: public void run() {
300: boolean success = ptLogical.selectNode(newDo
301: .getPrimaryFile());
302: if (!success) {
303: ptPhysical.selectNode(newDo
304: .getPrimaryFile());
305: }
306: }
307: });
308: }
309: });
310: }
311:
312: /** Makes the project tab visible
313: * @param requestFocus if set to true the project tab will not only become visible but also
314: * will gain focus
315: */
316: public static void makeProjectTabVisible(final boolean requestFocus) {
317: final ProjectTab ptLogical = ProjectTab
318: .findDefault(ProjectTab.ID_LOGICAL);
319:
320: // SwingUtilities.invokeLater (new Runnable () {
321: // public void run () {
322: ptLogical.open();
323: if (requestFocus) {
324: ptLogical.requestActive();
325: } else {
326: ptLogical.requestVisible();
327: }
328: // }
329: // });
330:
331: }
332:
333: /** Checks if the given file name can be created in the target folder.
334: *
335: * @param targetFolder target folder (e.g. source group)
336: * @param folderName name of the folder relative to target folder (null or /-separated)
337: * @param newObjectName name of created file
338: * @param extension extension of created file
339: * @param allowFileSeparator if '/' (and possibly other file separator, see {@link FileUtil#createFolder FileUtil#createFolder})
340: * is allowed in the newObjectName
341: * @return localized error message or null if all right
342: */
343: public static String canUseFileName(FileObject targetFolder,
344: String folderName, String newObjectName, String extension,
345: boolean allowFileSeparator) {
346: assert newObjectName != null; // SimpleTargetChooserPanel.isValid returns false if it is... XXX should it use an error label instead?
347:
348: boolean allowSlash = false;
349: boolean allowBackslash = false;
350: int errorVariant = 0;
351:
352: if (allowFileSeparator) {
353: if (File.separatorChar == '\\') {
354: errorVariant = 3;
355: allowSlash = allowBackslash = true;
356: } else {
357: errorVariant = 1;
358: allowSlash = true;
359: }
360: }
361:
362: if ((!allowSlash && newObjectName.indexOf('/') != -1)
363: || (!allowBackslash && newObjectName.indexOf('\\') != -1)) {
364: //if errorVariant == 3, the test above should never be true:
365: assert errorVariant == 0 || errorVariant == 1 : "Invalid error variant: "
366: + errorVariant;
367:
368: return NbBundle.getMessage(ProjectUtilities.class,
369: "MSG_not_valid_filename", newObjectName,
370: new Integer(errorVariant));
371: }
372:
373: // test whether the selected folder on selected filesystem already exists
374: if (targetFolder == null) {
375: return NbBundle.getMessage(ProjectUtilities.class,
376: "MSG_fs_or_folder_does_not_exist"); // NOI18N
377: }
378:
379: // target filesystem should be writable
380: if (!targetFolder.canWrite()) {
381: return NbBundle.getMessage(ProjectUtilities.class,
382: "MSG_fs_is_readonly"); // NOI18N
383: }
384:
385: // file should not already exist
386: StringBuffer relFileName = new StringBuffer();
387: if (folderName != null) {
388: if (!allowBackslash && folderName.indexOf('\\') != -1) {
389: return NbBundle.getMessage(ProjectUtilities.class,
390: "MSG_not_valid_folder", folderName,
391: new Integer(1));
392: }
393: relFileName.append(folderName);
394: relFileName.append('/');
395: }
396: relFileName.append(newObjectName);
397: if (extension != null && extension.length() != 0) {
398: relFileName.append('.');
399: relFileName.append(extension);
400: }
401: if (targetFolder.getFileObject(relFileName.toString()) != null) {
402: return NbBundle.getMessage(ProjectUtilities.class,
403: "MSG_file_already_exist", newObjectName); // NOI18N
404: }
405:
406: // all ok
407: return null;
408: }
409:
410: public static class WaitCursor implements Runnable {
411:
412: private boolean show;
413:
414: private WaitCursor(boolean show) {
415: this .show = show;
416: }
417:
418: public static void show() {
419: invoke(new WaitCursor(true));
420: }
421:
422: public static void hide() {
423: invoke(new WaitCursor(false));
424: }
425:
426: private static void invoke(WaitCursor wc) {
427: if (SwingUtilities.isEventDispatchThread()) {
428: wc.run();
429: } else {
430: SwingUtilities.invokeLater(wc);
431: }
432: }
433:
434: public void run() {
435: try {
436: JFrame f = (JFrame) WindowManager.getDefault()
437: .getMainWindow();
438: Component c = f.getGlassPane();
439: c.setVisible(show);
440: c
441: .setCursor(show ? Cursor
442: .getPredefinedCursor(Cursor.WAIT_CURSOR)
443: : null);
444: } catch (NullPointerException npe) {
445: Exceptions.printStackTrace(npe);
446: }
447: }
448: }
449:
450: /** Closes all documents in editor area which are owned by one of given projects.
451: * If some documents are modified then an user is notified by Save/Discard/Cancel dialog.
452: * Dialog is showed only once for all project's documents together.
453: * URLs of closed documents are stored to <code>private.xml</code>.
454: *
455: * @param p project to close
456: * @return false if the user cancelled the Save/Discard/Cancel dialog, true otherwise
457: */
458: public static boolean closeAllDocuments(Project[] projects,
459: boolean notifyUI) {
460: if (projects == null) {
461: throw new IllegalArgumentException(
462: "No projects are specified."); // NOI18N
463: }
464:
465: if (projects.length == 0) {
466: // no projects to close, no documents will be closed
467: return true;
468: }
469:
470: Map<Project, SortedSet<String>> urls4project = OPEN_CLOSE_PROJECT_DOCUMENT_IMPL
471: .close(projects, notifyUI);
472:
473: if (urls4project != null) {
474: // store project's documents
475: // loop all project being closed
476: for (Map.Entry<Project, SortedSet<String>> entry : urls4project
477: .entrySet()) {
478: storeProjectOpenFiles(entry.getKey(), entry.getValue());
479: }
480: }
481:
482: return urls4project != null;
483: }
484:
485: static private void storeProjectOpenFiles(Project p,
486: SortedSet<String> urls) {
487: AuxiliaryConfiguration aux = p.getLookup().lookup(
488: AuxiliaryConfiguration.class);
489: if (aux != null) {
490:
491: aux.removeConfigurationFragment(OPEN_FILES_ELEMENT,
492: OPEN_FILES_NS, false);
493:
494: Document xml = XMLUtil.createDocument(OPEN_FILES_ELEMENT,
495: OPEN_FILES_NS, null, null);
496: Element fileEl;
497:
498: Element openFiles = xml.createElementNS(OPEN_FILES_NS,
499: OPEN_FILES_ELEMENT);
500:
501: // loop all open files of given project
502: for (String url : urls) {
503: fileEl = openFiles.getOwnerDocument().createElement(
504: FILE_ELEMENT);
505: fileEl.appendChild(fileEl.getOwnerDocument()
506: .createTextNode(url));
507: openFiles.appendChild(fileEl);
508: }
509:
510: aux.putConfigurationFragment(openFiles, false);
511: } else {
512: ERR.log(Level.WARNING, "No AuxiliaryConfiguration in {0}",
513: p);
514: }
515: }
516:
517: /** Opens the project's files read from the private <code>project.xml</code> file
518: *
519: * @param p project
520: */
521: public static void openProjectFiles(Project p) {
522: ERR.log(Level.FINE, "Trying to open files from {0}...", p);
523:
524: AuxiliaryConfiguration aux = p.getLookup().lookup(
525: AuxiliaryConfiguration.class);
526:
527: if (aux == null) {
528: ERR.log(Level.WARNING, "No AuxiliaryConfiguration in {0}",
529: p);
530: return;
531: }
532:
533: Element openFiles = aux.getConfigurationFragment(
534: OPEN_FILES_ELEMENT, OPEN_FILES_NS, false);
535: if (openFiles == null) {
536: return;
537: }
538:
539: NodeList list = openFiles.getElementsByTagName(FILE_ELEMENT);
540:
541: for (int i = 0; i < list.getLength(); i++) {
542: String url = list.item(i).getChildNodes().item(0)
543: .getNodeValue();
544: ERR.log(Level.FINE, "Will try to open {0}", url);
545: FileObject fo;
546: try {
547: fo = URLMapper.findFileObject(new URL(url));
548: } catch (MalformedURLException mue) {
549: assert false : "MalformedURLException in " + url;
550: continue;
551: }
552: if (fo == null) {
553: ERR.log(Level.FINE, "Could not find {0}", url);
554: continue;
555: }
556:
557: //#109676
558: if (FileOwnerQuery.getOwner(fo) != p) {
559: ERR.log(Level.FINE, "File " + url
560: + " doesn't belong to project at "
561: + p.getProjectDirectory().getPath());
562: continue;
563: }
564:
565: OPEN_CLOSE_PROJECT_DOCUMENT_IMPL.open(fo);
566: }
567:
568: // clean-up stored files
569: aux.removeConfigurationFragment(OPEN_FILES_ELEMENT,
570: OPEN_FILES_NS, false);
571: }
572:
573: // interface for handling project's documents stored in project private.xml
574: // it serves for a unit test of OpenProjectList
575: interface OpenCloseProjectDocument {
576:
577: // opens stored document in the document area
578: boolean open(FileObject fo);
579:
580: // closes documents of given projects and returns mapped document's urls by project
581: // it's used as base for storing documents in project private.xml
582: Map<Project, SortedSet<String>> close(Project[] projects,
583: boolean notifyUI);
584: }
585:
586: }
|