f027b546415cef6c5e83a9ca82039a9ae93d22fc
[idea/community.git] / platform / configuration-store-impl / src / SchemeManagerImpl.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.ex.DecodeDefaultsUtil
19 import com.intellij.openapi.application.runWriteAction
20 import com.intellij.openapi.components.RoamingType
21 import com.intellij.openapi.components.impl.stores.FileStorageCoreUtil
22 import com.intellij.openapi.components.impl.stores.FileStorageCoreUtil.DEFAULT_EXT
23 import com.intellij.openapi.diagnostic.catchAndLog
24 import com.intellij.openapi.extensions.AbstractExtensionPointBean
25 import com.intellij.openapi.options.*
26 import com.intellij.openapi.util.Condition
27 import com.intellij.openapi.util.WriteExternalException
28 import com.intellij.openapi.util.io.FileUtil
29 import com.intellij.openapi.util.text.StringUtilRt
30 import com.intellij.openapi.vfs.LocalFileSystem
31 import com.intellij.openapi.vfs.SafeWriteRequestor
32 import com.intellij.openapi.vfs.VirtualFile
33 import com.intellij.openapi.vfs.VirtualFileManager
34 import com.intellij.openapi.vfs.newvfs.BulkFileListener
35 import com.intellij.openapi.vfs.newvfs.NewVirtualFile
36 import com.intellij.openapi.vfs.newvfs.events.VFileContentChangeEvent
37 import com.intellij.openapi.vfs.newvfs.events.VFileCreateEvent
38 import com.intellij.openapi.vfs.newvfs.events.VFileDeleteEvent
39 import com.intellij.openapi.vfs.newvfs.events.VFileEvent
40 import com.intellij.util.*
41 import com.intellij.util.containers.ConcurrentList
42 import com.intellij.util.containers.ContainerUtil
43 import com.intellij.util.containers.catch
44 import com.intellij.util.io.*
45 import com.intellij.util.messages.MessageBus
46 import com.intellij.util.text.UniqueNameGenerator
47 import gnu.trove.THashSet
48 import org.jdom.Document
49 import org.jdom.Element
50 import java.io.File
51 import java.io.IOException
52 import java.io.InputStream
53 import java.nio.file.Path
54 import java.util.*
55 import java.util.concurrent.atomic.AtomicBoolean
56 import java.util.concurrent.atomic.AtomicReference
57 import java.util.function.Function
58
59 class SchemeManagerImpl<T : Scheme, MUTABLE_SCHEME : T>(val fileSpec: String,
60                                                         private val processor: SchemeProcessor<T, MUTABLE_SCHEME>,
61                                                         private val provider: StreamProvider?,
62                                                         private val ioDirectory: Path,
63                                                         val roamingType: RoamingType = RoamingType.DEFAULT,
64                                                         val presentableName: String? = null,
65                                                         private val isUseOldFileNameSanitize: Boolean = false,
66                                                         private val messageBus: MessageBus? = null) : SchemeManager<T>(), SafeWriteRequestor {
67   private val isLoadingSchemes = AtomicBoolean()
68
69   private val schemesRef = AtomicReference(ContainerUtil.createLockFreeCopyOnWriteList<T>() as ConcurrentList<T>)
70
71   private val schemes: ConcurrentList<T>
72     get() = schemesRef.get()
73
74   private val readOnlyExternalizableSchemes = ContainerUtil.newConcurrentMap<String, T>()
75
76   /**
77    * Schemes can be lazy loaded, so, client should be able to set current scheme by name, not only by instance.
78    */
79   private @Volatile var currentPendingSchemeName: String? = null
80
81   private var currentScheme: T? = null
82
83   private var cachedVirtualDirectory: VirtualFile? = null
84
85   private val schemeExtension: String
86   private val updateExtension: Boolean
87
88   private val filesToDelete = ContainerUtil.newConcurrentSet<String>()
89
90   // scheme could be changed - so, hashcode will be changed - we must use identity hashing strategy
91   private val schemeToInfo = ContainerUtil.newConcurrentMap<T, ExternalInfo>(ContainerUtil.identityStrategy())
92
93   private val useVfs = messageBus != null
94
95   init {
96     if (processor is SchemeExtensionProvider) {
97       schemeExtension = processor.schemeExtension
98       updateExtension = true
99     }
100     else {
101       schemeExtension = FileStorageCoreUtil.DEFAULT_EXT
102       updateExtension = false
103     }
104
105     if (useVfs && (provider == null || !provider.isApplicable(fileSpec, roamingType))) {
106       LOG.catchAndLog { refreshVirtualDirectoryAndAddListener() }
107     }
108   }
109
110   private inner class SchemeFileTracker() : BulkFileListener.Adapter() {
111     private fun isMy(file: VirtualFile) = isMy(file.nameSequence)
112     private fun isMy(name: CharSequence) = name.endsWith(schemeExtension, ignoreCase = true) && (processor !is LazySchemeProcessor || processor.isSchemeFile(name))
113
114     override fun after(events: MutableList<out VFileEvent>) {
115       eventLoop@ for (event in events) {
116         if (event.requestor is SchemeManagerImpl<*, *>) {
117           continue
118         }
119
120         fun isMyDirectory(parent: VirtualFile) = cachedVirtualDirectory.let { if (it == null) ioDirectory.systemIndependentPath == parent.path else it == parent }
121
122         when (event) {
123           is VFileContentChangeEvent -> {
124             if (!isMy(event.file) || !isMyDirectory(event.file.parent)) {
125               continue@eventLoop
126             }
127
128             val oldCurrentScheme = currentScheme
129             findExternalizableSchemeByFileName(event.file.name)?.let {
130               removeScheme(it)
131               processor.onSchemeDeleted(it)
132             }
133
134             updateCurrentScheme(oldCurrentScheme, readSchemeFromFile(event.file)?.let {
135               processor.initScheme(it)
136               processor.onSchemeAdded(it)
137               it
138             })
139           }
140
141           is VFileCreateEvent -> {
142             if (isMy(event.childName)) {
143               if (isMyDirectory(event.parent)) {
144                 event.file?.let { schemeCreatedExternally(it) }
145               }
146             }
147             else if (event.file?.isDirectory ?: false) {
148               val dir = virtualDirectory
149               if (event.file == dir) {
150                 for (file in dir!!.children) {
151                   if (isMy(file)) {
152                     schemeCreatedExternally(file)
153                   }
154                 }
155               }
156             }
157           }
158           is VFileDeleteEvent -> {
159             val oldCurrentScheme = currentScheme
160             if (event.file.isDirectory) {
161               val dir = virtualDirectory
162               if (event.file == dir) {
163                 cachedVirtualDirectory = null
164                 removeExternalizableSchemes()
165               }
166             }
167             else if (isMy(event.file) && isMyDirectory(event.file.parent)) {
168               val scheme = findExternalizableSchemeByFileName(event.file.name) ?: continue@eventLoop
169               removeScheme(scheme)
170               processor.onSchemeDeleted(scheme)
171             }
172
173             updateCurrentScheme(oldCurrentScheme)
174           }
175         }
176       }
177     }
178
179     private fun schemeCreatedExternally(file: VirtualFile) {
180       val readScheme = readSchemeFromFile(file)
181       if (readScheme != null) {
182         processor.initScheme(readScheme)
183         processor.onSchemeAdded(readScheme)
184       }
185     }
186
187     private fun updateCurrentScheme(oldScheme: T?, newScheme: T? = null) {
188       if (currentScheme != null) {
189         return
190       }
191
192       if (oldScheme != currentScheme) {
193         val scheme = newScheme ?: schemes.firstOrNull()
194         currentPendingSchemeName = null
195         currentScheme = scheme
196         // must be equals by reference
197         if (oldScheme !== scheme) {
198           processor.onCurrentSchemeSwitched(oldScheme, scheme)
199         }
200       }
201       else if (newScheme != null) {
202         processPendingCurrentSchemeName(newScheme)
203       }
204     }
205   }
206
207   private fun refreshVirtualDirectoryAndAddListener() {
208     // store refreshes root directory, so, we don't need to use refreshAndFindFile
209     val directory = LocalFileSystem.getInstance().findFileByPath(ioDirectory.systemIndependentPath) ?: return
210
211     this.cachedVirtualDirectory = directory
212     directory.children
213     if (directory is NewVirtualFile) {
214       directory.markDirty()
215     }
216
217     directory.refresh(true, false)
218   }
219
220   override fun loadBundledScheme(resourceName: String, requestor: Any) {
221     try {
222       val url = if (requestor is AbstractExtensionPointBean)
223         requestor.loaderForClass.getResource(resourceName)
224       else
225         DecodeDefaultsUtil.getDefaults(requestor, resourceName)
226       if (url == null) {
227         LOG.error("Cannot read scheme from $resourceName")
228         return
229       }
230
231       val bytes = URLUtil.openStream(url).readBytes()
232       lazyPreloadScheme(bytes, isUseOldFileNameSanitize) { name, parser ->
233         val attributeProvider = Function<String, String?> { parser.getAttributeValue(null, it) }
234         val fileName = PathUtilRt.getFileName(url.path)
235         val extension = getFileExtension(fileName, true)
236         val externalInfo = ExternalInfo(fileName.substring(0, fileName.length - extension.length), extension)
237
238         val schemeName = name ?: (processor as LazySchemeProcessor).getName(attributeProvider, externalInfo.fileNameWithoutExtension)
239         externalInfo.schemeName = schemeName
240
241         val scheme = (processor as LazySchemeProcessor).createScheme(SchemeDataHolderImpl(bytes, externalInfo), schemeName, attributeProvider, true)
242         val oldInfo = schemeToInfo.put(scheme, externalInfo)
243         LOG.assertTrue(oldInfo == null)
244         val oldScheme = readOnlyExternalizableSchemes.put(scheme.name, scheme)
245         if (oldScheme != null) {
246           LOG.warn("Duplicated scheme ${scheme.name} - old: $oldScheme, new $scheme")
247         }
248         schemes.add(scheme)
249       }
250     }
251     catch (e: Throwable) {
252       LOG.error("Cannot read scheme from $resourceName", e)
253     }
254   }
255
256   private fun getFileExtension(fileName: CharSequence, allowAny: Boolean): String {
257     return if (StringUtilRt.endsWithIgnoreCase(fileName, schemeExtension)) {
258       schemeExtension
259     }
260     else if (StringUtilRt.endsWithIgnoreCase(fileName, DEFAULT_EXT)) {
261       DEFAULT_EXT
262     }
263     else if (allowAny) {
264       PathUtil.getFileExtension(fileName.toString())!!
265     }
266     else {
267       throw IllegalStateException("Scheme file extension $fileName is unknown, must be filtered out")
268     }
269   }
270
271   override fun loadSchemes(): Collection<T> {
272     if (!isLoadingSchemes.compareAndSet(false, true)) {
273       throw IllegalStateException("loadSchemes is already called")
274     }
275
276     try {
277       val filesToDelete = THashSet<String>()
278       val oldSchemes = schemes
279       val schemes = oldSchemes.toMutableList()
280       val newSchemesOffset = schemes.size
281       if (provider != null && provider.isApplicable(fileSpec, roamingType)) {
282         provider.processChildren(fileSpec, roamingType, { canRead(it) }) { name, input, readOnly ->
283           catchAndLog(name) {
284             val scheme = loadScheme(name, input, schemes, filesToDelete)
285             if (readOnly && scheme != null) {
286               readOnlyExternalizableSchemes.put(scheme.name, scheme)
287             }
288           }
289           true
290         }
291       }
292       else {
293         ioDirectory.directoryStreamIfExists({ canRead(it.fileName.toString()) }) {
294           for (file in it) {
295             if (file.isDirectory()) {
296               continue
297             }
298
299             catchAndLog(file.fileName.toString()) { filename ->
300               file.inputStream().use { loadScheme(filename, it, schemes, filesToDelete) }
301             }
302           }
303         }
304       }
305
306       this.filesToDelete.addAll(filesToDelete)
307       replaceSchemeList(oldSchemes, schemes)
308
309       @Suppress("UNCHECKED_CAST")
310       for (i in newSchemesOffset..schemes.size - 1) {
311         val scheme = schemes.get(i) as MUTABLE_SCHEME
312         processor.initScheme(scheme)
313         @Suppress("UNCHECKED_CAST")
314         processPendingCurrentSchemeName(scheme)
315       }
316
317       messageBus?.connect()?.subscribe(VirtualFileManager.VFS_CHANGES, SchemeFileTracker())
318
319       return schemes.subList(newSchemesOffset, schemes.size)
320     }
321     finally {
322       isLoadingSchemes.set(false)
323     }
324   }
325
326   private fun replaceSchemeList(oldList: ConcurrentList<T>, newList: List<T>) {
327     if (!schemesRef.compareAndSet(oldList, ContainerUtil.createLockFreeCopyOnWriteList(newList) as ConcurrentList<T>)) {
328       throw IllegalStateException("Scheme list was modified")
329     }
330   }
331
332   override fun reload() {
333     // we must not remove non-persistent (e.g. predefined) schemes, because we cannot load it (obviously)
334     removeExternalizableSchemes()
335
336     loadSchemes()
337
338     (processor as? LazySchemeProcessor)?.reloaded()
339   }
340
341   private fun removeExternalizableSchemes() {
342     // todo check is bundled/read-only schemes correctly handled
343     val iterator = schemes.iterator()
344     for (scheme in iterator) {
345       if (processor.getState(scheme) == SchemeState.NON_PERSISTENT) {
346         continue
347       }
348
349       currentScheme?.let {
350         if (scheme === it) {
351           currentPendingSchemeName = it.name
352           currentScheme = null
353         }
354       }
355
356       iterator.remove()
357
358       @Suppress("UNCHECKED_CAST")
359       processor.onSchemeDeleted(scheme as MUTABLE_SCHEME)
360     }
361     retainExternalInfo()
362   }
363
364   @Suppress("UNCHECKED_CAST")
365   private fun findExternalizableSchemeByFileName(fileName: String) = schemes.firstOrNull { fileName == "${it.fileName}$schemeExtension" } as MUTABLE_SCHEME?
366
367   private fun isOverwriteOnLoad(existingScheme: T): Boolean {
368     val info = schemeToInfo.get(existingScheme)
369     // scheme from file with old extension, so, we must ignore it
370     return info != null && schemeExtension != info.fileExtension
371   }
372
373   private inner class SchemeDataHolderImpl(private val bytes: ByteArray, private val externalInfo: ExternalInfo) : SchemeDataHolder<MUTABLE_SCHEME> {
374     override fun read(): Element = loadElement(bytes.inputStream())
375
376     override fun updateDigest(scheme: MUTABLE_SCHEME) {
377       try {
378         updateDigest(processor.writeScheme(scheme) as Element)
379       }
380       catch (e: WriteExternalException) {
381         LOG.error("Cannot update digest", e)
382       }
383     }
384
385     override fun updateDigest(data: Element) {
386       externalInfo.digest = data.digest()
387     }
388   }
389
390   private fun loadScheme(fileName: String, input: InputStream, schemes: MutableList<T>, filesToDelete: MutableSet<String>? = null): MUTABLE_SCHEME? {
391     val extension = getFileExtension(fileName, false)
392     if (filesToDelete != null && filesToDelete.contains(fileName)) {
393       LOG.warn("Scheme file \"$fileName\" is not loaded because marked to delete")
394       return null
395     }
396
397     val fileNameWithoutExtension = fileName.substring(0, fileName.length - extension.length)
398     fun checkExisting(schemeName: String): Boolean {
399       if (filesToDelete == null) {
400         return true
401       }
402
403       schemes.firstOrNull({ it.name == schemeName})?.let { existingScheme ->
404         if (readOnlyExternalizableSchemes.get(existingScheme.name) === existingScheme) {
405           // so, bundled scheme is shadowed
406           removeFirstScheme(schemes, scheduleDelete = false) { it === existingScheme }
407           return true
408         }
409         else if (processor.isExternalizable(existingScheme) && isOverwriteOnLoad(existingScheme)) {
410           removeFirstScheme(schemes) { it === existingScheme }
411         }
412         else {
413           if (schemeExtension != extension && schemeToInfo.get(existingScheme as Scheme)?.fileNameWithoutExtension == fileNameWithoutExtension) {
414             // 1.oldExt is loading after 1.newExt - we should delete 1.oldExt
415             filesToDelete.add(fileName)
416           }
417           else {
418             // We don't load scheme with duplicated name - if we generate unique name for it, it will be saved then with new name.
419             // It is not what all can expect. Such situation in most cases indicates error on previous level, so, we just warn about it.
420             LOG.warn("Scheme file \"$fileName\" is not loaded because defines duplicated name \"$schemeName\"")
421           }
422           return false
423         }
424       }
425
426       return true
427     }
428
429     fun createInfo(schemeName: String, element: Element?): ExternalInfo {
430       val info = ExternalInfo(fileNameWithoutExtension, extension)
431       element?.let {
432         info.digest = it.digest()
433       }
434       info.schemeName = schemeName
435       return info
436     }
437
438     val duringLoad = filesToDelete != null
439     var scheme: MUTABLE_SCHEME? = null
440     if (processor is LazySchemeProcessor) {
441       val bytes = input.readBytes()
442       lazyPreloadScheme(bytes, isUseOldFileNameSanitize) { name, parser ->
443         val attributeProvider = Function<String, String?> { parser.getAttributeValue(null, it) }
444         val schemeName = name ?: processor.getName(attributeProvider, fileNameWithoutExtension)
445         if (!checkExisting(schemeName)) {
446           return null
447         }
448
449         val externalInfo = createInfo(schemeName, null)
450         scheme = processor.createScheme(SchemeDataHolderImpl(bytes, externalInfo), schemeName, attributeProvider)
451         schemeToInfo.put(scheme, externalInfo)
452         this.filesToDelete.remove(fileName)
453       }
454     }
455     else {
456       val element = loadElement(input)
457       scheme = (processor as NonLazySchemeProcessor).readScheme(element, duringLoad) ?: return null
458       val schemeName = scheme!!.name
459       if (!checkExisting(schemeName)) {
460         return null
461       }
462
463       schemeToInfo.put(scheme, createInfo(schemeName, element))
464       this.filesToDelete.remove(fileName)
465     }
466
467     @Suppress("UNCHECKED_CAST")
468     if (duringLoad) {
469       schemes.add(scheme as T)
470     }
471     else {
472       addNewScheme(scheme as T, true)
473     }
474     return scheme
475   }
476
477   private val T.fileName: String?
478     get() = schemeToInfo.get(this)?.fileNameWithoutExtension
479
480   fun canRead(name: CharSequence) = (updateExtension && name.endsWith(DEFAULT_EXT, true) || name.endsWith(schemeExtension, true)) && (processor !is LazySchemeProcessor || processor.isSchemeFile(name))
481
482   private fun readSchemeFromFile(file: VirtualFile, schemes: MutableList<T> = this.schemes): MUTABLE_SCHEME? {
483     val fileName = file.name
484     if (file.isDirectory || !canRead(fileName)) {
485       return null
486     }
487
488     catchAndLog(fileName) {
489       return file.inputStream.use { loadScheme(fileName, it, schemes) }
490     }
491
492     return null
493   }
494
495   fun save(errors: MutableList<Throwable>) {
496     if (isLoadingSchemes.get()) {
497       LOG.warn("Skip save - schemes are loading")
498     }
499
500     var hasSchemes = false
501     val nameGenerator = UniqueNameGenerator()
502     val schemesToSave = SmartList<MUTABLE_SCHEME>()
503     for (scheme in schemes) {
504       val state = processor.getState(scheme)
505       if (state == SchemeState.NON_PERSISTENT) {
506         continue
507       }
508
509       hasSchemes = true
510
511       if (state != SchemeState.UNCHANGED) {
512         @Suppress("UNCHECKED_CAST")
513         schemesToSave.add(scheme as MUTABLE_SCHEME)
514       }
515
516       val fileName = scheme.fileName
517       if (fileName != null && !isRenamed(scheme)) {
518         nameGenerator.addExistingName(fileName)
519       }
520     }
521
522     for (scheme in schemesToSave) {
523       try {
524         saveScheme(scheme, nameGenerator)
525       }
526       catch (e: Throwable) {
527         errors.add(RuntimeException("Cannot save scheme $fileSpec/$scheme", e))
528       }
529     }
530
531     val filesToDelete = THashSet(filesToDelete)
532     if (!filesToDelete.isEmpty) {
533       this.filesToDelete.removeAll(filesToDelete)
534       deleteFiles(errors, filesToDelete)
535       // remove empty directory only if some file was deleted - avoid check on each save
536       if (!hasSchemes && (provider == null || !provider.isApplicable(fileSpec, roamingType))) {
537         removeDirectoryIfEmpty(errors)
538       }
539     }
540   }
541
542   private fun removeDirectoryIfEmpty(errors: MutableList<Throwable>) {
543     ioDirectory.directoryStreamIfExists {
544       for (file in it) {
545         if (!file.isHidden()) {
546           LOG.info("Directory ${ioDirectory.fileName} is not deleted: at least one file ${file.fileName} exists")
547           return@removeDirectoryIfEmpty
548         }
549       }
550     }
551
552     LOG.info("Remove schemes directory ${ioDirectory.fileName}")
553     cachedVirtualDirectory = null
554
555     var deleteUsingIo = !useVfs
556     if (!deleteUsingIo) {
557       virtualDirectory?.let {
558         runWriteAction {
559           try {
560             it.delete(this)
561           }
562           catch (e: IOException) {
563             deleteUsingIo = true
564             errors.add(e)
565           }
566         }
567       }
568     }
569
570     if (deleteUsingIo) {
571       errors.catch { ioDirectory.delete() }
572     }
573   }
574
575   private fun saveScheme(scheme: MUTABLE_SCHEME, nameGenerator: UniqueNameGenerator) {
576     var externalInfo: ExternalInfo? = schemeToInfo.get(scheme)
577     val currentFileNameWithoutExtension = externalInfo?.fileNameWithoutExtension
578     val parent = processor.writeScheme(scheme)
579     val element = parent as? Element ?: (parent as Document).detachRootElement()
580     if (element.isEmpty()) {
581       externalInfo?.scheduleDelete()
582       return
583     }
584
585     var fileNameWithoutExtension = currentFileNameWithoutExtension
586     if (fileNameWithoutExtension == null || isRenamed(scheme)) {
587       fileNameWithoutExtension = nameGenerator.generateUniqueName(FileUtil.sanitizeFileName(scheme.name, isUseOldFileNameSanitize))
588     }
589
590     val newDigest = element!!.digest()
591     if (externalInfo != null && currentFileNameWithoutExtension === fileNameWithoutExtension && externalInfo.isDigestEquals(newDigest)) {
592       return
593     }
594
595     // save only if scheme differs from bundled
596     if (isEqualToBundledScheme(externalInfo, newDigest, scheme)) {
597       return
598     }
599
600     // we must check it only here to avoid delete old scheme just because it is empty (old idea save -> new idea delete on open)
601     if (processor is LazySchemeProcessor && processor.isSchemeDefault(scheme, newDigest)) {
602       externalInfo?.scheduleDelete()
603       return
604     }
605
606     val fileName = fileNameWithoutExtension!! + schemeExtension
607     // file will be overwritten, so, we don't need to delete it
608     filesToDelete.remove(fileName)
609
610     // stream provider always use LF separator
611     val byteOut = element.toBufferExposingByteArray()
612
613     var providerPath: String?
614     if (provider != null && provider.enabled) {
615       providerPath = "$fileSpec/$fileName"
616       if (!provider.isApplicable(providerPath, roamingType)) {
617         providerPath = null
618       }
619     }
620     else {
621       providerPath = null
622     }
623
624     // if another new scheme uses old name of this scheme, so, we must not delete it (as part of rename operation)
625     val renamed = externalInfo != null && fileNameWithoutExtension !== currentFileNameWithoutExtension && nameGenerator.value(currentFileNameWithoutExtension)
626     if (providerPath == null) {
627       if (useVfs) {
628         var file: VirtualFile? = null
629         var dir = virtualDirectory
630         if (dir == null || !dir.isValid) {
631           dir = createDir(ioDirectory, this)
632           cachedVirtualDirectory = dir
633         }
634
635         if (renamed) {
636           file = dir.findChild(externalInfo!!.fileName)
637           if (file != null) {
638             runWriteAction {
639               file!!.rename(this, fileName)
640             }
641           }
642         }
643
644         if (file == null) {
645           file = dir.getOrCreateChild(fileName, this)
646         }
647
648         runWriteAction {
649           file!!.getOutputStream(this).use {
650             byteOut.writeTo(it)
651           }
652         }
653       }
654       else {
655         if (renamed) {
656           externalInfo!!.scheduleDelete()
657         }
658         ioDirectory.resolve(fileName).write(byteOut.internalBuffer, 0, byteOut.size())
659       }
660     }
661     else {
662       if (renamed) {
663         externalInfo!!.scheduleDelete()
664       }
665       provider!!.write(providerPath, byteOut.internalBuffer, byteOut.size(), roamingType)
666     }
667
668     if (externalInfo == null) {
669       externalInfo = ExternalInfo(fileNameWithoutExtension, schemeExtension)
670       schemeToInfo.put(scheme, externalInfo)
671     }
672     else {
673       externalInfo.setFileNameWithoutExtension(fileNameWithoutExtension, schemeExtension)
674     }
675     externalInfo.digest = newDigest
676     externalInfo.schemeName = scheme.name
677   }
678
679   private fun isEqualToBundledScheme(externalInfo: ExternalInfo?, newDigest: ByteArray, scheme: MUTABLE_SCHEME): Boolean {
680     fun serializeIfPossible(scheme: T): Element? {
681       LOG.catchAndLog {
682         @Suppress("UNCHECKED_CAST")
683         val bundledAsMutable = scheme as? MUTABLE_SCHEME ?: return null
684         return processor.writeScheme(bundledAsMutable) as Element
685       }
686       return null
687     }
688
689     val bundledScheme = readOnlyExternalizableSchemes.get(scheme.name)
690     if (bundledScheme == null) {
691       if ((processor as? LazySchemeProcessor)?.isSchemeEqualToBundled(scheme) ?: false) {
692         externalInfo?.scheduleDelete()
693         return true
694       }
695       return false
696     }
697
698     val bundledExternalInfo = schemeToInfo.get(bundledScheme) ?: return false
699     if (bundledExternalInfo.digest == null) {
700       serializeIfPossible(bundledScheme)?.let {
701         bundledExternalInfo.digest = it.digest()
702       } ?: return false
703     }
704     if (bundledExternalInfo.isDigestEquals(newDigest)) {
705       externalInfo?.scheduleDelete()
706       return true
707     }
708     return false
709   }
710
711   private fun ExternalInfo.scheduleDelete() {
712     filesToDelete.add(fileName)
713   }
714
715   private fun isRenamed(scheme: T): Boolean {
716     val info = schemeToInfo.get(scheme)
717     return info != null && scheme.name != info.schemeName
718   }
719
720   private fun deleteFiles(errors: MutableList<Throwable>, filesToDelete: MutableSet<String>) {
721     if (provider != null && provider.enabled) {
722       val iterator = filesToDelete.iterator()
723       for (name in iterator) {
724         errors.catch {
725           val spec = "$fileSpec/$name"
726           if (provider.isApplicable(spec, roamingType)) {
727             iterator.remove()
728             provider.delete(spec, roamingType)
729           }
730         }
731       }
732     }
733
734     if (filesToDelete.isEmpty()) {
735       return
736     }
737
738     if (useVfs) {
739       virtualDirectory?.let {
740         val childrenToDelete = it.children.filter { filesToDelete.contains(it.name) }
741         if (childrenToDelete.isNotEmpty()) {
742           runWriteAction {
743             childrenToDelete.forEach { file ->
744               errors.catch { file.delete(this) }
745             }
746           }
747         }
748         return
749       }
750     }
751
752     for (name in filesToDelete) {
753       errors.catch { ioDirectory.resolve(name).delete() }
754     }
755   }
756
757   private val virtualDirectory: VirtualFile?
758     get() {
759       var result = cachedVirtualDirectory
760       if (result == null) {
761         result = LocalFileSystem.getInstance().findFileByPath(ioDirectory.systemIndependentPath)
762         cachedVirtualDirectory = result
763       }
764       return result
765     }
766
767   override fun getRootDirectory(): File = ioDirectory.toFile()
768
769   override fun setSchemes(newSchemes: List<T>, newCurrentScheme: T?, removeCondition: Condition<T>?) {
770     if (removeCondition == null) {
771       schemes.clear()
772     }
773     else {
774       val iterator = schemes.iterator()
775       for (scheme in iterator) {
776         if (removeCondition.value(scheme)) {
777           iterator.remove()
778         }
779       }
780     }
781
782     schemes.addAll(newSchemes)
783
784     val oldCurrentScheme = currentScheme
785     retainExternalInfo()
786
787     if (oldCurrentScheme != newCurrentScheme) {
788       val newScheme: T?
789       if (newCurrentScheme != null) {
790         currentScheme = newCurrentScheme
791         newScheme = newCurrentScheme
792       }
793       else if (oldCurrentScheme != null && !schemes.contains(oldCurrentScheme)) {
794         newScheme = schemes.firstOrNull()
795         currentScheme = newScheme
796       }
797       else {
798         newScheme = null
799       }
800
801       if (oldCurrentScheme != newScheme) {
802         processor.onCurrentSchemeSwitched(oldCurrentScheme, newScheme)
803       }
804     }
805   }
806
807   private fun retainExternalInfo() {
808     if (schemeToInfo.isEmpty()) {
809       return
810     }
811
812     val iterator = schemeToInfo.entries.iterator()
813     l@ for ((scheme, info) in iterator) {
814       if (readOnlyExternalizableSchemes.get(scheme.name) == scheme) {
815         continue
816       }
817
818       for (s in schemes) {
819         if (s === scheme) {
820           filesToDelete.remove(info.fileName)
821           continue@l
822         }
823       }
824
825       iterator.remove()
826       info.scheduleDelete()
827     }
828   }
829
830   override fun addNewScheme(scheme: T, replaceExisting: Boolean) {
831     var toReplace = -1
832     val schemes = schemes
833     for (i in schemes.indices) {
834       val existing = schemes.get(i)
835       if (existing.name == scheme.name) {
836         if (existing.javaClass != scheme.javaClass) {
837           LOG.warn("'${scheme.name}' ${existing.javaClass.simpleName} replaced with ${scheme.javaClass.simpleName}")
838         }
839
840         toReplace = i
841         if (replaceExisting && processor.isExternalizable(existing)) {
842           val oldInfo = schemeToInfo.remove(existing)
843           if (oldInfo != null && processor.isExternalizable(scheme) && !schemeToInfo.containsKey(scheme)) {
844             schemeToInfo.put(scheme, oldInfo)
845           }
846         }
847         break
848       }
849     }
850     if (toReplace == -1) {
851       schemes.add(scheme)
852     }
853     else if (replaceExisting || !processor.isExternalizable(scheme)) {
854       schemes.set(toReplace, scheme)
855     }
856     else {
857       (scheme as ExternalizableScheme).renameScheme(UniqueNameGenerator.generateUniqueName(scheme.name, collectExistingNames(schemes)))
858       schemes.add(scheme)
859     }
860
861     if (processor.isExternalizable(scheme) && filesToDelete.isNotEmpty()) {
862       schemeToInfo.get(scheme)?.let {
863         filesToDelete.remove(it.fileName)
864       }
865     }
866
867     processPendingCurrentSchemeName(scheme)
868   }
869
870   private fun collectExistingNames(schemes: Collection<T>): Collection<String> {
871     val result = THashSet<String>(schemes.size)
872     schemes.mapTo(result) { it.name }
873     return result
874   }
875
876   override fun clearAllSchemes() {
877     for (it in schemeToInfo.values) {
878       it.scheduleDelete()
879     }
880
881     currentScheme = null
882     schemes.clear()
883     schemeToInfo.clear()
884   }
885
886   override fun getAllSchemes(): List<T> = Collections.unmodifiableList(schemes)
887
888   override fun isEmpty() = schemes.isEmpty()
889
890   override fun findSchemeByName(schemeName: String) = schemes.firstOrNull { it.name == schemeName }
891
892   override fun setCurrent(scheme: T?, notify: Boolean) {
893     currentPendingSchemeName = null
894
895     val oldCurrent = currentScheme
896     currentScheme = scheme
897     if (notify && oldCurrent !== scheme) {
898       processor.onCurrentSchemeSwitched(oldCurrent, scheme)
899     }
900   }
901
902   override fun setCurrentSchemeName(schemeName: String?, notify: Boolean) {
903     currentPendingSchemeName = schemeName
904
905     val scheme = if (schemeName == null) null else findSchemeByName(schemeName)
906     // don't set current scheme if no scheme by name - pending resolution (see currentSchemeName field comment)
907     if (scheme != null || schemeName == null) {
908       setCurrent(scheme, notify)
909     }
910   }
911
912   override fun getCurrentScheme() = currentScheme
913
914   override fun getCurrentSchemeName() = currentScheme?.name ?: currentPendingSchemeName
915
916   private fun processPendingCurrentSchemeName(newScheme: T) {
917     if (newScheme.name == currentPendingSchemeName) {
918       setCurrent(newScheme, false)
919     }
920   }
921
922   override fun removeScheme(schemeName: String) = removeFirstScheme(schemes) {it.name == schemeName}
923
924   override fun removeScheme(scheme: T) = removeFirstScheme(schemes) { it == scheme } != null
925
926   private fun removeFirstScheme(schemes: MutableList<T>, scheduleDelete: Boolean = true, condition: (T) -> Boolean): T? {
927     val iterator = schemes.iterator()
928     for (scheme in iterator) {
929       if (!condition(scheme)) {
930         continue
931       }
932
933       if (currentScheme === scheme) {
934         currentScheme = null
935       }
936
937       iterator.remove()
938
939       if (scheduleDelete && processor.isExternalizable(scheme)) {
940         schemeToInfo.remove(scheme)?.scheduleDelete()
941       }
942       return scheme
943     }
944
945     return null
946   }
947
948   override fun getAllSchemeNames() = schemes.let { if (it.isEmpty()) emptyList() else it.map { it.name } }
949
950   override fun isMetadataEditable(scheme: T) = !readOnlyExternalizableSchemes.containsKey(scheme.name)
951
952   override fun toString() = fileSpec
953 }
954
955 private fun ExternalizableScheme.renameScheme(newName: String) {
956   if (newName != name) {
957     name = newName
958     LOG.assertTrue(newName == name)
959   }
960 }
961
962 private inline fun catchAndLog(fileName: String, runnable: (fileName: String) -> Unit) {
963   try {
964     runnable(fileName)
965   }
966   catch (e: Throwable) {
967     LOG.error("Cannot read scheme $fileName", e)
968   }
969 }