package dev.jeka.core.api.tooling.intellij; import dev.jeka.core.api.depmanagement.*; import dev.jeka.core.api.file.JkPathTree; import dev.jeka.core.api.file.JkPathTreeSet; import dev.jeka.core.api.java.JkJavaVersion; import dev.jeka.core.api.java.project.JkJavaIdeSupport; import dev.jeka.core.api.system.JkLocator; import dev.jeka.core.api.system.JkLog; import dev.jeka.core.api.utils.*; import dev.jeka.core.tool.JkConstants; import org.w3c.dom.Document; import org.w3c.dom.Element; import javax.xml.stream.FactoryConfigurationError; import javax.xml.stream.XMLOutputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamWriter; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; /** * Provides method to generate and read Eclipse metadata files. */ public final class JkImlGenerator { private static final String ENCODING = "UTF-8"; private static final String T1 = " "; private static final String T2 = T1 + T1; private static final String T3 = T2 + T1; private static final String T4 = T3 + T1; private static final String T5 = T4 + T1; private final JkJavaIdeSupport ideSupport; /** Dependency resolver to fetch module dependencies for build classes */ private JkDependencyResolver defDependencyResolver; private JkDependencySet defDependencies; /** Can be empty but not null */ private Iterable<String> importedTestModules = JkUtilsIterable.listOf(); private boolean forceJdkVersion; private boolean failOnDepsResolutionError; /* When true, path will be mentioned with $JEKA_HOME$ and $JEKA_REPO$ instead of explicit absolute path. */ private boolean useVarPath; // Keep trace of already processed module-library entries to avoid duplicates private final Set<String> processedLibEntries = new HashSet<>(); // Keep trace of already processed module entries to avoid duplicates private final Set<String> processedModueEntries = new HashSet<>(); private XMLStreamWriter writer; private JkImlGenerator(JkJavaIdeSupport ideSupport) { this.ideSupport = ideSupport; } /** * Constructs a {@link JkImlGenerator} to the project base directory */ public static JkImlGenerator of(JkJavaIdeSupport ideSupport) { return new JkImlGenerator(ideSupport); } /** Generate the .classpath file */ public String generate() { try { return _generate(); } catch (final Exception e) { throw JkUtilsThrowable.unchecked(e); } } private String _generate() throws IOException, XMLStreamException, FactoryConfigurationError { final ByteArrayOutputStream fos = new ByteArrayOutputStream(); writer = createWriter(fos); writeHead(); writeOutput(); writeJdk(); writeContent(); writeOrderEntrySourceFolder(); final Set<Path> allPaths = new HashSet<>(); final Set<Path> allModules = new HashSet<>(); if (this.ideSupport.getDependencyResolver()!= null) { writeDependencies(ideSupport.getDependencies(), ideSupport.getDependencyResolver(), allPaths, allModules, false); } if (this.defDependencyResolver != null) { writeDependencies(this.defDependencies, this.defDependencyResolver, allPaths, allModules, true); } writeIntellijModuleImportDependencies(this.importedTestModules, "TEST"); writeFoot(); writer.close(); return fos.toString(ENCODING); } private void writeHead() throws XMLStreamException { Path pluginXml = findPluginXml(); boolean pluginModule = pluginXml != null; writer.writeStartDocument(ENCODING, "1.0"); writer.writeCharacters("\n"); writer.writeStartElement("module"); writer.writeAttribute("type", pluginModule ? "PLUGIN_MODULE" : "JAVA_MODULE"); writer.writeAttribute("version", "4"); writer.writeCharacters("\n" + T1); if (pluginModule) { writer.writeEmptyElement("component"); writer.writeAttribute("name", "DevKit.ModuleBuildProperties"); writer.writeAttribute("url", "file://$MODULE_DIR$/" + ideSupport.getProdLayout() .getBaseDir().relativize(pluginXml) .toString().replace("\\", "/")); writer.writeCharacters("\n" + T1); } writer.writeStartElement("component"); writer.writeAttribute("name", "NewModuleRootManager"); writer.writeAttribute("inherit-compileRunner-output", "false"); writer.writeCharacters("\n"); } private void writeFoot() throws XMLStreamException { writer.writeCharacters(T1); writer.writeEndElement(); writer.writeCharacters("\n"); writer.writeEndElement(); writer.writeEndDocument(); writer.flush(); writer.close(); } private void writeOutput() throws XMLStreamException { writer.writeCharacters(T2); writer.writeEmptyElement("output"); writer.writeAttribute("url", "file://$MODULE_DIR$/.idea/output/production"); writer.writeCharacters("\n"); writer.writeCharacters(T2); writer.writeEmptyElement("output-test"); writer.writeAttribute("url", "file://$MODULE_DIR$/.idea/output/test"); writer.writeCharacters("\n"); writer.writeCharacters(T2); writer.writeEmptyElement("exclude-output"); writer.writeCharacters("\n"); } private void writeContent() throws XMLStreamException { writer.writeCharacters(T2); writer.writeStartElement("content"); writer.writeAttribute("url", "file://$MODULE_DIR$"); writer.writeCharacters("\n"); // Write build sources writer.writeCharacters(T3); writer.writeEmptyElement("sourceFolder"); writer.writeAttribute("url", "file://$MODULE_DIR$/" + JkConstants.DEF_DIR); writer.writeAttribute("isTestSource", "true"); writer.writeCharacters("\n"); writer.writeCharacters(T2); // Write test sources final Path projectDir = ideSupport.getProdLayout().getBaseDir(); if (ideSupport.getTestLayout() != null) { for (final JkPathTree fileTree : ideSupport.getTestLayout().resolveSources().toList()) { if (fileTree.exists()) { writer.writeCharacters(T1); writer.writeEmptyElement("sourceFolder"); final String path = projectDir.relativize(fileTree.getRoot()).normalize().toString().replace('\\', '/'); writer.writeAttribute("url", "file://$MODULE_DIR$/" + path); writer.writeAttribute("isTestSource", "true"); writer.writeCharacters("\n"); } } for (final JkPathTree fileTree : ideSupport.getTestLayout().resolveResources().toList()) { if (fileTree.exists() && !contains(ideSupport.getTestLayout().resolveSources(), fileTree.getRootDirOrZipFile())) { writer.writeCharacters(T3); writer.writeEmptyElement("sourceFolder"); final String path = projectDir.relativize(fileTree.getRoot()).normalize().toString().replace('\\', '/'); writer.writeAttribute("url", "file://$MODULE_DIR$/" + path); writer.writeAttribute("type", "java-test-resource"); writer.writeCharacters("\n"); } } } // Write production sources if (ideSupport.getProdLayout() != null) { for (final JkPathTree fileTree : ideSupport.getProdLayout().resolveSources().toList()) { if (fileTree.exists()) { writer.writeCharacters(T3); writer.writeEmptyElement("sourceFolder"); final String path = projectDir.relativize(fileTree.getRoot()).normalize().toString().replace('\\', '/'); writer.writeAttribute("url", "file://$MODULE_DIR$/" + path); writer.writeAttribute("isTestSource", "false"); writer.writeCharacters("\n"); } } // Write production test resources for (final JkPathTree fileTree : ideSupport.getProdLayout().resolveResources().toList()) { if (fileTree.exists() && !contains(ideSupport.getProdLayout().resolveSources(), fileTree.getRootDirOrZipFile())) { writer.writeCharacters(T3); writer.writeEmptyElement("sourceFolder"); final String path = projectDir.relativize(fileTree.getRoot()).normalize().toString().replace('\\', '/'); writer.writeAttribute("url", "file://$MODULE_DIR$/" + path); writer.writeAttribute("type", "java-resource"); writer.writeCharacters("\n"); } } } writer.writeCharacters(T3); writer.writeEmptyElement("excludeFolder"); writer.writeAttribute("url", "file://$MODULE_DIR$/" + JkConstants.OUTPUT_PATH); writer.writeCharacters("\n"); writer.writeCharacters(T3); writer.writeEmptyElement("excludeFolder"); writer.writeAttribute("url", "file://$MODULE_DIR$/" + JkConstants.WORK_PATH); writer.writeCharacters("\n"); writer.writeCharacters(T3); writer.writeEmptyElement("excludeFolder"); writer.writeAttribute("url", "file://$MODULE_DIR$/.idea/output"); writer.writeCharacters("\n"); writer.writeCharacters(T2); writer.writeEndElement(); writer.writeCharacters("\n"); } private static boolean contains(JkPathTreeSet treeSet, Path path) { for (JkPathTree tree : treeSet.toList()) { if (JkUtilsPath.isSameFile(tree.getRoot(), path)) { return true; } } return false; } private void writeIntellijModuleImportDependencies(Iterable<String> moduleNames, String scope) throws XMLStreamException { for (final String moduleName : moduleNames) { if (!processedModueEntries.contains(moduleName)) { writeOrderEntryForModule(moduleName, scope); processedModueEntries.add(moduleName); } } } private void writeDependencies(JkDependencySet dependencies, JkDependencyResolver resolver, Set<Path> allPaths, Set<Path> allModules, boolean forceTest) throws XMLStreamException { final JkResolveResult resolveResult = resolver.resolve(dependencies.minusModuleDependenciesWithIdeProjectDir()); if (resolveResult.getErrorReport().hasErrors()) { if (failOnDepsResolutionError) { throw new IllegalStateException("Fail at resolvig dependencies : " + resolveResult.getErrorReport()); } else { JkLog.warn(resolveResult.getErrorReport().toString()); JkLog.warn("The generated iml file won't take in account missing files."); } } final JkDependencyNode tree = resolveResult.getDependencyTree(); for (final JkDependencyNode node : tree.toFlattenList()) { // Maven dependency if (node.isModuleNode()) { final String ideScope = forceTest ? "TEST" : ideScope(node.getModuleInfo().getResolvedScopes()); final List<LibPath> paths = toLibPath(node.getModuleInfo(), resolver.getRepos(), ideScope); for (final LibPath libPath : paths) { if (!allPaths.contains(libPath.bin)) { writeOrderEntryForLib(libPath); allPaths.add(libPath.bin); } } // File dependencies (file ofSystem + computed) } else { final String ideScope = forceTest ? "TEST" : ideScope(node.getNodeInfo().getDeclaredScopes()); final JkDependencyNode.JkFileNodeInfo fileNodeInfo = (JkDependencyNode.JkFileNodeInfo) node.getNodeInfo(); if (fileNodeInfo.isComputed()) { final Path projectDir = fileNodeInfo.computationOrigin().getIdeProjectDir(); if (projectDir != null && !allModules.contains(projectDir)) { writeOrderEntryForModule(projectDir.getFileName().toString(), ideScope); allModules.add(projectDir); } } else { writeFileEntries(fileNodeInfo.getFiles(), processedLibEntries, ideScope); } } } } private void writeFileEntries(Iterable<Path> files, Set<String> paths, String ideScope) throws XMLStreamException { for (final Path file : files) { final LibPath libPath = new LibPath(); libPath.bin = file; libPath.scope = ideScope; libPath.source = lookForSources(file); libPath.javadoc = lookForJavadoc(file); writeOrderEntryForLib(libPath); paths.add(file.toString()); } } private List<LibPath> toLibPath(JkDependencyNode.JkModuleNodeInfo moduleInfo, JkRepoSet repos, String scope) { final List<LibPath> result = new LinkedList<>(); final JkModuleId moduleId = moduleInfo.getModuleId(); final JkVersion version = moduleInfo.getResolvedVersion(); final JkVersionedModule versionedModule = JkVersionedModule.of(moduleId, version); final List<Path> files = moduleInfo.getFiles(); for (final Path file : files) { final LibPath libPath = new LibPath(); libPath.bin = file; libPath.scope = scope; libPath.source = repos.get(JkModuleDependency.of(versionedModule).withClassifier("sources")); libPath.javadoc = repos.get(JkModuleDependency.of(versionedModule).withClassifier("javadoc")); result.add(libPath); } return result; } private static Set<String> toStringScopes(Set<JkScope> scopes) { final Set<String> result = new HashSet<>(); for (final JkScope scope : scopes) { result.add(scope.getName()); } return result; } private static String ideScope(Set<JkScope> scopesArg) { final Set<String> scopes = toStringScopes(scopesArg); if (scopes.contains(JkScope.COMPILE.getName())) { return "COMPILE"; } if (scopes.contains(JkScope.PROVIDED.getName())) { return "PROVIDED"; } if (scopes.contains(JkScope.RUNTIME.getName())) { return "RUNTIME"; } if (scopes.contains(JkScope.TEST.getName())) { return "TEST"; } return "COMPILE"; } private void writeJdk() throws XMLStreamException { writer.writeCharacters(T2); writer.writeEmptyElement("orderEntry"); if (this.forceJdkVersion && ideSupport.getSourceVersion() != null) { writer.writeAttribute("type", "jdk"); final String jdkVersion = jdkVersion(this.ideSupport.getSourceVersion()); writer.writeAttribute("jdkName", jdkVersion); writer.writeAttribute("jdkType", "JavaSDK"); } else { writer.writeAttribute("type", "inheritedJdk"); } writer.writeCharacters("\n"); } private void writeOrderEntrySourceFolder() throws XMLStreamException { writer.writeCharacters(T2); writer.writeEmptyElement("orderEntry"); writer.writeAttribute("type", "sourceFolder"); writer.writeAttribute("forTests", "false"); writer.writeCharacters("\n"); } private void writeOrderEntryForLib(LibPath libPath) throws XMLStreamException { writer.writeCharacters(T2); writer.writeStartElement("orderEntry"); writer.writeAttribute("type", "module-library"); if (libPath.scope != null) { writer.writeAttribute("scope", libPath.scope); } writer.writeAttribute("exported", ""); writer.writeCharacters("\n"); writer.writeCharacters(T3); writer.writeStartElement("library"); writer.writeCharacters("\n"); writeLibType("CLASSES", libPath.bin); writer.writeCharacters("\n"); writeLibType("JAVADOC", libPath.javadoc); writer.writeCharacters("\n"); writeLibType("SOURCES", libPath.source); writer.writeCharacters("\n" + T3); writer.writeEndElement(); writer.writeCharacters("\n" + T2); writer.writeEndElement(); writer.writeCharacters("\n"); } private void writeOrderEntryForModule(String ideaModuleName, String scope) throws XMLStreamException { if (processedModueEntries.contains(ideaModuleName)) { return; } processedModueEntries.add(ideaModuleName); writer.writeCharacters(T2); writer.writeEmptyElement("orderEntry"); writer.writeAttribute("type", "module"); if (scope != null) { writer.writeAttribute("scope", scope); } writer.writeAttribute("module-name", ideaModuleName); writer.writeAttribute("exported", ""); writer.writeCharacters("\n"); } private void writeLibType(String type, Path file) throws XMLStreamException { writer.writeCharacters(T4); if (file != null) { writer.writeStartElement(type); writer.writeCharacters("\n"); writer.writeCharacters(T5); writer.writeEmptyElement("root"); writer.writeAttribute("url", ideaPath(ideSupport.getProdLayout().getBaseDir(), file)); writer.writeCharacters("\n" + T4); writer.writeEndElement(); } else { writer.writeEmptyElement(type); } } private String ideaPath(Path projectDir, Path file) { boolean jarFile = file.getFileName().toString().toLowerCase().endsWith(".jar"); String type = jarFile ? "jar" : "file"; Path basePath = projectDir; String varName = "MODULE_DIR"; if (useVarPath && file.toAbsolutePath().startsWith(JkLocator.getJekaUserHomeDir())) { basePath = JkLocator.getJekaUserHomeDir(); varName = "JEKA_USER_HOME"; } else if (useVarPath && file.toAbsolutePath().startsWith(JkLocator.getJekaHomeDir())) { basePath = JkLocator.getJekaHomeDir(); varName = "JEKA_HOME"; } String result; if (file.startsWith(basePath)) { final String relPath = basePath.relativize(file).normalize().toString(); result = type + "://$" + varName + "$/" + replacePathWithVar(relPath).replace('\\', '/'); } else { if (file.isAbsolute()) { result = type + "://" + file.normalize().toString().replace('\\', '/'); } else { result = type + "://$MODULE_DIR$/" + file.normalize().toString().replace('\\', '/'); } } if (jarFile) { result = result + "!/"; } return result; } private static String jdkVersion(JkJavaVersion javaVersion) { if (JkJavaVersion.V1_4.equals(javaVersion)) { return "1.4"; } if (JkJavaVersion.V5.equals(javaVersion)) { return "1.5"; } if (JkJavaVersion.V6.equals(javaVersion)) { return "1.6"; } if (JkJavaVersion.V7.equals(javaVersion)) { return "1.7"; } if (JkJavaVersion.V8.equals(javaVersion)) { return "1.8"; } return javaVersion.get(); } private static class LibPath { Path bin; Path source; Path javadoc; String scope; @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } final LibPath libPath = (LibPath) o; return bin.equals(libPath.bin); } @Override public int hashCode() { return bin.hashCode(); } } private String replacePathWithVar(String path) { if (!useVarPath) { return path; } final String userHome = JkLocator.getJekaUserHomeDir().toAbsolutePath().normalize().toString().replace('\\', '/'); final String home = JkLocator.getJekaHomeDir().toAbsolutePath().normalize().toString().replace('\\', '/'); final String result = path.replace(userHome, "$JEKA_USER_HOME$"); if (!result.equals(path)) { return result; } return path.replace(home, "$JEKA_HOME$"); } private static XMLStreamWriter createWriter(ByteArrayOutputStream fos) { try { return XMLOutputFactory.newInstance().createXMLStreamWriter(fos, ENCODING); } catch (final XMLStreamException e) { throw JkUtilsThrowable.unchecked(e); } } private Path lookForSources(Path binary) { final String name = binary.getFileName().toString(); final String nameWithoutExt = JkUtilsString.substringBeforeLast(name, "."); final String ext = JkUtilsString.substringAfterLast(name, "."); final String sourceName = nameWithoutExt + "-sources." + ext; final List<Path> folders = JkUtilsIterable.listOf( binary.resolve(".."), binary.resolve("../../../libs-sources"), binary.resolve("../../libs-sources"), binary.resolve("../libs-sources")); final List<String> names = JkUtilsIterable.listOf(sourceName, nameWithoutExt + "-sources.zip"); return lookFileHere(folders, names); } private Path lookForJavadoc(Path binary) { final String name = binary.getFileName().toString(); final String nameWithoutExt = JkUtilsString.substringBeforeLast(name, "."); final String ext = JkUtilsString.substringAfterLast(name, "."); final String sourceName = nameWithoutExt + "-javadoc." + ext; final List<Path> folders = JkUtilsIterable.listOf( binary.resolve(".."), binary.resolve("../../../libs-javadoc"), binary.resolve("../../libs-javadoc"), binary.resolve("../libs-javadoc")); final List<String> names = JkUtilsIterable.listOf(sourceName, nameWithoutExt + "-javadoc.zip"); return lookFileHere(folders, names); } private Path lookFileHere(Iterable<Path> folders, Iterable<String> names) { for (final Path folder : folders) { for (final String name : names) { final Path candidate = folder.resolve(name).normalize(); if (Files.exists(candidate)) { return candidate; } } } return null; } // --------------------------- setters ------------------------------------------------ public JkImlGenerator setImportedTestModules(Iterable<String> importedTestModules) { this.importedTestModules = importedTestModules; return this; } public JkImlGenerator setForceJdkVersion(boolean forceJdkVersion) { this.forceJdkVersion = forceJdkVersion; return this; } public JkImlGenerator setUseVarPath(boolean useVarPath) { this.useVarPath = useVarPath; return this; } public JkImlGenerator setFailOnDepsResolutionError(boolean fail) { this.failOnDepsResolutionError = fail; return this; } public JkImlGenerator setDefDependencyResolver(JkDependencyResolver defDependencyResolver) { this.defDependencyResolver = defDependencyResolver; return this; } public JkImlGenerator setDefDependencies(JkDependencySet defDependencies) { this.defDependencies = defDependencies; return this; } public JkImlGenerator setWriter(XMLStreamWriter writer) { this.writer = writer; return this; } private Path findPluginXml() { List<Path> candidates = ideSupport.getProdLayout().resolveResources().getExistingFiles("META-INF/plugin.xml"); if (candidates.isEmpty()) { return null; } return candidates.stream().filter(JkImlGenerator::isPlateformPlugin).findFirst().orElse(null); } private static boolean isPlateformPlugin(Path pluginXmlFile) { try { Document doc = JkUtilsXml.documentFrom(pluginXmlFile); Element root = JkUtilsXml.directChild(doc, "idea-plugin"); return root != null; } catch (RuntimeException e) { return false; } } }