log.warn if not found, fix comment
[idea/community.git] / platform / built-in-server / src / org / jetbrains / ide / OpenFileHttpService.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 org.jetbrains.ide
17
18 import com.intellij.ide.impl.ProjectUtil.focusProjectWindow
19 import com.intellij.openapi.application.ApplicationManager
20 import com.intellij.openapi.application.ModalityState
21 import com.intellij.openapi.application.runWriteAction
22 import com.intellij.openapi.fileEditor.OpenFileDescriptor
23 import com.intellij.openapi.project.Project
24 import com.intellij.openapi.project.ProjectManager
25 import com.intellij.openapi.project.ProjectUtil
26 import com.intellij.openapi.util.io.FileUtil
27 import com.intellij.openapi.util.text.StringUtil
28 import com.intellij.openapi.util.text.StringUtilRt
29 import com.intellij.openapi.vcs.ProjectLevelVcsManager
30 import com.intellij.openapi.vfs.LocalFileSystem
31 import com.intellij.openapi.vfs.VirtualFile
32 import com.intellij.openapi.vfs.newvfs.ManagingFS
33 import com.intellij.openapi.vfs.newvfs.RefreshQueue
34 import com.intellij.ui.AppUIUtil
35 import com.intellij.util.exists
36 import com.intellij.util.systemIndependentPath
37 import io.netty.channel.ChannelHandlerContext
38 import io.netty.handler.codec.http.*
39 import org.jetbrains.builtInWebServer.WebServerPathToFileManager
40 import org.jetbrains.builtInWebServer.checkAccess
41 import org.jetbrains.concurrency.AsyncPromise
42 import org.jetbrains.concurrency.Promise
43 import org.jetbrains.concurrency.catchError
44 import org.jetbrains.concurrency.rejectedPromise
45 import java.nio.file.Path
46 import java.nio.file.Paths
47 import java.util.concurrent.ConcurrentLinkedQueue
48 import java.util.regex.Pattern
49
50 private val NOT_FOUND = Promise.createError("not found")
51 private val LINE_AND_COLUMN = Pattern.compile("^(.*?)(?::(\\d+))?(?::(\\d+))?$")
52
53 /**
54  * @api {get} /file Open file
55  * @apiName file
56  * @apiGroup Platform
57  *
58  * @apiParam {String} file The path of the file. Relative (to project base dir, VCS root, module source or content root) or absolute.
59  * @apiParam {Integer} [line] The line number of the file (1-based).
60  * @apiParam {Integer} [column] The column number of the file (1-based).
61  * @apiParam {Boolean} [focused=true] Whether to focus project window.
62  *
63  * @apiExample {curl} Absolute path
64  * curl http://localhost:63342/api/file//absolute/path/to/file.kt
65  *
66  * @apiExample {curl} Relative path
67  * curl http://localhost:63342/api/file/relative/to/module/root/path/to/file.kt
68  *
69  * @apiExample {curl} With line and column
70  * curl http://localhost:63342/api/file/relative/to/module/root/path/to/file.kt:100:34
71  *
72  * @apiExample {curl} Query parameters
73  * curl http://localhost:63342/api/file?file=path/to/file.kt&line=100&column=34
74  */
75 internal class OpenFileHttpService : RestService() {
76   @Volatile private var refreshSessionId: Long = 0
77   private val requests = ConcurrentLinkedQueue<OpenFileTask>()
78
79   override fun getServiceName() = "file"
80
81   override fun isMethodSupported(method: HttpMethod) = method === HttpMethod.GET || method === HttpMethod.POST
82
83   override fun execute(urlDecoder: QueryStringDecoder, request: FullHttpRequest, context: ChannelHandlerContext): String? {
84     val keepAlive = HttpUtil.isKeepAlive(request)
85     val channel = context.channel()
86
87     val apiRequest: OpenFileRequest
88     if (request.method() === HttpMethod.POST) {
89       apiRequest = gson.value.fromJson(createJsonReader(request), OpenFileRequest::class.java)
90     }
91     else {
92       apiRequest = OpenFileRequest()
93       apiRequest.file = StringUtil.nullize(getStringParameter("file", urlDecoder), true)
94       apiRequest.line = getIntParameter("line", urlDecoder)
95       apiRequest.column = getIntParameter("column", urlDecoder)
96       apiRequest.focused = getBooleanParameter("focused", urlDecoder, true)
97     }
98
99     val prefixLength = 1 + PREFIX.length + 1 + serviceName.length + 1
100     val path = urlDecoder.path()
101     if (path.length > prefixLength) {
102       val matcher = LINE_AND_COLUMN.matcher(path).region(prefixLength, path.length)
103       LOG.assertTrue(matcher.matches())
104       if (apiRequest.file == null) {
105         apiRequest.file = matcher.group(1).trim { it <= ' ' }
106       }
107       if (apiRequest.line == -1) {
108         apiRequest.line = StringUtilRt.parseInt(matcher.group(2), 1)
109       }
110       if (apiRequest.column == -1) {
111         apiRequest.column = StringUtilRt.parseInt(matcher.group(3), 1)
112       }
113     }
114
115     if (apiRequest.file == null) {
116       sendStatus(HttpResponseStatus.BAD_REQUEST, keepAlive, channel)
117       return null
118     }
119
120     val promise = openFile(apiRequest, context, request) ?: return null
121     promise.done { sendStatus(HttpResponseStatus.OK, keepAlive, channel) }
122       .rejected {
123         if (it === NOT_FOUND) {
124           // don't expose file status
125           sendStatus(HttpResponseStatus.OK, keepAlive, channel)
126           LOG.warn("File ${apiRequest.file} not found")
127         }
128         else {
129           // todo send error
130           sendStatus(HttpResponseStatus.INTERNAL_SERVER_ERROR, keepAlive, channel)
131           LOG.error(it)
132         }
133       }
134     return null
135   }
136
137   fun openFile(request: OpenFileRequest, context: ChannelHandlerContext?, httpRequest: HttpRequest?): Promise<Void>? {
138     val path = FileUtil.expandUserHome(request.file!!)
139     val file = Paths.get(FileUtil.toSystemDependentName(path))
140     if (file.isAbsolute) {
141       if (!file.exists()) {
142         return rejectedPromise(NOT_FOUND)
143       }
144       return if (context == null || checkAccess(context.channel(), file, httpRequest!!, doNotExposeStatus = true)) openAbsolutePath(file, request) else null
145     }
146
147     // we don't want to call refresh for each attempt on findFileByRelativePath call, so, we do what ourSaveAndSyncHandlerImpl does on frame activation
148     val queue = RefreshQueue.getInstance()
149     queue.cancelSession(refreshSessionId)
150     val mainTask = OpenFileTask(FileUtil.toCanonicalPath(FileUtil.toSystemIndependentName(path), '/'), request)
151     requests.offer(mainTask)
152     val session = queue.createSession(true, true, {
153       while (true) {
154         val task = requests.poll() ?: break
155         task.promise.catchError {
156           if (openRelativePath(task.path, task.request)) {
157             task.promise.setResult(null)
158           }
159           else {
160             task.promise.setError(NOT_FOUND)
161           }
162         }
163       }
164     }, ModalityState.NON_MODAL)
165
166     session.addAllFiles(*ManagingFS.getInstance().localRoots)
167     refreshSessionId = session.id
168     session.launch()
169     return mainTask.promise
170   }
171
172   override fun isAccessible(request: HttpRequest) = true
173 }
174
175 internal class OpenFileRequest {
176   var file: String? = null
177   // The line number of the file (1-based)
178   var line = 0
179   // The column number of the file (1-based)
180   var column = 0
181
182   var focused = true
183 }
184
185 private class OpenFileTask(internal val path: String, internal val request: OpenFileRequest) {
186   internal val promise = AsyncPromise<Void>()
187 }
188
189 private fun navigate(project: Project?, file: VirtualFile, request: OpenFileRequest) {
190   val effectiveProject = project ?: RestService.getLastFocusedOrOpenedProject() ?: ProjectManager.getInstance().defaultProject
191   // OpenFileDescriptor line and column number are 0-based.
192   OpenFileDescriptor(effectiveProject, file, Math.max(request.line - 1, 0), Math.max(request.column - 1, 0)).navigate(true)
193   if (request.focused) {
194     focusProjectWindow(project, true)
195   }
196 }
197
198 // path must be normalized
199 private fun openRelativePath(path: String, request: OpenFileRequest): Boolean {
200   var virtualFile: VirtualFile? = null
201   var project: Project? = null
202
203   val projects = ProjectManager.getInstance().openProjects
204   for (openedProject in projects) {
205     openedProject.baseDir?.let {
206       virtualFile = it.findFileByRelativePath(path)
207     }
208
209     if (virtualFile == null) {
210       virtualFile = WebServerPathToFileManager.getInstance(openedProject).findVirtualFile(path)
211     }
212     if (virtualFile != null) {
213       project = openedProject
214       break
215     }
216   }
217
218   if (virtualFile == null) {
219     for (openedProject in projects) {
220       for (vcsRoot in ProjectLevelVcsManager.getInstance(openedProject).allVcsRoots) {
221         val root = vcsRoot.path
222         if (root != null) {
223           virtualFile = root.findFileByRelativePath(path)
224           if (virtualFile != null) {
225             project = openedProject
226             break
227           }
228         }
229       }
230     }
231   }
232
233   return virtualFile?.let {
234     AppUIUtil.invokeLaterIfProjectAlive(project!!, Runnable { navigate(project, it, request) })
235     true
236   } ?: false
237 }
238
239 private fun openAbsolutePath(file: Path, request: OpenFileRequest): Promise<Void> {
240   val promise = AsyncPromise<Void>()
241   ApplicationManager.getApplication().invokeLater {
242     promise.catchError {
243       val virtualFile = runWriteAction {  LocalFileSystem.getInstance().refreshAndFindFileByPath(file.systemIndependentPath) }
244       if (virtualFile == null) {
245         promise.setError(NOT_FOUND)
246       }
247       else {
248         navigate(ProjectUtil.guessProjectForContentFile(virtualFile), virtualFile, request)
249         promise.setResult(null)
250       }
251     }
252   }
253   return promise
254 }