3cf590b9b88a93ab8bf62dbe27923a77d15aa10a
[idea/community.git] / platform / workspaceModel-ide / src / com / intellij / workspace / jps / ModuleImlFileEntitiesSerializer.kt
1 // Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.workspace.jps
3
4 import com.intellij.ide.plugins.PluginManagerCore
5 import com.intellij.openapi.extensions.PluginId
6 import com.intellij.openapi.module.impl.ModuleManagerImpl
7 import com.intellij.openapi.module.impl.ModulePath
8 import com.intellij.openapi.projectRoots.ProjectJdkTable
9 import com.intellij.openapi.util.JDOMUtil
10 import com.intellij.openapi.vfs.VfsUtil
11 import com.intellij.util.isEmpty
12 import com.intellij.workspace.api.*
13 import com.intellij.workspace.ide.JpsFileEntitySource
14 import com.intellij.workspace.legacyBridge.intellij.toLibraryTableId
15 import com.intellij.workspace.legacyBridge.libraries.libraries.LegacyBridgeLibraryImpl
16 import org.jdom.Element
17 import org.jetbrains.jps.model.serialization.JDomSerializationUtil
18 import org.jetbrains.jps.model.serialization.java.JpsJavaModelSerializerExtension.*
19 import org.jetbrains.jps.model.serialization.module.JpsModuleRootModelSerializer.*
20 import org.jetbrains.jps.util.JpsPathUtil
21 import java.io.File
22 import java.io.StringReader
23
24 private const val MODULE_ROOT_MANAGER_COMPONENT_NAME = "NewModuleRootManager"
25 private const val URL_ATTRIBUTE = "url"
26
27 internal class ModuleImlFileEntitiesSerializer(private val modulePath: ModulePath,
28                                                override val fileUrl: VirtualFileUrl,
29                                                override val entitySource: JpsFileEntitySource,
30                                                private val serializeFacets: Boolean) : JpsFileEntitiesSerializer<ModuleEntity> {
31   override val mainEntityClass: Class<ModuleEntity>
32     get() = ModuleEntity::class.java
33
34   override fun equals(other: Any?) = (other as? ModuleImlFileEntitiesSerializer)?.modulePath == modulePath
35
36   override fun hashCode() = modulePath.hashCode()
37
38   override fun loadEntities(builder: TypedEntityStorageBuilder,
39                             reader: JpsFileContentReader) {
40     val moduleName = modulePath.moduleName
41     val source = entitySource
42     val moduleEntity = builder.addModuleEntity(moduleName, listOf(ModuleDependencyItem.ModuleSourceDependency), source)
43
44     val rootManagerElement = reader.loadComponent(fileUrl.url, MODULE_ROOT_MANAGER_COMPONENT_NAME)?.clone()
45     if (rootManagerElement == null) {
46       return
47     }
48
49     for (contentElement in rootManagerElement.getChildrenAndDetach(CONTENT_TAG)) {
50       for (sourceRootElement in contentElement.getChildren(SOURCE_FOLDER_TAG)) {
51         val url = sourceRootElement.getAttributeValueStrict(URL_ATTRIBUTE)
52         val isTestSource = sourceRootElement.getAttributeValue(IS_TEST_SOURCE_ATTRIBUTE)?.toBoolean() == true
53         val type = sourceRootElement.getAttributeValue(SOURCE_ROOT_TYPE_ATTRIBUTE) ?: (if (isTestSource) JAVA_TEST_ROOT_TYPE_ID else JAVA_SOURCE_ROOT_TYPE_ID)
54         val virtualFileUrl = VirtualFileUrlManager.fromUrl(url)
55         val sourceRoot = builder.addSourceRootEntity(moduleEntity, virtualFileUrl,
56                                                      type == JAVA_TEST_ROOT_TYPE_ID || type == JAVA_TEST_RESOURCE_ROOT_ID,
57                                                      type, source)
58         if (type == JAVA_SOURCE_ROOT_TYPE_ID || type == JAVA_TEST_ROOT_TYPE_ID) {
59           builder.addJavaSourceRootEntity(sourceRoot, sourceRootElement.getAttributeValue(IS_GENERATED_ATTRIBUTE)?.toBoolean() ?: false,
60                                           sourceRootElement.getAttributeValue(PACKAGE_PREFIX_ATTRIBUTE) ?: "", source)
61         }
62         else if (type == JAVA_RESOURCE_ROOT_ID || type == JAVA_TEST_RESOURCE_ROOT_ID) {
63           builder.addJavaResourceRootEntity(sourceRoot, sourceRootElement.getAttributeValue(IS_GENERATED_ATTRIBUTE)?.toBoolean() ?: false,
64                                             sourceRootElement.getAttributeValue(RELATIVE_OUTPUT_PATH_ATTRIBUTE) ?: "", source)
65         }
66         else {
67           val elem = sourceRootElement.clone()
68           elem.removeAttribute(URL_ATTRIBUTE)
69           elem.removeAttribute(SOURCE_ROOT_TYPE_ATTRIBUTE)
70           builder.addCustomSourceRootPropertiesEntity(sourceRoot, JDOMUtil.write(elem), source)
71         }
72       }
73       val excludeRootsUrls = contentElement.getChildren(EXCLUDE_FOLDER_TAG)
74         .map { it.getAttributeValueStrict(URL_ATTRIBUTE) }
75         .map { VirtualFileUrlManager.fromUrl(it) }
76       val excludePatterns = contentElement.getChildren(EXCLUDE_PATTERN_TAG)
77         .map { it.getAttributeValue(EXCLUDE_PATTERN_ATTRIBUTE) }
78       val contentRootUrl = contentElement
79         .getAttributeValueStrict(URL_ATTRIBUTE)
80         .let { VirtualFileUrlManager.fromUrl(it) }
81       builder.addContentRootEntity(contentRootUrl, excludeRootsUrls, excludePatterns, moduleEntity, source)
82     }
83     fun Element.readScope(): ModuleDependencyItem.DependencyScope {
84       val attributeValue = getAttributeValue(SCOPE_ATTRIBUTE)
85                            ?: return ModuleDependencyItem.DependencyScope.COMPILE
86       return try {
87         ModuleDependencyItem.DependencyScope.valueOf(attributeValue)
88       }
89       catch (e: IllegalArgumentException) {
90         ModuleDependencyItem.DependencyScope.COMPILE
91       }
92     }
93
94     fun Element.isExported() = getAttributeValue(EXPORTED_ATTRIBUTE) != null
95     val moduleLibraryNames = mutableListOf<String>()
96     val dependencyItems = rootManagerElement.getChildrenAndDetach(ORDER_ENTRY_TAG).mapTo(ArrayList()) { dependencyElement ->
97       when (dependencyElement.getAttributeValue(TYPE_ATTRIBUTE)) {
98         SOURCE_FOLDER_TYPE -> ModuleDependencyItem.ModuleSourceDependency
99         JDK_TYPE -> ModuleDependencyItem.SdkDependency(dependencyElement.getAttributeValueStrict(JDK_NAME_ATTRIBUTE),
100                                                        dependencyElement.getAttributeValue(JDK_TYPE_ATTRIBUTE))
101         INHERITED_JDK_TYPE -> ModuleDependencyItem.InheritedSdkDependency
102         LIBRARY_TYPE -> {
103           val level = dependencyElement.getAttributeValueStrict(LEVEL_ATTRIBUTE)
104           val parentId = toLibraryTableId(level)
105           val libraryId = LibraryId(dependencyElement.getAttributeValueStrict(NAME_ATTRIBUTE), parentId)
106           ModuleDependencyItem.Exportable.LibraryDependency(libraryId, dependencyElement.isExported(), dependencyElement.readScope())
107         }
108         MODULE_LIBRARY_TYPE -> {
109           val libraryElement = dependencyElement.getChild(LIBRARY_TAG)!!
110           // TODO. Probably we want a fixed name based on hashed library roots
111           val nameAttributeValue = libraryElement.getAttributeValue(NAME_ATTRIBUTE)
112           val name = LegacyBridgeLibraryImpl.generateLibraryEntityName(nameAttributeValue) { nameToCheck -> moduleLibraryNames.contains(nameToCheck) }
113           moduleLibraryNames.add(name)
114           val tableId = LibraryTableId.ModuleLibraryTableId(ModuleId(moduleName))
115           loadLibrary(name, libraryElement, tableId, builder, source)
116           val libraryId = LibraryId(name, tableId)
117           ModuleDependencyItem.Exportable.LibraryDependency(libraryId, dependencyElement.isExported(), dependencyElement.readScope())
118         }
119         MODULE_TYPE -> {
120           val depModuleName = dependencyElement.getAttributeValueStrict(MODULE_NAME_ATTRIBUTE)
121           ModuleDependencyItem.Exportable.ModuleDependency(ModuleId(depModuleName), dependencyElement.isExported(),
122                                                            dependencyElement.readScope(),
123                                                            dependencyElement.getAttributeValue("production-on-test") != null)
124         }
125         else -> error(dependencyElement.name)
126       }
127     }
128
129     if (dependencyItems.none { it is ModuleDependencyItem.ModuleSourceDependency }) {
130       dependencyItems.add(ModuleDependencyItem.ModuleSourceDependency)
131     }
132
133     val inheritedCompilerOutput = rootManagerElement.getAttributeAndDetach(INHERIT_COMPILER_OUTPUT_ATTRIBUTE)
134     val excludeOutput = rootManagerElement.getChildAndDetach(EXCLUDE_OUTPUT_TAG) != null
135     val compilerOutput = rootManagerElement.getChildAndDetach(OUTPUT_TAG)?.getAttributeValue(URL_ATTRIBUTE)
136     val compilerOutputForTests = rootManagerElement.getChildAndDetach(TEST_OUTPUT_TAG)?.getAttributeValue(URL_ATTRIBUTE)
137
138     builder.addJavaModuleSettingsEntity(
139       inheritedCompilerOutput = inheritedCompilerOutput?.toBoolean() ?: false,
140       excludeOutput = excludeOutput,
141       compilerOutput = compilerOutput?.let { VirtualFileUrlManager.fromUrl(it) },
142       compilerOutputForTests = compilerOutputForTests?.let { VirtualFileUrlManager.fromUrl(it) },
143       module = moduleEntity,
144       source = source
145     )
146     if (!rootManagerElement.isEmpty()) {
147       builder.addModuleCustomImlDataEntity(
148         rootManagerTagCustomData = JDOMUtil.write(rootManagerElement),
149         module = moduleEntity,
150         source = source
151       )
152     }
153     builder.modifyEntity(ModifiableModuleEntity::class.java, moduleEntity) {
154       dependencies = dependencyItems
155     }
156
157     if (serializeFacets) {
158       FacetEntitiesSerializer(fileUrl, source).loadFacetEntities(builder, moduleEntity, reader)
159     }
160   }
161
162   private fun Element.getChildrenAndDetach(cname: String): List<Element> {
163     val result = getChildren(cname).toList()
164     result.forEach { it.detach() }
165     return result
166   }
167
168   private fun Element.getAttributeAndDetach(name: String): String? {
169     val result = getAttributeValue(name)
170     removeAttribute(name)
171     return result
172   }
173
174   private fun Element.getChildAndDetach(cname: String): Element? =
175     getChild(cname)?.also { it.detach() }
176
177   override fun saveEntities(mainEntities: Collection<ModuleEntity>,
178                             entities: Map<Class<out TypedEntity>, List<TypedEntity>>,
179                             writer: JpsFileContentWriter): List<TypedEntity> {
180     val module = mainEntities.single()
181     val savedEntities = ArrayList<TypedEntity>()
182     savedEntities.add(module)
183     val rootManagerElement = JDomSerializationUtil.createComponentElement(MODULE_ROOT_MANAGER_COMPONENT_NAME)
184
185     saveJavaSettings(module.javaSettings, rootManagerElement, savedEntities)
186
187     val customImlData = module.customImlData
188     if (customImlData != null) {
189       savedEntities.add(customImlData)
190       val element = JDOMUtil.load(StringReader(customImlData.rootManagerTagCustomData))
191       JDOMUtil.merge(rootManagerElement, element)
192     }
193     //todo ensure that custom data is written in proper order
194
195     val contentEntities = module.contentRoots.filter { it.entitySource == module.entitySource }.sortedBy { it.url.url }
196     val contentUrlToSourceRoots = module.sourceRoots.groupByTo(HashMap()) { sourceRoot ->
197       contentEntities.find { VfsUtil.isEqualOrAncestor(it.url.url, sourceRoot.url.url) }?.url?.url ?: sourceRoot.url.url
198     }
199
200     contentEntities.forEach { contentEntry ->
201       savedEntities.add(contentEntry)
202       val contentRootTag = Element(CONTENT_TAG)
203       contentRootTag.setAttribute(URL_ATTRIBUTE, contentEntry.url.url)
204       val sourceRoots = contentUrlToSourceRoots[contentEntry.url.url]
205       sourceRoots?.forEach {
206         contentRootTag.addContent(saveSourceRoot(it, savedEntities))
207       }
208       contentEntry.excludedUrls.forEach {
209         contentRootTag.addContent(Element(EXCLUDE_FOLDER_TAG).setAttribute(URL_ATTRIBUTE, it.url))
210       }
211       contentEntry.excludedPatterns.forEach {
212         contentRootTag.addContent(Element(EXCLUDE_PATTERN_TAG).setAttribute(EXCLUDE_PATTERN_ATTRIBUTE, it))
213       }
214       rootManagerElement.addContent(contentRootTag)
215     }
216
217     @Suppress("UNCHECKED_CAST")
218     val moduleLibraries = (entities[LibraryEntity::class.java] as List<LibraryEntity>? ?: emptyList()).associateBy { it.name }
219     module.dependencies.forEach {
220       rootManagerElement.addContent(saveDependencyItem(it, moduleLibraries, savedEntities))
221     }
222
223     writer.saveComponent(fileUrl.url, MODULE_ROOT_MANAGER_COMPONENT_NAME, rootManagerElement)
224
225     if (serializeFacets) {
226       @Suppress("UNCHECKED_CAST")
227       val facets = entities[FacetEntity::class.java] as List<FacetEntity>? ?: emptyList()
228       if (facets.isNotEmpty()) {
229         FacetEntitiesSerializer(fileUrl, entitySource).saveFacetEntities(module, facets, writer)
230       }
231     }
232     return savedEntities
233   }
234
235   private fun javaPluginPresent() = PluginManagerCore.getPlugin(PluginId.findId("com.intellij.java")) != null
236
237   private fun saveJavaSettings(javaSettings: JavaModuleSettingsEntity?,
238                                rootManagerElement: Element,
239                                savedEntities: MutableList<TypedEntity>) {
240     if (javaSettings == null) {
241       if (javaPluginPresent()) {
242         rootManagerElement.setAttribute(INHERIT_COMPILER_OUTPUT_ATTRIBUTE, true.toString())
243         rootManagerElement.addContent(Element(EXCLUDE_OUTPUT_TAG))
244       }
245
246       return
247     }
248
249     savedEntities.add(javaSettings)
250     if (javaSettings.inheritedCompilerOutput) {
251       rootManagerElement.setAttribute(INHERIT_COMPILER_OUTPUT_ATTRIBUTE, true.toString())
252     }
253     else {
254       val outputUrl = javaSettings.compilerOutput?.url
255       if (outputUrl != null) {
256         rootManagerElement.addContent(Element(OUTPUT_TAG).setAttribute(URL_ATTRIBUTE, outputUrl))
257       }
258       val testOutputUrl = javaSettings.compilerOutputForTests?.url
259       if (testOutputUrl != null) {
260         rootManagerElement.addContent(Element(TEST_OUTPUT_TAG).setAttribute(URL_ATTRIBUTE, testOutputUrl))
261       }
262     }
263     if (javaSettings.excludeOutput) {
264       rootManagerElement.addContent(Element(EXCLUDE_OUTPUT_TAG))
265     }
266   }
267
268   private fun saveDependencyItem(dependencyItem: ModuleDependencyItem, moduleLibraries: Map<String, LibraryEntity>,
269                                  savedEntities: MutableList<TypedEntity>)
270     = when (dependencyItem) {
271     is ModuleDependencyItem.ModuleSourceDependency -> createOrderEntryTag(SOURCE_FOLDER_TYPE).setAttribute("forTests", "false")
272     is ModuleDependencyItem.SdkDependency -> createOrderEntryTag(JDK_TYPE).apply {
273       setAttribute(JDK_NAME_ATTRIBUTE, dependencyItem.sdkName)
274
275       val sdkType = dependencyItem.sdkType
276       if (sdkType == null) {
277         val jdk = ProjectJdkTable.getInstance().findJdk(dependencyItem.sdkName)
278         val sdkTypeName = jdk?.sdkType?.name
279         sdkTypeName?.let { setAttribute(JDK_TYPE_ATTRIBUTE, it) }
280       } else {
281         setAttribute(JDK_TYPE_ATTRIBUTE, sdkType)
282       }
283     }
284     is ModuleDependencyItem.InheritedSdkDependency -> createOrderEntryTag(INHERITED_JDK_TYPE)
285     is ModuleDependencyItem.Exportable.LibraryDependency -> {
286       val library = dependencyItem.library
287       if (library.tableId is LibraryTableId.ModuleLibraryTableId) {
288         createOrderEntryTag(MODULE_LIBRARY_TYPE).apply {
289           setExportedAndScopeAttributes(dependencyItem)
290           addContent(saveLibrary(moduleLibraries.getValue(library.name), savedEntities))
291         }
292       } else {
293         createOrderEntryTag(LIBRARY_TYPE).apply {
294           setExportedAndScopeAttributes(dependencyItem)
295           setAttribute(NAME_ATTRIBUTE, library.name)
296           setAttribute(LEVEL_ATTRIBUTE, library.tableId.level)
297         }
298       }
299     }
300     is ModuleDependencyItem.Exportable.ModuleDependency -> createOrderEntryTag(MODULE_TYPE).apply {
301       setAttribute(MODULE_NAME_ATTRIBUTE, dependencyItem.module.name)
302       setExportedAndScopeAttributes(dependencyItem)
303       if (dependencyItem.productionOnTest) {
304         setAttribute("production-on-test", "")
305       }
306     }
307   }
308
309   private fun Element.setExportedAndScopeAttributes(item: ModuleDependencyItem.Exportable) {
310     if (item.exported) {
311       setAttribute(EXPORTED_ATTRIBUTE, "")
312     }
313     if (item.scope != ModuleDependencyItem.DependencyScope.COMPILE) {
314       setAttribute(SCOPE_ATTRIBUTE, item.scope.name)
315     }
316   }
317
318   private fun createOrderEntryTag(type: String) = Element(ORDER_ENTRY_TAG).setAttribute(TYPE_ATTRIBUTE, type)
319
320   private fun saveSourceRoot(sourceRoot: SourceRootEntity,
321                              savedEntities: MutableList<TypedEntity>): Element {
322     savedEntities.add(sourceRoot)
323     val sourceRootTag = Element(SOURCE_FOLDER_TAG)
324     sourceRootTag.setAttribute(URL_ATTRIBUTE, sourceRoot.url.url)
325     val rootType = sourceRoot.rootType
326     if (rootType !in listOf(JAVA_SOURCE_ROOT_TYPE_ID, JAVA_TEST_ROOT_TYPE_ID)) {
327       sourceRootTag.setAttribute(SOURCE_ROOT_TYPE_ATTRIBUTE, rootType)
328     }
329     val javaRootProperties = sourceRoot.asJavaSourceRoot()
330     if (javaRootProperties != null) {
331       savedEntities.add(javaRootProperties)
332       sourceRootTag.setAttribute(IS_TEST_SOURCE_ATTRIBUTE, sourceRoot.tests.toString())
333       val packagePrefix = javaRootProperties.packagePrefix
334       if (packagePrefix.isNotEmpty()) {
335         sourceRootTag.setAttribute(PACKAGE_PREFIX_ATTRIBUTE, packagePrefix)
336       }
337       if (javaRootProperties.generated) {
338         sourceRootTag.setAttribute(IS_GENERATED_ATTRIBUTE, true.toString())
339       }
340     }
341
342     val javaResourceRootProperties = sourceRoot.asJavaResourceRoot()
343     if (javaResourceRootProperties != null) {
344       savedEntities.add(javaResourceRootProperties)
345       val relativeOutputPath = javaResourceRootProperties.relativeOutputPath
346       if (relativeOutputPath.isNotEmpty()) {
347         sourceRootTag.setAttribute(RELATIVE_OUTPUT_PATH_ATTRIBUTE, relativeOutputPath)
348       }
349       if (javaResourceRootProperties.generated) {
350         sourceRootTag.setAttribute(IS_GENERATED_ATTRIBUTE, true.toString())
351       }
352     }
353     val customProperties = sourceRoot.asCustomSourceRoot()
354     if (customProperties != null) {
355       savedEntities.add(customProperties)
356       val element = JDOMUtil.load(StringReader(customProperties.propertiesXmlTag))
357       JDOMUtil.merge(sourceRootTag, element)
358     }
359     return sourceRootTag
360   }
361 }
362
363 private const val MODULE_MANAGER_COMPONENT_NAME = "ProjectModuleManager"
364
365 internal class ModuleSerializersFactory(override val fileUrl: String, private val serializeFacets: Boolean) : JpsFileSerializerFactory<ModuleEntity> {
366   override val entityClass: Class<ModuleEntity>
367     get() = ModuleEntity::class.java
368
369   override fun getFileName(entity: ModuleEntity): String {
370     return "${entity.name}.iml"
371   }
372
373   override fun createSerializer(source: JpsFileEntitySource, fileUrl: VirtualFileUrl): JpsFileEntitiesSerializer<ModuleEntity> {
374     return ModuleImlFileEntitiesSerializer(ModulePath(JpsPathUtil.urlToPath(fileUrl.filePath), null), fileUrl,  source, serializeFacets)
375   }
376
377   override fun loadFileList(reader: JpsFileContentReader): List<VirtualFileUrl> {
378     val moduleManagerTag = reader.loadComponent(fileUrl, MODULE_MANAGER_COMPONENT_NAME) ?: return emptyList()
379     return ModuleManagerImpl.getPathsToModuleFiles(moduleManagerTag).map {
380       //todo load module groups
381       File(it.path).toVirtualFileUrl()
382     }
383   }
384
385   override fun saveEntitiesList(entities: Sequence<ModuleEntity>, writer: JpsFileContentWriter) {
386     val componentTag = JDomSerializationUtil.createComponentElement(MODULE_MANAGER_COMPONENT_NAME)
387     val entitiesToSave = entities
388       .mapNotNullTo(ArrayList()) { module -> (module.entitySource as? JpsFileEntitySource.FileInDirectory)?.let { Pair(it, module) } }
389       .sortedBy { it.second.name }
390     if (entitiesToSave.isNotEmpty()) {
391       val modulesTag = Element("modules")
392       entitiesToSave
393         .forEach { (source, module) ->
394           val moduleTag = Element("module")
395           val fileUrl = getModuleFileUrl(source, module)
396           moduleTag.setAttribute("fileurl", fileUrl)
397           moduleTag.setAttribute("filepath", JpsPathUtil.urlToPath(fileUrl))
398           module.groupPath?.let {
399             moduleTag.setAttribute("group", it.path.joinToString("/"))
400           }
401           modulesTag.addContent(moduleTag)
402         }
403       componentTag.addContent(modulesTag)
404     }
405
406     writer.saveComponent(fileUrl, MODULE_MANAGER_COMPONENT_NAME, componentTag)
407   }
408
409   override fun deleteObsoleteFile(fileUrl: String, writer: JpsFileContentWriter) {
410     writer.saveComponent(fileUrl, MODULE_ROOT_MANAGER_COMPONENT_NAME, null)
411   }
412
413   private fun getModuleFileUrl(source: JpsFileEntitySource.FileInDirectory,
414                                module: ModuleEntity) = source.directory.url + "/" + module.name + ".iml"
415 }