Source code: javax/ide/extension/spi/DependencyTree.java
1 package javax.ide.extension.spi;
2
3 import java.io.IOException;
4 import java.io.InputStream;
5
6 import java.util.ArrayList;
7 import java.util.Collection;
8 import java.util.Collections;
9 import java.util.HashMap;
10 import java.util.HashSet;
11 import java.util.Iterator;
12 import java.util.List;
13 import java.util.Map;
14 import java.util.Set;
15 import java.util.logging.Level;
16
17 import java.util.logging.Logger;
18 import javax.ide.extension.ElementEndContext;
19 import javax.ide.extension.ElementStartContext;
20 import javax.ide.extension.ElementVisitor;
21 import javax.ide.extension.Extension;
22 import javax.ide.extension.ExtensionDependency;
23 import javax.ide.extension.ExtensionHook;
24
25 import javax.ide.util.Version;
26 import javax.xml.parsers.ParserConfigurationException;
27
28 import org.xml.sax.InputSource;
29 import org.xml.sax.SAXException;
30
31 /**
32 * DependencyTree is responsible for determining dependencies between extensions
33 * and providing a load order that satisfies those dependencies.
34 */
35 public final class DependencyTree
36 {
37 private static final String SOURCE = "source";
38
39 private final Map _sourcesByExtension;
40 private final Map _extensionsById = new HashMap();
41
42 private List _topologicalExtensionList;
43 private final Map _unsatisfiedDependencies = new HashMap();
44 private final List _cycles = new ArrayList();
45
46 private DependencyTree( List failedSources, Map sourcesByExtension )
47 {
48 _sourcesByExtension = sourcesByExtension;
49 removeDuplicates();
50
51 for ( Iterator i = _sourcesByExtension.keySet().iterator(); i.hasNext(); )
52 {
53 Extension extension = (Extension) i.next();
54 _extensionsById.put( extension.getID(), extension );
55 }
56
57 // Now create a topological sort of the extensions. We must try the
58 // sort from all vertices because we may have multiple disconnected
59 // graph fragments. However, this is not as inefficient as it seems, since
60 // we remember which vertices we've already visited.
61
62 TopoSortState state = new TopoSortState();
63 for ( Iterator i = _sourcesByExtension.keySet().iterator(); i.hasNext(); )
64 {
65 Extension thisExt = (Extension) i.next();
66 if ( state.isUnvisited( thisExt ) )
67 {
68 topologicalSort( state, thisExt );
69 }
70 }
71
72 _topologicalExtensionList = state.getTopoList();
73
74 }
75
76 /**
77 * Build a dependency tree for the specified collection of extension
78 * sources.
79 *
80 * @param extensionSources a collection of
81 * javax.ide.extension.spi.ExtensionSource.
82 * @return an instance of DependencyTree.
83 */
84 public static DependencyTree buildTree( Collection extensionSources,
85 EnabledExtensionLookup lookup, Logger logger,
86 DefaultElementContext initialContext )
87 {
88 SAXManifestParser minimalParser = new SAXManifestParser( initialContext );
89 ((DefaultElementContext)minimalParser.getContext()).setMessageReporter( logger );
90 MinimalExtensionVisitor visitor = new MinimalExtensionVisitor();
91
92 minimalParser.getContext().registerChildVisitor(
93 ExtensionVisitor.ELEMENT, visitor
94 );
95
96 List failedSources = new ArrayList();
97
98 for ( Iterator i = extensionSources.iterator(); i.hasNext(); )
99 {
100 ExtensionSource source = (ExtensionSource) i.next();
101 minimalParser.getContext().getScopeData().put( SOURCE, source );
102 InputStream inputStream = null;
103 try
104 {
105 // TODO handle encoding... Always a pain in the butt with xml. We need
106 // to partially read the xml stream to figure out which encoding to use.
107
108 inputStream = source.getInputStream();
109
110 InputSource inputSource = new InputSource( inputStream );
111 inputSource.setSystemId( source.getManifestURI().toString() );
112 minimalParser.parse( inputSource );
113 }
114 catch ( ParserConfigurationException pce )
115 {
116 throw new IllegalStateException( "JAXP is misconfigured" ); // , pce );
117 }
118 catch ( SAXException saxe )
119 {
120 minimalParser.getContext().getLogger().log(
121 Level.SEVERE,
122 "Failed to process extension source "+ source.getName()+": "+saxe.getLocalizedMessage(),
123 new LocatorImpl( source.getManifestURI().toString() )
124 );
125 failedSources.add( source );
126 }
127 catch ( IOException ioe )
128 {
129 minimalParser.getContext().getLogger().log(
130 Level.SEVERE,
131 "Failed to process extension source "+ source.getName()+": "+ioe.getLocalizedMessage(),
132 new LocatorImpl( source.getManifestURI().toString() )
133 );
134 failedSources.add( source );
135 }
136 finally
137 {
138 try
139 {
140 if ( inputStream != null )
141 inputStream.close();
142 }
143 catch ( IOException ioe )
144 {
145 minimalParser.getContext().getLogger().log(
146 Level.SEVERE,
147 "Exception closing stream",
148 ioe
149 );
150 }
151 }
152 }
153
154 Map sbe = visitor.getSourcesByExtension();
155 for ( Iterator i = sbe.keySet().iterator(); i.hasNext(); )
156 {
157 Extension e = (Extension) i.next();
158 if ( !lookup.isExtensionEnabled( e ) )
159 {
160 i.remove();
161 }
162 }
163
164 return new DependencyTree( failedSources, sbe );
165 }
166
167 /**
168 * Resolve duplicate extension ids. If two or more extensions are present
169 * with the same id but different versions, we discard the older versions.
170 * If two or more extensions have the same version, we "arbitrarily" pick
171 * only one.
172 */
173 private void removeDuplicates()
174 {
175 Map latestExtensionById = new HashMap();
176 List extensionsToRemove = new ArrayList();
177
178 for ( Iterator i = _sourcesByExtension.keySet().iterator(); i.hasNext(); )
179 {
180 Extension extension = (Extension) i.next();
181 Extension latest =
182 (Extension) latestExtensionById.get( extension.getID() );
183 if ( latest == null )
184 {
185 latestExtensionById.put( extension.getID(), extension );
186 }
187 // Do we have a later version?
188 else if ( extension.getVersion().compareTo( latest.getVersion() ) > 0 )
189 {
190 // Remember the older extension. We will need to remove it from the map
191 // later.
192 extensionsToRemove.add( latest );
193 latestExtensionById.put( extension.getID(), extension );
194 }
195 else
196 {
197 // Else, the extension version is <= the latest one, so we skip it.
198 extensionsToRemove.add( extension );
199 }
200 }
201
202 // Now remove all extensions in the list of duplicates we've built.
203 for ( Iterator i = extensionsToRemove.iterator(); i.hasNext(); )
204 {
205 Extension extension = (Extension) i.next();
206 _sourcesByExtension.remove( extension );
207 }
208 }
209
210 /**
211 * The state of a topological sort. Primarily avoids passing lots of
212 * parameters around...
213 */
214 private class TopoSortState
215 {
216 private Map _stateByExtension = new HashMap();
217 private Set _unsatisfied = new HashSet();
218 private List _currentlyVisiting = new ArrayList();
219 private List _topoList = new ArrayList();
220
221 public List getTopoList()
222 {
223 return _topoList;
224 }
225
226 public boolean isVisiting( Extension extension )
227 {
228 return _stateByExtension.get( extension ) == STATE_VISITING;
229 }
230
231 public boolean isVisited( Extension extension )
232 {
233 return _stateByExtension.get( extension ) == STATE_VISITED;
234 }
235
236 public boolean isUnvisited( Extension extension )
237 {
238 Object o = _stateByExtension.get( extension );
239 return o == null || o == STATE_NOT_VISITED;
240 }
241
242
243 public void addUnsatisfied( Extension extension )
244 {
245 _unsatisfied.add( extension );
246 }
247
248 public boolean isUnsatisfied( Extension extension )
249 {
250 return _unsatisfied.contains( extension );
251 }
252
253 public void startVisiting( Extension extension )
254 {
255 _currentlyVisiting.add( extension );
256 _stateByExtension.put( extension, STATE_VISITING );
257 }
258
259 public void endVisiting( Extension extension )
260 {
261 _currentlyVisiting.remove( extension );
262 _stateByExtension.put( extension, STATE_VISITED );
263 }
264
265 public void markUnsatisfiedChain()
266 {
267 for ( Iterator i = _currentlyVisiting.iterator(); i.hasNext(); )
268 {
269 addUnsatisfied( (Extension) i.next() );
270 }
271 }
272
273 public void markCycleChain( Extension current )
274 {
275 ArrayList al = new ArrayList();
276 al.addAll( _currentlyVisiting );
277 al.add( current );
278 _cycles.add( al );
279 }
280
281 public void addToTopo( Extension extension )
282 {
283 _topoList.add( extension );
284 }
285 }
286
287 /**
288 * Topologically sort the set of extensions in _sourcesByExtension based on
289 * their dependencies. This algorithm detects cycles, but does not abort
290 * on the detection of them. Instead, it discards cyclic dependencies for
291 * the purposes of topo sorts (we should probably signal some kind of error
292 * condition so that it can be reported)
293 *
294 * @param stateByExtension a map of STATE_ constants by extension. Indicate
295 * the current "color" of this vertex in the algorithm.
296 * @param topo the current topological value.
297 * @param topoByExtension assigned topological values for visited vertices.
298 * @param vertex the current vertex (extension).
299 */
300 private void topologicalSort( TopoSortState state, Extension ext )
301 {
302
303 try
304 {
305 state.startVisiting( ext );
306 for ( Iterator i = ext.getDependencies().iterator(); i.hasNext(); )
307 {
308 ExtensionDependency dep = (ExtensionDependency) i.next();
309 Extension depExt = (Extension) _extensionsById.get( dep.getID() );
310
311 if ( depExt == null ||
312 state.isUnsatisfied( depExt ) ||
313 !isVersionSatisfied( dep, depExt ) )
314 {
315 // We hit an unsatisfied dependency.
316 state.markUnsatisfiedChain();
317 List unsat = (List) _unsatisfiedDependencies.get( ext );
318 if ( unsat == null )
319 {
320 unsat = new ArrayList();
321 _unsatisfiedDependencies.put( ext, unsat );
322 }
323 unsat.add( dep );
324 }
325 // Follow to extensions which this one depends on even if they are not
326 // satisfied (by version). This means if we have
327 // A 1.0 requires B 1.1 requires C 1.2
328 // but the installed versions are:
329 // A 1.0
330 // B 1.5
331 // C 1.2
332 // we still process (and load) C, but not B.
333 if ( depExt != null )
334 {
335 if ( state.isVisiting( depExt ) )
336 {
337 // We hit a cycle.
338 state.markCycleChain( depExt );
339 }
340 if ( state.isUnvisited( depExt ) )
341 {
342 topologicalSort( state, depExt );
343 }
344 }
345 }
346 }
347 finally
348 {
349 if ( !state.isUnsatisfied( ext ) )
350 {
351 state.addToTopo( ext );
352 }
353 state.endVisiting( ext );
354 }
355 }
356
357 /**
358 * Get an extension which satisfies the specified dependency.
359 *
360 * @param dep the dependency to satisfy.
361 * @return the extension satisfying this dependency, or null if no
362 * extension was found satisfying the dependency.
363 */
364 private boolean isVersionSatisfied( ExtensionDependency dep, Extension ext )
365 {
366 if ( ext != null )
367 {
368 // Check minimum version.
369 if ( dep.getMinimumVersion() != null &&
370 dep.getMinimumVersion().compareTo( ext.getVersion() ) > 0 )
371 {
372 return false;
373 }
374
375 // Check maximum version.
376 if ( dep.getMaximumVersion() != null &&
377 dep.getMaximumVersion().compareTo( ext.getVersion() ) < 0 )
378 {
379 return false;
380 }
381 }
382
383 return true;
384 }
385
386 /**
387 * Get the list of extension ids, sorted in order of the topological sort of
388 * their dependency graph.<p>
389 *
390 * This list will not contain the ids of extensions which have unsatisfied
391 * dependencies. To get a list of unsatisfied dependencies, call
392 * {@link #getUnsatisfiedExtensions()}.<p>
393 *
394 * This list may contain extensions whose dependencies are not entirely
395 * satisfied because the dependency graph contained cycles. To get a list
396 * of all identified cycles in the dependency graph, call
397 * getCycles().<p>
398 *
399 * IDE implementations should generally process the manifests of extensions
400 * in the order the extensions are returned by this method.
401 *
402 * @return the set of all extension ids retrieved from processed sources.
403 */
404 public List getSortedExtensionIDs()
405 {
406 ArrayList idList = new ArrayList( _topologicalExtensionList.size() );
407 for ( Iterator i = _topologicalExtensionList.iterator(); i.hasNext(); )
408 {
409 Extension ext = (Extension) i.next();
410 idList.add( ext.getID() );
411 }
412
413 return Collections.unmodifiableList( idList );
414 }
415
416 /**
417 * Get the resolved version of the specified extension id.
418 *
419 * @param id the id of an extension.
420 * @return the resolved version of the extension.
421 */
422 public Version getResolvedVersion( String id )
423 {
424 Extension ext = (Extension) _extensionsById.get( id );
425 return ext.getVersion();
426 }
427
428 /**
429 * Get cycles in the dependency graph. This method returns a collection of
430 * Lists, one for each identified cycle in the graph. Each list entry consists
431 * of Extension instances for each extension in the cycle, starting at some
432 * arbitrary extension and ending with the second encountered instance of
433 * some extension.
434 *
435 * @return a collection of lists.
436 */
437 public Collection getCycles()
438 {
439 return _cycles;
440 }
441
442 /**
443 * Get a collection of extensions with unsatisfied dependencies.
444 *
445 * @return a collection of Extensions which have one or more unsatisfied
446 * dependencies.
447 */
448 public Collection getUnsatisfiedExtensions()
449 {
450 return Collections.unmodifiableCollection( _unsatisfiedDependencies.keySet() );
451 }
452
453 /**
454 * Get the set of unsatisfied dependencies for the specified extension.
455 *
456 * @param unsatisfied an unsatisfied Extension. Must be in the set of
457 * extensions returned by getUnsatisfiedExtensions(). Must not be null.
458 * @return a collection of ExtensionDependency instances, one for each
459 * dependency of this extension that was missing.
460 */
461 public Collection getUnsatisfiedDependencies( Extension unsatisfied )
462 {
463 if ( unsatisfied == null )
464 {
465 throw new NullPointerException( "unsatisfied is null" );
466 }
467 Collection result = (Collection) _unsatisfiedDependencies.get( unsatisfied );
468 if ( result == null )
469 {
470 throw new IllegalArgumentException(
471 "Not in the list of unsatisfied extensions: " + unsatisfied );
472 }
473
474 return Collections.unmodifiableCollection( result );
475 }
476
477
478
479 /**
480 * Get the source of an extension. If there is more than one extension with
481 * the same id, this returns the most recent version of the extension.
482 *
483 * @param id the id of an extension. Must not be null, must be a known
484 * extension.
485 * @return the source of the most recent version of the specified extension.
486 */
487 public ExtensionSource getSource( String id )
488 {
489 if ( id == null )
490 {
491 throw new NullPointerException( "id is null" );
492 }
493
494 Extension extension = (Extension) _extensionsById.get( id );
495 if ( extension == null )
496 {
497 throw new IllegalArgumentException( "Unknown extension id " + id );
498 }
499
500 return (ExtensionSource) _sourcesByExtension.get( extension );
501 }
502
503
504
505 /**
506 * A minimal visitor which just processes the id, version, esdk-version and
507 * dependencies of an extension.
508 */
509 private static class MinimalExtensionVisitor extends BaseExtensionVisitor
510 {
511 private final Map _sourcesByExtension = new HashMap();
512 private ElementVisitor _dependenciesVisitor = new DependenciesVisitor();
513
514 public Map getSourcesByExtension()
515 {
516 return _sourcesByExtension;
517 }
518
519 public void start( ElementStartContext start )
520 {
521 DefaultExtension ext = processExtension( start );
522 if ( ext != null )
523 {
524 start.registerChildVisitor( DependenciesVisitor.ELEMENT,
525 _dependenciesVisitor );
526 }
527 }
528
529 public void end( ElementEndContext end )
530 {
531 ExtensionSource source =
532 (ExtensionSource) end.getScopeData().get( SOURCE );
533 Extension extension =
534 (Extension) end.getScopeData().get( ExtensionHook.KEY_EXTENSION );
535 if ( extension != null )
536 {
537 _sourcesByExtension.put( extension, source );
538 }
539 }
540 }
541
542 // Vertex states for topologicalSort(). Should really be an enum.
543 private Object STATE_NOT_VISITED = "notvisited";
544 private Object STATE_VISITING = "visiting";
545 private Object STATE_VISITED = "visited";
546
547
548 /**
549 * Mechanism used by this class to look up whether an extension is
550 * enabled.
551 */
552 public static interface EnabledExtensionLookup
553 {
554 public boolean isExtensionEnabled( Extension e );
555 }
556 }