package org.rakam.analysis;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.netty.handler.codec.http.HttpResponseStatus;
import org.rakam.analysis.ProjectHttpService.Collection;
import org.rakam.collection.SchemaField;
import org.rakam.plugin.ContinuousQuery;
import org.rakam.report.QueryExecutorService;
import org.rakam.report.QueryResult;
import org.rakam.server.http.HttpService;
import org.rakam.server.http.RakamHttpRequest;
import org.rakam.server.http.annotations.Api;
import org.rakam.server.http.annotations.ApiOperation;
import org.rakam.server.http.annotations.ApiParam;
import org.rakam.server.http.annotations.ApiResponse;
import org.rakam.server.http.annotations.ApiResponses;
import org.rakam.server.http.annotations.Authorization;
import org.rakam.server.http.annotations.BodyParam;
import org.rakam.server.http.annotations.IgnoreApi;
import org.rakam.server.http.annotations.JsonRequest;
import org.rakam.util.SuccessMessage;
import org.rakam.util.RakamException;
import javax.inject.Inject;
import javax.inject.Named;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR;
import static java.lang.Boolean.TRUE;
import static org.rakam.analysis.ApiKeyService.AccessKeyType.MASTER_KEY;
@Path("/continuous-query")
@Api(value = "/continuous-query", nickname = "continuousQuery", description = "Continuous Query", tags = "continuous-query")
public class ContinuousQueryHttpService extends HttpService {
private final ContinuousQueryService service;
private final QueryExecutorService queryExecutorService;
private final QueryHttpService queryHttpService;
@Inject
public ContinuousQueryHttpService(ContinuousQueryService service, QueryHttpService queryHttpService, QueryExecutorService queryExecutorService) {
this.service = service;
this.queryExecutorService = queryExecutorService;
this.queryHttpService = queryHttpService;
}
public static final String PARTITION_KEY_INVALID = "Partition keys are not valid.";
@JsonRequest
@ApiOperation(value = "Create stream", authorizations = @Authorization(value = "master_key"), notes =
"Creates a new continuous query for specified SQL query.\n" +
"Rakam will process data in batches keep the result of query in-memory all the time.\n" +
"Compared to reports, continuous queries continuously aggregate the data on the fly and the result is always available either in-memory or disk.")
@ApiResponses(value = {@ApiResponse(code = 400, message = PARTITION_KEY_INVALID)})
@Path("/create")
public CompletableFuture<SuccessMessage> createQuery(@Named("project") String project, @ApiParam("continuous_query") ContinuousQuery report, @ApiParam(value = "replay", required = false) Boolean replay) {
if (service.test(project, report.query)) {
CompletableFuture<SuccessMessage> err = new CompletableFuture<>();
// TODO: more readable message is needed.
err.completeExceptionally(new RakamException("Query is not valid.", BAD_REQUEST));
}
CompletableFuture<List<SchemaField>> schemaFuture = queryExecutorService.metadata(project, report.query);
return schemaFuture.thenApply(schema -> {
if (report.partitionKeys.stream().filter(key -> !schema.stream().anyMatch(a -> a.getName().equals(key))).findAny().isPresent()) {
throw new RakamException(PARTITION_KEY_INVALID, BAD_REQUEST);
}
try {
QueryResult f = service.create(project, report, TRUE.equals(replay)).getResult().join();
return SuccessMessage.map(f);
} catch (IllegalArgumentException e) {
throw new RakamException(e.getMessage(), BAD_REQUEST);
}
});
}
@JsonRequest
@ApiOperation(value = "List queries", authorizations = @Authorization(value = "read_key"))
@Path("/list")
public List<ContinuousQuery> listQueries(@Named("project") String project) {
return service.list(project);
}
@JsonRequest
@ApiOperation(value = "Get query schema", authorizations = @Authorization(value = "read_key"))
@Path("/schema")
public List<Collection> getSchemaOfQuery(@Named("project") String project,
@ApiParam(value = "names", required = false) List<String> names) {
Map<String, List<SchemaField>> schemas = service.getSchemas(project);
if (schemas == null) {
throw new RakamException("Project does not exist", HttpResponseStatus.NOT_FOUND);
}
Stream<Collection> collectionStream = schemas.entrySet().stream()
.map(entry -> new Collection(entry.getKey(), entry.getValue()));
if (names != null) {
collectionStream = collectionStream.filter(a -> names.contains(a.name));
}
return collectionStream.collect(Collectors.toList());
}
@JsonRequest
@ApiOperation(value = "Delete stream", authorizations = @Authorization(value = "master_key"))
@Path("/delete")
public CompletableFuture<SuccessMessage> deleteQuery(@Named("project") String project,
@ApiParam("table_name") String tableName) {
return service.delete(project, tableName).thenApply(success -> {
if (success) {
return SuccessMessage.success();
} else {
throw new RakamException("Error while deleting.", INTERNAL_SERVER_ERROR);
}
});
}
@ApiOperation(value = "Delete stream", authorizations = @Authorization(value = "master_key"))
@Path("/refresh")
@Consumes("text/event-stream")
@GET
@IgnoreApi
public void refreshQuery(RakamHttpRequest request) {
queryHttpService.handleServerSentQueryExecution(request, RefreshQuery.class,
(project, q) -> service.refresh(project, q.table_name),
MASTER_KEY, false);
}
public static class RefreshQuery {
public final String table_name;
@JsonCreator
public RefreshQuery(@ApiParam("table_name") String table_name) {
this.table_name = table_name;
}
}
@JsonRequest
@ApiOperation(value = "Test continuous query", authorizations = @Authorization(value = "read_key"))
@Path("/test")
public boolean testQuery(@Named("project") String project, @ApiParam("query") String query) {
return service.test(project, query);
}
@JsonRequest
@ApiOperation(value = "Get continuous query", authorizations = @Authorization(value = "read_key"))
@Path("/get")
public ContinuousQuery getQuery(@Named("project") String project, @ApiParam("table_name") String tableName) {
return service.get(project, tableName);
}
}