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