diff --git a/java.j2seproject/src/org/netbeans/modules/java/j2seproject/J2SEProjectConvertor.java b/java.j2seproject/src/org/netbeans/modules/java/j2seproject/J2SEProjectConvertor.java new file mode 100644 --- /dev/null +++ b/java.j2seproject/src/org/netbeans/modules/java/j2seproject/J2SEProjectConvertor.java @@ -0,0 +1,133 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2014 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2014 Sun Microsystems, Inc. + */ +package org.netbeans.modules.java.j2seproject; + +import java.io.IOException; +import java.io.InputStream; +import java.util.concurrent.Callable; +import javax.swing.Icon; +import org.netbeans.api.annotations.common.CheckForNull; +import org.netbeans.api.annotations.common.NonNull; +import org.netbeans.api.annotations.common.StaticResource; +import org.netbeans.api.project.Project; +import org.netbeans.api.project.ProjectManager; +import org.netbeans.modules.java.j2seproject.api.J2SEProjectBuilder; +import org.netbeans.spi.project.support.ant.AntProjectHelper; +import org.netbeans.spi.project.ui.ProjectConvertor; +import org.openide.filesystems.FileObject; +import org.openide.filesystems.FileUtil; +import org.openide.util.Exceptions; +import org.openide.util.ImageUtilities; +import org.openide.util.Lookup; +import org.openide.util.Parameters; +import org.openide.util.lookup.ServiceProvider; +import org.openide.xml.XMLUtil; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +/** + * + * @author Tomas Zezula + */ +@ServiceProvider(service = ProjectConvertor.class) +public class J2SEProjectConvertor implements ProjectConvertor { + + @StaticResource + private static final String ICON = "org/netbeans/modules/java/j2seproject/ui/resources/j2seProject.png"; //NOI18N + + @Override + public Result isProject(@NonNull final FileObject projectDirectory) { + final FileObject buildScript = projectDirectory.getFileObject("build.xml"); + if (buildScript != null) { + final String displayName = getDisplayName(buildScript); + return new Result( + Lookup.EMPTY, + new Factory(projectDirectory, displayName), + displayName, + ImageUtilities.image2Icon(ImageUtilities.loadImage(ICON))); + } + return null; + } + + private static final class Factory implements Callable { + private final FileObject projectDirectory; + private final String displayName; + + Factory( + @NonNull final FileObject projectDirectory, + @NonNull final String displayName) { + Parameters.notNull("projectDirectory", projectDirectory); //NOI18N + Parameters.notNull("displayName", displayName); //NOI18N + this.projectDirectory = projectDirectory; + this.displayName = displayName; + } + + @Override + public Project call() throws Exception { + final J2SEProjectBuilder pb = new J2SEProjectBuilder( + FileUtil.toFile(projectDirectory), + displayName); + pb.setBuildXmlName("nbbuild.xml"); + final AntProjectHelper helper = pb.build(); + return ProjectManager.getDefault().findProject(projectDirectory); + } + } + + @NonNull + private String getDisplayName(@NonNull final FileObject buildScript) { + String displayName = buildScript.getName(); + try (InputStream in = buildScript.getInputStream()) { + final InputSource is = new InputSource(in); + Document doc = XMLUtil.parse(is, false, false, null, null); + final Element prj = doc.getDocumentElement(); + if (prj != null && "project".equals(prj.getNodeName())) { //NOI18N + final String val = prj.getAttribute("name"); //NOI18N + if (val != null) { + displayName = val; + } + } + } catch (IOException | SAXException e) {} + return displayName; + } +} diff --git a/projectuiapi.base/apichanges.xml b/projectuiapi.base/apichanges.xml --- a/projectuiapi.base/apichanges.xml +++ b/projectuiapi.base/apichanges.xml @@ -107,7 +107,25 @@ - + + + Added ProjectConvertor adding an ability to convert a folder into a project. + + + + + +

+ Added an ability to convert a folder into a project. + For a folder accepted by the ProjectConvertor an artifical in memory + project is created causing the folder looks like a regular Project in the UI. + The folder is converted into a regular Project when the artificial Project + is opened. +

+
+ + +
Split the api into a desktop (swing, awt) and NetBeans dependent and independent part. diff --git a/projectuiapi.base/nbproject/project.properties b/projectuiapi.base/nbproject/project.properties --- a/projectuiapi.base/nbproject/project.properties +++ b/projectuiapi.base/nbproject/project.properties @@ -42,7 +42,7 @@ javac.compilerargs=-Xlint -Xlint:-serial javac.source=1.7 -spec.version.base=1.78.0 +spec.version.base=1.79.0 is.autoload=true javadoc.arch=${basedir}/arch.xml javadoc.apichanges=${basedir}/apichanges.xml diff --git a/projectuiapi.base/src/org/netbeans/modules/project/ui/ProjectConvertorFactory.java b/projectuiapi.base/src/org/netbeans/modules/project/ui/ProjectConvertorFactory.java new file mode 100644 --- /dev/null +++ b/projectuiapi.base/src/org/netbeans/modules/project/ui/ProjectConvertorFactory.java @@ -0,0 +1,368 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2014 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2014 Sun Microsystems, Inc. + */ +package org.netbeans.modules.project.ui; + +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.io.IOException; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import javax.swing.Icon; +import org.netbeans.api.annotations.common.CheckForNull; +import org.netbeans.api.annotations.common.NonNull; +import org.netbeans.api.annotations.common.NullAllowed; +import org.netbeans.api.project.Project; +import org.netbeans.api.project.ProjectInformation; +import org.netbeans.api.project.ProjectManager; +import org.netbeans.modules.project.uiapi.ProjectOpenedTrampoline; +import org.netbeans.spi.project.ProjectFactory; +import org.netbeans.spi.project.ProjectFactory2; +import org.netbeans.spi.project.ProjectState; +import org.netbeans.spi.project.ui.ProjectConvertor; +import org.netbeans.spi.project.ui.ProjectOpenedHook; +import org.openide.filesystems.FileObject; +import org.openide.util.Exceptions; +import org.openide.util.Lookup; +import org.openide.util.LookupEvent; +import org.openide.util.LookupListener; +import org.openide.util.Mutex; +import org.openide.util.MutexException; +import org.openide.util.Parameters; +import org.openide.util.WeakListeners; +import org.openide.util.lookup.Lookups; +import org.openide.util.lookup.ProxyLookup; +import org.openide.util.lookup.ServiceProvider; + +/** + * + * @author Tomas Zezula + */ +@ServiceProvider(service = ProjectFactory.class, position = Integer.MAX_VALUE) +public final class ProjectConvertorFactory implements ProjectFactory2 { + + private final Lookup.Result convertors; + private final Set excluded; + + public ProjectConvertorFactory() { + this.convertors = Lookup.getDefault().lookupResult(ProjectConvertor.class); + this.excluded = new HashSet<>(); + } + + @Override + @CheckForNull + public ProjectManager.Result isProject2(@NonNull FileObject projectDirectory) { + Parameters.notNull("projectDirectory", projectDirectory); //NOI18N + final ProjectConvertor.Result res = isProjectImpl(projectDirectory); + return res != null ? + toProjectManagerResult(res) : + null; + } + + @Override + public boolean isProject(@NonNull FileObject projectDirectory) { + return isProject2(projectDirectory) != null; + } + + @Override + @CheckForNull + public Project loadProject( + @NonNull final FileObject projectDirectory, + @NonNull final ProjectState state) throws IOException { + Parameters.notNull("projectDirectory", projectDirectory); //NOI18N + Parameters.notNull("state", state); //NOI18N + final ProjectConvertor.Result res = isProjectImpl(projectDirectory); + return res != null ? + new ConvertorProject(projectDirectory, state, res): + null; + } + + @Override + public void saveProject(@NonNull final Project project) throws IOException, ClassCastException { + Parameters.notNull("project", project); //NOI18N + throw new IllegalStateException("ConvertorProject cannot be modified"); //NOI18N + } + + @CheckForNull + private ProjectConvertor.Result isProjectImpl(@NonNull final FileObject projectDirectory) { + return ProjectManager.mutex().readAccess(new Mutex.Action() { + @Override + public ProjectConvertor.Result run() { + if (!excluded.contains(projectDirectory)) { + for (ProjectConvertor pc : convertors.allInstances()) { + ProjectConvertor.Result result = pc.isProject(projectDirectory); + if (result != null) { + return result; + } + } + } + return null; + } + }); + } + + @NonNull + private ProjectManager.Result toProjectManagerResult(@NonNull final ProjectConvertor.Result res) { + return new ProjectManager.Result(res.getDisplayName(), null, res.getIcon()); + } + + private final class ConvertorProject implements Project { + private final FileObject projectDirectory; + private final ProjectState projectState; + private final ProjectConvertor.Result result; + private final DynamicLookup projectLkp; + + ConvertorProject( + @NonNull final FileObject projectDirectory, + @NonNull final ProjectState projectState, + @NonNull final ProjectConvertor.Result result) { + Parameters.notNull("projectDirectory", projectDirectory); //NOI18N + Parameters.notNull("projectState", projectState); //NOI18N + Parameters.notNull("result", result); //NOI18N + this.projectDirectory = projectDirectory; + this.projectState = projectState; + this.result = result; + final Lookup convertorLkp = this.result.getLookup(); + if (convertorLkp == null) { + throw new IllegalStateException(String.format( + "Convertor: %s returned null lookup.", //NOI18N + this.result)); + } + this.projectLkp = new DynamicLookup(); + final Lookup preLkp = Lookups.singleton(new OpenHook()); + final Lookup postLkp = Lookups.singleton(new ProjectInfo(projectLkp)); + this.projectLkp.update(preLkp, convertorLkp, postLkp); + } + + @Override + @NonNull + public FileObject getProjectDirectory() { + return projectDirectory; + } + + @Override + @NonNull + public Lookup getLookup() { + return projectLkp; + } + + @Override + public int hashCode() { + return projectDirectory.hashCode(); + } + + @Override + public boolean equals(@NullAllowed final Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof Project)) { + return false; + } + return projectDirectory.equals(((Project)obj).getProjectDirectory()); + } + + @NonNull + private Project createProject() throws IOException { + try { + return ProjectManager.mutex().writeAccess(new Mutex.ExceptionAction() { + @Override + public Project run() throws Exception { + excluded.add(projectDirectory); + try { + projectState.notifyDeleted(); + final Project prj = result.createProject(); + if (prj == null) { + throw new IllegalStateException(String.format( + "The convertor %s created null project.", //NOI18N + result)); + } + //Set the Lookup to the created project lookup + //Remove OpenHook as the OpenProjectList calls all POH in + //the project's Lookup even there the non merged, so it's safer + //to remove it. + //Also remove ProjectInfo as it's overriden by project's own + //no need for it anymore + projectLkp.update(prj.getLookup()); + return prj; + } finally { + excluded.remove(projectDirectory); + } + } + }); + } catch (final MutexException e) { + final Exception root = e.getException(); + if (root instanceof RuntimeException) { + throw (RuntimeException) root; + } else if (root instanceof IOException) { + throw (IOException) root; + } else { + throw new RuntimeException(root); + } + } + } + + private final class ProjectInfo implements ProjectInformation, LookupListener { + + private final PropertyChangeSupport pcs; + private final Lookup.Result eventSource; + private volatile ProjectInformation delegate; + + ProjectInfo(@NonNull final Lookup prjLookup) { + Parameters.notNull("prjLookup", prjLookup); //NOI18N + this.pcs = new PropertyChangeSupport(this); + this.eventSource = prjLookup.lookupResult(ProjectInformation.class); + this.eventSource.addLookupListener(WeakListeners.create(LookupListener.class, this, eventSource)); + } + + @Override + @NonNull + public String getName() { + final ProjectInformation d = delegate; + return d != null ? + d.getName() : + projectDirectory.getName(); + } + + @Override + @NonNull + public String getDisplayName() { + final ProjectInformation d = delegate; + if (d != null) { + return d.getDisplayName(); + } else { + String res = result.getDisplayName(); + if (res == null) { + res = getName(); + } + return res; + } + } + + @Override + @NonNull + public Icon getIcon() { + final ProjectInformation d = delegate; + if (d != null) { + return d.getIcon(); + } else { + Icon res = result.getIcon(); + //Todo: Handle null res + return res; + } + } + + @Override + @NonNull + public Project getProject() { + final ProjectInformation d = delegate; + if (d != null) { + return d.getProject(); + } else { + return ConvertorProject.this; + } + } + + @Override + public void addPropertyChangeListener(@NonNull final PropertyChangeListener listener) { + Parameters.notNull("listener", listener); //NOI18N + pcs.addPropertyChangeListener(listener); + } + + @Override + public void removePropertyChangeListener(@NonNull final PropertyChangeListener listener) { + Parameters.notNull("listener", listener); //NOI18N + pcs.removePropertyChangeListener(listener); + } + + @Override + public void resultChanged(LookupEvent ev) { + //In case someone holds this transient ProjectInfo + //keep it alive and delegate to real one. + final Collection instances = eventSource.allInstances(); + if (!instances.isEmpty()) { + final ProjectInformation instance = instances.iterator().next(); + if (instance != this) { + delegate = instance; + pcs.firePropertyChange(PROP_NAME, null, null); + pcs.firePropertyChange(PROP_DISPLAY_NAME, null, null); + pcs.firePropertyChange(PROP_ICON, null, null); + } + } + } + } + + private final class OpenHook extends ProjectOpenedHook { + + @Override + protected void projectOpened() { + try { + final Project prj = createProject(); + for( ProjectOpenedHook hook : prj.getLookup().lookupAll(ProjectOpenedHook.class)) { + ProjectOpenedTrampoline.DEFAULT.projectOpened(hook); + } + } catch (IOException ioe) { + Exceptions.printStackTrace(ioe); + } + } + + @Override + protected void projectClosed() { + //Not called as the OpenHook is removed from project's Lookup + //anyway it should do nothing as the OpenProjectList call all + //POH in registered in the project's Lookup + } + } + } + + private static final class DynamicLookup extends ProxyLookup { + + DynamicLookup() { + super(); + } + + void update(@NonNull final Lookup... lkps) { + Parameters.notNull("lkps", lkps); //NOI18N + setLookups(lkps); + } + } +} diff --git a/projectuiapi.base/src/org/netbeans/spi/project/ui/ProjectConvertor.java b/projectuiapi.base/src/org/netbeans/spi/project/ui/ProjectConvertor.java new file mode 100644 --- /dev/null +++ b/projectuiapi.base/src/org/netbeans/spi/project/ui/ProjectConvertor.java @@ -0,0 +1,154 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2014 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2014 Sun Microsystems, Inc. + */ +package org.netbeans.spi.project.ui; + +import java.io.IOException; +import java.util.concurrent.Callable; +import javax.swing.Icon; +import org.netbeans.api.annotations.common.CheckForNull; +import org.netbeans.api.annotations.common.NonNull; +import org.netbeans.api.annotations.common.NullAllowed; +import org.netbeans.api.project.Project; +import org.netbeans.api.project.ProjectInformation; +import org.openide.filesystems.FileObject; +import org.openide.util.Lookup; +import org.openide.util.Parameters; + +/** + * The ability to convert a folder into a project. + * The implementation are registered in the global lookup. + * For the folder accepted by the {@link ProjectConvertor} an artifical in memory + * project is created causing the folder looks like a regular {@link Project} in the UI. + * The folder is converted into a regular {@link Project} when the artificial {@link Project} + * is opened. + * @author Tomas Zezula + * @since 1.79 + */ +public interface ProjectConvertor { + /** + * Checks if given folder can be converted into a {@link Project}. + * @param projectDirectory the folder to check + * @return the {@link ProjectConvertor.Result} if the folder can be + * converted to a {@link Project} or null. + */ + @CheckForNull + Result isProject(@NonNull FileObject projectDirectory); + + /** + * The result of project check. + */ + public final class Result implements Lookup.Provider { + + private final Lookup lkp; + private final Callable projectFactory; + private final String displayName; + private final Icon icon; + + /** + * Creates a {@link Result}. + * @param lookup the transient {@link Project} {@link Lookup} which may contain additional project + * services. The {@link ProjectInformation} is added automatically but can be + * overridden by a custom implementation in this lookup + * @param projectFactory the factory method converting a folder into {@link Project} + * @param displayName the {@link Project} display name may be null + * @param icon the {@link Project} icon may be null + */ + public Result( + @NonNull final Lookup lookup, + @NonNull final Callable projectFactory, + @NullAllowed final String displayName, + @NullAllowed final Icon icon) { + Parameters.notNull("lookup", lookup); //NOI18N + Parameters.notNull("projectFactory", projectFactory); //NOI18N + this.lkp = lookup; + this.projectFactory = projectFactory; + this.displayName = displayName; + this.icon = icon; + } + + /** + * Returns the {@link Project} display name. + * @return the display name + */ + @CheckForNull + public String getDisplayName() { + return displayName; + } + + /** + * Returns the {@link Project} icon. + * @return the icon + */ + @CheckForNull + public Icon getIcon() { + return icon; + } + + /** + * Converts the folder into the {@link Project}. + * @return the created {@link Project} + * @throws IOException in case of error + */ + @NonNull + public Project createProject() throws IOException { + try { + return projectFactory.call(); + } catch (final Exception e) { + if (e instanceof IOException) { + throw (IOException) e; + } else { + throw new IOException(e); + } + } + } + + /** + * The {@link Lookup} with additional {@link Project} services. + * @return the {@link Project}'s {@link Lookup}. + */ + @Override + @NonNull + public Lookup getLookup() { + return lkp; + } + } +} diff --git a/projectuiapi.base/test/unit/src/org/netbeans/modules/project/ui/ProjectConvertorFactoryTest.java b/projectuiapi.base/test/unit/src/org/netbeans/modules/project/ui/ProjectConvertorFactoryTest.java new file mode 100644 --- /dev/null +++ b/projectuiapi.base/test/unit/src/org/netbeans/modules/project/ui/ProjectConvertorFactoryTest.java @@ -0,0 +1,237 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2014 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2014 Sun Microsystems, Inc. + */ +package org.netbeans.modules.project.ui; + +import java.beans.PropertyChangeListener; +import java.io.File; +import java.io.IOException; +import java.util.concurrent.Callable; +import javax.swing.Icon; +import org.netbeans.api.annotations.common.NonNull; +import org.netbeans.api.project.Project; +import org.netbeans.api.project.ProjectInformation; +import org.netbeans.api.project.ProjectManager; +import org.netbeans.api.project.ui.OpenProjects; +import org.netbeans.junit.MockServices; +import org.netbeans.junit.NbTestCase; +import org.openide.filesystems.FileObject; +import org.openide.filesystems.FileUtil; +import org.openide.util.Exceptions; +import org.openide.util.Lookup; +import org.openide.util.Parameters; +import org.openide.util.lookup.Lookups; +import org.openide.util.test.MockPropertyChangeListener; + +/** + * + * @author Tomas Zezula + */ +public class ProjectConvertorFactoryTest extends NbTestCase { + + private FileObject projectDir; + + public ProjectConvertorFactoryTest(String name) { + super(name); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + clearWorkDir(); + MockServices.setServices( + TestProject.Factory.class, + TestProject.Convertor.class, + TestTrampoline.class); + projectDir = createCheckout(getWorkDir()); + } + + public void testProjectConvertor() throws IOException { + final TestProject.OpenHook oh1 = new TestProject.OpenHook(); + final TestProject.OpenHook oh2 = new TestProject.OpenHook(); + TestProject.Factory.LOOKUP_FACTORY = new Callable() { + @Override + public Lookup call() throws Exception { + return Lookups.fixed( + oh1, + oh2, + new TestPI(projectDir), + new ProjectAdditionalService()); + } + }; + TestProject.Convertor.LOOKUP_FACTORY = new Callable() { + @Override + public Lookup call() throws Exception { + return Lookups.singleton( + new ConvertorAdditionalService()); + } + }; + final Project artPrj = ProjectManager.getDefault().findProject(projectDir); + assertNotNull(artPrj); + assertEquals(projectDir, artPrj.getProjectDirectory()); + final ProjectInformation artPi = artPrj.getLookup().lookup(ProjectInformation.class); + assertNotNull(artPi); + assertEquals(projectDir.getName(), artPi.getDisplayName()); + assertNotNull(artPrj.getLookup().lookup(ConvertorAdditionalService.class)); + assertNull(artPrj.getLookup().lookup(ProjectAdditionalService.class)); + final MockPropertyChangeListener ml = new MockPropertyChangeListener( + ProjectInformation.PROP_DISPLAY_NAME, + ProjectInformation.PROP_NAME, + ProjectInformation.PROP_ICON); + artPi.addPropertyChangeListener(ml); + OpenProjects.getDefault().open(new Project[]{artPrj}, false); + assertTrue(OpenProjects.getDefault().isProjectOpen(artPrj)); + assertNotNull(projectDir.getFileObject(TestProject.PROJECT_MARKER)); + final Project realPrj = ProjectManager.getDefault().findProject(projectDir); + assertNotNull(realPrj); + assertNotSame(artPrj, realPrj); + assertNotSame(artPrj.getClass(), realPrj.getClass()); + assertEquals(artPrj, realPrj); + assertNull(realPrj.getLookup().lookup(ConvertorAdditionalService.class)); + assertNotNull(realPrj.getLookup().lookup(ProjectAdditionalService.class)); + assertEquals(1, oh1.openCalls.get()); + assertEquals(0, oh1.closeCalls.get()); + assertEquals(1, oh2.openCalls.get()); + assertEquals(0, oh2.closeCalls.get()); + OpenProjects.getDefault().close(new Project[]{realPrj}); + assertFalse(OpenProjects.getDefault().isProjectOpen(artPrj)); + assertFalse(OpenProjects.getDefault().isProjectOpen(realPrj)); + assertEquals(1, oh1.openCalls.get()); + assertEquals(1, oh1.closeCalls.get()); + assertEquals(1, oh2.openCalls.get()); + assertEquals(1, oh2.closeCalls.get()); + //Artificial ProjectInformation should be updated and events should be fired + assertEquals(projectDir.getPath(), artPi.getDisplayName()); + ml.assertEvents( + ProjectInformation.PROP_DISPLAY_NAME, + ProjectInformation.PROP_NAME, + ProjectInformation.PROP_ICON); + } + + public void testProjectConvertorWithExplicitProjectInfo() throws IOException { + TestProject.Factory.LOOKUP_FACTORY = new Callable() { + @Override + public Lookup call() throws Exception { + return Lookups.fixed( + new ProjectAdditionalService()); + } + }; + final ProjectInformation testPI = new TestPI(projectDir); + TestProject.Convertor.LOOKUP_FACTORY = new Callable() { + @Override + public Lookup call() throws Exception { + return Lookups.fixed( + new ConvertorAdditionalService(), + testPI); + } + }; + final Project artPrj = ProjectManager.getDefault().findProject(projectDir); + assertNotNull(artPrj); + assertEquals(projectDir, artPrj.getProjectDirectory()); + assertNotNull(artPrj.getLookup().lookup(ProjectInformation.class)); + assertEquals(projectDir.getPath(), artPrj.getLookup().lookup(ProjectInformation.class).getDisplayName()); + assertNotNull(artPrj.getLookup().lookup(ConvertorAdditionalService.class)); + assertNull(artPrj.getLookup().lookup(ProjectAdditionalService.class)); + OpenProjects.getDefault().open(new Project[]{artPrj}, false); + assertTrue(OpenProjects.getDefault().isProjectOpen(artPrj)); + assertNotNull(projectDir.getFileObject(TestProject.PROJECT_MARKER)); + final Project realPrj = ProjectManager.getDefault().findProject(projectDir); + assertNotNull(realPrj); + assertNotSame(artPrj, realPrj); + assertNotSame(artPrj.getClass(), realPrj.getClass()); + assertEquals(artPrj, realPrj); + assertNull(realPrj.getLookup().lookup(ConvertorAdditionalService.class)); + assertNotNull(realPrj.getLookup().lookup(ProjectAdditionalService.class)); + } + + @NonNull + private static FileObject createCheckout(@NonNull final File workDir) throws IOException { + final FileObject projectDir = FileUtil.createFolder( + FileUtil.toFileObject(workDir), + "checkout"); //NOI18N + projectDir.createData(TestProject.CONVERTOR_MARKER); //NOI18N + return projectDir; + } + + private static final class ConvertorAdditionalService {} + private static final class ProjectAdditionalService {} + private static final class TestPI implements ProjectInformation { + + private final FileObject projectDir; + + TestPI(@NonNull final FileObject projectDir) { + Parameters.notNull("projectDir", projectDir); + this.projectDir = projectDir; + } + + @Override + public String getName() { + return projectDir.getPath(); + } + + @Override + public String getDisplayName() { + return getName(); + } + + @Override + public Icon getIcon() { + return null; + } + + @Override + public Project getProject() { + try { + return ProjectManager.getDefault().findProject(projectDir); + } catch (IOException | IllegalArgumentException ex) { + return null; + } + } + + @Override + public void addPropertyChangeListener(PropertyChangeListener listener) { + } + + @Override + public void removePropertyChangeListener(PropertyChangeListener listener) { + } + } +} diff --git a/projectuiapi.base/test/unit/src/org/netbeans/modules/project/ui/TestProject.java b/projectuiapi.base/test/unit/src/org/netbeans/modules/project/ui/TestProject.java new file mode 100644 --- /dev/null +++ b/projectuiapi.base/test/unit/src/org/netbeans/modules/project/ui/TestProject.java @@ -0,0 +1,195 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2014 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2014 Sun Microsystems, Inc. + */ +package org.netbeans.modules.project.ui; + +import java.io.IOException; +import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicInteger; +import org.netbeans.api.annotations.common.NonNull; +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.netbeans.spi.project.ui.ProjectConvertor; +import org.netbeans.spi.project.ui.ProjectOpenedHook; +import org.openide.filesystems.FileObject; +import org.openide.util.Lookup; +import org.openide.util.Parameters; +import org.openide.util.lookup.Lookups; + +/** + * + * @author Tomas Zezula + */ +public final class TestProject implements Project { + + static final String PROJECT_MARKER = "nbproject"; //NOI18N + static final String CONVERTOR_MARKER = "build.gradle"; //NOI18N + + private final FileObject projectDirectory; + private final ProjectState state; + private final Lookup lkp; + + TestProject( + @NonNull final FileObject projectDirectory, + @NonNull final ProjectState state, + @NonNull final Lookup lkp) { + Parameters.notNull("projectDirectory", projectDirectory); //NOI18N + Parameters.notNull("state", state); //NOI18N + Parameters.notNull("lkp", lkp); //NOI18N + this.projectDirectory = projectDirectory; + this.state = state; + this.lkp = lkp; + } + + @Override + @NonNull + public FileObject getProjectDirectory() { + return projectDirectory; + } + + @Override + @NonNull + public Lookup getLookup() { + return lkp; + } + + @Override + public int hashCode() { + return projectDirectory.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Project)) { + return false; + } + return projectDirectory.equals(((Project)obj).getProjectDirectory()); + } + + public static final class Factory implements ProjectFactory { + + public static volatile Callable LOOKUP_FACTORY = DefaultLookupFactory.INSTANCE; + + public Factory() { + } + + @Override + public boolean isProject(FileObject projectDirectory) { + return projectDirectory.getFileObject(PROJECT_MARKER) != null; + } + + @Override + public Project loadProject(FileObject projectDirectory, ProjectState state) throws IOException { + if (isProject(projectDirectory)) { + try { + return new TestProject(projectDirectory, state, LOOKUP_FACTORY.call()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + return null; + } + + @Override + public void saveProject(Project project) throws IOException, ClassCastException { + } + } + + public static final class Convertor implements ProjectConvertor { + + public static volatile Callable LOOKUP_FACTORY = DefaultLookupFactory.INSTANCE; + + @Override + public Result isProject(@NonNull final FileObject projectDirectory) { + if (projectDirectory.getFileObject(CONVERTOR_MARKER) != null) { + try { + return new Result( + LOOKUP_FACTORY.call(), + new Callable() { + @Override + public Project call() throws Exception { + projectDirectory.createFolder(PROJECT_MARKER); + return ProjectManager.getDefault().findProject(projectDirectory); + } + }, + projectDirectory.getName(), + null); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + return null; + } + } + + static final class OpenHook extends ProjectOpenedHook { + + final AtomicInteger openCalls = new AtomicInteger(); + final AtomicInteger closeCalls = new AtomicInteger(); + + @Override + protected void projectOpened() { + openCalls.incrementAndGet(); + } + + @Override + protected void projectClosed() { + closeCalls.incrementAndGet(); + } + } + + static class DefaultLookupFactory implements Callable { + + static final DefaultLookupFactory INSTANCE = new DefaultLookupFactory(); + + private DefaultLookupFactory() {} + + @Override + public Lookup call() throws Exception { + return Lookup.EMPTY; + } + } +} diff --git a/projectuiapi.base/test/unit/src/org/netbeans/modules/project/ui/TestTrampoline.java b/projectuiapi.base/test/unit/src/org/netbeans/modules/project/ui/TestTrampoline.java new file mode 100644 --- /dev/null +++ b/projectuiapi.base/test/unit/src/org/netbeans/modules/project/ui/TestTrampoline.java @@ -0,0 +1,219 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2014 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2014 Sun Microsystems, Inc. + */ +package org.netbeans.modules.project.ui; + +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.util.HashSet; +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.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import org.netbeans.api.project.Project; +import org.netbeans.api.project.ui.OpenProjects; +import org.netbeans.api.project.ui.ProjectGroup; +import org.netbeans.api.project.ui.ProjectGroupChangeListener; +import org.netbeans.insane.live.CancelException; +import org.netbeans.modules.project.uiapi.OpenProjectsTrampoline; +import org.netbeans.modules.project.uiapi.ProjectOpenedTrampoline; +import org.netbeans.spi.project.ui.ProjectOpenedHook; + +/** + * + * @author Tomas Zezula + */ +public final class TestTrampoline implements OpenProjectsTrampoline { + + private final PropertyChangeSupport support = new PropertyChangeSupport(this); + private final ReadWriteLock rwLock = new ReentrantReadWriteLock(); + private final Set opened = new HashSet<>(); + private volatile Project mainProject; + + @Override + public Project[] getOpenProjectsAPI() { + rwLock.readLock().lock(); + try { + return opened.toArray(new Project[opened.size()]); + } finally { + rwLock.readLock().unlock(); + } + } + + @Override + public void openAPI(Project[] projects, boolean openRequiredProjects, boolean showProgress) { + final Set justOpened = new HashSet<>(); + rwLock.writeLock().lock(); + try { + for (Project p : projects) { + if (opened.add(p)) { + justOpened.add(p); + } + } + } finally { + rwLock.writeLock().unlock(); + } + for (Project p : justOpened) { + for (ProjectOpenedHook hook : p.getLookup().lookupAll(ProjectOpenedHook.class)) { + ProjectOpenedTrampoline.DEFAULT.projectOpened(hook); + } + } + if (!justOpened.isEmpty()) { + support.firePropertyChange(OpenProjects.PROPERTY_OPEN_PROJECTS, null, null); + } + } + + @Override + public void closeAPI(Project[] projects) { + final Set justClosed = new HashSet<>(); + rwLock.writeLock().lock(); + try { + for (Project p : projects) { + if (opened.remove(p)) { + justClosed.add(p); + } + } + } finally { + rwLock.writeLock().unlock(); + } + for (Project p : justClosed) { + for (ProjectOpenedHook hook : p.getLookup().lookupAll(ProjectOpenedHook.class)) { + ProjectOpenedTrampoline.DEFAULT.projectClosed(hook); + } + } + if (!justClosed.isEmpty()) { + support.firePropertyChange(OpenProjects.PROPERTY_OPEN_PROJECTS, null, null); + } + } + + @Override + public void addPropertyChangeListenerAPI(PropertyChangeListener listener, Object source) { + this.support.addPropertyChangeListener(listener); + } + + @Override + public void removePropertyChangeListenerAPI(PropertyChangeListener listener) { + this.support.removePropertyChangeListener(listener); + } + + @Override + public Future openProjectsAPI() { + return new F(); + } + + + @Override + public Project getMainProject() { + return mainProject; + } + + @Override + public void setMainProject(Project project) { + mainProject = project; + support.firePropertyChange(OpenProjects.PROPERTY_MAIN_PROJECT, null, null); + } + + @Override + public ProjectGroup getActiveProjectGroupAPI() { + return null; + } + + @Override + public void addProjectGroupChangeListenerAPI(ProjectGroupChangeListener listener) { + } + + @Override + public void removeProjectGroupChangeListenerAPI(ProjectGroupChangeListener listener) { + } + + private final class F implements Future { + + private volatile Project[] res; + private volatile boolean canceled; + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return canceled = true; + } + + @Override + public boolean isCancelled() { + return canceled; + } + + @Override + public boolean isDone() { + return canceled || res != null; + } + + @Override + public Project[] get() throws InterruptedException, ExecutionException { + if (canceled) { + throw new CancelException(); + } + if (res != null) { + return res; + } + return res = getOpenProjectsAPI(); + } + + @Override + public Project[] get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + if (canceled) { + throw new CancelException(); + } + if (res != null) { + return res; + } + if (rwLock.readLock().tryLock(timeout, unit)) { + try { + return res = getOpenProjectsAPI(); + }finally { + rwLock.readLock().unlock(); + } + } + throw new TimeoutException(); + } + } +}