Merge remote-tracking branch 'origin/master' into develar/is
[idea/community.git] / platform / configuration-store-impl / src / FileBasedStorage.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.notification.Notification
19 import com.intellij.notification.NotificationType
20 import com.intellij.notification.Notifications
21 import com.intellij.openapi.application.ApplicationManager
22 import com.intellij.openapi.application.runWriteAction
23 import com.intellij.openapi.components.RoamingType
24 import com.intellij.openapi.components.StateStorage
25 import com.intellij.openapi.components.StoragePathMacros
26 import com.intellij.openapi.components.TrackingPathMacroSubstitutor
27 import com.intellij.openapi.components.impl.stores.StorageUtil
28 import com.intellij.openapi.diagnostic.debug
29 import com.intellij.openapi.fileEditor.impl.LoadTextUtil
30 import com.intellij.openapi.util.JDOMUtil
31 import com.intellij.openapi.util.io.BufferExposingByteArrayOutputStream
32 import com.intellij.openapi.vfs.LocalFileSystem
33 import com.intellij.openapi.vfs.VirtualFile
34 import com.intellij.util.*
35 import org.jdom.Element
36 import org.jdom.JDOMException
37 import org.jdom.Parent
38 import java.io.IOException
39 import java.nio.file.Files
40 import java.nio.file.NoSuchFileException
41 import java.nio.file.Path
42 import java.nio.file.attribute.BasicFileAttributes
43
44 open class FileBasedStorage(file: Path,
45                             fileSpec: String,
46                             rootElementName: String?,
47                             pathMacroManager: TrackingPathMacroSubstitutor? = null,
48                             roamingType: RoamingType? = null,
49                             provider: StreamProvider? = null) : XmlElementStorage(fileSpec, rootElementName, pathMacroManager, roamingType, provider) {
50   private @Volatile var cachedVirtualFile: VirtualFile? = null
51   private var lineSeparator: LineSeparator? = null
52   private var blockSavingTheContent = false
53
54   @Volatile var file = file
55     private set
56
57   init {
58     if (ApplicationManager.getApplication().isUnitTestMode && file.toString().startsWith('$')) {
59       throw AssertionError("It seems like some macros were not expanded for path: $file")
60     }
61   }
62
63   protected open val isUseXmlProlog: Boolean = false
64
65   // we never set io file to null
66   fun setFile(virtualFile: VirtualFile?, ioFileIfChanged: Path?) {
67     cachedVirtualFile = virtualFile
68     if (ioFileIfChanged != null) {
69       file = ioFileIfChanged
70     }
71   }
72
73   override fun createSaveSession(states: StateMap) = FileSaveSession(states, this)
74
75   protected open class FileSaveSession(storageData: StateMap, storage: FileBasedStorage) : XmlElementStorage.XmlElementStorageSaveSession<FileBasedStorage>(storageData, storage) {
76     override fun save() {
77       if (!storage.blockSavingTheContent) {
78         super.save()
79       }
80     }
81
82     override fun saveLocally(element: Element?) {
83       if (storage.lineSeparator == null) {
84         storage.lineSeparator = if (storage.isUseXmlProlog) LineSeparator.LF else LineSeparator.getSystemLineSeparator()
85       }
86
87       val virtualFile = storage.virtualFile
88       if (element == null) {
89         deleteFile(storage.file, this, virtualFile)
90         storage.cachedVirtualFile = null
91       }
92       else {
93         storage.cachedVirtualFile = writeFile(storage.file, this, virtualFile, element, if (storage.isUseXmlProlog) storage.lineSeparator!! else LineSeparator.LF, storage.isUseXmlProlog)
94       }
95     }
96   }
97
98   val virtualFile: VirtualFile?
99     get() {
100       var result = cachedVirtualFile
101       if (result == null) {
102         result = LocalFileSystem.getInstance().findFileByPath(file.systemIndependentPath)
103         cachedVirtualFile = result
104       }
105       return cachedVirtualFile
106     }
107
108   override fun loadLocalData(): Element? {
109     blockSavingTheContent = false
110
111     val attributes: BasicFileAttributes?
112     try {
113       attributes = Files.readAttributes(file, BasicFileAttributes::class.java)
114     }
115     catch (e: NoSuchFileException) {
116       LOG.debug(e) { "Document was not loaded for $fileSpec, doesn't exists" }
117       return null
118     }
119     catch (e: IOException) {
120       processReadException(e)
121       return null
122     }
123
124     try {
125       if (!attributes.isRegularFile) {
126         LOG.debug { "Document was not loaded for $fileSpec, not a file" }
127       }
128       else if (attributes.size() == 0L) {
129         processReadException(null)
130       }
131       else {
132         val data = file.readChars()
133         lineSeparator = detectLineSeparators(data, if (isUseXmlProlog) null else LineSeparator.LF)
134         return loadElement(data)
135       }
136     }
137     catch (e: JDOMException) {
138       processReadException(e)
139     }
140     catch (e: IOException) {
141       processReadException(e)
142     }
143     return null
144   }
145
146   private fun processReadException(e: Exception?) {
147     val contentTruncated = e == null
148     blockSavingTheContent = !contentTruncated && (PROJECT_FILE == fileSpec || fileSpec.startsWith(PROJECT_CONFIG_DIR) || fileSpec == StoragePathMacros.MODULE_FILE || fileSpec == StoragePathMacros.WORKSPACE_FILE)
149     if (!ApplicationManager.getApplication().isUnitTestMode && !ApplicationManager.getApplication().isHeadlessEnvironment) {
150       if (e != null) {
151         LOG.info(e)
152       }
153       Notification(Notifications.SYSTEM_MESSAGES_GROUP_ID,
154         "Load Settings",
155         "Cannot load settings from file '$file': ${if (contentTruncated) "content truncated" else e!!.message}\n${if (blockSavingTheContent) "Please correct the file content" else "File content will be recreated"}",
156         NotificationType.WARNING)
157         .notify(null)
158     }
159   }
160
161   override fun toString() = file.systemIndependentPath
162 }
163
164 fun writeFile(file: Path?, requestor: Any, virtualFile: VirtualFile?, element: Element, lineSeparator: LineSeparator, prependXmlProlog: Boolean): VirtualFile {
165   val result = if (file != null && (virtualFile == null || !virtualFile.isValid)) {
166     StorageUtil.getOrCreateVirtualFile(requestor, file)
167   }
168   else {
169     virtualFile!!
170   }
171
172   if (LOG.isDebugEnabled || ApplicationManager.getApplication().isUnitTestMode) {
173     val content = element.toBufferExposingByteArray(lineSeparator.separatorString)
174     if (isEqualContent(result, lineSeparator, content, prependXmlProlog)) {
175       throw IllegalStateException("Content equals, but it must be handled not on this level: ${result.name}")
176     }
177     else if (StorageUtil.DEBUG_LOG != null && ApplicationManager.getApplication().isUnitTestMode) {
178       StorageUtil.DEBUG_LOG = "${result.path}:\n$content\nOld Content:\n${LoadTextUtil.loadText(result)}\n---------"
179     }
180   }
181
182   doWrite(requestor, result, element, lineSeparator, prependXmlProlog)
183   return result
184 }
185
186 private val XML_PROLOG = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>".toByteArray()
187
188 private fun isEqualContent(result: VirtualFile, lineSeparator: LineSeparator, content: BufferExposingByteArrayOutputStream, prependXmlProlog: Boolean): Boolean {
189   val headerLength = if (!prependXmlProlog) 0 else XML_PROLOG.size + lineSeparator.separatorBytes.size
190   if (result.length.toInt() != (headerLength + content.size())) {
191     return false
192   }
193
194   val oldContent = result.contentsToByteArray()
195
196   if (prependXmlProlog && (!ArrayUtil.startsWith(oldContent, XML_PROLOG) || !ArrayUtil.startsWith(oldContent, XML_PROLOG.size, lineSeparator.separatorBytes))) {
197     return false
198   }
199
200   for (i in headerLength..oldContent.size - 1) {
201     if (oldContent[i] != content.internalBuffer[i - headerLength]) {
202       return false
203     }
204   }
205   return true
206 }
207
208 private fun doWrite(requestor: Any, file: VirtualFile, content: Any, lineSeparator: LineSeparator, prependXmlProlog: Boolean) {
209   LOG.debug { "Save ${file.presentableUrl}" }
210
211   if (!file.isWritable) {
212     // may be element is not long-lived, so, we must write it to byte array
213     val byteArray = if (content is Element) content.toBufferExposingByteArray(lineSeparator.separatorString) else (content as BufferExposingByteArrayOutputStream)
214     throw ReadOnlyModificationException(file, StateStorage.SaveSession { doWrite(requestor, file, byteArray, lineSeparator, prependXmlProlog) })
215   }
216
217   runWriteAction {
218     file.getOutputStream(requestor).use { out ->
219       if (prependXmlProlog) {
220         out.write(XML_PROLOG)
221         out.write(lineSeparator.separatorBytes)
222       }
223       if (content is Element) {
224         JDOMUtil.writeParent(content, out, lineSeparator.separatorString)
225       }
226       else {
227         (content as BufferExposingByteArrayOutputStream).writeTo(out)
228       }
229     }
230   }
231 }
232
233 internal fun Parent.toBufferExposingByteArray(lineSeparator: String = "\n"): BufferExposingByteArrayOutputStream {
234   val out = BufferExposingByteArrayOutputStream(512)
235   JDOMUtil.writeParent(this, out, lineSeparator)
236   return out
237 }
238
239 internal fun detectLineSeparators(chars: CharSequence, defaultSeparator: LineSeparator?): LineSeparator {
240   for (c in chars) {
241     if (c == '\r') {
242       return LineSeparator.CRLF
243     }
244     else if (c == '\n') {
245       // if we are here, there was no \r before
246       return LineSeparator.LF
247     }
248   }
249   return defaultSeparator ?: LineSeparator.getSystemLineSeparator()
250 }
251
252 private fun deleteFile(file: Path, requestor: Any, virtualFile: VirtualFile?) {
253   if (virtualFile == null) {
254     LOG.warn("Cannot find virtual file $file")
255   }
256
257   if (virtualFile == null) {
258     if (file.exists()) {
259       file.delete()
260     }
261   }
262   else if (virtualFile.exists()) {
263     if (virtualFile.isWritable) {
264       deleteFile(requestor, virtualFile)
265     }
266     else {
267       throw ReadOnlyModificationException(virtualFile, StateStorage.SaveSession { deleteFile(requestor, virtualFile) })
268     }
269   }
270 }
271
272 fun deleteFile(requestor: Any, virtualFile: VirtualFile) {
273   runWriteAction { virtualFile.delete(requestor) }
274 }
275
276 internal class ReadOnlyModificationException(val file: VirtualFile, val session: StateStorage.SaveSession?) : RuntimeException("File is read-only: "+file)