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