Merge branch 'db/javac-ast'
[idea/community.git] / platform / analysis-impl / src / com / intellij / profile / codeInspection / ProjectInspectionProfileManager.kt
1 /*
2  * Copyright 2000-2016 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.profile.codeInspection
17
18 import com.intellij.codeInspection.InspectionProfile
19 import com.intellij.codeInspection.ex.InspectionProfileImpl
20 import com.intellij.codeInspection.ex.InspectionToolRegistrar
21 import com.intellij.configurationStore.*
22 import com.intellij.openapi.Disposable
23 import com.intellij.openapi.application.ApplicationManager
24 import com.intellij.openapi.components.PersistentStateComponent
25 import com.intellij.openapi.components.State
26 import com.intellij.openapi.components.Storage
27 import com.intellij.openapi.options.SchemeManagerFactory
28 import com.intellij.openapi.project.Project
29 import com.intellij.openapi.project.ProjectManager
30 import com.intellij.openapi.project.ProjectManagerListener
31 import com.intellij.openapi.startup.StartupActivity
32 import com.intellij.openapi.util.Disposer
33 import com.intellij.openapi.util.text.StringUtil
34 import com.intellij.packageDependencies.DependencyValidationManager
35 import com.intellij.profile.Profile
36 import com.intellij.project.isDirectoryBased
37 import com.intellij.psi.search.scope.packageSet.NamedScopeManager
38 import com.intellij.psi.search.scope.packageSet.NamedScopesHolder
39 import com.intellij.util.loadElement
40 import com.intellij.util.ui.UIUtil
41 import com.intellij.util.xmlb.Accessor
42 import com.intellij.util.xmlb.SkipDefaultValuesSerializationFilters
43 import com.intellij.util.xmlb.XmlSerializer
44 import com.intellij.util.xmlb.annotations.OptionTag
45 import org.jdom.Element
46 import org.jetbrains.annotations.TestOnly
47 import org.jetbrains.concurrency.Promise
48 import org.jetbrains.concurrency.resolvedPromise
49 import org.jetbrains.concurrency.runAsync
50 import java.util.*
51 import java.util.function.Function
52
53 const val PROFILE = "profile"
54
55 private const val VERSION = "1.0"
56 private const val SCOPE = "scope"
57 private const val NAME = "name"
58 private const val PROJECT_DEFAULT_PROFILE_NAME = "Project Default"
59
60 private val defaultSchemeDigest = loadElement("""<component name="InspectionProjectProfileManager">
61   <profile version="1.0">
62     <option name="myName" value="Project Default" />
63   </profile>
64 </component>""").digest()
65
66 @State(name = "InspectionProjectProfileManager", storages = arrayOf(Storage(value = "inspectionProfiles/profiles_settings.xml", exclusive = true)))
67 class ProjectInspectionProfileManager(val project: Project,
68                                       private val applicationProfileManager: InspectionProfileManager,
69                                       private val scopeManager: DependencyValidationManager,
70                                       private val localScopesHolder: NamedScopeManager,
71                                       schemeManagerFactory: SchemeManagerFactory) : BaseInspectionProfileManager(project.messageBus), PersistentStateComponent<Element> {
72   companion object {
73     @JvmStatic
74     fun getInstanceImpl(project: Project): ProjectInspectionProfileManager {
75       return InspectionProjectProfileManager.getInstance(project) as ProjectInspectionProfileManager
76     }
77   }
78
79   private var scopeListener: NamedScopesHolder.ScopeListener? = null
80
81   private var state = State()
82
83   private val initialLoadSchemesFuture: Promise<Any?>
84
85   private val skipDefaultsSerializationFilter = object : SkipDefaultValuesSerializationFilters(State()) {
86     override fun accepts(accessor: Accessor, bean: Any, beanValue: Any?): Boolean {
87       if (beanValue == null && accessor.name == "projectProfile") {
88         return false
89       }
90       return super.accepts(accessor, bean, beanValue)
91     }
92   }
93
94   private val schemeManagerIprProvider = if (project.isDirectoryBased) null else SchemeManagerIprProvider("profile")
95
96   override val schemeManager = schemeManagerFactory.create("inspectionProfiles", object : InspectionProfileProcessor() {
97     override fun createScheme(dataHolder: SchemeDataHolder<InspectionProfileImpl>,
98                               name: String,
99                               attributeProvider: Function<String, String?>,
100                               isBundled: Boolean): InspectionProfileImpl {
101       val profile = InspectionProfileImpl(name, InspectionToolRegistrar.getInstance(), this@ProjectInspectionProfileManager,
102                                           InspectionProfileImpl.getDefaultProfile(), dataHolder)
103       profile.isProjectLevel = true
104       return profile
105     }
106
107     override fun isSchemeFile(name: CharSequence) = !StringUtil.equals(name, "profiles_settings.xml")
108
109     override fun isSchemeDefault(scheme: InspectionProfileImpl, digest: ByteArray): Boolean {
110       return scheme.name == PROJECT_DEFAULT_PROFILE_NAME && Arrays.equals(digest, defaultSchemeDigest)
111     }
112
113     override fun onSchemeDeleted(scheme: InspectionProfileImpl) {
114       schemeRemoved(scheme)
115     }
116
117     override fun onSchemeAdded(scheme: InspectionProfileImpl) {
118       if (scheme.wasInitialized()) {
119         fireProfileChanged(scheme)
120       }
121     }
122
123     override fun onCurrentSchemeSwitched(oldScheme: InspectionProfileImpl?, newScheme: InspectionProfileImpl?) {
124       for (adapter in profileListeners) {
125         adapter.profileActivated(oldScheme, newScheme)
126       }
127     }
128   }, isUseOldFileNameSanitize = true, streamProvider = schemeManagerIprProvider)
129
130   private data class State(@field:OptionTag("PROJECT_PROFILE") var projectProfile: String? = PROJECT_DEFAULT_PROFILE_NAME,
131                            @field:OptionTag("USE_PROJECT_PROFILE") var useProjectProfile: Boolean = true)
132
133   init {
134     val app = ApplicationManager.getApplication()
135     if (!project.isDirectoryBased || app.isUnitTestMode) {
136       initialLoadSchemesFuture = resolvedPromise()
137     }
138     else {
139       initialLoadSchemesFuture = runAsync { schemeManager.loadSchemes() }
140     }
141
142     project.messageBus.connect().subscribe(ProjectManager.TOPIC, object: ProjectManagerListener {
143       override fun projectClosed(project: Project) {
144         val cleanupInspectionProfilesRunnable = {
145           cleanupSchemes(project)
146           (InspectionProfileManager.getInstance() as BaseInspectionProfileManager).cleanupSchemes(project)
147           fireProfilesShutdown()
148         }
149
150         if (app.isUnitTestMode || app.isHeadlessEnvironment) {
151           cleanupInspectionProfilesRunnable.invoke()
152         }
153         else {
154           app.executeOnPooledThread(cleanupInspectionProfilesRunnable)
155         }
156       }
157     })
158   }
159
160   @TestOnly
161   fun forceLoadSchemes() {
162     LOG.assertTrue(ApplicationManager.getApplication().isUnitTestMode)
163     schemeManager.loadSchemes()
164   }
165
166   fun isCurrentProfileInitialized() = currentProfile.wasInitialized()
167
168   @Synchronized override fun updateProfile(profile: Profile) {
169     super.updateProfile(profile)
170
171     (profile as InspectionProfileImpl).initInspectionTools(project)
172   }
173
174   override fun schemeRemoved(scheme: InspectionProfile) {
175     scheme.cleanup(project)
176   }
177
178   @Suppress("unused")
179   private class ProjectInspectionProfileStartUpActivity : StartupActivity {
180     override fun runActivity(project: Project) {
181       getInstanceImpl(project).apply {
182         initialLoadSchemesFuture.done {
183           currentProfile.initInspectionTools(project)
184           fireProfilesInitialized()
185
186           val app = ApplicationManager.getApplication()
187           if (app.isUnitTestMode && app.isDispatchThread) {
188             // do not restart daemon in the middle of the test
189             //noinspection TestOnlyProblems
190             UIUtil.dispatchAllInvocationEvents()
191           }
192         }
193
194         scopeListener = NamedScopesHolder.ScopeListener {
195           for (profile in schemeManager.allSchemes) {
196             profile.scopesChanged()
197           }
198         }
199
200         scopeManager.addScopeListener(scopeListener!!)
201         localScopesHolder.addScopeListener(scopeListener!!)
202         Disposer.register(project, Disposable {
203           scopeManager.removeScopeListener(scopeListener!!)
204           localScopesHolder.removeScopeListener(scopeListener!!)
205         })
206       }
207     }
208   }
209
210   @Synchronized override fun getState(): Element? {
211     val result = Element("settings")
212
213     schemeManagerIprProvider?.writeState(result)
214
215     val state = this.state
216     if (state.useProjectProfile) {
217       state.projectProfile = schemeManager.currentSchemeName
218     }
219
220     XmlSerializer.serializeInto(state, result, skipDefaultsSerializationFilter)
221     if (!result.children.isEmpty()) {
222       result.addContent(Element("version").setAttribute("value", VERSION))
223     }
224
225     severityRegistrar.writeExternal(result)
226
227     return wrapState(result, project)
228   }
229
230   @Synchronized override fun loadState(state: Element) {
231     val data = unwrapState(state, project, schemeManagerIprProvider, schemeManager)
232
233     val newState = State()
234
235     data?.let {
236       try {
237         severityRegistrar.readExternal(it)
238       }
239       catch (e: Throwable) {
240         LOG.error(e)
241       }
242
243       XmlSerializer.deserializeInto(newState, it)
244     }
245
246     this.state = newState
247
248     if (data != null && data.getChild("version")?.getAttributeValue("value") != VERSION) {
249       for (o in data.getChildren("option")) {
250         if (o.getAttributeValue("name") == "USE_PROJECT_LEVEL_SETTINGS") {
251           if (o.getAttributeValue("value").toBoolean()) {
252             if (newState.projectProfile != null) {
253               currentProfile.convert(data, project)
254             }
255           }
256           break
257         }
258       }
259     }
260
261     if (newState.useProjectProfile) {
262       schemeManager.currentSchemeName = newState.projectProfile
263     }
264   }
265
266   override fun getScopesManager() = scopeManager
267
268   @Synchronized override fun getProfiles(): Collection<Profile> {
269     currentProfile
270     return schemeManager.allSchemes
271   }
272
273   @Synchronized override fun getAvailableProfileNames(): Array<String> = schemeManager.allSchemeNames.toTypedArray()
274
275   val projectProfile: String?
276     get() = schemeManager.currentSchemeName
277
278   @Synchronized override fun setRootProfile(name: String?) {
279     if (name != schemeManager.currentSchemeName) {
280       schemeManager.currentSchemeName = name
281       state.useProjectProfile = name != null
282     }
283   }
284
285   @Synchronized fun useApplicationProfile(name: String) {
286     schemeManager.currentSchemeName = null
287     state.useProjectProfile = false
288     // yes, we reuse the same field - useProjectProfile field will be used to distinguish — is it app or project level
289     // to avoid data format change
290     state.projectProfile = name
291   }
292
293   @Synchronized fun setCurrentProfile(profile: InspectionProfileImpl?) {
294     schemeManager.setCurrent(profile)
295     state.useProjectProfile = profile != null
296   }
297
298   @Synchronized override fun getCurrentProfile(): InspectionProfileImpl {
299     if (!state.useProjectProfile) {
300       return (state.projectProfile?.let {
301         applicationProfileManager.getProfile(it, false)
302       } ?: applicationProfileManager.currentProfile) as InspectionProfileImpl
303     }
304
305     var currentScheme = schemeManager.currentScheme
306     if (currentScheme == null) {
307       currentScheme = schemeManager.allSchemes.firstOrNull()
308       if (currentScheme == null) {
309         currentScheme = InspectionProfileImpl(PROJECT_DEFAULT_PROFILE_NAME, InspectionToolRegistrar.getInstance(), this,
310                                               InspectionProfileImpl.getDefaultProfile(), null)
311         currentScheme.copyFrom(applicationProfileManager.currentProfile)
312         currentScheme.isProjectLevel = true
313         currentScheme.name = PROJECT_DEFAULT_PROFILE_NAME
314         schemeManager.addScheme(currentScheme)
315       }
316       schemeManager.setCurrent(currentScheme, false)
317     }
318     return currentScheme
319   }
320
321   private fun fireProfilesInitialized() {
322     for (listener in profileListeners) {
323       listener.profilesInitialized()
324     }
325   }
326
327   private fun fireProfilesShutdown() {
328     for (profileChangeAdapter in profileListeners) {
329       profileChangeAdapter.profilesShutdown()
330     }
331   }
332
333   @Synchronized override fun getProfile(name: String, returnRootProfileIfNamedIsAbsent: Boolean): Profile? {
334     val profile = schemeManager.findSchemeByName(name)
335     return profile ?: applicationProfileManager.getProfile(name, returnRootProfileIfNamedIsAbsent)
336   }
337 }