package com.google.jstestdriver.idea.execution.generator; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.text.StringUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.File; import java.io.IOException; import java.util.*; public class JstdGeneratedConfigStructure { private static Set<File> OUR_FILE_SYSTEM_ROOTS = Sets.newHashSet(Arrays.asList(File.listRoots())); private List<File> myLoadFiles = Lists.newArrayList(); public void addLoadFile(@NotNull File loadFile) { try { File canonicalLoadFile = loadFile.getCanonicalFile(); myLoadFiles.add(canonicalLoadFile); } catch (IOException e) { throw new RuntimeException("File.getCanonicalFile() failed for " + loadFile); } } @NotNull public String asFileContent() { StringBuilder builder = new StringBuilder(); builder.append("# Generated by IDEA\n"); Pair<String, List<String>> result = generateBasePathAndLoadPaths(); String basePath = result.getFirst(); if (!basePath.isEmpty()) { builder.append("basepath: \"").append(basePath).append("\"\n\n"); } builder.append("load:\n"); for (String path : result.getSecond()) { builder.append(" - \"").append(path).append("\"\n"); } return builder.toString(); } @Nullable private static File findLCA(@NotNull List<File> files) { List<List<File>> allParents = new ArrayList<List<File>>(); for (File file : files) { File dir = file.isDirectory() ? file : file.getParentFile(); List<File> parents = buildParents(dir); allParents.add(parents); } List<File> common = null; for (List<File> next : allParents) { if (common == null) { common = next; } else { List<File> newCommon = new ArrayList<File>(); for (int i = 0; i < Math.min(common.size(), next.size()); i++) { if (common.get(i).equals(next.get(i))) { newCommon.add(common.get(i)); } else { break; } } common = newCommon; } } if (common == null) { throw new RuntimeException(); } if (common.isEmpty()) { return null; } return common.get(common.size() - 1); } @NotNull private Pair<String, List<String>> generateBasePathAndLoadPaths() { File loadFilesLCA = findLCA(myLoadFiles); if (loadFilesLCA == null) { // Windows-specific logic return createEmptyBasePathAndAbsoluteLoadPaths(); } return createAbsoluteBasePathAndRelativeLoadPaths(loadFilesLCA); } private Pair<String, List<String>> createEmptyBasePathAndAbsoluteLoadPaths() { List<String> absolutePaths = new ArrayList<String>(); for (File loadFile : myLoadFiles) { absolutePaths.add(loadFile.getAbsolutePath()); } return Pair.create("", absolutePaths); } private Pair<String, List<String>> createAbsoluteBasePathAndRelativeLoadPaths(@NotNull File loadFilesLCA) { List<String> absolutePaths = new ArrayList<String>(); for (File loadFile : myLoadFiles) { List<String> path = Pathfinder.FROM_ANCESTOR_EXCLUDED_TO_DESCENDANT_INCLUDED.findPath(loadFilesLCA, loadFile); absolutePaths.add(StringUtil.join(path, "/")); } return Pair.create(FileUtil.toSystemIndependentName(loadFilesLCA.getAbsolutePath()), absolutePaths); } private static List<File> buildParents(@NotNull final File dir) { List<File> parents = Lists.newArrayList(); File f = dir; while (f != null) { parents.add(f); if (OUR_FILE_SYSTEM_ROOTS.contains(f)) { break; } f = f.getParentFile(); } Collections.reverse(parents); return parents; } private static class Pathfinder { private static Pathfinder FROM_ANCESTOR_EXCLUDED_TO_DESCENDANT_INCLUDED = new Pathfinder(false, true, true); private final boolean myIncludeAncestor; private final boolean myIncludeDescendant; private final boolean myFromAncestorToDescendant; private Pathfinder(boolean includeAncestor, boolean includeDescendant, boolean fromAncestorToDescendant) { myIncludeAncestor = includeAncestor; myIncludeDescendant = includeDescendant; myFromAncestorToDescendant = fromAncestorToDescendant; } /** * Constructs string list that represent a path between ancestor directory and descendant file. * Whether {@code ancestor} directory and {@code descendant} are included in path is controlled by {@code myIncludeAncestor} and {@code myIncludeDescendant} flags. * <pre> * Pathfinder pathfinder = new Pathfinder(true, false, true); * List<String> path = pathfinder.findPath(new File("/var/www"), new File("/var/www/js-test-driver/test.js"); * // path is ["www", "js-test-driver"]; * </pre> * * @param ancestor directory, that contains descendant file * @param descendant file or directory, that is contained in ancestor directory * @return string list of files or directories names */ private List<String> findPath(@NotNull final File ancestor, @NotNull final File descendant) { List<String> fileNames = Lists.newArrayList(); File file = descendant; boolean ancestorMet = false; boolean add = myIncludeDescendant; while (file != null) { if (add) { String name = file.getName(); if (name.isEmpty() && OUR_FILE_SYSTEM_ROOTS.contains(file)) { name = file.getAbsolutePath(); } fileNames.add(name); } if (ancestorMet) { break; } file = file.getParentFile(); ancestorMet = ancestor.equals(file); add = !ancestorMet || myIncludeAncestor; } if (!ancestorMet) { throw new RuntimeException("Ancestor is not visited! (ancestor:'" + ancestor + "', descendant:'" + descendant + "')"); } if (myFromAncestorToDescendant) { Collections.reverse(fileNames); } return fileNames; } } }