66ba7676553e6f11fbecdac1f99698bd9a225561
[idea/community.git] / platform / configuration-store-impl / src / ExportSettingsAction.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.ide.actions
17
18 import com.intellij.AbstractBundle
19 import com.intellij.CommonBundle
20 import com.intellij.configurationStore.ROOT_CONFIG
21 import com.intellij.configurationStore.SchemeManagerFactoryBase
22 import com.intellij.configurationStore.path
23 import com.intellij.configurationStore.sortByDeprecated
24 import com.intellij.ide.IdeBundle
25 import com.intellij.ide.plugins.IdeaPluginDescriptor
26 import com.intellij.ide.plugins.PluginManager
27 import com.intellij.ide.plugins.PluginManagerCore
28 import com.intellij.openapi.actionSystem.AnAction
29 import com.intellij.openapi.actionSystem.AnActionEvent
30 import com.intellij.openapi.application.ApplicationManager
31 import com.intellij.openapi.application.PathManager
32 import com.intellij.openapi.application.impl.ApplicationImpl
33 import com.intellij.openapi.components.*
34 import com.intellij.openapi.components.impl.ServiceManagerImpl
35 import com.intellij.openapi.components.impl.stores.StateStorageManager
36 import com.intellij.openapi.components.impl.stores.StoreUtil
37 import com.intellij.openapi.diagnostic.Logger
38 import com.intellij.openapi.extensions.PluginDescriptor
39 import com.intellij.openapi.options.OptionsBundle
40 import com.intellij.openapi.options.SchemesManagerFactory
41 import com.intellij.openapi.project.DumbAware
42 import com.intellij.openapi.ui.Messages
43 import com.intellij.openapi.util.io.FileUtilRt
44 import com.intellij.openapi.vfs.CharsetToolkit
45 import com.intellij.util.*
46 import com.intellij.util.containers.putValue
47 import com.intellij.util.io.ZipUtil
48 import gnu.trove.THashMap
49 import gnu.trove.THashSet
50 import java.io.IOException
51 import java.io.OutputStream
52 import java.io.OutputStreamWriter
53 import java.nio.file.Path
54 import java.nio.file.Paths
55 import java.util.*
56 import java.util.zip.ZipEntry
57 import java.util.zip.ZipOutputStream
58
59 private class ExportSettingsAction : AnAction(), DumbAware {
60   override fun actionPerformed(e: AnActionEvent?) {
61     ApplicationManager.getApplication().saveSettings()
62
63     val dialog = ChooseComponentsToExportDialog(getExportableComponentsMap(true, true), true,
64       IdeBundle.message("title.select.components.to.export"),
65       IdeBundle.message(
66         "prompt.please.check.all.components.to.export"))
67     if (!dialog.showAndGet()) {
68       return
69     }
70
71     val markedComponents = dialog.exportableComponents
72     if (markedComponents.isEmpty()) {
73       return
74     }
75
76     val exportFiles = THashSet<Path>()
77     for (markedComponent in markedComponents) {
78       exportFiles.addAll(markedComponent.files)
79     }
80
81     val saveFile = dialog.exportFile
82     try {
83       if (saveFile.exists() && Messages.showOkCancelDialog(
84         IdeBundle.message("prompt.overwrite.settings.file", saveFile.toString()),
85         IdeBundle.message("title.file.already.exists"), Messages.getWarningIcon()) != Messages.OK) {
86         return
87       }
88
89       exportSettings(exportFiles, saveFile.outputStream(), FileUtilRt.toSystemIndependentName(PathManager.getConfigPath()))
90       ShowFilePathAction.showDialog(AnAction.getEventProject(e), IdeBundle.message("message.settings.exported.successfully"),
91         IdeBundle.message("title.export.successful"), saveFile.toFile(), null)
92     }
93     catch (e1: IOException) {
94       Messages.showErrorDialog(IdeBundle.message("error.writing.settings", e1.toString()), IdeBundle.message("title.error.writing.file"))
95     }
96   }
97 }
98
99 // not internal only to test
100 fun exportSettings(exportFiles: Set<Path>, out: OutputStream, configPath: String) {
101   val zipOut = MyZipOutputStream(out)
102   try {
103     val writtenItemRelativePaths = THashSet<String>()
104     for (file in exportFiles) {
105       if (file.exists()) {
106         val relativePath = FileUtilRt.getRelativePath(configPath, file.toAbsolutePath().systemIndependentPath, '/')!!
107         ZipUtil.addFileOrDirRecursively(zipOut, null, file.toFile(), relativePath, null, writtenItemRelativePaths)
108       }
109     }
110
111     exportInstalledPlugins(zipOut)
112
113     val zipEntry = ZipEntry(ImportSettingsFilenameFilter.SETTINGS_JAR_MARKER)
114     zipOut.putNextEntry(zipEntry)
115     zipOut.closeEntry()
116   }
117   finally {
118     zipOut.doClose()
119   }
120 }
121
122 private class MyZipOutputStream(out: OutputStream) : ZipOutputStream(out) {
123   override fun close() {
124   }
125
126   fun doClose() {
127     super.close()
128   }
129 }
130
131 data class ExportableItem(val files: List<Path>, val presentableName: String, val roamingType: RoamingType = RoamingType.DEFAULT)
132
133 private val LOG = Logger.getInstance(ExportSettingsAction::class.java)
134
135 private fun exportInstalledPlugins(zipOut: MyZipOutputStream) {
136   val plugins = ArrayList<String>()
137   for (descriptor in PluginManagerCore.getPlugins()) {
138     if (!descriptor.isBundled && descriptor.isEnabled) {
139       plugins.add(descriptor.pluginId.idString)
140     }
141   }
142   if (plugins.isEmpty()) {
143     return
144   }
145
146   val e = ZipEntry(PluginManager.INSTALLED_TXT)
147   zipOut.putNextEntry(e)
148   try {
149     PluginManagerCore.writePluginsList(plugins, OutputStreamWriter(zipOut, CharsetToolkit.UTF8_CHARSET))
150   }
151   finally {
152     zipOut.closeEntry()
153   }
154 }
155
156 // onlyPaths - include only specified paths (relative to config dir, ends with "/" if directory)
157 fun getExportableComponentsMap(onlyExisting: Boolean,
158                                computePresentableNames: Boolean,
159                                storageManager: StateStorageManager = ApplicationManager.getApplication().stateStore.stateStorageManager,
160                                onlyPaths: Set<String>? = null): Map<Path, List<ExportableItem>> {
161   val result = LinkedHashMap<Path, MutableList<ExportableItem>>()
162   val processor = { component: ExportableComponent ->
163     val item = ExportableItem(component.exportFiles.map { it.toPath() }, component.presentableName, RoamingType.DEFAULT)
164     for (exportFile in item.files) {
165       result.putValue(exportFile, item)
166     }
167   }
168
169   @Suppress("DEPRECATION")
170   ApplicationManager.getApplication().getComponents(ExportableApplicationComponent::class.java).forEach(processor)
171   ServiceBean.loadServicesFromBeans(ExportableComponent.EXTENSION_POINT, ExportableComponent::class.java).forEach(processor)
172
173   val configPath = storageManager.expandMacros(ROOT_CONFIG)
174
175   fun isSkipFile(file: Path): Boolean {
176     if (onlyPaths != null) {
177       var relativePath = FileUtilRt.getRelativePath(configPath, file.systemIndependentPath, '/')!!
178       if (!file.fileName.toString().contains('.') && !file.isFile()) {
179         relativePath += '/'
180       }
181       if (!onlyPaths.contains(relativePath)) {
182         return true
183       }
184     }
185
186     return onlyExisting && !file.exists()
187   }
188
189   if (onlyExisting || onlyPaths != null) {
190     result.keys.removeAll(::isSkipFile)
191   }
192
193   val fileToContent = THashMap<Path, String>()
194
195   ServiceManagerImpl.processAllImplementationClasses(ApplicationManager.getApplication() as ApplicationImpl, PairProcessor<Class<*>, PluginDescriptor> { aClass, pluginDescriptor ->
196     val stateAnnotation = StoreUtil.getStateSpec(aClass)
197     if (stateAnnotation == null || stateAnnotation.name.isNullOrEmpty() || ExportableComponent::class.java.isAssignableFrom(aClass)) {
198       return@PairProcessor true
199     }
200
201     val storage = stateAnnotation.storages.sortByDeprecated().firstOrNull() ?: return@PairProcessor true
202     if (!(storage.roamingType != RoamingType.DISABLED && storage.storageClass == StateStorage::class && !storage.path.isNullOrEmpty())) {
203       return@PairProcessor true
204     }
205
206     var additionalExportFile: Path? = null
207     var additionalExportPath = stateAnnotation.additionalExportFile
208     if (additionalExportPath.isNotEmpty()) {
209       // backward compatibility - path can contain macro
210       if (additionalExportPath[0] == '$') {
211         additionalExportFile = Paths.get(storageManager.expandMacros(additionalExportPath))
212       }
213       else {
214         additionalExportFile = Paths.get(storageManager.expandMacros(ROOT_CONFIG), additionalExportPath)
215       }
216       if (isSkipFile(additionalExportFile)) {
217         additionalExportFile = null
218       }
219     }
220
221     val file = Paths.get(storageManager.expandMacros(storage.path))
222     val isFileIncluded = !isSkipFile(file)
223     if (isFileIncluded || additionalExportFile != null) {
224       if (computePresentableNames && onlyExisting && additionalExportFile == null && file.fileName.toString().endsWith(".xml")) {
225         val content = fileToContent.getOrPut(file) { file.readText() }
226         if (!content.contains("""<component name="${stateAnnotation.name}">""")) {
227           return@PairProcessor true
228         }
229       }
230
231       val files = if (additionalExportFile == null) listOf(file) else if (isFileIncluded) listOf(file, additionalExportFile) else listOf(additionalExportFile)
232       val item = ExportableItem(files, if (computePresentableNames) getComponentPresentableName(stateAnnotation, aClass, pluginDescriptor) else "", storage.roamingType)
233       result.putValue(file, item)
234       if (additionalExportFile != null) {
235         result.putValue(additionalExportFile, item)
236       }
237     }
238     true
239   })
240
241   // must be in the end - because most of SchemeManager clients specify additionalExportFile in the State spec
242   (SchemesManagerFactory.getInstance() as SchemeManagerFactoryBase).process {
243     if (it.roamingType != RoamingType.DISABLED && it.presentableName != null && it.fileSpec.getOrNull(0) != '$') {
244       val file = Paths.get(storageManager.expandMacros(ROOT_CONFIG), it.fileSpec)
245       if (!result.containsKey(file) && !isSkipFile(file)) {
246         result.putValue(file, ExportableItem(listOf(file), it.presentableName, it.roamingType))
247       }
248     }
249   }
250   return result
251 }
252
253 private fun getComponentPresentableName(state: State, aClass: Class<*>, pluginDescriptor: PluginDescriptor?): String {
254   val presentableName = state.presentableName.java
255   if (presentableName != State.NameGetter::class.java) {
256     try {
257       return ReflectionUtil.newInstance(presentableName).get()
258     }
259     catch (e: Exception) {
260       LOG.error(e)
261     }
262   }
263
264   val defaultName = state.name
265
266   fun trimDefaultName() = defaultName.removeSuffix("Settings")
267
268   var resourceBundleName: String?
269   if (pluginDescriptor is IdeaPluginDescriptor && "com.intellij" != pluginDescriptor.pluginId.idString) {
270     resourceBundleName = pluginDescriptor.resourceBundleBaseName
271     if (resourceBundleName == null) {
272       if (pluginDescriptor.vendor == "JetBrains") {
273         resourceBundleName = OptionsBundle.PATH_TO_BUNDLE
274       }
275        else {
276         return trimDefaultName()
277       }
278     }
279   }
280   else {
281     resourceBundleName = OptionsBundle.PATH_TO_BUNDLE
282   }
283
284   var classLoader = pluginDescriptor?.pluginClassLoader ?: aClass.classLoader
285   if (classLoader != null) {
286     val message = messageOrDefault(classLoader, resourceBundleName, defaultName)
287     if (message !== defaultName) {
288       return message
289     }
290
291     if (PlatformUtils.isRubyMine()) {
292       // ruby plugin in RubyMine has id "com.intellij", so, we cannot set "resource-bundle" in plugin.xml
293       return messageOrDefault(classLoader, "org.jetbrains.plugins.ruby.RBundle", defaultName)
294     }
295   }
296   return trimDefaultName()
297 }
298
299 private fun messageOrDefault(classLoader: ClassLoader, bundleName: String, defaultName: String): String {
300   val bundle = AbstractBundle.getResourceBundle(bundleName, classLoader) ?: return defaultName
301   return CommonBundle.messageOrDefault(bundle, "exportable.$defaultName.presentable.name", defaultName)
302 }
303