RestService — base class of IDE REST service

DiffHttpService — diff service (/rest/diff) to compare n contents
IDEA-133605 Implement true CORS in IDEA
This commit is contained in:
Vladimir Krivosheev
2015-01-30 21:43:22 +01:00
parent 03a7b0de29
commit a8850f9303
9 changed files with 291 additions and 31 deletions
@@ -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<String, Object>(2);
myAdditional = new ArrayList<Pair<String, DiffRequest>>(0);
myAdditional = new SmartList<Pair<String, DiffRequest>>();
}
public void setToolbarAddons(@NotNull ToolbarAddons toolbarAddons) {
@@ -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.
@@ -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;
}
@@ -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<DiffContent> contents = new ArrayList<DiffContent>();
final List<String> titles = new ArrayList<String>();
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<DiffContent> contents, @NotNull List<String> 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;
}
}
@@ -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;
}
}
@@ -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()));
}
}
@@ -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);
}
}
@@ -339,6 +339,8 @@
<httpRequestHandler implementation="com.intellij.platform.ProjectSetRequestHandler"/>
<postStartupActivity implementation="org.jetbrains.ide.BuiltInServerManagerImpl$MyPostStartupActivity"/>
<httpRequestHandler implementation="org.jetbrains.ide.DiffHttpService"/>
<editorNotificationProvider implementation="com.intellij.openapi.editor.impl.ForcedSoftWrapsNotificationProvider"/>
<editorNotificationProvider implementation="com.intellij.ide.FileChangedNotificationProvider"/>
<editorNotificationProvider implementation="com.intellij.openapi.updateSettings.impl.pluginsAdvertisement.PluginAdvertiserEditorNotificationProvider"/>
@@ -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;