diff --git a/build/restApiDocGenerator/gulpfile.js b/build/restApiDocGenerator/gulpfile.js index 01409544d8cd..867525bf41fe 100644 --- a/build/restApiDocGenerator/gulpfile.js +++ b/build/restApiDocGenerator/gulpfile.js @@ -2,12 +2,12 @@ var gulp = require('gulp') var apidoc = require('gulp-apidoc') var path = require('path') -var sources = path.resolve("../../platform/") +var sources = path.resolve("../../platform/built-in-server/src") gulp.task('apidoc', function () { var destDir = (process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE) + "/idea-rest-api"; - console.info("generating docs from " + sources + " to " + destDir) - apidoc.exec({src: sources, dest: destDir, debug: true}) + console.info("Generating docs from " + sources + " to " + destDir) + apidoc.exec({src: sources, dest: destDir}) }) gulp.task('default', ['apidoc']) diff --git a/lib/org/junit/rules/annotations.xml b/lib/org/junit/rules/annotations.xml new file mode 100644 index 000000000000..74e0fbfe3403 --- /dev/null +++ b/lib/org/junit/rules/annotations.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/platform/built-in-server/built-in-server.iml b/platform/built-in-server/built-in-server.iml index 578fcf967551..dc4ee511dba7 100644 --- a/platform/built-in-server/built-in-server.iml +++ b/platform/built-in-server/built-in-server.iml @@ -21,5 +21,6 @@ + \ No newline at end of file diff --git a/platform/built-in-server/src/org/jetbrains/ide/DiffHttpService.java b/platform/built-in-server/src/org/jetbrains/ide/DiffHttpService.java index ea0ebbc7af99..b91a5575c2de 100644 --- a/platform/built-in-server/src/org/jetbrains/ide/DiffHttpService.java +++ b/platform/built-in-server/src/org/jetbrains/ide/DiffHttpService.java @@ -44,15 +44,15 @@ import java.util.List; * @apiName diff * @apiGroup Platform * - * @apiParam (properties) {String} [fileType] The file type name of the contents (see FileType.getName()). + * @apiParam {String} [fileType] The file type name of the contents (see FileType.getName()). * You can get registered file types using /rest/about?registeredFileTypes request. - * @apiParam (properties) {String} [windowTitle=Diff Service] The title of the diff window. - * @apiParam (properties) {Boolean} [focused=true] Whether to focus project window. + * @apiParam {String} [windowTitle=Diff Service] The title of the diff window. + * @apiParam {Boolean} [focused=true] Whether to focus project window. * - * @apiParam (properties) {Object[]{2..}} contents The list of the contents to diff. - * @apiParam (properties) {String} [contents.title] The title of the content. - * @apiParam (properties) {String} [contents.fileType] The file type name of the content. - * @apiParam (properties) {String} contents.content The data of the content. + * @apiParam {Object[]{2..}} contents The list of the contents to diff. + * @apiParam {String} [contents.title] The title of the content. + * @apiParam {String} [contents.fileType] The file type name of the content. + * @apiParam {String} contents.content The data of the content. * * @apiUse DiffRequestExample */ @@ -107,7 +107,7 @@ final class DiffHttpService extends RestService { return "Empty request"; } - Project project = guessProject(); + Project project = getLastFocusedOrOpenedProject(); if (project == null) { // Argument for @NotNull parameter 'project' of com/intellij/openapi/components/ServiceManager.getService must not be null project = ProjectManager.getInstance().getDefaultProject(); diff --git a/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.java b/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.java new file mode 100644 index 000000000000..8812640397be --- /dev/null +++ b/platform/built-in-server/src/org/jetbrains/ide/OpenFileHttpService.java @@ -0,0 +1,322 @@ +/* + * Copyright 2000-2015 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jetbrains.ide; + +import com.google.gson.Gson; +import com.intellij.openapi.application.AccessToken; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ModalityState; +import com.intellij.openapi.application.WriteAction; +import com.intellij.openapi.fileEditor.OpenFileDescriptor; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.project.ProjectManager; +import com.intellij.openapi.project.ProjectUtil; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.util.text.StringUtilRt; +import com.intellij.openapi.vcs.ProjectLevelVcsManager; +import com.intellij.openapi.vcs.VcsRoot; +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.vfs.newvfs.ManagingFS; +import com.intellij.openapi.vfs.newvfs.RefreshQueue; +import com.intellij.openapi.vfs.newvfs.RefreshSession; +import com.intellij.ui.AppUIUtil; +import com.intellij.util.Consumer; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.builtInWebServer.WebServerPathToFileManager; +import org.jetbrains.concurrency.AsyncPromise; +import org.jetbrains.concurrency.Promise; + +import java.io.File; +import java.io.IOException; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @api {get} /file Open file + * @apiName file + * @apiGroup Platform + * + * @apiParam {String} file The path of the file. Relative (to project base dir, VCS root, module source or content root) or absolute. + * @apiParam {Integer} [line] The line number of the file (1-based). + * @apiParam {Integer} [column] The column number of the file (1-based). + * @apiParam {Boolean} [focused=true] Whether to focus project window. + * + * @apiExample {curl} Absolute path + * curl http://localhost:63342/api/file//absolute/path/to/file.kt + * + * @apiExample {curl} Relative path + * curl http://localhost:63342/api/file/relative/to/module/root/path/to/file.kt + * + * @apiExample {curl} With line and column + * curl http://localhost:63342/api/file/relative/to/module/root/path/to/file.kt:100:34 + * + * @apiExample {curl} Query parameters + * curl http://localhost:63342/api/file?file=path/to/file.kt&line=100&column=34 + */ +class OpenFileHttpService extends RestService { + private static final RuntimeException NOT_FOUND = Promise.createError("not found"); + private static final Pattern LINE_AND_COLUMN = Pattern.compile("^(.*?)(?::(\\d+))?(?::(\\d+))?$"); + + private volatile long refreshSessionId = 0; + private final ConcurrentLinkedQueue requests = new ConcurrentLinkedQueue(); + + @NotNull + @Override + protected String getServiceName() { + return "file"; + } + + @Override + protected boolean isMethodSupported(@NotNull HttpMethod method) { + return method == HttpMethod.GET || method == HttpMethod.POST; + } + + @Nullable + @Override + public String execute(@NotNull QueryStringDecoder urlDecoder, @NotNull FullHttpRequest request, @NotNull ChannelHandlerContext context) throws IOException { + final boolean keepAlive = HttpHeaders.isKeepAlive(request); + final Channel channel = context.channel(); + + String file; + int line; + int column; + + if (request.method() == HttpMethod.POST) { + OpenFileRequest jsonRequest = new Gson().fromJson(createJsonReader(request), OpenFileRequest.class); + file = jsonRequest.file; + line = jsonRequest.line; + column = jsonRequest.column; + } + else { + file = StringUtil.nullize(getStringParameter("file", urlDecoder), true); + line = getIntParameter("line", urlDecoder); + column = getIntParameter("column", urlDecoder); + } + + + int prefixLength = 1 + PREFIX.length() + 1 + getServiceName().length() + 1; + String path = urlDecoder.path(); + if (path.length() > prefixLength) { + Matcher matcher = LINE_AND_COLUMN.matcher(path).region(prefixLength, path.length()); + LOG.assertTrue(matcher.matches()); + if (file == null) { + file = matcher.group(1).trim(); + } + if (line == -1) { + line = StringUtilRt.parseInt(matcher.group(2), 1); + } + if (column == -1) { + column = StringUtilRt.parseInt(matcher.group(3), 1); + } + } + + if (file == null) { + sendStatus(HttpResponseStatus.BAD_REQUEST, keepAlive, channel); + return null; + } + + openFile(file, line, column, getBooleanParameter("focused", urlDecoder)) + .done(new Consumer() { + @Override + public void consume(Void aVoid) { + sendStatus(HttpResponseStatus.OK, keepAlive, channel); + } + }) + .rejected(new Consumer() { + @Override + public void consume(Throwable throwable) { + if (throwable == NOT_FOUND) { + sendStatus(HttpResponseStatus.NOT_FOUND, keepAlive, channel); + } + else { + // todo send error + sendStatus(HttpResponseStatus.INTERNAL_SERVER_ERROR, keepAlive, channel); + LOG.error(throwable); + } + } + }); + return null; + } + + private static void navigate(@Nullable Project project, @NotNull VirtualFile file, int line, int column, boolean focused) { + if (project == null) { + project = getLastFocusedOrOpenedProject(); + if (project == null) { + project = ProjectManager.getInstance().getDefaultProject(); + } + } + + new OpenFileDescriptor(project, file, line, column).navigate(true); + if (focused) { + com.intellij.ide.impl.ProjectUtil.focusProjectWindow(project, true); + } + } + + @NotNull + Promise openFile(@NotNull String path, int line, final int column, final boolean focused) { + path = FileUtil.expandUserHome(path); + final File file = new File(FileUtil.toSystemDependentName(path)); + if (file.isAbsolute()) { + return openAbsolutePath(file, line, column, focused); + } + + // we don't want to call refresh for each attempt on findFileByRelativePath call, so, we do what ourSaveAndSyncHandlerImpl does on frame activation + RefreshQueue queue = RefreshQueue.getInstance(); + queue.cancelSession(refreshSessionId); + OpenFileTask request = new OpenFileTask(FileUtil.toCanonicalPath(FileUtil.toSystemIndependentName(path), '/'), line, column); + requests.offer(request); + RefreshSession session = queue.createSession(true, true, new Runnable() { + @Override + public void run() { + OpenFileTask request; + while ((request = requests.poll()) != null) { + try { + if (openRelativePath(request.path, request.line, request.column, focused)) { + request.promise.setResult(null); + } + else { + request.promise.setError(NOT_FOUND); + } + } + catch (Throwable e) { + request.promise.setError(e); + } + } + } + }, ModalityState.NON_MODAL); + + session.addAllFiles(ManagingFS.getInstance().getLocalRoots()); + refreshSessionId = session.getId(); + session.launch(); + return request.promise; + } + + // path must be normalized + private static boolean openRelativePath(@NotNull final String path, final int line, final int column, final boolean focused) { + VirtualFile virtualFile = null; + Project project = null; + + Project[] projects = ProjectManager.getInstance().getOpenProjects(); + for (Project openedProject : projects) { + VirtualFile openedProjectBaseDir = openedProject.getBaseDir(); + if (openedProjectBaseDir != null) { + virtualFile = openedProjectBaseDir.findFileByRelativePath(path); + } + + if (virtualFile == null) { + virtualFile = WebServerPathToFileManager.getInstance(openedProject).get(path); + } + if (virtualFile != null) { + project = openedProject; + break; + } + } + + if (virtualFile == null) { + for (Project openedProject : projects) { + for (VcsRoot vcsRoot : ProjectLevelVcsManager.getInstance(openedProject).getAllVcsRoots()) { + VirtualFile root = vcsRoot.getPath(); + if (root != null) { + virtualFile = root.findFileByRelativePath(path); + if (virtualFile != null) { + project = openedProject; + break; + } + } + } + } + } + + if (virtualFile == null) { + return false; + } + + final Project finalProject = project; + final VirtualFile finalVirtualFile = virtualFile; + AppUIUtil.invokeLaterIfProjectAlive(project, new Runnable() { + @Override + public void run() { + navigate(finalProject, finalVirtualFile, line, column, focused); + } + }); + return true; + } + + @NotNull + private static Promise openAbsolutePath(@NotNull final File file, final int line, final int column, final boolean focused) { + if (!file.exists()) { + return Promise.reject(NOT_FOUND); + } + + final AsyncPromise promise = new AsyncPromise(); + ApplicationManager.getApplication().invokeLater(new Runnable() { + @Override + public void run() { + try { + VirtualFile virtualFile; + AccessToken token = WriteAction.start(); + try { + virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file); + } + finally { + token.finish(); + } + + if (virtualFile == null) { + promise.setError(NOT_FOUND); + } + else { + navigate(ProjectUtil.guessProjectForContentFile(virtualFile), virtualFile, line, column, focused); + promise.setResult(null); + } + } + catch (Throwable e) { + promise.setError(e); + } + } + }); + return promise; + } + + private static final class OpenFileTask { + final String path; + final int line; + final int column; + + final AsyncPromise promise = new AsyncPromise(); + + OpenFileTask(@NotNull String path, int line, int column) { + this.path = path; + this.line = line; + this.column = column; + } + } + + private static class OpenFileRequest { + public String file; + public int line; + public int column; + + public boolean focused; + } +} diff --git a/platform/built-in-server/src/org/jetbrains/ide/OpenFileXmlRpcHandler.java b/platform/built-in-server/src/org/jetbrains/ide/OpenFileXmlRpcHandler.java index 52617bf9a43e..4d6e8457f32f 100644 --- a/platform/built-in-server/src/org/jetbrains/ide/OpenFileXmlRpcHandler.java +++ b/platform/built-in-server/src/org/jetbrains/ide/OpenFileXmlRpcHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2012 JetBrains s.r.o. + * Copyright 2000-2015 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,30 +15,11 @@ */ package org.jetbrains.ide; -import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.fileEditor.FileEditorManager; -import com.intellij.openapi.fileEditor.FileEditorProvider; -import com.intellij.openapi.fileEditor.OpenFileDescriptor; -import com.intellij.openapi.fileEditor.ex.FileEditorProviderManager; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.project.ProjectLocator; -import com.intellij.openapi.project.ProjectManager; -import com.intellij.openapi.util.Computable; -import com.intellij.openapi.util.Pair; -import com.intellij.openapi.util.io.FileUtil; -import com.intellij.openapi.vcs.ProjectLevelVcsManager; -import com.intellij.openapi.vcs.VcsRoot; -import com.intellij.openapi.vfs.LocalFileSystem; -import com.intellij.openapi.vfs.VirtualFile; -import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.concurrency.Promise; -import java.io.File; - -/** - * @author mike - */ -public class OpenFileXmlRpcHandler { +class OpenFileXmlRpcHandler { private static final Logger LOG = Logger.getInstance(OpenFileXmlRpcHandler.class); // XML-RPC interface method - keep the signature intact @@ -55,75 +36,7 @@ public class OpenFileXmlRpcHandler { return doOpen(path, line, column); } - private static boolean doOpen(final String path, final int line, final int column) { - ApplicationManager.getApplication().invokeLater(new Runnable() { - @Override - public void run() { - Pair data = new File(path).isAbsolute() ? findByAbsolutePath(path) : findByRelativePath(path); - if (data == null) return; - - FileEditorProvider[] providers = FileEditorProviderManager.getInstance().getProviders(data.second, data.first); - if (providers.length == 0) return; - - OpenFileDescriptor descriptor = new OpenFileDescriptor(data.second, data.first, line, column); - FileEditorManager.getInstance(data.second).openTextEditor(descriptor, true); - } - }); - return true; + private static boolean doOpen(@NotNull String path, int line, int column) { + return HttpRequestHandler.EP_NAME.findExtension(OpenFileHttpService.class).openFile(path, line, column, false).getState() != Promise.State.REJECTED; } - - @Nullable - private static Pair findByAbsolutePath(String path) { - File file = new File(FileUtil.toSystemDependentName(path)); - if (file.exists()) { - VirtualFile vFile = findVirtualFile(file); - if (vFile != null) { - Project project = ProjectLocator.getInstance().guessProjectForFile(vFile); - if (project != null) { - return Pair.create(vFile, project); - } - } - } - - return null; - } - - @Nullable - private static Pair findByRelativePath(String path) { - Project[] projects = ProjectManager.getInstance().getOpenProjects(); - String localPath = FileUtil.toSystemDependentName(path); - - for (Project project : projects) { - File file = new File(project.getBasePath(), localPath); - if (file.exists()) { - VirtualFile vFile = findVirtualFile(file); - return vFile != null ? Pair.create(vFile, project) : null; - } - } - - for (Project project : projects) { - for (VcsRoot vcsRoot : ProjectLevelVcsManager.getInstance(project).getAllVcsRoots()) { - VirtualFile root = vcsRoot.getPath(); - if (root != null) { - File file = new File(FileUtil.toSystemDependentName(root.getPath()), localPath); - if (file.exists()) { - VirtualFile vFile = findVirtualFile(file); - return vFile != null ? Pair.create(vFile, project) : null; - } - } - } - } - - return null; - } - - @Nullable - private static VirtualFile findVirtualFile(final File file) { - return ApplicationManager.getApplication().runWriteAction(new Computable() { - @Override - public VirtualFile compute() { - return LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file); - } - }); - } -} +} \ No newline at end of file diff --git a/platform/platform-impl/src/com/intellij/platform/ProjectSetRequestHandler.coffee b/platform/built-in-server/src/org/jetbrains/ide/ProjectSetRequestHandler.coffee similarity index 100% rename from platform/platform-impl/src/com/intellij/platform/ProjectSetRequestHandler.coffee rename to platform/built-in-server/src/org/jetbrains/ide/ProjectSetRequestHandler.coffee diff --git a/platform/built-in-server/src/org/jetbrains/ide/ProjectSetRequestHandler.java b/platform/built-in-server/src/org/jetbrains/ide/ProjectSetRequestHandler.java index 4044bf6d92c9..085eaf90d415 100644 --- a/platform/built-in-server/src/org/jetbrains/ide/ProjectSetRequestHandler.java +++ b/platform/built-in-server/src/org/jetbrains/ide/ProjectSetRequestHandler.java @@ -36,8 +36,8 @@ import java.io.IOException; * @apiGroup Platform * @apiDescription Checkout a repository from source control and then open an IDEA project from it. * - * @apiParam (properties) {Object} vcs The map of the VCS. - * @apiParam (properties) {String} project The path to project to be opened. + * @apiParam {Object} vcs The map of the VCS. + * @apiParam {String} project The path to project to be opened. * * @apiUse OpenProjectSetRequestExample * @apiUse OpenProjectSetRequestExampleMulti diff --git a/platform/built-in-server/src/org/jetbrains/ide/RestService.java b/platform/built-in-server/src/org/jetbrains/ide/RestService.java index e9ad4c0c62d9..185b5c13f78a 100644 --- a/platform/built-in-server/src/org/jetbrains/ide/RestService.java +++ b/platform/built-in-server/src/org/jetbrains/ide/RestService.java @@ -22,6 +22,8 @@ import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; import com.intellij.openapi.project.ProjectManager; import com.intellij.openapi.util.io.BufferExposingByteArrayOutputStream; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.util.text.StringUtilRt; import com.intellij.openapi.vfs.CharsetToolkit; import com.intellij.openapi.wm.IdeFocusManager; import com.intellij.openapi.wm.IdeFrame; @@ -29,6 +31,7 @@ import com.intellij.util.ExceptionUtil; import com.intellij.util.containers.ContainerUtil; import io.netty.buffer.ByteBufInputStream; import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.*; import org.jetbrains.annotations.NotNull; @@ -50,7 +53,7 @@ import java.util.List; */ public abstract class RestService extends HttpRequestHandler { protected static final Logger LOG = Logger.getInstance(RestService.class); - private static final String PREFIX = "api"; + public static final String PREFIX = "api"; @Override public final boolean isSupported(@NotNull FullHttpRequest request) { @@ -142,7 +145,7 @@ public abstract class RestService extends HttpRequestHandler { } @Nullable - protected static Project guessProject() { + protected static Project getLastFocusedOrOpenedProject() { IdeFrame lastFocusedFrame = IdeFocusManager.getGlobalInstance().getLastFocusedFrame(); Project project = lastFocusedFrame == null ? null : lastFocusedFrame.getProject(); if (project == null) { @@ -153,7 +156,18 @@ public abstract class RestService extends HttpRequestHandler { } protected static void sendOk(@NotNull FullHttpRequest request, @NotNull ChannelHandlerContext context) { - Responses.send(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK), context.channel(), request); + sendStatus(HttpResponseStatus.OK, HttpHeaders.isKeepAlive(request), context.channel()); + } + + protected static void sendStatus(@NotNull HttpResponseStatus status, boolean keepAlive, @NotNull Channel channel) { + DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status); + HttpHeaders.setContentLength(response, 0); + Responses.addCommonHeaders(response); + Responses.addNoCache(response); + if (keepAlive) { + HttpHeaders.setKeepAlive(response, true); + } + Responses.send(response, channel, !keepAlive); } protected static void send(@NotNull BufferExposingByteArrayOutputStream byteOut, @NotNull FullHttpRequest request, @NotNull ChannelHandlerContext context) { @@ -162,6 +176,15 @@ public abstract class RestService extends HttpRequestHandler { Responses.send(response, context.channel(), request); } + @Nullable + protected static String getStringParameter(@NotNull String name, @NotNull QueryStringDecoder urlDecoder) { + return ContainerUtil.getLastItem(urlDecoder.parameters().get(name)); + } + + protected static int getIntParameter(@NotNull String name, @NotNull QueryStringDecoder urlDecoder) { + return StringUtilRt.parseInt(StringUtil.nullize(ContainerUtil.getLastItem(urlDecoder.parameters().get(name)), true), -1); + } + protected static boolean getBooleanParameter(@NotNull String name, @NotNull QueryStringDecoder urlDecoder) { List values = urlDecoder.parameters().get(name); if (ContainerUtil.isEmpty(values)) { diff --git a/platform/built-in-server/src/org/jetbrains/io/Responses.java b/platform/built-in-server/src/org/jetbrains/io/Responses.java index 6e05a1acd605..6a8197b5fc15 100644 --- a/platform/built-in-server/src/org/jetbrains/io/Responses.java +++ b/platform/built-in-server/src/org/jetbrains/io/Responses.java @@ -109,7 +109,7 @@ public final class Responses { send(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.copiedBuffer(content, charset)), channel, request); } - private static void send(@NotNull HttpResponse response, @NotNull Channel channel, boolean close) { + public static void send(@NotNull HttpResponse response, @NotNull Channel channel, boolean close) { if (!channel.isActive()) { return; } diff --git a/platform/built-in-server/testSrc/FixtureRule.kt b/platform/built-in-server/testSrc/FixtureRule.kt new file mode 100644 index 000000000000..e6437c7bc5f0 --- /dev/null +++ b/platform/built-in-server/testSrc/FixtureRule.kt @@ -0,0 +1,35 @@ +package org.jetbrains.ide + +import com.intellij.testFramework.PlatformTestCase +import com.intellij.testFramework.fixtures.IdeaTestFixtureFactory +import org.junit.rules.ExternalResource +import javax.swing.SwingUtilities +import com.intellij.openapi.application.WriteAction + +fun invokeAndWaitIfNeed(runnable: () -> Unit) { + if (SwingUtilities.isEventDispatchThread()) runnable() else SwingUtilities.invokeAndWait(runnable) +} + +inline fun runWriteAction(runnable: () -> Unit) { + val token = WriteAction.start() + try { + runnable() + } + finally { + token.finish() + } +} + +public class FixtureRule() : ExternalResource() { + val projectFixture = IdeaTestFixtureFactory.getFixtureFactory().createLightFixtureBuilder().getFixture() + + override fun before() { + PlatformTestCase.initPlatformLangPrefix() + + invokeAndWaitIfNeed { projectFixture.setUp() } + } + + override fun after() { + invokeAndWaitIfNeed { projectFixture.tearDown() } + } +} \ No newline at end of file diff --git a/platform/built-in-server/testSrc/RestApiTest.kt b/platform/built-in-server/testSrc/RestApiTest.kt new file mode 100644 index 000000000000..6068a03e1ff1 --- /dev/null +++ b/platform/built-in-server/testSrc/RestApiTest.kt @@ -0,0 +1,108 @@ +package org.jetbrains.ide + +import io.netty.handler.codec.http.HttpResponseStatus +import org.junit.Rule +import org.junit.Test +import org.junit.rules.RuleChain + +import org.hamcrest.CoreMatchers.equalTo +import org.jetbrains.ide.TestManager.TestDescriptor +import org.junit.Assert.assertThat +import java.net.URL +import java.net.HttpURLConnection +import com.google.gson.stream.JsonWriter +import java.io.OutputStreamWriter +import com.intellij.openapi.vfs.CharsetToolkit +import java.io.BufferedOutputStream + +public class RestApiTest { + private val fixtureManager = FixtureRule() + private val manager = TestManager(fixtureManager.projectFixture) + + private val _chain = RuleChain + .outerRule(fixtureManager) + .around(manager) + + Rule + public fun getChain(): RuleChain = _chain + + Test + TestDescriptor(filePath = "", status = 400) + public fun fileEmptyRequest() { + doTest() + } + + Test + TestDescriptor(filePath = "foo.txt", relativeToProject = true, status = 200) + public fun relativeToProject() { + doTest() + } + + Test + TestDescriptor(filePath = "foo.txt", relativeToProject = true, line = 1, status = 200) + public fun relativeToProjectWithLine() { + doTest() + } + + Test + TestDescriptor(filePath = "foo.txt", relativeToProject = true, line = 1, column = 13, status = 200) + public fun relativeToProjectWithLineAndColumn() { + doTest() + } + + Test + TestDescriptor(filePath = "fileInExcludedDir.txt", excluded = true, status = 200) + public fun inExcludedDir() { + doTest() + } + + Test + TestDescriptor(filePath = "bar/42/foo.txt", doNotCreate = true, status = 404) + public fun relativeNonExistent() { + doTest() + } + + Test + TestDescriptor(filePath = "_tmp_", doNotCreate = true, status = 404) + public fun absoluteNonExistent() { + doTest() + } + + Test + TestDescriptor(filePath = "_tmp_", status = 200) + public fun absolute() { + doTest() + } + + private fun doTest() { + val serviceUrl = "http://localhost:" + BuiltInServerManager.getInstance().getPort() + "/api/file" + var url = serviceUrl + (if (manager.filePath == null) "" else ("/" + manager.filePath)) + val line = manager.annotation?.line ?: -1 + if (line != -1) { + url += ":$line" + } + val column = manager.annotation?.column ?: -1 + if (column != -1) { + url += ":$column" + } + + var connection = URL(url).openConnection() as HttpURLConnection + val expectedStatus = HttpResponseStatus.valueOf(manager.annotation?.status ?: 200) + assertThat(HttpResponseStatus.valueOf(connection.getResponseCode()), equalTo(expectedStatus)) + + connection = URL("$serviceUrl?file=${manager.filePath ?: ""}&line=$line&column=$column").openConnection() as HttpURLConnection + assertThat(HttpResponseStatus.valueOf(connection.getResponseCode()), equalTo(expectedStatus)) + + connection = URL("$serviceUrl").openConnection() as HttpURLConnection + connection.setRequestMethod("POST") + connection.setDoOutput(true) + val writer = JsonWriter(OutputStreamWriter(BufferedOutputStream(connection.getOutputStream()), CharsetToolkit.UTF8_CHARSET)) + writer.beginObject() + writer.name("file").value(manager.filePath) + writer.name("line").value(line) + writer.name("column").value(column) + writer.endObject() + writer.close() + assertThat(HttpResponseStatus.valueOf(connection.getResponseCode()), equalTo(expectedStatus)) + } +} diff --git a/platform/built-in-server/testSrc/TestManager.kt b/platform/built-in-server/testSrc/TestManager.kt new file mode 100644 index 000000000000..818da87fd8f2 --- /dev/null +++ b/platform/built-in-server/testSrc/TestManager.kt @@ -0,0 +1,116 @@ +package org.jetbrains.ide + +import com.intellij.openapi.vfs.VirtualFile +import org.junit.rules.TestWatcher +import org.junit.runner.Description + +import java.lang.annotation.ElementType +import java.lang.annotation.Retention +import java.lang.annotation.RetentionPolicy +import java.lang.annotation.Target +import com.intellij.openapi.util.text.StringUtil +import com.intellij.testFramework.fixtures.IdeaProjectTestFixture +import com.intellij.openapi.roots.ModuleRootManager +import com.intellij.openapi.roots.ModuleRootModificationUtil +import com.intellij.openapi.util.io.FileUtilRt +import java.io.File + +class TestManager(val projectFixture: IdeaProjectTestFixture) : TestWatcher() { + class object { + val EXCLUDED_DIR_NAME = "excludedDir" + } + + public var annotation: TestDescriptor? = null + + public var filePath: String? = null + private var fileToDelete: VirtualFile? = null + + private var ioFileToDelete: File? = null + + Retention(RetentionPolicy.RUNTIME) + Target(ElementType.METHOD) + annotation public class TestDescriptor(public val filePath: String, + public val line: Int = -1, + public val column: Int = -1, + public val relativeToProject: Boolean = false, + public val excluded: Boolean = false, + public val doNotCreate: Boolean = false, + public val status: Int = 200) + + override fun starting(description: Description) { + annotation = description.getAnnotation(javaClass()) + if (annotation == null) { + return + } + + filePath = StringUtil.nullize(annotation!!.filePath) + if (filePath == null) { + return + } + + if (filePath!! == "_tmp_") { + if (annotation!!.doNotCreate) { + filePath = FileUtilRt.generateRandomTemporaryPath().getPath() + } + else { + ioFileToDelete = FileUtilRt.createTempFile("testFile", ".txt") + filePath = ioFileToDelete!!.getPath() + } + return + } + + if (annotation!!.doNotCreate) { + return + } + + invokeAndWaitIfNeed { + val normalizedFilePath = FileUtilRt.toSystemIndependentName(filePath) + if (annotation!!.relativeToProject) { + val root = projectFixture.getProject().getBaseDir() + runWriteAction { + fileToDelete = root.findOrCreateChildData(this@TestManager, normalizedFilePath) + } + } + else { + if (annotation!!.excluded) { + ModuleRootModificationUtil.updateModel(projectFixture.getModule()) { model -> + val contentEntry = model.getContentEntries()[0] + val contentRoot = contentEntry.getFile()!! + runWriteAction { + contentRoot.findChild(EXCLUDED_DIR_NAME)?.delete(this@TestManager) + fileToDelete = contentRoot.createChildDirectory(this@TestManager, EXCLUDED_DIR_NAME) + fileToDelete!!.createChildData(this@TestManager, normalizedFilePath) + } + contentEntry.addExcludeFolder(fileToDelete!!) + } + + filePath = "$EXCLUDED_DIR_NAME/$filePath" + } + else { + val root = ModuleRootManager.getInstance(projectFixture.getModule()).getSourceRoots()[0] + runWriteAction { + fileToDelete = root.findOrCreateChildData(this@TestManager, normalizedFilePath) + } + } + } + } + } + + override fun finished(description: Description?) { + if (annotation!!.excluded) { + ModuleRootModificationUtil.updateModel(projectFixture.getModule()) { model -> model.getContentEntries()[0].removeExcludeFolder(EXCLUDED_DIR_NAME) } + } + + if (fileToDelete != null) { + invokeAndWaitIfNeed { runWriteAction { fileToDelete?.delete(this@TestManager) } } + fileToDelete = null + } + + if (ioFileToDelete != null && !FileUtilRt.delete(ioFileToDelete)) { + ioFileToDelete!!.deleteOnExit() + } + + annotation = null + filePath = null + } +} \ No newline at end of file diff --git a/platform/platform-resources/src/META-INF/built-in-server.xml b/platform/platform-resources/src/META-INF/built-in-server.xml index da799d064d12..2d4189f1a334 100644 --- a/platform/platform-resources/src/META-INF/built-in-server.xml +++ b/platform/platform-resources/src/META-INF/built-in-server.xml @@ -23,6 +23,7 @@ +