/**
* 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.apache.aurora.scheduler.http.api;
import java.io.OutputStreamWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Objects;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.StreamingOutput;
import com.google.common.collect.Lists;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSyntaxException;
import org.apache.aurora.gen.AuroraAdmin.Iface;
import org.apache.aurora.scheduler.storage.entities.AuroraAdminMetadata;
import org.apache.aurora.scheduler.thrift.Responses;
import org.apache.aurora.scheduler.thrift.aop.AnnotatedAuroraAdmin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.apache.aurora.scheduler.http.api.GsonMessageBodyHandler.GSON;
/**
* A scheduler interface that allows interaction with the thrift API via traditional JSON,
* rather than thrift's preferred means which uses field IDs.
*/
@Path(ApiBeta.PATH)
public class ApiBeta {
static final String PATH = "/apibeta";
private static final Logger LOG = LoggerFactory.getLogger(ApiBeta.class);
private final Iface api;
@Inject
ApiBeta(AnnotatedAuroraAdmin api) {
this.api = Objects.requireNonNull(api);
}
private JsonElement getJsonMember(JsonObject json, String memberName) {
return (json == null) ? null : json.get(memberName);
}
private static Response errorResponse(Status status, String message) {
return Response.status(status)
.entity(Responses.error(message))
.build();
}
private static Response badRequest(String message) {
return errorResponse(Status.BAD_REQUEST, message);
}
/**
* Parses method parameters into the appropriate types. For a method call to be successful,
* the elements supplied in the request must match the names of those specified in the thrift
* method definition. If a method parameter does not exist in the request object, {@code null}
* will be substituted.
*
* @param json Incoming request data, to translate into method parameters.
* @param method The thrift method to bind parameters for.
* @return Parsed method parameters.
* @throws WebApplicationException If a parameter could not be parsed.
*/
private Object[] readParams(JsonObject json, Method method)
throws WebApplicationException {
List<Object> params = Lists.newArrayList();
for (Parameter param : method.getParameters()) {
try {
params.add(GSON.fromJson(getJsonMember(json, param.getName()), param.getType()));
} catch (JsonParseException e) {
throw new WebApplicationException(
e,
badRequest("Failed to parse parameter " + param + ": " + e.getMessage()));
}
}
return params.toArray();
}
private Method getApiMethod(String name, Class<?>[] parameterTypes) {
try {
return Iface.class.getMethod(name, parameterTypes);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
@POST
@Path("{method}")
@Produces(MediaType.APPLICATION_JSON)
public Response invoke(@PathParam("method") String methodName, String postData) {
LOG.debug("Call to {} with data: {}", methodName, postData);
// First, verify that this is a valid method on the interface.
Class<?>[] methodParameterTypes = AuroraAdminMetadata.METHODS.get(methodName);
if (methodParameterTypes == null) {
return errorResponse(Status.NOT_FOUND, "Method " + methodName + " does not exist.");
}
JsonObject parameters;
try {
JsonElement json = GSON.fromJson(postData, JsonElement.class);
// The parsed object will be null if there was no post data. This is okay, since that is
// expected for a zero-parameter method.
if (json != null && !(json instanceof JsonObject)) {
throw new WebApplicationException(
badRequest("Request data must be a JSON object of method parameters."));
}
parameters = (JsonObject) json;
} catch (JsonSyntaxException e) {
throw new WebApplicationException(e, badRequest("Request must be valid JSON"));
}
final Method method = getApiMethod(methodName, methodParameterTypes);
final Object[] params = readParams(parameters, method);
return Response.ok((StreamingOutput) output -> {
try {
Object response = method.invoke(api, params);
try (OutputStreamWriter out = new OutputStreamWriter(output, StandardCharsets.UTF_8)) {
GSON.toJson(response, out);
}
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}).build();
}
@GET
@Produces(MediaType.TEXT_HTML)
public Response getIndex() {
return Response.seeOther(URI.create("/apihelp/index.html")).build();
}
}