blob: 7d31dc3818accf8fe8f02c3ed2a562a0c4689e6b [file] [log] [blame]
/*
* 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.awt.Color;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.text.Collator;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.swing.DefaultListModel;
import javax.swing.Icon;
import javax.swing.JFileChooser;
import javax.swing.ListModel;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.filechooser.FileFilter;
import javax.swing.filechooser.FileView;
import org.netbeans.api.project.Project;
import org.netbeans.api.project.ProjectManager;
import org.netbeans.api.project.ProjectUtils;
import org.netbeans.api.queries.CollocationQuery;
import org.netbeans.spi.project.ProjectContainerProvider;
import org.netbeans.spi.project.SubprojectProvider;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.util.Cancellable;
import org.openide.util.Exceptions;
import org.openide.util.NbBundle;
import org.openide.util.RequestProcessor;
import org.openide.util.Utilities;
/**
* Special component on side of project filechooser.
*/
public class ProjectChooserAccessory extends javax.swing.JPanel
implements ActionListener, PropertyChangeListener {
private RequestProcessor.Task updateSubprojectsTask;
private RequestProcessor.Task displayNameTask;
private RequestProcessor RP;
private RequestProcessor RP2;
ModelUpdater modelUpdater; //#101227 -> non-private
private Map<Project,Set<? extends Project>> subprojectsCache = new HashMap<Project,Set<? extends Project>>(); // #59098
/** Creates new form ProjectChooserAccessory */
public ProjectChooserAccessory(JFileChooser chooser, boolean isOpenSubprojects) {
initComponents();
modelUpdater = new ModelUpdater();
//#98080
RP = new RequestProcessor(ModelUpdater.class.getName(), 1);
RP2 = new RequestProcessor(ModelUpdater.class.getName(), 1);
updateSubprojectsTask = RP.create(modelUpdater);
updateSubprojectsTask.setPriority( Thread.MIN_PRIORITY );
// Listen on the subproject checkbox to change the option accordingly
jCheckBoxSubprojects.setSelected( isOpenSubprojects );
jCheckBoxSubprojects.addActionListener( this );
// Listen on the chooser to update the Accessory
chooser.addPropertyChangeListener( this );
// Set default list model for the subprojects list
jListSubprojects.setModel( new DefaultListModel() );
// Disable the Accessory. JFileChooser does not select a file
// by default
setAccessoryEnablement( false, 0 );
}
/** This method is called from within the constructor to
* initialize the form.
* WARNING: Do NOT modify this code. The content of this method is
* always regenerated by the Form Editor.
*/
// <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
private void initComponents() {
java.awt.GridBagConstraints gridBagConstraints;
jLabelProjectName = new javax.swing.JLabel();
jTextFieldProjectName = new javax.swing.JTextField();
jCheckBoxSubprojects = new javax.swing.JCheckBox();
jScrollPaneSubprojects = new javax.swing.JScrollPane();
jListSubprojects = new javax.swing.JList();
setBorder(javax.swing.BorderFactory.createEmptyBorder(0, 12, 0, 0));
setLayout(new java.awt.GridBagLayout());
jLabelProjectName.setLabelFor(jTextFieldProjectName);
org.openide.awt.Mnemonics.setLocalizedText(jLabelProjectName, org.openide.util.NbBundle.getMessage(ProjectChooserAccessory.class, "LBL_PrjChooser_ProjectName_Label")); // NOI18N
gridBagConstraints = new java.awt.GridBagConstraints();
gridBagConstraints.gridwidth = java.awt.GridBagConstraints.REMAINDER;
gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST;
gridBagConstraints.insets = new java.awt.Insets(0, 0, 2, 0);
add(jLabelProjectName, gridBagConstraints);
jLabelProjectName.getAccessibleContext().setAccessibleName(org.openide.util.NbBundle.getMessage(ProjectChooserAccessory.class, "AN_ProjectName")); // NOI18N
jLabelProjectName.getAccessibleContext().setAccessibleDescription(org.openide.util.NbBundle.getMessage(ProjectChooserAccessory.class, "AD_ProjectName")); // NOI18N
jTextFieldProjectName.setEditable(false);
gridBagConstraints = new java.awt.GridBagConstraints();
gridBagConstraints.gridwidth = java.awt.GridBagConstraints.REMAINDER;
gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
gridBagConstraints.weightx = 1.0;
gridBagConstraints.insets = new java.awt.Insets(0, 0, 6, 0);
add(jTextFieldProjectName, gridBagConstraints);
org.openide.awt.Mnemonics.setLocalizedText(jCheckBoxSubprojects, org.openide.util.NbBundle.getMessage(ProjectChooserAccessory.class, "LBL_PrjChooser_Subprojects_CheckBox")); // NOI18N
jCheckBoxSubprojects.setMargin(new java.awt.Insets(2, 0, 2, 2));
gridBagConstraints = new java.awt.GridBagConstraints();
gridBagConstraints.gridwidth = java.awt.GridBagConstraints.REMAINDER;
gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST;
gridBagConstraints.insets = new java.awt.Insets(0, 0, 2, 0);
add(jCheckBoxSubprojects, gridBagConstraints);
jCheckBoxSubprojects.getAccessibleContext().setAccessibleDescription(org.openide.util.NbBundle.getMessage(ProjectChooserAccessory.class, "ACSD_ProjectChooserAccessory_jCheckBoxSubprojects")); // NOI18N
jListSubprojects.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION);
jListSubprojects.setEnabled(false);
jScrollPaneSubprojects.setViewportView(jListSubprojects);
jListSubprojects.getAccessibleContext().setAccessibleName(org.openide.util.NbBundle.getMessage(ProjectChooserAccessory.class, "ACSN_ProjectChooserAccessory_jListSubprojects")); // NOI18N
jListSubprojects.getAccessibleContext().setAccessibleDescription(org.openide.util.NbBundle.getMessage(ProjectChooserAccessory.class, "ACSD_ProjectChooserAccessory_jListSubprojects")); // NOI18N
gridBagConstraints = new java.awt.GridBagConstraints();
gridBagConstraints.gridwidth = java.awt.GridBagConstraints.REMAINDER;
gridBagConstraints.gridheight = java.awt.GridBagConstraints.REMAINDER;
gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
gridBagConstraints.weightx = 1.0;
gridBagConstraints.weighty = 1.0;
add(jScrollPaneSubprojects, gridBagConstraints);
}// </editor-fold>//GEN-END:initComponents
// Variables declaration - do not modify//GEN-BEGIN:variables
private javax.swing.JCheckBox jCheckBoxSubprojects;
private javax.swing.JLabel jLabelProjectName;
private javax.swing.JList jListSubprojects;
private javax.swing.JScrollPane jScrollPaneSubprojects;
private javax.swing.JTextField jTextFieldProjectName;
// End of variables declaration//GEN-END:variables
// Implementation of action listener ---------------------------------------
@Override
public void actionPerformed( ActionEvent e ) {
if ( e.getSource() == jCheckBoxSubprojects ) {
OpenProjectListSettings.getInstance().setOpenSubprojects( jCheckBoxSubprojects.isSelected() );
}
}
@Override
public void propertyChange( PropertyChangeEvent e ) {
if ( JFileChooser.SELECTED_FILE_CHANGED_PROPERTY.equals( e.getPropertyName() ) ||
JFileChooser.SELECTED_FILES_CHANGED_PROPERTY.equals( e.getPropertyName() ) ) {
// We have to update the Accessory
JFileChooser chooser = (JFileChooser)e.getSource();
final ListModel spListModel = jListSubprojects.getModel();
final File[] projectDirs;
if ( chooser.isMultiSelectionEnabled() ) {
projectDirs = chooser.getSelectedFiles();
}
else {
projectDirs = new File[] { chooser.getSelectedFile() };
}
// #87119: do not block EQ loading projects
jTextFieldProjectName.setText(NbBundle.getMessage(ProjectChooserAccessory.class, "MSG_PrjChooser_WaitMessage"));
if (displayNameTask != null) {
displayNameTask.cancel();
}
displayNameTask = RP2.post(new Runnable() {
@Override
public void run() {
final List<Project> projects = new ArrayList<Project>( projectDirs.length );
//#155766 load the display names off the AWT thead.
final List<String> projectNames = new ArrayList<String>(projectDirs.length);
for (File dir : projectDirs) {
if (dir != null) {
if (Thread.interrupted()) {
return;
}
Project project = getProject(FileUtil.normalizeFile(dir));
if ( project != null ) {
projects.add( project );
projectNames.add(ProjectUtils.getInformation(project).getDisplayName());
}
}
}
if (Thread.interrupted()) {
return;
}
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
if ( !projects.isEmpty() ) {
// Enable all components acessory
setAccessoryEnablement( true, projects.size() );
if ( projects.size() == 1 ) {
String projectName = projectNames.get(0);
jTextFieldProjectName.setText( projectName );
jTextFieldProjectName.setToolTipText( projectName );
}
else {
jTextFieldProjectName.setText(NbBundle.getMessage(ProjectChooserAccessory.class, "LBL_PrjChooser_Multiselection", projects.size()));
StringBuffer toolTipText = new StringBuffer( "<html>" ); // NOI18N
for(String str : projectNames) {
toolTipText.append( str );
toolTipText.append( "<br>" ); // NOI18N
}
toolTipText.setLength(toolTipText.length() - "<br>".length());
toolTipText.append( "</html>" ); // NOI18N
jTextFieldProjectName.setToolTipText( toolTipText.toString() );
}
if (spListModel instanceof DefaultListModel) {
((DefaultListModel)spListModel).clear();
} else {
jListSubprojects.setListData (new String[0]);
}
if (modelUpdater != null) { // #72495
modelUpdater.projects = projects;
updateSubprojectsTask.schedule( 100 );
}
}
else {
// Clear the accessory data if the dir is not project dir
jTextFieldProjectName.setText( "" ); // NOI18N
if (modelUpdater != null) { // #72495
modelUpdater.projects = null;
}
if (spListModel instanceof DefaultListModel) {
((DefaultListModel)spListModel).clear();
} else {
jListSubprojects.setListData (new String[0]);
}
// Disable all components in accessory
setAccessoryEnablement( false, 0 );
// But, in case it is a load error, show that:
if (projectDirs.length == 1 && projectDirs[0] != null) {
File dir = FileUtil.normalizeFile(projectDirs[0]);
FileObject fo = FileUtil.toFileObject(dir);
ProjectManager.getDefault().clearNonProjectCache(); // #113976: otherwise isProject will be false
if (fo != null && fo.isFolder() && ProjectManager.getDefault().isProject(fo)) {
try {
Project prj = ProjectManager.getDefault().findProject(fo);
if (prj == null) {
jTextFieldProjectName.setText(NbBundle.getMessage(ProjectChooserAccessory.class, "LBL_PrjChooser_Unrecognized"));
// Only so it can be focussed and message scrolled accessibly:
jLabelProjectName.setEnabled(true);
jTextFieldProjectName.setEnabled(true);
}
} catch (IOException x) {
String msg = Exceptions.findLocalizedMessage(x);
if (msg == null) {
msg = x.getLocalizedMessage();
}
jTextFieldProjectName.setText(msg);
jTextFieldProjectName.setCaretPosition(0);
Color error = UIManager.getColor("nb.errorForeground"); // NOI18N
if (error != null) {
jTextFieldProjectName.setForeground(error);
}
// Only so it can be focussed and message scrolled accessibly:
jLabelProjectName.setEnabled(true);
jTextFieldProjectName.setEnabled(true);
}
}
}
}
}
});
}
}, 100, Thread.MIN_PRIORITY);
}
else if ( JFileChooser.DIRECTORY_CHANGED_PROPERTY.equals( e.getPropertyName() ) ) {
// Selection lost => disable accessory
setAccessoryEnablement( false, 0 );
}
}
// Private methods ---------------------------------------------------------
private static Project getProject( File dir ) {
return OpenProjectList.fileToProject( dir );
}
private static ProjectManager.Result getProjectResult(File dir) {
FileObject fo = FileUtil.toFileObject(dir);
if (fo != null && /* #60518 */ fo.isFolder()) {
return ProjectManager.getDefault().isProject2(fo);
} else {
return null;
}
}
private void setAccessoryEnablement( boolean enable, int numberOfProjects ) {
jLabelProjectName.setEnabled( enable );
jTextFieldProjectName.setEnabled( enable );
jTextFieldProjectName.setForeground(/* i.e. L&F default */null);
jCheckBoxSubprojects.setEnabled( enable );
jScrollPaneSubprojects.setEnabled( enable );
}
/**
* Get a slash-separated relative path from f1 to f2, if they are collocated
* and this is possible.
* May return null.
*/
private static String relativizePath(File f1, File f2) {
if (f1 == null || f2 == null) {
return null;
}
if (!CollocationQuery.areCollocated(f1, f2)) {
return null;
}
// Copied from PropertyUtils.relativizeFile, more or less:
StringBuffer b = new StringBuffer();
File base = f1;
String filepath = f2.getAbsolutePath();
while (!filepath.startsWith(slashify(base.getAbsolutePath()))) {
base = base.getParentFile();
if (base == null) {
return null;
}
if (base.equals(f2)) {
// #61687: file is a parent of basedir
b.append(".."); // NOI18N
return b.toString();
}
b.append("../"); // NOI18N
}
URI u = Utilities.toURI(base).relativize(Utilities.toURI(f2));
assert !u.isAbsolute() : u + " from " + f1 + " and " + f2 + " with common root " + base;
b.append(u.getPath());
if (b.charAt(b.length() - 1) == '/') {
// file is an existing directory and file.toURI ends in /
// we do not want the trailing slash
b.setLength(b.length() - 1);
}
return b.toString();
}
private static String slashify(String path) {
if (path.endsWith(File.separator)) {
return path;
} else {
return path + File.separatorChar;
}
}
// Other methods -----------------------------------------------------------
/** Factory method for project chooser
*/
public static JFileChooser createProjectChooser( boolean defaultAccessory ) {
ProjectManager.getDefault().clearNonProjectCache(); // #41882
OpenProjectListSettings opls = OpenProjectListSettings.getInstance();
JFileChooser chooser = new ProjectFileChooser();
chooser.setFileSelectionMode( JFileChooser.DIRECTORIES_ONLY );
if ("GTK".equals(javax.swing.UIManager.getLookAndFeel().getID())) { // NOI18N
// see BugTraq #5027268
chooser.putClientProperty("GTKFileChooser.showDirectoryIcons", Boolean.TRUE); // NOI18N
//chooser.putClientProperty("GTKFileChooser.showFileIcons", Boolean.TRUE); // NOI18N
}
chooser.setApproveButtonText( NbBundle.getMessage( ProjectChooserAccessory.class, "BTN_PrjChooser_ApproveButtonText" ) ); // NOI18N
chooser.setApproveButtonMnemonic( NbBundle.getMessage( ProjectChooserAccessory.class, "MNM_PrjChooser_ApproveButtonText" ).charAt (0) ); // NOI18N
chooser.setApproveButtonToolTipText (NbBundle.getMessage( ProjectChooserAccessory.class, "BTN_PrjChooser_ApproveButtonTooltipText")); // NOI18N
// chooser.setMultiSelectionEnabled( true );
chooser.setDialogTitle( NbBundle.getMessage( ProjectChooserAccessory.class, "LBL_PrjChooser_Title" ) ); // NOI18N
//#61789 on old macosx (jdk 1.4.1) these two method need to be called in this order.
chooser.setAcceptAllFileFilterUsed( false );
chooser.setFileFilter( ProjectDirFilter.INSTANCE );
// A11Y
chooser.getAccessibleContext().setAccessibleName(org.openide.util.NbBundle.getMessage(ProjectChooserAccessory.class, "AN_ProjectChooserAccessory"));
chooser.getAccessibleContext().setAccessibleDescription(org.openide.util.NbBundle.getMessage(ProjectChooserAccessory.class, "AD_ProjectChooserAccessory"));
if ( defaultAccessory ) {
chooser.setAccessory(new ProjectChooserAccessory(chooser, opls.isOpenSubprojects()));
}
File currDir = null;
String dir = opls.getLastOpenProjectDir();
if ( dir != null ) {
File d = new File( dir );
if ( d.exists() && d.isDirectory() ) {
currDir = d;
}
}
FileUtil.preventFileChooserSymlinkTraversal(chooser, currDir);
new ProjectFileView(chooser);
return chooser;
}
@Override
public void removeNotify() { // #72006
super.removeNotify();
if (modelUpdater != null) { // #101286 - might be already null
modelUpdater.cancel();
}
if (updateSubprojectsTask != null) {
updateSubprojectsTask.cancel();
}
if (displayNameTask != null) {
displayNameTask.cancel();
}
modelUpdater = null;
subprojectsCache = null;
updateSubprojectsTask = null;
displayNameTask = null;
}
// Aditional innerclasses for the file chooser -----------------------------
private static class ProjectFileChooser extends JFileChooser {
@Override
public void approveSelection() {
File selectedFile = getSelectedFile();
if (selectedFile != null) {
File dir = FileUtil.normalizeFile(selectedFile);
FileObject fo = FileUtil.toFileObject(dir);
if (fo != null && fo.isFolder() && ProjectManager.getDefault().isProject(fo)) {
super.approveSelection();
}
else {
setCurrentDirectory( dir );
}
}
}
}
private static class ProjectDirFilter extends FileFilter {
private static final FileFilter INSTANCE = new ProjectDirFilter( );
@Override
public boolean accept( File f ) {
if ( f.isDirectory() ) {
//#114765
if ("CVS".equalsIgnoreCase(f.getName()) && new File(f, "Entries").exists()) { //NOI18N
return false;
}
return true; // Directory selected
}
return false;
}
@Override
public String getDescription() {
return NbBundle.getMessage( ProjectDirFilter.class, "LBL_PrjChooser_ProjectDirectoryFilter_Name" ); // NOI18N
}
}
private static class ProjectFileView extends FileView implements Runnable {
private final JFileChooser chooser;
private final Map<File,Icon> knownProjectIcons = new HashMap<File,Icon>();
private final RequestProcessor.Task task = Hacks.RP.create(this);
private File lookingForIcon;
public ProjectFileView(JFileChooser chooser) {
this.chooser = chooser;
chooser.setFileView(this);
task.setPriority(Thread.MIN_PRIORITY);
}
@Override
public Icon getIcon(File f) {
if(f == null) {
// avoid NPE issue #268498
return null;
}
synchronized (this) { //#233480 to reduce number of calls to IO layer
Icon icon = knownProjectIcons.get(f);
if (icon != null) {
return icon;
}
}
if (
!f.toString().matches("/[^/]+") && // Unix: /net, /proc, etc.
f.getParentFile() != null) { // #173958: do not call ProjectManager.isProject now, could block
synchronized (this) {
if (lookingForIcon == null) {
lookingForIcon = f;
task.schedule(20);
// Only calculate one at a time.
// When the view refreshes, the next unknown icon
// should trigger the task to be reloaded.
}
}
}
try {
return chooser.getFileSystemView().getSystemIcon(f);
} catch (NullPointerException ex) {
//#159646: Workaround for JDK issue #6357445
// Can happen when a file was deleted on disk while project
// dialog was still open. In that case, throws an exception
// repeatedly from FSV.gSI during repaint.
return null;
}
}
public @Override void run() {
if (!lookingForIcon.isDirectory()) {
synchronized (this) {
lookingForIcon = null;
}
return;
}
File d = FileUtil.normalizeFile(lookingForIcon);
ProjectManager.Result r = getProjectResult(d);
Icon icon;
if (r != null) {
icon = r.getIcon();
if (icon == null) {
Project p = getProject(d);
if (p != null) {
icon = ProjectUtils.getInformation(p).getIcon();
} else {
icon = chooser.getFileSystemView().getSystemIcon(lookingForIcon);
}
}
} else {
try {
icon = chooser.getFileSystemView().getSystemIcon(lookingForIcon);
} catch (NullPointerException ex) {
//#159646: Workaround for JDK issue #6357445
// Can happen when a file was deleted on disk while project
// dialog was still open. In that case, throws an exception
// repeatedly from FSV.gSI during repaint.
icon = null;
}
}
synchronized (this) {
knownProjectIcons.put(lookingForIcon, icon);
lookingForIcon = null;
}
chooser.repaint();
}
}
class ModelUpdater implements Runnable, Cancellable { //#101227 -> non-private
// volatile Project project;
volatile List<Project> projects;
private DefaultListModel subprojectsToSet;
private boolean cancel = false;
@Override
public void run() {
if ( !SwingUtilities.isEventDispatchThread() ) {
if (cancel) {
return;
}
List<Project> currentProjects = projects;
if ( currentProjects == null ) {
return;
}
Map<Project,Set<? extends Project>> cache = subprojectsCache;
if (cache == null) {
return;
}
jListSubprojects.setListData (new String [] {NbBundle.getMessage (ProjectChooserAccessory.class, "MSG_PrjChooser_WaitMessage")});
List<Project> subprojects = new ArrayList<Project>(currentProjects.size() * 5);
for (Project p : currentProjects) {
if (cancel) {
return;
}
addSubprojects(p, subprojects, cache); // Find the projects recursively
}
if (cancel) {
return;
}
List<String> subprojectNames = new ArrayList<String>(subprojects.size());
if ( !subprojects.isEmpty() ) {
String pattern = NbBundle.getMessage( ProjectChooserAccessory.class, "LBL_PrjChooser_SubprojectName_Format" ); // NOI18N
File pDir = currentProjects.size() == 1 ?
FileUtil.toFile( currentProjects.get(0).getProjectDirectory() ) :
null;
// Replace projects in the list with formated names
for (Project p : subprojects) {
if (cancel) {
return;
}
FileObject spDir = p.getProjectDirectory();
// Try to compute relative path
String relPath = null;
if ( pDir != null ) { // If only one project is selected
relPath = relativizePath(pDir, FileUtil.toFile( spDir ));
}
if (relPath == null) {
// Cannot get a relative path; display it as absolute.
relPath = FileUtil.getFileDisplayName(spDir);
}
String displayName = MessageFormat.format(
pattern,
ProjectUtils.getInformation(p).getDisplayName(),
relPath);
subprojectNames.add(displayName);
}
// Sort the list
Collections.sort( subprojectNames, Collator.getInstance() );
}
if (currentProjects != projects || cancel) {
return;
}
DefaultListModel listModel = new DefaultListModel();
// Put all the strings into the list model
for (String displayName : subprojectNames) {
listModel.addElement(displayName);
}
subprojectsToSet = listModel;
if (cancel) {
return;
}
SwingUtilities.invokeLater( this );
return;
}
else {
if ( projects == null ) {
ListModel spListModel = jListSubprojects.getModel();
if (spListModel instanceof DefaultListModel) {
((DefaultListModel)spListModel).clear();
} else {
jListSubprojects.setListData (new String[0]);
}
jCheckBoxSubprojects.setEnabled( false );
}
else {
jListSubprojects.setModel(subprojectsToSet);
// If no soubprojects checkbox should be disabled
jCheckBoxSubprojects.setEnabled( !subprojectsToSet.isEmpty() );
projects = null;
}
}
}
/** Gets all subprojects recursively
*/
void addSubprojects(Project p, List<Project> result, Map<Project,Set<? extends Project>> cache) {
if (cancel) {
return;
}
Set<? extends Project> subprojects = cache.get(p);
boolean recurse = true;
if (subprojects == null) {
ProjectContainerProvider pcp = p.getLookup().lookup(ProjectContainerProvider.class);
if (pcp != null) {
ProjectContainerProvider.Result res = pcp.getContainedProjects();
if (res.isRecursive()) {
recurse = false;
}
if (cancel) {
return;
}
subprojects = res.getProjects();
} else {
SubprojectProvider spp = p.getLookup().lookup(SubprojectProvider.class);
if (spp != null) {
if (cancel) {
return;
}
subprojects = spp.getSubprojects();
} else {
subprojects = Collections.emptySet();
}
}
cache.put(p, subprojects);
}
for (Project sp : subprojects) {
if (cancel) {
return;
}
if ( !result.contains( sp ) ) {
result.add( sp );
if (recurse) {
//#70029: only add sp's subprojects if sp is not already in result,
//to prevent StackOverflow caused by misconfigured projects:
addSubprojects(sp, result, cache);
}
}
}
}
@Override
public boolean cancel() {
cancel = true;
// we don't really care that much to wait for cancelation here..
return true;
}
}
}