001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041:
042: package org.netbeans.modules.project.ui.groups;
043:
044: import java.awt.EventQueue;
045: import java.io.IOException;
046: import java.net.URL;
047: import java.text.Collator;
048: import java.util.Arrays;
049: import java.util.Collections;
050: import java.util.Comparator;
051: import java.util.HashSet;
052: import java.util.Set;
053: import java.util.SortedSet;
054: import java.util.TreeSet;
055: import java.util.logging.Level;
056: import java.util.logging.LogRecord;
057: import java.util.logging.Logger;
058: import java.util.prefs.BackingStoreException;
059: import java.util.prefs.Preferences;
060: import org.netbeans.api.progress.ProgressHandle;
061: import org.netbeans.api.progress.ProgressHandleFactory;
062: import org.netbeans.api.project.Project;
063: import org.netbeans.api.project.ProjectManager;
064: import org.netbeans.api.project.ProjectUtils;
065: import org.netbeans.api.project.ui.OpenProjects;
066: import org.netbeans.modules.project.ui.ProjectTab;
067: import org.openide.filesystems.FileObject;
068: import org.openide.filesystems.FileStateInvalidException;
069: import org.openide.filesystems.URLMapper;
070: import org.openide.util.Exceptions;
071: import org.openide.util.NbBundle;
072: import org.openide.util.NbPreferences;
073:
074: /**
075: * Represents a project group.
076: * Static methods represent set of groups and group selection.
077: * @author Jesse Glick
078: */
079: public abstract class Group {
080:
081: private static final Logger LOG = Logger.getLogger(Group.class
082: .getName());
083: private static final Logger UILOG = Logger
084: .getLogger("org.netbeans.ui.project.groups");
085:
086: protected static final Preferences NODE = NbPreferences.forModule(
087: Group.class).node("groups");
088: /** Preferences key for the active group ID. */
089: private static final String KEY_ACTIVE = "active"; // NOI18N
090: /** Preferences key for display name of group. */
091: protected static final String KEY_NAME = "name"; // NOI18N
092: /** Preferences key for kind of group (see constants in subclasses). */
093: protected static final String KEY_KIND = "kind"; // NOI18N
094: /** Preferences key for path (space-separated) of project URLs for AdHocGroup, or single project dir URL for SubprojectsGroup, or dir URL for DirectoryGroup. */
095: protected static final String KEY_PATH = "path"; // NOI18N
096: /** Preferences key for main project path URL for AdHocGroup or DirectoryGroup. */
097: protected static final String KEY_MAIN = "main"; // NOI18N
098:
099: private static Group load(String id) {
100: if (id == null) {
101: return null;
102: }
103: String kind = NODE.node(id).get(KEY_KIND, null);
104: if (AdHocGroup.KIND.equals(kind)) {
105: return new AdHocGroup(id);
106: } else if (SubprojectsGroup.KIND.equals(kind)) {
107: return new SubprojectsGroup(id);
108: } else if (DirectoryGroup.KIND.equals(kind)) {
109: return new DirectoryGroup(id);
110: } else {
111: LOG.log(Level.WARNING,
112: "Cannot find project group kind for id={0}", id);
113: return null;
114: }
115: }
116:
117: /**
118: * Find all groups.
119: * Sorted by display name.
120: */
121: public static SortedSet<Group> allGroups() {
122: SortedSet<Group> groups = new TreeSet<Group>(
123: displayNameComparator());
124: try {
125: for (String groupId : NODE.childrenNames()) {
126: LOG.log(Level.FINER,
127: "Considering project group id={0}", groupId);
128: Group g = load(groupId);
129: if (g != null) {
130: groups.add(g);
131: }
132: }
133: } catch (BackingStoreException x) {
134: Exceptions.printStackTrace(x);
135: }
136: return groups;
137: }
138:
139: /**
140: * Find the currently active group (or null).
141: */
142: public static Group getActiveGroup() {
143: return load(NODE.get(KEY_ACTIVE, null));
144: }
145:
146: /**
147: * Set the currently active group (or null).
148: */
149: public static void setActiveGroup(Group nue) {
150: LOG.log(Level.FINE, "set active group: {0}", nue);
151: if (UILOG.isLoggable(Level.FINER)) {
152: LogRecord rec = new LogRecord(Level.FINER,
153: "Group.UI.setActiveGroup");
154: rec.setParameters(new Object[] { nue != null ? nue
155: .toString(true) : null });
156: rec.setResourceBundle(NbBundle.getBundle(Group.class));
157: rec.setLoggerName(UILOG.getName());
158: UILOG.log(rec);
159: }
160: Group old = getActiveGroup();
161: if (old != null) {
162: old.closed();
163: }
164: if (nue != null) {
165: NODE.put(KEY_ACTIVE, nue.id);
166: } else {
167: NODE.remove(KEY_ACTIVE);
168: }
169: // OK if g == old; still want to fix open projects.
170: open(nue);
171: }
172:
173: protected static String sanitizeNameAndUniquifyForId(String name) {
174: String sanitizedId = name.replaceAll("[^a-zA-Z0-9_.-]+", "_");
175: Set<String> existing;
176: try {
177: existing = new HashSet<String>(Arrays.asList(NODE
178: .childrenNames()));
179: } catch (BackingStoreException x) {
180: Exceptions.printStackTrace(x);
181: return sanitizedId;
182: }
183: if (existing.contains(sanitizedId)) {
184: for (int i = 2;; i++) {
185: String candidate = sanitizedId + "_" + i;
186: if (!existing.contains(candidate)) {
187: return candidate;
188: }
189: }
190: } else {
191: return sanitizedId;
192: }
193: }
194:
195: protected final String id;
196:
197: protected Group(String id) {
198: this .id = id;
199: assert id.indexOf('/') == -1;
200: }
201:
202: protected Preferences prefs() {
203: return NODE.node(id);
204: }
205:
206: /**
207: * The name of a group; may be used for display purposes.
208: */
209: public String getName() {
210: String n = getNameOrNull();
211: if (n == null) {
212: n = id;
213: }
214: return n;
215: }
216:
217: protected String getNameOrNull() {
218: return prefs().get(KEY_NAME, null);
219: }
220:
221: /**
222: * Change the current display name.
223: */
224: public void setName(String n) {
225: prefs().put(KEY_NAME, n);
226: if (this .equals(getActiveGroup())) {
227: EventQueue.invokeLater(new Runnable() {
228: public void run() {
229: ProjectTab.findDefault(ProjectTab.ID_LOGICAL)
230: .setGroup(Group.this );
231: }
232: });
233: }
234: }
235:
236: protected static Project projectForPath(String path) {
237: if (path != null) {
238: try {
239: FileObject fo = URLMapper.findFileObject(new URL(path));
240: if (fo != null && fo.isFolder()) {
241: return ProjectManager.getDefault().findProject(fo);
242: }
243: } catch (IOException x) {
244: Exceptions.printStackTrace(x);
245: }
246: }
247: return null;
248: }
249:
250: /**
251: * The projects (currently) contained in the group.
252: */
253: public Set<Project> getProjects() {
254: return getProjects(null, 0, 0);
255: }
256:
257: private Set<Project> getProjects(ProgressHandle h, int start,
258: int end) {
259: if (h != null) {
260: h.progress("", start);
261: }
262: Set<Project> projects = new HashSet<Project>();
263: findProjects(projects, h, start, end);
264: if (h != null) {
265: h.progress("", end);
266: }
267: assert !projects.contains(null) : "Found null in " + projects
268: + " from " + this ;
269: return projects;
270: }
271:
272: protected abstract void findProjects(Set<Project> projects,
273: ProgressHandle h, int start, int end);
274:
275: protected static String progressMessage(Project p) {
276: return NbBundle.getMessage(Group.class,
277: "Group.progress_project", ProjectUtils
278: .getInformation(p).getDisplayName());
279: }
280:
281: /**
282: * The main project for this group (if any).
283: */
284: public Project getMainProject() {
285: return projectForPath(prefs().get(KEY_MAIN, null));
286: }
287:
288: /**
289: * Change the main project in the group.
290: * @throws IllegalArgumentException unless the main project is among {@link #getProjects}
291: */
292: public void setMainProject(Project mainProject)
293: throws IllegalArgumentException {
294: LOG.log(Level.FINE, "updating main project for {0} to {1}",
295: new Object[] { id, mainProject });
296: URL f = null;
297: if (mainProject != null && getProjects().contains(mainProject)) {
298: try {
299: f = mainProject.getProjectDirectory().getURL();
300: } catch (FileStateInvalidException x) {
301: LOG.log(Level.WARNING, null, x);
302: }
303: }
304: if (f != null) {
305: prefs().put(KEY_MAIN, f.toExternalForm());
306: } else {
307: if (mainProject != null) {
308: LOG
309: .log(Level.WARNING,
310: "...but not an open project or disk path not found");
311: }
312: prefs().remove(KEY_MAIN);
313: }
314: }
315:
316: /**
317: * Open a group, replacing any open projects with this group's project set.
318: */
319: private static void open(final Group g) {
320: EventQueue.invokeLater(new Runnable() {
321: public void run() {
322: ProjectTab.findDefault(ProjectTab.ID_LOGICAL).setGroup(
323: g);
324: }
325: });
326: String handleLabel;
327: if (g != null) {
328: handleLabel = NbBundle.getMessage(Group.class,
329: "Group.open_handle", g.getName());
330: } else {
331: handleLabel = NbBundle.getMessage(Group.class,
332: "Group.close_handle");
333: }
334: ProgressHandle h = ProgressHandleFactory
335: .createHandle(handleLabel);
336: h.start(200);
337: OpenProjects op = OpenProjects.getDefault();
338: Set<Project> oldOpen = new HashSet<Project>(Arrays.asList(op
339: .getOpenProjects()));
340: Set<Project> newOpen = g != null ? g.getProjects(h, 10, 100)
341: : Collections.<Project> emptySet();
342: Set<Project> toClose = new HashSet<Project>(oldOpen);
343: toClose.removeAll(newOpen);
344: Set<Project> toOpen = new HashSet<Project>(newOpen);
345: toOpen.removeAll(oldOpen);
346: assert !toClose.contains(null) : toClose;
347: assert !toOpen.contains(null) : toOpen;
348: h.progress(NbBundle.getMessage(Group.class,
349: "Group.progress_closing", toClose.size()), 120);
350: op.close(toClose.toArray(new Project[toClose.size()]));
351: h.progress(NbBundle.getMessage(Group.class,
352: "Group.progress_opening", toOpen.size()), 140);
353: op.open(toOpen.toArray(new Project[toOpen.size()]), false);
354: if (g != null) {
355: op.setMainProject(g.getMainProject());
356: }
357: h.finish();
358: }
359:
360: /**
361: * Called before a group is closed.
362: */
363: protected void closed() {
364: setMainProject(OpenProjects.getDefault().getMainProject());
365: }
366:
367: /**
368: * Delete this group.
369: */
370: public void destroy() {
371: LOG.log(Level.FINE, "destroying: {0}", id);
372: if (equals(getActiveGroup())) {
373: setActiveGroup(null);
374: }
375: try {
376: Preferences p = prefs();
377: p.removeNode();
378: assert !p.nodeExists("") : "failed to destroy " + id;
379: } catch (BackingStoreException x) {
380: Exceptions.printStackTrace(x);
381: }
382: }
383:
384: public abstract GroupEditPanel createPropertiesPanel();
385:
386: /**
387: * Compares groups according to display name.
388: */
389: public static Comparator<Group> displayNameComparator() {
390: return new Comparator<Group>() {
391: Collator COLLATOR = Collator.getInstance();
392:
393: public int compare(Group g1, Group g2) {
394: return COLLATOR.compare(g1.getName(), g2.getName());
395: }
396: };
397: }
398:
399: public int hashCode() {
400: return id.hashCode();
401: }
402:
403: public boolean equals(Object obj) {
404: return obj instanceof Group && id.equals(((Group) obj).id);
405: }
406:
407: public String toString() {
408: return toString(false);
409: }
410:
411: protected String toString(boolean scrubPersonalInfo) {
412: return getClass().getName().replaceFirst("^.+\\.", "") + "[id="
413: + (scrubPersonalInfo ? "#" + id.hashCode() : id)
414: + ",|projects|=" + getProjects().size() + "]";
415: }
416:
417: /**
418: * True if the projects specified by this group are exactly those open at the moment.
419: * More precisely, true if closing and reopening this group would leave you with the same
420: * set of projects (incl. main project) as you currently have.
421: */
422: public boolean isPristine() {
423: return getProjects().equals(
424: new HashSet<Project>(Arrays.asList(OpenProjects
425: .getDefault().getOpenProjects())));
426: }
427:
428: }
|