# HG changeset patch # Parent 4561a7c92e4eb94e30d23c2d9918861a393ef7a6 # User Jesse Glick #215135: support folders. diff --git a/hudson/src/org/netbeans/modules/hudson/api/HudsonFolder.java b/hudson/src/org/netbeans/modules/hudson/api/HudsonFolder.java new file mode 100644 --- /dev/null +++ b/hudson/src/org/netbeans/modules/hudson/api/HudsonFolder.java @@ -0,0 +1,75 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2013 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 2013 Sun Microsystems, Inc. + */ + +package org.netbeans.modules.hudson.api; + +import java.util.Collection; +import javax.swing.event.ChangeListener; + +/** + * An item which is a folder for other jobs (or subfolders). + * Could be any {@code ViewGroup} but probably from the CloudBees Folders plugin for Jenkins. + *

The current interface defines no {@code getViews} method, showing only the primary view. + * This is not such a loss, since heavy users of folders are not likely to need lots of views anyway. + * The standard connector also assumes that there is an exported {@link primaryView} field, + * defined in the Jenkins version of the interface. + *

In the Hudson/Jenkins model, this would really in an inheritance hierarchy with {@link HudsonJob} and {@link HudsonInstance}, + * but due to the many methods in those interfaces which make no sense on folders, it seems better to separate them. + * @since XXX + */ +public interface HudsonFolder { + + String getName(); + + String getUrl(); + + Collection getJobs(); + + Collection getFolders(); + + HudsonInstance getInstance(); + + void addChangeListener(ChangeListener listener); + + void removeChangeListener(ChangeListener listener); + +} diff --git a/hudson/src/org/netbeans/modules/hudson/api/HudsonInstance.java b/hudson/src/org/netbeans/modules/hudson/api/HudsonInstance.java --- a/hudson/src/org/netbeans/modules/hudson/api/HudsonInstance.java +++ b/hudson/src/org/netbeans/modules/hudson/api/HudsonInstance.java @@ -99,6 +99,9 @@ */ public Collection getJobs(); + /** @since XXX */ + Collection getFolders(); + /** * Returns all Hudson views from registered instance * diff --git a/hudson/src/org/netbeans/modules/hudson/impl/HudsonConnector.java b/hudson/src/org/netbeans/modules/hudson/impl/HudsonConnector.java --- a/hudson/src/org/netbeans/modules/hudson/impl/HudsonConnector.java +++ b/hudson/src/org/netbeans/modules/hudson/impl/HudsonConnector.java @@ -63,6 +63,7 @@ import java.util.regex.Pattern; import org.netbeans.api.annotations.common.CheckForNull; import org.netbeans.modules.hudson.api.ConnectionBuilder; +import org.netbeans.modules.hudson.api.HudsonFolder; import org.netbeans.modules.hudson.api.HudsonJob; import org.netbeans.modules.hudson.api.HudsonJob.Color; import org.netbeans.modules.hudson.api.HudsonJobBuild; @@ -119,6 +120,7 @@ Document docInstance = getDocument(instanceUrl + XML_API_URL + (canUseTree(authentication) ? "?tree=primaryView[name],views[name,url,jobs[name]]," + "jobs[name,url,color,displayName,buildable,inQueue," + + "primaryView," + // #215135: marker for folders "lastBuild[number],lastFailedBuild[number],lastStableBuild[number],lastSuccessfulBuild[number],lastCompletedBuild[number]," + "modules[name,displayName,url,color]]," + "securedJobs[name,url]" : // HUDSON-3924 @@ -131,14 +133,35 @@ if (null == docInstance) { return new InstanceData( Collections.emptyList(), - Collections.emptyList()); + Collections.emptyList(), + Collections.emptyList()); } // Clear cache cache.clear(); // Parse jobs and return them - Collection viewsData = getViewData(docInstance); - Collection jobsData = getJobsData(docInstance, viewsData); - return new InstanceData(jobsData, viewsData); + Collection viewsData = getViewData(docInstance, instanceUrl); + Collection foldersData = new ArrayList(); + Collection jobsData = getJobsData(docInstance, instanceUrl, viewsData, foldersData); + return new InstanceData(jobsData, viewsData, foldersData); + } + + @Override public InstanceData getInstanceData(HudsonFolder parentFolder, boolean authentication) { + Document docInstance = getDocument(parentFolder.getUrl() + XML_API_URL + "?tree=jobs[name,url,color,displayName,buildable,inQueue,primaryView," + + "lastBuild[number],lastFailedBuild[number],lastStableBuild[number],lastSuccessfulBuild[number],lastCompletedBuild[number]," + + "modules[name,displayName,url,color]]," + + "securedJobs[name,url]", authentication); // NOI18N + + if (null == docInstance) { + return new InstanceData( + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList()); + } + // Clear cache + cache.clear(); + Collection foldersData = new ArrayList(); + Collection jobsData = getJobsData(docInstance, parentFolder.getUrl(), Collections.emptySet(), foldersData); + return new InstanceData(jobsData, Collections.emptySet(), foldersData); } @Override @@ -235,7 +258,7 @@ } - private Collection getViewData(Document doc) { + private Collection getViewData(Document doc, String baseUrl) { String primaryViewName = null; Element primaryViewEl = XMLUtil.findElement(doc.getDocumentElement(), "primaryView", null); // NOI18N if (primaryViewEl != null) { @@ -267,7 +290,7 @@ name = o.getFirstChild().getTextContent(); isPrimary = name.equals(primaryViewName); } else if (o.getNodeName().equals(XML_API_URL_ELEMENT)) { - url = normalizeUrl(o.getFirstChild().getTextContent(), isPrimary ? "" : "view/[^/]+/"); // NOI18N + url = normalizeUrl(baseUrl, o.getFirstChild().getTextContent(), isPrimary ? "" : "view/[^/]+/"); // NOI18N } } @@ -300,8 +323,8 @@ return views; } - private Collection getJobsData(Document doc, - Collection viewsData) { + private Collection getJobsData(Document doc, String baseUrl, + Collection viewsData, Collection foldersData) { Collection jobs = new ArrayList(); NodeList nodes = doc.getDocumentElement().getChildNodes(); @@ -314,6 +337,8 @@ JobData jd = new JobData(); jd.setSecured(secured); + FolderData fd = new FolderData(); + boolean isFolder = false; NodeList jobDetails = n.getChildNodes(); for (int k = 0; k < jobDetails.getLength(); k++) { @@ -324,8 +349,13 @@ String nodeName = d.getNodeName(); if (nodeName.equals(XML_API_NAME_ELEMENT)) { jd.setJobName(d.getFirstChild().getTextContent()); + fd.setName(d.getFirstChild().getTextContent()); } else if (nodeName.equals(XML_API_URL_ELEMENT)) { - jd.setJobUrl(normalizeUrl(d.getFirstChild().getTextContent(), "job/[^/]+/")); // NOI18N + String u = normalizeUrl(baseUrl, d.getFirstChild().getTextContent(), "job/[^/]+/"); // NOI18N + jd.setJobUrl(u); + fd.setUrl(u); + } else if (nodeName.equals("primaryView")) { // NOI18N + isFolder = true; } else if (nodeName.equals(XML_API_COLOR_ELEMENT)) { jd.setColor(Color.find(d.getFirstChild().getTextContent().trim())); } else if (nodeName.equals(XML_API_DISPLAY_NAME_ELEMENT)) { @@ -362,7 +392,7 @@ } else if (nodeName2.equals("displayName")) { // NOI18N displayName = text; } else if (nodeName2.equals("url")) { // NOI18N - url = normalizeUrl(text, "job/[^/]+/[^/]+/"); // NOI18N + url = normalizeUrl(baseUrl, text, "job/[^/]+/[^/]+/"); // NOI18N } else if (nodeName2.equals("color")) { // NOI18N color = Color.find(text); } else { @@ -391,7 +421,11 @@ jd.addView(v.getName()); } } - jobs.add(jd); + if (isFolder) { + foldersData.add(fd); + } else { + jobs.add(jd); + } } return jobs; } @@ -403,7 +437,7 @@ * @return analogous URL constructed from instance root, e.g. {@code https://my.facade/hudson/job/My%20Job/} * @see "#165735" */ - private String normalizeUrl(String suggested, String relativePattern) { + private static String normalizeUrl(String instanceUrl, String suggested, String relativePattern) { Pattern tailPattern; synchronized (tailPatterns) { tailPattern = tailPatterns.get(relativePattern); @@ -620,4 +654,5 @@ } return items; } + } diff --git a/hudson/src/org/netbeans/modules/hudson/impl/HudsonFolderImpl.java b/hudson/src/org/netbeans/modules/hudson/impl/HudsonFolderImpl.java new file mode 100644 --- /dev/null +++ b/hudson/src/org/netbeans/modules/hudson/impl/HudsonFolderImpl.java @@ -0,0 +1,131 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2013 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 2013 Sun Microsystems, Inc. + */ + +package org.netbeans.modules.hudson.impl; + +import java.util.Collection; +import javax.swing.event.ChangeListener; +import org.netbeans.modules.hudson.api.HudsonChangeListener; +import org.netbeans.modules.hudson.api.HudsonFolder; +import org.netbeans.modules.hudson.api.HudsonInstance; +import org.netbeans.modules.hudson.api.HudsonJob; +import org.netbeans.modules.hudson.spi.BuilderConnector; +import org.netbeans.modules.hudson.ui.interfaces.OpenableInBrowser; +import org.openide.util.ChangeSupport; +import org.openide.util.WeakListeners; + +public class HudsonFolderImpl implements HudsonFolder, OpenableInBrowser, HudsonChangeListener { + + private final ChangeSupport cs = new ChangeSupport(this); + private final HudsonInstanceImpl instance; + private final String name, url; + private BuilderConnector.InstanceData children; + private Collection jobs; + private Collection folders; + + HudsonFolderImpl(HudsonInstanceImpl instance, String name, String url) { + this.instance = instance; + this.name = name; + this.url = url; + instance.addHudsonChangeListener(WeakListeners.create(HudsonChangeListener.class, this, instance)); + } + + @Override public String getName() { + return name; + } + + @Override public String getUrl() { + return url; + } + + private synchronized void load() { + if (children == null) { + children = instance.getBuilderConnector().getInstanceData(this, true); + jobs = instance.createJobs(children.getJobsData()); + folders = instance.createFolders(children.getFoldersData()); + } + } + + @Override public Collection getJobs() { + load(); + return jobs; + } + + @Override public Collection getFolders() { + load(); + return folders; + } + + @Override public HudsonInstance getInstance() { + return instance; + } + + @Override public String toString() { + return url; + } + + @Override public boolean equals(Object obj) { + return obj instanceof HudsonFolderImpl && ((HudsonFolderImpl) obj).url.equals(url); + } + + @Override public int hashCode() { + return url.hashCode(); + } + + @Override public void addChangeListener(ChangeListener listener) { + cs.addChangeListener(listener); + } + + @Override public void removeChangeListener(ChangeListener listener) { + cs.removeChangeListener(listener); + } + + @Override public void stateChanged() { + synchronized (this) { + children = null; + } + cs.fireChange(); + } + + @Override public void contentChanged() {} + +} diff --git a/hudson/src/org/netbeans/modules/hudson/impl/HudsonInstanceImpl.java b/hudson/src/org/netbeans/modules/hudson/impl/HudsonInstanceImpl.java --- a/hudson/src/org/netbeans/modules/hudson/impl/HudsonInstanceImpl.java +++ b/hudson/src/org/netbeans/modules/hudson/impl/HudsonInstanceImpl.java @@ -64,6 +64,7 @@ import org.netbeans.api.progress.ProgressHandleFactory; import org.netbeans.modules.hudson.api.HudsonChangeAdapter; import org.netbeans.modules.hudson.api.HudsonChangeListener; +import org.netbeans.modules.hudson.api.HudsonFolder; import org.netbeans.modules.hudson.api.HudsonInstance; import org.netbeans.modules.hudson.api.HudsonJob; import org.netbeans.modules.hudson.api.HudsonJobBuild; @@ -107,6 +108,7 @@ private final Task synchronization; private Collection jobs = new ArrayList(); + private Collection folders = new ArrayList(); private Collection views = new ArrayList(); private HudsonView primaryView; private final Collection listeners = new ArrayList(); @@ -220,6 +222,7 @@ forbidden = false; version = null; jobs.clear(); + folders.clear(); views.clear(); primaryView = null; @@ -239,6 +242,7 @@ assert !(connector instanceof HudsonConnector); this.builderConnector = connector; this.jobs.clear(); + folders.clear(); this.views.clear(); synchronize(false); } @@ -273,6 +277,10 @@ return jobs; } + @Override public synchronized Collection getFolders() { + return folders; + } + boolean isSalient(HudsonJobImpl job) { HudsonInstanceProperties props = getProperties(); if (HudsonInstanceProperties.split(props.get(INSTANCE_SUPPRESSED_JOBS)).contains(job.getName())) { @@ -377,6 +385,7 @@ configureViews(instanceData.getViewsData()); Collection retrieved = createJobs( instanceData.getJobsData()); + Collection retrievedFolders = createFolders(instanceData.getFoldersData()); // Exit when instance is terminated if (terminated) { @@ -405,12 +414,13 @@ } // When there are no changes return and do not fire changes - if (getJobs().equals(retrieved) && oldViews.equals(getViews())) { + if (jobs.equals(retrieved) && folders.equals(retrievedFolders) && oldViews.equals(getViews())) { return; } // Update jobs jobs = retrieved; + folders = retrievedFolders; // Fire all changes fireContentChanges(); @@ -551,6 +561,14 @@ return jobList; } + public Collection createFolders(Collection foldersData) { + Collection result = new ArrayList(); + for (BuilderConnector.FolderData datum : foldersData) { + result.add(new HudsonFolderImpl(this, datum.getName(), datum.getUrl())); + } + return result; + } + private void configureViews(Collection viewsData) { Collection viewList = new ArrayList(); diff --git a/hudson/src/org/netbeans/modules/hudson/spi/BuilderConnector.java b/hudson/src/org/netbeans/modules/hudson/spi/BuilderConnector.java --- a/hudson/src/org/netbeans/modules/hudson/spi/BuilderConnector.java +++ b/hudson/src/org/netbeans/modules/hudson/spi/BuilderConnector.java @@ -42,12 +42,14 @@ package org.netbeans.modules.hudson.spi; import java.util.Collection; +import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import org.netbeans.api.annotations.common.CheckForNull; import org.netbeans.api.annotations.common.NonNull; +import org.netbeans.modules.hudson.api.HudsonFolder; import org.netbeans.modules.hudson.api.HudsonJob; import org.netbeans.modules.hudson.api.HudsonJob.Color; import org.netbeans.modules.hudson.api.HudsonJobBuild; @@ -76,6 +78,15 @@ boolean authentication); /** + * Like {@link #getInstanceData(boolean)} but gets the contents of a folder rather than top level. + * Consider abstract; the default implementation produces an empty result. + * @since XXX + */ + public /*abstract*/ @NonNull InstanceData getInstanceData(@NonNull HudsonFolder parentFolder, boolean authentication) { + return new InstanceData(Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); + } + + /** * Get builds for the specified job. */ public abstract @NonNull Collection getJobBuildsData( @@ -336,6 +347,30 @@ } } + /** @since XXX */ + public static final class FolderData { + + private String name; + private String url; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + } + /** * Type for storing build module data */ @@ -428,10 +463,18 @@ private Collection jobsData; private Collection viewsData; + private final Collection foldersData; + @Deprecated public InstanceData(Collection jobsData, Collection viewsData) { + this(jobsData, viewsData, Collections.emptySet()); + } + + /** @since XXX */ + public InstanceData(Collection jobsData, Collection viewsData, Collection foldersData) { this.jobsData = jobsData; this.viewsData = viewsData; + this.foldersData = foldersData; } public Collection getJobsData() { @@ -441,5 +484,11 @@ public Collection getViewsData() { return viewsData; } + + /** @since XXX */ + public Collection getFoldersData() { + return foldersData; + } + } } diff --git a/hudson/src/org/netbeans/modules/hudson/ui/nodes/HudsonFolderNode.java b/hudson/src/org/netbeans/modules/hudson/ui/nodes/HudsonFolderNode.java new file mode 100644 --- /dev/null +++ b/hudson/src/org/netbeans/modules/hudson/ui/nodes/HudsonFolderNode.java @@ -0,0 +1,125 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2013 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 2013 Sun Microsystems, Inc. + */ + +package org.netbeans.modules.hudson.ui.nodes; + +import java.awt.Image; +import java.util.ArrayList; +import java.util.List; +import javax.swing.Action; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import org.netbeans.modules.hudson.api.HudsonFolder; +import org.netbeans.modules.hudson.api.HudsonJob; +import org.netbeans.modules.hudson.ui.actions.OpenUrlAction; +import org.netbeans.modules.hudson.ui.interfaces.OpenableInBrowser; +import org.openide.filesystems.FileUtil; +import org.openide.loaders.DataFolder; +import org.openide.nodes.AbstractNode; +import org.openide.nodes.ChildFactory; +import org.openide.nodes.Children; +import org.openide.nodes.Node; +import org.openide.util.Union2; +import org.openide.util.WeakListeners; +import org.openide.util.lookup.Lookups; + +class HudsonFolderNode extends AbstractNode { + + private static final Node iconDelegate = DataFolder.findFolder(FileUtil.getConfigRoot()).getNodeDelegate(); + + private final HudsonFolder folder; + + HudsonFolderNode(HudsonFolder folder) { + super(Children.create(new HudsonFolderChildren(folder), true), Lookups.singleton(folder)); + this.folder = folder; + setName(folder.getName()); + } + + public @Override Image getIcon(int type) { + return iconDelegate.getIcon(type); + } + + public @Override Image getOpenedIcon(int type) { + return iconDelegate.getOpenedIcon(type); + } + + @Override public Action[] getActions(boolean context) { + List actions = new ArrayList(); + if (folder instanceof OpenableInBrowser) { + actions.add(OpenUrlAction.forOpenable((OpenableInBrowser) folder)); + } + return actions.toArray(new Action[actions.size()]); + } + + private static final class HudsonFolderChildren extends ChildFactory.Detachable> implements ChangeListener { + + private final HudsonFolder folder; + + HudsonFolderChildren(HudsonFolder folder) { + this.folder = folder; + } + + @Override protected boolean createKeys(List> toPopulate) { + for (HudsonFolder subfolder : folder.getFolders()) { + toPopulate.add(Union2.createSecond(subfolder)); + } + for (HudsonJob job : folder.getJobs()) { + toPopulate.add(Union2.createFirst(job)); + } + return true; + } + + @Override protected Node createNodeForKey(Union2 key) { + return key.hasFirst() ? new HudsonJobNode(key.first()) : new HudsonFolderNode(key.second()); + } + + @Override protected void addNotify() { + folder.addChangeListener(WeakListeners.change(this, folder)); + } + + @Override public void stateChanged(ChangeEvent e) { + refresh(false); + } + + } + +} diff --git a/hudson/src/org/netbeans/modules/hudson/ui/nodes/HudsonInstanceNode.java b/hudson/src/org/netbeans/modules/hudson/ui/nodes/HudsonInstanceNode.java --- a/hudson/src/org/netbeans/modules/hudson/ui/nodes/HudsonInstanceNode.java +++ b/hudson/src/org/netbeans/modules/hudson/ui/nodes/HudsonInstanceNode.java @@ -47,11 +47,13 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedList; import java.util.List; import java.util.prefs.PreferenceChangeEvent; import java.util.prefs.PreferenceChangeListener; import javax.swing.Action; import org.netbeans.modules.hudson.api.HudsonChangeListener; +import org.netbeans.modules.hudson.api.HudsonFolder; import org.netbeans.modules.hudson.api.HudsonInstance; import org.netbeans.modules.hudson.api.HudsonJob; import org.netbeans.modules.hudson.api.HudsonJob.Color; @@ -65,6 +67,7 @@ import org.openide.nodes.Children; import org.openide.nodes.Node; import org.openide.util.NbBundle.Messages; +import org.openide.util.Union2; import org.openide.util.lookup.Lookups; /** @@ -195,7 +198,7 @@ */ public static final String SELECTED_VIEW = "view"; // NOI18N - private static class InstanceNodeChildren extends Children.Keys implements HudsonChangeListener { + private static class InstanceNodeChildren extends Children.Keys> implements HudsonChangeListener { private final HudsonInstanceImpl instance; @@ -209,15 +212,15 @@ }); } - @Override protected Node[] createNodes(HudsonJob job) { - return new Node[] {new HudsonJobNode(job)}; + @Override protected Node[] createNodes(Union2 item) { + return new Node[] {item.hasFirst() ? new HudsonJobNode(item.first()) : new HudsonFolderNode(item.second())}; } @Override protected void addNotify() { super.addNotify(); if (!instance.isConnected()/* && seems undesirable: !instance.isForbidden()*/) { - setKeys(Collections.emptySet()); + setKeys(Collections.>emptySet()); instance.synchronize(true); } else { refreshKeys(); @@ -226,7 +229,7 @@ @Override protected void removeNotify() { - setKeys(Collections.emptySet()); + setKeys(Collections.>emptySet()); super.removeNotify(); } @@ -249,7 +252,15 @@ jobs.add(job); } Collections.sort(jobs); - setKeys(jobs); + List> items = new LinkedList>(); + for (HudsonFolder folder : instance.getFolders()) { + // XXX ideally should restrict by selected view, like jobs + items.add(Union2.createSecond(folder)); + } + for (HudsonJob job : jobs) { + items.add(Union2.createFirst(job)); + } + setKeys(items); } @Override public void stateChanged() {