From 24a62f9091aa8b5ac0387d4dad0b1363e4999ce8 Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Wed, 7 Oct 2026 15:43:14 +0200 Subject: [PATCH 01/15] Separating OpenProjectLoading logic into its own class --- .../netbeans/modules/project/ui/Hacks.java | 1 - .../modules/project/ui/LazyProject.java | 5 +- .../modules/project/ui/OpenProjectList.java | 946 +++++------------- .../project/ui/OpenProjectsLoading.java | 406 ++++++++ .../project/ui/OpenProjectsLogging.java | 118 +++ .../modules/project/ui/ProjectsRootNode.java | 48 +- 6 files changed, 823 insertions(+), 701 deletions(-) create mode 100644 ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectsLoading.java create mode 100644 ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectsLogging.java diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/Hacks.java b/ide/projectui/src/org/netbeans/modules/project/ui/Hacks.java index 11eb5d275d0a..c8acbb31c413 100644 --- a/ide/projectui/src/org/netbeans/modules/project/ui/Hacks.java +++ b/ide/projectui/src/org/netbeans/modules/project/ui/Hacks.java @@ -33,7 +33,6 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Level; import java.util.logging.Logger; -import org.netbeans.api.project.FileOwnerQuery; import org.netbeans.api.project.Project; import org.netbeans.api.project.ProjectInformation; import org.netbeans.api.project.ProjectUtils; diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/LazyProject.java b/ide/projectui/src/org/netbeans/modules/project/ui/LazyProject.java index fe016212db40..ed3cfac70012 100644 --- a/ide/projectui/src/org/netbeans/modules/project/ui/LazyProject.java +++ b/ide/projectui/src/org/netbeans/modules/project/ui/LazyProject.java @@ -23,6 +23,7 @@ import java.net.URL; import java.util.Collection; import java.util.Collections; +import java.util.logging.Level; import javax.swing.Action; import javax.swing.Icon; import org.netbeans.api.project.Project; @@ -55,7 +56,7 @@ final class LazyProject implements ExtIcon icon; private FileObject fo; - public LazyProject(URL url, String displayName, ExtIcon icon) { + LazyProject(URL url, String displayName, ExtIcon icon) { super(); this.url = url; this.displayName = displayName; @@ -69,7 +70,7 @@ public FileObject getProjectDirectory() { } fo = URLMapper.findFileObject(url); if (fo == null) { - OpenProjectList.LOGGER.warning("Project dir with " + url + " not found!"); + OpenProjectsLogging.LOGGER.log(Level.WARNING, "Project dir with {0} not found!", url); fo = FileUtil.createMemoryFileSystem().getRoot(); } return fo; diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java b/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java index 982cb07f7156..01113d38b757 100644 --- a/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java +++ b/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java @@ -35,7 +35,6 @@ import java.util.Comparator; import java.util.ConcurrentModificationException; import java.util.HashMap; -import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.LinkedList; @@ -44,24 +43,14 @@ import java.util.Set; import java.util.StringTokenizer; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.locks.Condition; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.LogRecord; -import java.util.logging.Logger; -import java.util.prefs.Preferences; import javax.swing.Icon; import org.netbeans.api.annotations.common.NonNull; import org.netbeans.api.annotations.common.NullAllowed; import org.netbeans.api.progress.ProgressHandle; -import org.netbeans.api.project.FileOwnerQuery; import org.netbeans.api.project.Project; import org.netbeans.api.project.ProjectInformation; import org.netbeans.api.project.ProjectManager; @@ -95,16 +84,11 @@ import org.openide.util.Exceptions; import org.openide.util.ImageUtilities; import org.openide.util.Lookup; -import org.openide.util.LookupEvent; -import org.openide.util.LookupListener; import org.openide.util.Mutex; import org.openide.util.Mutex.Action; -import org.openide.util.NbBundle; import org.openide.util.NbBundle.Messages; import org.openide.util.Parameters; import org.openide.util.RequestProcessor; -import org.openide.util.Utilities; -import org.openide.util.WeakListeners; import org.openide.util.lookup.Lookups; import org.openide.util.lookup.ProxyLookup; import org.openide.windows.WindowManager; @@ -116,10 +100,10 @@ * @author Petr Hrebejk */ public final class OpenProjectList { - /** - * a mutex protecting just the private parts of this class, + /** + * a mutex protecting just the private parts of this class, * WARNING the mutex read or write access section SHOULD NEVER include anything that eventually aquires ProjectManager.MUTEX - * otherwise we get a deadlock fairly fast + * otherwise we get a deadlock fairly fast */ static final Mutex MUTEX = new Mutex(); @@ -133,54 +117,42 @@ public static Comparator projectByPath() { public static final String PROPERTY_MAIN_PROJECT = "MainProject"; public static final String PROPERTY_RECENT_PROJECTS = "RecentProjects"; public static final String PROPERTY_REPLACE = "ReplaceProject"; - + private static OpenProjectList INSTANCE; - + // number of templates in LRU list private static final int NUM_TEMPLATES = 15; - + public static final RequestProcessor OPENING_RP = new RequestProcessor("Opening projects", 1); private static final RequestProcessor FILE_DELETED_RP = new RequestProcessor(OpenProjectList.class); private static final RequestProcessor RP3 = new RequestProcessor(OpenProjectList.class); - static final Logger LOGGER = Logger.getLogger(OpenProjectList.class.getName()); - static void log(LogRecord r) { - LOGGER.log(r); - } - static void log(Level l, String msg, Object... params) { - LOGGER.log(l, msg, params); - } - static void log(Level l, String msg, Throwable e) { - LOGGER.log(l, msg, e); - } - - /** List which holds the open projects */ private List openProjects; private final HashMap> openProjectsModuleInfos; - + /** Main project */ private Project mainProject; - + /** List of recently closed projects */ private final RecentProjectList recentProjects; /** LRU List of recently used templates */ private final List recentTemplates; - + /** Property change listeners */ private final PropertyChangeSupport pchSupport; - + private final ProjectDeletionListener deleteListener = new ProjectDeletionListener(); private final NbProjectDeletionListener nbprojectDeleteListener = new NbProjectDeletionListener(); - + private final PropertyChangeListener infoListener; - private final LoadOpenProjects LOAD; + private final OpenProjectsLoading LOAD; private final ArrayList projectGroupSupport; private final AtomicBoolean groupChanging = new AtomicBoolean(false); - + OpenProjectList() { - LOAD = new LoadOpenProjects(0); + LOAD = new OpenProjectsLoading(0, new LoadingCallback()); openProjects = new ArrayList(); openProjectsModuleInfos = new HashMap>(); infoListener = new PropertyChangeListener() { @@ -196,10 +168,10 @@ public void propertyChange(PropertyChangeEvent evn) { recentTemplates = new ArrayList(); projectGroupSupport = new ArrayList(); } - - + + // Implementation of the class --------------------------------------------- - + public static OpenProjectList getDefault() { return MUTEX.readAccess(new Mutex.Action() { public @Override OpenProjectList run() { @@ -216,7 +188,7 @@ public static OpenProjectList getDefault() { } }); } - + public static void waitProjectsFullyOpen() { getDefault().LOAD.waitFinished(0); } @@ -246,7 +218,7 @@ final Project unwrapProject(Project wrap) { return wrap; } - /** Modifications to the recentTemplates variables shall be done only + /** Modifications to the recentTemplates variables shall be done only * when holding a lock. * @return the list */ @@ -254,20 +226,20 @@ private List getRecentTemplates() { assert MUTEX.isReadAccess() || MUTEX.isWriteAccess(); return recentTemplates; } - + void addProjectGroupChangeListener(ProjectGroupChangeListener listener) { synchronized (projectGroupSupport) { projectGroupSupport.add(listener); } } - + void removeProjectGroupChangeListener(ProjectGroupChangeListener listener) { synchronized (projectGroupSupport) { projectGroupSupport.remove(listener); } } - + public void fireProjectGroupChanging(Group oldGroup, Group newGroup) { groupChanging(); List list = new ArrayList(); @@ -281,7 +253,7 @@ public void fireProjectGroupChanging(Group oldGroup, Group newGroup) { l.projectGroupChanging(event); } } - + public void fireProjectGroupChanged(Group oldGroup, Group newGroup) { groupChanged(); List list = new ArrayList(); @@ -319,335 +291,8 @@ private void groupChanged() { private void groupChanging() { groupChanging.compareAndSet(false, true); } - - private final class LoadOpenProjects implements Runnable, LookupListener, Future { - final RequestProcessor RP = new RequestProcessor("Load Open Projects"); // NOI18N - final RequestProcessor.Task TASK = RP.create(this); - private int action; - private final LinkedList toOpenProjects = new LinkedList(); - private List lazilyOpenedProjects; - private List recentTemplates; - private Project lazyMainProject; - private Lookup.Result currentFiles; - private int entered; - private final Lock enteredGuard = new ReentrantLock(); - private final Condition enteredZeroed = enteredGuard.newCondition(); - private final ProgressHandle progress; - - @Messages("CAP_Opening_Projects=Opening Projects") - @SuppressWarnings("LeakingThisInConstructor") - public LoadOpenProjects(int a) { - action = a; - currentFiles = Utilities.actionsGlobalContext().lookupResult(FileObject.class); - currentFiles.addLookupListener(WeakListeners.create(LookupListener.class, this, currentFiles)); - progress = ProgressHandle.createHandle(CAP_Opening_Projects()); - } - - final boolean waitFinished(long timeout) { - log(Level.FINER, "waitFinished, action {0}", action); // NOI18N - if (action == 0) { - run(); - } - log(Level.FINER, "waitFinished, before wait"); // NOI18N - if (timeout == 0) { - TASK.waitFinished(); - } else { - try { - if (!TASK.waitFinished(timeout)) { - return false; - } - } catch (InterruptedException ex) { - return false; - } - } - log(Level.FINER, "waitFinished, after wait"); // NOI18N - return true; - } - - @Override - public void run() { - log(Level.FINE, "LoadOpenProjects.run: {0}", action); // NOI18N - switch (action) { - case 0: - action = 1; - TASK.schedule(0); - resultChanged(null); - return; - case 1: - if (!RP.isRequestProcessorThread()) { - return; - } - action = 2; - try { - progress.start(); - loadInBackground(); - } finally { - progress.finish(); - } - updateGlobalState(); - ProjectsRootNode.checkNoLazyNode(); - Group.projectsLoaded(); - return; - case 2: - // finished, oK - return; - default: - throw new IllegalStateException("unknown action: " + action); - } - } - - final void preferredProject(final Set lazyPDirs) { - OpenProjectList.MUTEX.writeAccess(new Mutex.Action() { - public @Override Void run() { - for (Project p : new ArrayList(toOpenProjects)) { - FileObject dir = p.getProjectDirectory(); - assert dir != null : "Project has real directory " + p; - if (lazyPDirs.contains(dir)) { - toOpenProjects.remove(p); - toOpenProjects.addFirst(p); - return null; - } - } - return null; - } - }); - } - - private void updateGlobalState() { - log(Level.FINER, "updateGlobalState"); // NOI18N - OpenProjectList.MUTEX.writeAccess(new Mutex.Action() { - public @Override Void run() { - INSTANCE.openProjects = lazilyOpenedProjects; - log(Level.FINER, "openProjects changed: {0}", lazilyOpenedProjects); // NOI18N - if (lazyMainProject != null) { - INSTANCE.mainProject = lazyMainProject; - } - INSTANCE.mainProject = unwrapProject(INSTANCE.mainProject); - INSTANCE.getRecentTemplates().addAll(recentTemplates); - log(Level.FINER, "updateGlobalState, applied"); // NOI18N - return null; - } - }); - - Project[] opened = lazilyOpenedProjects.toArray(Project[]::new); - INSTANCE.pchSupport.firePropertyChange(PROPERTY_OPEN_PROJECTS, new Project[0], opened); - Project main = INSTANCE.mainProject; - if (main != null) { // else PROPERTY_MAIN_PROJECT would be fired spuriously - INSTANCE.pchSupport.firePropertyChange(PROPERTY_MAIN_PROJECT, null, main); - } - - if (checkFirstRun() && opened.length > 0) { - OPENING_RP.execute(() -> { - for (Project p : opened) { - Project del = p.getLookup().lookup(Project.class); - ProjectUtilities.openProjectFiles(del == null ? p : del); - } - }); - } - - log(Level.FINER, "updateGlobalState, done, notified"); // NOI18N - } - - private boolean checkFirstRun() { - Preferences prefs = OpenProjectListSettings.getInstance().getPreferences(); - String prefKey = "projectListVersion"; // NOI18N - String build = System.getProperty("netbeans.buildnumber", "0"); // NOI18N - if (!prefs.get(prefKey, "").equals(build)) { - prefs.put(prefKey, build); - return true; - } else { - return false; - } - } - - boolean closeBeforeOpen(final Project[] arr) { - return OpenProjectList.MUTEX.writeAccess(new Mutex.Action() { - public @Override Boolean run() { - NEXT: for (Project p : arr) { - FileObject dir = p.getProjectDirectory(); - for (Iterator it = toOpenProjects.iterator(); it.hasNext();) { - if (dir.equals(it.next().getProjectDirectory())) { - it.remove(); - continue NEXT; - } - } - return false; - } - return true; - } - }); - } - @NbBundle.Messages({ - "#NOI18N", - "LOAD_PROJECTS_ON_START=true" - }) - private void loadInBackground() { - lazilyOpenedProjects = new ArrayList<>(); - final boolean loadProjectsOnStart = "true".equals(Bundle.LOAD_PROJECTS_ON_START()); - List urls = loadProjectsOnStart ? - OpenProjectListSettings.getInstance().getOpenProjectsURLs() : - Collections.emptyList(); - final List initial = new ArrayList<>(); - final Collection projects = URLs2Projects(urls); - OpenProjectList.MUTEX.writeAccess(new Mutex.Action() { - public @Override Void run() { - toOpenProjects.addAll(projects); - log(Level.FINER, "loadOnBackground {0}", toOpenProjects); // NOI18N - initial.addAll(toOpenProjects); - return null; - } - }); - recentTemplates = new ArrayList( OpenProjectListSettings.getInstance().getRecentTemplates() ); - final URL mainProjectURL = OpenProjectListSettings.getInstance().getMainProjectURL(); - int max = OpenProjectList.MUTEX.writeAccess(new Mutex.Action() { - public @Override Integer run() { - for (Project p : toOpenProjects) { - INSTANCE.addModuleInfo(p); - // Set main project - if ( mainProjectURL != null && - mainProjectURL.equals( p.getProjectDirectory().toURL() ) ) { - lazyMainProject = p; - } - } - return toOpenProjects.size(); - } - }); - progress.switchToDeterminate(max); - for (;;) { - final AtomicInteger openPrjSize = new AtomicInteger(); - Project p = OpenProjectList.MUTEX.writeAccess(new Mutex.Action() { - public @Override Project run() { - if (toOpenProjects.isEmpty()) { - return null; - } - Project p = toOpenProjects.remove(); - log(Level.FINER, "after remove {0}", toOpenProjects); // NOI18N - openPrjSize.set(toOpenProjects.size()); - return p; - } - }); - if (p == null) { - break; - } - log(Level.FINE, "about to open a project {0}", p); // NOI18N - if (notifyOpened(p)) { - lazilyOpenedProjects.add(p); - log(Level.FINE, "notify opened {0}", p); // NOI18N - PropertyChangeEvent ev = new PropertyChangeEvent(this, PROPERTY_REPLACE, null, p); - try { - pchSupport.firePropertyChange(ev); - } catch (Throwable t) { - log(Level.WARNING, "broken node for {0}", t); - } - log(Level.FINE, "property change notified {0}", p); // NOI18N - //same as in doOpenProject() but here for initially opened projects - p.getProjectDirectory().addFileChangeListener(INSTANCE.deleteListener); - p.getProjectDirectory().addFileChangeListener(INSTANCE.nbprojectDeleteListener); - } else { - // opened failed, remove main project if same. - if (lazyMainProject == p) { - lazyMainProject = null; - } - } - progress.progress(max - openPrjSize.get()); - } - - if (initial != null) { - Project[] initialA = initial.toArray(new Project[0]); - log(createRecord("UI_INIT_PROJECTS", initialA),"org.netbeans.ui.projects"); - log(createRecordMetrics("USG_PROJECT_OPEN", initialA),"org.netbeans.ui.metrics.projects"); - } - - } - - private final RequestProcessor.Task resChangedTask = Hacks.RP.create(new Runnable() { - public @Override void run() { - Set lazyPDirs = new HashSet(); - for (FileObject fileObject : currentFiles.allInstances()) { - Project p = FileOwnerQuery.getOwner(fileObject); - if (p != null) { - lazyPDirs.add(p.getProjectDirectory()); - } - } - if (!lazyPDirs.isEmpty()) { - getDefault().LOAD.preferredProject(lazyPDirs); - } - } - }); - public @Override void resultChanged(LookupEvent ev) { - resChangedTask.schedule(50); - } - - final void enter() { - try { - enteredGuard.lock(); - entered++; - } finally { - enteredGuard.unlock(); - } - } - - final void exit() { - try { - enteredGuard.lock(); - if (--entered == 0) { - enteredZeroed.signalAll(); - } - } finally { - enteredGuard.unlock(); - } - } - - @Override - public boolean cancel(boolean mayInterruptIfRunning) { - return false; - } - - @Override - public boolean isCancelled() { - return false; - } - - @Override - public boolean isDone() { - return TASK.isFinished() && entered == 0; - } - - @Override - public Project[] get() throws InterruptedException, ExecutionException { - waitFinished(0); - try { - enteredGuard.lock(); - while (entered > 0) { - enteredZeroed.await(); - } - } finally { - enteredGuard.unlock(); - } - return getDefault().getOpenProjects(); - } - @Override - public Project[] get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { - long ms = unit.convert(timeout, TimeUnit.MILLISECONDS); - if (!waitFinished(timeout)) { - throw new TimeoutException(); - } - try { - enteredGuard.lock(); - if (entered > 0) { - if (!enteredZeroed.await(ms, TimeUnit.MILLISECONDS)) { - throw new TimeoutException(); - } - } - } finally { - enteredGuard.unlock(); - } - return getDefault().getOpenProjects(); - } - } - public void open( Project p ) { open( new Project[] {p}, false ); } @@ -659,7 +304,7 @@ public void open (Project p, boolean openSubprojects ) { public void open( Project[] projects, boolean openSubprojects ) { open(projects, openSubprojects, false); } - + public void open(final Project[] projects, final boolean openSubprojects, final boolean asynchronously) { open(projects, false, openSubprojects, asynchronously, null); } @@ -669,9 +314,9 @@ public void open(final Project[] projects, boolean prime, final boolean openSubp //nothing to do: return ; } - + long start = System.currentTimeMillis(); - + if (asynchronously) { class Cancellation extends AtomicBoolean implements Cancellable { Thread t; @@ -705,11 +350,11 @@ class Cancellation extends AtomicBoolean implements Cancellable { setMainProject(mainProject); } } - + long end = System.currentTimeMillis(); - - if (LOGGER.isLoggable(Level.FINE)) { - log(Level.FINE, "opening projects took: " + (end - start) + "ms"); + + if (OpenProjectsLogging.LOGGER.isLoggable(Level.FINE)) { + OpenProjectsLogging.log(Level.FINE, "opening projects took: " + (end - start) + "ms"); } } @@ -719,7 +364,7 @@ class Cancellation extends AtomicBoolean implements Cancellable { }) public void open(Project[] projects, boolean prime, boolean openSubprojects, ProgressHandle handle, AtomicBoolean canceled) { LOAD.waitFinished(0); - + List toHandle = new LinkedList(); pchSupport.firePropertyChange(PROPERTY_WILL_OPEN_PROJECTS, null, projects); @@ -730,11 +375,11 @@ public void open(Project[] projects, boolean prime, boolean openSubprojects, Pro if (p2 != null) { toHandle.add(p2); } else { - LOGGER.log(Level.WARNING, "Project in {0} disappeared", p.getProjectDirectory()); + OpenProjectsLogging.LOGGER.log(Level.WARNING, "Project in {0} disappeared", p.getProjectDirectory()); } if (prime) { ActionProvider ap = p2.getLookup().lookup(ActionProvider.class); - if (ap != null && + if (ap != null && Arrays.asList(ap.getSupportedActions()).contains(ActionProvider.COMMAND_PRIME) && ap.isActionEnabled(ActionProvider.COMMAND_PRIME, p2.getLookup())) { final CountDownLatch[] await = new CountDownLatch[1]; @@ -763,10 +408,10 @@ public void finished(boolean success) { } } } catch (InterruptedException | IOException | IllegalArgumentException ex) { - LOGGER.log(Level.INFO, "Cannot convert " + p.getProjectDirectory(), ex); + OpenProjectsLogging.LOGGER.log(Level.INFO, "Cannot convert " + p.getProjectDirectory(), ex); } } - + try { LOAD.enter(); boolean recentProjectsChanged = false; @@ -774,12 +419,12 @@ public void finished(boolean success) { double workForSubprojects = maxWork / (openSubprojects ? 2.0 : 10.0); double currentWork = 0; Collection projectsToOpen = new LinkedHashSet(); - + if (handle != null) { handle.switchToDeterminate(maxWork); handle.progress(0); } - + Map> subprojectsCache = new HashMap>(); // #59098 while (!toHandle.isEmpty()) { if (canceled != null && canceled.get()) { @@ -818,9 +463,9 @@ public void finished(boolean success) { } subprojectsCache.put(p, subprojects); } - + projectsToOpen.add(p); - + for (Project sub : subprojects) { assert sub != null; if (sub != null && /** #224592 we need to test for null sub as some subprojectProvider implementations could be faulty and return null and with final releases assert won't fire */ @@ -832,12 +477,12 @@ public void finished(boolean success) { } } } - + double workPerOneProject = (workForSubprojects - currentWork) / (toHandle.size() + 1); int lastState = (int) currentWork; - + currentWork += workPerOneProject; - + if (handle != null && lastState < (int) currentWork) { handle.progress((int) currentWork); } @@ -846,9 +491,9 @@ public void finished(boolean success) { if (projectsToOpen.isEmpty()) { return; } - + double workPerProject = (maxWork - workForSubprojects) / projectsToOpen.size(); - + final List oldprjs = new ArrayList(); final List newprjs = new ArrayList(); MUTEX.writeAccess(new Mutex.Action() { @@ -857,7 +502,7 @@ public void finished(boolean success) { return null; } }); - + for (Project p: projectsToOpen) { if (canceled != null && canceled.get()) { break; @@ -865,13 +510,13 @@ public void finished(boolean success) { if (handle != null) { handle.progress(ProjectUtils.getInformation(p).getDisplayName()); } - + recentProjectsChanged |= doOpenProject(p); - + int lastState = (int) currentWork; - + currentWork += workPerProject; - + if (handle != null && lastState < (int) currentWork) { handle.progress((int) currentWork); } @@ -898,14 +543,14 @@ public void finished(boolean success) { return null; } }); - + final boolean recentProjectsChangedCopy = recentProjectsChanged; - - LogRecord[] addedRec = createRecord("UI_OPEN_PROJECTS", projectsToOpen.toArray(new Project[0])); // NOI18N - log(addedRec,"org.netbeans.ui.projects"); - addedRec = createRecordMetrics("USG_PROJECT_OPEN", projectsToOpen.toArray(new Project[0])); // NOI18N - log(addedRec,"org.netbeans.ui.metrics.projects"); - + + LogRecord[] addedRec = OpenProjectsLogging.createRecord("UI_OPEN_PROJECTS", projectsToOpen.toArray(new Project[0])); // NOI18N + OpenProjectsLogging.log(addedRec,"org.netbeans.ui.projects"); + addedRec = OpenProjectsLogging.createRecordMetrics("USG_PROJECT_OPEN", projectsToOpen.toArray(new Project[0])); // NOI18N + OpenProjectsLogging.log(addedRec,"org.netbeans.ui.metrics.projects"); + Mutex.EVENT.readAccess(new Action() { @Override public Void run() { @@ -913,7 +558,7 @@ public Void run() { if ( recentProjectsChangedCopy ) { pchSupport.firePropertyChange( PROPERTY_RECENT_PROJECTS, null, null ); } - + return null; } }); @@ -921,33 +566,33 @@ public Void run() { LOAD.exit(); } } - + public void close( Project someProjects[], boolean notifyUI) { Group act = Group.getActiveGroup(); close(someProjects, notifyUI, act != null ? act.getName() : null); } - + public void close( Project someProjects[], boolean notifyUI, String groupName ) { boolean doSave = false; if (!LOAD.closeBeforeOpen(someProjects)) { doSave = true; LOAD.waitFinished(0); } - + final Project[] projects = new Project[someProjects.length]; for (int i = 0; i < someProjects.length; i++) { projects[i] = unwrapProject(someProjects[i]); } - - + + if (!ProjectUtilities.closeAllDocuments (projects, notifyUI, groupName )) { return; } - + try { LOAD.enter(); ProjectUtilities.WaitCursor.show(); - logProjects("close(): closing project: ", projects); + OpenProjectsLogging.logProjects("close(): closing project: ", projects); final AtomicBoolean mainClosed = new AtomicBoolean(); final AtomicBoolean someClosed = new AtomicBoolean(); final List oldprjs = new ArrayList(); @@ -1025,7 +670,7 @@ public void run() { } } }); - logProjects("close(): openProjects == ", openProjects.toArray(new Project[0])); // NOI18N + OpenProjectsLogging.logProjects("close(): openProjects == ", openProjects.toArray(new Project[0])); // NOI18N if (someClosed.get()) { pchSupport.firePropertyChange(PROPERTY_OPEN_PROJECTS, oldprjs.toArray(new Project[0]), newprjs.toArray(new Project[0]) ); @@ -1050,10 +695,10 @@ public void run() { } } } - LogRecord[] removedRec = createRecord("UI_CLOSED_PROJECTS", projects); // NOI18N - log(removedRec, "org.netbeans.ui.projects"); - removedRec = createRecordMetrics("USG_PROJECT_CLOSE", projects); // NOI18N - log(removedRec, "org.netbeans.ui.metrics.projects"); + LogRecord[] removedRec = OpenProjectsLogging.createRecord("UI_CLOSED_PROJECTS", projects); // NOI18N + OpenProjectsLogging.log(removedRec, "org.netbeans.ui.projects"); + removedRec = OpenProjectsLogging.createRecordMetrics("USG_PROJECT_CLOSE", projects); // NOI18N + OpenProjectsLogging.log(removedRec, "org.netbeans.ui.metrics.projects"); } finally { ProjectUtilities.WaitCursor.hide(); LOAD.exit(); @@ -1068,7 +713,7 @@ public Project[] getOpenProjects() { } }); } - + public boolean isOpen(final Project p) { return MUTEX.readAccess(new Mutex.Action() { public @Override Boolean run() { @@ -1094,7 +739,7 @@ public boolean isMainProject(final Project p) { } }); } - + public Project getMainProject() { return MUTEX.readAccess(new Mutex.Action() { public @Override Project run() { @@ -1102,19 +747,19 @@ public Project getMainProject() { } }); } - + public void setMainProject( Project project ) { - LOGGER.log(Level.FINER, "Setting main project: {0}", project); // NOI18N - logProjects("setMainProject(): openProjects == ", openProjects.toArray(new Project[0])); // NOI18N + OpenProjectsLogging.LOGGER.log(Level.FINER, "Setting main project: {0}", project); // NOI18N + OpenProjectsLogging.logProjects("setMainProject(): openProjects == ", openProjects.toArray(new Project[0])); // NOI18N //called here to avoid wrapping projectManager.MUTEX within PrivateMutex.MUTEX //#139965 the project passed in here can be different from the current one. // eg when the ManProjectAction shows a list of opened projects, it lists the "non-loaded skeletons" - // but when the user eventually selects one, the openProjects list already might hold the + // but when the user eventually selects one, the openProjects list already might hold the // correct loaded list. try { final Project prj = project != null ? ProjectManager.getDefault().findProject(project.getProjectDirectory()) : null; final String dn = project != null ? ProjectUtils.getInformation(project).getDisplayName() : ""; - + MUTEX.writeAccess(new Mutex.Action() { public @Override Void run() { Project main = prj; @@ -1134,12 +779,12 @@ public void setMainProject( Project project ) { } } if (fail) { - LOGGER.log(Level.WARNING, "Project {0} is not open and cannot be set as main.", dn); - logProjects("setMainProject(): openProjects == ", openProjects.toArray(new Project[0])); // NOI18N + OpenProjectsLogging.LOGGER.log(Level.WARNING, "Project {0} is not open and cannot be set as main.", dn); + OpenProjectsLogging.logProjects("setMainProject(): openProjects == ", openProjects.toArray(new Project[0])); // NOI18N return null; } } - + mainProject = main; saveMainProject(main); return null; @@ -1150,7 +795,7 @@ public void setMainProject( Project project ) { } pchSupport.firePropertyChange( PROPERTY_MAIN_PROJECT, null, null ); } - + public List getRecentProjects() { return MUTEX.readAccess(new Mutex.Action>() { @Override @@ -1159,16 +804,16 @@ public List run() { } }); } - + public boolean isRecentProjectsEmpty() { return MUTEX.readAccess(new Mutex.Action() { @Override public Boolean run() { return recentProjects.isEmpty(); } - }); + }); } - + public List getRecentProjectsInformation() { return MUTEX.readAccess(new Mutex.Action>() { @Override @@ -1177,26 +822,26 @@ public List run() { } }); } - - /** As this class is singletnon, which is not GCed it is good idea to + + /** As this class is singletnon, which is not GCed it is good idea to *add WeakListeners or remove the listeners properly. */ - + public void addPropertyChangeListener( PropertyChangeListener l ) { - pchSupport.addPropertyChangeListener( l ); + pchSupport.addPropertyChangeListener( l ); } - + public void removePropertyChangeListener( PropertyChangeListener l ) { - pchSupport.removePropertyChangeListener( l ); + pchSupport.removePropertyChangeListener( l ); } - - // Used from NewFile action + + // Used from NewFile action public List getTemplatesLRU( @NullAllowed Project project, PrivilegedTemplates priv ) { List pLRU = getTemplateNamesLRU( project, priv ); List templates = new ArrayList(); // Using folder is preferred option - try { + try { FileObject fo = FileUtil.getConfigFile( "Templates/Other/Folder" ); //NOI18N if ( fo != null ) { DataObject dobj = DataObject.find( fo ); @@ -1210,7 +855,7 @@ public List getTemplatesLRU( @NullAllowed Project project, Privileg FileObject fo = it.next(); if ( fo != null ) { try { - DataObject dobj = DataObject.find( fo ); + DataObject dobj = DataObject.find( fo ); templates.add( dobj ); } catch ( DataObjectNotFoundException e ) { @@ -1222,63 +867,63 @@ public List getTemplatesLRU( @NullAllowed Project project, Privileg it.remove(); } } - + return templates; } - - - // Used from NewFile action + + + // Used from NewFile action public void updateTemplatesLRU(final FileObject template) { MUTEX.writeAccess(new Mutex.Action() { public @Override Void run() { - + String templateName = template.getPath(); - + getRecentTemplates().remove(templateName); getRecentTemplates().add( 0, templateName ); - + if ( getRecentTemplates().size() > 100 ) { getRecentTemplates().remove( 100 ); } - + OpenProjectListSettings.getInstance().setRecentTemplates( new ArrayList( getRecentTemplates() ) ); return null; } }); } - - + + // Package private methods ------------------------------------------------- // Used from ProjectUiModule static void shutdown() { if (INSTANCE != null) { try { - //a bit on magic here. We want to do the goup document persistence before notifyClosed in hope of the + //a bit on magic here. We want to do the goup document persistence before notifyClosed in hope of the // ant projects saving their project data before being closed. (ant ptojects call saveProjct() in the openclose hook. - // the caller of this method calls saveAllProjectt() later. + // the caller of this method calls saveAllProjectt() later. Group.onShutdown(new LinkedHashSet<>(INSTANCE.openProjects)); - for (Project p : INSTANCE.openProjects) { - notifyClosed(p); + for (Project p : INSTANCE.openProjects) { + notifyClosed(p); } } catch (ConcurrentModificationException x) { - LOGGER.log(Level.INFO, "#198097: could not get list of projects to close", x); + OpenProjectsLogging.LOGGER.log(Level.INFO, "#198097: could not get list of projects to close", x); } } } - + // Used from OpenProjectAction public static Project fileToProject( File projectDir ) { - + try { - + FileObject fo = FileUtil.toFileObject(projectDir); if (fo != null && /* #60518 */ fo.isFolder()) { return ProjectManager.getDefault().findProject(fo); } else { return null; } - + } catch ( IOException e ) { /* Ignore; will be reported e.g. by ProjectChooserAccessory: @@ -1286,48 +931,26 @@ public static Project fileToProject( File projectDir ) { */ return null; } - + } - - - + + + // Private methods --------------------------------------------------------- - - private static Set URLs2Projects(Collection urls) { - Set result = new LinkedHashSet<>(); - for (URL url : urls) { - FileObject dir = URLMapper.findFileObject(url); - if (dir != null && dir.isFolder()) { - try { - Project p = ProjectManager.getDefault().findProject(dir); - if (p != null && !result.contains(p)) { //#238093, #238811 if multiple entries point to the same project we end up with the same instance multiple times in the linked list. That's wrong. - result.add(p); - } - } catch (Throwable t) { - //something bad happened during loading the project. - //log the problem, but allow the other projects to be load - //see issue #65900 - ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, t); - } - } - } - return result; - } - private static boolean notifyOpened(Project p) { boolean ok = true; for (ProjectOpenedHook hook : p.getLookup().lookupAll(ProjectOpenedHook.class)) { try { ProjectOpenedTrampoline.DEFAULT.projectOpened(hook); } catch (RuntimeException e) { - log(Level.WARNING, null, e); + OpenProjectsLogging.log(Level.WARNING, null, e); // Do not try to call its close hook if its open hook already failed: INSTANCE.openProjects.remove(p); INSTANCE.removeModuleInfo(p); ok = false; } catch (Error e) { - log(Level.WARNING, null, e); + OpenProjectsLogging.log(Level.WARNING, null, e); INSTANCE.openProjects.remove(p); INSTANCE.removeModuleInfo(p); ok = false; @@ -1339,15 +962,15 @@ private static boolean notifyOpened(Project p) { } return ok; } - + private static void notifyClosed(Project p) { for (ProjectOpenedHook hook : p.getLookup().lookupAll(ProjectOpenedHook.class)) { try { ProjectOpenedTrampoline.DEFAULT.projectClosed(hook); } catch (RuntimeException e) { - log(Level.WARNING, null, e); + OpenProjectsLogging.log(Level.WARNING, null, e); } catch (Error e) { - log(Level.WARNING, null, e); + OpenProjectsLogging.log(Level.WARNING, null, e); } } } @@ -1375,11 +998,11 @@ public static List prepareTemplates(@NullAllowed Project project, } private boolean doOpenProject(final @NonNull Project p) { - LOGGER.log(Level.FINER, "doOpenProject: {0}", p); + OpenProjectsLogging.LOGGER.log(Level.FINER, "doOpenProject: {0}", p); final AtomicBoolean alreadyOpen = new AtomicBoolean(); boolean recentProjectsChanged = MUTEX.writeAccess(new Mutex.Action() { public @Override Boolean run() { - log(Level.FINER, "already opened: {0} ", openProjects); + OpenProjectsLogging.log(Level.FINER, "already opened: {0} ", openProjects); for (Project existing : openProjects) { // TODO An old hack due to broken equals() contract; see https://bz.apache.org/netbeans/show_bug.cgi?id=156536 if (p.equals(existing) || existing.equals(p)) { @@ -1392,14 +1015,14 @@ private boolean doOpenProject(final @NonNull Project p) { //initially opened projects need to have these listeners also added. p.getProjectDirectory().addFileChangeListener(deleteListener); p.getProjectDirectory().addFileChangeListener(nbprojectDeleteListener); - + return recentProjects.remove(p); } }); if (alreadyOpen.get()) { return false; } - logProjects("doOpenProject(): openProjects == ", openProjects.toArray(new Project[0])); // NOI18N + OpenProjectsLogging.logProjects("doOpenProject(): openProjects == ", openProjects.toArray(new Project[0])); // NOI18N // Notify projects opened notifyOpened(p); @@ -1408,30 +1031,28 @@ private boolean doOpenProject(final @NonNull Project p) { ProjectUtilities.openProjectFiles(p); } }); - + return recentProjectsChanged; } - + private static List loadProjectList() { assert MUTEX.isReadAccess() || MUTEX.isWriteAccess(); List URLs = OpenProjectListSettings.getInstance().getOpenProjectsURLs(); List names = OpenProjectListSettings.getInstance().getOpenProjectsDisplayNames(); List icons = OpenProjectListSettings.getInstance().getOpenProjectsIcons(); List projects = new ArrayList(); - + Iterator urlIt = URLs.iterator(); Iterator namesIt = names.iterator(); Iterator iconIt = icons.iterator(); - + while(urlIt.hasNext() && namesIt.hasNext() && iconIt.hasNext()) { projects.add(new LazyProject(urlIt.next(), namesIt.next(), iconIt.next())); } - - //List projects = URLs2Projects( URLs ); - + return projects; } - + private static List projects2Unloaded( List projects ) { assert !MUTEX.isReadAccess() && !MUTEX.isWriteAccess(); //using ProjectUtils.getInformation() - aquires project mutex List toRet = new ArrayList(); @@ -1444,8 +1065,8 @@ private static List projects2Unloaded( List } return toRet; } - - + + private static void saveProjectList( List projects ) { assert MUTEX.isWriteAccess(); List URLs = new ArrayList(); @@ -1462,33 +1083,33 @@ private static void saveProjectList( List projects ) OpenProjectListSettings.getInstance().setOpenProjectsDisplayNames(names); OpenProjectListSettings.getInstance().setOpenProjectsIcons(icons); } - - private static void saveMainProject( Project mainProject ) { + + private static void saveMainProject( Project mainProject ) { assert MUTEX.isWriteAccess(); URL mainRoot = mainProject == null ? null : mainProject.getProjectDirectory().toURL(); OpenProjectListSettings.getInstance().setMainProjectURL( mainRoot ); } - + private ArrayList getTemplateNamesLRU( @NullAllowed final Project project, PrivilegedTemplates priv ) { // First take recently used templates and try to find those which // are supported by the project. - + final ArrayList result = new ArrayList(NUM_TEMPLATES); - + PrivilegedTemplates pt = priv != null ? priv : project != null ? project.getLookup().lookup( PrivilegedTemplates.class ) : null; - String ptNames[] = pt == null ? null : pt.getPrivilegedTemplates(); + String ptNames[] = pt == null ? null : pt.getPrivilegedTemplates(); final ArrayList privilegedTemplates = new ArrayList( Arrays.asList( pt == null ? new String[0]: ptNames ) ); final ArrayList toRemove = new ArrayList(); if (priv == null) { // when the privileged templates are part of the active lookup, // do not mix them with the recent templates, but use only the privileged ones. // eg. on Webservices node, one is not interested in a recent "jsp" file template.. - - MUTEX.readAccess(new Mutex.Action() { //#201355 changed from writeAccess to readAccess no apparent data modification going on with exception of + + MUTEX.readAccess(new Mutex.Action() { //#201355 changed from writeAccess to readAccess no apparent data modification going on with exception of //invalid recent templates removal.. postpone that to a later async time public @Override Void run() { String[] rtNames = getRecommendedTypes(project); - + Iterator it = getRecentTemplates().iterator(); for( int i = 0; i < NUM_TEMPLATES && it.hasNext(); i++ ) { String templateName = it.next(); @@ -1512,7 +1133,7 @@ else if ( isRecommended( project, rtNames, fo ) ) { @Override public void run() { - OpenProjectList.MUTEX.writeAccess(new Mutex.Action() { //#201355 changed from writeAccess to readAccess no apparent data modification going on. + OpenProjectList.MUTEX.writeAccess(new Mutex.Action() { //#201355 changed from writeAccess to readAccess no apparent data modification going on. public @Override Void run() { getRecentTemplates().removeAll(toRemove); return null; @@ -1521,7 +1142,7 @@ public void run() { } }); } - + // If necessary fill the list with the rest of privileged templates Iterator it = privilegedTemplates.iterator(); for( int i = result.size(); i < NUM_TEMPLATES && it.hasNext(); i++ ) { @@ -1531,9 +1152,9 @@ public void run() { result.add( fo ); } } - + return result; - + } static boolean isRecommended(@NonNull String[] recommendedTypes, @NonNull FileObject primaryFile) { @@ -1541,7 +1162,7 @@ static boolean isRecommended(@NonNull String[] recommendedTypes, @NonNull FileOb // if no recommendedTypes are supported (i.e. freeform) -> disaply all templates return true; } - + Object o = primaryFile.getAttribute ("templateCategory"); // NOI18N if (o != null) { assert o instanceof String : primaryFile + " attr templateCategory = " + o; @@ -1585,7 +1206,7 @@ static boolean isRecommended(@NullAllowed Project project, @NonNull String[] rec RecommendedTemplates rt = project.getLookup().lookup(RecommendedTemplates.class); return rt == null ? new String[0] : rt.getRecommendedTypes(); } - + private static List getCategories (String source) { ArrayList categories = new ArrayList (); StringTokenizer cattok = new StringTokenizer (source, ","); // NOI18N @@ -1594,18 +1215,18 @@ private static List getCategories (String source) { } return categories; } - + // Private innerclasses ---------------------------------------------------- - + /** Maintains recent project list - */ + */ private class RecentProjectList { - + private final List recentProjects; private final List recentProjectsInfos; - + private final int size; - + /** *@size Max number of the project list. */ @@ -1613,11 +1234,11 @@ public RecentProjectList( int size ) { this.size = size; recentProjects = new ArrayList( size ); recentProjectsInfos = new ArrayList(size); - if (LOGGER.isLoggable(Level.FINE)) { - log(Level.FINE, "created a RecentProjectList: size=" + size); + if (OpenProjectsLogging.LOGGER.isLoggable(Level.FINE)) { + OpenProjectsLogging.log(Level.FINE, "created a RecentProjectList: size=" + size); } } - + public void add(final Project p) { final UnloadedProjectInformation projectInfo; // #183681: call outside of lock @@ -1630,8 +1251,8 @@ public void add(final Project p) { int index = getIndex(p); if (index == -1) { // Project not in list - if (LOGGER.isLoggable(Level.FINE)) { - log(Level.FINE, "add new recent project: " + p); + if (OpenProjectsLogging.LOGGER.isLoggable(Level.FINE)) { + OpenProjectsLogging.log(Level.FINE, "add new recent project: " + p); } if (recentProjects.size() == size) { // Need some space for the newly added project @@ -1639,7 +1260,7 @@ public void add(final Project p) { recentProjectsInfos.remove(size - 1); } } else { - LOGGER.log(Level.FINE, "re-add recent project: {0} @{1}", new Object[] {p, index}); + OpenProjectsLogging.LOGGER.log(Level.FINE, "re-add recent project: {0} @{1}", new Object[] {p, index}); // Project is in list => just move it to first place recentProjects.remove(index); recentProjectsInfos.remove(index); @@ -1650,13 +1271,13 @@ public void add(final Project p) { } }); } - + public boolean remove(final Project p) { return OpenProjectList.MUTEX.writeAccess(new Mutex.Action() { public @Override Boolean run() { int index = getIndex( p ); if ( index != -1 ) { - LOGGER.log(Level.FINE, "remove recent project: {0} @{1}", new Object[] {p, index}); + OpenProjectsLogging.LOGGER.log(Level.FINE, "remove recent project: {0} @{1}", new Object[] {p, index}); recentProjects.remove( index ); recentProjectsInfos.remove(index); return true; @@ -1674,10 +1295,10 @@ public void run() { final List unloadedRefs = new ArrayList(); final List refsToRemove = new ArrayList(); final List unloadedRefsToRemove = new ArrayList(); - + //this is split into readMutex-noMutex-WriteMutex section because we want to avoid the situation when OPL.Mutex is wrapping //projectManager.Mutex that could prove to be a major source of deadlocks in the codebase. - + OpenProjectList.MUTEX.readAccess(new Runnable() { @Override public void run() { @@ -1722,7 +1343,7 @@ public void run() { } }); } - + public List getProjects() { assert OpenProjectList.MUTEX.isReadAccess(); List result = new ArrayList( recentProjects.size() ); @@ -1732,38 +1353,38 @@ public List getProjects() { Project p = pRef.getProject(); if ( p == null || !p.getProjectDirectory().isValid() ) { remove( p ); // Folder does not exist any more => remove from - if (LOGGER.isLoggable(Level.FINE)) { - log(Level.FINE, "removing dead recent project: " + p); + if (OpenProjectsLogging.LOGGER.isLoggable(Level.FINE)) { + OpenProjectsLogging.log(Level.FINE, "removing dead recent project: " + p); } } else { result.add( p ); } } - if (LOGGER.isLoggable(Level.FINE)) { - log(Level.FINE, "recent projects: " + result); + if (OpenProjectsLogging.LOGGER.isLoggable(Level.FINE)) { + OpenProjectsLogging.log(Level.FINE, "recent projects: " + result); } return result; } - + public boolean isEmpty() { assert OpenProjectList.MUTEX.isReadAccess(); boolean empty = recentProjects.isEmpty(); - if (LOGGER.isLoggable(Level.FINE)) { - log(Level.FINE, "recent projects empty? " + empty); + if (OpenProjectsLogging.LOGGER.isLoggable(Level.FINE)) { + OpenProjectsLogging.log(Level.FINE, "recent projects empty? " + empty); } return empty; } - + public void load() { //read mutex only in the case of OPL.getDefault(), otherwise needs to be write, as it's mutating content. assert OpenProjectList.MUTEX.isReadAccess() || OpenProjectList.MUTEX.isWriteAccess(); - + List URLs = OpenProjectListSettings.getInstance().getRecentProjectsURLs(); List names = OpenProjectListSettings.getInstance().getRecentProjectsDisplayNames(); List icons = OpenProjectListSettings.getInstance().getRecentProjectsIcons(); - if (LOGGER.isLoggable(Level.FINE)) { - log(Level.FINE, "recent project list load: " + URLs); + if (OpenProjectsLogging.LOGGER.isLoggable(Level.FINE)) { + OpenProjectsLogging.log(Level.FINE, "recent project list load: " + URLs); } recentProjects.clear(); for (URL url : URLs) { @@ -1793,7 +1414,7 @@ public void load() { p.getProjectDirectory().addFileChangeListener(nbprojectDeleteListener); } } - + public void save() { assert OpenProjectList.MUTEX.isWriteAccess(); List URLs = new ArrayList( recentProjects.size() ); @@ -1804,7 +1425,7 @@ public void save() { } } List _recentProjectsInfos = getRecentProjectsInfo(); - LOGGER.log(Level.FINE, "save recent project list: recentProjects={0} recentProjectsInfos={1} URLs={2}", + OpenProjectsLogging.LOGGER.log(Level.FINE, "save recent project list: recentProjects={0} recentProjectsInfos={1} URLs={2}", new Object[] {recentProjects, _recentProjectsInfos, URLs}); OpenProjectListSettings.getInstance().setRecentProjectsURLs( URLs ); int listSize = _recentProjectsInfos.size(); @@ -1819,15 +1440,15 @@ public void save() { OpenProjectListSettings.getInstance().setRecentProjectsDisplayNames(names); OpenProjectListSettings.getInstance().setRecentProjectsIcons(icons); } - + private int getIndex( Project p ) { if (p == null || p.getProjectDirectory() == null) { return -1; } URL pURL = p.getProjectDirectory().toURL(); - + int i = 0; - + for (ProjectReference pRef : recentProjects) { URL p2URL = pRef.getURL(); if ( pURL.equals( p2URL ) ) { @@ -1836,10 +1457,10 @@ private int getIndex( Project p ) { i++; } } - + return -1; } - + private List getRecentProjectsInfo() { // #166408: refreshing is too time expensive and we want to be fast, not correct //refresh(); @@ -1849,67 +1470,67 @@ private List getRecentProjectsInfo() { } }); } - + private class ProjectReference { - + private WeakReference projectReference; private final URL projectURL; - - public ProjectReference( URL url ) { + + public ProjectReference( URL url ) { this.projectURL = url; } - + public ProjectReference( Project p ) { this.projectReference = new WeakReference( p ); projectURL = p.getProjectDirectory().toURL(); } - + public Project getProject() { - - Project p = null; - + + Project p = null; + if ( projectReference != null ) { // Reference to project exists p = projectReference.get(); if ( p != null ) { // And refers to some project, check for validity: if ( ProjectManager.getDefault().isValid( p ) ) - return p; + return p; else return null; } } - - if (LOGGER.isLoggable(Level.FINE)) { - log(Level.FINE, "no active project reference for " + projectURL); + + if (OpenProjectsLogging.LOGGER.isLoggable(Level.FINE)) { + OpenProjectsLogging.log(Level.FINE, "no active project reference for " + projectURL); } - if ( projectURL != null ) { + if ( projectURL != null ) { FileObject dir = URLMapper.findFileObject( projectURL ); if ( dir != null && dir.isFolder() ) { try { p = ProjectManager.getDefault().findProject( dir ); if ( p != null ) { - projectReference = new WeakReference( p ); - if (LOGGER.isLoggable(Level.FINE)) { - log(Level.FINE, "found " + p); + projectReference = new WeakReference( p ); + if (OpenProjectsLogging.LOGGER.isLoggable(Level.FINE)) { + OpenProjectsLogging.log(Level.FINE, "found " + p); } return p; } - } + } catch ( IOException e ) { // Ignore invalid folders - if (LOGGER.isLoggable(Level.FINE)) { - log(Level.FINE, "could not load recent project from " + projectURL); + if (OpenProjectsLogging.LOGGER.isLoggable(Level.FINE)) { + OpenProjectsLogging.log(Level.FINE, "could not load recent project from " + projectURL); } } } } - - if (LOGGER.isLoggable(Level.FINE)) { - log(Level.FINE, "no recent project in " + projectURL); + + if (OpenProjectsLogging.LOGGER.isLoggable(Level.FINE)) { + OpenProjectsLogging.log(Level.FINE, "no recent project in " + projectURL); } - return null; // Empty reference + return null; // Empty reference } - + public URL getURL() { return projectURL; } @@ -1917,11 +1538,11 @@ public URL getURL() { public @Override String toString() { return projectURL.toString(); } - + } - + } - + private static class ProjectByPathComparator implements Comparator { @Override public int compare(Project p1, Project p2) { @@ -1937,23 +1558,23 @@ public int compare(Project p1, Project p2) { return p1.getProjectDirectory().getPath().compareTo(p2.getProjectDirectory().getPath()); } } - + private final class NbProjectDeletionListener extends FileChangeAdapter { - + public NbProjectDeletionListener() {} - + @Override public void fileDeleted(FileEvent fe) { recentProjects.refresh(); } - + } - + /** * Closes deleted projects. */ private final class ProjectDeletionListener extends FileChangeAdapter { - + public ProjectDeletionListener() {} public @Override void fileDeleted(final FileEvent fe) { @@ -1981,10 +1602,9 @@ public void run () { } }); } - + } - - + private void addModuleInfo(final Project prj) { final ModuleInfo info = Modules.getDefault().ownerOf(prj.getClass()); if (info != null) { @@ -2001,11 +1621,11 @@ private void addModuleInfo(final Project prj) { }); } } - + private void removeModuleInfo(Project prj) { removeModuleInfo(prj, Modules.getDefault().ownerOf(prj.getClass())); } - + private void removeModuleInfo(final Project prj, final ModuleInfo info) { // info can be null in case we are closing a project from disabled module if (info != null) { @@ -2037,84 +1657,62 @@ private void checkModuleInfo(ModuleInfo info) { close(toRemove.toArray(new Project[0]), false); } } - - private static LogRecord[] createRecord(String msg, Project[] projects) { - if (projects.length == 0) { - return null; - } - - Map counts = new HashMap(); - for (Project p : projects) { - String n = p.getClass().getName(); - int[] cnt = counts.get(n); - if (cnt == null) { - cnt = new int[1]; - counts.put(n, cnt); - } - cnt[0]++; - } - - Logger logger = Logger.getLogger("org.netbeans.ui.projects"); // NOI18N - LogRecord[] arr = new LogRecord[counts.size()]; - int i = 0; - for (Map.Entry entry : counts.entrySet()) { - LogRecord rec = new LogRecord(Level.CONFIG, msg); - rec.setParameters(new Object[] { entry.getKey(), afterLastDot(entry.getKey()), entry.getValue()[0] }); - rec.setLoggerName(logger.getName()); - rec.setResourceBundle(NbBundle.getBundle(OpenProjectList.class)); - rec.setResourceBundleName(OpenProjectList.class.getPackage().getName()+".Bundle"); - - arr[i++] = rec; - } - - return arr; - } - private static LogRecord[] createRecordMetrics (String msg, Project[] projects) { - if (projects.length == 0) { - return null; + final class LoadingCallback implements OpenProjectsLoading.Callback { + @Override + public void updateGlobalState(List lazilyOpenedProjects, Project lazyMainProject, List recentTemplates, boolean checkFirstRun) { + assert MUTEX.isWriteAccess(); + + openProjects = lazilyOpenedProjects; + if (lazyMainProject != null) { + mainProject = lazyMainProject; + } + mainProject = unwrapProject(mainProject); + getRecentTemplates().addAll(recentTemplates); + + MUTEX.postReadRequest(() -> { + Project[] opened = lazilyOpenedProjects.toArray(Project[]::new); + pchSupport.firePropertyChange(OpenProjectList.PROPERTY_OPEN_PROJECTS, new Project[0], opened); + Project main = mainProject; + if (main != null) { + // else PROPERTY_MAIN_PROJECT would be fired spuriously + pchSupport.firePropertyChange(OpenProjectList.PROPERTY_MAIN_PROJECT, null, main); + } + if (checkFirstRun && opened.length > 0) { + OpenProjectList.OPENING_RP.execute(() -> { + for (Project p : opened) { + Project del = p.getLookup().lookup(Project.class); + ProjectUtilities.openProjectFiles(del == null ? p : del); + } + }); + } + OpenProjectsLogging.log(Level.FINER, "updateGlobalState, done, notified"); // NOI18N + }); } - Logger logger = Logger.getLogger("org.netbeans.ui.metrics.projects"); // NOI18N - - LogRecord[] arr = new LogRecord[projects.length]; - int i = 0; - for (Project p : projects) { - LogRecord rec = new LogRecord(Level.INFO, msg); - rec.setParameters(new Object[] { p.getClass().getName() }); - rec.setLoggerName(logger.getName()); - - arr[i++] = rec; + @Override + public boolean finishOpening(Project p) { + if (notifyOpened(p)) { + OpenProjectsLogging.log(Level.FINE, "notify opened {0}", p); // NOI18N + PropertyChangeEvent ev = new PropertyChangeEvent(this, OpenProjectList.PROPERTY_REPLACE, null, p); + try { + pchSupport.firePropertyChange(ev); + } catch (Throwable t) { + OpenProjectsLogging.log(Level.WARNING, "broken node for {0}", t); + } + OpenProjectsLogging.log(Level.FINE, "property change notified {0}", p); // NOI18N + //same as in doOpenProject() but here for initially opened projects + p.getProjectDirectory().addFileChangeListener(deleteListener); + p.getProjectDirectory().addFileChangeListener(nbprojectDeleteListener); + return true; + } else { + return false; + } } - return arr; - } - - private static void log(LogRecord[] arr, String loggerName) { - if (arr == null) { - return; - } - Logger logger = Logger.getLogger(loggerName); // NOI18N - for (LogRecord r : arr) { - logger.log(r); - } - } - - private static String afterLastDot(String s) { - int index = s.lastIndexOf('.'); - if (index == -1) { - return s; - } - return s.substring(index + 1); - } - - private static void logProjects(String message, Project[] projects) { - if (projects.length == 0) { - return; - } - for (Project p : projects) { - LOGGER.log(Level.FINER, "{0} {1}", new Object[]{ message, p == null ? null : p.toString()}); + @Override + public void beginOpening(Project p) { + addModuleInfo(p); } } - } diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectsLoading.java b/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectsLoading.java new file mode 100644 index 000000000000..4cdedc824488 --- /dev/null +++ b/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectsLoading.java @@ -0,0 +1,406 @@ +/* + * 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.beans.PropertyChangeEvent; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.logging.Level; +import java.util.prefs.Preferences; +import org.netbeans.api.progress.ProgressHandle; +import org.netbeans.api.project.FileOwnerQuery; +import org.netbeans.api.project.Project; +import org.netbeans.api.project.ProjectManager; +import static org.netbeans.modules.project.ui.OpenProjectList.MUTEX; +import org.netbeans.modules.project.ui.groups.Group; +import org.openide.ErrorManager; +import org.openide.filesystems.FileObject; +import org.openide.filesystems.URLMapper; +import org.openide.util.Lookup; +import org.openide.util.LookupEvent; +import org.openide.util.LookupListener; +import org.openide.util.Mutex; +import org.openide.util.NbBundle; +import org.openide.util.RequestProcessor; +import org.openide.util.Utilities; +import org.openide.util.WeakListeners; + +final class OpenProjectsLoading implements Runnable, LookupListener, Future { + static final RequestProcessor RP = new RequestProcessor("Load Open Projects"); // NOI18N + final RequestProcessor.Task TASK = RP.create(this); + private int action; + private final LinkedList toOpenProjects = new LinkedList<>(); + private List lazilyOpenedProjects; + private List recentTemplates; + private Project lazyMainProject; + private Lookup.Result currentFiles; + private int entered; + private final Lock enteredGuard = new ReentrantLock(); + private final Condition enteredZeroed = enteredGuard.newCondition(); + private final ProgressHandle progress; + private final Callback outer; + + @NbBundle.Messages(value = "CAP_Opening_Projects=Opening Projects") + @SuppressWarnings(value = "LeakingThisInConstructor") + public OpenProjectsLoading(int a, final Callback outer) { + this.outer = outer; + action = a; + currentFiles = Utilities.actionsGlobalContext().lookupResult(FileObject.class); + currentFiles.addLookupListener(WeakListeners.create(LookupListener.class, this, currentFiles)); + progress = ProgressHandle.createHandle(Bundle.CAP_Opening_Projects()); + } + + final boolean waitFinished(long timeout) { + OpenProjectsLogging.log(Level.FINER, "waitFinished, action {0}", action); // NOI18N + if (action == 0) { + run(); + } + OpenProjectsLogging.log(Level.FINER, "waitFinished, before wait"); // NOI18N + if (timeout == 0) { + TASK.waitFinished(); + } else { + try { + if (!TASK.waitFinished(timeout)) { + return false; + } + } catch (InterruptedException ex) { + return false; + } + } + OpenProjectsLogging.log(Level.FINER, "waitFinished, after wait"); // NOI18N + return true; + } + + @Override + public void run() { + OpenProjectsLogging.log(Level.FINE, "LoadOpenProjects.run: {0}", action); // NOI18N + switch (action) { + case 0 -> { + action = 1; + TASK.schedule(0); + resultChanged(null); + return; + } + case 1 -> { + if (!RP.isRequestProcessorThread()) { + return; + } + action = 2; + try { + progress.start(); + loadInBackground(); + } finally { + progress.finish(); + } + updateGlobalState(); + ProjectsRootNode.checkNoLazyNode(); + Group.projectsLoaded(); + return; + } + case 2 -> { + // finished, oK + return; + } + default -> throw new IllegalStateException("unknown action: " + action); + } + } + + final void preferredProject(final Set lazyPDirs) { + OpenProjectList.MUTEX.writeAccess((Mutex.Action) () -> { + for (Project p : new ArrayList(toOpenProjects)) { + FileObject dir = p.getProjectDirectory(); + assert dir != null : "Project has real directory " + p; + if (lazyPDirs.contains(dir)) { + toOpenProjects.remove(p); + toOpenProjects.addFirst(p); + return null; + } + } + return null; + }); + } + + private void updateGlobalState() { + OpenProjectsLogging.log(Level.FINER, "updateGlobalState"); // NOI18N + OpenProjectList.MUTEX.writeAccess(new Mutex.Action() { + @Override + public Void run() { + OpenProjectsLogging.log(Level.FINER, "openProjects changed: {0}", lazilyOpenedProjects); // NOI18N + outer.updateGlobalState(lazilyOpenedProjects, lazyMainProject, recentTemplates, checkFirstRun()); + OpenProjectsLogging.log(Level.FINER, "updateGlobalState, applied"); // NOI18N + return null; + } + }); + } + + private boolean checkFirstRun() { + Preferences prefs = OpenProjectListSettings.getInstance().getPreferences(); + String prefKey = "projectListVersion"; // NOI18N + String build = System.getProperty("netbeans.buildnumber", "0"); // NOI18N + if (!prefs.get(prefKey, "").equals(build)) { + prefs.put(prefKey, build); + return true; + } else { + return false; + } + } + + boolean closeBeforeOpen(final Project[] arr) { + return OpenProjectList.MUTEX.writeAccess(new Mutex.Action() { + @Override + public Boolean run() { + NEXT: + for (Project p : arr) { + FileObject dir = p.getProjectDirectory(); + for (Iterator it = toOpenProjects.iterator(); it.hasNext();) { + if (dir.equals(it.next().getProjectDirectory())) { + it.remove(); + continue NEXT; + } + } + return false; + } + return true; + } + }); + } + + @NbBundle.Messages(value = {"#NOI18N", "LOAD_PROJECTS_ON_START=true"}) + private void loadInBackground() { + lazilyOpenedProjects = new ArrayList<>(); + final boolean loadProjectsOnStart = "true".equals(Bundle.LOAD_PROJECTS_ON_START()); + List urls = loadProjectsOnStart ? OpenProjectListSettings.getInstance().getOpenProjectsURLs() : Collections.emptyList(); + final List initial = new ArrayList<>(); + final Collection projects = urls2Projects(urls); + OpenProjectList.MUTEX.writeAccess(new Mutex.Action() { + @Override + public Void run() { + toOpenProjects.addAll(projects); + OpenProjectsLogging.log(Level.FINER, "loadOnBackground {0}", toOpenProjects); // NOI18N + initial.addAll(toOpenProjects); + return null; + } + }); + recentTemplates = new ArrayList(OpenProjectListSettings.getInstance().getRecentTemplates()); + final URL mainProjectURL = OpenProjectListSettings.getInstance().getMainProjectURL(); + int max = OpenProjectList.MUTEX.writeAccess(new Mutex.Action() { + @Override + public Integer run() { + for (Project p : toOpenProjects) { + outer.beginOpening(p); + // Set main project + if (mainProjectURL != null && mainProjectURL.equals(p.getProjectDirectory().toURL())) { + lazyMainProject = p; + } + } + return toOpenProjects.size(); + } + }); + progress.switchToDeterminate(max); + for (;;) { + final AtomicInteger openPrjSize = new AtomicInteger(); + Project p = OpenProjectList.MUTEX.writeAccess(new Mutex.Action() { + @Override + public Project run() { + if (toOpenProjects.isEmpty()) { + return null; + } + Project p = toOpenProjects.remove(); + OpenProjectsLogging.log(Level.FINER, "after remove {0}", toOpenProjects); // NOI18N + openPrjSize.set(toOpenProjects.size()); + return p; + } + }); + if (p == null) { + break; + } + OpenProjectsLogging.log(Level.FINE, "about to open a project {0}", p); // NOI18N + boolean successfullyOpened = outer.finishOpening(p); + if (successfullyOpened) { + lazilyOpenedProjects.add(p); + } else { + // opened failed, remove main project if same. + if (lazyMainProject == p) { + lazyMainProject = null; + } + } + progress.progress(max - openPrjSize.get()); + } + if (initial != null) { + Project[] initialA = initial.toArray(new Project[0]); + OpenProjectsLogging.log(OpenProjectsLogging.createRecord("UI_INIT_PROJECTS", initialA), "org.netbeans.ui.projects"); + OpenProjectsLogging.log(OpenProjectsLogging.createRecordMetrics("USG_PROJECT_OPEN", initialA), "org.netbeans.ui.metrics.projects"); + } + } + private final RequestProcessor.Task resChangedTask = Hacks.RP.create(new Runnable() { + @Override + public void run() { + Set lazyPDirs = new HashSet(); + for (FileObject fileObject : currentFiles.allInstances()) { + Project p = FileOwnerQuery.getOwner(fileObject); + if (p != null) { + lazyPDirs.add(p.getProjectDirectory()); + } + } + if (!lazyPDirs.isEmpty()) { + preferredProject(lazyPDirs); + } + } + }); + + @Override + public void resultChanged(LookupEvent ev) { + resChangedTask.schedule(50); + } + + final void enter() { + try { + enteredGuard.lock(); + entered++; + } finally { + enteredGuard.unlock(); + } + } + + final void exit() { + try { + enteredGuard.lock(); + if (--entered == 0) { + enteredZeroed.signalAll(); + } + } finally { + enteredGuard.unlock(); + } + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return false; + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public boolean isDone() { + return TASK.isFinished() && entered == 0; + } + + private Project[] getOpenProjects() { + return MUTEX.readAccess(new Mutex.Action() { + public @Override Project[] run() { + return lazilyOpenedProjects.toArray(new Project[0]); + } + }); + } + + + @Override + public Project[] get() throws InterruptedException, ExecutionException { + waitFinished(0); + try { + enteredGuard.lock(); + while (entered > 0) { + enteredZeroed.await(); + } + } finally { + enteredGuard.unlock(); + } + return getOpenProjects(); + } + + @Override + public Project[] get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + long ms = unit.convert(timeout, TimeUnit.MILLISECONDS); + if (!waitFinished(timeout)) { + throw new TimeoutException(); + } + try { + enteredGuard.lock(); + if (entered > 0) { + if (!enteredZeroed.await(ms, TimeUnit.MILLISECONDS)) { + throw new TimeoutException(); + } + } + } finally { + enteredGuard.unlock(); + } + return getOpenProjects(); + } + + private static Set urls2Projects(Collection urls) { + Set result = new LinkedHashSet<>(); + + for (URL url : urls) { + FileObject dir = URLMapper.findFileObject(url); + if (dir != null && dir.isFolder()) { + try { + Project p = ProjectManager.getDefault().findProject(dir); + if (p != null && !result.contains(p)) { //#238093, #238811 if multiple entries point to the same project we end up with the same instance multiple times in the linked list. That's wrong. + result.add(p); + } + } catch (Throwable t) { + //something bad happened during loading the project. + //log the problem, but allow the other projects to be load + //see issue #65900 + ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, t); + } + } + } + return result; + } + + /** + * A currated interface to {@link OpenProjectList}. + */ + sealed interface Callback permits OpenProjectList.LoadingCallback { + /** Called when computation of project opening is finished */ + public void updateGlobalState(List lazilyOpenedProjects, Project lazyMainProject, List recentTemplates, boolean checkFirstRun); + + /** Notifies a project that's about to be open */ + public void beginOpening(Project p); + + /** Notifies that a project has been opened. + * + * @param p the project to finish opening + * @return {@code true} if the project has successfully been opened, {@code false} if something failed} + */ + public boolean finishOpening(Project p); + + + } +} diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectsLogging.java b/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectsLogging.java new file mode 100644 index 000000000000..18e7c9290c80 --- /dev/null +++ b/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectsLogging.java @@ -0,0 +1,118 @@ +/* + * 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.util.HashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; +import org.netbeans.api.project.Project; +import org.openide.util.NbBundle; + +/** Logging utilities related to {@link OpenProjectList}. Some of these + * methods help with gathering telemetry data, which are not really processed + * anywhere anymore. + */ +final class OpenProjectsLogging { + static final Logger LOGGER = Logger.getLogger(OpenProjectList.class.getName()); + + static LogRecord[] createRecord(String msg, Project[] projects) { + if (projects.length == 0) { + return null; + } + var counts = new HashMap(); + for (Project p : projects) { + String n = p.getClass().getName(); + int[] cnt = counts.get(n); + if (cnt == null) { + cnt = new int[1]; + counts.put(n, cnt); + } + cnt[0]++; + } + Logger logger = Logger.getLogger("org.netbeans.ui.projects"); // NOI18N + LogRecord[] arr = new LogRecord[counts.size()]; + int i = 0; + for (Map.Entry entry : counts.entrySet()) { + LogRecord rec = new LogRecord(Level.CONFIG, msg); + rec.setParameters(new Object[]{entry.getKey(), afterLastDot(entry.getKey()), entry.getValue()[0]}); + rec.setLoggerName(logger.getName()); + rec.setResourceBundle(NbBundle.getBundle(OpenProjectList.class)); + rec.setResourceBundleName(OpenProjectList.class.getPackage().getName() + ".Bundle"); + arr[i++] = rec; + } + return arr; + } + + static LogRecord[] createRecordMetrics(String msg, Project[] projects) { + if (projects.length == 0) { + return null; + } + Logger logger = Logger.getLogger("org.netbeans.ui.metrics.projects"); // NOI18N + LogRecord[] arr = new LogRecord[projects.length]; + int i = 0; + for (Project p : projects) { + LogRecord rec = new LogRecord(Level.INFO, msg); + rec.setParameters(new Object[]{p.getClass().getName()}); + rec.setLoggerName(logger.getName()); + arr[i++] = rec; + } + return arr; + } + + private static String afterLastDot(String s) { + int index = s.lastIndexOf('.'); + if (index == -1) { + return s; + } + return s.substring(index + 1); + } + + static void log(LogRecord r) { + LOGGER.log(r); + } + + static void log(Level l, String msg, Object... params) { + LOGGER.log(l, msg, params); + } + + static void log(Level l, String msg, Throwable e) { + LOGGER.log(l, msg, e); + } + + static void log(LogRecord[] arr, String loggerName) { + if (arr == null) { + return; + } + Logger logger = Logger.getLogger(loggerName); // NOI18N + for (LogRecord r : arr) { + logger.log(r); + } + } + + static void logProjects(String message, Project[] projects) { + if (projects.length == 0) { + return; + } + for (Project p : projects) { + LOGGER.log(Level.FINER, "{0} {1}", new Object[]{message, p == null ? null : p.toString()}); + } + } +} diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/ProjectsRootNode.java b/ide/projectui/src/org/netbeans/modules/project/ui/ProjectsRootNode.java index 5878e621bc21..2d5c733a31f5 100644 --- a/ide/projectui/src/org/netbeans/modules/project/ui/ProjectsRootNode.java +++ b/ide/projectui/src/org/netbeans/modules/project/ui/ProjectsRootNode.java @@ -216,7 +216,7 @@ static void checkNoLazyNode(Children children) { } if (n.getLookup().lookup(LazyProject.class) != null) { - OpenProjectList.LOGGER.warning("LazyProjects remain visible"); + OpenProjectsLogging.LOGGER.warning("LazyProjects remain visible"); } } } @@ -591,7 +591,7 @@ private void replaceProject(Project newProj) { return; } } catch (IOException | IllegalArgumentException ex) { - OpenProjectList.LOGGER.log(Level.INFO, "No project for " + pair.fo, ex); // NOI18N + OpenProjectsLogging.LOGGER.log(Level.INFO, "No project for " + pair.fo, ex); // NOI18N } } @@ -600,14 +600,14 @@ private void replaceProject(Project newProj) { return; } - if (OpenProjectList.LOGGER.isLoggable(Level.FINER)) { - OpenProjectList.log(Level.FINER, "replacing for {0}", toStringForLog()); + if (OpenProjectsLogging.LOGGER.isLoggable(Level.FINER)) { + OpenProjectsLogging.log(Level.FINER, "replacing for {0}", toStringForLog()); } Project p = getLookup().lookup(Project.class); if (p == null) { - if (OpenProjectList.LOGGER.isLoggable(Level.FINE)) { - OpenProjectList.log(Level.FINE, "no project in lookup {0}", toStringForLog()); + if (OpenProjectsLogging.LOGGER.isLoggable(Level.FINE)) { + OpenProjectsLogging.log(Level.FINE, "no project in lookup {0}", toStringForLog()); } return; } @@ -619,39 +619,39 @@ private void replaceProject(Project newProj) { newProj, ch.rootKeys.createInfo(newProj, logicalView), null); - OpenProjectList.log(Level.FINER, "logical view {0}", n); + OpenProjectsLogging.log(Level.FINER, "logical view {0}", n); } else { Node[] arr = PhysicalView.createNodesForProject(newProj); - OpenProjectList.log(Level.FINER, "physical view {0}", Arrays.asList(arr)); + OpenProjectsLogging.log(Level.FINER, "physical view {0}", Arrays.asList(arr)); if (arr.length > 1) { pair.update(newProj); - OpenProjectList.log(Level.FINER, "refreshing for {0}", newProj); + OpenProjectsLogging.log(Level.FINER, "refreshing for {0}", newProj); ch.refresh(newProj); - OpenProjectList.log(Level.FINER, "refreshed for {0}", newProj); + OpenProjectsLogging.log(Level.FINER, "refreshed for {0}", newProj); return; } else if (arr.length == 1) { n = arr[0]; } else { - OpenProjectList.log(Level.WARNING, "newProject yields null node: " + newProj); + OpenProjectsLogging.log(Level.WARNING, "newProject yields null node: " + newProj); n = Node.EMPTY; } } - if (OpenProjectList.LOGGER.isLoggable(Level.FINER)) { - OpenProjectList.log(Level.FINER, "change original: {0}", n); - OpenProjectList.log(Level.FINER, "children before change original: {0}", getChildren()); - OpenProjectList.log(Level.FINER, "delegate children before change original: {0}", getOriginal().getChildren()); + if (OpenProjectsLogging.LOGGER.isLoggable(Level.FINER)) { + OpenProjectsLogging.log(Level.FINER, "change original: {0}", n); + OpenProjectsLogging.log(Level.FINER, "children before change original: {0}", getChildren()); + OpenProjectsLogging.log(Level.FINER, "delegate children before change original: {0}", getOriginal().getChildren()); } changeOriginal(n, true); - if (OpenProjectList.LOGGER.isLoggable(Level.FINER)) { - OpenProjectList.log(Level.FINER, "delegate after change original: {0}", getOriginal()); - OpenProjectList.log(Level.FINER, "name after change original: {0}", getName()); - OpenProjectList.log(Level.FINER, "children after change original: {0}", getChildren()); - OpenProjectList.log(Level.FINER, "delegate children after change original: {0}", getOriginal().getChildren()); + if (OpenProjectsLogging.LOGGER.isLoggable(Level.FINER)) { + OpenProjectsLogging.log(Level.FINER, "delegate after change original: {0}", getOriginal()); + OpenProjectsLogging.log(Level.FINER, "name after change original: {0}", getName()); + OpenProjectsLogging.log(Level.FINER, "children after change original: {0}", getChildren()); + OpenProjectsLogging.log(Level.FINER, "delegate children after change original: {0}", getOriginal().getChildren()); } BadgingLookup bl = (BadgingLookup) getLookup(); bl.setMyLookups(n.getLookup()); - if (OpenProjectList.LOGGER.isLoggable(Level.FINER)) { - OpenProjectList.log(Level.FINER, "done {0}", toStringForLog()); + if (OpenProjectsLogging.LOGGER.isLoggable(Level.FINER)) { + OpenProjectsLogging.log(Level.FINER, "done {0}", toStringForLog()); } setProjectFilesAsynch(); } else { @@ -661,8 +661,8 @@ private void replaceProject(Project newProj) { OpenProjectList.getDefault().close(new Project[] { pair.project() }, false); }); } - if (OpenProjectList.LOGGER.isLoggable(Level.FINER)) { - OpenProjectList.log(Level.FINER, "wrong directories. current: " + fo + if (OpenProjectsLogging.LOGGER.isLoggable(Level.FINER)) { + OpenProjectsLogging.log(Level.FINER, "wrong directories. current: " + fo + " new " + (newProj != null ? newProj.getProjectDirectory() : null)); } } From 4a83c82eea3f557ca64a8c6ff89424d518177a43 Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Wed, 7 Oct 2026 15:59:14 +0200 Subject: [PATCH 02/15] Making the ProjectsRootNodePhysicalViewTest more robust by holding projects with TestProjectOpenedHookImpl --- .../ui/ProjectsRootNodePhysicalViewTest.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/ide/projectui/test/unit/src/org/netbeans/modules/project/ui/ProjectsRootNodePhysicalViewTest.java b/ide/projectui/test/unit/src/org/netbeans/modules/project/ui/ProjectsRootNodePhysicalViewTest.java index b9578450653e..9ff592754ce1 100644 --- a/ide/projectui/test/unit/src/org/netbeans/modules/project/ui/ProjectsRootNodePhysicalViewTest.java +++ b/ide/projectui/test/unit/src/org/netbeans/modules/project/ui/ProjectsRootNodePhysicalViewTest.java @@ -32,7 +32,6 @@ import org.netbeans.api.project.ProjectManager; import org.netbeans.junit.MockServices; import org.netbeans.junit.NbTestCase; -import org.netbeans.junit.RandomlyFails; import org.netbeans.modules.project.ui.actions.TestSupport; import org.netbeans.modules.projectapi.nb.TimedWeakReference; import org.netbeans.spi.project.ui.LogicalViewProvider; @@ -55,7 +54,8 @@ * @author Jaroslav Tulach */ public class ProjectsRootNodePhysicalViewTest extends NbTestCase { - CountDownLatch down; + private final CountDownLatch down = new CountDownLatch(1); + private final List holdemAll = new ArrayList<>(); public ProjectsRootNodePhysicalViewTest(String testName) { super(testName); @@ -65,6 +65,11 @@ Lookup createLookup(TestSupport.TestProject project, Object instance) { return Lookups.fixed(instance, new LVP()); } + @Override + protected int timeOut() { + return 20_000; + } + @Override protected void setUp() throws Exception { clearWorkDir(); @@ -76,7 +81,6 @@ protected void setUp() throws Exception { FileObject workDir = FileUtil.toFileObject(getWorkDir()); assertNotNull(workDir); - down = new CountDownLatch(1); List list = new ArrayList(); List icons = new ArrayList(); @@ -90,6 +94,7 @@ protected void setUp() throws Exception { TestSupport.TestProject tmp = (TestSupport.TestProject)ProjectManager.getDefault ().findProject (prj); assertNotNull("Project found", tmp); tmp.setLookup(createLookup(tmp, new TestProjectOpenedHookImpl(down))); + holdemAll.add(tmp); } OpenProjectListSettings.getInstance().setOpenProjectsURLs(list); @@ -97,13 +102,13 @@ protected void setUp() throws Exception { OpenProjectListSettings.getInstance().setOpenProjectsIcons(icons); } - @RandomlyFails // NB-Core-Build #3939: "Can be garbage collected when closed" involving TimedWeakReference public void testBehaviourOfProjectsLogicNode() throws InterruptedException { Node n = doBehaviourOfProjectsNode(); + holdemAll.clear(); Project p = n.getLookup().lookup(Project.class); assertNotNull("Project is in the node", p); - + WeakReference ref = new WeakReference(p); p = null; From 210017bd3071b3cbc63813b7eb4e896a58065e56 Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Wed, 7 Oct 2026 16:30:09 +0200 Subject: [PATCH 03/15] OpenProjectList.openProjects future must wait, but then return the most up-to-date array of projects --- .../modules/project/ui/OpenProjectList.java | 36 +++++++++- .../project/ui/OpenProjectsLoading.java | 71 ++----------------- .../ui/OpenProjectListSetMainTest.java | 3 + 3 files changed, 43 insertions(+), 67 deletions(-) diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java b/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java index 01113d38b757..1e0e39e7d3f9 100644 --- a/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java +++ b/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java @@ -27,7 +27,6 @@ import java.io.IOException; import java.lang.ref.WeakReference; import java.net.URL; -import java.text.Collator; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -43,7 +42,10 @@ import java.util.Set; import java.util.StringTokenizer; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.LogRecord; @@ -204,7 +206,37 @@ static void preferredProject(final Project lazyP) { } public Future openProjectsAPI() { - return LOAD; + return new Future() { + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return false; + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public boolean isDone() { + return LOAD.isDone(); + } + + @Override + public Project[] get() throws InterruptedException, ExecutionException { + LOAD.waitFinished(0L); + return getOpenProjects(); + } + + @Override + public Project[] get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + if (LOAD.waitFinished(unit.toMillis(timeout))) { + return getOpenProjects(); + } else { + throw new TimeoutException(); + } + } + }; } final Project unwrapProject(Project wrap) { diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectsLoading.java b/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectsLoading.java index 4cdedc824488..8ef3468a2055 100644 --- a/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectsLoading.java +++ b/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectsLoading.java @@ -18,7 +18,6 @@ */ package org.netbeans.modules.project.ui; -import java.beans.PropertyChangeEvent; import java.net.URL; import java.util.ArrayList; import java.util.Collection; @@ -29,10 +28,6 @@ import java.util.LinkedList; import java.util.List; import java.util.Set; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; @@ -43,7 +38,6 @@ import org.netbeans.api.project.FileOwnerQuery; import org.netbeans.api.project.Project; import org.netbeans.api.project.ProjectManager; -import static org.netbeans.modules.project.ui.OpenProjectList.MUTEX; import org.netbeans.modules.project.ui.groups.Group; import org.openide.ErrorManager; import org.openide.filesystems.FileObject; @@ -57,10 +51,10 @@ import org.openide.util.Utilities; import org.openide.util.WeakListeners; -final class OpenProjectsLoading implements Runnable, LookupListener, Future { +final class OpenProjectsLoading implements Runnable, LookupListener { static final RequestProcessor RP = new RequestProcessor("Load Open Projects"); // NOI18N - final RequestProcessor.Task TASK = RP.create(this); - private int action; + private final RequestProcessor.Task TASK = RP.create(this); + private volatile int action; private final LinkedList toOpenProjects = new LinkedList<>(); private List lazilyOpenedProjects; private List recentTemplates; @@ -74,9 +68,9 @@ final class OpenProjectsLoading implements Runnable, LookupListener, Future() { - public @Override Project[] run() { - return lazilyOpenedProjects.toArray(new Project[0]); - } - }); - } - - - @Override - public Project[] get() throws InterruptedException, ExecutionException { - waitFinished(0); - try { - enteredGuard.lock(); - while (entered > 0) { - enteredZeroed.await(); - } - } finally { - enteredGuard.unlock(); - } - return getOpenProjects(); - } - - @Override - public Project[] get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { - long ms = unit.convert(timeout, TimeUnit.MILLISECONDS); - if (!waitFinished(timeout)) { - throw new TimeoutException(); - } - try { - enteredGuard.lock(); - if (entered > 0) { - if (!enteredZeroed.await(ms, TimeUnit.MILLISECONDS)) { - throw new TimeoutException(); - } - } - } finally { - enteredGuard.unlock(); - } - return getOpenProjects(); - } - private static Set urls2Projects(Collection urls) { Set result = new LinkedHashSet<>(); diff --git a/ide/projectui/test/unit/src/org/netbeans/modules/project/ui/OpenProjectListSetMainTest.java b/ide/projectui/test/unit/src/org/netbeans/modules/project/ui/OpenProjectListSetMainTest.java index a6d94c57d80a..4a7bd91e56d6 100644 --- a/ide/projectui/test/unit/src/org/netbeans/modules/project/ui/OpenProjectListSetMainTest.java +++ b/ide/projectui/test/unit/src/org/netbeans/modules/project/ui/OpenProjectListSetMainTest.java @@ -191,11 +191,13 @@ public TestProjectOpenedHookImpl(CountDownLatch toWaitOn) { this.toWaitOn = toWaitOn; } + @Override protected void projectClosed() { closed++; } Project[] arr; + @Override public void run() { try { arr = OpenProjects.getDefault().openProjects().get(50, TimeUnit.MILLISECONDS); @@ -208,6 +210,7 @@ public void run() { } } + @Override protected void projectOpened() { assertFalse("Running", OpenProjects.getDefault().openProjects().isDone()); // now verify that other threads do not see results from the Future From 91603029975bf58391e2bb1ae6b80070f1813383 Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Wed, 7 Oct 2026 16:55:02 +0200 Subject: [PATCH 04/15] Recent templates shall not be handled by OpenProjectsLoading --- .../org/netbeans/modules/project/ui/OpenProjectList.java | 4 +++- .../netbeans/modules/project/ui/OpenProjectsLoading.java | 8 ++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java b/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java index 1e0e39e7d3f9..2c16bf349d8c 100644 --- a/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java +++ b/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java @@ -1692,9 +1692,11 @@ private void checkModuleInfo(ModuleInfo info) { final class LoadingCallback implements OpenProjectsLoading.Callback { @Override - public void updateGlobalState(List lazilyOpenedProjects, Project lazyMainProject, List recentTemplates, boolean checkFirstRun) { + public void updateGlobalState(List lazilyOpenedProjects, Project lazyMainProject, boolean checkFirstRun) { assert MUTEX.isWriteAccess(); + List recentTemplates = new ArrayList(OpenProjectListSettings.getInstance().getRecentTemplates()); + openProjects = lazilyOpenedProjects; if (lazyMainProject != null) { mainProject = lazyMainProject; diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectsLoading.java b/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectsLoading.java index 8ef3468a2055..b5343ac7fa3a 100644 --- a/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectsLoading.java +++ b/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectsLoading.java @@ -57,7 +57,6 @@ final class OpenProjectsLoading implements Runnable, LookupListener { private volatile int action; private final LinkedList toOpenProjects = new LinkedList<>(); private List lazilyOpenedProjects; - private List recentTemplates; private Project lazyMainProject; private Lookup.Result currentFiles; private int entered; @@ -152,7 +151,7 @@ private void updateGlobalState() { @Override public Void run() { OpenProjectsLogging.log(Level.FINER, "openProjects changed: {0}", lazilyOpenedProjects); // NOI18N - outer.updateGlobalState(lazilyOpenedProjects, lazyMainProject, recentTemplates, checkFirstRun()); + outer.updateGlobalState(lazilyOpenedProjects, lazyMainProject, checkFirstRun()); OpenProjectsLogging.log(Level.FINER, "updateGlobalState, applied"); // NOI18N return null; } @@ -207,7 +206,6 @@ public Void run() { return null; } }); - recentTemplates = new ArrayList(OpenProjectListSettings.getInstance().getRecentTemplates()); final URL mainProjectURL = OpenProjectListSettings.getInstance().getMainProjectURL(); int max = OpenProjectList.MUTEX.writeAccess(new Mutex.Action() { @Override @@ -330,7 +328,7 @@ private static Set urls2Projects(Collection urls) { */ sealed interface Callback permits OpenProjectList.LoadingCallback { /** Called when computation of project opening is finished */ - public void updateGlobalState(List lazilyOpenedProjects, Project lazyMainProject, List recentTemplates, boolean checkFirstRun); + public void updateGlobalState(List lazilyOpenedProjects, Project lazyMainProject, boolean checkFirstRun); /** Notifies a project that's about to be open */ public void beginOpening(Project p); @@ -341,7 +339,5 @@ sealed interface Callback permits OpenProjectList.LoadingCallback { * @return {@code true} if the project has successfully been opened, {@code false} if something failed} */ public boolean finishOpening(Project p); - - } } From 48cd05977def7e88797ab2726c0e9f832386c279 Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Wed, 7 Oct 2026 17:19:56 +0200 Subject: [PATCH 05/15] No references to OpenProjectListSettings.getInstance() in OpenProjectsLoading --- .../modules/project/ui/OpenProjectList.java | 29 +++++++++++- .../project/ui/OpenProjectsLoading.java | 45 ++++++++++--------- 2 files changed, 52 insertions(+), 22 deletions(-) diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java b/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java index 2c16bf349d8c..03d817b394bb 100644 --- a/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java +++ b/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java @@ -49,6 +49,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.LogRecord; +import java.util.prefs.Preferences; import javax.swing.Icon; import org.netbeans.api.annotations.common.NonNull; import org.netbeans.api.annotations.common.NullAllowed; @@ -1690,9 +1691,21 @@ private void checkModuleInfo(ModuleInfo info) { } } + private static boolean checkFirstRun() { + Preferences prefs = OpenProjectListSettings.getInstance().getPreferences(); + String prefKey = "projectListVersion"; // NOI18N + String build = System.getProperty("netbeans.buildnumber", "0"); // NOI18N + if (!prefs.get(prefKey, "").equals(build)) { + prefs.put(prefKey, build); + return true; + } else { + return false; + } + } + final class LoadingCallback implements OpenProjectsLoading.Callback { @Override - public void updateGlobalState(List lazilyOpenedProjects, Project lazyMainProject, boolean checkFirstRun) { + public void updateGlobalState(List lazilyOpenedProjects, Project lazyMainProject) { assert MUTEX.isWriteAccess(); List recentTemplates = new ArrayList(OpenProjectListSettings.getInstance().getRecentTemplates()); @@ -1712,7 +1725,7 @@ public void updateGlobalState(List lazilyOpenedProjects, Project lazyMa // else PROPERTY_MAIN_PROJECT would be fired spuriously pchSupport.firePropertyChange(OpenProjectList.PROPERTY_MAIN_PROJECT, null, main); } - if (checkFirstRun && opened.length > 0) { + if (checkFirstRun() && opened.length > 0) { OpenProjectList.OPENING_RP.execute(() -> { for (Project p : opened) { Project del = p.getLookup().lookup(Project.class); @@ -1748,5 +1761,17 @@ public boolean finishOpening(Project p) { public void beginOpening(Project p) { addModuleInfo(p); } + + @Override + public List getOpenProjectsURLs() { + final boolean loadProjectsOnStart = "true".equals(Bundle.LOAD_PROJECTS_ON_START()); + List urls = loadProjectsOnStart ? OpenProjectListSettings.getInstance().getOpenProjectsURLs() : Collections.emptyList(); + return urls; + } + + @Override + public URL getMainProjectURL() { + return OpenProjectListSettings.getInstance().getMainProjectURL(); + } } } diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectsLoading.java b/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectsLoading.java index b5343ac7fa3a..3fb8cf77b731 100644 --- a/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectsLoading.java +++ b/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectsLoading.java @@ -21,7 +21,6 @@ import java.net.URL; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; @@ -33,7 +32,6 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; -import java.util.prefs.Preferences; import org.netbeans.api.progress.ProgressHandle; import org.netbeans.api.project.FileOwnerQuery; import org.netbeans.api.project.Project; @@ -51,10 +49,25 @@ import org.openide.util.Utilities; import org.openide.util.WeakListeners; +/** Helper class used while asynchronously loading projects. Primarily used + * when loading projects after startup. This class shall however be usable + * when switching list of open projects after opening a new {@link Group}. + *

+ *

How it Should Work?

+ * + * When there is a needed to change the list of opened projects, then let's + * instantiate this class and pass instance of {@link Callback} into its + * constructor. Then let the class do its work via {@link #run()}. + * When another request to open/modify projects is needed, throw this instance + * away and create new one. Repeat. + * + * + */ final class OpenProjectsLoading implements Runnable, LookupListener { static final RequestProcessor RP = new RequestProcessor("Load Open Projects"); // NOI18N private final RequestProcessor.Task TASK = RP.create(this); private volatile int action; + /** @GuardedBy("MUTEX.writeAccess") */ private final LinkedList toOpenProjects = new LinkedList<>(); private List lazilyOpenedProjects; private Project lazyMainProject; @@ -67,7 +80,7 @@ final class OpenProjectsLoading implements Runnable, LookupListener { @NbBundle.Messages(value = "CAP_Opening_Projects=Opening Projects") @SuppressWarnings(value = "LeakingThisInConstructor") - public OpenProjectsLoading(int action, Callback callback) { + OpenProjectsLoading(int action, Callback callback) { this.outer = callback; this.action = action; currentFiles = Utilities.actionsGlobalContext().lookupResult(FileObject.class); @@ -151,25 +164,13 @@ private void updateGlobalState() { @Override public Void run() { OpenProjectsLogging.log(Level.FINER, "openProjects changed: {0}", lazilyOpenedProjects); // NOI18N - outer.updateGlobalState(lazilyOpenedProjects, lazyMainProject, checkFirstRun()); + outer.updateGlobalState(lazilyOpenedProjects, lazyMainProject); OpenProjectsLogging.log(Level.FINER, "updateGlobalState, applied"); // NOI18N return null; } }); } - private boolean checkFirstRun() { - Preferences prefs = OpenProjectListSettings.getInstance().getPreferences(); - String prefKey = "projectListVersion"; // NOI18N - String build = System.getProperty("netbeans.buildnumber", "0"); // NOI18N - if (!prefs.get(prefKey, "").equals(build)) { - prefs.put(prefKey, build); - return true; - } else { - return false; - } - } - boolean closeBeforeOpen(final Project[] arr) { return OpenProjectList.MUTEX.writeAccess(new Mutex.Action() { @Override @@ -192,9 +193,9 @@ public Boolean run() { @NbBundle.Messages(value = {"#NOI18N", "LOAD_PROJECTS_ON_START=true"}) private void loadInBackground() { + assert lazilyOpenedProjects == null; lazilyOpenedProjects = new ArrayList<>(); - final boolean loadProjectsOnStart = "true".equals(Bundle.LOAD_PROJECTS_ON_START()); - List urls = loadProjectsOnStart ? OpenProjectListSettings.getInstance().getOpenProjectsURLs() : Collections.emptyList(); + List urls = outer.getOpenProjectsURLs(); final List initial = new ArrayList<>(); final Collection projects = urls2Projects(urls); OpenProjectList.MUTEX.writeAccess(new Mutex.Action() { @@ -206,7 +207,7 @@ public Void run() { return null; } }); - final URL mainProjectURL = OpenProjectListSettings.getInstance().getMainProjectURL(); + final URL mainProjectURL = outer.getMainProjectURL(); int max = OpenProjectList.MUTEX.writeAccess(new Mutex.Action() { @Override public Integer run() { @@ -328,7 +329,7 @@ private static Set urls2Projects(Collection urls) { */ sealed interface Callback permits OpenProjectList.LoadingCallback { /** Called when computation of project opening is finished */ - public void updateGlobalState(List lazilyOpenedProjects, Project lazyMainProject, boolean checkFirstRun); + public void updateGlobalState(List lazilyOpenedProjects, Project lazyMainProject); /** Notifies a project that's about to be open */ public void beginOpening(Project p); @@ -339,5 +340,9 @@ sealed interface Callback permits OpenProjectList.LoadingCallback { * @return {@code true} if the project has successfully been opened, {@code false} if something failed} */ public boolean finishOpening(Project p); + + public List getOpenProjectsURLs(); + + public URL getMainProjectURL(); } } From 20128091ed08cad382232639d399652271207567 Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Wed, 7 Oct 2026 18:03:13 +0200 Subject: [PATCH 06/15] Extracting communication interface to OpenProjectsLoading task --- .../modules/project/ui/OpenProjectList.java | 23 +++++++++++++++++-- .../project/ui/OpenProjectsLoading.java | 22 ++++++++++-------- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java b/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java index 03d817b394bb..345c16f3e73d 100644 --- a/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java +++ b/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java @@ -94,6 +94,7 @@ import org.openide.util.RequestProcessor; import org.openide.util.lookup.Lookups; import org.openide.util.lookup.ProxyLookup; +import org.openide.util.Utilities; import org.openide.windows.WindowManager; import static org.netbeans.modules.project.ui.Bundle.*; @@ -150,12 +151,12 @@ public static Comparator projectByPath() { private final NbProjectDeletionListener nbprojectDeleteListener = new NbProjectDeletionListener(); private final PropertyChangeListener infoListener; - private final OpenProjectsLoading LOAD; + private final Loading LOAD; private final ArrayList projectGroupSupport; private final AtomicBoolean groupChanging = new AtomicBoolean(false); OpenProjectList() { - LOAD = new OpenProjectsLoading(0, new LoadingCallback()); + LOAD = new OpenProjectsLoading(0, new LoadingCallback(), Utilities.actionsGlobalContext().lookupResult(FileObject.class)); openProjects = new ArrayList(); openProjectsModuleInfos = new HashMap>(); infoListener = new PropertyChangeListener() { @@ -1703,6 +1704,24 @@ private static boolean checkFirstRun() { } } + /** + * Interface to asynchronous manipulation with project list. + */ + sealed interface Loading extends Runnable permits OpenProjectsLoading { + public boolean isDone(); + + public boolean waitFinished(long toMillis); + + public void enter(); + + public boolean closeBeforeOpen(Project[] someProjects); + + public void exit(); + + public void preferredProject(Set singleton); + + } + final class LoadingCallback implements OpenProjectsLoading.Callback { @Override public void updateGlobalState(List lazilyOpenedProjects, Project lazyMainProject) { diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectsLoading.java b/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectsLoading.java index 3fb8cf77b731..663f4bd0cbfb 100644 --- a/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectsLoading.java +++ b/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectsLoading.java @@ -46,7 +46,6 @@ import org.openide.util.Mutex; import org.openide.util.NbBundle; import org.openide.util.RequestProcessor; -import org.openide.util.Utilities; import org.openide.util.WeakListeners; /** Helper class used while asynchronously loading projects. Primarily used @@ -63,7 +62,7 @@ * * */ -final class OpenProjectsLoading implements Runnable, LookupListener { +final class OpenProjectsLoading implements Runnable, LookupListener, OpenProjectList.Loading { static final RequestProcessor RP = new RequestProcessor("Load Open Projects"); // NOI18N private final RequestProcessor.Task TASK = RP.create(this); private volatile int action; @@ -80,15 +79,16 @@ final class OpenProjectsLoading implements Runnable, LookupListener { @NbBundle.Messages(value = "CAP_Opening_Projects=Opening Projects") @SuppressWarnings(value = "LeakingThisInConstructor") - OpenProjectsLoading(int action, Callback callback) { + OpenProjectsLoading(int action, Callback callback, Lookup.Result currentFiles) { this.outer = callback; this.action = action; - currentFiles = Utilities.actionsGlobalContext().lookupResult(FileObject.class); + this.currentFiles = currentFiles; currentFiles.addLookupListener(WeakListeners.create(LookupListener.class, this, currentFiles)); progress = ProgressHandle.createHandle(Bundle.CAP_Opening_Projects()); } - final boolean waitFinished(long timeout) { + @Override + public final boolean waitFinished(long timeout) { OpenProjectsLogging.log(Level.FINER, "waitFinished, action {0}", action); // NOI18N if (action == 0) { run(); @@ -143,7 +143,8 @@ public void run() { } } - final void preferredProject(final Set lazyPDirs) { + @Override + public final void preferredProject(final Set lazyPDirs) { OpenProjectList.MUTEX.writeAccess((Mutex.Action) () -> { for (Project p : new ArrayList(toOpenProjects)) { FileObject dir = p.getProjectDirectory(); @@ -171,7 +172,8 @@ public Void run() { }); } - boolean closeBeforeOpen(final Project[] arr) { + @Override + public boolean closeBeforeOpen(final Project[] arr) { return OpenProjectList.MUTEX.writeAccess(new Mutex.Action() { @Override public Boolean run() { @@ -278,7 +280,8 @@ public void resultChanged(LookupEvent ev) { resChangedTask.schedule(50); } - final void enter() { + @Override + public final void enter() { try { enteredGuard.lock(); entered++; @@ -287,7 +290,8 @@ final void enter() { } } - final void exit() { + @Override + public final void exit() { try { enteredGuard.lock(); if (--entered == 0) { From 57328229b5dfbb083b7c397b1e2597f24b3633a3 Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Thu, 8 Oct 2026 06:14:08 +0200 Subject: [PATCH 07/15] Creating a dedicated OpenProjectOperation --- .../modules/project/ui/OpenProjectList.java | 23 ++-------- .../project/ui/OpenProjectOperation.java | 42 +++++++++++++++++++ .../project/ui/OpenProjectsLoading.java | 5 +-- 3 files changed, 47 insertions(+), 23 deletions(-) create mode 100644 ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectOperation.java diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java b/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java index 345c16f3e73d..a9524c365fe5 100644 --- a/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java +++ b/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java @@ -151,12 +151,13 @@ public static Comparator projectByPath() { private final NbProjectDeletionListener nbprojectDeleteListener = new NbProjectDeletionListener(); private final PropertyChangeListener infoListener; - private final Loading LOAD; + private final OpenProjectOperation LOAD; private final ArrayList projectGroupSupport; private final AtomicBoolean groupChanging = new AtomicBoolean(false); OpenProjectList() { - LOAD = new OpenProjectsLoading(0, new LoadingCallback(), Utilities.actionsGlobalContext().lookupResult(FileObject.class)); + Lookup.Result selectedFiles = Utilities.actionsGlobalContext().lookupResult(FileObject.class); + LOAD = new OpenProjectsLoading(new LoadingCallback(), selectedFiles); openProjects = new ArrayList(); openProjectsModuleInfos = new HashMap>(); infoListener = new PropertyChangeListener() { @@ -1704,24 +1705,6 @@ private static boolean checkFirstRun() { } } - /** - * Interface to asynchronous manipulation with project list. - */ - sealed interface Loading extends Runnable permits OpenProjectsLoading { - public boolean isDone(); - - public boolean waitFinished(long toMillis); - - public void enter(); - - public boolean closeBeforeOpen(Project[] someProjects); - - public void exit(); - - public void preferredProject(Set singleton); - - } - final class LoadingCallback implements OpenProjectsLoading.Callback { @Override public void updateGlobalState(List lazilyOpenedProjects, Project lazyMainProject) { diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectOperation.java b/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectOperation.java new file mode 100644 index 000000000000..9ad4a5a68773 --- /dev/null +++ b/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectOperation.java @@ -0,0 +1,42 @@ +/* + * 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.util.Set; +import org.netbeans.api.project.Project; +import org.openide.filesystems.FileObject; + +/** + * Interface to asynchronous manipulation with project list. + */ +sealed interface OpenProjectOperation extends Runnable permits OpenProjectsLoading { + + public boolean isDone(); + + public boolean waitFinished(long toMillis); + + public void enter(); + + public boolean closeBeforeOpen(Project[] someProjects); + + public void exit(); + + public void preferredProject(Set singleton); + +} diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectsLoading.java b/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectsLoading.java index 663f4bd0cbfb..3101f2886416 100644 --- a/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectsLoading.java +++ b/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectsLoading.java @@ -62,7 +62,7 @@ * * */ -final class OpenProjectsLoading implements Runnable, LookupListener, OpenProjectList.Loading { +final class OpenProjectsLoading implements Runnable, LookupListener, OpenProjectOperation { static final RequestProcessor RP = new RequestProcessor("Load Open Projects"); // NOI18N private final RequestProcessor.Task TASK = RP.create(this); private volatile int action; @@ -79,9 +79,8 @@ final class OpenProjectsLoading implements Runnable, LookupListener, OpenProject @NbBundle.Messages(value = "CAP_Opening_Projects=Opening Projects") @SuppressWarnings(value = "LeakingThisInConstructor") - OpenProjectsLoading(int action, Callback callback, Lookup.Result currentFiles) { + OpenProjectsLoading(Callback callback, Lookup.Result currentFiles) { this.outer = callback; - this.action = action; this.currentFiles = currentFiles; currentFiles.addLookupListener(WeakListeners.create(LookupListener.class, this, currentFiles)); progress = ProgressHandle.createHandle(Bundle.CAP_Opening_Projects()); From 195dffb5b0530cbe0b107bcd772c3e9da37f11d5 Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Thu, 8 Oct 2026 06:54:55 +0200 Subject: [PATCH 08/15] Making OpenProjectListTest more reliable --- .../modules/project/ui/ProjectUtilities.java | 150 ++++++++++-------- .../project/ui/OpenProjectListNPUTest.java | 6 +- .../project/ui/OpenProjectListTest.java | 117 +++++++++----- 3 files changed, 163 insertions(+), 110 deletions(-) diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/ProjectUtilities.java b/ide/projectui/src/org/netbeans/modules/project/ui/ProjectUtilities.java index ddc3676fcb55..83006f43cd5d 100644 --- a/ide/projectui/src/org/netbeans/modules/project/ui/ProjectUtilities.java +++ b/ide/projectui/src/org/netbeans/modules/project/ui/ProjectUtilities.java @@ -30,6 +30,7 @@ import java.net.URL; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -42,6 +43,7 @@ import javax.swing.JFrame; import javax.swing.SwingUtilities; import org.netbeans.api.actions.Openable; +import org.netbeans.api.annotations.common.NonNull; import org.netbeans.api.project.Project; import org.netbeans.api.project.ProjectUtils; import org.netbeans.modules.project.ui.groups.Group; @@ -69,25 +71,41 @@ import org.w3c.dom.NodeList; import static org.netbeans.modules.project.ui.Bundle.*; +import org.openide.util.Lookup; /** The util methods for projectui module. * * @author Jiri Rechtacek */ public class ProjectUtilities { - + private static final Logger LOG = Logger.getLogger(ProjectUtilities.class.getName()); - + 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 () { + + + /** Support for unit testing. Making {@link OpenCloseProjectDocument} an + * injectable singleton with default value. + * + * @return the implementation to call + */ + @NonNull + private static OpenCloseProjectDocument openCloseProjectDocument() { + Collection all = Lookup.getDefault().lookupAll(OpenCloseProjectDocument.class); + if (all.isEmpty()) { + return OPEN_CLOSE_PROJECT_DOCUMENT_DEFAULT; + } else { + assert all.size() == 1; + return all.iterator().next(); + } + } + + private static final OpenCloseProjectDocument OPEN_CLOSE_PROJECT_DOCUMENT_DEFAULT = new OpenCloseProjectDocument () { @Override public boolean open (FileObject fo) { DataObject dobj; @@ -112,7 +130,7 @@ public boolean open (FileObject fo) { } return true; } - + @Override public Map> close(Project[] projects, boolean notifyUI) { Map> project2FilesMap = new LinkedHashMap<>(); @@ -125,7 +143,7 @@ public Map> close(Project[] projects, boolean notifyUI) { LOG.finer("Closing TCs"); List openedTC = getOpenedTCs(); - + for (TopComponent tc : openedTC) { DataObject dobj = tc.getLookup().lookup(DataObject.class); @@ -213,13 +231,17 @@ private List getOpenedTCs() { } }; + static void open(FileObject fo) { + openCloseProjectDocument().open(fo); + } + 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); @@ -238,9 +260,9 @@ public void run () { // fallback.. projNode = root.getChildren ().findChild( ProjectUtils.getInformation( p ).getName() ); } - + if ( projNode != null ) { - try { + try { ptLogial.getExplorerManager ().setSelectedNodes( new Node[] { projNode } ); ptLogial.expandNode( projNode ); // ptLogial.open (); @@ -251,9 +273,9 @@ public void run () { } } }); - + } - + /** 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. @@ -261,7 +283,7 @@ public void run () { * 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 () { @@ -295,7 +317,7 @@ public void run () { } }); } - + /** Makes the project tab visible * @param requestFocus if set to true the project tab will not only become visible but also * will gain focus @@ -308,7 +330,7 @@ public static void makeProjectTabVisible() { 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) @@ -318,7 +340,7 @@ public static void makeProjectTabVisible() { * @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 (\\)}.", @@ -333,7 +355,7 @@ public static String canUseFileName (FileObject targetFolder, String folderName, boolean allowSlash = false; boolean allowBackslash = false; int errorVariant = 0; - + if (allowFileSeparator) { if (File.separatorChar == '\\') { errorVariant = 3; @@ -343,23 +365,23 @@ public static String canUseFileName (FileObject targetFolder, String folderName, 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; + FileObject targetDir = (folderName != null) ? targetFolder.getFileObject(folderName) : targetFolder; if (targetDir != null) { if (targetDir.isValid()&& ! targetDir.canWrite ()) { return MSG_fs_is_readonly(); @@ -386,7 +408,7 @@ public static String canUseFileName (FileObject targetFolder, String folderName, if (targetFolder.getFileObject(relFileName.toString()) != null) { return MSG_file_already_exist(safeEncode(newObjectName + ext)); } - + // all ok return null; } @@ -400,24 +422,24 @@ private static String safeEncode(String text) { // #208432 return text; } } - - + + public static class WaitCursor implements Runnable { - + private boolean show; - + private WaitCursor( boolean show ) { this.show = show; } - - public static void show() { + + public static void show() { invoke( new WaitCursor( true ) ); } - + public static void hide() { - invoke( new WaitCursor( false ) ); + invoke( new WaitCursor( false ) ); } - + private static void invoke( WaitCursor wc ) { if (GraphicsEnvironment.isHeadless()) { return; @@ -429,21 +451,21 @@ private static void invoke( WaitCursor wc ) { SwingUtilities.invokeLater( wc ); } } - + @Override public void run() { - try { + 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. @@ -451,18 +473,18 @@ public void run() { * * @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> urls4project = OPEN_CLOSE_PROJECT_DOCUMENT_IMPL.close(projects, notifyUI); + + Map> urls4project = openCloseProjectDocument().close(projects, notifyUI); if (urls4project != null) { // store project's documents @@ -471,10 +493,10 @@ public static boolean closeAllDocuments(Project[] projects, boolean notifyUI, St storeProjectOpenFiles(entry.getKey(), new ArrayList<>(entry.getValue()), groupName); } } - + return urls4project != null; } - + public static void storeProjectOpenFiles(Project p, List urls, String groupName) { List openFileUrls = getOpenFilesUrls(p, groupName); @@ -516,20 +538,20 @@ public static void storeProjectOpenFiles(Project p, List urls, String gr aux.putConfigurationFragment (openFiles, false); } - + /** Opens the project's files read from the private project.xml file - * + * * @param p project */ public static Set openProjectFiles (Project p) { Group grp = Group.getActiveGroup(); return openProjectFiles(p, grp); } - + public static Set openProjectFiles (Project p, Group grp) { String groupName = grp == null ? null : grp.getName(); LOG.log(Level.FINE, "Trying to open files from {0}...", p); - + List urls = getOpenFilesUrls(p, groupName); Set toRet = new LinkedHashSet<>(); for (String url : urls) { @@ -545,21 +567,21 @@ public static Set openProjectFiles (Project p, Group grp) { LOG.log(Level.FINE, "Could not find {0}", url); continue; } - + //#109676 if (ProjectConvertors.getNonConvertorOwner(fo) != p) { LOG.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); + + openCloseProjectDocument().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) { @@ -569,18 +591,18 @@ public static Set openProjectFiles (Project p, Group grp) { // } return toRet; } - + // Returns an deduplicated list of opened file URLs in encounter order for this project and group. private static List 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.emptyList(); } 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); @@ -590,20 +612,20 @@ private static List getOpenFilesUrls(Project p, String groupName) { break; } } - + if (groupEl == null) { return Collections.emptyList(); } - + NodeList list = groupEl.getElementsByTagNameNS(OPEN_FILES_NS2, FILE_ELEMENT); Set set = new LinkedHashSet<>(); for (int i = 0; i < list.getLength (); i++) { String url = list.item(i).getChildNodes().item(0).getNodeValue(); set.add(url); - } + } return new ArrayList<>(set); } - + // called from (ide.branding) layer.xml on Favorites tab open public static URL getProjectsFolder() throws MalformedURLException { File projectsFolder = OpenProjectListSettings.getInstance().getProjectsFolder(true); @@ -613,13 +635,13 @@ public static URL getProjectsFolder() throws MalformedURLException { // 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> close(Project[] projects, boolean notifyUI); } - + } diff --git a/ide/projectui/test/unit/src/org/netbeans/modules/project/ui/OpenProjectListNPUTest.java b/ide/projectui/test/unit/src/org/netbeans/modules/project/ui/OpenProjectListNPUTest.java index 1b8ee767e81e..efb6786eb46b 100644 --- a/ide/projectui/test/unit/src/org/netbeans/modules/project/ui/OpenProjectListNPUTest.java +++ b/ide/projectui/test/unit/src/org/netbeans/modules/project/ui/OpenProjectListNPUTest.java @@ -87,9 +87,9 @@ protected void setUp () throws Exception { ((TestSupport.TestProject) project2).setLookup(Lookups.fixed(TestSupport.createAuxiliaryConfiguration())); // prepare set of open documents for both projects - ProjectUtilities.OPEN_CLOSE_PROJECT_DOCUMENT_IMPL.open (f1_1_open); - ProjectUtilities.OPEN_CLOSE_PROJECT_DOCUMENT_IMPL.open (f1_2_open); - ProjectUtilities.OPEN_CLOSE_PROJECT_DOCUMENT_IMPL.open (f2_1_open); + ProjectUtilities.open (f1_1_open); + ProjectUtilities.open (f1_2_open); + ProjectUtilities.open (f2_1_open); // close both projects with own open files OpenProjectList.getDefault().close(new Project[] {project1, project2}, false); diff --git a/ide/projectui/test/unit/src/org/netbeans/modules/project/ui/OpenProjectListTest.java b/ide/projectui/test/unit/src/org/netbeans/modules/project/ui/OpenProjectListTest.java index ffc848f9956f..3de25c2144b4 100644 --- a/ide/projectui/test/unit/src/org/netbeans/modules/project/ui/OpenProjectListTest.java +++ b/ide/projectui/test/unit/src/org/netbeans/modules/project/ui/OpenProjectListTest.java @@ -28,7 +28,6 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.TreeSet; @@ -56,6 +55,7 @@ import org.openide.filesystems.URLMapper; import org.openide.loaders.DataObject; import org.openide.loaders.DataObjectNotFoundException; +import org.openide.util.Lookup; import org.openide.util.RequestProcessor; import org.openide.util.lookup.Lookups; @@ -68,8 +68,9 @@ public class OpenProjectListTest extends NbTestCase { FileObject f1_1_open, f1_2_open, f1_3_close; FileObject f2_1_open; - Project project1, project2; - TestOpenCloseProjectDocument handler = new OpenProjectListTest.TestOpenCloseProjectDocument (); + TestSupport.TestProject project1; + TestSupport.TestProject project2; + TestOpenCloseProjectDocument handler; public OpenProjectListTest (String testName) { super (testName); @@ -80,12 +81,14 @@ protected Level logLevel() { return Level.FINE; } - protected @Override void setUp() throws Exception { + @Override + protected void setUp() throws Exception { super.setUp (); - MockServices.setServices(TestSupport.TestProjectFactory.class); + MockServices.setServices(TestSupport.TestProjectFactory.class, TestOpenCloseProjectDocument.class); clearWorkDir (); - ProjectUtilities.OPEN_CLOSE_PROJECT_DOCUMENT_IMPL = handler; + handler = Lookup.getDefault().lookup(TestOpenCloseProjectDocument.class); + assertNotNull("TestOpenCloseProjectDocument is registered", handler); FileObject workDir = FileUtil.toFileObject (getWorkDir ()); @@ -94,20 +97,20 @@ protected Level logLevel() { f1_2_open = p1.createData("f1_2.java"); f1_3_close = p1.createData("f1_3.java"); - project1 = ProjectManager.getDefault ().findProject (p1); - ((TestSupport.TestProject) project1).setLookup (Lookups.singleton (TestSupport.createAuxiliaryConfiguration ())); + project1 = (TestSupport.TestProject) ProjectManager.getDefault ().findProject (p1); + project1.setLookup (Lookups.singleton (TestSupport.createAuxiliaryConfiguration ())); FileObject p2 = TestSupport.createTestProject (workDir, "project2"); f2_1_open = p2.createData ("f2_1.java"); // project2 depends on projects1 - project2 = ProjectManager.getDefault ().findProject (p2); - ((TestSupport.TestProject) project2).setLookup(Lookups.fixed(TestSupport.createAuxiliaryConfiguration(), new MySubprojectProvider(project1))); + project2 = (TestSupport.TestProject) ProjectManager.getDefault ().findProject (p2); + project2.setLookup(Lookups.fixed(TestSupport.createAuxiliaryConfiguration(), new MySubprojectProvider(project1))); // prepare set of open documents for both projects - ProjectUtilities.OPEN_CLOSE_PROJECT_DOCUMENT_IMPL.open (f1_1_open); - ProjectUtilities.OPEN_CLOSE_PROJECT_DOCUMENT_IMPL.open (f1_2_open); - ProjectUtilities.OPEN_CLOSE_PROJECT_DOCUMENT_IMPL.open (f2_1_open); + ProjectUtilities.open (f1_1_open); + ProjectUtilities.open (f1_2_open); + ProjectUtilities.open (f2_1_open); OpenProjectList.getDefault().close(OpenProjectList.getDefault().getOpenProjects(), false); } @@ -123,8 +126,8 @@ public void testOpen () throws Exception { fail("There should be TestProject\n" + log.toString()); } - assertTrue ("Document f1_1_open is loaded.", handler.openFiles.contains (f1_1_open.toURL ().toExternalForm ())); - assertTrue ("Document f1_2_open is loaded.", handler.openFiles.contains (f1_2_open.toURL ().toExternalForm ())); + handler.assertOpened("Document f1_1_open is loaded.",f1_1_open); + handler.assertOpened("Document f1_2_open is loaded.", f1_2_open); /* XXX always fails; what was this testing? assertFalse ("Document f2_1_open isn't loaded.", handler.openFiles.contains (f2_1_open.getURL ().toExternalForm ())); */ @@ -165,8 +168,8 @@ public void testClose () throws Exception { if (!m.find()) { fail("There should be TestProject\n" + log); } - assertFalse ("Document f1_1_open isn't loaded.", handler.openFiles.contains (f1_1_open.toURL ().toExternalForm ())); - assertFalse ("Document f1_2_open isn't loaded.", handler.openFiles.contains (f1_2_open.toURL ().toExternalForm ())); + handler.assertNotOpened("Document f1_1_open is loaded.",f1_1_open); + handler.assertNotOpened("Document f1_2_open is loaded.", f1_2_open); /* XXX fails, see above assertFalse ("Document f2_1_open isn't loaded.", handler.openFiles.contains (f2_1_open.getURL ().toExternalForm ())); */ @@ -175,16 +178,16 @@ public void testClose () throws Exception { OpenProjectList.getDefault ().open (project2); // close all project1's documents - handler.openFiles.remove (f1_1_open.toURL ().toExternalForm ()); - handler.openFiles.remove (f1_2_open.toURL ().toExternalForm ()); + handler.removeOpenFile(f1_1_open); + handler.removeOpenFile(f1_2_open); ProjectUtilities.closeAllDocuments(new Project[] {project1}, false, null); OpenProjectList.getDefault().close(new Project[] {project1}, false, null); OpenProjectList.getDefault ().open (project1); - assertFalse ("Document f1_1_open isn't loaded.", handler.openFiles.contains (f1_1_open.toURL ().toExternalForm ())); - assertFalse ("Document f1_2_open isn't loaded.", handler.openFiles.contains (f1_2_open.toURL ().toExternalForm ())); - assertTrue ("Document f2_1_open is still loaded.", handler.openFiles.contains (f2_1_open.toURL ().toExternalForm ())); + handler.assertNotOpened("Document f1_1_open isn't loaded", f1_1_open); + handler.assertNotOpened("Document f1_2_open isn't loaded.", f1_2_open); + handler.assertOpened("Document f2_1_open is still loaded.",f1_1_open); } public void testSerialize() throws Exception { @@ -221,9 +224,9 @@ public void testOpenDependingProject () throws Exception { fail("There should be TestProject\n" + log); } - assertTrue ("Document f1_1_open is loaded.", handler.openFiles.contains (f1_1_open.toURL ().toExternalForm ())); - assertTrue ("Document f1_2_open is loaded.", handler.openFiles.contains (f1_2_open.toURL ().toExternalForm ())); - assertTrue ("Document f2_1_open is loaded.", handler.openFiles.contains (f2_1_open.toURL ().toExternalForm ())); + handler.assertOpened("Document f1_1_open is loaded.", f1_1_open); + handler.assertOpened("Document f1_2_open is loaded.", f1_2_open); + handler.assertOpened("Document f2_1_open is loaded.", f2_1_open); } public void testCloseProjectWithoutOpenDocuments () throws Exception { @@ -232,9 +235,8 @@ public void testCloseProjectWithoutOpenDocuments () throws Exception { assertFalse ("Project1 isn't opened.", OpenProjectList.getDefault ().isOpen (project1)); assertTrue ("Project2 is opened.", OpenProjectList.getDefault ().isOpen (project2)); - handler.openFiles.remove (f2_1_open.toURL ().toExternalForm ()); - - assertFalse ("Document f2_1_open isn't loaded.", handler.openFiles.contains (f2_1_open.toURL ().toExternalForm ())); + handler.removeOpenFile(f2_1_open); + handler.assertNotOpened("Document f2_1_open isn't loaded.", f2_1_open); ProjectUtilities.closeAllDocuments(new Project[] {project2}, false, null); OpenProjectList.getDefault().close(new Project[] {project2}, false); @@ -243,7 +245,7 @@ public void testCloseProjectWithoutOpenDocuments () throws Exception { } public void testProjectOpenedClosed() throws Exception { - ((TestSupport.TestProject) project1).setLookup(Lookups.fixed(new Object[] { + project1.setLookup(Lookups.fixed(new Object[] { new TestProjectOpenedHookImpl(), new TestProjectOpenedHookImpl(), })); @@ -405,26 +407,24 @@ public void run() { } } }); - ((TestSupport.TestProject) project1).setLookup(Lookups.singleton(poh)); + project1.setLookup(Lookups.singleton(poh)); OpenProjectList.getDefault().open(project1); - Future becomesProjects = OpenProjectList.getDefault().openProjectsAPI(); - Project[] projects = becomesProjects.get(); - assertTrue("Open done", becomesProjects.isDone()); //NOI18N + Future withOneProject = OpenProjectList.getDefault().openProjectsAPI(); + Project[] withOneArr = withOneProject.get(); + assertTrue("Open done", withOneProject.isDone()); //NOI18N assertEquals("projectOpened called", 1, poh.opened.get()); //NOI18N assertEquals("No projectClosed called", 0, poh.closed.get()); //NOI18N - assertEquals("One project opened", 1, projects.length); //NOI18N + assertEquals("One project opened", 1, withOneArr.length); //NOI18N OpenProjectList.getDefault().close(new Project[] {project1}, false); assertEquals("no projectClosed called yet", 0, poh.closed.get()); //NOI18N - becomesProjects = OpenProjectList.getDefault().openProjectsAPI(); + Future becomesProjects = OpenProjectList.getDefault().openProjectsAPI(); assertFalse("Close not yet done", becomesProjects.isDone()); //NOI18N barrier.countDown(); - projects = becomesProjects.get(); + Project[] projects = becomesProjects.get(); assertTrue("Close done", becomesProjects.isDone()); //NOI18N assertEquals("projectClosed called", 1, poh.closed.get()); //NOI18N assertEquals("No projects", 0, projects.length); //NOI18N - - } // helper code @@ -443,10 +443,11 @@ public void addChangeListener (javax.swing.event.ChangeListener changeListener) } - private static class TestOpenCloseProjectDocument implements ProjectUtilities.OpenCloseProjectDocument { - public Set openFiles = new HashSet(); - public Map> urls4project = new HashMap>(); + public static final class TestOpenCloseProjectDocument implements ProjectUtilities.OpenCloseProjectDocument { + private final Map openFiles = new HashMap<>(); + private Map> urls4project = new HashMap<>(); + @Override public boolean open (FileObject fo) { Project owner = FileOwnerQuery.getOwner (fo); if (!urls4project.containsKey (owner)) { @@ -459,19 +460,20 @@ public boolean open (FileObject fo) { dobj = DataObject.find (fo); url = dobj.getPrimaryFile ().toURL (); urls4project.get(owner).add(url.toExternalForm()); - openFiles.add (fo.toURL ().toExternalForm ()); + openFiles.put(fo.toURL ().toExternalForm (), new Exception("Opened at")); } catch (DataObjectNotFoundException donfe) { fail ("DataObjectNotFoundException on " + fo); } return true; } + @Override public Map> close(Project[] projects, boolean notifyUI) { for (int i = 0; i < projects.length; i++) { Set projectOpenFiles = urls4project.get(projects [i]); if (projectOpenFiles != null) { - projectOpenFiles.retainAll (openFiles); + projectOpenFiles.retainAll (openFiles.keySet()); urls4project.put (projects [i], projectOpenFiles); for (String url : projectOpenFiles) { FileObject fo = null; @@ -487,6 +489,35 @@ public Map> close(Project[] projects, boolean notifyUI) { return urls4project; } + + void assertOpened(String msg, FileObject fo) throws Exception { + assertFalse("Not on the thread", OpenProjectList.OPENING_RP.isRequestProcessorThread()); + OpenProjectList.OPENING_RP.post(() -> {}).waitFinished(); + Exception who = openFiles.get(fo.toURL ().toExternalForm ()); + for (int cnt = 0; cnt < 10; cnt++) { + if (who != null) { + if (cnt > 0) { + throw new AssertionError("TOO LATE: " + msg); + } + break; + } + Thread.sleep(50); + if (cnt == 10) { + assertNotNull(msg + ":" + openFiles, who); + } + } + } + void assertNotOpened(String msg, FileObject fo) { + Exception who = openFiles.get(fo.toURL ().toExternalForm ()); + if (who != null) { + throw new AssertionError(msg, who); + } + } + + void removeOpenFile(FileObject fo) { + var previousValue = openFiles.remove(fo.toURL().toExternalForm()); + assertNotNull("There should be previous value for " + fo); + } } private static class TestProjectOpenedHookImpl extends ProjectOpenedHook From cd283457bb19256f2b9d6694f9287211c78a5947 Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Thu, 8 Oct 2026 07:46:00 +0200 Subject: [PATCH 09/15] Request projects to open when creating OpenProjectsLoading --- .../modules/project/ui/OpenProjectList.java | 29 +++++++------------ .../project/ui/OpenProjectsLoading.java | 12 ++++---- 2 files changed, 16 insertions(+), 25 deletions(-) diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java b/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java index a9524c365fe5..70c7469804dc 100644 --- a/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java +++ b/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java @@ -157,8 +157,11 @@ public static Comparator projectByPath() { OpenProjectList() { Lookup.Result selectedFiles = Utilities.actionsGlobalContext().lookupResult(FileObject.class); - LOAD = new OpenProjectsLoading(new LoadingCallback(), selectedFiles); - openProjects = new ArrayList(); + boolean loadProjectsOnStart = "true".equals(Bundle.LOAD_PROJECTS_ON_START()); + List urls = loadProjectsOnStart ? OpenProjectListSettings.getInstance().getOpenProjectsURLs() : Collections.emptyList(); + URL main = OpenProjectListSettings.getInstance().getMainProjectURL(); + LOAD = new OpenProjectsLoading(new LoadingCallback(), selectedFiles, urls, main); + openProjects = loadProjectList(); openProjectsModuleInfos = new HashMap>(); infoListener = new PropertyChangeListener() { @Override @@ -170,6 +173,7 @@ public void propertyChange(PropertyChangeEvent evn) { }; pchSupport = new PropertyChangeSupport( this ); recentProjects = new RecentProjectList(10); // #47134 + recentProjects.load(); recentTemplates = new ArrayList(); projectGroupSupport = new ArrayList(); } @@ -180,15 +184,16 @@ public void propertyChange(PropertyChangeEvent evn) { public static OpenProjectList getDefault() { return MUTEX.readAccess(new Mutex.Action() { public @Override OpenProjectList run() { + boolean newlyCreated = false; synchronized (OpenProjectList.class) { // must be read access, but must not run concurrently if (INSTANCE == null) { + newlyCreated = true; INSTANCE = new OpenProjectList(); - INSTANCE.openProjects = loadProjectList(); - // Load recent project list - INSTANCE.recentProjects.load(); - WindowManager.getDefault().invokeWhenUIReady(INSTANCE.LOAD); } } + if (newlyCreated) { + WindowManager.getDefault().invokeWhenUIReady(INSTANCE.LOAD); + } return INSTANCE; } }); @@ -1763,17 +1768,5 @@ public boolean finishOpening(Project p) { public void beginOpening(Project p) { addModuleInfo(p); } - - @Override - public List getOpenProjectsURLs() { - final boolean loadProjectsOnStart = "true".equals(Bundle.LOAD_PROJECTS_ON_START()); - List urls = loadProjectsOnStart ? OpenProjectListSettings.getInstance().getOpenProjectsURLs() : Collections.emptyList(); - return urls; - } - - @Override - public URL getMainProjectURL() { - return OpenProjectListSettings.getInstance().getMainProjectURL(); - } } } diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectsLoading.java b/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectsLoading.java index 3101f2886416..4ca71e82477b 100644 --- a/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectsLoading.java +++ b/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectsLoading.java @@ -76,12 +76,16 @@ final class OpenProjectsLoading implements Runnable, LookupListener, OpenProject private final Condition enteredZeroed = enteredGuard.newCondition(); private final ProgressHandle progress; private final Callback outer; + private final List urls; + private final URL mainProjectURL; @NbBundle.Messages(value = "CAP_Opening_Projects=Opening Projects") @SuppressWarnings(value = "LeakingThisInConstructor") - OpenProjectsLoading(Callback callback, Lookup.Result currentFiles) { + OpenProjectsLoading(Callback callback, Lookup.Result currentFiles, List prjUrls, URL mainPrj) { this.outer = callback; this.currentFiles = currentFiles; + this.urls = prjUrls; + this.mainProjectURL = mainPrj; currentFiles.addLookupListener(WeakListeners.create(LookupListener.class, this, currentFiles)); progress = ProgressHandle.createHandle(Bundle.CAP_Opening_Projects()); } @@ -196,7 +200,6 @@ public Boolean run() { private void loadInBackground() { assert lazilyOpenedProjects == null; lazilyOpenedProjects = new ArrayList<>(); - List urls = outer.getOpenProjectsURLs(); final List initial = new ArrayList<>(); final Collection projects = urls2Projects(urls); OpenProjectList.MUTEX.writeAccess(new Mutex.Action() { @@ -208,7 +211,6 @@ public Void run() { return null; } }); - final URL mainProjectURL = outer.getMainProjectURL(); int max = OpenProjectList.MUTEX.writeAccess(new Mutex.Action() { @Override public Integer run() { @@ -343,9 +345,5 @@ sealed interface Callback permits OpenProjectList.LoadingCallback { * @return {@code true} if the project has successfully been opened, {@code false} if something failed} */ public boolean finishOpening(Project p); - - public List getOpenProjectsURLs(); - - public URL getMainProjectURL(); } } From b2fcfdecf5918e98221fbff53c2adac21e4d278b Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Thu, 8 Oct 2026 08:31:08 +0200 Subject: [PATCH 10/15] Make sure the erasured type is Future --- .../project/ui/TrampolineSignatureTest.java | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 ide/projectuiapi.base/test/unit/src/org/netbeans/modules/project/ui/TrampolineSignatureTest.java diff --git a/ide/projectuiapi.base/test/unit/src/org/netbeans/modules/project/ui/TrampolineSignatureTest.java b/ide/projectuiapi.base/test/unit/src/org/netbeans/modules/project/ui/TrampolineSignatureTest.java new file mode 100644 index 000000000000..02cb00843a80 --- /dev/null +++ b/ide/projectuiapi.base/test/unit/src/org/netbeans/modules/project/ui/TrampolineSignatureTest.java @@ -0,0 +1,36 @@ +/* + * 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.lang.reflect.Method; +import java.util.concurrent.Future; +import org.netbeans.junit.NbTestCase; +import org.netbeans.modules.project.uiapi.OpenProjectsTrampoline; + +public class TrampolineSignatureTest extends NbTestCase { + + public TrampolineSignatureTest(String name) { + super(name); + } + + public void testErasuredSignatureIsFuture() throws Exception { + Method m = OpenProjectsTrampoline.class.getMethod("openProjectsAPI"); + assertEquals("Return type is Future", Future.class, m.getReturnType()); + } +} From 0674e5c9fc6b1321cf2034d41e8bbd8d1dc10caa Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Thu, 8 Oct 2026 08:31:35 +0200 Subject: [PATCH 11/15] Allow replacing projects --- .../netbeans/modules/project/ui/ExtIcon.java | 3 +- .../modules/project/ui/OpenProjectList.java | 42 ++++++++++++++----- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/ExtIcon.java b/ide/projectui/src/org/netbeans/modules/project/ui/ExtIcon.java index e832ad8388aa..85239cb94e39 100644 --- a/ide/projectui/src/org/netbeans/modules/project/ui/ExtIcon.java +++ b/ide/projectui/src/org/netbeans/modules/project/ui/ExtIcon.java @@ -28,14 +28,13 @@ import javax.swing.Icon; import javax.swing.ImageIcon; import org.openide.util.ImageUtilities; -import org.openide.util.Utilities; /** * Class for persisting icons * @author Milan Kubec, mkleint */ -public class ExtIcon { +public final class ExtIcon { Icon icon; diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java b/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java index 70c7469804dc..224852b206fd 100644 --- a/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java +++ b/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java @@ -151,17 +151,15 @@ public static Comparator projectByPath() { private final NbProjectDeletionListener nbprojectDeleteListener = new NbProjectDeletionListener(); private final PropertyChangeListener infoListener; - private final OpenProjectOperation LOAD; + private volatile OpenProjectOperation LOAD; private final ArrayList projectGroupSupport; private final AtomicBoolean groupChanging = new AtomicBoolean(false); OpenProjectList() { - Lookup.Result selectedFiles = Utilities.actionsGlobalContext().lookupResult(FileObject.class); - boolean loadProjectsOnStart = "true".equals(Bundle.LOAD_PROJECTS_ON_START()); - List urls = loadProjectsOnStart ? OpenProjectListSettings.getInstance().getOpenProjectsURLs() : Collections.emptyList(); URL main = OpenProjectListSettings.getInstance().getMainProjectURL(); - LOAD = new OpenProjectsLoading(new LoadingCallback(), selectedFiles, urls, main); - openProjects = loadProjectList(); + pchSupport = new PropertyChangeSupport( this ); + recentTemplates = new ArrayList(); + projectGroupSupport = new ArrayList(); openProjectsModuleInfos = new HashMap>(); infoListener = new PropertyChangeListener() { @Override @@ -171,11 +169,9 @@ public void propertyChange(PropertyChangeEvent evn) { } } }; - pchSupport = new PropertyChangeSupport( this ); + replaceProjectsImpl(loadProjectList(), main); recentProjects = new RecentProjectList(10); // #47134 recentProjects.load(); - recentTemplates = new ArrayList(); - projectGroupSupport = new ArrayList(); } @@ -607,6 +603,24 @@ public Void run() { } } + final void replaceProjects(List projects, URL mainProject) { + MUTEX.writeAccess(() -> { + replaceProjectsImpl(projects, mainProject); + }); + } + + private final void replaceProjectsImpl(List projects, URL mainProject) { + if (openProjects != null) { + close(openProjects.toArray(Project[]::new), false); + } + openProjects = new ArrayList<>(projects); + pchSupport.firePropertyChange(PROPERTY_WILL_OPEN_PROJECTS, null, openProjects.toArray(Project[]::new)); + Lookup.Result selectedFiles = Utilities.actionsGlobalContext().lookupResult(FileObject.class); + var urls = projects.stream().map(p -> p.url).toList(); + var load = new OpenProjectsLoading(new LoadingCallback(), selectedFiles, urls, mainProject); + LOAD = load; + } + public void close( Project someProjects[], boolean notifyUI) { Group act = Group.getActiveGroup(); close(someProjects, notifyUI, act != null ? act.getName() : null); @@ -1075,12 +1089,16 @@ private boolean doOpenProject(final @NonNull Project p) { return recentProjectsChanged; } - private static List loadProjectList() { + private static List loadProjectList() { assert MUTEX.isReadAccess() || MUTEX.isWriteAccess(); + boolean loadProjectsOnStart = "true".equals(Bundle.LOAD_PROJECTS_ON_START()); + if (!loadProjectsOnStart) { + return Collections.emptyList(); + } List URLs = OpenProjectListSettings.getInstance().getOpenProjectsURLs(); List names = OpenProjectListSettings.getInstance().getOpenProjectsDisplayNames(); List icons = OpenProjectListSettings.getInstance().getOpenProjectsIcons(); - List projects = new ArrayList(); + List projects = new ArrayList<>(); Iterator urlIt = URLs.iterator(); Iterator namesIt = names.iterator(); @@ -1710,6 +1728,8 @@ private static boolean checkFirstRun() { } } + + final class LoadingCallback implements OpenProjectsLoading.Callback { @Override public void updateGlobalState(List lazilyOpenedProjects, Project lazyMainProject) { From 89d3f904e70227ccbfb7a6290ba08ba43dd7d96c Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Thu, 8 Oct 2026 09:56:04 +0200 Subject: [PATCH 12/15] Allow multiple calls into replaceProjects --- .../netbeans/modules/project/ui/LazyProject.java | 6 ++++++ .../modules/project/ui/OpenProjectList.java | 13 ++++--------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/LazyProject.java b/ide/projectui/src/org/netbeans/modules/project/ui/LazyProject.java index ed3cfac70012..3dd61eda07f1 100644 --- a/ide/projectui/src/org/netbeans/modules/project/ui/LazyProject.java +++ b/ide/projectui/src/org/netbeans/modules/project/ui/LazyProject.java @@ -63,6 +63,12 @@ final class LazyProject implements this.icon = icon; } + static LazyProject forUrl(URL url) { + var slash = url.toString().lastIndexOf('/'); + var dn = url.toString().substring(slash + 1); + return new LazyProject(url, dn, new ExtIcon()); + } + @Override public FileObject getProjectDirectory() { if (fo != null) { diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java b/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java index 224852b206fd..b6421785d894 100644 --- a/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java +++ b/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java @@ -180,16 +180,11 @@ public void propertyChange(PropertyChangeEvent evn) { public static OpenProjectList getDefault() { return MUTEX.readAccess(new Mutex.Action() { public @Override OpenProjectList run() { - boolean newlyCreated = false; synchronized (OpenProjectList.class) { // must be read access, but must not run concurrently if (INSTANCE == null) { - newlyCreated = true; INSTANCE = new OpenProjectList(); } } - if (newlyCreated) { - WindowManager.getDefault().invokeWhenUIReady(INSTANCE.LOAD); - } return INSTANCE; } }); @@ -603,22 +598,22 @@ public Void run() { } } - final void replaceProjects(List projects, URL mainProject) { + public final void replaceProjects(Collection urls, URL mainProject) { + close(openProjects.toArray(Project[]::new), false); + List projects = urls.stream().map(LazyProject::forUrl).toList(); MUTEX.writeAccess(() -> { replaceProjectsImpl(projects, mainProject); }); } private final void replaceProjectsImpl(List projects, URL mainProject) { - if (openProjects != null) { - close(openProjects.toArray(Project[]::new), false); - } openProjects = new ArrayList<>(projects); pchSupport.firePropertyChange(PROPERTY_WILL_OPEN_PROJECTS, null, openProjects.toArray(Project[]::new)); Lookup.Result selectedFiles = Utilities.actionsGlobalContext().lookupResult(FileObject.class); var urls = projects.stream().map(p -> p.url).toList(); var load = new OpenProjectsLoading(new LoadingCallback(), selectedFiles, urls, mainProject); LOAD = load; + WindowManager.getDefault().invokeWhenUIReady(load); } public void close( Project someProjects[], boolean notifyUI) { From db6c7d9b08f28f1fcbab3dc76879d5b7b8fe79bf Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Thu, 8 Oct 2026 10:03:28 +0200 Subject: [PATCH 13/15] Describing intended implementation --- .../modules/project/ui/OpenProjectList.java | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java b/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java index b6421785d894..c10880c9dcaf 100644 --- a/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java +++ b/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java @@ -100,8 +100,18 @@ import static org.netbeans.modules.project.ui.Bundle.*; /** - * List of projects open in the GUI. - * @author Petr Hrebejk + * List of projects open in the GUI. The idea behind this class is to operated + * as a proxy to various {@link OpenProjectOperation} implementation. Each + * client API method of this class creates an operation and schedules it for + * execution. This class also holds the latest finished and latest pending + * operation: + *
    + *
  • latest finished returns current state of projects + *
  • latest pending can be awaited for via a {@link Future} + *
+ * Operations are processed one by one in a dedicated single throughput + * request processor. Once a pending operation is finished, it replaces the + * previous finished, notifies changes, and so on, so on. */ public final class OpenProjectList { /** @@ -110,7 +120,7 @@ public final class OpenProjectList { * otherwise we get a deadlock fairly fast */ static final Mutex MUTEX = new Mutex(); - + public static Comparator projectByPath() { return new ProjectByPathComparator(); } From 5a27f5956088f2d39fa8fc3d07313511d0ddb5f7 Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Thu, 8 Oct 2026 10:07:05 +0200 Subject: [PATCH 14/15] Moving comparator to its own class --- .../modules/project/ui/OpenProjectList.java | 19 -------- .../project/ui/ProjectByPathComparator.java | 47 +++++++++++++++++++ .../modules/project/ui/ProjectsRootKeys.java | 2 +- .../project/ui/TemplateChooserPanelGUI.java | 2 +- .../project/ui/actions/SetMainProject.java | 3 +- 5 files changed, 51 insertions(+), 22 deletions(-) create mode 100644 ide/projectui/src/org/netbeans/modules/project/ui/ProjectByPathComparator.java diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java b/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java index c10880c9dcaf..da2617e1a32b 100644 --- a/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java +++ b/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java @@ -121,10 +121,6 @@ public final class OpenProjectList { */ static final Mutex MUTEX = new Mutex(); - public static Comparator projectByPath() { - return new ProjectByPathComparator(); - } - // Property names public static final String PROPERTY_OPEN_PROJECTS = "OpenProjects"; public static final String PROPERTY_WILL_OPEN_PROJECTS = "willOpenProjects"; // NOI18N @@ -1606,21 +1602,6 @@ public URL getURL() { } - private static class ProjectByPathComparator implements Comparator { - @Override - public int compare(Project p1, Project p2) { - if (p1 == null && p2 == null) { - return 0; - } - if (p1 == null) { - return -1; - } - if (p2 == null) { - return 1; - } - return p1.getProjectDirectory().getPath().compareTo(p2.getProjectDirectory().getPath()); - } - } private final class NbProjectDeletionListener extends FileChangeAdapter { diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/ProjectByPathComparator.java b/ide/projectui/src/org/netbeans/modules/project/ui/ProjectByPathComparator.java new file mode 100644 index 000000000000..0a7e0c73d947 --- /dev/null +++ b/ide/projectui/src/org/netbeans/modules/project/ui/ProjectByPathComparator.java @@ -0,0 +1,47 @@ +/* + * 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.util.Comparator; +import org.netbeans.api.project.Project; + +/** + * Compares projects by their real path. As a result siblings and nested + * projects are co-located to each other. + */ +final class ProjectByPathComparator implements Comparator { + + public static Comparator projectByPath() { + return new ProjectByPathComparator(); + } + @Override + public int compare(Project p1, Project p2) { + if (p1 == null && p2 == null) { + return 0; + } + if (p1 == null) { + return -1; + } + if (p2 == null) { + return 1; + } + return p1.getProjectDirectory().getPath().compareTo(p2.getProjectDirectory().getPath()); + } + +} diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/ProjectsRootKeys.java b/ide/projectui/src/org/netbeans/modules/project/ui/ProjectsRootKeys.java index cf3fc1c672b1..2cfe0664984c 100644 --- a/ide/projectui/src/org/netbeans/modules/project/ui/ProjectsRootKeys.java +++ b/ide/projectui/src/org/netbeans/modules/project/ui/ProjectsRootKeys.java @@ -84,7 +84,7 @@ synchronized final Set clear() { Collection getKeys() { var projects = Arrays.asList(listProjects()); - projects.sort(OpenProjectList.projectByPath()); + projects.sort(ProjectByPathComparator.projectByPath()); var dirs = new ArrayList(projects.size()); final java.util.Map snapshot = new HashMap<>(); diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/TemplateChooserPanelGUI.java b/ide/projectui/src/org/netbeans/modules/project/ui/TemplateChooserPanelGUI.java index 5370324c4aa7..633df11456b5 100644 --- a/ide/projectui/src/org/netbeans/modules/project/ui/TemplateChooserPanelGUI.java +++ b/ide/projectui/src/org/netbeans/modules/project/ui/TemplateChooserPanelGUI.java @@ -119,7 +119,7 @@ private void initValues( @NullAllowed Project p ) { DefaultComboBoxModel projectsModel; if (includeTemplatesWithProjects) { Project openProjects[] = OpenProjectList.getDefault().getOpenProjects(); - Arrays.sort(openProjects, OpenProjectList.projectByPath()); + Arrays.sort(openProjects, ProjectByPathComparator.projectByPath()); projectsModel = new DefaultComboBoxModel( openProjects ); selectProject(p); } else { diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/actions/SetMainProject.java b/ide/projectui/src/org/netbeans/modules/project/ui/actions/SetMainProject.java index bf1330035c6a..66ec2420014b 100644 --- a/ide/projectui/src/org/netbeans/modules/project/ui/actions/SetMainProject.java +++ b/ide/projectui/src/org/netbeans/modules/project/ui/actions/SetMainProject.java @@ -40,6 +40,7 @@ import org.netbeans.api.project.ProjectInformation; import org.netbeans.api.project.ProjectUtils; import org.netbeans.modules.project.ui.OpenProjectList; +import org.netbeans.modules.project.ui.ProjectByPathComparator; import org.netbeans.modules.project.ui.ProjectsRootNode; import static org.netbeans.modules.project.ui.actions.Bundle.*; import org.openide.awt.ActionID; @@ -162,7 +163,7 @@ public void run() { "LBL_NoneMainProject_Name=&None" }) private void createSubMenu(Project[] projects) { - Arrays.sort(projects, OpenProjectList.projectByPath()); + Arrays.sort(projects, ProjectByPathComparator.projectByPath()); // Enable disable the action according to number of open projects if (projects.length == 0) { From b14f1e98b4dfd8da3b8d579f3ce2791a245cacc3 Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Thu, 8 Oct 2026 14:53:34 +0200 Subject: [PATCH 15/15] Can every problem be solved by another layer of indirection? --- .../modules/project/ui/OpenProjectImpl.java | 85 ++++++++++++ .../modules/project/ui/OpenProjectList.java | 126 ++++++++---------- .../project/ui/ProjectByPathComparator.java | 5 +- 3 files changed, 148 insertions(+), 68 deletions(-) create mode 100644 ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectImpl.java diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectImpl.java b/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectImpl.java new file mode 100644 index 000000000000..33fa8eddf760 --- /dev/null +++ b/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectImpl.java @@ -0,0 +1,85 @@ +/* + * 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.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import org.netbeans.api.project.Project; +import org.openide.filesystems.FileObject; +import org.openide.util.Lookup; +import org.openide.windows.WindowManager; + +final class OpenProjectImpl { + /** Main project */ + private Project mainProject; + /** List which holds the open projects */ + private List openProjects = List.of(); + private volatile OpenProjectOperation LOAD; + + OpenProjectImpl() { + + } + + final List openProjects() { + return openProjects; + } + + final void replaceProjectsImpl(OpenProjectsLoading.Callback callback, List projects, URL mainProject, Lookup.Result selectedFiles) { + openProjects = new ArrayList<>(projects); + var urls = projects.stream().map(p -> p.url).toList(); + var load = new OpenProjectsLoading(callback, selectedFiles, urls, mainProject); + LOAD = load; + WindowManager.getDefault().invokeWhenUIReady(load); + } + + boolean waitFinished(long timeout) { + return LOAD.waitFinished(timeout); + } + + void preferredProject(Set singleton) { + LOAD.preferredProject(singleton); + } + + boolean isDone() { + return LOAD.isDone(); + } + + void enter() { + LOAD.enter(); + } + + void exit() { + LOAD.exit(); + } + + boolean closeBeforeOpen(Project[] someProjects) { + return LOAD.closeBeforeOpen(someProjects); + } + + void updateGlobalState(List projects, Project mainProject) { + this.openProjects = projects; + this.mainProject = mainProject; + } + + Project mainProject() { + return this.mainProject; + } +} diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java b/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java index da2617e1a32b..2782648871e6 100644 --- a/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java +++ b/ide/projectui/src/org/netbeans/modules/project/ui/OpenProjectList.java @@ -31,7 +31,6 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.Comparator; import java.util.ConcurrentModificationException; import java.util.HashMap; import java.util.Iterator; @@ -95,7 +94,6 @@ import org.openide.util.lookup.Lookups; import org.openide.util.lookup.ProxyLookup; import org.openide.util.Utilities; -import org.openide.windows.WindowManager; import static org.netbeans.modules.project.ui.Bundle.*; @@ -138,12 +136,8 @@ public final class OpenProjectList { private static final RequestProcessor RP3 = new RequestProcessor(OpenProjectList.class); /** List which holds the open projects */ - private List openProjects; private final HashMap> openProjectsModuleInfos; - /** Main project */ - private Project mainProject; - /** List of recently closed projects */ private final RecentProjectList recentProjects; @@ -157,11 +151,12 @@ public final class OpenProjectList { private final NbProjectDeletionListener nbprojectDeleteListener = new NbProjectDeletionListener(); private final PropertyChangeListener infoListener; - private volatile OpenProjectOperation LOAD; + private final OpenProjectImpl openMainProjects; private final ArrayList projectGroupSupport; private final AtomicBoolean groupChanging = new AtomicBoolean(false); OpenProjectList() { + this.openMainProjects = new OpenProjectImpl(); URL main = OpenProjectListSettings.getInstance().getMainProjectURL(); pchSupport = new PropertyChangeSupport( this ); recentTemplates = new ArrayList(); @@ -197,14 +192,14 @@ public static OpenProjectList getDefault() { } public static void waitProjectsFullyOpen() { - getDefault().LOAD.waitFinished(0); + getDefault().openMainProjects.waitFinished(0); } static void preferredProject(final Project lazyP) { if (lazyP != null) { OPENING_RP.post(new Runnable() { @Override public void run() { - getDefault().LOAD.preferredProject(Collections.singleton(lazyP.getProjectDirectory())); + getDefault().openMainProjects.preferredProject(Collections.singleton(lazyP.getProjectDirectory())); } }); } @@ -224,18 +219,18 @@ public boolean isCancelled() { @Override public boolean isDone() { - return LOAD.isDone(); + return openMainProjects.isDone(); } @Override public Project[] get() throws InterruptedException, ExecutionException { - LOAD.waitFinished(0L); + openMainProjects.waitFinished(0L); return getOpenProjects(); } @Override public Project[] get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { - if (LOAD.waitFinished(unit.toMillis(timeout))) { + if (openMainProjects.waitFinished(unit.toMillis(timeout))) { return getOpenProjects(); } else { throw new TimeoutException(); @@ -376,14 +371,14 @@ class Cancellation extends AtomicBoolean implements Cancellable { } finally { handle.finish(); } - if (mainProject != null && Arrays.asList(projects).contains(mainProject) && openProjects.contains(mainProject)) { + if (mainProject != null && Arrays.asList(projects).contains(mainProject) && openMainProjects.openProjects().contains(mainProject)) { setMainProject(mainProject); } } }); } else { open(projects, prime, openSubprojects, null, null); - if (mainProject != null && Arrays.asList(projects).contains(mainProject) && openProjects.contains(mainProject)) { + if (mainProject != null && Arrays.asList(projects).contains(mainProject) && openMainProjects.openProjects().contains(mainProject)) { setMainProject(mainProject); } } @@ -400,7 +395,7 @@ class Cancellation extends AtomicBoolean implements Cancellable { "# {0} - project path", "OpenProjectList.deleted_project={0} seems to have been deleted." }) public void open(Project[] projects, boolean prime, boolean openSubprojects, ProgressHandle handle, AtomicBoolean canceled) { - LOAD.waitFinished(0); + openMainProjects.waitFinished(0); List toHandle = new LinkedList(); @@ -450,7 +445,7 @@ public void finished(boolean success) { } try { - LOAD.enter(); + openMainProjects.enter(); boolean recentProjectsChanged = false; int maxWork = 1000; double workForSubprojects = maxWork / (openSubprojects ? 2.0 : 10.0); @@ -535,7 +530,7 @@ public void finished(boolean success) { final List newprjs = new ArrayList(); MUTEX.writeAccess(new Mutex.Action() { public @Override Void run() { - oldprjs.addAll(openProjects); + oldprjs.addAll(openMainProjects.openProjects()); return null; } }); @@ -563,7 +558,7 @@ public void finished(boolean success) { final boolean _recentProjectsChanged = recentProjectsChanged; MUTEX.readAccess(new Mutex.Action() { public @Override Void run() { - openprjs.addAll(openProjects); + openprjs.addAll(openMainProjects.openProjects()); return null; } }); @@ -572,7 +567,7 @@ public void finished(boolean success) { MUTEX.writeAccess(new Mutex.Action() { public @Override Void run() { - newprjs.addAll(openProjects); + newprjs.addAll(openMainProjects.openProjects()); saveProjectList(openProjectsData); if (_recentProjectsChanged) { recentProjects.save(); @@ -600,26 +595,22 @@ public Void run() { } }); } finally { - LOAD.exit(); + openMainProjects.exit(); } } public final void replaceProjects(Collection urls, URL mainProject) { - close(openProjects.toArray(Project[]::new), false); + close(openMainProjects.openProjects().toArray(Project[]::new), false); List projects = urls.stream().map(LazyProject::forUrl).toList(); MUTEX.writeAccess(() -> { replaceProjectsImpl(projects, mainProject); }); } - private final void replaceProjectsImpl(List projects, URL mainProject) { - openProjects = new ArrayList<>(projects); - pchSupport.firePropertyChange(PROPERTY_WILL_OPEN_PROJECTS, null, openProjects.toArray(Project[]::new)); + private void replaceProjectsImpl(List projects, URL mainProject1) { + pchSupport.firePropertyChange(PROPERTY_WILL_OPEN_PROJECTS, null, openMainProjects.openProjects().toArray(Project[]::new)); Lookup.Result selectedFiles = Utilities.actionsGlobalContext().lookupResult(FileObject.class); - var urls = projects.stream().map(p -> p.url).toList(); - var load = new OpenProjectsLoading(new LoadingCallback(), selectedFiles, urls, mainProject); - LOAD = load; - WindowManager.getDefault().invokeWhenUIReady(load); + openMainProjects.replaceProjectsImpl(new OpenProjectList.LoadingCallback(), projects, mainProject1, selectedFiles); } public void close( Project someProjects[], boolean notifyUI) { @@ -629,9 +620,9 @@ public void close( Project someProjects[], boolean notifyUI) { public void close( Project someProjects[], boolean notifyUI, String groupName ) { boolean doSave = false; - if (!LOAD.closeBeforeOpen(someProjects)) { + if (!openMainProjects.closeBeforeOpen(someProjects)) { doSave = true; - LOAD.waitFinished(0); + openMainProjects.waitFinished(0); } final Project[] projects = new Project[someProjects.length]; @@ -645,7 +636,7 @@ public void close( Project someProjects[], boolean notifyUI, String groupName ) } try { - LOAD.enter(); + openMainProjects.enter(); ProjectUtilities.WaitCursor.show(); OpenProjectsLogging.logProjects("close(): closing project: ", projects); final AtomicBoolean mainClosed = new AtomicBoolean(); @@ -655,9 +646,9 @@ public void close( Project someProjects[], boolean notifyUI, String groupName ) final List notifyList = new ArrayList(); MUTEX.writeAccess(new Mutex.Action() { public @Override Void run() { - oldprjs.addAll(openProjects); + oldprjs.addAll(openMainProjects.openProjects()); for (Project p : projects) { - Iterator it = openProjects.iterator(); + Iterator it = openMainProjects.openProjects().iterator(); boolean found = false; while (it.hasNext()) { if (it.next().equals(p)) { @@ -671,7 +662,7 @@ public void close( Project someProjects[], boolean notifyUI, String groupName ) if (!mainClosed.get()) { mainClosed.set(isMainProject(p)); } - // remove the project from openProjects + // remove the project from LOAD.openProjects() it.remove(); removeModuleInfo(p); @@ -682,11 +673,12 @@ public void close( Project someProjects[], boolean notifyUI, String groupName ) someClosed.set(true); } if (someClosed.get()) { - newprjs.addAll(openProjects); + newprjs.addAll(openMainProjects.openProjects()); } if (mainClosed.get()) { - mainProject = null; - saveMainProject( mainProject ); + // clear the main project + openMainProjects.updateGlobalState(openMainProjects.openProjects(), null); + saveMainProject( null ); } return null; } @@ -712,7 +704,7 @@ public void close( Project someProjects[], boolean notifyUI, String groupName ) }); } //#125750 not necessary to call notifyClosed() under synchronized lock. - LOAD.enter(); + openMainProjects.enter(); OPENING_RP.post(new Runnable() { // #177427 - this can be slow, better to do asynch @Override public void run() { @@ -721,11 +713,11 @@ public void run() { notifyClosed(closed); } } finally { - LOAD.exit(); + openMainProjects.exit(); } } }); - OpenProjectsLogging.logProjects("close(): openProjects == ", openProjects.toArray(new Project[0])); // NOI18N + OpenProjectsLogging.logProjects("close(): LOAD.openProjects() == ", openMainProjects.openProjects().toArray(new Project[0])); // NOI18N if (someClosed.get()) { pchSupport.firePropertyChange(PROPERTY_OPEN_PROJECTS, oldprjs.toArray(new Project[0]), newprjs.toArray(new Project[0]) ); @@ -756,7 +748,7 @@ public void run() { OpenProjectsLogging.log(removedRec, "org.netbeans.ui.metrics.projects"); } finally { ProjectUtilities.WaitCursor.hide(); - LOAD.exit(); + openMainProjects.exit(); } } @@ -764,7 +756,7 @@ public void run() { public Project[] getOpenProjects() { return MUTEX.readAccess(new Mutex.Action() { public @Override Project[] run() { - return openProjects.toArray(new Project[0]); + return openMainProjects.openProjects().toArray(new Project[0]); } }); } @@ -772,7 +764,7 @@ public Project[] getOpenProjects() { public boolean isOpen(final Project p) { return MUTEX.readAccess(new Mutex.Action() { public @Override Boolean run() { - for (Project cp : openProjects) { + for (Project cp : openMainProjects.openProjects()) { //check for folder quity is necessary because of the lazy projects initially populating the list if (p.getProjectDirectory().equals(cp.getProjectDirectory())) { return true; @@ -786,6 +778,7 @@ public boolean isOpen(final Project p) { public boolean isMainProject(final Project p) { return MUTEX.readAccess(new Mutex.Action() { public @Override Boolean run() { + Project mainProject = openMainProjects.mainProject(); if (mainProject != null && p != null && mainProject.getProjectDirectory().equals(p.getProjectDirectory())) { return true; } else { @@ -798,18 +791,18 @@ public boolean isMainProject(final Project p) { public Project getMainProject() { return MUTEX.readAccess(new Mutex.Action() { public @Override Project run() { - return mainProject; + return openMainProjects.mainProject(); } }); } public void setMainProject( Project project ) { OpenProjectsLogging.LOGGER.log(Level.FINER, "Setting main project: {0}", project); // NOI18N - OpenProjectsLogging.logProjects("setMainProject(): openProjects == ", openProjects.toArray(new Project[0])); // NOI18N + OpenProjectsLogging.logProjects("setMainProject(): LOAD.openProjects() == ", openMainProjects.openProjects().toArray(new Project[0])); // NOI18N //called here to avoid wrapping projectManager.MUTEX within PrivateMutex.MUTEX //#139965 the project passed in here can be different from the current one. // eg when the ManProjectAction shows a list of opened projects, it lists the "non-loaded skeletons" - // but when the user eventually selects one, the openProjects list already might hold the + // but when the user eventually selects one, the LOAD.openProjects() list already might hold the // correct loaded list. try { final Project prj = project != null ? ProjectManager.getDefault().findProject(project.getProjectDirectory()) : null; @@ -818,9 +811,9 @@ public void setMainProject( Project project ) { MUTEX.writeAccess(new Mutex.Action() { public @Override Void run() { Project main = prj; - if (main != null && !openProjects.contains(main)) { + if (main != null && !openMainProjects.openProjects().contains(main)) { boolean fail = true; - for (Project p : openProjects) { + for (Project p : openMainProjects.openProjects()) { if (p.equals(main)) { fail = false; break; @@ -835,12 +828,11 @@ public void setMainProject( Project project ) { } if (fail) { OpenProjectsLogging.LOGGER.log(Level.WARNING, "Project {0} is not open and cannot be set as main.", dn); - OpenProjectsLogging.logProjects("setMainProject(): openProjects == ", openProjects.toArray(new Project[0])); // NOI18N + OpenProjectsLogging.logProjects("setMainProject(): LOAD.openProjects() == ", openMainProjects.openProjects().toArray(new Project[0])); // NOI18N return null; } } - - mainProject = main; + openMainProjects.updateGlobalState(openMainProjects.openProjects(), main); saveMainProject(main); return null; } @@ -957,8 +949,8 @@ static void shutdown() { //a bit on magic here. We want to do the goup document persistence before notifyClosed in hope of the // ant projects saving their project data before being closed. (ant ptojects call saveProjct() in the openclose hook. // the caller of this method calls saveAllProjectt() later. - Group.onShutdown(new LinkedHashSet<>(INSTANCE.openProjects)); - for (Project p : INSTANCE.openProjects) { + Group.onShutdown(new LinkedHashSet<>(INSTANCE.openMainProjects.openProjects())); + for (Project p : INSTANCE.openMainProjects.openProjects()) { notifyClosed(p); } } catch (ConcurrentModificationException x) { @@ -1001,12 +993,12 @@ private static boolean notifyOpened(Project p) { } catch (RuntimeException e) { OpenProjectsLogging.log(Level.WARNING, null, e); // Do not try to call its close hook if its open hook already failed: - INSTANCE.openProjects.remove(p); + INSTANCE.openMainProjects.openProjects().remove(p); INSTANCE.removeModuleInfo(p); ok = false; } catch (Error e) { OpenProjectsLogging.log(Level.WARNING, null, e); - INSTANCE.openProjects.remove(p); + INSTANCE.openMainProjects.openProjects().remove(p); INSTANCE.removeModuleInfo(p); ok = false; } @@ -1057,15 +1049,15 @@ private boolean doOpenProject(final @NonNull Project p) { final AtomicBoolean alreadyOpen = new AtomicBoolean(); boolean recentProjectsChanged = MUTEX.writeAccess(new Mutex.Action() { public @Override Boolean run() { - OpenProjectsLogging.log(Level.FINER, "already opened: {0} ", openProjects); - for (Project existing : openProjects) { + OpenProjectsLogging.log(Level.FINER, "already opened: {0} ", openMainProjects.openProjects()); + for (Project existing : openMainProjects.openProjects()) { // TODO An old hack due to broken equals() contract; see https://bz.apache.org/netbeans/show_bug.cgi?id=156536 if (p.equals(existing) || existing.equals(p)) { alreadyOpen.set(true); return false; } } - openProjects.add(p); + openMainProjects.openProjects().add(p); addModuleInfo(p); //initially opened projects need to have these listeners also added. p.getProjectDirectory().addFileChangeListener(deleteListener); @@ -1077,7 +1069,7 @@ private boolean doOpenProject(final @NonNull Project p) { if (alreadyOpen.get()) { return false; } - OpenProjectsLogging.logProjects("doOpenProject(): openProjects == ", openProjects.toArray(new Project[0])); // NOI18N + OpenProjectsLogging.logProjects("doOpenProject(): LOAD.openProjects() == ", openMainProjects.openProjects().toArray(new Project[0])); // NOI18N // Notify projects opened notifyOpened(p); @@ -1467,8 +1459,8 @@ public void load() { recentProjectsInfos.clear(); } // register project delete listener to all open projects - for (Project p : openProjects) { - assert p != null : "There is null in " + openProjects; + for (Project p : openMainProjects.openProjects()) { + assert p != null : "There is null in " + openMainProjects.openProjects(); assert p.getProjectDirectory() != null : "Project " + p + " has null project directory"; p.getProjectDirectory().addFileChangeListener(nbprojectDeleteListener); } @@ -1625,7 +1617,7 @@ public ProjectDeletionListener() {} OpenProjectList.MUTEX.readAccess(new Mutex.Action() { public @Override Void run() { Project toRemove = null; - for (Project prj : openProjects) { + for (Project prj : openMainProjects.openProjects()) { if (fe.getFile().equals(prj.getProjectDirectory())) { toRemove = prj; break; @@ -1723,17 +1715,17 @@ public void updateGlobalState(List lazilyOpenedProjects, Project lazyMa List recentTemplates = new ArrayList(OpenProjectListSettings.getInstance().getRecentTemplates()); - openProjects = lazilyOpenedProjects; - if (lazyMainProject != null) { - mainProject = lazyMainProject; + if (lazyMainProject == null) { + lazyMainProject = openMainProjects.mainProject(); } - mainProject = unwrapProject(mainProject); + openMainProjects.updateGlobalState(lazilyOpenedProjects, unwrapProject(lazyMainProject)); + getRecentTemplates().addAll(recentTemplates); MUTEX.postReadRequest(() -> { Project[] opened = lazilyOpenedProjects.toArray(Project[]::new); pchSupport.firePropertyChange(OpenProjectList.PROPERTY_OPEN_PROJECTS, new Project[0], opened); - Project main = mainProject; + Project main = openMainProjects.mainProject(); if (main != null) { // else PROPERTY_MAIN_PROJECT would be fired spuriously pchSupport.firePropertyChange(OpenProjectList.PROPERTY_MAIN_PROJECT, null, main); diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/ProjectByPathComparator.java b/ide/projectui/src/org/netbeans/modules/project/ui/ProjectByPathComparator.java index 0a7e0c73d947..61ea90d104c0 100644 --- a/ide/projectui/src/org/netbeans/modules/project/ui/ProjectByPathComparator.java +++ b/ide/projectui/src/org/netbeans/modules/project/ui/ProjectByPathComparator.java @@ -25,11 +25,14 @@ * Compares projects by their real path. As a result siblings and nested * projects are co-located to each other. */ -final class ProjectByPathComparator implements Comparator { +public final class ProjectByPathComparator implements Comparator { + private ProjectByPathComparator() { + } public static Comparator projectByPath() { return new ProjectByPathComparator(); } + @Override public int compare(Project p1, Project p2) { if (p1 == null && p2 == null) {