PageRenderTime 72ms CodeModel.GetById 17ms app.highlight 48ms RepoModel.GetById 1ms app.codeStats 1ms

/hudson-core/src/main/java/hudson/model/DependencyGraph.java

http://github.com/hudson/hudson
Java | 527 lines | 315 code | 68 blank | 144 comment | 42 complexity | 7d391ad887942af0d7f11ea7ecad047c MD5 | raw file
  1/*
  2 * The MIT License
  3 * 
  4 * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi,
  5 * Martin Eigenbrodt. Seiji Sogabe, Alan Harder
  6 * 
  7 * Permission is hereby granted, free of charge, to any person obtaining a copy
  8 * of this software and associated documentation files (the "Software"), to deal
  9 * in the Software without restriction, including without limitation the rights
 10 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 11 * copies of the Software, and to permit persons to whom the Software is
 12 * furnished to do so, subject to the following conditions:
 13 * 
 14 * The above copyright notice and this permission notice shall be included in
 15 * all copies or substantial portions of the Software.
 16 * 
 17 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 18 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 19 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 20 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 21 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 22 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 23 * THE SOFTWARE.
 24 */
 25package hudson.model;
 26
 27import hudson.security.ACL;
 28import hudson.security.NotSerilizableSecurityContext;
 29import org.acegisecurity.context.SecurityContext;
 30import org.acegisecurity.context.SecurityContextHolder;
 31import org.kohsuke.stapler.StaplerRequest;
 32import org.kohsuke.stapler.StaplerResponse;
 33import org.kohsuke.graph_layouter.Layout;
 34import org.kohsuke.graph_layouter.Navigator;
 35import org.kohsuke.graph_layouter.Direction;
 36
 37import javax.servlet.ServletOutputStream;
 38import javax.imageio.ImageIO;
 39import java.util.ArrayList;
 40import java.util.Collection;
 41import java.util.Collections;
 42import java.util.Comparator;
 43import java.util.HashMap;
 44import java.util.HashSet;
 45import java.util.LinkedHashSet;
 46import java.util.List;
 47import java.util.ListIterator;
 48import java.util.Map;
 49import java.util.Map.Entry;
 50import java.util.Set;
 51import java.util.Stack;
 52import java.io.IOException;
 53import java.awt.Dimension;
 54import java.awt.Font;
 55import java.awt.Rectangle;
 56import java.awt.Graphics2D;
 57import java.awt.Color;
 58import java.awt.Point;
 59import java.awt.HeadlessException;
 60import java.awt.FontMetrics;
 61import java.awt.geom.AffineTransform;
 62import java.awt.image.BufferedImage;
 63
 64/**
 65 * Maintains the build dependencies between {@link AbstractProject}s
 66 * for efficient dependency computation.
 67 *
 68 * <p>
 69 * The "master" data of dependencies are owned/persisted/maintained by
 70 * individual {@link AbstractProject}s, but because of that, it's relatively
 71 * slow  to compute backward edges.
 72 *
 73 * <p>
 74 * This class builds the complete bi-directional dependency graph
 75 * by collecting information from all {@link AbstractProject}s.
 76 *
 77 * <p>
 78 * Once built, {@link DependencyGraph} is immutable, and every time
 79 * there's a change (which is relatively rare), a new instance
 80 * will be created. This eliminates the need of synchronization.
 81 *
 82 * @see Hudson#getDependencyGraph() 
 83 * @author Kohsuke Kawaguchi
 84 */
 85public final class DependencyGraph implements Comparator<AbstractProject> {
 86
 87    private Map<AbstractProject, List<DependencyGroup>> forward = new HashMap<AbstractProject, List<DependencyGroup>>();
 88    private Map<AbstractProject, List<DependencyGroup>> backward = new HashMap<AbstractProject, List<DependencyGroup>>();
 89
 90    private boolean built;
 91
 92    /**
 93     * A unique set that holds the list of projects that have already computed its dependency graph
 94     */
 95    private Set<AbstractProject> alreadyComputedProjects = new HashSet<AbstractProject>();
 96
 97    /**
 98     * Builds the dependency graph.
 99     */
100    public DependencyGraph() {
101        // Set full privileges while computing to avoid missing any projects the current user cannot see.
102        // Use setContext (NOT getContext().setAuthentication()) so we don't affect concurrent threads for same HttpSession.
103        SecurityContext saveCtx = SecurityContextHolder.getContext();
104        try {
105            NotSerilizableSecurityContext system = new NotSerilizableSecurityContext();
106            system.setAuthentication(ACL.SYSTEM);
107            SecurityContextHolder.setContext(system);
108            for( AbstractProject p : Hudson.getInstance().getAllItems(AbstractProject.class) )
109                p.buildDependencyGraph(this);
110
111            forward = finalize(forward);
112            backward = finalize(backward);
113
114            built = true;
115            alreadyComputedProjects.clear();
116        } finally {
117            SecurityContextHolder.setContext(saveCtx);
118        }
119    }
120
121    /**
122     * Special constructor for creating an empty graph
123     */
124    private DependencyGraph(boolean dummy) {
125        forward = backward = Collections.emptyMap();
126        built = true;
127    }
128
129    /**
130     * Add this project to the set of projects that have already computed its dependency graph
131     * @param project
132     */
133    public void addToAlreadyComputedProjects(AbstractProject project) {
134        alreadyComputedProjects.add(project);
135    }
136
137    /**
138     * Check if the project has already computed its dependency graph
139     * @param project
140     */
141    public boolean isAlreadyComputedProject(AbstractProject project) {
142        return alreadyComputedProjects.contains(this);
143    }
144
145
146    /**
147     * Gets all the immediate downstream projects (IOW forward edges) of the given project.
148     *
149     * @return
150     *      can be empty but never null.
151     */
152    public List<AbstractProject> getDownstream(AbstractProject p) {
153        return get(forward,p,false);
154    }
155
156    /**
157     * Gets all the immediate upstream projects (IOW backward edges) of the given project.
158     *
159     * @return
160     *      can be empty but never null.
161     */
162    public List<AbstractProject> getUpstream(AbstractProject p) {
163        return get(backward,p,true);
164    }
165
166    private List<AbstractProject> get(Map<AbstractProject, List<DependencyGroup>> map, AbstractProject src, boolean up) {
167        List<DependencyGroup> v = map.get(src);
168        if(v==null) return Collections.emptyList();
169        List<AbstractProject> result = new ArrayList<AbstractProject>(v.size());
170        for (Dependency d : v) result.add(up ? d.getUpstreamProject() : d.getDownstreamProject());
171        return result;
172    }
173
174    /**
175     * @since 1.341
176     */
177    public List<Dependency> getDownstreamDependencies(AbstractProject p) {
178        return get(forward,p);
179    }
180
181    /**
182     * @since 1.341
183     */
184    public List<Dependency> getUpstreamDependencies(AbstractProject p) {
185        return get(backward,p);
186    }
187
188    private List<Dependency> get(Map<AbstractProject, List<DependencyGroup>> map, AbstractProject src) {
189        List<DependencyGroup> v = map.get(src);
190        if(v!=null) return Collections.<Dependency>unmodifiableList(v);
191        else        return Collections.emptyList();
192    }
193
194    /**
195     * @deprecated since 1.341; use {@link #addDependency(Dependency)}
196     */
197    @Deprecated
198    public void addDependency(AbstractProject upstream, AbstractProject downstream) {
199        addDependency(new Dependency(upstream,downstream));
200    }
201
202    /**
203     * Called during the dependency graph build phase to add a dependency edge.
204     */
205    public void addDependency(Dependency dep) {
206        if(built)
207            throw new IllegalStateException();
208        add(forward,dep.getUpstreamProject(),dep);
209        add(backward,dep.getDownstreamProject(),dep);
210    }
211
212    /**
213     * @deprecated since 1.341
214     */
215    @Deprecated
216    public void addDependency(AbstractProject upstream, Collection<? extends AbstractProject> downstream) {
217        for (AbstractProject p : downstream)
218            addDependency(upstream,p);
219    }
220
221    /**
222     * @deprecated since 1.341
223     */
224    @Deprecated
225    public void addDependency(Collection<? extends AbstractProject> upstream, AbstractProject downstream) {
226        for (AbstractProject p : upstream)
227            addDependency(p,downstream);
228    }
229
230    /**
231     * Lists up {@link DependecyDeclarer} from the collection and let them builds dependencies.
232     */
233    public void addDependencyDeclarers(AbstractProject upstream, Collection<?> possibleDependecyDeclarers) {
234        for (Object o : possibleDependecyDeclarers) {
235            if (o instanceof DependecyDeclarer) {
236                DependecyDeclarer dd = (DependecyDeclarer) o;
237                dd.buildDependencyGraph(upstream,this);
238            }
239        }
240    }
241
242    /**
243     * Returns true if a project has a non-direct dependency to another project.
244     * <p>
245     * A non-direct dependency is a path of dependency "edge"s from the source to the destination,
246     * where the length is greater than 1.
247     */
248    public boolean hasIndirectDependencies(AbstractProject src, AbstractProject dst) {
249        Set<AbstractProject> visited = new HashSet<AbstractProject>();
250        Stack<AbstractProject> queue = new Stack<AbstractProject>();
251
252        queue.addAll(getDownstream(src));
253        queue.remove(dst);
254
255        while(!queue.isEmpty()) {
256            AbstractProject p = queue.pop();
257            if(p==dst)
258                return true;
259            if(visited.add(p))
260                queue.addAll(getDownstream(p));
261        }
262
263        return false;
264    }
265
266    /**
267     * Gets all the direct and indirect upstream dependencies of the given project.
268     */
269    public Set<AbstractProject> getTransitiveUpstream(AbstractProject src) {
270        return getTransitive(backward,src,true);
271    }
272
273    /**
274     * Gets all the direct and indirect downstream dependencies of the given project.
275     */
276    public Set<AbstractProject> getTransitiveDownstream(AbstractProject src) {
277        return getTransitive(forward,src,false);
278    }
279
280    private Set<AbstractProject> getTransitive(Map<AbstractProject, List<DependencyGroup>> direction, AbstractProject src, boolean up) {
281        Set<AbstractProject> visited = new HashSet<AbstractProject>();
282        Stack<AbstractProject> queue = new Stack<AbstractProject>();
283
284        queue.add(src);
285
286        while(!queue.isEmpty()) {
287            AbstractProject p = queue.pop();
288
289            for (AbstractProject child : get(direction,p,up)) {
290                if(visited.add(child))
291                    queue.add(child);
292            }
293        }
294
295        return visited;
296    }
297
298    private void add(Map<AbstractProject, List<DependencyGroup>> map, AbstractProject key, Dependency dep) {
299        List<DependencyGroup> set = map.get(key);
300        if(set==null) {
301            set = new ArrayList<DependencyGroup>();
302            map.put(key,set);
303        }
304        for (ListIterator<DependencyGroup> it = set.listIterator(); it.hasNext();) {
305            DependencyGroup d = it.next();
306            // Check for existing edge that connects the same two projects:
307            if (d.getUpstreamProject()==dep.getUpstreamProject() && d.getDownstreamProject()==dep.getDownstreamProject()) {
308                d.add(dep);
309                return;
310            }
311        }
312        // Otherwise add to list:
313        set.add(new DependencyGroup(dep));
314    }
315
316    private Map<AbstractProject, List<DependencyGroup>> finalize(Map<AbstractProject, List<DependencyGroup>> m) {
317        for (Entry<AbstractProject, List<DependencyGroup>> e : m.entrySet()) {
318            Collections.sort( e.getValue(), NAME_COMPARATOR );
319            e.setValue( Collections.unmodifiableList(e.getValue()) );
320        }
321        return Collections.unmodifiableMap(m);
322    }
323
324    /**
325     * Experimental visualization of project dependencies.
326     */
327    public void doGraph( StaplerRequest req, StaplerResponse rsp ) throws IOException {
328        // Require admin permission for now (avoid exposing project names with restricted permissions)
329        Hudson.getInstance().checkPermission(Hudson.ADMINISTER);
330        try {
331
332            // creates a dummy graphics just so that we can measure font metrics
333            BufferedImage emptyImage = new BufferedImage(1,1, BufferedImage.TYPE_INT_RGB );
334            Graphics2D graphics = emptyImage.createGraphics();
335            graphics.setFont(FONT);
336            final FontMetrics fontMetrics = graphics.getFontMetrics();
337
338            // TODO: timestamp check
339            Layout<AbstractProject> layout = new Layout<AbstractProject>(new Navigator<AbstractProject>() {
340                public Collection<AbstractProject> vertices() {
341                    // only include projects that have some dependency
342                    List<AbstractProject> r = new ArrayList<AbstractProject>();
343                    for (AbstractProject p : Hudson.getInstance().getAllItems(AbstractProject.class)) {
344                        if(!getDownstream(p).isEmpty() || !getUpstream(p).isEmpty())
345                            r.add(p);
346                    }
347                    return r;
348                }
349
350                public Collection<AbstractProject> edge(AbstractProject p) {
351                    return getDownstream(p);
352                }
353
354                public Dimension getSize(AbstractProject p) {
355                    int w = fontMetrics.stringWidth(p.getDisplayName()) + MARGIN*2;
356                    return new Dimension(w, fontMetrics.getHeight() + MARGIN*2);
357                }
358            }, Direction.LEFTRIGHT);
359
360            Rectangle area = layout.calcDrawingArea();
361            area.grow(4,4); // give it a bit of margin
362            BufferedImage image = new BufferedImage(area.width, area.height, BufferedImage.TYPE_INT_RGB );
363            Graphics2D g2 = image.createGraphics();
364            g2.setTransform(AffineTransform.getTranslateInstance(-area.x,-area.y));
365            g2.setPaint(Color.WHITE);
366            g2.fill(area);
367            g2.setFont(FONT);
368
369            g2.setPaint(Color.BLACK);
370            for( AbstractProject p : layout.vertices() ) {
371                final Point sp = center(layout.vertex(p));
372
373                for (AbstractProject q : layout.edges(p)) {
374                    Point cur=sp;
375                    for( Point pt : layout.edge(p,q) ) {
376                        g2.drawLine(cur.x, cur.y, pt.x, pt.y);
377                        cur=pt;
378                    }
379
380                    final Point ep = center(layout.vertex(q));
381                    g2.drawLine(cur.x, cur.y, ep.x, ep.y);
382                }
383            }
384
385            int diff = fontMetrics.getAscent()+fontMetrics.getLeading()/2;
386            
387            for( AbstractProject p : layout.vertices() ) {
388                Rectangle r = layout.vertex(p);
389                g2.setPaint(Color.WHITE);
390                g2.fillRect(r.x, r.y, r.width, r.height);
391                g2.setPaint(Color.BLACK);
392                g2.drawRect(r.x, r.y, r.width, r.height);
393                g2.drawString(p.getDisplayName(), r.x+MARGIN, r.y+MARGIN+ diff);
394            }
395
396            rsp.setContentType("image/png");
397            ServletOutputStream os = rsp.getOutputStream();
398            ImageIO.write(image, "PNG", os);
399            os.close();
400        } catch(HeadlessException e) {
401            // not available. send out error message
402            rsp.sendRedirect2(req.getContextPath()+"/images/headless.png");
403        }
404    }
405
406    private Point center(Rectangle r) {
407        return new Point(r.x+r.width/2,r.y+r.height/2);
408    }
409
410    private static final Font FONT = new Font("TimesRoman", Font.PLAIN, 10);
411    /**
412     * Margins between the project name and its bounding box.
413     */
414    private static final int MARGIN = 4;
415
416
417    private static final Comparator<Dependency> NAME_COMPARATOR = new Comparator<Dependency>() {
418        public int compare(Dependency lhs, Dependency rhs) {
419            int cmp = lhs.getUpstreamProject().getName().compareTo(rhs.getUpstreamProject().getName());
420            return cmp != 0 ? cmp : lhs.getDownstreamProject().getName().compareTo(rhs.getDownstreamProject().getName());
421        }
422    };
423
424    public static final DependencyGraph EMPTY = new DependencyGraph(false);
425
426    /**
427     * Compare to Projects based on the topological order defined by this Dependency Graph
428     */
429    public int compare(AbstractProject o1, AbstractProject o2) {
430        Set<AbstractProject> o1sdownstreams = getTransitiveDownstream(o1);
431        Set<AbstractProject> o2sdownstreams = getTransitiveDownstream(o2);
432        if (o1sdownstreams.contains(o2)) {
433            if (o2sdownstreams.contains(o1)) return 0; else return 1;                       
434        } else {
435            if (o2sdownstreams.contains(o1)) return -1; else return 0; 
436        }               
437    }
438
439    /**
440     * Represents an edge in the dependency graph.
441     * @since 1.341
442     */
443    public static class Dependency {
444        private AbstractProject upstream, downstream;
445
446        public Dependency(AbstractProject upstream, AbstractProject downstream) {
447            this.upstream = upstream;
448            this.downstream = downstream;
449        }
450
451        public AbstractProject getUpstreamProject() {
452            return upstream;
453        }
454
455        public AbstractProject getDownstreamProject() {
456            return downstream;
457        }
458
459        /**
460         * Decide whether build should be triggered and provide any Actions for the build.
461         * Default implementation always returns true (for backward compatibility), and
462         * adds no Actions. Subclasses may override to control how/if the build is triggered.
463         * @param build Build of upstream project that just completed
464         * @param listener For any error/log output
465         * @param actions Add Actions for the triggered build to this list; never null
466         * @return True to trigger a build of the downstream project
467         */
468        public boolean shouldTriggerBuild(AbstractBuild build, TaskListener listener,
469                                          List<Action> actions) {
470            return true;
471        }
472
473        /**
474         * Does this method point to itself?
475         */
476        public boolean pointsItself() {
477            return upstream==downstream;
478        }
479
480        @Override
481        public boolean equals(Object obj) {
482            if (obj == null) return false;
483            if (getClass() != obj.getClass()) return false;
484
485            final Dependency that = (Dependency) obj;
486            return this.upstream == that.upstream || this.downstream == that.downstream;
487        }
488
489        @Override
490        public int hashCode() {
491            int hash = 7;
492            hash = 23 * hash + this.upstream.hashCode();
493            hash = 23 * hash + this.downstream.hashCode();
494            return hash;
495        }
496    }
497
498    /**
499     * Collect multiple dependencies between the same two projects.
500     */
501    private static class DependencyGroup extends Dependency {
502        private Set<Dependency> group = new LinkedHashSet<Dependency>();
503
504        DependencyGroup(Dependency first) {
505            super(first.getUpstreamProject(), first.getDownstreamProject());
506            group.add(first);
507        }
508
509        void add(Dependency next) {
510            group.add(next);
511        }
512
513        @Override
514        public boolean shouldTriggerBuild(AbstractBuild build, TaskListener listener,
515                                          List<Action> actions) {
516            List<Action> check = new ArrayList<Action>();
517            for (Dependency d : group) {
518                if (d.shouldTriggerBuild(build, listener, check)) {
519                    actions.addAll(check);
520                    return true;
521                } else
522                    check.clear();
523            }
524            return false;
525        }
526    }
527}