2 * Copyright 2000-2015 JetBrains s.r.o.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 package com.intellij.configurationStore
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
50 import java.io.IOException
52 import java.util.concurrent.CopyOnWriteArrayList
53 import com.intellij.openapi.util.Pair as JBPair
55 internal val LOG = Logger.getInstance(ComponentStoreImpl::class.java)
57 abstract class ComponentStoreImpl : IComponentStore {
58 private val components = Collections.synchronizedMap(THashMap<String, Any>())
59 private val settingsSavingComponents = CopyOnWriteArrayList<SettingsSavingComponent>()
61 protected open val project: Project?
64 open val loadPolicy: StateLoadPolicy
65 get() = StateLoadPolicy.LOAD
67 abstract val storageManager: StateStorageManager
69 override final fun getStateStorageManager() = storageManager
71 // return null if not applicable
72 protected open fun selectDefaultStorages(storages: Array<Storage>, operation: StateStorageOperation): Array<Storage>? = null
74 override final fun initComponent(component: Any, service: Boolean) {
75 if (component is SettingsSavingComponent) {
76 settingsSavingComponents.add(component)
79 @Suppress("DEPRECATION")
80 if (!(component is JDOMExternalizable || component is PersistentStateComponent<*>)) {
84 val componentNameIfStateExists: String?
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)
93 @Suppress("DEPRECATION")
94 initJdomExternalizable(component as JDOMExternalizable)
97 catch (e: ProcessCanceledException) {
100 catch (e: Exception) {
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)
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)
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)
126 val duration = System.currentTimeMillis() - start
128 it.append("\n").append(name).append(" took ").append(duration).append(" ms: ").append((duration / 60000)).append(" min ").append(((duration % 60000) / 1000)).append("sec")
133 if (timeLog != null && timeLog.length > timeLogPrefix.length) {
134 LOG.debug(timeLog.toString())
138 var errors: MutableList<Throwable>? = null
139 for (settingsSavingComponent in settingsSavingComponents) {
141 settingsSavingComponent.save()
143 catch (e: Throwable) {
144 if (errors == null) {
145 errors = SmartList<Throwable>()
151 if (externalizationSession != null) {
152 errors = doSave(externalizationSession.createSaveSessions(), readonlyFiles, errors)
154 CompoundRuntimeException.throwIfNotEmpty(errors)
157 override @TestOnly fun saveApplicationComponent(component: Any) {
158 val externalizationSession = storageManager.startExternalization() ?: return
160 commitComponent(externalizationSession, component, null)
161 val sessions = externalizationSession.createSaveSessions()
162 if (sessions.isEmpty()) {
167 val state = StoreUtil.getStateSpec(component.javaClass)
169 file = File(storageManager.expandMacros(findNonDeprecated(state.storages).file))
171 else if (component is ExportableApplicationComponent && component is NamedJDOMExternalizable) {
172 file = PathManager.getOptionsFile(component)
175 throw AssertionError("${component.javaClass} doesn't have @State annotation and doesn't implement ExportableApplicationComponent")
178 val token = WriteAction.start()
180 VfsRootAccess.allowRootAccess(file.absolutePath)
181 CompoundRuntimeException.throwIfNotEmpty(doSave(sessions))
185 VfsRootAccess.disallowRootAccess(file.absolutePath)
193 private fun commitComponent(session: ExternalizationSession, component: Any, componentName: String?) {
194 @Suppress("DEPRECATION")
195 if (component is PersistentStateComponent<*>) {
196 val state = component.state
198 val stateSpec = StoreUtil.getStateSpec(component)
199 session.setState(getStorageSpecs(component, stateSpec, StateStorageOperation.WRITE), component, componentName ?: stateSpec.name, state)
202 else if (component is JDOMExternalizable) {
203 session.setStateInOldStorage(component, componentName ?: ComponentManagerImpl.getComponentName(component), component)
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)
215 private fun initJdomExternalizable(@Suppress("DEPRECATION") component: JDOMExternalizable): String? {
216 val componentName = ComponentManagerImpl.getComponentName(component)
217 doAddComponent(componentName, component)
219 if (loadPolicy != StateLoadPolicy.LOAD) {
224 getDefaultState(component, componentName, Element::class.java)?.let { component.readExternal(it) }
226 catch (e: Throwable) {
230 val element = storageManager.getOldStorage(component, componentName, StateStorageOperation.READ)?.getState(component, componentName, Element::class.java, null, false) ?: return null
232 component.readExternal(element)
234 catch (e: InvalidDataException) {
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}")
249 private fun <T: Any> initPersistentComponent(stateSpec: State, component: PersistentStateComponent<T>, changedStorages: Set<StateStorage>?, reloadData: Boolean): String? {
250 if (loadPolicy == StateLoadPolicy.NOT_LOAD) {
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")
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) {
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)
276 var state = if (stateGetter == null) storage.getState(component, name, stateClass, defaultState, reloadData) else stateGetter.getState(defaultState)
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)!!
289 component.loadState(state)
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)
305 protected open fun isUseLoadedStateAsExisting(storage: StateStorage): Boolean = (storage as? XmlElementStorage)?.roamingType != RoamingType.DISABLED
307 protected open fun getPathMacroManagerForDefaults(): PathMacroManager? = null
309 private fun <T : Any> getDefaultState(component: Any, componentName: String, stateClass: Class<T>): T? {
310 val url = DecodeDefaultsUtil.getDefaults(component, componentName) ?: return null
312 val documentElement = JDOMXIncluder.resolve(JDOMUtil.loadDocument(url), url.toExternalForm()).detachRootElement()
313 getPathMacroManagerForDefaults()?.expandPaths(documentElement)
314 return DefaultStateSerializer.deserializeState(documentElement, stateClass, null)
316 catch (e: Throwable) {
317 throw IOException("Error loading default state from $url", e)
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) {
327 if (storages.isEmpty()) {
328 if (stateSpec.defaultStateAsResource) {
332 throw AssertionError("No storage specified")
335 val defaultStorages = selectDefaultStorages(storages, operation)
336 if (defaultStorages != null) {
337 return defaultStorages
340 return sortStoragesByDeprecated(storages)
343 override final fun isReloadPossible(componentNames: MutableSet<String>) = !componentNames.any { isNotReloadable(it) }
345 private fun isNotReloadable(component: Any?) = component != null && (component !is PersistentStateComponent<*> || !StoreUtil.getStateSpec(component).reloadable)
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>()
354 notReloadableComponents.add(componentName)
357 return notReloadableComponents ?: emptySet<String>()
360 override final fun reloadStates(componentNames: MutableSet<String>, messageBus: MessageBus) {
361 runBatchUpdate(messageBus) {
362 reinitComponents(componentNames)
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)
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) {
382 val changedStoragesEmpty = changedStorages.isEmpty()
383 initPersistentComponent(StoreUtil.getStateSpec(component), component, if (changedStoragesEmpty) null else changedStorages, changedStoragesEmpty)
390 * empty list if nothing to reload
391 * list of not reloadable components (reload is not performed)
393 fun reload(changedStorages: Set<StateStorage>): Collection<String>? {
394 if (changedStorages.isEmpty()) {
398 val componentNames = SmartHashSet<String>()
399 for (storage in changedStorages) {
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)
405 catch (e: Throwable) {
410 if (componentNames.isEmpty) {
414 val notReloadableComponents = getNotReloadableComponents(componentNames)
415 reinitComponents(componentNames, changedStorages, notReloadableComponents)
416 return if (notReloadableComponents.isEmpty()) null else notReloadableComponents
419 // used in settings repository plugin
421 * You must call it in batch mode (use runBatchUpdate)
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)
431 @TestOnly fun removeComponent(name: String) {
432 components.remove(name)
436 internal fun executeSave(session: SaveSession, readonlyFiles: MutableList<JBPair<SaveSession, VirtualFile>>, previousErrors: MutableList<Throwable>?): MutableList<Throwable>? {
437 var errors = previousErrors
441 catch (e: ReadOnlyModificationException) {
443 readonlyFiles.add(JBPair.create<SaveSession, VirtualFile>(e.session ?: session, e.file))
445 catch (e: Exception) {
446 if (errors == null) {
447 errors = SmartList<Throwable>()
455 private fun findNonDeprecated(storages: Array<Storage>): Storage {
456 for (storage in storages) {
457 if (!storage.deprecated) {
461 throw AssertionError("All storages are deprecated")
464 enum class StateLoadPolicy {
465 LOAD, LOAD_ONLY_DEFAULT, NOT_LOAD
468 internal fun sortStoragesByDeprecated(storages: Array<Storage>): Array<out Storage> {
469 if (storages.isEmpty()) {
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
482 if (othersAreDeprecated) {
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