I'm having a problem in writing an algorithm to help me scan a file system and find all subclasses of a certain class.
Details:
I've an app that scans an external application using nio
Files.walk()
while retrieving I check for "extends SuperClass" while reading the file if the word exits, I add the class name in my list as follows:
List<String> subclasses = new ArrayList<>();
Files.walk(appPath)
.filter(p->Files.isRegularFile(p) && p.toString()
.endsWith(".java")).forEach(path -> {
try {
List<String> lines = Files.readAllLines(path);
Pattern pattern = Pattern.compile("\\bextends SuperClass\\b");
Matcher matcher = pattern
.matcher(lines.stream()
.collect(Collectors.joining(" ")));
boolean isChild = matcher.find();
if(isChild) subclasses.add(path.getFileName().toString());
}catch (IOException e){
//handle IOE
}
The problem with the above is that it only gets direct subclasses of SuperClass
but I need to retrieve all direct and indirect subclasses.
I thought about recursion since I've no Idea how many subclasses of SuperClass
there is but I couldn't implement any reasonable implementation.
NOTES:
SuperClass
there isextends
exists Edit:
I use the following regex to compare both name and import to make sure even in case of same name different packages the output is correct:
Pattern pattern = Pattern.compile("("+superClasss.getPackage()+")[\\s\\S]*(\\bextends "+superClass.getName()+"\\b)[\\s\\S]");
I also tried:
Pattern pattern = Pattern.compile("\\bextends "+superClass.getName()+"\\b");
But there is also some missing subclasses, I believe that the code bellow skips some checks, and doesn't fully work:
public static List<SuperClass> getAllSubClasses(Path path, SuperClass parentClass) throws IOException{
classesToDo.add(baseClass);
while(classesToDo.size() > 0) {
SuperClass superClass = classesToDo.remove(0);
List<SuperClass> subclasses = getDirectSubClasses(parentPath,parentClass);
if(subclasses.size() > 0)
classes.addAll(subclasses);
classesToDo.addAll(subclasses);
}
return classes;
}
Any help is truly appreciated!
Edit 2
I also noticed another problem, is that when I detect a subclass
I get the file name currentPath.getFileName()
which might or might not be the subclass name as the subclass may be a nested
or non-public class
in the same file.
I strongly recommend parsing compiled class files instead of source code. Since these class files are already optimized for being processed by machines, a lot of the complexity and corner cases of the source code file processing has been eliminated.
So a solution to build a complete class hierarchy tree using the ASM library would look like this:
public static Map<String, Set<String>> getClassHierarchy(Path root) throws IOException {
return Files.walk(root)
.filter(p->Files.isRegularFile(p) && isClass(p.getFileName().toString()))
.map(p -> getClassAndSuper(p))
.collect(Collectors.groupingBy(Map.Entry::getValue,
Collectors.mapping(Map.Entry::getKey, Collectors.toSet())));
}
private static boolean isClass(String fName) {
// skip package-info and module-info
return fName.endsWith(".class") && !fName.endsWith("-info.class");
}
private static Map.Entry<String,String> getClassAndSuper(Path p) {
final class CV extends ClassVisitor {
Map.Entry<String,String> result;
public CV() {
super(Opcodes.ASM5);
}
@Override
public void visit(int version, int access,
String name, String signature, String superName, String[] interfaces) {
result = new AbstractMap.SimpleImmutableEntry<>(
Type.getObjectType(name).getClassName(),
superName!=null? Type.getObjectType(superName).getClassName(): "");
}
}
try {
final CV visitor = new CV();
new ClassReader(Files.readAllBytes(p)).accept(visitor, ClassReader.SKIP_CODE);
return visitor.result;
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
As a bonus, resp. to create some test cases, the following method adds the ability to build the hierarchy for a runtime class’ source:
public static Map<String, Set<String>> getClassHierarchy(Class<?> context)
throws IOException, URISyntaxException {
Path p;
URI clURI = context.getResource(context.getSimpleName()+".class").toURI();
if(clURI.getScheme().equals("jrt")) p = Paths.get(URI.create("jrt:/modules"));
else {
if(!clURI.getScheme().equals("file")) try {
FileSystems.getFileSystem(clURI);
} catch(FileSystemNotFoundException ex) {
FileSystems.newFileSystem(clURI, Collections.emptyMap());
}
String qn = context.getName();
p = Paths.get(clURI).getParent();
for(int ix = qn.indexOf('.'); ix>0; ix = qn.indexOf('.', ix+1)) p = p.getParent();
}
return getClassHierarchy(p);
}
Then, you can do
Map<String, Set<String>> hierarchy = getClassHierarchy(Number.class);
System.out.println("Direct subclasses of "+Number.class);
hierarchy.getOrDefault("java.lang.Number", Collections.emptySet())
.forEach(System.out::println);
and get
Direct subclasses of class java.lang.Number
java.lang.Float
java.math.BigDecimal
java.util.concurrent.atomic.AtomicLong
java.lang.Double
java.lang.Long
java.util.concurrent.atomic.AtomicInteger
java.lang.Short
java.math.BigInteger
java.lang.Byte
java.util.concurrent.atomic.Striped64
java.lang.Integer
or
Map<String, Set<String>> hierarchy = getClassHierarchy(Number.class);
System.out.println("All subclasses of "+Number.class);
printAllClasses(hierarchy, "java.lang.Number", " ");
private static void printAllClasses(
Map<String, Set<String>> hierarchy, String parent, String i) {
hierarchy.getOrDefault(parent, Collections.emptySet())
.forEach(x -> {
System.out.println(i+x);
printAllClasses(hierarchy, x, i+" ");
});
}
to get
All subclasses of class java.lang.Number
java.lang.Float
java.math.BigDecimal
java.util.concurrent.atomic.AtomicLong
java.lang.Double
java.lang.Long
java.util.concurrent.atomic.AtomicInteger
java.lang.Short
java.math.BigInteger
java.lang.Byte
java.util.concurrent.atomic.Striped64
java.util.concurrent.atomic.LongAdder
java.util.concurrent.atomic.LongAccumulator
java.util.concurrent.atomic.DoubleAdder
java.util.concurrent.atomic.DoubleAccumulator
java.lang.Integer
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With