/*
* 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.util.io.BufferExposingByteArrayOutputStream;
import com.intellij.openapi.vfs.CharsetToolkit;
import com.intellij.openapi.wm.IdeFocusManager;
import com.intellij.openapi.wm.IdeFrame;
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.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;
import java.util.List;
/**
* Document your service using apiDoc. To extract big example from source code, consider to use *.coffee file near your source file.
* (or Python/Ruby, but coffee recommended because it's plugin is lightweight). See {@link AboutHttpService} for example.
*/
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 uri = request.uri();
if (isPrefixlessAllowed() && checkPrefix(uri, getServiceName())) {
return true;
}
String prefix = "rest";
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);
return c == '/' || c == '?';
}
}
return false;
}
/**
* Service url must be "/rest/$serviceName", but to preserve backward compatibility, prefixless path could be also supported
*/
protected boolean isPrefixlessAllowed() {
return false;
}
@NotNull
/**
* Use human-readable name or UUID if it is an internal service.
*/
protected abstract String getServiceName();
protected abstract boolean isMethodSupported(@NotNull HttpMethod method);
@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.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")
/**
* Return error or send response using {@link #sendOk(FullHttpRequest, ChannelHandlerContext)}, {@link #send(BufferExposingByteArrayOutputStream, FullHttpRequest, ChannelHandlerContext)}
*/
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;
}
protected static void sendOk(@NotNull FullHttpRequest request, @NotNull ChannelHandlerContext context) {
Responses.send(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK), context.channel(), request);
}
protected static void send(@NotNull BufferExposingByteArrayOutputStream byteOut, @NotNull FullHttpRequest request, @NotNull ChannelHandlerContext context) {
HttpResponse response = Responses.response("application/json", Unpooled.wrappedBuffer(byteOut.getInternalBuffer(), 0, byteOut.size()));
Responses.addNoCache(response);
Responses.send(response, context.channel(), request);
}
protected static boolean getBooleanParameter(@NotNull String name, @NotNull QueryStringDecoder urlDecoder) {
List values = urlDecoder.parameters().get(name);
if (ContainerUtil.isEmpty(values)) {
return false;
}
String value = values.get(values.size() - 1);
// if just name specified, so, true
return value.isEmpty() || Boolean.parseBoolean(value);
}
}