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