-
-
Save InfoSec812/e95b3fd4a0ec4b83aefecbf151ec06d9 to your computer and use it in GitHub Desktop.
Groovy Verticle using Closures and currying to eliminate duplicate logic
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.sungardas.cc.edison.verticles | |
import static com.sungardas.cc.edison.support.Queries.* | |
import static com.sungardas.cc.vertx.HTTPUtil.* | |
import static groovy.json.JsonOutput.toJson | |
import static io.netty.handler.codec.http.HttpResponseStatus.* | |
import static io.vertx.core.http.HttpMethod.GET | |
import static io.vertx.core.http.HttpMethod.POST | |
import com.sungardas.cc.vertx.handlers.AuthHandler | |
import groovy.transform.PackageScope | |
import io.netty.handler.codec.http.HttpResponseStatus | |
import io.vertx.core.AsyncResult | |
import io.vertx.core.Future | |
import io.vertx.core.Handler | |
import io.vertx.core.json.DecodeException | |
import io.vertx.core.json.JsonArray | |
import io.vertx.core.json.JsonObject | |
import io.vertx.core.logging.Logger | |
import io.vertx.core.logging.LoggerFactory | |
import io.vertx.groovy.core.CompositeFuture | |
import io.vertx.groovy.core.Context | |
import io.vertx.groovy.core.Vertx | |
import io.vertx.groovy.core.buffer.Buffer | |
import io.vertx.groovy.core.eventbus.Message | |
import io.vertx.groovy.ext.web.Router | |
import io.vertx.groovy.ext.web.RoutingContext | |
import io.vertx.lang.groovy.GroovyVerticle | |
import java.util.stream.Collectors | |
/** | |
* REST API Verticle | |
*/ | |
class REST extends GroovyVerticle { | |
private static final Logger LOG = LoggerFactory.getLogger(REST) | |
public static final String QUERY_ADDR = 'cc.v5.database.query' | |
@PackageScope | |
Map config | |
/** | |
* Default constructor with default parameters. Defined this way for easier unit testing | |
* @param vertx The {@link Vertx} instance to be used | |
* @param context The {@link Context} instance to be used | |
*/ | |
REST(final Vertx vertx=null, final Context context=null) { | |
super() | |
this.vertx = vertx?:this.vertx | |
this.context = context?:this.context | |
} | |
@Override | |
void start(final Future<Void> startFuture) throws Exception { | |
config = context.config() | |
final Router rtr = Router.router(vertx) | |
// Map all unhandled exceptions to this exception handler | |
rtr.exceptionHandler(this.&exceptionHandler) | |
// All requests are passed through the auth hander | |
final AuthHandler authHandler = new AuthHandler(vertx, config) | |
rtr.route() .handler(authHandler.&handle) | |
// Create references to the transformer methods and rest handler so that they are short and readable | |
def (rest, dsTransform, vmTransform, diskTransform, simple) = [this.&rest, this.&dsTransform, this.&vmTransform, | |
this.&diskTransform, this.&simpleTranform] | |
// This is jumping through some extra hoops to reduce duplicate code and logic. | |
// Each handler is passed a Closure which is curried with the QUERY and the transformation method (and possibly | |
// auth requirements), thus just leaving the RoutingContext parameter to be passed when the handler calls the | |
// closure. Keeping in mind that the handler() expects a Closure which ONLY accepts a RoutingContext param. | |
// By currying in this manner, the code can be written so that most logic flows through the same path and only | |
// the portions which are different get special treatment (e.g. the transformation methods) | |
// In the `rest` handler, we use the transformation Closure to do any required manipulation of the SQL results | |
rtr.route(GET, '/v3/status*') .handler(this.&status) | |
rtr.route(GET, '/v3/ds/:id') .handler(rest.curry(DATASTORE, dsTransform)) | |
rtr.route(GET, '/v3/cluster/:id') .handler(rest.curry(CLUSTER, simple)) | |
rtr.route(GET, '/v3/network/:id') .handler(rest.curry(NETWORK, simple)) | |
rtr.route(GET, '/v3/customer/:snt_code') .handler(rest.curry(CUSTOMER, simple)) | |
rtr.route(GET, '/v3/vdc/:id') .handler(rest.curry(VDC, simple)) | |
rtr.route(GET, '/v3/vm/:uuid') .handler(rest.curry(VIRTUALMACHINE, vmTransform)) | |
rtr.route(GET, '/v3/vm/:uuid/hdd/:order') .handler(rest.curry(DISK, diskTransform)) | |
rtr.route(GET, '/v3/vdc/:id/password') .handler(rest.curry(VDC_PASSWORD, simple).rcurry('admin')) | |
rtr.route(POST, '/v3/vm/lb/update') .handler(this.&lbUpdate) | |
rtr.route(POST, '/v3/vm/lb/clean/:vdc/:name').handler(this.&lbClean) | |
final serverOpts = [ | |
logActivity: true, | |
host: config.listenAddr, | |
port: config.listenPort, | |
reuseAddress: true, | |
compressionSupported: true | |
] | |
if (config.ssl) { | |
serverOpts['ssl'] = true | |
serverOpts['pemKeyCertOptions'] = [keyPath: config.key, certPath: config.certificate] | |
} | |
LOG.debug('Finished deploying REST Verticle') | |
vertx.createHttpServer(serverOpts).requestHandler(rtr.&accept).listen(startFuture.completer()) | |
} | |
/** | |
* Handles any uncaught exceptions in the Router processing | |
* @param throwable | |
*/ | |
static void exceptionHandler(final Throwable throwable) { | |
LOG.error('Unexpected exception in router', throwable) | |
} | |
/** | |
* Handle HTTP(s) requests for simple REST API GET endpoints. All path params from the request are mapped to SQL | |
* where clause entries | |
* @param ctx The {@link RoutingContext} for the request | |
* @param query The SQL Query to be executed to retrieve the appropriate data | |
* @param handler A {@link Closure} to be called on the SQL result row in order to do an required transformations | |
* before sending the response to the HTTP client | |
* @param requireAuth Enable or disable authentication for the given route | |
* @param requiredRole If more than just authentication is required, allow the required role to be specified | |
*/ | |
@PackageScope | |
void rest(final String query, final Closure transform, final RoutingContext ctx, | |
final Boolean requireAuth = true, final String requiredRole = null) { | |
if (isAuthorized(requireAuth, requiredRole, ctx)) { | |
final params = [] | |
final conditions = ctx.pathParams() // Get path parameters Map | |
.entrySet() // Convert Map to List<Map.Entry> | |
.stream() // Iterate over path parameters | |
.peek({ params.add(it.value) }) // Add each parameter value to the parameters list | |
.map({ "`${it.key}`=?" }) // Add each parameter key to the WHERE clause | |
.collect(Collectors.toList()) // Collect results into a List<String> | |
.join(' AND ') // Join the items together with AND statements | |
final msg = new JsonObject([query: "${query} WHERE ${conditions}".toString(), params: params]) | |
vertx.eventBus().send(QUERY_ADDR, msg, this.&respond.curry(ctx).rcurry(transform) as Handler) | |
} else { | |
unauthorized(ctx, new JsonObject([ | |
error: 'Unauthorized', | |
details: "${requiredRole} role is required for LB update" | |
])) | |
} | |
} | |
/** | |
* Determine if a user is authorized | |
* @param requireAuth Enable/Disable auth checking | |
* @param requiredRole The name of the role required for this authorization | |
* @param ctx The {@link RoutingContext} which contains the user details like the roles | |
* @return {@code true} if authorized, otherwise {@link false} | |
*/ | |
boolean isAuthorized(final boolean requireAuth, final String requiredRole, final RoutingContext ctx) { | |
def inRequiredRole = (requiredRole != null) && (ctx.get('user')?.roles[requiredRole]) | |
return config.disableAuth || !requireAuth || inRequiredRole | |
} | |
/** | |
* A REST endpoint which will return the current status of the application | |
* @param ctx The {@link RoutingContext} for the current request | |
*/ | |
@PackageScope | |
void status(final RoutingContext ctx) { | |
def authVal = ctx?.get('auth') | |
final authorized = (authVal !=null && authVal) | |
def msg = new JsonObject([auth: authorized]) | |
vertx.eventBus().send('cc.v5.edison.status', msg, this.&statusResponseHandler.curry(ctx)) | |
} | |
/** | |
* Sends the results from the Status Verticle back to the HTTP client | |
* @param ctx The {@link RoutingContext} for the current request | |
* @param res The response from the Status Verticle | |
*/ | |
static void statusResponseHandler(final RoutingContext ctx, final AsyncResult<Message<Map>> res) { | |
HttpResponseStatus status = OK | |
JsonObject result | |
if (res.succeeded()) { | |
result = new JsonObject(res.result().body()) | |
} else { | |
status = INTERNAL_SERVER_ERROR | |
result = new JsonObject(toJson(res.cause())) | |
} | |
final jsonBody = result.encodePrettily() | |
buildJsonResponse(ctx, status).end(jsonBody) | |
} | |
/** | |
* Map the SQL results of a DataStore query into an appropriate datastore object for serialization | |
* @param dsMap The SQL result row as a Map | |
* @return The map after having processed the DataStore specific fields into the correct format | |
*/ | |
Map dsTransform(final Map dsMap) { | |
final prefacedSelf = "${config.baseUrl}${dsMap.self}".toString() | |
dsMap.self = prefacedSelf | |
final vdcList = Arrays.stream(((String) dsMap.vdc).split(',')) | |
.map({ "${config.baseUrl}/v3/vdc/${it}".toString() }) | |
.collect(Collectors.toList()) | |
dsMap.vdc = vdcList | |
return dsMap | |
} | |
/** | |
* Map the SQL results of a VirtualMachine query into an appropriate vm object for serialization | |
* @param vmMap The SQL result row as a Map | |
* @return The map after having processed the VirtualMachine specific fields into the correct format | |
*/ | |
Map vmTransform(final Map vmMap) { | |
final prefacedSelf = "${config.baseUrl}${vmMap.self}".toString() | |
vmMap.self = prefacedSelf | |
final ifString = "${vmMap.interfaces}".replaceAll(/,$/, '') | |
final interfaces = new JsonArray("[${ifString}]") | |
vmMap.interfaces = interfaces | |
return vmMap | |
} | |
/** | |
* Map the SQL results of a Disk query into an appropriate disk object for serialization | |
* @param diskMap The SQL result row as a Map | |
* @return The map after having processed the Disk specific fields into the correct format | |
*/ | |
Map diskTransform(final Map diskMap) { | |
final prefacedSelf = "${config.baseUrl}${diskMap.self}".toString() | |
final datastore = "${config.baseUrl}${diskMap.datastore}".toString() | |
diskMap.self = prefacedSelf | |
diskMap.datastore = datastore | |
return diskMap | |
} | |
/** | |
* Map the SQL results of a document query into an appropriate object for serialization | |
* @param body The SQL result row as a Map | |
* @return The map after having processed the DataStore specific fields into the correct format | |
*/ | |
Map simpleTranform(Map body) { | |
final prefacedSelf = "${config.baseUrl}${body.self}".toString() | |
body.self = prefacedSelf | |
return body | |
} | |
/** | |
* Handle simple DB query responses which require no special mapping | |
* @param ctx The {@link RoutingContext} for the request | |
* @param reply The database result message from the Database verticle | |
*/ | |
@PackageScope | |
void respond(final RoutingContext ctx, final AsyncResult<Message<Map>> reply, | |
final Closure transform = this.&simpleTranform) { | |
if (reply.succeeded()) { | |
final body = reply.result().body() | |
if (body) { | |
ok(ctx, new JsonObject(transform.call(body) as Map)) | |
} else { | |
notFound(ctx) | |
} | |
} else { | |
serverError(ctx, reply.cause()) | |
} | |
} | |
/** | |
* Handle requests for LB update | |
* @param ctx The {@link RoutingContext} for the request | |
*/ | |
@PackageScope | |
void lbUpdate(final RoutingContext ctx) { | |
if (ctx?.get('user')?.roles?.admin) { | |
ctx.request().bodyHandler({ final body -> lbUpdateBodyHandler(body, ctx) }) | |
} else { | |
unauthorized(ctx, new JsonObject([error: 'Unauthorized', details: 'ADMIN role is required for LB update'])) | |
} | |
} | |
/** | |
* Handles the body content once the entire POST body has been recieved. | |
* @param body A {@link Buffer} containing the POST body content | |
* @param ctx The {@link RoutingContext} for the request | |
*/ | |
@PackageScope | |
void lbUpdateBodyHandler(final Buffer body, final RoutingContext ctx) { | |
try { | |
final update = body.toJsonObject() | |
final params = [ | |
update.uuid, | |
update.oldName, | |
update.os, | |
update.template, | |
update.newName | |
] | |
final query = LB_UPDATE | |
final resultHandler = { final AsyncResult<Message> result -> handleLBUpdateResult(ctx, result, update) } | |
vertx.eventBus().send(QUERY_ADDR, new JsonObject([query: query, params: params]), resultHandler) | |
} catch (final DecodeException de) { | |
LOG.error('Unable to decode POST body a JSON', de) | |
badRequest(ctx, de) | |
} | |
} | |
/** | |
* Handle the DB response for an LB Update operation | |
* @param ctx The {@link RoutingContext} for the request | |
* @param res The {@link AsyncResult} which indicates if the process succeeded or failed. | |
* @param update The updated record as returned by the DB | |
*/ | |
@PackageScope | |
void handleLBUpdateResult(final RoutingContext ctx, final AsyncResult res, final Map update) { | |
if (res.succeeded()) { | |
final body = [:] | |
body.putAll(update) | |
body.remove('oldName') | |
final prefacedSelf = "${config.baseUrl}${body.self}".toString() | |
body.self = prefacedSelf | |
buildJsonResponse(ctx, ACCEPTED).end(new JsonObject(body).encodePrettily()) | |
} else { | |
LOG.error('Unable to update LB details: {}', res.cause(), new JsonObject(update).encodePrettily()) | |
serverError(ctx, res.cause()) | |
} | |
} | |
/** | |
* Handle a LB Cleanup request | |
* @param ctx The {@link RoutingContext} for the request | |
*/ | |
@PackageScope | |
void lbClean(final RoutingContext ctx) { | |
if (ctx?.get('user')?.roles?.admin) { | |
final queries = [ | |
CD_DELETE, | |
HDD_DELETE, | |
VM_DELETE | |
] | |
final futureList = [] | |
queries.each { | |
final future = io.vertx.groovy.core.Future.future() | |
futureList.add(future) | |
final query = [query: it, params: [ctx.request().getParam('vdc'), ctx.request().getParam('name')]] | |
vertx.eventBus().send(QUERY_ADDR, query, future.completer()) | |
} | |
CompositeFuture.join(futureList).setHandler({ final res -> handleLBCleanResult(ctx, res) }) | |
} else { | |
unauthorized(ctx, new JsonObject([error: 'Unauthorized', details: 'ADMIN role is required for LB Clean'])) | |
} | |
} | |
/** | |
* Handle the Async result from the LB Clean query | |
* @param ctx The {@link RoutingContext} for the request | |
* @param res The {@link AsyncResult} which indicates if the process succeeded or failed. | |
*/ | |
static void handleLBCleanResult(RoutingContext ctx, AsyncResult res) { | |
if (res.succeeded()) { | |
buildJsonResponse(ctx, ACCEPTED).end() | |
} else { | |
serverError(ctx, res.cause()) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment