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