/*
* Copyright (c) 2013-2017 Cinchapi Inc.
*
* 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 com.cinchapi.concourse.server.http.router;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang.StringUtils;
import com.cinchapi.concourse.lang.NaturalLanguage;
import com.cinchapi.concourse.server.ConcourseServer;
import com.cinchapi.concourse.server.GlobalState;
import com.cinchapi.concourse.server.http.RouteArgs;
import com.cinchapi.concourse.server.http.EndpointContainer;
import com.cinchapi.concourse.server.http.HttpRequest;
import com.cinchapi.concourse.server.http.HttpRequests;
import com.cinchapi.concourse.server.http.HttpResponse;
import com.cinchapi.concourse.server.http.JsonEndpoint;
import com.cinchapi.concourse.server.http.errors.BadLoginSyntaxError;
import com.cinchapi.concourse.thrift.AccessToken;
import com.cinchapi.concourse.thrift.TObject;
import com.cinchapi.concourse.thrift.TransactionToken;
import com.cinchapi.concourse.util.ByteBuffers;
import com.cinchapi.concourse.util.Convert;
import com.cinchapi.concourse.util.ObjectUtils;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.Maps;
import com.google.common.primitives.Longs;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSyntaxException;
/**
* The core/default router.
*
* @author Jeff Nelson
*/
public class IndexRouter extends EndpointContainer {
/**
* @apiDefine LoginApi
* @apiGroup Auth
* @apiParam {String} username the username with which to connect
* @apiParam {String} password the password with which to authenticate
* @apiParamExample {json} Sample Credentials:
* {"username": "admin", "password": "admin"}
* @apiSuccess (200) {String} token an authentication token
* @apiSuccess (200) {String} environment the name of the environment to
* which the
* token is associated
* @apiSuccessExample {json} Successful Response:
* {
* "token":
* "V6CzvyRSMEVJM-ECdY1E_MweUA4gGY3o1JwzzmgnvA61xFRpeJWUQVYMQZp417o8hCOfCI-LhgtkKdckz7V3uNUliDphyE28wJbKmEx2ZFh9FfrOnTIx76NJ-FEBxOCMwu5KGBf4UuItutHv8gWqnw=="
* ,
* "environment": "default"
* }
* @apiError (401) Unauthorized The <code>username</code>/
* <code>password</code> combination is invalid
*/
/**
* @api {post} /login Login to default environment
* @apiDescription Provide a JSON object containing credentials to login to
* the default environment of Concourse Server and, if
* successful, receive an auth token for further
* interaction. All core and plugin API endpoints must be
* preceded by a login call. The provided auth token is
* stored in a <code>concourse_db_auth_token</code> cookie;
* however, it is recommended that you supply it using the
* <code>X-Auth-Token-Header</code> on subsequent requests.
* @apiGroup Auth
* @apiName Login
* @apiUse LoginApi
*/
/**
* @api {post} /:environment/login Login to specific environment
* @apiDescription An alternative login endpoint to connect to a specific
* environment. Same rules apply: provide a JSON object
* containing credentials to login to Concourse Server and,
* if successful, receive an auth token for further
* interaction. All core and plugin API endpoints must be
* preceded by a login call. The provided auth token is
* stored in a <code>concourse_db_auth_token</code> cookie;
* however, it is recommended that you supply it using the
* <code>X-Auth-Token-Header</code> on subsequent requests.
* @apiGroup Auth
* @apiName LoginEnvironment
* @apiParam {String} environment the name of the environment to which the
* connection should be made
* @apiUse LoginApi
* @apiVersion 0.5.0
*/
public final JsonEndpoint postLogin = new JsonEndpoint() {
@Override
public Object serve(HttpRequest request, AccessToken creds,
TransactionToken transaction, String environment,
HttpResponse response) throws Exception {
try {
JsonElement body = request.bodyAsJson();
JsonObject credentials;
if(body.isJsonObject()
&& (credentials = (JsonObject) body).has("username")
&& credentials.has("password")) {
ByteBuffer username = ByteBuffers.fromString(
credentials.get("username").getAsString());
ByteBuffer password = ByteBuffers.fromString(
credentials.get("password").getAsString());
AccessToken access = concourse.login(username, password,
environment);
String token = HttpRequests.encodeAuthToken(access,
environment, request);
response.cookie("/", GlobalState.HTTP_AUTH_TOKEN_COOKIE,
token, 900, false);
Map<String, Object> payload = Maps
.newHashMapWithExpectedSize(2);
payload.put("token", token);
payload.put("environment", environment);
return payload;
}
}
catch (JsonSyntaxException e) {}
throw BadLoginSyntaxError.INSTANCE;
}
};
/**
* GET /record/audit?timestamp=<ts>
* GET /record/audit?start=<ts>&end=<te>
*/
public final JsonEndpoint get$RecordAudit = new JsonEndpoint() {
@Override
public Object serve(HttpRequest request, AccessToken creds,
TransactionToken transaction, String environment,
HttpResponse response) throws Exception {
String arg1 = request.getParamValue(":record");
String start = request.getParamValue("start");
String end = request.getParamValue("end");
start = ObjectUtils.firstNonNullOrNull(start,
request.getParamValue("timestamp"));
Long record = Longs.tryParse(arg1);
Object data;
Preconditions.checkArgument(record != null,
"Cannot perform audit on %s because it "
+ "is not a valid record",
arg1);
if(start != null && end != null) {
data = concourse.auditRecordStartEnd(record,
NaturalLanguage.parseMicros(start),
NaturalLanguage.parseMicros(end), creds, transaction,
environment);
}
else if(start != null) {
data = concourse.auditRecordStart(record,
NaturalLanguage.parseMicros(start), creds, transaction,
environment);
}
else {
data = concourse.auditRecord(record, creds, transaction,
environment);
}
return data;
}
};
/**
* @apiDefine AuthHeader
* @apiHeader {String} X-Auth-Token the unique token that is returned from
* the <code>/login</code> endpoint
* @apiError (401) Unauthorized the provided auth token is invalid
*/
/**
* DELETE /record/key
* DELETE /key/record
*/
public final JsonEndpoint delete$Arg1$Arg2 = new JsonEndpoint() {
@Override
public Object serve(HttpRequest request, AccessToken creds,
TransactionToken transaction, String environment,
HttpResponse response) throws Exception {
RouteArgs args = RouteArgs.parse(request.getParamValue(":arg1"),
request.getParamValue(":arg2"));
String key = args.key();
Long record = args.record();
String body = request.body();
if(StringUtils.isBlank(body)) {
concourse.clearKeyRecord(key, record, creds, transaction,
environment);
return NO_DATA;
}
else {
TObject value = Convert
.javaToThrift(Convert.stringToJava(request.body()));
Object data = concourse.removeKeyValueRecord(key, value, record,
creds, transaction, environment);
return data;
}
}
};
/**
* DELETE /:record
*/
public final JsonEndpoint delete$Record = new JsonEndpoint() {
@Override
public Object serve(HttpRequest request, AccessToken creds,
TransactionToken transaction, String environment,
HttpResponse response) throws Exception {
long record = Long.parseLong(request.getParamValue(":record"));
concourse.clearRecord(record, creds, transaction, environment);
return NO_DATA;
}
};
/**
* @api {get} / Return a list of all record ids
* @apiDescription Return all the records that have current or historical
* data as a JSON array containing record ids. This endpoint
* is analogous to the following driver methods:
* <ul>
* <li><code>inventory()</code></li>
* </ul>
*
* @apiName Inventory
* @apiGroup Reading
* @apiUse AuthHeader
*/
/**
* @api {get} / Select all the data from records that match a criteria.
* @apiDescription Select all the data from records that match a criteria as
* an array of JSON objects. This endpoint
* is analogous to the following driver methods:
* <ul>
* <li><code>select(ccl)</code></li>
* <li><code>select(ccl, timestamp)</code></li>
* </ul>
* @apiName SelectCcl
* @apiGroup Reading
* @apiParam {String} query a CCL statement used to filter matching records
* @apiParam {StringOrInteger} [timestamp] the historical timestamp to use
* when reading the data
* @apiUse AuthHeader
*/
/**
* @api {get} / Select values for specific keys from records criteria.
* @apiDescription Return all the values stored for each of the specific
* keys that match the criteria. This endpoint
* is analogous to the following driver methods:
* <ul>
* <li><code>select(key, ccl)</code></li>
* <li><code>select(key, ccl, timestamp)</code></li>
* <li><code>select(keys, ccl)</code></li>
* <li><code>select(keys, ccl, timestamp)</code></li>
* </ul>
* @apiName SelectKeysCcl
* @apiGroup Reading
* @apiParam {String} query a CCL statement used to filter matching records
* @apiParam {String} keys a comma separated list of keys to select from
* each of the matching records
* @apiParam {StringOrInteger} [timestamp] the historical timestamp to use
* when reading the data
* @apiUse AuthHeader
*/
public JsonEndpoint get = new JsonEndpoint() {
@Override
public Object serve(HttpRequest request, AccessToken creds,
TransactionToken transaction, String environment,
HttpResponse response) throws Exception {
String ccl = request.getParamValue("query");
Object data;
if(!Strings.isNullOrEmpty(ccl)) {
List<String> keys = request.getParamValues("select");
String ts = request.getParamValue("timestamp");
Long timestamp = ts != null ? Longs.tryParse(ts) : null;
if(keys.isEmpty()) {
data = timestamp == null
? concourse.selectCcl(ccl, creds, transaction,
environment)
: concourse.selectCclTime(ccl, timestamp, creds,
transaction, environment);
}
else {
data = timestamp == null
? concourse.selectKeysCcl(keys, ccl, creds,
transaction, environment)
: concourse.selectKeysCclTime(keys, ccl, timestamp,
creds, transaction, environment);
}
return data;
}
else {
data = concourse.inventory(creds, null, environment);
return new Gson().toJsonTree(data);
}
}
};
/**
* GET /:arg1
*/
public final JsonEndpoint get$Arg1 = new JsonEndpoint() {
@Override
public Object serve(HttpRequest request, AccessToken creds,
TransactionToken transaction, String environment,
HttpResponse response) throws Exception {
String arg1 = request.getParamValue(":arg1");
String ts = request.getParamValue("timestamp");
Long timestamp = ts == null ? null
: NaturalLanguage.parseMicros(ts);
Long record = Longs.tryParse(arg1);
Object data;
if(record != null) {
data = timestamp == null
? concourse.selectRecord(record, creds, null,
environment)
: concourse.selectRecordTime(record, timestamp, creds,
transaction, environment);
}
else {
data = timestamp == null
? concourse.browseKey(arg1, creds, null, environment)
: concourse.browseKeyTime(arg1, timestamp, creds,
transaction, environment);
}
return data;
}
};
/**
* GET /key/record[?timestamp=<ts>]
* GET /record/key[?timestamp=<ts>]
*/
public final JsonEndpoint get$Arg1$Arg2 = new JsonEndpoint() {
@Override
public Object serve(HttpRequest request, AccessToken creds,
TransactionToken transaction, String environment,
HttpResponse response) throws Exception {
String ts = request.getParamValue("timestamp");
Long timestamp = ts == null ? null
: NaturalLanguage.parseMicros(ts);
String arg1 = request.getParamValue(":arg1");
String arg2 = request.getParamValue(":arg2");
RouteArgs args = RouteArgs.parse(arg1, arg2);
String key = args.key();
Long record = args.record();
Object data;
if(timestamp == null) {
data = concourse.selectKeyRecord(key, record, creds,
transaction, environment);
}
else {
data = concourse.selectKeyRecordTime(key, record, timestamp,
creds, transaction, environment);
}
return data;
}
};
/**
* GET /record/key/audit?timestamp=<ts>
* GET /record/key/audit?start=<ts>&end=<te>
* GET /key/record/audit?timestamp=<ts>
* GET /key/record/audit?start=<ts>&end=<te>
*/
public final JsonEndpoint get$Arg1$Arg2Audit = new JsonEndpoint() {
@Override
public Object serve(HttpRequest request, AccessToken creds,
TransactionToken transaction, String environment,
HttpResponse response) throws Exception {
String arg1 = request.getParamValue(":arg1");
String arg2 = request.getParamValue(":arg2");
String start = request.getParamValueOrAlias("start", "timestamp");
String end = request.getParamValue("end");
RouteArgs args = RouteArgs.parse(arg1, arg2);
String key = args.key();
Long record = args.record();
Preconditions.checkArgument(
record != null && !StringUtils.isBlank(key),
"Cannot perform audit on %s/%s because it "
+ "is not a valid key/record combination",
arg1, arg2);
Object data = null;
if(start != null && end != null) {
data = concourse.auditKeyRecordStartEnd(key, record,
NaturalLanguage.parseMicros(start),
NaturalLanguage.parseMicros(end), creds, transaction,
environment);
}
else if(start != null) {
data = concourse.auditKeyRecordStart(key, record,
NaturalLanguage.parseMicros(start), creds, transaction,
environment);
}
else {
data = concourse.auditKeyRecord(key, record, creds, transaction,
environment);
}
return data;
}
};
/**
* GET /record/key/chronologize?timestamp=<ts>
* GET /record/key/chronologize?start=<ts>&end=<te>
* GET /key/record/chronologize?timestamp=<ts>
* GET /key/record/chronologize?start=<ts>&end=<te>
*/
public final JsonEndpoint get$Arg1$Arg2Chronologize = new JsonEndpoint() {
@Override
public Object serve(HttpRequest request, AccessToken creds,
TransactionToken transaction, String environment,
HttpResponse response) throws Exception {
String arg1 = request.getParamValue(":arg1");
String arg2 = request.getParamValue(":arg2");
String start = request.getParamValueOrAlias("start", "timestamp");
String end = request.getParamValue("end");
RouteArgs args = RouteArgs.parse(arg1, arg2);
String key = args.key();
Long record = args.record();
Object data = null;
if(start == null) {
data = concourse.chronologizeKeyRecord(key, record, creds,
transaction, environment);
}
else if(end == null) {
data = concourse.chronologizeKeyRecordStart(key, record,
NaturalLanguage.parseMicros(start), creds, transaction,
environment);
}
else {
data = concourse.chronologizeKeyRecordStartEnd(key, record,
NaturalLanguage.parseMicros(start),
NaturalLanguage.parseMicros(end), creds, transaction,
environment);
}
return data;
}
};
/**
* GET /record/key/diff?start=<ts>&end=<te>
* GET /key/record/diff?start=<ts>
* GET /record/diff?start=<ts>&end=<te>
* GET /key/diff?start=<ts>&end=<te>
*
* @return
*/
public final JsonEndpoint get$Arg1$Arg2Diff = new JsonEndpoint() {
@Override
public Object serve(HttpRequest request, AccessToken creds,
TransactionToken transaction, String environment,
HttpResponse response) throws Exception {
String arg1 = request.getParamValue(":arg1");
String arg2 = request.getParamValue(":arg2");
String start = request.getParamValue("start");
String end = request.getParamValue("end");
RouteArgs args = RouteArgs.parse(arg1, arg2);
String key = args.key();
Long record = args.record();
Object data = null;
if(key != null && record != null && start != null & end != null) {
data = concourse.diffKeyRecordStartEnd(key, record,
NaturalLanguage.parseMicros(start),
NaturalLanguage.parseMicros(end), creds, transaction,
environment);
}
else if(key != null && record != null
&& start != null & end == null) {
data = concourse.diffKeyRecordStart(key, record,
NaturalLanguage.parseMicros(start), creds, transaction,
environment);
}
else if(key == null && record != null
&& start != null & end != null) {
data = concourse.diffRecordStartEnd(record,
NaturalLanguage.parseMicros(start),
NaturalLanguage.parseMicros(end), creds, transaction,
environment);
}
else if(key != null && record == null
&& start != null & end != null) {
data = concourse.diffKeyStartEnd(key,
NaturalLanguage.parseMicros(start),
NaturalLanguage.parseMicros(end), creds, transaction,
environment);
}
return data;
}
};
/**
* GET /record/key/revert?timestamp=<ts>
* GET /key/record/revert?timestamp=<ts>
*/
public final JsonEndpoint get$Arg1$Arg2Revert = new JsonEndpoint() {
@Override
public Object serve(HttpRequest request, AccessToken creds,
TransactionToken transaction, String environment,
HttpResponse response) throws Exception {
String arg1 = request.getParamValue(":arg1");
String arg2 = request.getParamValue(":arg2");
String ts = request.getParamValue("timestamp");
RouteArgs args = RouteArgs.parse(arg1, arg2);
String key = args.key();
Long record = args.record();
if(key != null && record != null) {
concourse.revertKeyRecordTime(key, record.longValue(),
NaturalLanguage.parseMicros(ts), creds, transaction,
environment);
}
return true;
}
};
/**
* @api {get} /abort Abort transaction
* @apiGroup Transactions
* @apiName Abort
* @apiUse AuthHeader
*/
public final JsonEndpoint getAbort = new JsonEndpoint() {
@Override
public Object serve(HttpRequest request, AccessToken creds,
TransactionToken transaction, String environment,
HttpResponse response) throws Exception {
concourse.abort(creds, transaction, environment);
response.removeCookie(GlobalState.HTTP_TRANSACTION_TOKEN_COOKIE);
return NO_DATA;
}
};
/**
* @api {get} /commit Commit transaction
* @apiGroup Transactions
* @apiName Commit
* @apiSuccess (200) {Boolean} success a boolean that indicates whether the
* transaction successfully committed
* @apiSuccessExample {json} Success-Response:
* true
* @apiUse AuthHeader
*/
public final JsonEndpoint getCommit = new JsonEndpoint() {
@Override
public Object serve(HttpRequest request, AccessToken creds,
TransactionToken transaction, String environment,
HttpResponse response) throws Exception {
boolean result = concourse.commit(creds, transaction, environment);
response.removeCookie(GlobalState.HTTP_TRANSACTION_TOKEN_COOKIE);
return new JsonPrimitive(result);
}
};
/**
* @api {get} /stage Start transaction
* @apiDescription Start a new transaction.
* @apiGroup Transactions
* @apiName Stage
* @apiSuccess (200) {String} transaction the transaction id
* @apiSuccessExample {json} Success-Response:
* {
* "transaction": "1457544886097000"
* }
* @apiUse AuthHeader
*/
public final JsonEndpoint getStage = new JsonEndpoint() {
@Override
public Object serve(HttpRequest request, AccessToken creds,
TransactionToken transaction, String environment,
HttpResponse response) throws Exception {
transaction = concourse.stage(creds, environment);
String token = Long.toString(transaction.timestamp);
response.cookie("/", GlobalState.HTTP_TRANSACTION_TOKEN_COOKIE,
token, 900, false);
JsonObject data = new JsonObject();
data.addProperty("transaction", token);
return data;
}
};
/**
* POST /record/key
* POST /key/record
*/
public final JsonEndpoint post$Arg1$Arg2 = new JsonEndpoint() {
@Override
public Object serve(HttpRequest request, AccessToken creds,
TransactionToken transaction, String environment,
HttpResponse response) throws Exception {
String arg1 = request.getParamValue(":arg1");
String arg2 = request.getParamValue(":arg2");
RouteArgs args = RouteArgs.parse(arg1, arg2);
String key = args.key();
Long record = args.record();
TObject value = Convert
.javaToThrift(Convert.stringToJava(request.body()));
boolean result = concourse.addKeyValueRecord(key, value, record,
creds, transaction, environment);
return result;
}
};
/**
* @api {post} /logout Logout
* @apiDescription End the current session. Afterwards, the provided auth
* token will no longer work.
* @apiGroup Auth
* @apiName Logout
* @apiUse AuthHeader
*/
public final JsonEndpoint postLogout = new JsonEndpoint() {
@Override
public Object serve(HttpRequest request, AccessToken creds,
TransactionToken transaction, String environment,
HttpResponse response) throws Exception {
concourse.logout(creds, environment);
response.removeCookie(GlobalState.HTTP_AUTH_TOKEN_COOKIE);
return NO_DATA;
}
};
/**
* PUT /record/key
* PUT /key/record
*/
public final JsonEndpoint put$Arg1$Arg2 = new JsonEndpoint() {
@Override
public Object serve(HttpRequest request, AccessToken creds,
TransactionToken transaction, String environment,
HttpResponse response) throws Exception {
String arg1 = request.getParamValue(":arg1");
String arg2 = request.getParamValue(":arg2");
RouteArgs args = RouteArgs.parse(arg1, arg2);
String key = args.key();
Long record = args.record();
TObject value = Convert
.javaToThrift(Convert.stringToJava(request.body()));
concourse.setKeyValueRecord(key, value, record, creds, transaction,
environment);
return NO_DATA;
}
};
/**
* POST /
* PUT /
*/
public final JsonEndpoint upsert = new JsonEndpoint() {
@Override
public Object serve(HttpRequest request, AccessToken creds,
TransactionToken transaction, String environment,
HttpResponse response) throws Exception {
String json = request.body();
Set<Long> records = concourse.insertJson(json, creds, transaction,
environment);
return records;
}
};
/**
* POST /:arg1
* PUT /:arg1
*/
public final JsonEndpoint upsert$Arg1 = new JsonEndpoint() {
@Override
public Object serve(HttpRequest request, AccessToken creds,
TransactionToken transaction, String environment,
HttpResponse response) throws Exception {
String arg1 = request.getParamValue(":arg1");
Long record = Longs.tryParse(arg1);
Object result;
if(record != null) {
String json = request.body();
result = concourse.insertJsonRecord(json, record, creds,
transaction, environment);
}
else {
TObject value = Convert
.javaToThrift(Convert.stringToJava(request.body()));
result = concourse.addKeyValue(arg1, value, creds, transaction,
environment);
}
return result;
}
};
/**
* Construct a new instance.
*
* @param concourse
*/
public IndexRouter(ConcourseServer concourse) {
super(concourse);
}
@Override
public int getWeight() {
return Integer.MAX_VALUE;
}
}