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.api.project;
043:
044: import java.io.IOException;
045: import java.lang.ref.Reference;
046: import java.util.Arrays;
047: import java.util.Collection;
048: import java.util.HashSet;
049: import java.util.Iterator;
050: import java.util.Map;
051: import java.util.Set;
052: import java.util.WeakHashMap;
053: import java.util.concurrent.Executor;
054: import java.util.logging.Level;
055: import java.util.logging.LogRecord;
056: import java.util.logging.Logger;
057: import org.netbeans.modules.projectapi.SimpleFileOwnerQueryImplementation;
058: import org.netbeans.modules.projectapi.TimedWeakReference;
059: import org.netbeans.spi.project.FileOwnerQueryImplementation;
060: import org.netbeans.spi.project.ProjectFactory;
061: import org.netbeans.spi.project.ProjectState;
062: import org.openide.filesystems.FileChangeAdapter;
063: import org.openide.filesystems.FileChangeListener;
064: import org.openide.filesystems.FileEvent;
065: import org.openide.filesystems.FileObject;
066: import org.openide.filesystems.FileSystem;
067: import org.openide.filesystems.FileUtil;
068: import org.openide.util.Lookup;
069: import org.openide.util.LookupEvent;
070: import org.openide.util.LookupListener;
071: import org.openide.util.Mutex;
072: import org.openide.util.MutexException;
073: import org.openide.util.Union2;
074: import org.openide.util.WeakSet;
075:
076: /**
077: * Manages loaded projects.
078: * @author Jesse Glick
079: */
080: public final class ProjectManager {
081:
082: // XXX need to figure out how to convince the system that a Project object is modified
083: // so that Save All and the exit dialog work... could temporarily use a DataLoader
084: // which recognizes project dirs and gives them a SaveCookie, perhaps
085: // see also #36280
086: // (but currently customizers always save the project on exit, so not so high priority)
087:
088: // XXX change listeners?
089:
090: private static final Logger LOG = Logger
091: .getLogger(ProjectManager.class.getName());
092: /** logger for timers/counters */
093: private static final Logger TIMERS = Logger
094: .getLogger("TIMER.projects"); // NOI18N
095:
096: private static final Lookup.Result<ProjectFactory> factories = Lookup
097: .getDefault().lookupResult(ProjectFactory.class);
098:
099: private ProjectManager() {
100: factories.addLookupListener(new LookupListener() {
101: public void resultChanged(LookupEvent e) {
102: clearNonProjectCache();
103: }
104: });
105: }
106:
107: private static final ProjectManager DEFAULT = new ProjectManager();
108:
109: /**
110: * Returns the singleton project manager instance.
111: * @return the default instance
112: */
113: public static ProjectManager getDefault() {
114: return DEFAULT;
115: }
116:
117: private static final Executor FS_EXEC = new Executor() {
118: public void execute(final Runnable command) {
119: try {
120: FileUtil.runAtomicAction(new FileSystem.AtomicAction() {
121: public void run() throws IOException {
122: command.run();
123: }
124: });
125: } catch (IOException ex) {
126: throw (IllegalStateException) new IllegalStateException()
127: .initCause(ex);
128: }
129: }
130: };
131:
132: private static final Mutex MUTEX = new Mutex(
133: new Mutex.Privileged(), FS_EXEC);
134:
135: /**
136: * Get a read/write lock to be used for all project metadata accesses.
137: * All methods relating to recognizing and loading projects, saving them,
138: * getting or setting their metadata, etc. should be controlled by this
139: * mutex and be marked as read operations or write operations. Unless
140: * otherwise stated, project-related methods automatically acquire the
141: * mutex for you, so you do not necessarily need to pay attention to it;
142: * but you may directly acquire the mutex in order to ensure that a block
143: * of reads does not have any interspersed writes, or in order to ensure
144: * that a write is not clobbering an unrelated write, etc.
145: * @return a general read/write lock for project metadata operations of all sorts
146: */
147: public static Mutex mutex() {
148: return MUTEX;
149: }
150:
151: private static enum LoadStatus {
152: /**
153: * Marker for a directory which is known to not be a project.
154: */
155: NO_SUCH_PROJECT,
156: /**
157: * Marker for a directory which is known to (probably) be a project but is not loaded.
158: */
159: SOME_SUCH_PROJECT,
160: /**
161: * Marker for a directory which may currently be being loaded as a project.
162: * When this is the value, other reader threads should wait for the result.
163: */
164: LOADING_PROJECT;
165:
166: public boolean is(Union2<Reference<Project>, LoadStatus> o) {
167: return o != null && o.hasSecond() && o.second() == this ;
168: }
169:
170: public Union2<Reference<Project>, LoadStatus> wrap() {
171: return Union2.createSecond(this );
172: }
173: }
174:
175: /**
176: * Cache of loaded projects (modified or not).
177: * Also caches a dir which is <em>not</em> a project.
178: */
179: private final Map<FileObject, Union2<Reference<Project>, LoadStatus>> dir2Proj = new WeakHashMap<FileObject, Union2<Reference<Project>, LoadStatus>>();
180:
181: /**
182: * Set of modified projects (subset of loaded projects).
183: */
184: private final Set<Project> modifiedProjects = new HashSet<Project>();
185:
186: private final Set<Project> removedProjects = new WeakSet<Project>();
187:
188: /**
189: * Mapping from projects to the factories that created them.
190: */
191: private final Map<Project, ProjectFactory> proj2Factory = new WeakHashMap<Project, ProjectFactory>();
192:
193: /**
194: * Checks for deleted projects.
195: */
196: private final FileChangeListener projectDeletionListener = new ProjectDeletionListener();
197:
198: /**
199: * Whether this thread is currently loading a project.
200: */
201: private ThreadLocal<Set<FileObject>> loadingThread = new ThreadLocal<Set<FileObject>>();
202:
203: /**
204: * Clear internal state.
205: * Useful from unit tests.
206: */
207: void reset() {
208: dir2Proj.clear();
209: modifiedProjects.clear();
210: proj2Factory.clear();
211: }
212:
213: /**
214: * Find an open project corresponding to a given project directory.
215: * Will be created in memory if necessary.
216: * <p>
217: * Acquires read access.
218: * </p>
219: * <p>
220: * It is <em>not</em> guaranteed that the returned instance will be identical
221: * to that which is created by the appropriate {@link ProjectFactory}. In
222: * particular, the project manager is free to return only wrapper <code>Project</code>
223: * instances which delegate to the factory's implementation. If you know your
224: * factory created a particular project, you cannot safely cast the return value
225: * of this method to your project type implementation class; you should instead
226: * place an implementation of some suitable private interface into your project's
227: * lookup, which would be safely proxied.
228: * </p>
229: * @param projectDirectory the project top directory
230: * @return the project (object identity may or may not vary between calls)
231: * or null if the directory is not recognized as a project by any
232: * registered {@link ProjectFactory}
233: * (might be null even if {@link #isProject} returns true)
234: * @throws IOException if the project was recognized but could not be loaded
235: * @throws IllegalArgumentException if the supplied file object is null or not a folder
236: */
237: public Project findProject(final FileObject projectDirectory)
238: throws IOException, IllegalArgumentException {
239: if (projectDirectory == null) {
240: throw new IllegalArgumentException(
241: "Attempted to pass a null directory to findProject"); // NOI18N
242: }
243: if (!projectDirectory.isFolder()) {
244: throw new IllegalArgumentException(
245: "Attempted to pass a non-directory to findProject: "
246: + projectDirectory); // NOI18N
247: }
248: try {
249: return mutex().readAccess(
250: new Mutex.ExceptionAction<Project>() {
251: public Project run() throws IOException {
252: // Read access, but still needs to synch on the cache since there
253: // may be >1 reader.
254: try {
255: boolean wasSomeSuchProject;
256: synchronized (dir2Proj) {
257: Union2<Reference<Project>, LoadStatus> o;
258: do {
259: o = dir2Proj
260: .get(projectDirectory);
261: if (LoadStatus.LOADING_PROJECT
262: .is(o)) {
263: try {
264: Set<FileObject> ldng = loadingThread
265: .get();
266: if (ldng != null
267: && ldng
268: .contains(projectDirectory)) {
269: throw new IllegalStateException(
270: "Attempt to call ProjectManager.findProject within the body of ProjectFactory.loadProject (hint: try using ProjectManager.mutex().postWriteRequest(...) within the body of your Project's constructor to prevent this)"); // NOI18N
271: }
272: LOG
273: .log(
274: Level.FINE,
275: "findProject({0}) in {1}: waiting for LOADING_PROJECT...",
276: new Object[] {
277: projectDirectory,
278: Thread
279: .currentThread()
280: .getName() });
281: dir2Proj.wait();
282: LOG
283: .log(
284: Level.FINE,
285: "findProject({0}) in {1}: ...done waiting for LOADING_PROJECT",
286: new Object[] {
287: projectDirectory,
288: Thread
289: .currentThread()
290: .getName() });
291: } catch (InterruptedException e) {
292: LOG.log(Level.WARNING,
293: null, e);
294: }
295: }
296: } while (LoadStatus.LOADING_PROJECT
297: .is(o));
298: assert !LoadStatus.LOADING_PROJECT
299: .is(o);
300: wasSomeSuchProject = LoadStatus.SOME_SUCH_PROJECT
301: .is(o);
302: if (LoadStatus.NO_SUCH_PROJECT
303: .is(o)) {
304: LOG
305: .log(
306: Level.FINE,
307: "findProject({0}) in {1}: NO_SUCH_PROJECT",
308: new Object[] {
309: projectDirectory,
310: Thread
311: .currentThread()
312: .getName() });
313: return null;
314: } else if (o != null
315: && !LoadStatus.SOME_SUCH_PROJECT
316: .is(o)) {
317: Project p = o.first().get();
318: if (p != null) {
319: LOG
320: .log(
321: Level.FINE,
322: "findProject({0}) in {1}: cached project",
323: new Object[] {
324: projectDirectory,
325: Thread
326: .currentThread()
327: .getName() });
328: return p;
329: }
330: }
331: // not in cache
332: dir2Proj.put(projectDirectory,
333: LoadStatus.LOADING_PROJECT
334: .wrap());
335: Set<FileObject> ldng = loadingThread
336: .get();
337: if (ldng == null) {
338: ldng = new HashSet<FileObject>();
339: loadingThread.set(ldng);
340: }
341: ldng.add(projectDirectory);
342: LOG
343: .log(
344: Level.FINE,
345: "findProject({0}) in {1}: will load new project...",
346: new Object[] {
347: projectDirectory,
348: Thread
349: .currentThread()
350: .getName() });
351: }
352: boolean resetLP = false;
353: try {
354: Project p = createProject(projectDirectory);
355: LOG
356: .log(
357: Level.FINE,
358: "findProject({0}) in {1}: created new project",
359: new Object[] {
360: projectDirectory,
361: Thread
362: .currentThread()
363: .getName() });
364: //Thread.dumpStack();
365: synchronized (dir2Proj) {
366: dir2Proj.notifyAll();
367: projectDirectory
368: .addFileChangeListener(projectDeletionListener);
369: if (p != null) {
370: dir2Proj
371: .put(
372: projectDirectory,
373: Union2
374: .<Reference<Project>, LoadStatus> createFirst(new TimedWeakReference<Project>(
375: p)));
376: resetLP = true;
377: return p;
378: } else {
379: dir2Proj
380: .put(
381: projectDirectory,
382: LoadStatus.NO_SUCH_PROJECT
383: .wrap());
384: resetLP = true;
385: if (wasSomeSuchProject) {
386: LOG
387: .log(
388: Level.FINE,
389: "Directory {0} was initially claimed to be a project folder but really was not",
390: FileUtil
391: .getFileDisplayName(projectDirectory));
392: }
393: return null;
394: }
395: }
396: } catch (IOException e) {
397: LOG
398: .log(
399: Level.FINE,
400: "findProject({0}) in {1}: error loading project: {2}",
401: new Object[] {
402: projectDirectory,
403: Thread
404: .currentThread()
405: .getName(),
406: e });
407: // Do not cache the exception. Might be useful in some cases
408: // but would also cause problems if there were a project that was
409: // temporarily corrupted, fP is called, then it is fixed, then fP is
410: // called again (without anything being GC'd)
411: throw e;
412: } finally {
413: loadingThread.get().remove(
414: projectDirectory);
415: if (!resetLP) {
416: // IOException or a runtime exception interrupted.
417: LOG
418: .log(
419: Level.FINE,
420: "findProject({0}) in {1}: cleaning up after error",
421: new Object[] {
422: projectDirectory,
423: Thread
424: .currentThread()
425: .getName() });
426: synchronized (dir2Proj) {
427: assert LoadStatus.LOADING_PROJECT
428: .is(dir2Proj
429: .get(projectDirectory));
430: dir2Proj
431: .remove(projectDirectory);
432: dir2Proj.notifyAll(); // make sure other threads can continue
433: }
434: }
435: }
436: // Workaround for issue #51911:
437: // Log project creation exception here otherwise it can get lost
438: // in following scenario:
439: // If project creation calls ProjectManager.postWriteRequest() (what for
440: // example FreeformSources.initSources does) and then it throws an
441: // exception then this exception can get lost because leaving read mutex
442: // will immediately execute the runnable posted by
443: // ProjectManager.postWriteRequest() and if this runnable fails (what
444: // for FreeformSources.initSources will happen because
445: // AntBasedProjectFactorySingleton.getProjectFor() will not find project in
446: // its helperRef cache) then only this second fail is logged, but the cause -
447: // the failure to create project - is never logged. So, better log it here:
448: } catch (Error e) {
449: LOG.log(Level.FINE, null, e);
450: throw e;
451: } catch (RuntimeException e) {
452: LOG.log(Level.FINE, null, e);
453: throw e;
454: } catch (IOException e) {
455: LOG.log(Level.FINE, null, e);
456: throw e;
457: }
458: }
459: });
460: } catch (MutexException e) {
461: throw (IOException) e.getException();
462: }
463: }
464:
465: /**
466: * Create a project from a given directory.
467: * @param dir the project dir
468: * @return a project made from it, or null if it is not recognized
469: * @throws IOException if there was a problem loading the project
470: */
471: private Project createProject(FileObject dir) throws IOException {
472: assert dir != null;
473: assert dir.isFolder();
474: assert mutex().isReadAccess();
475: ProjectStateImpl state = new ProjectStateImpl();
476: for (ProjectFactory factory : factories.allInstances()) {
477: Project p = factory.loadProject(dir, state);
478: if (p != null) {
479: if (TIMERS.isLoggable(Level.FINE)) {
480: LogRecord rec = new LogRecord(Level.FINE, "Project"); // NOI18N
481: rec.setParameters(new Object[] { p });
482: TIMERS.log(rec);
483: }
484: proj2Factory.put(p, factory);
485: state.attach(p);
486: return p;
487: }
488: }
489: return null;
490: }
491:
492: /**
493: * Check whether a given directory is likely to contain a project without
494: * actually loading it.
495: * Should be faster and use less memory than {@link #findProject} when called
496: * on a large number of directories.
497: * <p>The result is not guaranteed to be accurate; there may be false positives
498: * (directories for which <code>isProject</code> is true but {@link #findProject}
499: * will return false), for example if there is trouble loading the project.
500: * False negatives are possible only if there are bugs in the project factory.</p>
501: * <p>Acquires read access.</p>
502: * <p class="nonnormative">
503: * You do <em>not</em> need to call this method if you just plan to call {@link #findProject}
504: * afterwards. It is intended for only those clients which would discard the
505: * result of {@link #findProject} other than to check for null, and which
506: * can also tolerate false positives.
507: * </p>
508: * @param projectDirectory a directory which may be some project's top directory
509: * @return true if the directory is likely to contain a project according to
510: * some registered {@link ProjectFactory}
511: * @throws IllegalArgumentException if the supplied file object is null or not a folder
512: */
513: public boolean isProject(final FileObject projectDirectory)
514: throws IllegalArgumentException {
515: if (projectDirectory == null) {
516: throw new IllegalArgumentException(
517: "Attempted to pass a null directory to isProject"); // NOI18N
518: }
519: if (!projectDirectory.isFolder()) {
520: //#78215 it can happen that a no longer existing folder is queried. throw
521: // exception only for real wrong usage..
522: if (projectDirectory.isValid()) {
523: throw new IllegalArgumentException(
524: "Attempted to pass a non-directory to isProject: "
525: + projectDirectory); // NOI18N
526: } else {
527: return false;
528: }
529: }
530: return mutex().readAccess(new Mutex.Action<Boolean>() {
531: public Boolean run() {
532: synchronized (dir2Proj) {
533: Union2<Reference<Project>, LoadStatus> o;
534: do {
535: o = dir2Proj.get(projectDirectory);
536: if (LoadStatus.LOADING_PROJECT.is(o)) {
537: try {
538: dir2Proj.wait();
539: } catch (InterruptedException e) {
540: e.printStackTrace();
541: }
542: }
543: } while (LoadStatus.LOADING_PROJECT.is(o));
544: assert !LoadStatus.LOADING_PROJECT.is(o);
545: if (LoadStatus.NO_SUCH_PROJECT.is(o)) {
546: return false;
547: } else if (o != null) {
548: // Reference<Project> or SOME_SUCH_PROJECT
549: return true;
550: }
551: // Not in cache.
552: dir2Proj.put(projectDirectory,
553: LoadStatus.LOADING_PROJECT.wrap());
554: }
555: boolean resetLP = false;
556: try {
557: boolean p = checkForProject(projectDirectory);
558: synchronized (dir2Proj) {
559: resetLP = true;
560: dir2Proj.notifyAll();
561: if (p) {
562: dir2Proj
563: .put(
564: projectDirectory,
565: LoadStatus.SOME_SUCH_PROJECT
566: .wrap());
567: return true;
568: } else {
569: dir2Proj.put(projectDirectory,
570: LoadStatus.NO_SUCH_PROJECT.wrap());
571: return false;
572: }
573: }
574: } finally {
575: if (!resetLP) {
576: // some runtime exception interrupted.
577: assert LoadStatus.LOADING_PROJECT.is(dir2Proj
578: .get(projectDirectory));
579: dir2Proj.remove(projectDirectory);
580: }
581: }
582: }
583: });
584: }
585:
586: private boolean checkForProject(FileObject dir) {
587: assert dir != null;
588: assert dir.isFolder() : dir;
589: assert mutex().isReadAccess();
590: Iterator it = factories.allInstances().iterator();
591: while (it.hasNext()) {
592: ProjectFactory factory = (ProjectFactory) it.next();
593: if (factory.isProject(dir)) {
594: return true;
595: }
596: }
597: return false;
598: }
599:
600: /**
601: * Clear the cached list of folders thought <em>not</em> to be projects.
602: * This may be useful after creating project metadata in a folder, etc.
603: * Cached project objects, i.e. folders that <em>are</em> known to be
604: * projects, are not affected.
605: */
606: public void clearNonProjectCache() {
607: synchronized (dir2Proj) {
608: dir2Proj.values().removeAll(
609: Arrays.asList(new Object[] {
610: LoadStatus.NO_SUCH_PROJECT.wrap(),
611: LoadStatus.SOME_SUCH_PROJECT.wrap(), }));
612: // XXX remove everything too? but then e.g. AntProjectFactorySingleton
613: // will stay while its delegates are changed, which does no good
614: // XXX should there be any way to signal that a particular
615: // folder should be "reloaded" by a new factory?
616: }
617: }
618:
619: private final class ProjectStateImpl implements ProjectState {
620:
621: private Project p;
622:
623: void attach(Project p) {
624: assert p != null;
625: assert this .p == null;
626: this .p = p;
627: }
628:
629: public void markModified() {
630: assert p != null;
631: LOG.log(Level.FINE, "markModified({0})", p
632: .getProjectDirectory());
633: mutex().writeAccess(new Mutex.Action<Void>() {
634: public Void run() {
635: if (!proj2Factory.containsKey(p)) {
636: throw new IllegalStateException(
637: "An attempt to call ProjectState.markModified on a deleted project: "
638: + p.getProjectDirectory()); // NOI18N
639: }
640: modifiedProjects.add(p);
641: return null;
642: }
643: });
644: }
645:
646: public void notifyDeleted() throws IllegalStateException {
647: assert p != null;
648: mutex().writeAccess(new Mutex.Action<Void>() {
649: public Void run() {
650: if (proj2Factory.get(p) == null) {
651: throw new IllegalStateException(
652: "An attempt to call notifyDeleted more than once. Project: "
653: + p.getProjectDirectory()); // NOI18N
654: }
655:
656: dir2Proj.remove(p.getProjectDirectory());
657: proj2Factory.remove(p);
658: modifiedProjects.remove(p);
659: removedProjects.add(p);
660: //#111892
661: Collection<? extends FileOwnerQueryImplementation> col = Lookup
662: .getDefault().lookupAll(
663: FileOwnerQueryImplementation.class);
664: for (FileOwnerQueryImplementation impl : col) {
665: if (impl instanceof SimpleFileOwnerQueryImplementation) {
666: ((SimpleFileOwnerQueryImplementation) impl)
667: .resetLastFoundReferences();
668: }
669: }
670: return null;
671: }
672: });
673: }
674:
675: }
676:
677: /**
678: * Get a list of all projects which are modified and need to be saved.
679: * <p>Acquires read access.
680: * @return an immutable set of projects
681: */
682: public Set<Project> getModifiedProjects() {
683: return mutex().readAccess(new Mutex.Action<Set<Project>>() {
684: public Set<Project> run() {
685: return new HashSet<Project>(modifiedProjects);
686: }
687: });
688: }
689:
690: /**
691: * Check whether a given project is current modified.
692: * <p>Acquires read access.
693: * @param p a project loaded by this manager
694: * @return true if it is modified, false if has been saved since the last modification
695: * @throws IllegalArgumentException if the project was not created through this manager
696: */
697: public boolean isModified(final Project p)
698: throws IllegalArgumentException {
699: return mutex().readAccess(new Mutex.Action<Boolean>() {
700: public Boolean run() {
701: synchronized (dir2Proj) {
702: if (!proj2Factory.containsKey(p)) {
703: throw new IllegalArgumentException("Project "
704: + p + " not created by "
705: + ProjectManager.this
706: + " or was already deleted"); // NOI18N
707: }
708: }
709: return modifiedProjects.contains(p);
710: }
711: });
712: }
713:
714: /**
715: * Save one project (if it was in fact modified).
716: * <p>Acquires write access.</p>
717: * <p class="nonnormative">
718: * Although the project infrastructure permits a modified project to be saved
719: * at any time, current UI principles dictate that the "save project" concept
720: * should be internal only - i.e. a project customizer should automatically
721: * save the project when it is closed e.g. with an "OK" button. Currently there
722: * is no UI display of modified projects; this module does not ensure that modified projects
723: * are saved at system exit time the way modified files are, though the Project UI
724: * implementation module currently does this check.
725: * </p>
726: * @param p the project to save
727: * @throws IOException if it cannot be saved
728: * @throws IllegalArgumentException if the project was not created through this manager
729: */
730: public void saveProject(final Project p) throws IOException,
731: IllegalArgumentException {
732: try {
733: mutex().writeAccess(new Mutex.ExceptionAction<Void>() {
734: public Void run() throws IOException {
735: //removed projects are the ones that cannot be mapped to an existing project type anymore.
736: if (removedProjects.contains(p)) {
737: return null;
738: }
739: if (!proj2Factory.containsKey(p)) {
740: throw new IllegalArgumentException("Project "
741: + p + " not created by "
742: + ProjectManager.this
743: + " or was already deleted"); // NOI18N
744: }
745: if (modifiedProjects.contains(p)) {
746: ProjectFactory f = proj2Factory.get(p);
747: f.saveProject(p);
748: LOG.log(Level.FINE, "saveProject({0})", p
749: .getProjectDirectory());
750: modifiedProjects.remove(p);
751: }
752: return null;
753: }
754: });
755: } catch (MutexException e) {
756: //##91398 have a more descriptive error message, in case of RO folders.
757: // the correct reporting still up to the specific project type.
758: if (!p.getProjectDirectory().canWrite()) {
759: throw new IOException(
760: "Project folder is not writeable.");
761: }
762: throw (IOException) e.getException();
763: }
764: }
765:
766: /**
767: * Save all modified projects.
768: * <p>Acquires write access.
769: * @throws IOException if any of them cannot be saved
770: */
771: public void saveAllProjects() throws IOException {
772: try {
773: mutex().writeAccess(new Mutex.ExceptionAction<Void>() {
774: public Void run() throws IOException {
775: Iterator<Project> it = modifiedProjects.iterator();
776: while (it.hasNext()) {
777: Project p = it.next();
778: ProjectFactory f = proj2Factory.get(p);
779: assert f != null : p;
780: f.saveProject(p);
781: LOG.log(Level.FINE, "saveProject({0})", p
782: .getProjectDirectory());
783: it.remove();
784: }
785: return null;
786: }
787: });
788: } catch (MutexException e) {
789: throw (IOException) e.getException();
790: }
791: }
792:
793: /**
794: * Checks whether a project is still valid.
795: * <p>Acquires read access.</p>
796: *
797: * @since 1.6
798: *
799: * @param p a project loaded by this manager
800: * @return true if the project is still valid, false if it has been deleted
801: */
802: public boolean isValid(final Project p) {
803: return mutex().readAccess(new Mutex.Action<Boolean>() {
804: public Boolean run() {
805: synchronized (dir2Proj) {
806: return proj2Factory.containsKey(p);
807: }
808: }
809: });
810: }
811:
812: /**
813: * Removes cache entries for deleted projects.
814: */
815: private final class ProjectDeletionListener extends
816: FileChangeAdapter {
817:
818: public ProjectDeletionListener() {
819: }
820:
821: @Override
822: public void fileDeleted(FileEvent fe) {
823: synchronized (dir2Proj) {
824: dir2Proj.remove(fe.getFile());
825: }
826: }
827:
828: }
829:
830: }
|