IDEA-157763 Settings repository for IDE: unit tests
[idea/community.git] / platform / configuration-store-impl / src / StateStorageManagerImpl.kt
1 /*
2  * Copyright 2000-2015 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.configurationStore
17
18 import com.intellij.openapi.Disposable
19 import com.intellij.openapi.application.Application
20 import com.intellij.openapi.application.ApplicationManager
21 import com.intellij.openapi.components.*
22 import com.intellij.openapi.components.StateStorage.SaveSession
23 import com.intellij.openapi.components.StateStorageChooserEx.Resolution
24 import com.intellij.openapi.components.impl.stores.StateStorageManager
25 import com.intellij.openapi.util.Disposer
26 import com.intellij.openapi.util.io.FileUtilRt
27 import com.intellij.openapi.util.text.StringUtil
28 import com.intellij.openapi.vfs.newvfs.events.VFileEvent
29 import com.intellij.util.PathUtilRt
30 import com.intellij.util.ReflectionUtil
31 import com.intellij.util.SmartList
32 import com.intellij.util.ThreeState
33 import com.intellij.util.containers.ContainerUtil
34 import gnu.trove.THashMap
35 import org.jdom.Element
36 import org.jetbrains.annotations.TestOnly
37 import java.io.IOException
38 import java.nio.file.Path
39 import java.nio.file.Paths
40 import java.util.*
41 import java.util.concurrent.locks.ReentrantReadWriteLock
42 import java.util.regex.Pattern
43 import kotlin.concurrent.read
44 import kotlin.concurrent.write
45
46 private val MACRO_PATTERN = Pattern.compile("(\\$[^$]*\\$)")
47
48 /**
49  * If componentManager not specified, storage will not add file tracker
50  */
51 open class StateStorageManagerImpl(private val rootTagName: String,
52                                    private val pathMacroSubstitutor: TrackingPathMacroSubstitutor? = null,
53                                    val componentManager: ComponentManager? = null,
54                                    private val virtualFileTracker: StorageVirtualFileTracker? = StateStorageManagerImpl.createDefaultVirtualTracker(componentManager) ) : StateStorageManager {
55   private val macros: MutableList<Macro> = ContainerUtil.createLockFreeCopyOnWriteList()
56   private val storageLock = ReentrantReadWriteLock()
57   private val storages = THashMap<String, StateStorage>()
58
59   private val streamWrapper = StreamProviderWrapper()
60   var streamProvider: StreamProvider?
61     get() = streamWrapper
62     set (value) {
63       streamWrapper.streamProvider = value
64     }
65
66   // access under storageLock
67   private var isUseVfsListener = if (componentManager == null) ThreeState.NO else ThreeState.UNSURE // unsure because depends on stream provider state
68
69   protected open val isUseXmlProlog: Boolean
70     get() = true
71
72   companion object {
73     private fun createDefaultVirtualTracker(componentManager: ComponentManager?) = when (componentManager) {
74       null -> {
75         null
76       }
77       is Application -> {
78         StorageVirtualFileTracker(componentManager.messageBus)
79       }
80       else -> {
81         val tracker = (ApplicationManager.getApplication().stateStore.stateStorageManager as? StateStorageManagerImpl)?.virtualFileTracker
82         if (tracker != null) {
83           Disposer.register(componentManager, Disposable {
84             tracker.remove { it.storageManager.componentManager == componentManager }
85           })
86         }
87         tracker
88       }
89     }
90   }
91
92   override final fun getMacroSubstitutor() = pathMacroSubstitutor
93
94   private data class Macro(val key: String, var value: String)
95
96   @TestOnly fun getVirtualFileTracker() = virtualFileTracker
97
98   /**
99    * @param expansion System-independent.
100    */
101   fun addMacro(key: String, expansion: String):Boolean {
102     assert(!key.isEmpty())
103
104     val value: String
105     if (expansion.contains("\\")) {
106       val message = "Macro $key set to system-dependent expansion $expansion"
107       if (ApplicationManager.getApplication().isUnitTestMode) {
108         throw IllegalArgumentException(message)
109       }
110       else {
111         LOG.warn(message)
112         value = FileUtilRt.toSystemIndependentName(expansion)
113       }
114     }
115     else {
116       value = expansion
117     }
118
119     // you must not add duplicated macro, but our ModuleImpl.setModuleFilePath does it (it will be fixed later)
120     for (macro in macros) {
121       if (key == macro.key) {
122         macro.value = value
123         return false
124       }
125     }
126
127     macros.add(Macro(key, value))
128     return true
129   }
130
131   // system-independent paths
132   open fun pathRenamed(oldPath: String, newPath: String, event: VFileEvent?) {
133     for (macro in macros) {
134       if (oldPath == macro.value) {
135         macro.value = newPath
136       }
137     }
138   }
139
140   override final fun getStateStorage(storageSpec: Storage) = getOrCreateStorage(
141     storageSpec.path,
142     storageSpec.roamingType,
143     storageSpec.storageClass.java,
144     storageSpec.stateSplitter.java,
145     storageSpec.exclusive
146   )
147
148   protected open fun normalizeFileSpec(fileSpec: String): String {
149     val path = FileUtilRt.toSystemIndependentName(fileSpec)
150     // fileSpec for directory based storage could be erroneously specified as "name/"
151     return if (path.endsWith('/')) path.substring(0, path.length - 1) else path
152   }
153
154   fun getOrCreateStorage(collapsedPath: String,
155                          roamingType: RoamingType = RoamingType.DEFAULT,
156                          storageClass: Class<out StateStorage> = StateStorage::class.java,
157                          @Suppress("DEPRECATION") stateSplitter: Class<out StateSplitter> = StateSplitterEx::class.java,
158                          exclusive: Boolean = false): StateStorage {
159     val normalizedCollapsedPath = normalizeFileSpec(collapsedPath)
160     val key: String
161     if (storageClass == StateStorage::class.java) {
162       if (normalizedCollapsedPath.isEmpty()) {
163         throw Exception("Normalized path is empty, raw path '$collapsedPath'")
164       }
165       key = normalizedCollapsedPath
166     }
167     else {
168       key = storageClass.name!!
169     }
170     storageLock.read {
171       var storage = storages.get(key)
172       if (storage == null) {
173         storageLock.write {
174           storage = createStateStorage(storageClass, normalizedCollapsedPath, roamingType, stateSplitter, exclusive)
175           storages.put(key, storage)
176         }
177       }
178       return storage!!
179     }
180   }
181
182   fun getCachedFileStorages() = storageLock.read { storages.values.toSet() }
183
184   fun findCachedFileStorage(name: String) : StateStorage? = storageLock.read { storages[name] }
185
186   fun getCachedFileStorages(changed: Collection<String>, deleted: Collection<String>, pathNormalizer: ((String) -> String)? = null) = storageLock.read {
187     Pair(getCachedFileStorages(changed, pathNormalizer), getCachedFileStorages(deleted, pathNormalizer))
188   }
189
190   fun getCachedFileStorages(fileSpecs: Collection<String>, pathNormalizer: ((String) -> String)? = null): Collection<FileBasedStorage> {
191     if (fileSpecs.isEmpty()) {
192       return emptyList()
193     }
194
195     storageLock.read {
196       var result: MutableList<FileBasedStorage>? = null
197       for (fileSpec in fileSpecs) {
198         val path = normalizeFileSpec(pathNormalizer?.invoke(fileSpec) ?: fileSpec)
199         val storage = storages.get(path)
200         if (storage is FileBasedStorage) {
201           if (result == null) {
202             result = SmartList<FileBasedStorage>()
203           }
204           result.add(storage)
205         }
206       }
207       return result ?: emptyList()
208     }
209   }
210
211   // overridden in upsource
212   protected open fun createStateStorage(storageClass: Class<out StateStorage>,
213                                         collapsedPath: String,
214                                         roamingType: RoamingType,
215                                         @Suppress("DEPRECATION") stateSplitter: Class<out StateSplitter>,
216                                         exclusive: Boolean = false): StateStorage {
217     if (storageClass != StateStorage::class.java) {
218       val constructor = storageClass.constructors.get(0)!!
219       constructor.isAccessible = true
220       return constructor.newInstance(componentManager!!, this) as StateStorage
221     }
222
223     val effectiveRoamingType: RoamingType
224     if (roamingType != RoamingType.DISABLED && (collapsedPath == StoragePathMacros.WORKSPACE_FILE || collapsedPath == "other.xml")) {
225       effectiveRoamingType = RoamingType.DISABLED
226     }
227     else {
228       effectiveRoamingType = roamingType
229     }
230
231     if (isUseVfsListener == ThreeState.UNSURE) {
232       isUseVfsListener = ThreeState.fromBoolean(streamProvider == null || !streamProvider!!.isApplicable(collapsedPath, effectiveRoamingType))
233     }
234
235     val filePath = expandMacros(collapsedPath)
236     @Suppress("DEPRECATION")
237     if (stateSplitter != StateSplitter::class.java && stateSplitter != StateSplitterEx::class.java) {
238       val storage = createDirectoryBasedStorage(filePath, collapsedPath, ReflectionUtil.newInstance(stateSplitter))
239       if (storage is StorageVirtualFileTracker.TrackedStorage) {
240         virtualFileTracker?.put(filePath, storage)
241       }
242       return storage
243     }
244
245     if (!ApplicationManager.getApplication().isHeadlessEnvironment && PathUtilRt.getFileName(filePath).lastIndexOf('.') < 0) {
246       throw IllegalArgumentException("Extension is missing for storage file: $filePath")
247     }
248
249     val storage = createFileBasedStorage(filePath, collapsedPath, effectiveRoamingType, if (exclusive) null else this.rootTagName)
250     if (isUseVfsListener == ThreeState.YES && storage is StorageVirtualFileTracker.TrackedStorage) {
251       virtualFileTracker?.put(filePath, storage)
252     }
253     return storage
254   }
255
256   protected open fun createFileBasedStorage(path: String, collapsedPath: String, roamingType: RoamingType, rootTagName: String?): StateStorage
257       = MyFileStorage(this, Paths.get(path), collapsedPath, rootTagName, roamingType, getMacroSubstitutor(collapsedPath), streamProvider)
258
259   protected open fun createDirectoryBasedStorage(path: String, collapsedPath: String, @Suppress("DEPRECATION") splitter: StateSplitter): StateStorage
260       = MyDirectoryStorage(this, Paths.get(path), splitter)
261
262   private class MyDirectoryStorage(override val storageManager: StateStorageManagerImpl, file: Path, @Suppress("DEPRECATION") splitter: StateSplitter) :
263     DirectoryBasedStorage(file, splitter, storageManager.pathMacroSubstitutor), StorageVirtualFileTracker.TrackedStorage
264
265   private class MyFileStorage(override val storageManager: StateStorageManagerImpl,
266                               file: Path,
267                               fileSpec: String,
268                               rootElementName: String?,
269                               roamingType: RoamingType,
270                               pathMacroManager: TrackingPathMacroSubstitutor? = null,
271                               provider: StreamProvider? = null) : FileBasedStorage(file, fileSpec, rootElementName, pathMacroManager, roamingType, provider), StorageVirtualFileTracker.TrackedStorage {
272     override val isUseXmlProlog: Boolean
273       get() = rootElementName != null && storageManager.isUseXmlProlog
274
275     override fun beforeElementSaved(element: Element) {
276       if (rootElementName != null) {
277         storageManager.beforeElementSaved(element)
278       }
279       super.beforeElementSaved(element)
280     }
281
282     override fun beforeElementLoaded(element: Element) {
283       storageManager.beforeElementLoaded(element)
284       super.beforeElementLoaded(element)
285     }
286
287     override fun dataLoadedFromProvider(element: Element?) {
288       storageManager.dataLoadedFromProvider(this, element)
289     }
290   }
291
292   protected open fun beforeElementSaved(element: Element) {
293   }
294
295   protected open fun beforeElementLoaded(element: Element) {
296   }
297
298   protected open fun dataLoadedFromProvider(storage: FileBasedStorage, element: Element?) {
299   }
300
301   override final fun rename(path: String, newName: String) {
302     storageLock.write {
303       val storage = getOrCreateStorage(collapseMacros(path), RoamingType.DEFAULT) as FileBasedStorage
304
305       val file = storage.virtualFile
306       try {
307         if (file != null) {
308           file.rename(storage, newName)
309         }
310         else if (storage.file.fileName.toString() != newName) {
311           // old file didn't exist or renaming failed
312           val expandedPath = expandMacros(path)
313           val parentPath = PathUtilRt.getParentPath(expandedPath)
314           storage.setFile(null, Paths.get(parentPath, newName))
315           pathRenamed(expandedPath, "$parentPath/$newName", null)
316         }
317       }
318       catch (e: IOException) {
319         LOG.debug(e)
320       }
321     }
322   }
323
324   fun clearStorages() {
325     storageLock.write {
326       try {
327         virtualFileTracker?.let {
328           storages.forEachEntry { collapsedPath, storage ->
329             it.remove(expandMacros(collapsedPath))
330             true
331           }
332         }
333       }
334       finally {
335         storages.clear()
336       }
337     }
338   }
339
340   protected open fun getMacroSubstitutor(fileSpec: String): TrackingPathMacroSubstitutor? = pathMacroSubstitutor
341
342   override fun expandMacros(path: String): String {
343     // replacement can contains $ (php tests), so, this check must be performed before expand
344     val matcher = MACRO_PATTERN.matcher(path)
345     matcherLoop@
346     while (matcher.find()) {
347       val m = matcher.group(1)
348       for ((key) in macros) {
349         if (key == m) {
350           continue@matcherLoop
351         }
352       }
353       throw IllegalArgumentException("Unknown macro: $m in storage file spec: $path")
354     }
355
356     var expanded = path
357     for ((key, value) in macros) {
358       expanded = StringUtil.replace(expanded, key, value)
359     }
360     return expanded
361   }
362
363   fun expandMacro(macro: String): String {
364     for ((key, value) in macros) {
365       if (key == macro) {
366         return value
367       }
368     }
369
370     throw IllegalArgumentException("Unknown macro $macro")
371   }
372
373   fun collapseMacros(path: String): String {
374     var result = path
375     for ((key, value) in macros) {
376       result = result.replace(value, key)
377     }
378     return normalizeFileSpec(result)
379   }
380
381   override final fun startExternalization() = object : StateStorageManager.ExternalizationSession {
382     private val sessions = LinkedHashMap<StateStorage, StateStorage.ExternalizationSession>()
383
384     override fun setState(storageSpecs: Array<Storage>, component: Any, componentName: String, state: Any) {
385       val stateStorageChooser = component as? StateStorageChooserEx
386       for (storageSpec in storageSpecs) {
387         val resolution = if (stateStorageChooser == null) Resolution.DO else stateStorageChooser.getResolution(storageSpec, StateStorageOperation.WRITE)
388         if (resolution == Resolution.SKIP) {
389           continue
390         }
391
392         getExternalizationSession(getStateStorage(storageSpec))?.setState(component, componentName, if (storageSpec.deprecated || resolution == Resolution.CLEAR) Element("empty") else state)
393       }
394     }
395
396     override fun setStateInOldStorage(component: Any, componentName: String, state: Any) {
397       getOldStorage(component, componentName, StateStorageOperation.WRITE)?.let {
398         getExternalizationSession(it)?.setState(component, componentName, state)
399       }
400     }
401
402     private fun getExternalizationSession(storage: StateStorage): StateStorage.ExternalizationSession? {
403       var session = sessions.get(storage)
404       if (session == null) {
405         session = storage.startExternalization()
406         if (session != null) {
407           sessions.put(storage, session)
408         }
409       }
410       return session
411     }
412
413     override fun createSaveSessions(): List<SaveSession> {
414       if (sessions.isEmpty()) {
415         return emptyList()
416       }
417
418       var saveSessions: MutableList<SaveSession>? = null
419       val externalizationSessions = sessions.values
420       for (session in externalizationSessions) {
421         val saveSession = session.createSaveSession()
422         if (saveSession != null) {
423           if (saveSessions == null) {
424             if (externalizationSessions.size == 1) {
425               return listOf(saveSession)
426             }
427             saveSessions = SmartList<SaveSession>()
428           }
429           saveSessions.add(saveSession)
430         }
431       }
432       return saveSessions ?: emptyList()
433     }
434   }
435
436   override final fun getOldStorage(component: Any, componentName: String, operation: StateStorageOperation): StateStorage? {
437     val oldStorageSpec = getOldStorageSpec(component, componentName, operation) ?: return null
438     @Suppress("DEPRECATION")
439     return getOrCreateStorage(oldStorageSpec, if (component is com.intellij.openapi.util.RoamingTypeDisabled) RoamingType.DISABLED else RoamingType.DEFAULT)
440   }
441
442   protected open fun getOldStorageSpec(component: Any, componentName: String, operation: StateStorageOperation): String? = null
443 }
444
445 private fun String.startsWithMacro(macro: String): Boolean {
446   val i = macro.length
447   return length > i && this.get(i) == '/' && startsWith(macro)
448 }
449
450 fun removeMacroIfStartsWith(path: String, macro: String) = if (path.startsWithMacro(macro)) path.substring(macro.length + 1) else path
451
452 @Suppress("DEPRECATION")
453 internal val Storage.path: String
454   get() = if (value.isNullOrEmpty()) file else value