blob: 1debdf25c88d9647055e4b586b80eaba90cbce35 [file] [log] [blame]
Thomas Vachuska6b331262015-04-27 11:09:07 -07001package org.onlab.jdvue;
2
3
4import java.io.BufferedReader;
5import java.io.FileInputStream;
6import java.io.IOException;
7import java.io.InputStream;
8import java.io.InputStreamReader;
9import java.util.ArrayList;
10import java.util.Collection;
11import java.util.Collections;
12import java.util.HashMap;
13import java.util.HashSet;
14import java.util.List;
15import java.util.Map;
16import java.util.Set;
17
18import static com.google.common.base.Objects.toStringHelper;
19
20/**
21 * Produces a package & source catalogue.
22 *
23 * @author Thomas Vachuska
24 */
25public class Catalog {
26
27 private static final String PACKAGE = "package";
28 private static final String IMPORT = "import";
29 private static final String STATIC = "static";
30 private static final String SRC_ROOT = "src/main/java/";
31 private static final String WILDCARD = "\\.*$";
32
33 private final Map<String, JavaSource> sources = new HashMap<>();
34 private final Map<String, JavaPackage> packages = new HashMap<>();
35 private final Set<DependencyCycle> cycles = new HashSet<>();
36 private final Set<Dependency> cycleSegments = new HashSet<>();
37 private final Map<JavaPackage, Set<DependencyCycle>> packageCycles = new HashMap<>();
38 private final Map<JavaPackage, Set<Dependency>> packageCycleSegments = new HashMap<>();
39
40 /**
41 * Loads the catalog from the specified catalog file.
42 *
43 * @param catalogPath catalog file path
Thomas Vachuska266b4432015-04-30 18:13:25 -070044 * @throws IOException if unable to read the catalog file
Thomas Vachuska6b331262015-04-27 11:09:07 -070045 */
46 public void load(String catalogPath) throws IOException {
47 InputStream is = new FileInputStream(catalogPath);
48 BufferedReader br = new BufferedReader(new InputStreamReader(is));
49
50 String line;
51 while ((line = br.readLine()) != null) {
52 // Split the line into the two fields: path and pragmas
53 String fields[] = line.trim().split(":");
54 if (fields.length <= 1) {
55 continue;
56 }
57 String path = fields[0];
58
59 // Now split the pragmas on whitespace and trim punctuation
60 String pragma[] = fields[1].trim().replaceAll("[;\n\r]", "").split("[\t ]");
61
62 // Locate (or create) Java source entity based on the path
63 JavaSource source = getOrCreateSource(path);
64
65 // Now process the package or import statements
66 if (pragma[0].equals(PACKAGE)) {
67 processPackageDeclaration(source, pragma[1]);
68
69 } else if (pragma[0].equals(IMPORT)) {
70 if (pragma[1].equals(STATIC)) {
71 processImportStatement(source, pragma[2]);
72 } else {
73 processImportStatement(source, pragma[1]);
74 }
75 }
76 }
77 }
78
79 /**
80 * Analyzes the catalog by resolving imports and identifying circular
81 * package dependencies.
82 */
83 public void analyze() {
84 resolveImports();
85 findCircularDependencies();
86 }
87
88 /**
89 * Identifies circular package dependencies through what amounts to be a
90 * depth-first search rooted with each package.
91 */
92 private void findCircularDependencies() {
93 cycles.clear();
94 for (JavaPackage javaPackage : getPackages()) {
95 findCircularDependencies(javaPackage);
96 }
97
98 cycleSegments.clear();
99 packageCycles.clear();
100 packageCycleSegments.clear();
101
102 for (DependencyCycle cycle : getCycles()) {
103 recordCycleForPackages(cycle);
104 cycleSegments.addAll(cycle.getCycleSegments());
105 }
106 }
107
108 /**
109 * Records the specified cycle into a set for each involved package.
110 *
111 * @param cycle cycle to record for involved packages
112 */
113 private void recordCycleForPackages(DependencyCycle cycle) {
114 for (JavaPackage javaPackage : cycle.getCycle()) {
115 Set<DependencyCycle> cset = packageCycles.get(javaPackage);
116 if (cset == null) {
117 cset = new HashSet<>();
118 packageCycles.put(javaPackage, cset);
119 }
120 cset.add(cycle);
121
122 Set<Dependency> sset = packageCycleSegments.get(javaPackage);
123 if (sset == null) {
124 sset = new HashSet<>();
125 packageCycleSegments.put(javaPackage, sset);
126 }
127 sset.addAll(cycle.getCycleSegments());
128 }
129 }
130
131 /**
132 * Identifies circular dependencies in which this package participates
133 * using depth-first search.
134 *
135 * @param javaPackage Java package to inspect for dependency cycles
136 */
137 private void findCircularDependencies(JavaPackage javaPackage) {
138 // Setup a depth trace anchored at the given java package.
139 List<JavaPackage> trace = newTrace(new ArrayList<JavaPackage>(), javaPackage);
140
141 Set<JavaPackage> searched = new HashSet<>();
142 searchDependencies(javaPackage, trace, searched);
143 }
144
145 /**
146 * Generates a new trace using the previous one and a new element
147 *
148 * @param trace old search trace
149 * @param javaPackage package to add to the trace
150 * @return new search trace
151 */
152 private List<JavaPackage> newTrace(List<JavaPackage> trace,
153 JavaPackage javaPackage) {
154 List<JavaPackage> newTrace = new ArrayList<>(trace);
155 newTrace.add(javaPackage);
156 return newTrace;
157 }
158
159
160 /**
161 * Recursive depth-first search through dependency tree
162 *
163 * @param javaPackage java package being searched currently
164 * @param trace search trace
165 * @param searched set of java packages already searched
166 */
167 private void searchDependencies(JavaPackage javaPackage,
168 List<JavaPackage> trace,
169 Set<JavaPackage> searched) {
170 if (!searched.contains(javaPackage)) {
171 searched.add(javaPackage);
172 for (JavaPackage dependency : javaPackage.getDependencies()) {
173 if (trace.contains(dependency)) {
174 cycles.add(new DependencyCycle(trace, dependency));
175 } else {
176 searchDependencies(dependency, newTrace(trace, dependency), searched);
177 }
178 }
179 }
180 }
181
182 /**
183 * Resolves import names of Java sources into imports of entities known
184 * to this catalog. All other import names will be ignored.
185 */
186 private void resolveImports() {
187 for (JavaPackage javaPackage : getPackages()) {
188 Set<JavaPackage> dependencies = new HashSet<>();
189 for (JavaSource source : javaPackage.getSources()) {
190 Set<JavaEntity> imports = resolveImports(source);
191 source.setImports(imports);
192 dependencies.addAll(importedPackages(imports));
193 }
194 javaPackage.setDependencies(dependencies);
195 }
196 }
197
198 /**
199 * Produces a set of imported Java packages from the specified set of
200 * Java source entities.
201 *
202 * @param imports list of imported Java source entities
203 * @return list of imported Java packages
204 */
205 private Set<JavaPackage> importedPackages(Set<JavaEntity> imports) {
206 Set<JavaPackage> packages = new HashSet<>();
207 for (JavaEntity entity : imports) {
208 packages.add(entity instanceof JavaPackage ? (JavaPackage) entity :
209 ((JavaSource) entity).getPackage());
210 }
211 return packages;
212 }
213
214 /**
215 * Resolves import names of the specified Java source into imports of
216 * entities known to this catalog. All other import names will be ignored.
217 *
218 * @param source Java source
219 * @return list of resolved imports
220 */
221 private Set<JavaEntity> resolveImports(JavaSource source) {
222 Set<JavaEntity> imports = new HashSet<>();
223 for (String importName : source.getImportNames()) {
224 JavaEntity entity = importName.matches(WILDCARD) ?
225 getPackage(importName.replaceAll(WILDCARD, "")) :
226 getSource(importName);
227 if (entity != null) {
228 imports.add(entity);
229 }
230 }
231 return imports;
232 }
233
234 /**
235 * Returns either an existing or a newly created Java package.
236 *
237 * @param packageName Java package name
238 * @return Java package
239 */
240 private JavaPackage getOrCreatePackage(String packageName) {
241 JavaPackage javaPackage = packages.get(packageName);
242 if (javaPackage == null) {
243 javaPackage = new JavaPackage(packageName);
244 packages.put(packageName, javaPackage);
245 }
246 return javaPackage;
247 }
248
249 /**
250 * Returns either an existing or a newly created Java source.
251 *
252 * @param path Java source path
253 * @return Java source
254 */
255 private JavaSource getOrCreateSource(String path) {
256 String name = nameFromPath(path);
257 JavaSource source = sources.get(name);
258 if (source == null) {
259 source = new JavaSource(name, path);
260 sources.put(name, source);
261 }
262 return source;
263 }
264
265 /**
266 * Extracts a fully qualified source class name from the given path.
267 * <p/>
268 * For now, this implementation assumes standard Maven source structure
269 * and thus will look for start of package name under 'src/main/java/'.
270 * If it will not find such a prefix, it will simply return the path as
271 * the name.
272 *
273 * @param path source path
274 * @return source name
275 */
276 private String nameFromPath(String path) {
277 int i = path.indexOf(SRC_ROOT);
278 String name = i < 0 ? path : path.substring(i + SRC_ROOT.length());
279 return name.replaceAll("\\.java$", "").replace("/", ".");
280 }
281
282 /**
283 * Processes the package declaration pragma for the given source.
284 *
285 * @param source Java source
286 * @param packageName Java package name
287 */
288 private void processPackageDeclaration(JavaSource source, String packageName) {
289 JavaPackage javaPackage = getOrCreatePackage(packageName);
290 source.setPackage(javaPackage);
291 javaPackage.addSource(source);
292 }
293
294 /**
295 * Processes the import pragma for the given source.
296 *
297 * @param source Java source
298 * @param name name of the Java entity being imported (class or package)
299 */
300 private void processImportStatement(JavaSource source, String name) {
301 source.addImportName(name);
302 }
303
304 /**
305 * Returns the collection of java sources.
306 *
307 * @return collection of java sources
308 */
309 public Collection<JavaSource> getSources() {
310 return Collections.unmodifiableCollection(sources.values());
311 }
312
313 /**
314 * Returns the Java source with the specified name.
315 *
316 * @param name Java source name
317 * @return Java source
318 */
319 public JavaSource getSource(String name) {
320 return sources.get(name);
321 }
322
323 /**
324 * Returns the collection of all Java packages.
325 *
326 * @return collection of java packages
327 */
328 public Collection<JavaPackage> getPackages() {
329 return Collections.unmodifiableCollection(packages.values());
330 }
331
332 /**
333 * Returns the set of all Java package dependency cycles.
334 *
335 * @return set of dependency cycles
336 */
337 public Set<DependencyCycle> getCycles() {
338 return Collections.unmodifiableSet(cycles);
339 }
340
341 /**
342 * Returns the set of all Java package dependency cycle segments.
343 *
344 * @return set of dependency cycle segments
345 */
346 public Set<Dependency> getCycleSegments() {
347 return Collections.unmodifiableSet(cycleSegments);
348 }
349
350 /**
351 * Returns the set of dependency cycles which involve the specified package.
352 *
353 * @param javaPackage java package
354 * @return set of dependency cycles
355 */
356 public Set<DependencyCycle> getPackageCycles(JavaPackage javaPackage) {
357 Set<DependencyCycle> set = packageCycles.get(javaPackage);
358 return Collections.unmodifiableSet(set == null ? new HashSet<DependencyCycle>() : set);
359 }
360
361 /**
362 * Returns the set of dependency cycle segments which involve the specified package.
363 *
364 * @param javaPackage java package
365 * @return set of dependency cycle segments
366 */
367 public Set<Dependency> getPackageCycleSegments(JavaPackage javaPackage) {
368 Set<Dependency> set = packageCycleSegments.get(javaPackage);
369 return Collections.unmodifiableSet(set == null ? new HashSet<Dependency>() : set);
370 }
371
372 /**
373 * Returns the Java package with the specified name.
374 *
375 * @param name Java package name
376 * @return Java package
377 */
378 public JavaPackage getPackage(String name) {
379 return packages.get(name);
380 }
381
382 @Override
383 public String toString() {
384 return toStringHelper(this)
385 .add("packages", packages.size())
386 .add("sources", sources.size())
387 .add("cycles", cycles.size())
388 .add("cycleSegments", cycleSegments.size()).toString();
389 }
390
391}