From a8850f9303a586708e6d92bb2c5d6073130c40fe Mon Sep 17 00:00:00 2001 From: Vladimir Krivosheev Date: Fri, 30 Jan 2015 21:41:03 +0100 Subject: [PATCH] =?UTF-8?q?RestService=20=E2=80=94=20base=20class=20of=20I?= =?UTF-8?q?DE=20REST=20service=20DiffHttpService=20=E2=80=94=20diff=20serv?= =?UTF-8?q?ice=20(/rest/diff)=20to=20compare=20n=20contents=20IDEA-133605?= =?UTF-8?q?=20Implement=20true=20CORS=20in=20IDEA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../intellij/openapi/diff/DiffRequest.java | 3 +- .../org/jetbrains/ide/HttpRequestHandler.java | 2 +- .../com/intellij/ide/XmlRpcServerImpl.java | 5 - .../org/jetbrains/ide/DiffHttpService.java | 158 ++++++++++++++++++ .../src/org/jetbrains/ide/RestService.java | 118 +++++++++++++ .../src/org/jetbrains/io/NettyUtil.java | 4 +- .../src/org/jetbrains/io/Responses.java | 20 +-- .../src/META-INF/PlatformExtensions.xml | 2 + .../builtInWebServer/BuiltInWebServer.java | 10 +- 9 files changed, 291 insertions(+), 31 deletions(-) create mode 100644 platform/platform-impl/src/org/jetbrains/ide/DiffHttpService.java create mode 100644 platform/platform-impl/src/org/jetbrains/ide/RestService.java diff --git a/platform/platform-api/src/com/intellij/openapi/diff/DiffRequest.java b/platform/platform-api/src/com/intellij/openapi/diff/DiffRequest.java index 2262948ae269..4bb6a45862fe 100644 --- a/platform/platform-api/src/com/intellij/openapi/diff/DiffRequest.java +++ b/platform/platform-api/src/com/intellij/openapi/diff/DiffRequest.java @@ -20,6 +20,7 @@ import com.intellij.openapi.actionSystem.IdeActions; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Factory; import com.intellij.openapi.util.Pair; +import com.intellij.util.SmartList; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -45,7 +46,7 @@ public abstract class DiffRequest { protected DiffRequest(@Nullable Project project) { myProject = project; myGenericData = new HashMap(2); - myAdditional = new ArrayList>(0); + myAdditional = new SmartList>(); } public void setToolbarAddons(@NotNull ToolbarAddons toolbarAddons) { diff --git a/platform/platform-api/src/org/jetbrains/ide/HttpRequestHandler.java b/platform/platform-api/src/org/jetbrains/ide/HttpRequestHandler.java index d3d3803c61f4..7ac77ba49554 100644 --- a/platform/platform-api/src/org/jetbrains/ide/HttpRequestHandler.java +++ b/platform/platform-api/src/org/jetbrains/ide/HttpRequestHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2000-2013 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. diff --git a/platform/platform-impl/src/com/intellij/ide/XmlRpcServerImpl.java b/platform/platform-impl/src/com/intellij/ide/XmlRpcServerImpl.java index 3b8bacee5457..5057e5729d3c 100644 --- a/platform/platform-impl/src/com/intellij/ide/XmlRpcServerImpl.java +++ b/platform/platform-impl/src/com/intellij/ide/XmlRpcServerImpl.java @@ -115,11 +115,6 @@ public class XmlRpcServerImpl implements XmlRpcServer { Responses.send(Responses.response("text/xml", result), context.channel(), request); return true; } - else if (HttpMethod.POST.name().equals(request.headers().get("Access-Control-Request-Method"))) { - LOG.assertTrue(request.method() == HttpMethod.OPTIONS); - Responses.sendOptionsResponse("POST, OPTIONS", request, context); - return true; - } return false; } diff --git a/platform/platform-impl/src/org/jetbrains/ide/DiffHttpService.java b/platform/platform-impl/src/org/jetbrains/ide/DiffHttpService.java new file mode 100644 index 000000000000..e91418e6103e --- /dev/null +++ b/platform/platform-impl/src/org/jetbrains/ide/DiffHttpService.java @@ -0,0 +1,158 @@ +/* + * 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.stream.JsonReader; +import com.intellij.ide.impl.ProjectUtil; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.diff.DiffContent; +import com.intellij.openapi.diff.DiffManager; +import com.intellij.openapi.diff.DiffRequest; +import com.intellij.openapi.diff.SimpleContent; +import com.intellij.openapi.fileTypes.FileType; +import com.intellij.openapi.fileTypes.FileTypeRegistry; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.util.ArrayUtil; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.QueryStringDecoder; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +final class DiffHttpService extends RestService { + @Override + protected boolean isMethodSupported(@NotNull HttpMethod method) { + return method == HttpMethod.POST; + } + + @NotNull + @Override + protected String getServiceName() { + return "diff"; + } + + @Override + @Nullable + public String execute(@NotNull QueryStringDecoder urlDecoder, @NotNull FullHttpRequest request, @NotNull ChannelHandlerContext context) throws IOException { + final List contents = new ArrayList(); + final List titles = new ArrayList(); + + JsonReader reader = createJsonReader(request); + if (reader.hasNext()) { + String fileType = null; + boolean reformat; + reader.beginObject(); + while (reader.hasNext()) { + String name = reader.nextName(); + if (name.equals("fileTypeName")) { + fileType = reader.nextString(); + } + else if (name.equals("contents")) { + String error = readContent(reader, contents, titles, fileType); + if (error != null) { + return error; + } + } + else { + reader.skipValue(); + } + } + reader.endObject(); + } + + if (contents.isEmpty()) { + return "Empty request"; + } + + final Project project = guessProject(); + if (project == null) { + // Argument for @NotNull parameter 'project' of com/intellij/openapi/components/ServiceManager.getService must not be null + return "No opened project, please open any project"; + } + + ApplicationManager.getApplication().invokeLater(new Runnable() { + @Override + public void run() { + DiffManager.getInstance().getDiffTool().show(new DiffRequest(project) { + @NotNull + @Override + public DiffContent[] getContents() { + return contents.toArray(new DiffContent[contents.size()]); + } + + @Override + public String[] getContentTitles() { + return ArrayUtil.toStringArray(titles); + } + + @Override + public String getWindowTitle() { + return "Diff Service"; + } + }); + + ProjectUtil.focusProjectWindow(project, true); + } + }, project.getDisposed()); + return null; + } + + @Nullable + private static String readContent(@NotNull JsonReader reader, @NotNull List contents, @NotNull List titles, @Nullable String defaultFileTypeName) throws IOException { + FileTypeRegistry fileTypeRegistry = FileTypeRegistry.getInstance(); + + FileType defaultFileType = defaultFileTypeName == null ? null : fileTypeRegistry.findFileTypeByName(defaultFileTypeName); + reader.beginArray(); + while (reader.hasNext()) { + String title = null; + String fileType = null; + String content = null; + + reader.beginObject(); + while (reader.hasNext()) { + String name = reader.nextName(); + if (name.equals("title")) { + title = reader.nextString(); + } + else if (name.equals("fileTypeName")) { + fileType = reader.nextString(); + } + else if (name.equals("content")) { + content = reader.nextString(); + } + else { + reader.skipValue(); + } + } + reader.endObject(); + + if (content == null) {; + return "content is not specified"; + } + + contents.add(new SimpleContent(content, fileType == null ? defaultFileType : fileTypeRegistry.findFileTypeByName(fileType))); + titles.add(StringUtil.isEmptyOrSpaces(title) ? "" : title); + } + reader.endArray(); + return null; + } +} diff --git a/platform/platform-impl/src/org/jetbrains/ide/RestService.java b/platform/platform-impl/src/org/jetbrains/ide/RestService.java new file mode 100644 index 000000000000..7c21f5d90900 --- /dev/null +++ b/platform/platform-impl/src/org/jetbrains/ide/RestService.java @@ -0,0 +1,118 @@ +/* + * 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.stream.JsonReader; +import com.google.gson.stream.MalformedJsonException; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.project.ProjectManager; +import com.intellij.openapi.vfs.CharsetToolkit; +import com.intellij.openapi.wm.IdeFocusManager; +import com.intellij.openapi.wm.IdeFrame; +import com.intellij.util.ExceptionUtil; +import io.netty.buffer.ByteBufInputStream; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.io.Responses; + +import java.io.IOException; +import java.io.InputStreamReader; + +public abstract class RestService extends HttpRequestHandler { + protected static final Logger LOG = Logger.getInstance(RestService.class); + + @Override + public final boolean isSupported(@NotNull FullHttpRequest request) { + if (!isMethodSupported(request.method())) { + return false; + } + + String prefix = "rest"; + String uri = request.uri(); + String serviceName = getServiceName(); + int minLength = 1 + prefix.length() + 1 + serviceName.length(); + if (uri.length() >= minLength && + uri.charAt(0) == '/' && + uri.regionMatches(true, 1, prefix, 0, prefix.length()) && + uri.regionMatches(true, 2 + prefix.length(), serviceName, 0, serviceName.length())) { + if (uri.length() == minLength) { + return true; + } + else { + char c = uri.charAt(minLength + 1); + return c == '/' || c == '?'; + } + } + return false; + } + + protected abstract boolean isMethodSupported(@NotNull HttpMethod method); + + @NotNull + protected abstract String getServiceName(); + + @Override + public final boolean process(@NotNull QueryStringDecoder urlDecoder, @NotNull FullHttpRequest request, @NotNull ChannelHandlerContext context) throws IOException { + try { + String error = execute(urlDecoder, request, context); + if (error == null) { + Responses.send(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK), context.channel(), request); + } + else { + Responses.sendStatus(HttpResponseStatus.BAD_REQUEST, context.channel(), error, request); + } + } + catch (Throwable e) { + HttpResponseStatus status; + // JsonReader exception + //noinspection InstanceofCatchParameter + if (e instanceof MalformedJsonException || (e instanceof IllegalStateException && e.getMessage().startsWith("Expected a "))) { + LOG.warn(e); + status = HttpResponseStatus.BAD_REQUEST; + } + else { + LOG.error(e); + status = HttpResponseStatus.INTERNAL_SERVER_ERROR; + } + Responses.sendStatus(status, context.channel(), ExceptionUtil.getThrowableText(e), request); + } + return true; + } + + @Nullable("error text or null if successful") + public abstract String execute(@NotNull QueryStringDecoder urlDecoder, @NotNull FullHttpRequest request, @NotNull ChannelHandlerContext context) throws IOException; + + @NotNull + protected static JsonReader createJsonReader(@NotNull FullHttpRequest request) { + JsonReader reader = new JsonReader(new InputStreamReader(new ByteBufInputStream(request.content()), CharsetToolkit.UTF8_CHARSET)); + reader.setLenient(true); + return reader; + } + + @Nullable + protected static Project guessProject() { + IdeFrame lastFocusedFrame = IdeFocusManager.getGlobalInstance().getLastFocusedFrame(); + Project project = lastFocusedFrame == null ? null : lastFocusedFrame.getProject(); + if (project == null) { + Project[] openProjects = ProjectManager.getInstance().getOpenProjects(); + return openProjects.length > 0 ? openProjects[0] : null; + } + return project; + } +} diff --git a/platform/platform-impl/src/org/jetbrains/io/NettyUtil.java b/platform/platform-impl/src/org/jetbrains/io/NettyUtil.java index 763fe3670fdb..95abea992b77 100644 --- a/platform/platform-impl/src/org/jetbrains/io/NettyUtil.java +++ b/platform/platform-impl/src/org/jetbrains/io/NettyUtil.java @@ -29,6 +29,8 @@ import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.channel.socket.oio.OioSocketChannel; import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http.cors.CorsConfig; +import io.netty.handler.codec.http.cors.CorsHandler; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.ide.PooledThreadExecutor; @@ -174,6 +176,6 @@ public final class NettyUtil { } public static void addHttpServerCodec(ChannelPipeline pipeline) { - pipeline.addLast(new HttpServerCodec(), new HttpObjectAggregator(MAX_CONTENT_LENGTH)); + pipeline.addLast(new HttpServerCodec(), new HttpObjectAggregator(MAX_CONTENT_LENGTH), new CorsHandler(CorsConfig.withAnyOrigin().build())); } } \ No newline at end of file diff --git a/platform/platform-impl/src/org/jetbrains/io/Responses.java b/platform/platform-impl/src/org/jetbrains/io/Responses.java index 02d1d76ca1ad..096f1295489e 100644 --- a/platform/platform-impl/src/org/jetbrains/io/Responses.java +++ b/platform/platform-impl/src/org/jetbrains/io/Responses.java @@ -24,9 +24,9 @@ import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; -import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.*; import io.netty.util.CharsetUtil; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.nio.charset.Charset; @@ -63,10 +63,6 @@ public final class Responses { return response; } - public static void addAllowAnyOrigin(HttpResponse response) { - response.headers().add(HttpHeaders.Names.ACCESS_CONTROL_ALLOW_ORIGIN, "*"); - } - public static void addDate(HttpResponse response) { if (!response.headers().contains(HttpHeaders.Names.DATE)) { addDate(response, Calendar.getInstance().getTime()); @@ -99,7 +95,7 @@ public final class Responses { } } - public static void send(HttpResponse response, Channel channel, @Nullable HttpRequest request) { + public static void send(@NotNull HttpResponse response, Channel channel, @Nullable HttpRequest request) { if (response.status() != HttpResponseStatus.NOT_MODIFIED && !HttpHeaders.isContentLengthSet(response)) { HttpHeaders.setContentLength(response, response instanceof FullHttpResponse ? ((FullHttpResponse)response).content().readableBytes() : 0); @@ -120,7 +116,6 @@ public final class Responses { public static void addCommonHeaders(HttpResponse response) { addServer(response); addDate(response); - addAllowAnyOrigin(response); } public static void send(CharSequence content, Channel channel, @Nullable HttpRequest request) { @@ -131,7 +126,7 @@ public final class Responses { send(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.copiedBuffer(content, charset)), channel, request); } - private static void send(HttpResponse response, Channel channel, boolean close) { + private static void send(@NotNull HttpResponse response, @NotNull Channel channel, boolean close) { if (!channel.isActive()) { return; } @@ -154,7 +149,7 @@ public final class Responses { sendStatus(responseStatus, channel, null, request); } - public static void sendStatus(HttpResponseStatus responseStatus, Channel channel, @Nullable String description, @Nullable HttpRequest request) { + public static void sendStatus(@NotNull HttpResponseStatus responseStatus, Channel channel, @Nullable String description, @Nullable HttpRequest request) { send(createStatusResponse(responseStatus, request, description), channel, request); } @@ -175,11 +170,4 @@ public final class Responses { response.headers().set(HttpHeaders.Names.CONTENT_TYPE, "text/html"); return response; } - - public static void sendOptionsResponse(String allowHeaders, HttpRequest request, ChannelHandlerContext context) { - HttpResponse response = response(HttpResponseStatus.OK); - response.headers().set(HttpHeaders.Names.ACCESS_CONTROL_ALLOW_METHODS, allowHeaders); - response.headers().set(HttpHeaders.Names.ALLOW, allowHeaders); - send(response, context.channel(), request); - } } \ No newline at end of file diff --git a/platform/platform-resources/src/META-INF/PlatformExtensions.xml b/platform/platform-resources/src/META-INF/PlatformExtensions.xml index 5bd41b391f08..8327124dc5f7 100644 --- a/platform/platform-resources/src/META-INF/PlatformExtensions.xml +++ b/platform/platform-resources/src/META-INF/PlatformExtensions.xml @@ -339,6 +339,8 @@ + + diff --git a/xml/impl/src/org/jetbrains/builtInWebServer/BuiltInWebServer.java b/xml/impl/src/org/jetbrains/builtInWebServer/BuiltInWebServer.java index 9b68cf78a714..735e3232b36a 100644 --- a/xml/impl/src/org/jetbrains/builtInWebServer/BuiltInWebServer.java +++ b/xml/impl/src/org/jetbrains/builtInWebServer/BuiltInWebServer.java @@ -42,7 +42,8 @@ import java.io.IOException; import java.net.InetAddress; import java.net.UnknownHostException; -import static org.jetbrains.io.Responses.*; +import static org.jetbrains.io.Responses.addKeepAliveIfNeed; +import static org.jetbrains.io.Responses.sendStatus; public final class BuiltInWebServer extends HttpRequestHandler { static final Logger LOG = Logger.getInstance(BuiltInWebServer.class); @@ -77,16 +78,11 @@ public final class BuiltInWebServer extends HttpRequestHandler { @Override public boolean isSupported(@NotNull FullHttpRequest request) { - return super.isSupported(request) || request.method() == HttpMethod.POST || request.method() == HttpMethod.OPTIONS; + return super.isSupported(request) || request.method() == HttpMethod.POST; } @Override public boolean process(@NotNull QueryStringDecoder urlDecoder, @NotNull FullHttpRequest request, @NotNull ChannelHandlerContext context) { - if (request.method() == HttpMethod.OPTIONS) { - sendOptionsResponse("GET, POST, HEAD, OPTIONS", request, context); - return true; - } - String host = HttpHeaders.getHost(request); if (StringUtil.isEmpty(host)) { return false;