summaryrefslogtreecommitdiff
path: root/support/latex-dependency-grapher/source/main/java/ch/bfh/lpdg/DependencyScanner.java
blob: fa635b953219bb8a4a79a6e95ff1d8cdc9bf597c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
package ch.bfh.lpdg;

import ch.bfh.lpdg.datastructure.Dependency;
import ch.bfh.lpdg.datastructure.DependencyType;

import java.io.*;
import java.nio.file.Paths;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import static java.util.Collections.emptyList;

/**
 * This class is responsible for scanning dependencies for a file input.
 */
public class DependencyScanner {

    public static final String REGEX_DEPENDENCIES = "\\u005Cusepackage\\{(.*?)}|\\u005CRequirePackage\\{(.*?)}|\\u005Cinput\\{(.*?)}|\\u005Cinclude\\{(.*?)}";
    public final static DependencyScanner INSTANCE = new DependencyScanner();
    private String filePathWithoutFileName;

    private DependencyScanner() {
    }

    public static DependencyScanner getInstance() {
        return INSTANCE;
    }

    /**
     * This method can be called recursively, it loops through all dependencies of the given file.
     * This is done via a regex pattern -> it scans for all types in the DependencyType.java class.
     * It scans each line of the file individually and if the line matches one of the patterns in the regex the extractDependency()
     * method is called and is then added to the list of dependencies.
     *
     * @param filePath                 is needed to know the initial location of the document that has to be scanned
     * @param type                     describes what type the currently scanned document has
     * @param source                   describes what type of dependency the currently scanned dependency has
     * @param alreadyAddedDependencies is needed to avoid dependency loops
     * @param scanDepth                limits how deep the scan looks into the dependencies
     * @return returns a Dependency containing a list of all its dependencies itself
     */
    public Dependency findDependencies(final String filePath, final DependencyType type, final String source, final List<String> alreadyAddedDependencies, final int scanDepth) {
        final String fileName = Paths.get(filePath).getFileName().toString();
        final String fileNameWithoutExt = this.resolveDependencyName(fileName);
        filePathWithoutFileName = String.copyValueOf(filePath.toCharArray());
        filePathWithoutFileName = filePathWithoutFileName.replace(fileName, "");

        final Dependency dependency = new Dependency(type, fileNameWithoutExt, source);

        if(scanDepth > 0) {
            try {
                final BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(filePath)));
                final Pattern pattern = Pattern.compile(REGEX_DEPENDENCIES);

                String line;
                while ((line = br.readLine()) != null) {
                    final Matcher matcher = pattern.matcher(line);

                    while (matcher.find()) {
                        final String usepackage = matcher.group(1);
                        final String requirepackage = matcher.group(2);
                        final String input = matcher.group(3);
                        final String include = matcher.group(4);

                        // Create dependencies for dependency
                        final List<Dependency> extractedUP = this.extractDependencies(usepackage, DependencyType.USE_PACKAGE, matcher.group(0), alreadyAddedDependencies, scanDepth);
                        final List<Dependency> extractedRP = this.extractDependencies(requirepackage, DependencyType.REQUIRE_PACKAGE, matcher.group(0), alreadyAddedDependencies, scanDepth);
                        final List<Dependency> extractedINP = this.extractDependencies(input, DependencyType.INPUT, matcher.group(0), alreadyAddedDependencies, scanDepth);
                        final List<Dependency> extractedINC = this.extractDependencies(include, DependencyType.INCLUDE, matcher.group(0), alreadyAddedDependencies, scanDepth);

                        // Add dependency to tree
                        final List<Dependency> extractedDependencies = Stream.of(extractedUP, extractedRP, extractedINP, extractedINC).filter(Objects::nonNull).flatMap(Collection::stream).toList();
                        extractedDependencies.forEach(dep -> dependency.getDependencyList().add(dep));
                    }
                }
                br.close();
            } catch (FileNotFoundException ex) {
                InteractionHandler.getInstance().printDebugMessage("File: " + fileName + " was not found.");
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }

        var cleanDependencies = removeInvalidDependencies(dependency);
        dependency.setDependencyList(cleanDependencies);

        return dependency;
    }

    /**
     * This method extracts all dependencies for a Dependency scanned in the findDependencies() method.
     *
     * @return a list of dependencies, this is required for a format like requirePackage{array, color}
     */
    private List<Dependency> extractDependencies(final String dependencyName, final DependencyType type, final String source, final List<String> alreadyAddedDependencies, final int scanDepth) {
        final List<String> currentDependencies = new ArrayList<>(alreadyAddedDependencies);
        if (dependencyName != null && !dependencyName.isEmpty()) {
            // Resolve multiline usePackage dependencies, ex. usePackage{array,color}
            if (dependencyName.contains(",")) {
                final String[] packages = dependencyName.split(",");
                final List<Dependency> foundDependencies = new ArrayList<>();
                Arrays.stream(packages).forEach(p -> {
                    if (!currentDependencies.contains(p)) {
                        // Try to resolve the path for the found dependency, if a path is found, the file has to be scanned recursively.
                        final String filePath = this.resolvePath(p, type);
                        if (filePath == null) {
                            foundDependencies.add(new Dependency(type, p, source));
                        } else {
                            currentDependencies.add(p);
                            final Dependency dependency = this.findDependencies(filePath, type, source, currentDependencies, scanDepth-1);
                            if (dependency != null) {
                                foundDependencies.add(dependency);
                            }
                        }
                    }
                });
                return foundDependencies;
            } else {
                if (!alreadyAddedDependencies.contains(dependencyName)) {
                    // Try to resolve the path for the found dependency, if a path is found, the file has to be scanned recursively.
                    final String filePath = this.resolvePath(dependencyName, type);
                    if (filePath == null) {
                        return Collections.singletonList(new Dependency(type, dependencyName, source));
                    } else {
                        currentDependencies.add(dependencyName);
                        return List.of(this.findDependencies(filePath, type, source, currentDependencies, scanDepth -1));
                    }
                }
            }
        }
        return emptyList();
    }

    /**
     * This method resolves a path for a LaTeX dependency
     * @param packageName the name of the package that should be searched for with kpsewhich
     * @param type the type of the package, this is needed to search for project dependencies for input and include
     * @return returns the path of the dependency location
     */
    private String resolvePath(final String packageName, final DependencyType type) {
        //Execute kpsewhich tool to find path of package
        Process process;
        try {
            boolean hasExtension = packageName.lastIndexOf('.') != -1;

            //If the package doesn't have an extension add the standard one
            process = Runtime.getRuntime().exec(String.format("kpsewhich %s%s", packageName, hasExtension ? "" : ".sty"));
        } catch (IOException e) {
            System.err.println("Couldn't execute kpsewhich command.");
            throw new RuntimeException(e);
        }

        StringBuilder result = new StringBuilder();
        BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream()));
        String s;
        while (true) {
            try {
                if ((s = stdInput.readLine()) == null) break; //Path not found
            } catch (IOException e) {
                System.err.println("Couldn't read output of kpsewhich command.");
                throw new RuntimeException(e);
            }

            result.append(s);
        }

        if (result.isEmpty()) {
            if (type.equals(DependencyType.INCLUDE) || type.equals(DependencyType.INPUT)) {
                return filePathWithoutFileName + packageName + ".tex";
            }
            return null;
        }

        return Paths.get(result.toString()).toAbsolutePath().toString();
    }

    /**
     * Removes invalid dependencies like variable names.
     * Example: #1.def, \gin@driver
     *
     * @param d The List of all found dependencies
     * @return A clean list without variable names and other invalid dependencies
     */
    private List<Dependency> removeInvalidDependencies(Dependency d) {
        return d.getDependencyList().stream().filter(e -> !e.getName().contains("#") && !e.getName().contains("@")).toList();
    }

    private String resolveDependencyName(final String fileName) {
        return fileName.split("\\.")[0];
    }
}