[workspace model] provide implementation of FacetManager which stores data in workspa...
[idea/community.git] / platform / workspaceModel-ide / src / com / intellij / workspace / jps / JpsProjectModelSynchronizer.kt
1 package com.intellij.workspace.jps
2
3 import com.intellij.configurationStore.*
4 import com.intellij.openapi.Disposable
5 import com.intellij.openapi.application.ApplicationManager
6 import com.intellij.openapi.application.WriteAction
7 import com.intellij.openapi.application.runWriteAction
8 import com.intellij.openapi.components.StateSplitterEx
9 import com.intellij.openapi.components.impl.stores.FileStorageCoreUtil
10 import com.intellij.openapi.components.impl.stores.IProjectStore
11 import com.intellij.openapi.project.Project
12 import com.intellij.openapi.project.impl.ProjectLifecycleListener
13 import com.intellij.openapi.util.Pair
14 import com.intellij.openapi.util.io.FileUtil
15 import com.intellij.openapi.util.registry.Registry
16 import com.intellij.openapi.vfs.VirtualFileManager
17 import com.intellij.openapi.vfs.newvfs.BulkFileListener
18 import com.intellij.openapi.vfs.newvfs.events.VFileContentChangeEvent
19 import com.intellij.openapi.vfs.newvfs.events.VFileCreateEvent
20 import com.intellij.openapi.vfs.newvfs.events.VFileDeleteEvent
21 import com.intellij.openapi.vfs.newvfs.events.VFileEvent
22 import com.intellij.project.stateStore
23 import com.intellij.util.PathUtil
24 import com.intellij.workspace.api.EntityChange
25 import com.intellij.workspace.api.EntitySource
26 import com.intellij.workspace.api.EntityStoreChanged
27 import com.intellij.workspace.api.TypedEntityStorageBuilder
28 import com.intellij.workspace.ide.*
29 import org.jdom.Element
30 import org.jetbrains.jps.util.JpsPathUtil
31 import java.util.*
32 import java.util.concurrent.atomic.AtomicReference
33 import kotlin.collections.ArrayList
34 import kotlin.collections.LinkedHashSet
35
36 class JpsProjectModelSynchronizer(private val project: Project) : Disposable {
37   private val incomingChanges = Collections.synchronizedList(ArrayList<JpsConfigurationFilesChange>())
38   private lateinit var fileContentReader: StorageJpsConfigurationReader
39   private val serializationData = AtomicReference<JpsEntitiesSerializationData?>()
40   private val sourcesToSave = Collections.synchronizedSet(HashSet<EntitySource>())
41
42   init {
43     if (!project.isDefault && enabled) {
44       project.messageBus.connect(this).subscribe(ProjectLifecycleListener.TOPIC, object : ProjectLifecycleListener {
45         override fun projectComponentsInitialized(project: Project) {
46           if (project === this@JpsProjectModelSynchronizer.project) {
47             loadInitialProject(project.storagePlace!!)
48           }
49         }
50       })
51     }
52   }
53
54   internal fun needToReloadProjectEntities(): Boolean {
55     if (!enabled) return false
56     if (StoreReloadManager.getInstance().isReloadBlocked()) return false
57     if (serializationData.get() == null) return false
58
59     synchronized(incomingChanges) {
60       return incomingChanges.isNotEmpty()
61     }
62   }
63
64   internal fun reloadProjectEntities() {
65     if (!enabled) return
66
67     if (StoreReloadManager.getInstance().isReloadBlocked()) return
68     val data = serializationData.get() ?: return
69     val changes = getAndResetIncomingChanges() ?: return
70
71     val (changedEntities, builder) = data.reloadFromChangedFiles(changes, fileContentReader)
72     if (changedEntities.isEmpty() && builder.isEmpty()) return
73
74     ApplicationManager.getApplication().invokeAndWait(Runnable {
75       runWriteAction {
76         WorkspaceModel.getInstance(project).updateProjectModel { updater ->
77           updater.replaceBySource({ it in changedEntities }, builder.toStorage())
78         }
79         sourcesToSave.removeAll(changedEntities)
80       }
81     })
82   }
83
84   private fun registerListener() {
85     ApplicationManager.getApplication().messageBus.connect(this).subscribe(VirtualFileManager.VFS_CHANGES, object : BulkFileListener {
86       override fun after(events: MutableList<out VFileEvent>) {
87         //todo support move/rename
88         //todo optimize: filter events before creating lists
89         val toProcess = events.asSequence().filter { isFireStorageFileChangedEvent(it) }
90         val addedUrls = toProcess.filterIsInstance<VFileCreateEvent>().mapTo(ArrayList()) { JpsPathUtil.pathToUrl(it.path) }
91         val removedUrls = toProcess.filterIsInstance<VFileDeleteEvent>().mapTo(ArrayList()) { JpsPathUtil.pathToUrl(it.path) }
92         val changedUrls = toProcess.filterIsInstance<VFileContentChangeEvent>().mapTo(ArrayList()) { JpsPathUtil.pathToUrl(it.path) }
93         if (addedUrls.isNotEmpty() || removedUrls.isNotEmpty() || changedUrls.isNotEmpty()) {
94           val change = JpsConfigurationFilesChange(addedUrls, removedUrls, changedUrls)
95           incomingChanges.add(change)
96
97           StoreReloadManager.getInstance().scheduleProcessingChangedFiles()
98         }
99       }
100     })
101     project.messageBus.connect().subscribe(WorkspaceModelTopics.CHANGED, object : WorkspaceModelChangeListener {
102       override fun changed(event: EntityStoreChanged) {
103         event.getAllChanges().forEach {
104           when (it) {
105             is EntityChange.Added -> sourcesToSave.add(it.entity.entitySource)
106             is EntityChange.Removed -> sourcesToSave.add(it.entity.entitySource)
107             is EntityChange.Replaced -> {
108               sourcesToSave.add(it.oldEntity.entitySource)
109               sourcesToSave.add(it.newEntity.entitySource)
110             }
111           }
112         }
113       }
114     })
115   }
116
117   internal fun loadInitialProject(storagePlace: JpsProjectStoragePlace) {
118     val baseDirUrl = storagePlace.baseDirectoryUrl
119     fileContentReader = StorageJpsConfigurationReader(project, baseDirUrl)
120     val serializationData = JpsProjectEntitiesLoader.createProjectSerializers(storagePlace, fileContentReader, false, true)
121     this.serializationData.set(serializationData)
122     registerListener()
123     val builder = TypedEntityStorageBuilder.create()
124     serializationData.loadAll(fileContentReader, builder)
125     WriteAction.runAndWait<RuntimeException> {
126       WorkspaceModel.getInstance(project).updateProjectModel { updater ->
127         updater.replaceBySource({ it is JpsFileEntitySource }, builder.toStorage())
128       }
129     }
130   }
131
132   internal fun saveChangedProjectEntities(writer: JpsFileContentWriter) {
133     if (!enabled) return
134
135     val data = serializationData.get() ?: return
136     val storage = WorkspaceModel.getInstance(project).entityStore.current
137     val affectedSources = synchronized(sourcesToSave) {
138       val copy = HashSet(sourcesToSave)
139       sourcesToSave.clear()
140       copy
141     }
142     data.saveEntities(storage, affectedSources, writer)
143   }
144
145   private fun getAndResetIncomingChanges(): JpsConfigurationFilesChange? {
146     synchronized(incomingChanges) {
147       if (incomingChanges.isEmpty()) return null
148       val combinedChanges = combineChanges()
149       incomingChanges.clear()
150       return combinedChanges
151     }
152   }
153
154   private fun combineChanges(): JpsConfigurationFilesChange {
155     val singleChange = incomingChanges.singleOrNull()
156     if (singleChange != null) {
157       return singleChange
158     }
159     val allAdded = LinkedHashSet<String>()
160     val allRemoved = LinkedHashSet<String>()
161     val allChanged = LinkedHashSet<String>()
162     for (change in incomingChanges) {
163       allChanged.addAll(change.changedFileUrls)
164       for (addedUrl in change.addedFileUrls) {
165         if (allRemoved.remove(addedUrl)) {
166           allChanged.add(addedUrl)
167         }
168         else {
169           allAdded.add(addedUrl)
170         }
171       }
172       for (removedUrl in change.removedFileUrls) {
173         allChanged.remove(removedUrl)
174         if (!allAdded.remove(removedUrl)) {
175           allRemoved.add(removedUrl)
176         }
177       }
178     }
179     return JpsConfigurationFilesChange(allAdded, allRemoved, allChanged)
180   }
181
182   override fun dispose() {
183   }
184
185   companion object {
186     fun getInstance(project: Project): JpsProjectModelSynchronizer? = project.getComponent(JpsProjectModelSynchronizer::class.java)
187
188     var enabled = Registry.`is`("ide.workspace.model.jps.enabled")
189   }
190 }
191
192 private class StorageJpsConfigurationReader(private val project: Project,
193                                             private val baseDirUrl: String) : JpsFileContentReader {
194   override fun loadComponent(fileUrl: String, componentName: String): Element? {
195     val filePath = JpsPathUtil.urlToPath(fileUrl)
196     if (FileUtil.extensionEquals(filePath, "iml")) {
197       //todo fetch data from ModuleStore
198       return CachingJpsFileContentReader(baseDirUrl).loadComponent(fileUrl, componentName)
199     }
200     else {
201       val storage = getProjectStateStorage(filePath, project.stateStore)
202       val stateMap = storage.getStorageData()
203       return if (storage is DirectoryBasedStorageBase) {
204         val elementContent = stateMap.getElement(PathUtil.getFileName(filePath))
205         if (elementContent != null) {
206           Element(FileStorageCoreUtil.COMPONENT).setAttribute(FileStorageCoreUtil.NAME, componentName).addContent(elementContent)
207         }
208         else {
209           null
210         }
211       }
212       else {
213         stateMap.getElement(componentName)
214       }
215     }
216   }
217 }
218
219 internal fun getProjectStateStorage(filePath: String, store: IProjectStore): StateStorageBase<StateMap> {
220   val collapsedPath: String
221   val splitterClass: Class<out StateSplitterEx>
222   if (FileUtil.extensionEquals(filePath, "ipr")) {
223     collapsedPath = "\$PROJECT_FILE$"
224     splitterClass = StateSplitterEx::class.java
225   }
226   else {
227     val fileName = PathUtil.getFileName(filePath)
228     val parentPath = PathUtil.getParentPath(filePath)
229     if (PathUtil.getFileName(parentPath) == Project.DIRECTORY_STORE_FOLDER) {
230       collapsedPath = fileName
231       splitterClass = StateSplitterEx::class.java
232     }
233     else {
234       val grandParentPath = PathUtil.getParentPath(parentPath)
235       if (PathUtil.getFileName(grandParentPath) != Project.DIRECTORY_STORE_FOLDER) error("$filePath is not under .idea directory")
236       collapsedPath = PathUtil.getFileName(parentPath)
237       splitterClass = FakeDirectoryBasedStateSplitter::class.java
238     }
239   }
240   val storageSpec = FileStorageAnnotation(collapsedPath, false, splitterClass)
241   @Suppress("UNCHECKED_CAST")
242   return store.storageManager.getStateStorage(storageSpec) as StateStorageBase<StateMap>
243 }
244
245 /**
246  * This fake implementation is used to force creating directory based storage in StateStorageManagerImpl.createStateStorage
247  */
248 private class FakeDirectoryBasedStateSplitter : StateSplitterEx() {
249   override fun splitState(state: Element): MutableList<Pair<Element, String>> {
250     throw AssertionError()
251   }
252 }
253
254 internal class LegacyBridgeStoreReloadManager : StoreReloadManagerImpl() {
255   override fun mayHaveAdditionalConfigurations(project: Project): Boolean {
256     return JpsProjectModelSynchronizer.getInstance(project)?.needToReloadProjectEntities() ?: false
257   }
258
259   override fun reloadAdditionalConfigurations(project: Project) {
260     JpsProjectModelSynchronizer.getInstance(project)?.reloadProjectEntities()
261   }
262 }