save timing debug log
[idea/community.git] / platform / configuration-store-impl / src / ComponentStoreImpl.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.application.ApplicationManager
19 import com.intellij.openapi.application.PathManager
20 import com.intellij.openapi.application.WriteAction
21 import com.intellij.openapi.application.ex.DecodeDefaultsUtil
22 import com.intellij.openapi.application.runBatchUpdate
23 import com.intellij.openapi.components.*
24 import com.intellij.openapi.components.StateStorage.SaveSession
25 import com.intellij.openapi.components.StateStorageChooserEx.Resolution
26 import com.intellij.openapi.components.impl.ComponentManagerImpl
27 import com.intellij.openapi.components.impl.stores.*
28 import com.intellij.openapi.components.impl.stores.StateStorageManager.ExternalizationSession
29 import com.intellij.openapi.components.store.ReadOnlyModificationException
30 import com.intellij.openapi.diagnostic.Logger
31 import com.intellij.openapi.progress.ProcessCanceledException
32 import com.intellij.openapi.project.Project
33 import com.intellij.openapi.util.InvalidDataException
34 import com.intellij.openapi.util.JDOMExternalizable
35 import com.intellij.openapi.util.JDOMUtil
36 import com.intellij.openapi.util.NamedJDOMExternalizable
37 import com.intellij.openapi.util.registry.Registry
38 import com.intellij.openapi.vfs.VirtualFile
39 import com.intellij.openapi.vfs.newvfs.impl.VfsRootAccess
40 import com.intellij.util.ArrayUtilRt
41 import com.intellij.util.SmartList
42 import com.intellij.util.containers.SmartHashSet
43 import com.intellij.util.lang.CompoundRuntimeException
44 import com.intellij.util.messages.MessageBus
45 import com.intellij.util.xmlb.JDOMXIncluder
46 import gnu.trove.THashMap
47 import org.jdom.Element
48 import org.jetbrains.annotations.TestOnly
49 import java.io.File
50 import java.io.IOException
51 import java.util.*
52 import java.util.concurrent.CopyOnWriteArrayList
53 import com.intellij.openapi.util.Pair as JBPair
54
55 internal val LOG = Logger.getInstance(ComponentStoreImpl::class.java)
56
57 abstract class ComponentStoreImpl : IComponentStore {
58   private val components = Collections.synchronizedMap(THashMap<String, Any>())
59   private val settingsSavingComponents = CopyOnWriteArrayList<SettingsSavingComponent>()
60
61   protected open val project: Project?
62     get() = null
63
64   open val loadPolicy: StateLoadPolicy
65     get() = StateLoadPolicy.LOAD
66
67   abstract val storageManager: StateStorageManager
68
69   override final fun getStateStorageManager() = storageManager
70
71   // return null if not applicable
72   protected open fun selectDefaultStorages(storages: Array<Storage>, operation: StateStorageOperation): Array<Storage>? = null
73
74   override final fun initComponent(component: Any, service: Boolean) {
75     if (component is SettingsSavingComponent) {
76       settingsSavingComponents.add(component)
77     }
78
79     @Suppress("DEPRECATION")
80     if (!(component is JDOMExternalizable || component is PersistentStateComponent<*>)) {
81       return
82     }
83
84     val componentNameIfStateExists: String?
85     try {
86       componentNameIfStateExists = if (component is PersistentStateComponent<*>) {
87         val stateSpec = StoreUtil.getStateSpec(component)
88         doAddComponent(stateSpec.name, component)
89         @Suppress("UNCHECKED_CAST")
90         initPersistentComponent(stateSpec, component as PersistentStateComponent<Any>, null, false)
91       }
92       else {
93         @Suppress("DEPRECATION")
94         initJdomExternalizable(component as JDOMExternalizable)
95       }
96     }
97     catch (e: ProcessCanceledException) {
98       throw e
99     }
100     catch (e: Exception) {
101       LOG.error(e)
102       return
103     }
104
105     // if not service, so, component manager will check it later for all components
106     if (componentNameIfStateExists != null && service) {
107       val project = this.project
108       val app = ApplicationManager.getApplication()
109       if (project != null && !app.isHeadlessEnvironment && !app.isUnitTestMode && project.isInitialized) {
110         StorageUtil.notifyUnknownMacros(this, project, componentNameIfStateExists)
111       }
112     }
113   }
114
115   override fun save(readonlyFiles: MutableList<JBPair<StateStorage.SaveSession, VirtualFile>>) {
116     val externalizationSession = if (components.isEmpty()) null else storageManager.startExternalization()
117     if (externalizationSession != null) {
118       val names = ArrayUtilRt.toStringArray(components.keys)
119       Arrays.sort(names)
120       val timeLogPrefix = "Saving"
121       var timeLog = if (LOG.isDebugEnabled) StringBuilder(timeLogPrefix) else null
122       for (name in names) {
123         val start = if (timeLog == null) 0 else System.currentTimeMillis()
124         commitComponent(externalizationSession, components.get(name)!!, name)
125         timeLog?.let {
126           val duration = System.currentTimeMillis() - start
127           if (duration > 10) {
128             it.append("\n").append(name).append(" took ").append(duration).append(" ms: ").append((duration / 60000)).append(" min ").append(((duration % 60000) / 1000)).append("sec")
129           }
130         }
131       }
132
133       if (timeLog != null && timeLog.length > timeLogPrefix.length) {
134         LOG.debug(timeLog.toString())
135       }
136     }
137
138     var errors: MutableList<Throwable>? = null
139     for (settingsSavingComponent in settingsSavingComponents) {
140       try {
141         settingsSavingComponent.save()
142       }
143       catch (e: Throwable) {
144         if (errors == null) {
145           errors = SmartList<Throwable>()
146         }
147         errors.add(e)
148       }
149     }
150
151     if (externalizationSession != null) {
152       errors = doSave(externalizationSession.createSaveSessions(), readonlyFiles, errors)
153     }
154     CompoundRuntimeException.throwIfNotEmpty(errors)
155   }
156
157   override @TestOnly fun saveApplicationComponent(component: Any) {
158     val externalizationSession = storageManager.startExternalization() ?: return
159
160     commitComponent(externalizationSession, component, null)
161     val sessions = externalizationSession.createSaveSessions()
162     if (sessions.isEmpty()) {
163       return
164     }
165
166     val file: File
167     val state = StoreUtil.getStateSpec(component.javaClass)
168     if (state != null) {
169       file = File(storageManager.expandMacros(findNonDeprecated(state.storages).file))
170     }
171     else if (component is ExportableApplicationComponent && component is NamedJDOMExternalizable) {
172       file = PathManager.getOptionsFile(component)
173     }
174     else {
175       throw AssertionError("${component.javaClass} doesn't have @State annotation and doesn't implement ExportableApplicationComponent")
176     }
177
178     val token = WriteAction.start()
179     try {
180       VfsRootAccess.allowRootAccess(file.absolutePath)
181       CompoundRuntimeException.throwIfNotEmpty(doSave(sessions))
182     }
183     finally {
184       try {
185         VfsRootAccess.disallowRootAccess(file.absolutePath)
186       }
187       finally {
188         token.finish()
189       }
190     }
191   }
192
193   private fun commitComponent(session: ExternalizationSession, component: Any, componentName: String?) {
194     @Suppress("DEPRECATION")
195     if (component is PersistentStateComponent<*>) {
196       val state = component.state
197       if (state != null) {
198         val stateSpec = StoreUtil.getStateSpec(component)
199         session.setState(getStorageSpecs(component, stateSpec, StateStorageOperation.WRITE), component, componentName ?: stateSpec.name, state)
200       }
201     }
202     else if (component is JDOMExternalizable) {
203       session.setStateInOldStorage(component, componentName ?: ComponentManagerImpl.getComponentName(component), component)
204     }
205   }
206
207   protected open fun doSave(saveSessions: List<SaveSession>, readonlyFiles: MutableList<JBPair<SaveSession, VirtualFile>> = arrayListOf(), prevErrors: MutableList<Throwable>? = null): MutableList<Throwable>? {
208     var errors = prevErrors
209     for (session in saveSessions) {
210       errors = executeSave(session, readonlyFiles, prevErrors)
211     }
212     return errors
213   }
214
215   private fun initJdomExternalizable(@Suppress("DEPRECATION") component: JDOMExternalizable): String? {
216     val componentName = ComponentManagerImpl.getComponentName(component)
217     doAddComponent(componentName, component)
218
219     if (loadPolicy != StateLoadPolicy.LOAD) {
220       return null
221     }
222
223     try {
224       getDefaultState(component, componentName, Element::class.java)?.let { component.readExternal(it) }
225     }
226     catch (e: Throwable) {
227       LOG.error(e)
228     }
229
230     val element = storageManager.getOldStorage(component, componentName, StateStorageOperation.READ)?.getState(component, componentName, Element::class.java, null, false) ?: return null
231     try {
232       component.readExternal(element)
233     }
234     catch (e: InvalidDataException) {
235       LOG.error(e)
236       return null
237     }
238     return componentName
239   }
240
241   private fun doAddComponent(name: String, component: Any) {
242     val existing = components.put(name, component)
243     if (existing != null && existing !== component) {
244       components.put(name, existing)
245       LOG.error("Conflicting component name '$name': ${existing.javaClass} and ${component.javaClass}")
246     }
247   }
248
249   private fun <T: Any> initPersistentComponent(stateSpec: State, component: PersistentStateComponent<T>, changedStorages: Set<StateStorage>?, reloadData: Boolean): String? {
250     if (loadPolicy == StateLoadPolicy.NOT_LOAD) {
251       return null
252     }
253
254     val name = stateSpec.name
255     val stateClass = ComponentSerializationUtil.getStateClass<T>(component.javaClass)
256     if (!stateSpec.defaultStateAsResource && LOG.isDebugEnabled && getDefaultState(component, name, stateClass) != null) {
257       LOG.error("$name has default state, but not marked to load it")
258     }
259
260     val defaultState = if (stateSpec.defaultStateAsResource) getDefaultState(component, name, stateClass) else null
261     if (loadPolicy == StateLoadPolicy.LOAD) {
262       val storageSpecs = getStorageSpecs(component, stateSpec, StateStorageOperation.READ)
263       val storageChooser = component as? StateStorageChooserEx
264       for (storageSpec in storageSpecs) {
265         if (storageChooser?.getResolution(storageSpec, StateStorageOperation.READ) == Resolution.SKIP) {
266           continue
267         }
268
269         val storage = storageManager.getStateStorage(storageSpec)
270         var stateGetter = if (isUseLoadedStateAsExisting(storage) && (ApplicationManager.getApplication().isUnitTestMode || Registry.`is`("use.loaded.state.as.existing", false))) {
271           (storage as? StorageBaseEx<*>)?.createGetSession(component, name, stateClass)
272         }
273         else {
274           null
275         }
276         var state = if (stateGetter == null) storage.getState(component, name, stateClass, defaultState, reloadData) else stateGetter.getState(defaultState)
277         if (state == null) {
278           if (changedStorages != null && changedStorages.contains(storage)) {
279             // state will be null if file deleted
280             // we must create empty (initial) state to reinit component
281             state = DefaultStateSerializer.deserializeState(Element("state"), stateClass, null)!!
282           }
283           else {
284             continue
285           }
286         }
287
288         try {
289           component.loadState(state)
290         }
291         finally {
292           stateGetter?.close()
293         }
294         return name
295       }
296     }
297
298     // we load default state even if isLoadComponentState false - required for app components (for example, at least one color scheme must exists)
299     if (defaultState != null) {
300       component.loadState(defaultState)
301     }
302     return name
303   }
304
305   protected open fun isUseLoadedStateAsExisting(storage: StateStorage): Boolean = (storage as? XmlElementStorage)?.roamingType != RoamingType.DISABLED
306
307   protected open fun getPathMacroManagerForDefaults(): PathMacroManager? = null
308
309   private fun <T : Any> getDefaultState(component: Any, componentName: String, stateClass: Class<T>): T? {
310     val url = DecodeDefaultsUtil.getDefaults(component, componentName) ?: return null
311     try {
312       val documentElement = JDOMXIncluder.resolve(JDOMUtil.loadDocument(url), url.toExternalForm()).detachRootElement()
313       getPathMacroManagerForDefaults()?.expandPaths(documentElement)
314       return DefaultStateSerializer.deserializeState(documentElement, stateClass, null)
315     }
316     catch (e: Throwable) {
317       throw IOException("Error loading default state from $url", e)
318     }
319   }
320
321   protected open fun <T> getStorageSpecs(component: PersistentStateComponent<T>, stateSpec: State, operation: StateStorageOperation): Array<out Storage> {
322     val storages = stateSpec.storages
323     if (storages.size == 1 || component is StateStorageChooserEx) {
324       return storages
325     }
326
327     if (storages.isEmpty()) {
328       if (stateSpec.defaultStateAsResource) {
329         return storages
330       }
331
332       throw AssertionError("No storage specified")
333     }
334
335     val defaultStorages = selectDefaultStorages(storages, operation)
336     if (defaultStorages != null) {
337       return defaultStorages
338     }
339
340     return sortStoragesByDeprecated(storages)
341   }
342
343   override final fun isReloadPossible(componentNames: MutableSet<String>) = !componentNames.any { isNotReloadable(it) }
344
345   private fun isNotReloadable(component: Any?) = component != null && (component !is PersistentStateComponent<*> || !StoreUtil.getStateSpec(component).reloadable)
346
347   fun getNotReloadableComponents(componentNames: Collection<String>): Collection<String> {
348     var notReloadableComponents: MutableSet<String>? = null
349     for (componentName in componentNames) {
350       if (isNotReloadable(components.get(componentName))) {
351         if (notReloadableComponents == null) {
352           notReloadableComponents = LinkedHashSet<String>()
353         }
354         notReloadableComponents.add(componentName)
355       }
356     }
357     return notReloadableComponents ?: emptySet<String>()
358   }
359
360   override final fun reloadStates(componentNames: MutableSet<String>, messageBus: MessageBus) {
361     runBatchUpdate(messageBus) {
362       reinitComponents(componentNames)
363     }
364   }
365
366   override final fun reloadState(componentClass: Class<out PersistentStateComponent<*>>) {
367     val stateSpec = StoreUtil.getStateSpecOrError(componentClass)
368     @Suppress("UNCHECKED_CAST")
369     val component = components.get(stateSpec.name) as PersistentStateComponent<Any>?
370     if (component != null) {
371       initPersistentComponent(stateSpec, component, emptySet(), true)
372     }
373   }
374
375   private fun reloadState(componentName: String, changedStorages: Set<StateStorage>): Boolean {
376     @Suppress("UNCHECKED_CAST")
377     val component = components.get(componentName) as PersistentStateComponent<Any>?
378     if (component == null) {
379       return false
380     }
381     else {
382       val changedStoragesEmpty = changedStorages.isEmpty()
383       initPersistentComponent(StoreUtil.getStateSpec(component), component, if (changedStoragesEmpty) null else changedStorages, changedStoragesEmpty)
384       return true
385     }
386   }
387
388   /**
389    * null if reloaded
390    * empty list if nothing to reload
391    * list of not reloadable components (reload is not performed)
392    */
393   fun reload(changedStorages: Set<StateStorage>): Collection<String>? {
394     if (changedStorages.isEmpty()) {
395       return emptySet()
396     }
397
398     val componentNames = SmartHashSet<String>()
399     for (storage in changedStorages) {
400       try {
401         // we must update (reload in-memory storage data) even if non-reloadable component will be detected later
402         // not saved -> user does own modification -> new (on disk) state will be overwritten and not applied
403         storage.analyzeExternalChangesAndUpdateIfNeed(componentNames)
404       }
405       catch (e: Throwable) {
406         LOG.error(e)
407       }
408     }
409
410     if (componentNames.isEmpty) {
411       return emptySet()
412     }
413
414     val notReloadableComponents = getNotReloadableComponents(componentNames)
415     reinitComponents(componentNames, changedStorages, notReloadableComponents)
416     return if (notReloadableComponents.isEmpty()) null else notReloadableComponents
417   }
418
419   // used in settings repository plugin
420   /**
421    * You must call it in batch mode (use runBatchUpdate)
422    */
423   public fun reinitComponents(componentNames: Set<String>, changedStorages: Set<StateStorage> = emptySet(), notReloadableComponents: Collection<String> = emptySet()) {
424     for (componentName in componentNames) {
425       if (!notReloadableComponents.contains(componentName)) {
426         reloadState(componentName, changedStorages)
427       }
428     }
429   }
430
431   @TestOnly fun removeComponent(name: String) {
432     components.remove(name)
433   }
434 }
435
436 internal fun executeSave(session: SaveSession, readonlyFiles: MutableList<JBPair<SaveSession, VirtualFile>>, previousErrors: MutableList<Throwable>?): MutableList<Throwable>? {
437   var errors = previousErrors
438   try {
439     session.save()
440   }
441   catch (e: ReadOnlyModificationException) {
442     LOG.warn(e)
443     readonlyFiles.add(JBPair.create<SaveSession, VirtualFile>(e.session ?: session, e.file))
444   }
445   catch (e: Exception) {
446     if (errors == null) {
447       errors = SmartList<Throwable>()
448     }
449     errors.add(e)
450   }
451
452   return errors
453 }
454
455 private fun findNonDeprecated(storages: Array<Storage>): Storage {
456   for (storage in storages) {
457     if (!storage.deprecated) {
458       return storage
459     }
460   }
461   throw AssertionError("All storages are deprecated")
462 }
463
464 enum class StateLoadPolicy {
465   LOAD, LOAD_ONLY_DEFAULT, NOT_LOAD
466 }
467
468 internal fun sortStoragesByDeprecated(storages: Array<Storage>): Array<out Storage> {
469   if (storages.isEmpty()) {
470     return storages
471   }
472
473   if (!storages[0].deprecated) {
474     var othersAreDeprecated = true
475     for (i in 1..storages.size - 1) {
476       if (!storages[i].deprecated) {
477         othersAreDeprecated = false
478         break
479       }
480     }
481
482     if (othersAreDeprecated) {
483       return storages
484     }
485   }
486
487   return storages.sortedArrayWith(comparator { o1, o2 ->
488     val w1 = if (o1.deprecated) 1 else 0
489     val w2 = if (o2.deprecated) 1 else 0
490     w1 - w2
491   })
492 }