diff --git a/ide/projectapi.nb/nbproject/project.properties b/ide/projectapi.nb/nbproject/project.properties index ee2e82fe015a..5b0b05e3d8ff 100644 --- a/ide/projectapi.nb/nbproject/project.properties +++ b/ide/projectapi.nb/nbproject/project.properties @@ -15,5 +15,5 @@ # specific language governing permissions and limitations # under the License. is.eager=true -javac.release=17 +javac.release=21 javac.compilerargs=-Xlint -Xlint:-serial diff --git a/ide/projectapi.nb/nbproject/project.xml b/ide/projectapi.nb/nbproject/project.xml index ae31afbd3571..a127b99ea96a 100644 --- a/ide/projectapi.nb/nbproject/project.xml +++ b/ide/projectapi.nb/nbproject/project.xml @@ -40,7 +40,7 @@ 1 - 1.78 + 1.107 diff --git a/ide/projectapi/nbproject/project.xml b/ide/projectapi/nbproject/project.xml index 8bdd3709ed39..f7cd62807454 100644 --- a/ide/projectapi/nbproject/project.xml +++ b/ide/projectapi/nbproject/project.xml @@ -113,6 +113,11 @@ + + org.openide.util.ui.svg + + + org.openide.util.lookup diff --git a/ide/projectapi/src/org/netbeans/api/project/ProjectActionContext.java b/ide/projectapi/src/org/netbeans/api/project/ProjectActionContext.java index 2d142e98cf13..5ba4e28b5341 100644 --- a/ide/projectapi/src/org/netbeans/api/project/ProjectActionContext.java +++ b/ide/projectapi/src/org/netbeans/api/project/ProjectActionContext.java @@ -220,7 +220,7 @@ public Builder withLookup(Lookup lkp) { /** * Binds to a specific {@link ProjectConfiguration}, which must resolve to an instance of - * @see MavenConfiguration. + * @see MavenConfiguration * @param configuration * @return builder instance */ diff --git a/ide/projectapi/test/unit/src/org/netbeans/api/project/ProjectManagerTest.java b/ide/projectapi/test/unit/src/org/netbeans/api/project/ProjectManagerTest.java index eec0751279b4..5ca277ee81a8 100644 --- a/ide/projectapi/test/unit/src/org/netbeans/api/project/ProjectManagerTest.java +++ b/ide/projectapi/test/unit/src/org/netbeans/api/project/ProjectManagerTest.java @@ -50,27 +50,28 @@ * @author Jesse Glick */ public class ProjectManagerTest extends NbTestCase { - + static { // For easier testing. TimedWeakReference.TIMEOUT = 1000; } - + public ProjectManagerTest(String name) { super(name); } - + private FileObject scratch; private FileObject goodproject; private FileObject goodproject2; private FileObject badproject; private FileObject mysteryproject; + private FileObject justADir; private ProjectManager pm; protected @Override Level logLevel() { return Level.FINE; } - + @Override protected void setUp() throws Exception { super.setUp(); @@ -82,11 +83,12 @@ protected void setUp() throws Exception { badproject = scratch.createFolder("bad"); badproject.createFolder("testproject").createData("broken"); mysteryproject = scratch.createFolder("mystery"); + justADir = scratch.createFolder("justADir"); MockLookup.setInstances(TestUtil.testProjectFactory()); pm = ProjectManager.getDefault(); NbProjectManagerAccessor.reset(); } - + @Override protected void tearDown() throws Exception { scratch = null; @@ -96,7 +98,7 @@ protected void tearDown() throws Exception { pm = null; super.tearDown(); } - + public void testFindProject() throws Exception { Project p = null; CharSequence log = Log.enable("TIMER", Level.FINE); @@ -126,7 +128,7 @@ public void testFindProject() throws Exception { assertEquals("Repeated find calls should give same result", p, pm.findProject(goodproject)); assertEquals("ProjectFactory was called only once on goodproject", 1, TestUtil.projectLoadCount(goodproject)); } - + public void testFindProjectGC() throws Exception { Project p = null; try { @@ -157,7 +159,7 @@ public void testFindProjectGC() throws Exception { assertEquals("Correct project directory set again", goodproject, p.getProjectDirectory()); assertEquals("ProjectFactory was called only once on new goodproject folder object", 1, TestUtil.projectLoadCount(goodproject)); } - + public void testFindProjectDoesNotCacheLoadErrors() throws Exception { Project p = null; try { @@ -197,32 +199,32 @@ public void testFindProjectDoesNotCacheLoadErrors() throws Exception { // Expected. } } - + public void testIsProject() throws Exception { assertTrue("Should have recognized goodproject", pm.isProject(goodproject)); - + assertTrue("Should have recognized badproject", pm.isProject(badproject)); - + assertFalse("Should not have been able to load mysteryproject", pm.isProject(mysteryproject)); } - + public void testIsProject2() throws Exception { ProjectManager.Result r = pm.isProject2(goodproject); assertNotNull("Should have recognized goodproject", r); assertEquals(goodproject.getName(), r.getDisplayName()); assertEquals(TestUtil.TEST_PROJECT_ICON, r.getIcon()); assertEquals(TestUtil.TEST_PROJECT_TYPE, r.getProjectType()); - + ProjectManager.Result r2 = pm.isProject2(badproject); assertNotNull("Should have recognized badproject", r2); assertNull("Should not have a project name for badproject", r2.getDisplayName()); assertEquals(TestUtil.TEST_PROJECT_ICON, r2.getIcon()); assertNull("Should not have a project type for badproject", r2.getProjectType()); - + ProjectManager.Result r3 = pm.isProject2(mysteryproject); assertNull("Should not have been able to load mysteryproject", r3); } - + public void testModify() throws Exception { Project p1 = pm.findProject(goodproject); Project p2 = pm.findProject(goodproject2); @@ -239,7 +241,7 @@ public void testModify() throws Exception { assertTrue("p1 is modified", pm.isModified(p1)); assertTrue("and p2 is modified too", pm.isModified(p2)); } - + public void testSave() throws Exception { Project p1 = pm.findProject(goodproject); Project p2 = pm.findProject(goodproject2); @@ -275,7 +277,7 @@ public void testSave() throws Exception { assertEquals("p1 still only saved twice", 2, TestUtil.projectSaveCount(p1)); assertEquals("p2 still only saved twice", 2, TestUtil.projectSaveCount(p2)); } - + public void testSaveError() throws Exception { Project p1 = pm.findProject(goodproject); Project p2 = pm.findProject(goodproject2); @@ -328,7 +330,7 @@ public void testSaveError() throws Exception { assertEquals("p1 was now saved twice", 2, TestUtil.projectSaveCount(p1)); assertEquals("p2 was saved exactly once (by one or the other saveAllProjects)", 1, TestUtil.projectSaveCount(p2)); } - + public void testClearNonProjectCache() throws Exception { FileObject p1 = scratch.createFolder("p1"); p1.createFolder("testproject"); @@ -359,55 +361,55 @@ public void testClearNonProjectCache() throws Exception { public void testNotifyDeleted() throws Exception { FileObject p1 = scratch.createFolder("p1"); FileObject p1TestProject = p1.createFolder("testproject"); - + Project project1 = pm.findProject(p1); - + assertNotNull("project1 is recognized", project1); p1TestProject.delete(); TestUtil.notifyDeleted(project1); - + assertFalse("project1 is not valid", pm.isValid(project1)); assertNull("project1 is deleted", pm.findProject(p1)); FileObject p2 = scratch.createFolder("p2"); FileObject p2TestProject = p2.createFolder("testproject"); - + Project project2 = pm.findProject(p2); - + assertNotNull("project2 is recognized", project2); TestUtil.notifyDeleted(project2); - + assertFalse("project2 is not valid", pm.isValid(project2)); - + Project project2b = pm.findProject(p2); - + assertTrue("project2 is newly recognized", project2b != project2); assertNotNull("project2 is newly recognized", project2b); FileObject p3 = scratch.createFolder("p3"); FileObject p3TestProject = p3.createFolder("testproject"); - + Project project3 = pm.findProject(p3); - + assertNotNull("project3 is recognized", project3); TestUtil.modify(project3); assertTrue("project3 is modified", pm.isModified(project3)); TestUtil.notifyDeleted(project3); - + assertFalse("project3 is not valid", pm.isValid(project3)); - + assertFalse(pm.isModified(project3)); // please do not throw an exception here - + FileObject p4 = scratch.createFolder("p4"); FileObject p4TestProject = p4.createFolder("testproject"); Project project4 = pm.findProject(p4); - + assertNotNull("project4 is recognized", project4); TestUtil.notifyDeleted(project4); - + assertFalse("project4 is not valid", pm.isValid(project3)); - + TestUtil.notifyDeleted(project4); // please do not throw an exception here } @@ -445,7 +447,7 @@ public void testRenameProjectFolder() throws Exception { // #194046 TestUtil.notifyDeleted(project1); assertEquals(project2, pm.findProject(p2)); } - + /** * Helper method allowing to reset PM from other module's tests. * @param pm PM to reset diff --git a/ide/projectui/nbproject/project.xml b/ide/projectui/nbproject/project.xml index 6fe03beedfb3..93e7e3d5c846 100644 --- a/ide/projectui/nbproject/project.xml +++ b/ide/projectui/nbproject/project.xml @@ -93,7 +93,7 @@ 1 - 1.56 + 1.107 @@ -295,6 +295,10 @@ + + org.openide.util.ui.svg + + org.openide.util.lookup 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..af358049d88b 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; @@ -110,6 +109,7 @@ import org.openide.windows.WindowManager; import static org.netbeans.modules.project.ui.Bundle.*; +import org.netbeans.modules.project.ui.fallback.FallbackFactory; /** * List of projects open in the GUI. @@ -1300,9 +1300,11 @@ private static Set URLs2Projects(Collection 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); + if (FileOwnerQuery.getOwner(dir) == null) { + Project p = FallbackFactory.findProjectOrFallback(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. diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/fallback/FallbackFactory.java b/ide/projectui/src/org/netbeans/modules/project/ui/fallback/FallbackFactory.java new file mode 100644 index 000000000000..14e6792a0797 --- /dev/null +++ b/ide/projectui/src/org/netbeans/modules/project/ui/fallback/FallbackFactory.java @@ -0,0 +1,90 @@ +/* + * 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.fallback; + +import java.io.IOException; +import java.lang.ref.Reference; +import java.lang.ref.WeakReference; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.netbeans.api.project.Project; +import org.netbeans.api.project.ProjectManager; +import org.netbeans.spi.project.ProjectFactory; +import org.netbeans.spi.project.ProjectState; +import org.openide.filesystems.FileObject; +import org.openide.util.lookup.ServiceProvider; + +@ServiceProvider(service = ProjectFactory.class, position = 1_000_000) +public final class FallbackFactory implements ProjectFactory { + private static final ThreadLocal FALLBACK = new ThreadLocal<>(); + private static final Map> EXISTING = Collections.synchronizedMap(new HashMap<>()); + + static void clearNonProjectCache() { + ProjectManager.getDefault().clearNonProjectCache(); + EXISTING.clear(); + } + + static void clear(FileObject dir) { + Reference ref = EXISTING.get(dir); + Project existing = ref == null ? null : ref.get(); + if (existing instanceof FallbackProject prj) { + prj.notifyDeleted(); + } + } + + public FallbackFactory() { + } + + public static Project findProjectOrFallback(FileObject dir) throws IOException { + ProjectManager.getDefault().clearNonProjectCache(); + FileObject prev = FALLBACK.get(); + try { + FALLBACK.set(dir); + Project prj = ProjectManager.getDefault().findProject(dir); + EXISTING.put(dir, new WeakReference<>(prj)); + return prj; + } finally { + FALLBACK.set(prev); + } + + } + + @Override + public boolean isProject(FileObject dir) { + if (FALLBACK.get() == dir) { + return true; + } + Reference ref = EXISTING.get(dir); + return ref != null && ref.get() != null; + } + + @Override + public Project loadProject(FileObject projectDirectory, ProjectState state) throws IOException { + if (isProject(projectDirectory)) { + return new FallbackProject(projectDirectory, state); + } else { + return null; + } + } + + @Override + public void saveProject(Project project) throws IOException, ClassCastException { + } +} diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/fallback/FallbackProject.java b/ide/projectui/src/org/netbeans/modules/project/ui/fallback/FallbackProject.java new file mode 100644 index 000000000000..6f5f9b756ce5 --- /dev/null +++ b/ide/projectui/src/org/netbeans/modules/project/ui/fallback/FallbackProject.java @@ -0,0 +1,126 @@ +/* + * 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.fallback; + +import java.beans.PropertyChangeListener; +import javax.swing.Icon; +import javax.swing.event.ChangeListener; +import org.netbeans.api.project.Project; +import org.netbeans.api.project.ProjectInformation; +import org.netbeans.api.project.SourceGroup; +import org.netbeans.api.project.Sources; +import org.netbeans.spi.project.ProjectState; +import org.netbeans.spi.project.support.GenericSources; +import org.netbeans.spi.project.support.LookupProviderSupport; +import org.openide.filesystems.FileObject; +import org.openide.util.ImageUtilities; +import org.openide.util.Lookup; +import org.openide.util.NbBundle; +import org.openide.util.lookup.Lookups; + +final class FallbackProject implements Project, ProjectInformation, Sources { + private static final String DASHNAME = "org-netbeans-modules-project-fallback"; // NOI18N + private static final String ICON = "org/netbeans/modules/project/ui/fallback/fallback.svg"; // NOI18N + + private final FileObject dir; + private final ProjectState state; + private final Lookup lkp; + private final SourceGroup genericGroup; + + public FallbackProject(FileObject dir, ProjectState state) { + this.dir = dir; + this.state = state; + this.genericGroup = GenericSources.group( + this, dir, + Sources.TYPE_GENERIC, getDisplayName(), + getIcon(), getIcon() + ); + Lookup basicLookup = Lookups.fixed( + this, + dir, + LookupProviderSupport.createActionProviderMerger() + ); + this.lkp = LookupProviderSupport.createCompositeLookup(basicLookup, "Projects/" + DASHNAME + "/Lookup"); + } + + @Override + public FileObject getProjectDirectory() { + return dir; + } + + @Override + public Lookup getLookup() { + return lkp; + } + + @Override + public String getName() { + return dir.getNameExt(); + } + + @Override + @NbBundle.Messages({ + "# {0} - name of the folder", + "CTL_FallbackProjectDisplayName=Folder {0}" + }) + public String getDisplayName() { + return Bundle.CTL_FallbackProjectDisplayName(getName()); + } + + @Override + public Icon getIcon() { + Icon icon = ImageUtilities.loadIcon(ICON); + assert icon != null; + return icon; + } + + @Override + public Project getProject() { + return this; + } + + @Override + public void addPropertyChangeListener(PropertyChangeListener listener) { + } + + @Override + public void removePropertyChangeListener(PropertyChangeListener listener) { + } + + @Override + public SourceGroup[] getSourceGroups(String type) { + if (Sources.TYPE_GENERIC.equals(type)) { + return new SourceGroup[] { genericGroup }; + } else { + return new SourceGroup[0]; + } + } + + @Override + public void addChangeListener(ChangeListener listener) { + } + + @Override + public void removeChangeListener(ChangeListener listener) { + } + + void notifyDeleted() { + state.notifyDeleted(); + } +} diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/fallback/fallback.png b/ide/projectui/src/org/netbeans/modules/project/ui/fallback/fallback.png new file mode 100644 index 000000000000..7f9b26179abb Binary files /dev/null and b/ide/projectui/src/org/netbeans/modules/project/ui/fallback/fallback.png differ diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/fallback/fallback.svg b/ide/projectui/src/org/netbeans/modules/project/ui/fallback/fallback.svg new file mode 100644 index 000000000000..5f7295022b67 --- /dev/null +++ b/ide/projectui/src/org/netbeans/modules/project/ui/fallback/fallback.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/fallback/fallback24.png b/ide/projectui/src/org/netbeans/modules/project/ui/fallback/fallback24.png new file mode 100644 index 000000000000..50a4fa63a698 Binary files /dev/null and b/ide/projectui/src/org/netbeans/modules/project/ui/fallback/fallback24.png differ diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/fallback/fallback24.svg b/ide/projectui/src/org/netbeans/modules/project/ui/fallback/fallback24.svg new file mode 100644 index 000000000000..c10101837e94 --- /dev/null +++ b/ide/projectui/src/org/netbeans/modules/project/ui/fallback/fallback24.svg @@ -0,0 +1,68 @@ + + + + + + + + Apache NetBeans Logo + + + + + + + + + + + + + + + + + + + + + diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/groups/DirectoryGroup.java b/ide/projectui/src/org/netbeans/modules/project/ui/groups/DirectoryGroup.java index adacb9bb6248..f7e32fcd0a5e 100644 --- a/ide/projectui/src/org/netbeans/modules/project/ui/groups/DirectoryGroup.java +++ b/ide/projectui/src/org/netbeans/modules/project/ui/groups/DirectoryGroup.java @@ -32,6 +32,7 @@ import org.netbeans.api.project.Project; import org.netbeans.api.project.ProjectManager; import org.netbeans.api.queries.SharabilityQuery; +import org.netbeans.modules.project.ui.fallback.FallbackFactory; import org.netbeans.spi.project.ui.support.ProjectConvertors; import org.openide.filesystems.FileObject; import org.openide.filesystems.URLMapper; @@ -77,7 +78,7 @@ protected void findProjects(Set projects, ProgressHandle h, int start, } if (fo != null && fo.isFolder()) { try { - Project p = ProjectManager.getDefault().findProject(fo); + Project p = FallbackFactory.findProjectOrFallback(fo); if (p != null) { projects.add(p); if (h != null) { diff --git a/ide/projectui/src/org/netbeans/modules/project/ui/groups/NewGroupPanel.java b/ide/projectui/src/org/netbeans/modules/project/ui/groups/NewGroupPanel.java index 53fff6df79a4..ecbe746005f0 100644 --- a/ide/projectui/src/org/netbeans/modules/project/ui/groups/NewGroupPanel.java +++ b/ide/projectui/src/org/netbeans/modules/project/ui/groups/NewGroupPanel.java @@ -29,6 +29,7 @@ import javax.swing.JPanel; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; +import org.netbeans.api.project.FileOwnerQuery; import org.netbeans.api.project.Project; import org.netbeans.api.project.ProjectManager; import org.netbeans.api.project.ProjectUtils; @@ -439,6 +440,13 @@ private void directoryButtonActionPerformed(java.awt.event.ActionEvent evt) {//G chooser.setCurrentDirectory(start); if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) { File f = chooser.getSelectedFile(); + if (f != null) { + FileObject fo = FileUtil.toFileObject(f); + Project owner = FileOwnerQuery.getOwner(fo); + if (owner != null) { + f = FileUtil.toFile(owner.getProjectDirectory()); + } + } if (f != null) { directoryField.setText(f.getAbsolutePath()); updateNameField(); diff --git a/ide/projectui/test/unit/src/org/netbeans/modules/project/ui/fallback/FallbackFactoryTest.java b/ide/projectui/test/unit/src/org/netbeans/modules/project/ui/fallback/FallbackFactoryTest.java new file mode 100644 index 000000000000..0713ba5b77b6 --- /dev/null +++ b/ide/projectui/test/unit/src/org/netbeans/modules/project/ui/fallback/FallbackFactoryTest.java @@ -0,0 +1,265 @@ +/* + * 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.fallback; + +import java.lang.ref.WeakReference; +import java.util.logging.Level; +import static junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.assertFalse; +import static junit.framework.TestCase.assertNotNull; +import static junit.framework.TestCase.assertNotSame; +import static junit.framework.TestCase.assertNull; +import static junit.framework.TestCase.assertSame; +import static junit.framework.TestCase.assertTrue; +import org.netbeans.api.project.FileOwnerQuery; +import org.netbeans.api.project.Project; +import org.netbeans.api.project.ProjectInformation; +import org.netbeans.api.project.ProjectManager; +import org.netbeans.api.project.ProjectUtils; +import org.netbeans.api.project.SourceGroup; +import org.netbeans.api.project.Sources; +import org.netbeans.api.project.TestUtil; +import org.netbeans.junit.NbTestCase; +import static org.netbeans.junit.NbTestCase.assertGC; +import org.netbeans.modules.projectapi.nb.NbProjectManagerAccessor; +import org.netbeans.modules.projectapi.nb.TimedWeakReference; +import org.netbeans.spi.project.ActionProvider; +import org.openide.filesystems.FileObject; +import org.openide.filesystems.FileUtil; +import org.openide.util.Lookup; +import org.openide.util.Mutex; +import org.openide.util.lookup.AbstractLookup; +import org.openide.util.lookup.InstanceContent; +import org.openide.util.test.MockLookup; + +public class FallbackFactoryTest extends NbTestCase { + static { + // For easier testing. + TimedWeakReference.TIMEOUT = 1000; + } + + public FallbackFactoryTest(String name) { + super(name); + } + + private FileObject scratch; + private FileObject goodproject; + private FileObject goodproject2; + private FileObject badproject; + private FileObject mysteryproject; + private FileObject justADir; + private ProjectManager pm; + + protected @Override Level logLevel() { + return Level.FINE; + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + scratch = TestUtil.makeScratchDir(this); + goodproject = scratch.createFolder("good"); + goodproject.createFolder("testproject"); + goodproject2 = scratch.createFolder("good2"); + goodproject2.createFolder("testproject"); + badproject = scratch.createFolder("bad"); + badproject.createFolder("testproject").createData("broken"); + mysteryproject = scratch.createFolder("mystery"); + justADir = scratch.createFolder("justADir"); + MockLookup.setInstances(TestUtil.testProjectFactory()); + pm = ProjectManager.getDefault(); + NbProjectManagerAccessor.reset(); + } + + @Override + protected void tearDown() throws Exception { + scratch = null; + goodproject = null; + badproject = null; + mysteryproject = null; + pm = null; + super.tearDown(); + } + + + public void testIsFallbackProject() throws Exception { + var nothing = pm.findProject(justADir); + assertNull("No project is found for just a dir", nothing); + var generic = FallbackFactory.findProjectOrFallback(justADir); + assertNotNull("But one can ask for a fallback project", generic); + + var then = pm.findProject(justADir); + assertSame("since then findProject works for just a dir", generic, then); + + var ref = new WeakReference<>(generic); + generic = null; + then = null; + // give the references time to disappear + Thread.sleep(TimedWeakReference.TIMEOUT); + + assertGC("The fallback project gets GCed when no longer used", ref); + + var nothingAgain = pm.findProject(justADir); + assertNull("Since then, findProject again returns null", nothingAgain); + } + + public void testFallbackProjectLookup() throws Exception { + FileObject lookupDir = FileUtil.createFolder(FileUtil.getConfigRoot(), "Projects/org-netbeans-modules-project-fallback/Lookup"); + + var nothing = pm.findProject(justADir); + assertNull("No project is found for just a dir", nothing); + var fallbackProject = FallbackFactory.findProjectOrFallback(justADir); + assertNotNull("But one can ask for a fallback project", fallbackProject); + + var theProject = pm.findProject(justADir); + assertSame("since then findProject works for just a dir", fallbackProject, theProject); + + Sources src = theProject.getLookup().lookup(Sources.class); + assertNotNull("sources are provided", src); + SourceGroup[] genSrc = src.getSourceGroups(Sources.TYPE_GENERIC); + assertNotNull("They support the generic sources", genSrc); + assertEquals("They support the generic sources", 1, genSrc.length); + assertEquals("root is project root", theProject.getProjectDirectory(), genSrc[0].getRootFolder()); + + MockLookupForProject notFoundInLookup = theProject.getLookup().lookup(MockLookupForProject.class); + assertNull("No MockLookuptype found in project's lookup", notFoundInLookup); + + { + // create a registration in the lookup + FileObject mockLookupType = FileUtil.createData(lookupDir, MockLookupForProject.class.getName().replace(".", "-") + ".instance"); + MockLookupForProject found = FileUtil.getConfigObject(mockLookupType.getPath(), MockLookupForProject.class); + assertNotNull("MockLookupForProject is registered in the lookup directory", found); + } + + MockLookupForProject mockLookup = theProject.getLookup().lookup(MockLookupForProject.class); + assertNotNull("MockLookupForProject also found in project's lookup", mockLookup); + + ActionProvider ap = theProject.getLookup().lookup(ActionProvider.class); + assertNotNull("There are always actions for a project being merged together", ap); + { + var ap1 = new ActionProvider() { + @Override + public String[] getSupportedActions() { + return new String[] { "jedna", "dva" }; + } + + @Override + public void invokeAction(String command, Lookup context) throws IllegalArgumentException { + throw new UnsupportedOperationException(command); + } + + @Override + public boolean isActionEnabled(String command, Lookup context) throws IllegalArgumentException { + return false; + } + }; + + var ap2 = new ActionProvider() { + @Override + public String[] getSupportedActions() { + return new String[] { "odin", "dva" }; + } + + @Override + public void invokeAction(String command, Lookup context) throws IllegalArgumentException { + throw new UnsupportedOperationException(command); + } + + @Override + public boolean isActionEnabled(String command, Lookup context) throws IllegalArgumentException { + return false; + } + }; + + mockLookup.ic.add(ap1); + + String[] ap1Actions = theProject.getLookup().lookup(ActionProvider.class).getSupportedActions(); + assertEquals("Found two actions", 2, ap1Actions.length); + + mockLookup.ic.add(ap2); + String[] bothActions = theProject.getLookup().lookup(ActionProvider.class).getSupportedActions(); + assertEquals("Found three actions", 3, bothActions.length); + + assertSame("Action provider instance stays the same", ap, theProject.getLookup().lookup(ActionProvider.class)); + + ProjectInformation info = ProjectUtils.getInformation(theProject); + assertNotNull("Cannot be null", info); + assertEquals(theProject.getProjectDirectory().getNameExt(), info.getName()); + assertEquals("Folder " + justADir.getNameExt(), info.getDisplayName()); + assertNotNull("Icon is provided", info.getIcon()); + assertSame("Same icon as the group icon", info.getIcon(), genSrc[0].getIcon(true)); + } + } + + public static final class MockLookupForProject implements org.netbeans.spi.project.LookupProvider { + final InstanceContent ic = new InstanceContent(); + private final Lookup lkp = new AbstractLookup(ic); + { + ic.add(this); + } + + + @Override + public Lookup createAdditionalLookup(Lookup baseContext) { + return lkp; + } + } + + public void testFallbackProjectBecomesFileOwner() throws Exception { + FileObject script = justADir.createData("Hello.java"); + FileObject nested = justADir.createFolder("sub").createData("Other.java"); + assertNull("Loose file has no owner before", FileOwnerQuery.getOwner(script)); + assertFalse("justADir is not a project", pm.isProject(justADir)); + + Project fallback = FallbackFactory.findProjectOrFallback(justADir); + + assertNotNull("Project found", fallback); + assertTrue("Now the directory is recognized as a project", pm.isProject(justADir)); + assertSame("but findProject returns the fallback", fallback, pm.findProject(justADir)); + assertEquals("Now the justADir files have an owner", fallback, FileOwnerQuery.getOwner(script)); + assertEquals("Nested files also have an owner", fallback, FileOwnerQuery.getOwner(nested)); + } + + public void testFallbackReplacedAfterClearNonProjectCacheUnderWriteAccess() throws Exception { + Project fallback = FallbackFactory.findProjectOrFallback(justADir); + Project real = ProjectManager.mutex().writeAccess((Mutex.ExceptionAction) () -> { + // what project generators do: create metadata, clear cache, find project + justADir.createFolder("testproject"); + FallbackFactory.clear(justADir); + return pm.findProject(justADir); + }); + FallbackFactory.clear(justADir); + Project outside = pm.findProject(justADir); + assertNotNull("Project is found when mutex.writeAccess ends", outside); + assertNotSame("Outside project replaces the fallback", fallback, outside); + assertNotNull(real); + assertNotSame("Real project replaces the fallback", fallback, real); + assertSame("Outside and real are the same", outside, real); + } + + public void testReplacedFallbackIsNoLongerValid() throws Exception { + Project fallback = FallbackFactory.findProjectOrFallback(justADir); + justADir.createFolder("testproject"); + FallbackFactory.clear(justADir); + Project real = pm.findProject(justADir); + assertNotSame(fallback, real); + assertFalse("Replaced fallback should not be valid anymore", pm.isValid(fallback)); + } + +} diff --git a/ide/projectui/test/unit/src/org/netbeans/modules/project/ui/fallback/ProjectManagerFallbackTest.java b/ide/projectui/test/unit/src/org/netbeans/modules/project/ui/fallback/ProjectManagerFallbackTest.java new file mode 100644 index 000000000000..0255f9e20baf --- /dev/null +++ b/ide/projectui/test/unit/src/org/netbeans/modules/project/ui/fallback/ProjectManagerFallbackTest.java @@ -0,0 +1,101 @@ +/* + * 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 limitafile:/home/devel/NetBeansProjects/netbeans/ide/projectapi/test/unit/src/org/netbeans/api/project/ProjectManagerFallbackTest.java +tions + * under the License. + */ +package org.netbeans.modules.project.ui.fallback; + +import org.netbeans.api.project.FileOwnerQuery; +import org.netbeans.api.project.Project; +import org.netbeans.api.project.ProjectManager; +import org.netbeans.api.project.ProjectUtils; +import org.netbeans.api.project.SourceGroup; +import org.netbeans.api.project.Sources; +import org.netbeans.api.project.TestUtil; +import org.netbeans.junit.NbTestCase; +import org.netbeans.modules.projectapi.nb.NbProjectManagerAccessor; +import org.openide.filesystems.FileObject; +import org.openide.util.Mutex; +import org.openide.util.test.MockLookup; + +public class ProjectManagerFallbackTest extends NbTestCase { + + private ProjectManager pm; + private FileObject dir; + + public ProjectManagerFallbackTest(String name) { + super(name); + } + + @Override + protected void setUp() throws Exception { + FileObject scratch = TestUtil.makeScratchDir(this); + MockLookup.setInstances(TestUtil.testProjectFactory()); + pm = ProjectManager.getDefault(); + NbProjectManagerAccessor.reset(); + pm.clearNonProjectCache(); + dir = scratch.createFolder("folder"); + } + + public void testLooseJavaFileRemainsUnowned() throws Exception { + FileObject file = dir.createData("Hello.java"); + assertNull(FileOwnerQuery.getOwner(file)); + Project fallback = FallbackFactory.findProjectOrFallback(dir); + assertSame(fallback, pm.findProject(dir)); + assertEquals("Fallback project gains ownershipt", fallback, FileOwnerQuery.getOwner(file)); + } + + public void testReplacementUnderWriteLock() throws Exception { + Project fallback = FallbackFactory.findProjectOrFallback(dir); + Project real = ProjectManager.mutex().writeAccess((Mutex.ExceptionAction) () -> { + dir.createFolder("testproject"); + FallbackFactory.clear(dir); + return pm.findProject(dir); + }); + assertNotSame("Metadata must be recognized before leaving the write lock", fallback, real); + } + + public void testReplacedFallbackIsInvalid() throws Exception { + Project fallback = FallbackFactory.findProjectOrFallback(dir); + dir.createFolder("testproject"); + FallbackFactory.clear(dir); + assertNotSame(fallback, pm.findProject(dir)); + assertFalse("Replaced fallback must be invalid", pm.isValid(fallback)); + } + + public void testFactoryChangeUpdatesFileOwner() throws Exception { + FileObject file = dir.createData("Hello.java"); + Project fallback = FallbackFactory.findProjectOrFallback(dir); + assertSame(fallback, FileOwnerQuery.getOwner(file)); + dir.createFolder("testproject"); + MockLookup.setInstances(TestUtil.testProjectFactory()); + Project real = pm.findProject(dir); + assertNotSame("Fallback: " + fallback + " real: " + real, fallback, real); + final Project ownerOfFle = FileOwnerQuery.getOwner(file); + assertSame("Factory change must retire the cached fallback owner", real, ownerOfFle); + } + + public void testNestedFallbackContainsOwnRoot() throws Exception { + FileObject testProject = dir.createFolder("testproject"); + Project parent = pm.findProject(dir); + FileObject child = dir.createFolder("child"); + assertSame("Owned by " + testProject + " parent", parent, FileOwnerQuery.getOwner(child)); + Project fallback = FallbackFactory.findProjectOrFallback(child); + SourceGroup group = ProjectUtils.getSources(fallback).getSourceGroups(Sources.TYPE_GENERIC)[0]; + assertTrue("Every source group must contain its own root", group.contains(child)); + } +}