IDEA-CR-15578 final fix `rename A to B and B to A`
[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     when {
592       externalInfo != null && currentFileNameWithoutExtension === fileNameWithoutExtension && externalInfo.isDigestEquals(newDigest) -> return
593       isEqualToBundledScheme(externalInfo, newDigest, scheme) -> return
594
595       // we must check it only here to avoid delete old scheme just because it is empty (old idea save -> new idea delete on open)
596       processor is LazySchemeProcessor && processor.isSchemeDefault(scheme, newDigest) -> {
597         externalInfo?.scheduleDelete()
598         return
599       }
600     }
601
602     val fileName = fileNameWithoutExtension!! + schemeExtension
603     // file will be overwritten, so, we don't need to delete it
604     filesToDelete.remove(fileName)
605
606     // stream provider always use LF separator
607     val byteOut = element.toBufferExposingByteArray()
608
609     var providerPath: String?
610     if (provider != null && provider.enabled) {
611       providerPath = "$fileSpec/$fileName"
612       if (!provider.isApplicable(providerPath, roamingType)) {
613         providerPath = null
614       }
615     }
616     else {
617       providerPath = null
618     }
619
620     // if another new scheme uses old name of this scheme, we must not delete it (as part of rename operation)
621     val renamed = externalInfo != null && fileNameWithoutExtension !== currentFileNameWithoutExtension && nameGenerator.isUnique(currentFileNameWithoutExtension)
622     if (providerPath == null) {
623       if (useVfs) {
624         var file: VirtualFile? = null
625         var dir = virtualDirectory
626         if (dir == null || !dir.isValid) {
627           dir = createDir(ioDirectory, this)
628           cachedVirtualDirectory = dir
629         }
630
631         if (renamed) {
632           val oldFile = dir.findChild(externalInfo!!.fileName)
633           oldFile?.let {
634             // VFS doesn't allow to rename to existing file, so, check it
635             if (dir!!.findChild(fileName) == null) {
636               runWriteAction { it.rename(this, fileName) }
637               file = oldFile
638             }
639             else {
640               externalInfo!!.scheduleDelete()
641             }
642           }
643         }
644
645         if (file == null) {
646           file = dir.getOrCreateChild(fileName, this)
647         }
648
649         runWriteAction {
650           file!!.getOutputStream(this).use { byteOut.writeTo(it) }
651         }
652       }
653       else {
654         if (renamed) {
655           externalInfo!!.scheduleDelete()
656         }
657         ioDirectory.resolve(fileName).write(byteOut.internalBuffer, 0, byteOut.size())
658       }
659     }
660     else {
661       if (renamed) {
662         externalInfo!!.scheduleDelete()
663       }
664       provider!!.write(providerPath, byteOut.internalBuffer, byteOut.size(), roamingType)
665     }
666
667     if (externalInfo == null) {
668       externalInfo = ExternalInfo(fileNameWithoutExtension, schemeExtension)
669       schemeToInfo.put(scheme, externalInfo)
670     }
671     else {
672       externalInfo.setFileNameWithoutExtension(fileNameWithoutExtension, schemeExtension)
673     }
674     externalInfo.digest = newDigest
675     externalInfo.schemeName = scheme.name
676   }
677
678   private fun isEqualToBundledScheme(externalInfo: ExternalInfo?, newDigest: ByteArray, scheme: MUTABLE_SCHEME): Boolean {
679     fun serializeIfPossible(scheme: T): Element? {
680       LOG.catchAndLog {
681         @Suppress("UNCHECKED_CAST")
682         val bundledAsMutable = scheme as? MUTABLE_SCHEME ?: return null
683         return processor.writeScheme(bundledAsMutable) as Element
684       }
685       return null
686     }
687
688     val bundledScheme = readOnlyExternalizableSchemes.get(scheme.name)
689     if (bundledScheme == null) {
690       if ((processor as? LazySchemeProcessor)?.isSchemeEqualToBundled(scheme) ?: false) {
691         externalInfo?.scheduleDelete()
692         return true
693       }
694       return false
695     }
696
697     val bundledExternalInfo = schemeToInfo.get(bundledScheme) ?: return false
698     if (bundledExternalInfo.digest == null) {
699       serializeIfPossible(bundledScheme)?.let {
700         bundledExternalInfo.digest = it.digest()
701       } ?: return false
702     }
703     if (bundledExternalInfo.isDigestEquals(newDigest)) {
704       externalInfo?.scheduleDelete()
705       return true
706     }
707     return false
708   }
709
710   private fun ExternalInfo.scheduleDelete() {
711     filesToDelete.add(fileName)
712   }
713
714   private fun isRenamed(scheme: T): Boolean {
715     val info = schemeToInfo.get(scheme)
716     return info != null && scheme.name != info.schemeName
717   }
718
719   private fun deleteFiles(errors: MutableList<Throwable>, filesToDelete: MutableSet<String>) {
720     if (provider != null && provider.enabled) {
721       val iterator = filesToDelete.iterator()
722       for (name in iterator) {
723         errors.catch {
724           val spec = "$fileSpec/$name"
725           if (provider.isApplicable(spec, roamingType)) {
726             iterator.remove()
727             provider.delete(spec, roamingType)
728           }
729         }
730       }
731     }
732
733     if (filesToDelete.isEmpty()) {
734       return
735     }
736
737     if (useVfs) {
738       virtualDirectory?.let {
739         val childrenToDelete = it.children.filter { filesToDelete.contains(it.name) }
740         if (childrenToDelete.isNotEmpty()) {
741           runWriteAction {
742             childrenToDelete.forEach { file ->
743               errors.catch { file.delete(this) }
744             }
745           }
746         }
747         return
748       }
749     }
750
751     for (name in filesToDelete) {
752       errors.catch { ioDirectory.resolve(name).delete() }
753     }
754   }
755
756   private val virtualDirectory: VirtualFile?
757     get() {
758       var result = cachedVirtualDirectory
759       if (result == null) {
760         result = LocalFileSystem.getInstance().findFileByPath(ioDirectory.systemIndependentPath)
761         cachedVirtualDirectory = result
762       }
763       return result
764     }
765
766   override fun getRootDirectory(): File = ioDirectory.toFile()
767
768   override fun setSchemes(newSchemes: List<T>, newCurrentScheme: T?, removeCondition: Condition<T>?) {
769     if (removeCondition == null) {
770       schemes.clear()
771     }
772     else {
773       val iterator = schemes.iterator()
774       for (scheme in iterator) {
775         if (removeCondition.value(scheme)) {
776           iterator.remove()
777         }
778       }
779     }
780
781     schemes.addAll(newSchemes)
782
783     val oldCurrentScheme = currentScheme
784     retainExternalInfo()
785
786     if (oldCurrentScheme != newCurrentScheme) {
787       val newScheme: T?
788       if (newCurrentScheme != null) {
789         currentScheme = newCurrentScheme
790         newScheme = newCurrentScheme
791       }
792       else if (oldCurrentScheme != null && !schemes.contains(oldCurrentScheme)) {
793         newScheme = schemes.firstOrNull()
794         currentScheme = newScheme
795       }
796       else {
797         newScheme = null
798       }
799
800       if (oldCurrentScheme != newScheme) {
801         processor.onCurrentSchemeSwitched(oldCurrentScheme, newScheme)
802       }
803     }
804   }
805
806   private fun retainExternalInfo() {
807     if (schemeToInfo.isEmpty()) {
808       return
809     }
810
811     val iterator = schemeToInfo.entries.iterator()
812     l@ for ((scheme, info) in iterator) {
813       if (readOnlyExternalizableSchemes.get(scheme.name) == scheme) {
814         continue
815       }
816
817       for (s in schemes) {
818         if (s === scheme) {
819           filesToDelete.remove(info.fileName)
820           continue@l
821         }
822       }
823
824       iterator.remove()
825       info.scheduleDelete()
826     }
827   }
828
829   override fun addNewScheme(scheme: T, replaceExisting: Boolean) {
830     var toReplace = -1
831     val schemes = schemes
832     for (i in schemes.indices) {
833       val existing = schemes.get(i)
834       if (existing.name == scheme.name) {
835         if (existing.javaClass != scheme.javaClass) {
836           LOG.warn("'${scheme.name}' ${existing.javaClass.simpleName} replaced with ${scheme.javaClass.simpleName}")
837         }
838
839         toReplace = i
840         if (replaceExisting && processor.isExternalizable(existing)) {
841           val oldInfo = schemeToInfo.remove(existing)
842           if (oldInfo != null && processor.isExternalizable(scheme) && !schemeToInfo.containsKey(scheme)) {
843             schemeToInfo.put(scheme, oldInfo)
844           }
845         }
846         break
847       }
848     }
849     if (toReplace == -1) {
850       schemes.add(scheme)
851     }
852     else if (replaceExisting || !processor.isExternalizable(scheme)) {
853       schemes.set(toReplace, scheme)
854     }
855     else {
856       (scheme as ExternalizableScheme).renameScheme(UniqueNameGenerator.generateUniqueName(scheme.name, collectExistingNames(schemes)))
857       schemes.add(scheme)
858     }
859
860     if (processor.isExternalizable(scheme) && filesToDelete.isNotEmpty()) {
861       schemeToInfo.get(scheme)?.let {
862         filesToDelete.remove(it.fileName)
863       }
864     }
865
866     processPendingCurrentSchemeName(scheme)
867   }
868
869   private fun collectExistingNames(schemes: Collection<T>): Collection<String> {
870     val result = THashSet<String>(schemes.size)
871     schemes.mapTo(result) { it.name }
872     return result
873   }
874
875   override fun clearAllSchemes() {
876     for (it in schemeToInfo.values) {
877       it.scheduleDelete()
878     }
879
880     currentScheme = null
881     schemes.clear()
882     schemeToInfo.clear()
883   }
884
885   override fun getAllSchemes(): List<T> = Collections.unmodifiableList(schemes)
886
887   override fun isEmpty() = schemes.isEmpty()
888
889   override fun findSchemeByName(schemeName: String) = schemes.firstOrNull { it.name == schemeName }
890
891   override fun setCurrent(scheme: T?, notify: Boolean) {
892     currentPendingSchemeName = null
893
894     val oldCurrent = currentScheme
895     currentScheme = scheme
896     if (notify && oldCurrent !== scheme) {
897       processor.onCurrentSchemeSwitched(oldCurrent, scheme)
898     }
899   }
900
901   override fun setCurrentSchemeName(schemeName: String?, notify: Boolean) {
902     currentPendingSchemeName = schemeName
903
904     val scheme = if (schemeName == null) null else findSchemeByName(schemeName)
905     // don't set current scheme if no scheme by name - pending resolution (see currentSchemeName field comment)
906     if (scheme != null || schemeName == null) {
907       setCurrent(scheme, notify)
908     }
909   }
910
911   override fun getCurrentScheme() = currentScheme
912
913   override fun getCurrentSchemeName() = currentScheme?.name ?: currentPendingSchemeName
914
915   private fun processPendingCurrentSchemeName(newScheme: T) {
916     if (newScheme.name == currentPendingSchemeName) {
917       setCurrent(newScheme, false)
918     }
919   }
920
921   override fun removeScheme(schemeName: String) = removeFirstScheme(schemes) {it.name == schemeName}
922
923   override fun removeScheme(scheme: T) = removeFirstScheme(schemes) { it == scheme } != null
924
925   private fun removeFirstScheme(schemes: MutableList<T>, scheduleDelete: Boolean = true, condition: (T) -> Boolean): T? {
926     val iterator = schemes.iterator()
927     for (scheme in iterator) {
928       if (!condition(scheme)) {
929         continue
930       }
931
932       if (currentScheme === scheme) {
933         currentScheme = null
934       }
935
936       iterator.remove()
937
938       if (scheduleDelete && processor.isExternalizable(scheme)) {
939         schemeToInfo.remove(scheme)?.scheduleDelete()
940       }
941       return scheme
942     }
943
944     return null
945   }
946
947   override fun getAllSchemeNames() = schemes.let { if (it.isEmpty()) emptyList() else it.map { it.name } }
948
949   override fun isMetadataEditable(scheme: T) = !readOnlyExternalizableSchemes.containsKey(scheme.name)
950
951   override fun toString() = fileSpec
952 }
953
954 private fun ExternalizableScheme.renameScheme(newName: String) {
955   if (newName != name) {
956     name = newName
957     LOG.assertTrue(newName == name)
958   }
959 }
960
961 private inline fun catchAndLog(fileName: String, runnable: (fileName: String) -> Unit) {
962   try {
963     runnable(fileName)
964   }
965   catch (e: Throwable) {
966     LOG.error("Cannot read scheme $fileName", e)
967   }
968 }