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