blob: 880398c24fc4403d6a5786994e5a3ec645942a91 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.cassandra.sidecar.routes;
import java.util.NoSuchElementException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpStatusClass;
import io.vertx.core.Handler;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.net.SocketAddress;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.HttpException;
import org.apache.cassandra.sidecar.adapters.base.exception.OperationUnavailableException;
import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
import org.apache.cassandra.sidecar.common.data.Name;
import org.apache.cassandra.sidecar.common.data.QualifiedTableName;
import org.apache.cassandra.sidecar.common.exceptions.JmxAuthenticationException;
import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
import org.apache.cassandra.sidecar.utils.CassandraInputValidator;
import org.apache.cassandra.sidecar.utils.InstanceMetadataFetcher;
import static org.apache.cassandra.sidecar.utils.HttpExceptions.wrapHttpException;
/**
* An abstract {@link Handler Handler<RoutingContext>} that provides common functionality for handler
* implementations.
*
* @param <T> The type of the request object
*/
public abstract class AbstractHandler<T> implements Handler<RoutingContext>
{
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
protected static final String INSTANCE_ID = "instanceId";
protected static final String KEYSPACE_PATH_PARAM = "keyspace";
protected static final String TABLE_PATH_PARAM = "table";
protected final InstanceMetadataFetcher metadataFetcher;
protected final ExecutorPools executorPools;
protected final CassandraInputValidator validator;
/**
* Constructs a handler with the provided {@code metadataFetcher}
*
* @param metadataFetcher the interface to retrieve instance metadata
* @param executorPools the executor pools for blocking executions
* @param validator a validator instance to validate Cassandra-specific input
*/
protected AbstractHandler(InstanceMetadataFetcher metadataFetcher, ExecutorPools executorPools,
CassandraInputValidator validator)
{
this.metadataFetcher = metadataFetcher;
this.executorPools = executorPools;
this.validator = validator;
}
/**
* {@inheritDoc}
*/
@Override
public void handle(RoutingContext context)
{
HttpServerRequest request = context.request();
String host = host(context);
SocketAddress remoteAddress = request.remoteAddress();
T requestParams = null;
try
{
requestParams = extractParamsOrThrow(context);
logger.debug("{} received request={}, remoteAddress={}, instance={}",
this.getClass().getSimpleName(), requestParams, remoteAddress, host);
handleInternal(context, request, host, remoteAddress, requestParams);
}
catch (Exception exception)
{
processFailure(exception, context, host, remoteAddress, requestParams);
}
}
/**
* Extracts the request object from the {@code context}.
*
* @param context the request context
* @return the request object built from the {@code context}
*/
protected abstract T extractParamsOrThrow(RoutingContext context);
/**
* Handles the request with the parameters for this request.
*
* @param context the request context
* @param httpRequest the {@link HttpServerRequest} object
* @param host the host where this request is intended for
* @param remoteAddress the address where the request originates
* @param request the request object
*/
protected abstract void handleInternal(RoutingContext context,
HttpServerRequest httpRequest,
String host,
SocketAddress remoteAddress,
T request);
/**
* Returns the host from the path if the requests contains the {@code /instance/} path parameter,
* otherwise it returns the host parsed from the request.
*
* @param context the routing context
* @return the host for the routing context
* @throws HttpException when the {@code /instance/} path parameter is {@code null}
*/
public String host(RoutingContext context)
{
if (context.request().params().contains(INSTANCE_ID))
{
String instanceIdParam = context.request().getParam(INSTANCE_ID);
if (instanceIdParam == null)
{
throw new HttpException(HttpResponseStatus.BAD_REQUEST.code(),
"InstanceId query parameter must be provided");
}
InstanceMetadata instance;
try
{
int instanceId = Integer.parseInt(instanceIdParam);
instance = metadataFetcher.instance(instanceId);
}
catch (NumberFormatException ex)
{
throw new HttpException(HttpResponseStatus.BAD_REQUEST.code(),
"InstanceId query parameter must be a valid integer");
}
catch (NoSuchElementException | IllegalStateException ex)
{
throw new HttpException(HttpResponseStatus.NOT_FOUND.code(), ex.getMessage());
}
return instance.host();
}
else
{
return extractHostAddressWithoutPort(context.request().host());
}
}
/**
* Processes the failure while handling the request.
*
* @param cause the cause
* @param context the routing context
* @param host the host where this request is intended for
* @param remoteAddress the address where the request originates
* @param request the request object
*/
protected void processFailure(Throwable cause, RoutingContext context, String host, SocketAddress remoteAddress,
T request)
{
HttpException httpException = determineHttpException(cause);
if (HttpStatusClass.CLIENT_ERROR.contains(httpException.getStatusCode()))
{
logger.warn("{} request failed due to client error. request={}, remoteAddress={}, instance={}",
this.getClass().getSimpleName(), request, remoteAddress, host, cause);
}
else if (HttpStatusClass.SERVER_ERROR.contains(httpException.getStatusCode()))
{
logger.error("{} request failed due to server error. request={}, remoteAddress={}, instance={}",
this.getClass().getSimpleName(), request, remoteAddress, host, cause);
}
context.fail(httpException);
}
protected HttpException determineHttpException(Throwable cause)
{
if (cause instanceof HttpException)
{
return (HttpException) cause;
}
if (cause instanceof JmxAuthenticationException)
{
return wrapHttpException(HttpResponseStatus.SERVICE_UNAVAILABLE, cause);
}
if (cause instanceof OperationUnavailableException)
{
return wrapHttpException(HttpResponseStatus.SERVICE_UNAVAILABLE, cause.getMessage(), cause);
}
return wrapHttpException(HttpResponseStatus.INTERNAL_SERVER_ERROR, cause);
}
/**
* Returns the validated {@link QualifiedTableName} from the context, where the both keyspace and table name
* are required.
*
* @param context the event to handle
* @return the validated {@link QualifiedTableName} from the context
*/
protected QualifiedTableName qualifiedTableName(RoutingContext context)
{
return qualifiedTableName(context, true);
}
/**
* Returns the validated {@link QualifiedTableName} from the context.
*
* @param context the event to handle
* @param required whether keyspace and table name are required
* @return the validated {@link QualifiedTableName} from the context
*/
protected QualifiedTableName qualifiedTableName(RoutingContext context, boolean required)
{
return new QualifiedTableName(keyspace(context, required),
tableName(context, required));
}
/**
* Returns the validated keyspace name from the context.
*
* @param context the event to handle
* @param required whether the keyspace is required
* @return the validated keyspace name from the context
*/
protected Name keyspace(RoutingContext context, boolean required)
{
String keyspace = context.pathParam(KEYSPACE_PATH_PARAM);
if (required || keyspace != null)
{
return validator.validateKeyspaceName(keyspace);
}
return null;
}
/**
* Returns the validated table name from the context.
*
* @param context the event to handle
* @param required whether the table name is required
* @return the validated table name from the context
*/
private Name tableName(RoutingContext context, boolean required)
{
String tableName = context.pathParam(TABLE_PATH_PARAM);
if (required || tableName != null)
{
return validator.validateTableName(tableName);
}
return null;
}
/**
* Given a combined host address like 127.0.0.1:9042 or [2001:db8:0:0:0:ff00:42:8329]:9042, this method
* removes port information and returns 127.0.0.1 or 2001:db8:0:0:0:ff00:42:8329.
*
* @param address ip address
* @return host address without port information
*/
public static String extractHostAddressWithoutPort(String address)
{
if (address.contains(":"))
{
// just ipv6 host name present without port information
if (address.split(":").length > 2 && !address.startsWith("["))
{
return address;
}
String host = address.substring(0, address.lastIndexOf(':'));
// remove brackets from ipv6 addresses
return host.startsWith("[") ? host.substring(1, host.length() - 1) : host;
}
return address;
}
}