ProjectSetRequestHandler as REST API
[idea/community.git] / platform / platform-impl / src / org / jetbrains / ide / RestService.java
1 /*
2  * Copyright 2000-2015 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package org.jetbrains.ide;
17
18 import com.google.gson.stream.JsonReader;
19 import com.google.gson.stream.MalformedJsonException;
20 import com.intellij.openapi.diagnostic.Logger;
21 import com.intellij.openapi.project.Project;
22 import com.intellij.openapi.project.ProjectManager;
23 import com.intellij.openapi.util.io.BufferExposingByteArrayOutputStream;
24 import com.intellij.openapi.vfs.CharsetToolkit;
25 import com.intellij.openapi.wm.IdeFocusManager;
26 import com.intellij.openapi.wm.IdeFrame;
27 import com.intellij.util.ExceptionUtil;
28 import com.intellij.util.containers.ContainerUtil;
29 import io.netty.buffer.ByteBufInputStream;
30 import io.netty.buffer.Unpooled;
31 import io.netty.channel.ChannelHandlerContext;
32 import io.netty.handler.codec.http.*;
33 import org.jetbrains.annotations.NotNull;
34 import org.jetbrains.annotations.Nullable;
35 import org.jetbrains.io.Responses;
36
37 import java.io.IOException;
38 import java.io.InputStreamReader;
39 import java.util.List;
40
41 /**
42  * Document your service using <a href="http://apidocjs.com">apiDoc</a>. To extract big example from source code, consider to use *.coffee file near your source file.
43  * (or Python/Ruby, but coffee recommended because it's plugin is lightweight). See {@link AboutHttpService} for example.
44  */
45 public abstract class RestService extends HttpRequestHandler {
46   protected static final Logger LOG = Logger.getInstance(RestService.class);
47
48   @Override
49   public final boolean isSupported(@NotNull FullHttpRequest request) {
50     if (!isMethodSupported(request.method())) {
51       return false;
52     }
53
54     String uri = request.uri();
55
56     if (isPrefixlessAllowed() && checkPrefix(uri, getServiceName())) {
57       return true;
58     }
59
60     String prefix = "rest";
61     String serviceName = getServiceName();
62     int minLength = 1 + prefix.length() + 1 + serviceName.length();
63     if (uri.length() >= minLength &&
64         uri.charAt(0) == '/' &&
65         uri.regionMatches(true, 1, prefix, 0, prefix.length()) &&
66         uri.regionMatches(true, 2 + prefix.length(), serviceName, 0, serviceName.length())) {
67       if (uri.length() == minLength) {
68         return true;
69       }
70       else {
71         char c = uri.charAt(minLength);
72         return c == '/' || c == '?';
73       }
74     }
75     return false;
76   }
77
78   /**
79    * Service url must be "/rest/$serviceName", but to preserve backward compatibility, prefixless path could be also supported
80    */
81   protected boolean isPrefixlessAllowed() {
82     return false;
83   }
84
85   @NotNull
86   /**
87    * Use human-readable name or UUID if it is an internal service.
88    */
89   protected abstract String getServiceName();
90
91   protected abstract boolean isMethodSupported(@NotNull HttpMethod method);
92
93   @Override
94   public final boolean process(@NotNull QueryStringDecoder urlDecoder, @NotNull FullHttpRequest request, @NotNull ChannelHandlerContext context) throws IOException {
95     try {
96       String error = execute(urlDecoder, request, context);
97       if (error != null) {
98         Responses.sendStatus(HttpResponseStatus.BAD_REQUEST, context.channel(), error, request);
99       }
100     }
101     catch (Throwable e) {
102       HttpResponseStatus status;
103       // JsonReader exception
104       //noinspection InstanceofCatchParameter
105       if (e instanceof MalformedJsonException || (e instanceof IllegalStateException && e.getMessage().startsWith("Expected a "))) {
106         LOG.warn(e);
107         status = HttpResponseStatus.BAD_REQUEST;
108       }
109       else {
110         LOG.error(e);
111         status = HttpResponseStatus.INTERNAL_SERVER_ERROR;
112       }
113       Responses.sendStatus(status, context.channel(), ExceptionUtil.getThrowableText(e), request);
114     }
115     return true;
116   }
117
118   @Nullable("error text or null if successful")
119   /**
120    * Return error or send response using {@link #sendOk(FullHttpRequest, ChannelHandlerContext)}, {@link #send(BufferExposingByteArrayOutputStream, FullHttpRequest, ChannelHandlerContext)}
121    */
122   public abstract String execute(@NotNull QueryStringDecoder urlDecoder, @NotNull FullHttpRequest request, @NotNull ChannelHandlerContext context) throws IOException;
123
124   @NotNull
125   protected static JsonReader createJsonReader(@NotNull FullHttpRequest request) {
126     JsonReader reader = new JsonReader(new InputStreamReader(new ByteBufInputStream(request.content()), CharsetToolkit.UTF8_CHARSET));
127     reader.setLenient(true);
128     return reader;
129   }
130
131   @Nullable
132   protected static Project guessProject() {
133     IdeFrame lastFocusedFrame = IdeFocusManager.getGlobalInstance().getLastFocusedFrame();
134     Project project = lastFocusedFrame == null ? null : lastFocusedFrame.getProject();
135     if (project == null) {
136       Project[] openProjects = ProjectManager.getInstance().getOpenProjects();
137       return openProjects.length > 0 ? openProjects[0] : null;
138     }
139     return project;
140   }
141
142   protected static void sendOk(@NotNull FullHttpRequest request, @NotNull ChannelHandlerContext context) {
143     Responses.send(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK), context.channel(), request);
144   }
145
146   protected static void send(@NotNull BufferExposingByteArrayOutputStream byteOut, @NotNull FullHttpRequest request, @NotNull ChannelHandlerContext context) {
147     HttpResponse response = Responses.response("application/json", Unpooled.wrappedBuffer(byteOut.getInternalBuffer(), 0, byteOut.size()));
148     Responses.addNoCache(response);
149     Responses.send(response, context.channel(), request);
150   }
151
152   protected static boolean getBooleanParameter(@NotNull String name, @NotNull QueryStringDecoder urlDecoder) {
153     List<String> values = urlDecoder.parameters().get(name);
154     if (ContainerUtil.isEmpty(values)) {
155       return false;
156     }
157
158     String value = values.get(values.size() - 1);
159     // if just name specified, so, true
160     return value.isEmpty() || Boolean.parseBoolean(value);
161   }
162 }