2 * Copyright 2000-2015 JetBrains s.r.o.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 package org.jetbrains.ide
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
50 private val NOT_FOUND = Promise.createError("not found")
51 private val LINE_AND_COLUMN = Pattern.compile("^(.*?)(?::(\\d+))?(?::(\\d+))?$")
54 * @api {get} /file Open file
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.
63 * @apiExample {curl} Absolute path
64 * curl http://localhost:63342/api/file//absolute/path/to/file.kt
66 * @apiExample {curl} Relative path
67 * curl http://localhost:63342/api/file/relative/to/module/root/path/to/file.kt
69 * @apiExample {curl} With line and column
70 * curl http://localhost:63342/api/file/relative/to/module/root/path/to/file.kt:100:34
72 * @apiExample {curl} Query parameters
73 * curl http://localhost:63342/api/file?file=path/to/file.kt&line=100&column=34
75 internal class OpenFileHttpService : RestService() {
76 @Volatile private var refreshSessionId: Long = 0
77 private val requests = ConcurrentLinkedQueue<OpenFileTask>()
79 override fun getServiceName() = "file"
81 override fun isMethodSupported(method: HttpMethod) = method === HttpMethod.GET || method === HttpMethod.POST
83 override fun execute(urlDecoder: QueryStringDecoder, request: FullHttpRequest, context: ChannelHandlerContext): String? {
84 val keepAlive = HttpUtil.isKeepAlive(request)
85 val channel = context.channel()
87 val apiRequest: OpenFileRequest
88 if (request.method() === HttpMethod.POST) {
89 apiRequest = gson.value.fromJson(createJsonReader(request), OpenFileRequest::class.java)
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)
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 <= ' ' }
107 if (apiRequest.line == -1) {
108 apiRequest.line = StringUtilRt.parseInt(matcher.group(2), 1)
110 if (apiRequest.column == -1) {
111 apiRequest.column = StringUtilRt.parseInt(matcher.group(3), 1)
115 if (apiRequest.file == null) {
116 sendStatus(HttpResponseStatus.BAD_REQUEST, keepAlive, channel)
120 val promise = openFile(apiRequest, context, request) ?: return null
121 promise.done { sendStatus(HttpResponseStatus.OK, keepAlive, channel) }
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")
130 sendStatus(HttpResponseStatus.INTERNAL_SERVER_ERROR, keepAlive, channel)
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)
144 return if (context == null || checkAccess(context.channel(), file, httpRequest!!, doNotExposeStatus = true)) openAbsolutePath(file, request) else null
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, {
154 val task = requests.poll() ?: break
155 task.promise.catchError {
156 if (openRelativePath(task.path, task.request)) {
157 task.promise.setResult(null)
160 task.promise.setError(NOT_FOUND)
164 }, ModalityState.NON_MODAL)
166 session.addAllFiles(*ManagingFS.getInstance().localRoots)
167 refreshSessionId = session.id
169 return mainTask.promise
172 override fun isAccessible(request: HttpRequest) = true
175 internal class OpenFileRequest {
176 var file: String? = null
177 // The line number of the file (1-based)
179 // The column number of the file (1-based)
185 private class OpenFileTask(internal val path: String, internal val request: OpenFileRequest) {
186 internal val promise = AsyncPromise<Void>()
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)
198 // path must be normalized
199 private fun openRelativePath(path: String, request: OpenFileRequest): Boolean {
200 var virtualFile: VirtualFile? = null
201 var project: Project? = null
203 val projects = ProjectManager.getInstance().openProjects
204 for (openedProject in projects) {
205 openedProject.baseDir?.let {
206 virtualFile = it.findFileByRelativePath(path)
209 if (virtualFile == null) {
210 virtualFile = WebServerPathToFileManager.getInstance(openedProject).findVirtualFile(path)
212 if (virtualFile != null) {
213 project = openedProject
218 if (virtualFile == null) {
219 for (openedProject in projects) {
220 for (vcsRoot in ProjectLevelVcsManager.getInstance(openedProject).allVcsRoots) {
221 val root = vcsRoot.path
223 virtualFile = root.findFileByRelativePath(path)
224 if (virtualFile != null) {
225 project = openedProject
233 return virtualFile?.let {
234 AppUIUtil.invokeLaterIfProjectAlive(project!!, Runnable { navigate(project, it, request) })
239 private fun openAbsolutePath(file: Path, request: OpenFileRequest): Promise<Void> {
240 val promise = AsyncPromise<Void>()
241 ApplicationManager.getApplication().invokeLater {
243 val virtualFile = runWriteAction { LocalFileSystem.getInstance().refreshAndFindFileByPath(file.systemIndependentPath) }
244 if (virtualFile == null) {
245 promise.setError(NOT_FOUND)
248 navigate(ProjectUtil.guessProjectForContentFile(virtualFile), virtualFile, request)
249 promise.setResult(null)