2 * Copyright 2000-2015 JetBrains s.r.o.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 package com.intellij.configurationStore
18 import com.intellij.openapi.application.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
53 import java.io.IOException
54 import java.io.InputStream
55 import java.nio.file.Path
57 import java.util.concurrent.atomic.AtomicBoolean
58 import java.util.concurrent.atomic.AtomicReference
59 import java.util.function.Function
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()
71 private val schemesRef = AtomicReference(ContainerUtil.createLockFreeCopyOnWriteList<T>() as ConcurrentList<T>)
73 private val schemes: ConcurrentList<T>
74 get() = schemesRef.get()
76 private val readOnlyExternalizableSchemes = ContainerUtil.newConcurrentMap<String, T>()
79 * Schemes can be lazy loaded, so, client should be able to set current scheme by name, not only by instance.
81 private @Volatile var currentPendingSchemeName: String? = null
83 private var currentScheme: T? = null
85 private var cachedVirtualDirectory: VirtualFile? = null
87 private val schemeExtension: String
88 private val updateExtension: Boolean
90 private val filesToDelete = ContainerUtil.newConcurrentSet<String>()
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())
95 private val useVfs = messageBus != null
98 if (processor is SchemeExtensionProvider) {
99 schemeExtension = processor.schemeExtension
100 updateExtension = true
103 schemeExtension = FileStorageCoreUtil.DEFAULT_EXT
104 updateExtension = false
107 if (useVfs && (provider == null || !provider.isApplicable(fileSpec, roamingType))) {
108 LOG.catchAndLog { refreshVirtualDirectoryAndAddListener() }
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))
116 override fun after(events: MutableList<out VFileEvent>) {
117 eventLoop@ for (event in events) {
118 if (event.requestor is SchemeManagerImpl<*, *>) {
122 fun isMyDirectory(parent: VirtualFile) = cachedVirtualDirectory.let { if (it == null) ioDirectory.systemIndependentPath == parent.path else it == parent }
125 is VFileContentChangeEvent -> {
126 if (!isMy(event.file) || !isMyDirectory(event.file.parent)) {
130 val oldCurrentScheme = currentScheme
131 findExternalizableSchemeByFileName(event.file.name)?.let {
133 processor.onSchemeDeleted(it)
136 updateCurrentScheme(oldCurrentScheme, readSchemeFromFile(event.file)?.let {
137 processor.initScheme(it)
138 processor.onSchemeAdded(it)
143 is VFileCreateEvent -> {
144 if (isMy(event.childName)) {
145 if (isMyDirectory(event.parent)) {
146 event.file?.let { schemeCreatedExternally(it) }
149 else if (event.file?.isDirectory ?: false) {
150 val dir = virtualDirectory
151 if (event.file == dir) {
152 for (file in dir!!.children) {
154 schemeCreatedExternally(file)
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()
169 else if (isMy(event.file) && isMyDirectory(event.file.parent)) {
170 val scheme = findExternalizableSchemeByFileName(event.file.name) ?: continue@eventLoop
172 processor.onSchemeDeleted(scheme)
175 updateCurrentScheme(oldCurrentScheme)
181 private fun schemeCreatedExternally(file: VirtualFile) {
182 val readScheme = readSchemeFromFile(file)
183 if (readScheme != null) {
184 processor.initScheme(readScheme)
185 processor.onSchemeAdded(readScheme)
189 private fun updateCurrentScheme(oldScheme: T?, newScheme: T? = null) {
190 if (currentScheme != null) {
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)
203 else if (newScheme != null) {
204 processPendingCurrentSchemeName(newScheme)
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
213 this.cachedVirtualDirectory = directory
215 if (directory is NewVirtualFile) {
216 directory.markDirty()
219 directory.refresh(true, false)
222 override fun loadBundledScheme(resourceName: String, requestor: Any) {
224 val url = if (requestor is AbstractExtensionPointBean)
225 requestor.loaderForClass.getResource(resourceName)
227 DecodeDefaultsUtil.getDefaults(requestor, resourceName)
229 LOG.error("Cannot read scheme from $resourceName")
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)
240 val schemeName = name ?: (processor as LazySchemeProcessor).getName(attributeProvider, externalInfo.fileNameWithoutExtension)
241 externalInfo.schemeName = schemeName
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")
253 catch (e: Throwable) {
254 LOG.error("Cannot read scheme from $resourceName", e)
258 private fun getFileExtension(fileName: CharSequence, allowAny: Boolean): String {
259 return if (StringUtilRt.endsWithIgnoreCase(fileName, schemeExtension)) {
262 else if (StringUtilRt.endsWithIgnoreCase(fileName, DEFAULT_EXT)) {
266 PathUtil.getFileExtension(fileName.toString())!!
269 throw IllegalStateException("Scheme file extension $fileName is unknown, must be filtered out")
273 override fun loadSchemes(): Collection<T> {
274 if (!isLoadingSchemes.compareAndSet(false, true)) {
275 throw IllegalStateException("loadSchemes is already called")
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 ->
286 val scheme = loadScheme(name, input, schemes, filesToDelete)
287 if (readOnly && scheme != null) {
288 readOnlyExternalizableSchemes.put(scheme.name, scheme)
295 ioDirectory.directoryStreamIfExists({ canRead(it.fileName.toString()) }) {
297 if (file.isDirectory()) {
301 catchAndLog(file.fileName.toString()) { filename ->
302 file.inputStream().use { loadScheme(filename, it, schemes, filesToDelete) }
308 this.filesToDelete.addAll(filesToDelete)
309 replaceSchemeList(oldSchemes, schemes)
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)
319 messageBus?.connect()?.subscribe(VirtualFileManager.VFS_CHANGES, SchemeFileTracker())
321 return schemes.subList(newSchemesOffset, schemes.size)
324 isLoadingSchemes.set(false)
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")
334 override fun reload() {
335 // we must not remove non-persistent (e.g. predefined) schemes, because we cannot load it (obviously)
336 removeExternalizableSchemes()
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) {
349 if (scheme === currentScheme) {
355 @Suppress("UNCHECKED_CAST")
356 processor.onSchemeDeleted(scheme as MUTABLE_SCHEME)
361 @Suppress("UNCHECKED_CAST")
362 private fun findExternalizableSchemeByFileName(fileName: String) = schemes.firstOrNull { fileName == "${it.fileName}$schemeExtension" } as MUTABLE_SCHEME?
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
370 private inner class SchemeDataHolderImpl(private val bytes: ByteArray, private val externalInfo: ExternalInfo) : SchemeDataHolder<MUTABLE_SCHEME> {
371 override fun read(): Element = loadElement(bytes.inputStream())
373 override fun updateDigest(scheme: MUTABLE_SCHEME) {
375 updateDigest(processor.writeScheme(scheme) as Element)
377 catch (e: WriteExternalException) {
378 LOG.error("Cannot update digest", e)
382 override fun updateDigest(data: Element) {
383 externalInfo.digest = data.digest()
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")
394 val fileNameWithoutExtension = fileName.substring(0, fileName.length - extension.length)
395 fun checkExisting(schemeName: String): Boolean {
396 if (filesToDelete == null) {
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)
406 else if (processor.isExternalizable(existingScheme) && isOverwriteOnLoad(existingScheme)) {
407 removeFirstScheme({ it === existingScheme }, schemes)
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)
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\"")
426 fun createInfo(schemeName: String, element: Element?): ExternalInfo {
427 val info = ExternalInfo(fileNameWithoutExtension, extension)
429 info.digest = it.digest()
431 info.schemeName = schemeName
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)) {
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)
453 val element = loadElement(input)
454 scheme = (processor as NonLazySchemeProcessor).readScheme(element, duringLoad) ?: return null
455 val schemeName = scheme!!.name
456 if (!checkExisting(schemeName)) {
460 schemeToInfo.put(scheme, createInfo(schemeName, element))
461 this.filesToDelete.remove(fileName)
464 @Suppress("UNCHECKED_CAST")
466 schemes.add(scheme as T)
469 addNewScheme(scheme as T, true)
474 private val T.fileName: String?
475 get() = schemeToInfo.get(this)?.fileNameWithoutExtension
477 fun canRead(name: CharSequence) = (updateExtension && name.endsWith(DEFAULT_EXT, true) || name.endsWith(schemeExtension, true)) && (processor !is LazySchemeProcessor || processor.isSchemeFile(name))
479 private fun readSchemeFromFile(file: VirtualFile, schemes: MutableList<T> = this.schemes): MUTABLE_SCHEME? {
480 val fileName = file.name
481 if (file.isDirectory || !canRead(fileName)) {
485 catchAndLog(fileName) {
486 return file.inputStream.use { loadScheme(fileName, it, schemes) }
492 fun save(errors: MutableList<Throwable>) {
493 if (isLoadingSchemes.get()) {
494 LOG.warn("Skip save - schemes are loading")
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) {
508 if (state != SchemeState.UNCHANGED) {
509 @Suppress("UNCHECKED_CAST")
510 schemesToSave.add(scheme as MUTABLE_SCHEME)
513 val fileName = scheme.fileName
514 if (fileName != null && !isRenamed(scheme)) {
515 nameGenerator.addExistingName(fileName)
519 for (scheme in schemesToSave) {
521 saveScheme(scheme, nameGenerator)
523 catch (e: Throwable) {
524 errors.add(RuntimeException("Cannot save scheme $fileSpec/$scheme", e))
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)
539 private fun removeDirectoryIfEmpty(errors: MutableList<Throwable>) {
540 ioDirectory.directoryStreamIfExists {
542 if (!file.isHidden()) {
543 LOG.info("Directory ${ioDirectory.fileName} is not deleted: at least one file ${file.fileName} exists")
544 return@removeDirectoryIfEmpty
549 LOG.info("Remove schemes directory ${ioDirectory.fileName}")
550 cachedVirtualDirectory = null
552 var deleteUsingIo = !useVfs
553 if (!deleteUsingIo) {
554 virtualDirectory?.let {
559 catch (e: IOException) {
568 errors.catch { ioDirectory.delete() }
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()
582 var fileNameWithoutExtension = currentFileNameWithoutExtension
583 if (fileNameWithoutExtension == null || isRenamed(scheme)) {
584 fileNameWithoutExtension = nameGenerator.generateUniqueName(FileUtil.sanitizeFileName(scheme.name, isUseOldFileNameSanitize))
587 val newDigest = element!!.digest()
588 if (externalInfo != null && currentFileNameWithoutExtension === fileNameWithoutExtension && externalInfo.isDigestEquals(newDigest)) {
592 // save only if scheme differs from bundled
593 if (isEqualToBundledScheme(externalInfo, newDigest, scheme)) {
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()
603 val fileName = fileNameWithoutExtension!! + schemeExtension
604 // file will be overwritten, so, we don't need to delete it
605 filesToDelete.remove(fileName)
607 // stream provider always use LF separator
608 val byteOut = element.toBufferExposingByteArray()
610 var providerPath: String?
611 if (provider != null && provider.enabled) {
612 providerPath = "$fileSpec/$fileName"
613 if (!provider.isApplicable(providerPath, roamingType)) {
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) {
625 var file: VirtualFile? = null
626 var dir = virtualDirectory
627 if (dir == null || !dir.isValid) {
628 dir = createDir(ioDirectory, this)
629 cachedVirtualDirectory = dir
633 file = dir.findChild(externalInfo!!.fileName)
636 file!!.rename(this, fileName)
642 file = dir.getOrCreateChild(fileName, this)
646 file!!.getOutputStream(this).use {
653 externalInfo!!.scheduleDelete()
655 ioDirectory.resolve(fileName).write(byteOut.internalBuffer, 0, byteOut.size())
660 externalInfo!!.scheduleDelete()
662 provider!!.write(providerPath, byteOut.internalBuffer, byteOut.size(), roamingType)
665 if (externalInfo == null) {
666 externalInfo = ExternalInfo(fileNameWithoutExtension, schemeExtension)
667 schemeToInfo.put(scheme, externalInfo)
670 externalInfo.setFileNameWithoutExtension(fileNameWithoutExtension, schemeExtension)
672 externalInfo.digest = newDigest
673 externalInfo.schemeName = scheme.name
676 private fun isEqualToBundledScheme(externalInfo: ExternalInfo?, newDigest: ByteArray, scheme: MUTABLE_SCHEME): Boolean {
677 fun serializeIfPossible(scheme: T): Element? {
679 @Suppress("UNCHECKED_CAST")
680 val bundledAsMutable = scheme as? MUTABLE_SCHEME ?: return null
681 return processor.writeScheme(bundledAsMutable) as Element
686 val bundledScheme = readOnlyExternalizableSchemes.get(scheme.name)
687 if (bundledScheme == null) {
688 if ((processor as? LazySchemeProcessor)?.isSchemeEqualToBundled(scheme) ?: false) {
689 externalInfo?.scheduleDelete()
695 val bundledExternalInfo = schemeToInfo.get(bundledScheme) ?: return false
696 if (bundledExternalInfo.digest == null) {
697 serializeIfPossible(bundledScheme)?.let {
698 bundledExternalInfo.digest = it.digest()
701 if (bundledExternalInfo.isDigestEquals(newDigest)) {
702 externalInfo?.scheduleDelete()
708 private fun ExternalInfo.scheduleDelete() {
709 filesToDelete.add(fileName)
712 private fun isRenamed(scheme: T): Boolean {
713 val info = schemeToInfo.get(scheme)
714 return info != null && scheme.name != info.schemeName
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) {
722 val spec = "$fileSpec/$name"
723 if (provider.isApplicable(spec, roamingType)) {
725 provider.delete(spec, roamingType)
731 if (filesToDelete.isEmpty()) {
736 virtualDirectory?.let {
737 var token: AccessToken? = null
739 for (file in it.children) {
740 if (filesToDelete.contains(file.name)) {
742 token = WriteAction.start()
758 for (name in filesToDelete) {
759 errors.catch { ioDirectory.resolve(name).delete() }
763 private val virtualDirectory: VirtualFile?
765 var result = cachedVirtualDirectory
766 if (result == null) {
767 result = LocalFileSystem.getInstance().findFileByPath(ioDirectory.systemIndependentPath)
768 cachedVirtualDirectory = result
773 override fun getRootDirectory(): File = ioDirectory.toFile()
775 override fun setSchemes(newSchemes: List<T>, newCurrentScheme: T?, removeCondition: Condition<T>?) {
776 if (removeCondition == null) {
780 val iterator = schemes.iterator()
781 for (scheme in iterator) {
782 if (removeCondition.value(scheme)) {
788 schemes.addAll(newSchemes)
790 val oldCurrentScheme = currentScheme
793 if (oldCurrentScheme != newCurrentScheme) {
795 if (newCurrentScheme != null) {
796 currentScheme = newCurrentScheme
797 newScheme = newCurrentScheme
799 else if (oldCurrentScheme != null && !schemes.contains(oldCurrentScheme)) {
800 newScheme = schemes.firstOrNull()
801 currentScheme = newScheme
807 if (oldCurrentScheme != newScheme) {
808 processor.onCurrentSchemeSwitched(oldCurrentScheme, newScheme)
813 private fun retainExternalInfo() {
814 if (schemeToInfo.isEmpty()) {
818 val iterator = schemeToInfo.entries.iterator()
819 l@ for ((scheme, info) in iterator) {
820 if (readOnlyExternalizableSchemes.get(scheme.name) == scheme) {
826 filesToDelete.remove(info.fileName)
832 info.scheduleDelete()
836 override fun addNewScheme(scheme: T, replaceExisting: Boolean) {
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}")
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)
856 if (toReplace == -1) {
859 else if (replaceExisting || !processor.isExternalizable(scheme)) {
860 schemes.set(toReplace, scheme)
863 (scheme as ExternalizableScheme).renameScheme(UniqueNameGenerator.generateUniqueName(scheme.name, collectExistingNames(schemes)))
867 if (processor.isExternalizable(scheme) && filesToDelete.isNotEmpty()) {
868 schemeToInfo.get(scheme)?.let {
869 filesToDelete.remove(it.fileName)
873 processPendingCurrentSchemeName(scheme)
876 private fun collectExistingNames(schemes: Collection<T>): Collection<String> {
877 val result = THashSet<String>(schemes.size)
878 schemes.mapTo(result) { it.name }
882 override fun clearAllSchemes() {
883 for (it in schemeToInfo.values) {
892 override fun getAllSchemes(): List<T> = Collections.unmodifiableList(schemes)
894 override fun isEmpty() = schemes.isEmpty()
896 override fun findSchemeByName(schemeName: String) = schemes.firstOrNull { it.name == schemeName }
898 override fun setCurrent(scheme: T?, notify: Boolean) {
899 currentPendingSchemeName = null
901 val oldCurrent = currentScheme
902 currentScheme = scheme
903 if (notify && oldCurrent !== scheme) {
904 processor.onCurrentSchemeSwitched(oldCurrent, scheme)
908 override fun setCurrentSchemeName(schemeName: String?, notify: Boolean) {
909 currentPendingSchemeName = schemeName
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)
918 override fun getCurrentScheme() = currentScheme
920 override fun getCurrentSchemeName() = currentScheme?.name ?: currentPendingSchemeName
922 private fun processPendingCurrentSchemeName(newScheme: T) {
923 if (newScheme.name == currentPendingSchemeName) {
924 setCurrent(newScheme, false)
928 override fun removeScheme(schemeName: String) = removeFirstScheme({it.name == schemeName}, schemes)
930 override fun removeScheme(scheme: T) {
931 removeFirstScheme({ it == scheme }, schemes)
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)) {
941 if (currentScheme === scheme) {
947 if (scheduleDelete && processor.isExternalizable(scheme)) {
948 schemeToInfo.remove(scheme)?.scheduleDelete()
956 override fun getAllSchemeNames() = schemes.let { if (it.isEmpty()) emptyList() else it.map { it.name } }
958 override fun isMetadataEditable(scheme: T) = !readOnlyExternalizableSchemes.containsKey(scheme.name)
960 override fun toString() = fileSpec
963 private fun ExternalizableScheme.renameScheme(newName: String) {
964 if (newName != name) {
966 LOG.assertTrue(newName == name)
970 private inline fun catchAndLog(fileName: String, runnable: (fileName: String) -> Unit) {
974 catch (e: Throwable) {
975 LOG.error("Cannot read scheme $fileName", e)