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 @@
+