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 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 extractedUP = this.extractDependencies(usepackage, DependencyType.USE_PACKAGE, matcher.group(0), alreadyAddedDependencies, scanDepth); final List extractedRP = this.extractDependencies(requirepackage, DependencyType.REQUIRE_PACKAGE, matcher.group(0), alreadyAddedDependencies, scanDepth); final List extractedINP = this.extractDependencies(input, DependencyType.INPUT, matcher.group(0), alreadyAddedDependencies, scanDepth); final List extractedINC = this.extractDependencies(include, DependencyType.INCLUDE, matcher.group(0), alreadyAddedDependencies, scanDepth); // Add dependency to tree final List 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 extractDependencies(final String dependencyName, final DependencyType type, final String source, final List alreadyAddedDependencies, final int scanDepth) { final List 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 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 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]; } }