2
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
4
* Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
6
* The contents of this file are subject to the terms of either the GNU
7
* General Public License Version 2 only ("GPL") or the Common
8
* Development and Distribution License("CDDL") (collectively, the
9
* "License"). You may not use this file except in compliance with the
10
* License. You can obtain a copy of the License at
11
* http://www.netbeans.org/cddl-gplv2.html
12
* or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
13
* specific language governing permissions and limitations under the
14
* License. When distributing the software, include this License Header
15
* Notice in each file and include the License file at
16
* nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
17
* particular file as subject to the "Classpath" exception as provided
18
* by Sun in the GPL Version 2 section of the License file that
19
* accompanied this code. If applicable, add the following below the
20
* License Header, with the fields enclosed by brackets [] replaced by
21
* your own identifying information:
22
* "Portions Copyrighted [year] [name of copyright owner]"
26
* The Original Software is NetBeans. The Initial Developer of the Original
27
* Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
28
* Microsystems, Inc. All Rights Reserved.
30
* If you wish your version of this file to be governed by only the CDDL
31
* or only the GPL Version 2, indicate your decision by adding
32
* "[Contributor] elects to include this software in this distribution
33
* under the [CDDL or GPL Version 2] license." If you do not indicate a
34
* single choice of license, a recipient has the option to distribute
35
* your version of this file under either the CDDL, the GPL Version 2 or
36
* to extend the choice of license to its licensees as provided above.
37
* However, if you add GPL Version 2 code and therefore, elected the GPL
38
* Version 2 license, then the option applies only if the new code is
39
* made subject to such option by the copyright holder.
42
package org.netbeans.modules.project.ui.groups;
44
import java.awt.EventQueue;
45
import java.io.IOException;
47
import java.text.Collator;
48
import java.util.Arrays;
49
import java.util.Collections;
50
import java.util.Comparator;
51
import java.util.HashSet;
53
import java.util.SortedSet;
54
import java.util.TreeSet;
55
import java.util.logging.Level;
56
import java.util.logging.LogRecord;
57
import java.util.logging.Logger;
58
import java.util.prefs.BackingStoreException;
59
import java.util.prefs.Preferences;
60
import org.netbeans.api.progress.ProgressHandle;
61
import org.netbeans.api.progress.ProgressHandleFactory;
62
import org.netbeans.api.project.Project;
63
import org.netbeans.api.project.ProjectManager;
64
import org.netbeans.api.project.ProjectUtils;
65
import org.netbeans.api.project.ui.OpenProjects;
66
import org.netbeans.modules.project.ui.ProjectTab;
67
import org.openide.filesystems.FileObject;
68
import org.openide.filesystems.FileStateInvalidException;
69
import org.openide.filesystems.URLMapper;
70
import org.openide.util.Exceptions;
71
import org.openide.util.NbBundle;
72
import org.openide.util.NbPreferences;
75
* Represents a project group.
76
* Static methods represent set of groups and group selection.
79
public abstract class Group {
81
private static final Logger LOG = Logger.getLogger(Group.class.getName());
82
private static final Logger UILOG = Logger.getLogger("org.netbeans.ui.project.groups");
84
protected static final Preferences NODE = NbPreferences.forModule(Group.class).node("groups");
85
/** Preferences key for the active group ID. */
86
private static final String KEY_ACTIVE = "active"; // NOI18N
87
/** Preferences key for display name of group. */
88
protected static final String KEY_NAME = "name"; // NOI18N
89
/** Preferences key for kind of group (see constants in subclasses). */
90
protected static final String KEY_KIND = "kind"; // NOI18N
91
/** Preferences key for path (space-separated) of project URLs for AdHocGroup, or single project dir URL for SubprojectsGroup, or dir URL for DirectoryGroup. */
92
protected static final String KEY_PATH = "path"; // NOI18N
93
/** Preferences key for main project path URL for AdHocGroup or DirectoryGroup. */
94
protected static final String KEY_MAIN = "main"; // NOI18N
96
private static Group load(String id) {
100
String kind = NODE.node(id).get(KEY_KIND, null);
101
if (AdHocGroup.KIND.equals(kind)) {
102
return new AdHocGroup(id);
103
} else if (SubprojectsGroup.KIND.equals(kind)) {
104
return new SubprojectsGroup(id);
105
} else if (DirectoryGroup.KIND.equals(kind)) {
106
return new DirectoryGroup(id);
108
LOG.log(Level.WARNING, "Cannot find project group kind for id={0}", id);
115
* Sorted by display name.
117
public static SortedSet<Group> allGroups() {
118
SortedSet<Group> groups = new TreeSet<Group>(displayNameComparator());
120
for (String groupId : NODE.childrenNames()) {
121
LOG.log(Level.FINER, "Considering project group id={0}", groupId);
122
Group g = load(groupId);
127
} catch (BackingStoreException x) {
128
Exceptions.printStackTrace(x);
134
* Find the currently active group (or null).
136
public static Group getActiveGroup() {
137
return load(NODE.get(KEY_ACTIVE, null));
141
* Set the currently active group (or null).
143
public static void setActiveGroup(Group nue) {
144
LOG.log(Level.FINE, "set active group: {0}", nue);
145
if (UILOG.isLoggable(Level.FINER)) {
146
LogRecord rec = new LogRecord(Level.FINER, "Group.UI.setActiveGroup");
147
rec.setParameters(new Object[] {nue != null ? nue.toString(true) : null});
148
rec.setResourceBundle(NbBundle.getBundle(Group.class));
149
rec.setLoggerName(UILOG.getName());
152
Group old = getActiveGroup();
157
NODE.put(KEY_ACTIVE, nue.id);
159
NODE.remove(KEY_ACTIVE);
161
// OK if g == old; still want to fix open projects.
165
protected static String sanitizeNameAndUniquifyForId(String name) {
166
String sanitizedId = name.replaceAll("[^a-zA-Z0-9_.-]+", "_");
167
Set<String> existing;
169
existing = new HashSet<String>(Arrays.asList(NODE.childrenNames()));
170
} catch (BackingStoreException x) {
171
Exceptions.printStackTrace(x);
174
if (existing.contains(sanitizedId)) {
175
for (int i = 2; ; i++) {
176
String candidate = sanitizedId + "_" + i;
177
if (!existing.contains(candidate)) {
186
protected final String id;
188
protected Group(String id) {
190
assert id.indexOf('/') == -1;
193
protected Preferences prefs() {
194
return NODE.node(id);
198
* The name of a group; may be used for display purposes.
200
public String getName() {
201
String n = getNameOrNull();
208
protected String getNameOrNull() {
209
return prefs().get(KEY_NAME, null);
213
* Change the current display name.
215
public void setName(String n) {
216
prefs().put(KEY_NAME, n);
217
if (this.equals(getActiveGroup())) {
218
EventQueue.invokeLater(new Runnable() {
220
ProjectTab.findDefault(ProjectTab.ID_LOGICAL).setGroup(Group.this);
226
protected static Project projectForPath(String path) {
229
FileObject fo = URLMapper.findFileObject(new URL(path));
230
if (fo != null && fo.isFolder()) {
231
return ProjectManager.getDefault().findProject(fo);
233
} catch (IOException x) {
234
Exceptions.printStackTrace(x);
241
* The projects (currently) contained in the group.
243
public Set<Project> getProjects() {
244
return getProjects(null, 0, 0);
246
private Set<Project> getProjects(ProgressHandle h, int start, int end) {
248
h.progress("", start);
250
Set<Project> projects = new HashSet<Project>();
251
findProjects(projects, h, start, end);
255
assert !projects.contains(null) : "Found null in " + projects + " from " + this;
259
protected abstract void findProjects(Set<Project> projects, ProgressHandle h, int start, int end);
261
protected static String progressMessage(Project p) {
262
return NbBundle.getMessage(Group.class, "Group.progress_project", ProjectUtils.getInformation(p).getDisplayName());
266
* The main project for this group (if any).
268
public Project getMainProject() {
269
return projectForPath(prefs().get(KEY_MAIN, null));
273
* Change the main project in the group.
274
* @throws IllegalArgumentException unless the main project is among {@link #getProjects}
276
public void setMainProject(Project mainProject) throws IllegalArgumentException {
277
LOG.log(Level.FINE, "updating main project for {0} to {1}", new Object[] {id, mainProject});
279
if (mainProject != null && getProjects().contains(mainProject)) {
281
f = mainProject.getProjectDirectory().getURL();
282
} catch (FileStateInvalidException x) {
283
LOG.log(Level.WARNING, null, x);
287
prefs().put(KEY_MAIN, f.toExternalForm());
289
if (mainProject != null) {
290
LOG.log(Level.WARNING, "...but not an open project or disk path not found");
292
prefs().remove(KEY_MAIN);
297
* Open a group, replacing any open projects with this group's project set.
299
private static void open(final Group g) {
300
EventQueue.invokeLater(new Runnable() {
302
ProjectTab.findDefault(ProjectTab.ID_LOGICAL).setGroup(g);
307
handleLabel = NbBundle.getMessage(Group.class, "Group.open_handle", g.getName());
309
handleLabel = NbBundle.getMessage(Group.class, "Group.close_handle");
311
ProgressHandle h = ProgressHandleFactory.createHandle(handleLabel);
313
OpenProjects op = OpenProjects.getDefault();
314
Set<Project> oldOpen = new HashSet<Project>(Arrays.asList(op.getOpenProjects()));
315
Set<Project> newOpen = g != null ? g.getProjects(h, 10, 100) : Collections.<Project>emptySet();
316
Set<Project> toClose = new HashSet<Project>(oldOpen);
317
toClose.removeAll(newOpen);
318
Set<Project> toOpen = new HashSet<Project>(newOpen);
319
toOpen.removeAll(oldOpen);
320
assert !toClose.contains(null) : toClose;
321
assert !toOpen.contains(null) : toOpen;
322
h.progress(NbBundle.getMessage(Group.class, "Group.progress_closing", toClose.size()), 120);
323
op.close(toClose.toArray(new Project[toClose.size()]));
324
h.progress(NbBundle.getMessage(Group.class, "Group.progress_opening", toOpen.size()), 140);
325
op.open(toOpen.toArray(new Project[toOpen.size()]), false);
327
op.setMainProject(g.getMainProject());
333
* Called before a group is closed.
335
protected void closed() {
336
setMainProject(OpenProjects.getDefault().getMainProject());
342
public void destroy() {
343
LOG.log(Level.FINE, "destroying: {0}", id);
344
if (equals(getActiveGroup())) {
345
setActiveGroup(null);
348
Preferences p = prefs();
350
assert !p.nodeExists("") : "failed to destroy " + id;
351
} catch (BackingStoreException x) {
352
Exceptions.printStackTrace(x);
356
public abstract GroupEditPanel createPropertiesPanel();
359
* Compares groups according to display name.
361
public static Comparator<Group> displayNameComparator() {
362
return new Comparator<Group>() {
363
Collator COLLATOR = Collator.getInstance();
364
public int compare(Group g1, Group g2) {
365
return COLLATOR.compare(g1.getName(), g2.getName());
370
public int hashCode() {
371
return id.hashCode();
374
public boolean equals(Object obj) {
375
return obj instanceof Group && id.equals(((Group) obj).id);
378
public String toString() {
379
return toString(false);
381
protected String toString(boolean scrubPersonalInfo) {
382
return getClass().getName().replaceFirst("^.+\\.", "") + "[id=" + (scrubPersonalInfo ? "#" + id.hashCode() : id) + ",|projects|=" + getProjects().size() + "]";
386
* True if the projects specified by this group are exactly those open at the moment.
387
* More precisely, true if closing and reopening this group would leave you with the same
388
* set of projects (incl. main project) as you currently have.
390
public boolean isPristine() {
391
return getProjects().equals(new HashSet<Project>(Arrays.asList(OpenProjects.getDefault().getOpenProjects())));