Skip to content

Commit

Permalink
Merge pull request #168 from tsegismont/jira/HWKMETRICS-29
Browse files Browse the repository at this point in the history
HWKMETRICS-29 Create method return codes should be consistent
  • Loading branch information
jsanda committed Mar 16, 2015
2 parents 31b5d52 + 5532aeb commit 65b7e16
Show file tree
Hide file tree
Showing 9 changed files with 431 additions and 125 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE;
import static org.hawkular.metrics.core.api.MetricsService.DEFAULT_TENANT_ID;

import java.net.URI;
import java.util.Collection;
import java.util.DoubleSummaryStatistics;
import java.util.HashMap;
Expand All @@ -48,15 +49,19 @@
import javax.ws.rs.QueryParam;
import javax.ws.rs.container.AsyncResponse;
import javax.ws.rs.container.Suspended;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;

import org.hawkular.metrics.api.jaxrs.callback.MetricCreatedCallback;
import org.hawkular.metrics.api.jaxrs.callback.NoDataCallback;
import org.hawkular.metrics.api.jaxrs.callback.SimpleDataCallback;
import org.hawkular.metrics.core.api.Availability;
import org.hawkular.metrics.core.api.AvailabilityMetric;
import org.hawkular.metrics.core.api.Counter;
import org.hawkular.metrics.core.api.Metric;
import org.hawkular.metrics.core.api.MetricAlreadyExistsException;
import org.hawkular.metrics.core.api.MetricId;
import org.hawkular.metrics.core.api.MetricType;
import org.hawkular.metrics.core.api.MetricsService;
Expand Down Expand Up @@ -96,33 +101,72 @@ public class MetricHandler {
@ApiOperation(value = "Create numeric metric definition.", notes = "Clients are not required to explicitly create "
+ "a metric before storing data. Doing so however allows clients to prevent naming collisions and to "
+ "specify tags and data retention.")
@ApiResponses(value = { @ApiResponse(code = 200, message = "Metric definition created successfully"),
@ApiResponse(code = 400, message = "Metric with given id already exists or request is otherwise incorrect",
response = ApiError.class),
@ApiResponses(value = {
@ApiResponse(code = 201, message = "Metric definition created successfully"),
@ApiResponse(code = 400, message = "Missing or invalid payload"),
@ApiResponse(code = 409, message = "Numeric metric with given id already exists"),
@ApiResponse(code = 500, message = "Metric definition creation failed due to an unexpected error",
response = ApiError.class)
})
public void createNumericMetric(@Suspended final AsyncResponse asyncResponse,
@PathParam("tenantId") String tenantId, @ApiParam(required = true) NumericMetric metric) {
@PathParam("tenantId") String tenantId,
@ApiParam(required = true) NumericMetric metric,
@Context UriInfo uriInfo
) {
if (metric == null) {
Response response = Response.status(Status.BAD_REQUEST).entity(new ApiError("Payload is empty")).build();
asyncResponse.resume(response);
return;
}
metric.setTenantId(tenantId);
ListenableFuture<Void> future = metricsService.createMetric(metric);
Futures.addCallback(future, new NoDataCallback<Void>(asyncResponse));
URI created = uriInfo.getBaseUriBuilder()
.path("/{tenantId}/metrics/numeric/{id}")
.build(tenantId, metric.getId().getName());
MetricCreatedCallback metricCreatedCallback = new MetricCreatedCallback(
asyncResponse,
created,
MetricHandler::getMetricAlreadyExistsResponse
);
Futures.addCallback(future, metricCreatedCallback);
}

@POST
@Path("/{tenantId}/metrics/availability")
@ApiOperation(value = "Create availability metric definition. Same notes as creating numeric metric apply.")
@ApiResponses(value = { @ApiResponse(code = 200, message = "Metric definition created successfully"),
@ApiResponse(code = 400, message = "Metric with given id already exists",
response = ApiError.class),
@ApiResponses(value = {
@ApiResponse(code = 201, message = "Metric definition created successfully"),
@ApiResponse(code = 400, message = "Missing or invalid payload"),
@ApiResponse(code = 409, message = "Numeric metric with given id already exists"),
@ApiResponse(code = 500, message = "Metric definition creation failed due to an unexpected error",
response = ApiError.class)
})
public void createAvailabilityMetric(@Suspended final AsyncResponse asyncResponse,
@PathParam("tenantId") String tenantId, @ApiParam(required = true) AvailabilityMetric metric) {
@PathParam("tenantId") String tenantId,
@ApiParam(required = true) AvailabilityMetric metric,
@Context UriInfo uriInfo
) {
if (metric == null) {
Response response = Response.status(Status.BAD_REQUEST).entity(new ApiError("Payload is empty")).build();
asyncResponse.resume(response);
return;
}
metric.setTenantId(tenantId);
ListenableFuture<Void> future = metricsService.createMetric(metric);
Futures.addCallback(future, new NoDataCallback<Void>(asyncResponse));
URI created = uriInfo.getBaseUriBuilder()
.path("/{tenantId}/metrics/availability/{id}")
.build(tenantId, metric.getId().getName());
MetricCreatedCallback metricCreatedCallback = new MetricCreatedCallback(
asyncResponse,
created,
MetricHandler::getMetricAlreadyExistsResponse
);
Futures.addCallback(future, metricCreatedCallback);
}

private static Response getMetricAlreadyExistsResponse(MetricAlreadyExistsException e) {
String message = "A metric with name [" + e.getMetric().getId().getName() + "] already exists";
return Response.status(Status.CONFLICT).entity(new ApiError(message)).build();
}

@GET
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright 2014-2015 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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.hawkular.metrics.api.jaxrs;

import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;

import org.jboss.resteasy.spi.ReaderException;

import com.google.common.base.Throwables;

/**
* Exception mapper for any exception thrown by a body reader chain.
* <p>
* This mapper let us reply to the user with a pre-determined message format if, for example, a JSON entity cannot be
* parsed.
*
* @author Thomas Segismont
*/
@Provider
public class ReaderExceptionMapper implements ExceptionMapper<ReaderException> {

@Override
public Response toResponse(ReaderException exception) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ApiError(Throwables.getRootCause(exception).getMessage()))
.type(MediaType.APPLICATION_JSON)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE;

import java.net.URI;
import java.util.Collection;
import java.util.List;

Expand All @@ -30,9 +31,12 @@
import javax.ws.rs.Produces;
import javax.ws.rs.container.AsyncResponse;
import javax.ws.rs.container.Suspended;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;

import org.hawkular.metrics.api.jaxrs.callback.TenantCreatedCallback;
import org.hawkular.metrics.core.api.MetricsService;
import org.hawkular.metrics.core.api.Tenant;
import org.hawkular.metrics.core.api.TenantAlreadyExistsException;
Expand Down Expand Up @@ -65,38 +69,35 @@ public class TenantsHandler {
@ApiOperation(value = "Create a new tenant. ", notes = "Clients are not required to create explicitly create a "
+ "tenant before starting to store metric data. It is recommended to do so however to ensure that there "
+ "are no tenant id naming collisions and to provide default data retention settings. ")
@ApiResponses(value = { @ApiResponse(code = 200, message = "Tenant has been succesfully created."),
@ApiResponse(code = 400, message = "Retention properties are invalid. ",
response = ApiError.class),
@ApiResponse(code = 409, message = "Given tenant id has already been created.",
response = ApiError.class),
@ApiResponses(value = {
@ApiResponse(code = 201, message = "Tenant has been succesfully created."),
@ApiResponse(code = 400, message = "Missing or invalid retention properties. "),
@ApiResponse(code = 409, message = "Given tenant id has already been created."),
@ApiResponse(code = 500, message = "An unexpected error occured while trying to create a tenant.",
response = ApiError.class)
})
public void createTenant(@Suspended AsyncResponse asyncResponse, @ApiParam(required = true) Tenant params) {
public void createTenant(
@Suspended AsyncResponse asyncResponse, @ApiParam(required = true) Tenant params,
@Context UriInfo uriInfo
) {
if (params == null) {
Response response = Response.status(Status.BAD_REQUEST).entity(new ApiError("Payload is empty")).build();
asyncResponse.resume(response);
return;
}
ListenableFuture<Void> insertFuture = metricsService.createTenant(params);
Futures.addCallback(insertFuture, new FutureCallback<Void>() {
@Override
public void onSuccess(Void result) {
asyncResponse.resume(Response.ok().type(APPLICATION_JSON_TYPE).build());
}
URI created = uriInfo.getBaseUriBuilder().path("/tenants").build();
TenantCreatedCallback tenantCreatedCallback = new TenantCreatedCallback(
asyncResponse,
created,
TenantsHandler::getTenantAlreadyExistsResponse
);
Futures.addCallback(insertFuture, tenantCreatedCallback);
}

@Override
public void onFailure(Throwable t) {
if (t instanceof TenantAlreadyExistsException) {
TenantAlreadyExistsException exception = (TenantAlreadyExistsException) t;
ApiError errors = new ApiError("A tenant with id [" + exception.getTenantId() + "] already exists");
asyncResponse.resume(Response.status(Status.CONFLICT).entity(errors).type(APPLICATION_JSON_TYPE)
.build());
return;
}
ApiError errors = new ApiError(
"Failed to create tenant due to an "
+ "unexpected error: " + Throwables.getRootCause(t).getMessage());
asyncResponse.resume(Response.status(Status.INTERNAL_SERVER_ERROR).entity(errors)
.type(APPLICATION_JSON_TYPE).build());
}
});
private static Response getTenantAlreadyExistsResponse(TenantAlreadyExistsException e) {
String message = "A tenant with id [" + e.getTenantId() + "] already exists";
return Response.status(Status.CONFLICT).entity(new ApiError(message)).build();
}

@GET
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright 2014-2015 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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.hawkular.metrics.api.jaxrs.callback;

import java.net.URI;
import java.util.function.Function;

import javax.ws.rs.container.AsyncResponse;
import javax.ws.rs.core.Response;

import org.hawkular.metrics.api.jaxrs.ApiError;

import com.google.common.base.Throwables;
import com.google.common.util.concurrent.FutureCallback;

/**
* Base callback class used to build a JAX-RS response when creating an entity (metric, tenant, ...).
* <p>
* On success, a <em>201 CREATED</em> response is built with the <em>location</em> header to indicate the URI of the new
* resource.
* <p>
* On failure, a <em>409 CONFLICT</em> response is built if the exception indicates the resource already exists,
* otherwise a <em>500 SERVER ERROR</em> reponse is built.
*
* @author Thomas Segismont
*/
public abstract class EntityCreatedCallback<E> implements FutureCallback<Void> {
private final AsyncResponse asyncResponse;
private final URI location;
private final Class<E> alreadyExistsException;
private final Function<E, Response> alreadyExistsResponseBuilder;

/**
* @param asyncResponse JAX-RS asynchronous response reference
* @param location URI of the new resource if it is successfully created
* @param alreadyExistsExceptionType type of the exception indicating the resource already exists
* @param alreadyExistsResponseBuilder a function to build a resource already exists response
*/
public EntityCreatedCallback(
AsyncResponse asyncResponse,
URI location,
Class<E> alreadyExistsExceptionType,
Function<E, Response> alreadyExistsResponseBuilder
) {
this.asyncResponse = asyncResponse;
this.location = location;
this.alreadyExistsException = alreadyExistsExceptionType;
this.alreadyExistsResponseBuilder = alreadyExistsResponseBuilder;
}


@Override
public void onSuccess(Void result) {
asyncResponse.resume(Response.created(location).build());
}

@Override
public void onFailure(Throwable t) {
Response response;
if (alreadyExistsException.isAssignableFrom(t.getClass())) {
response = alreadyExistsResponseBuilder.apply(alreadyExistsException.cast(t));
} else {
String message = "Failed to create tenant due to an unexpected error: "
+ Throwables.getRootCause(t).getMessage();
response = Response.serverError().entity(new ApiError(message)).build();

}
asyncResponse.resume(response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright 2014-2015 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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.hawkular.metrics.api.jaxrs.callback;

import java.net.URI;
import java.util.function.Function;

import javax.ws.rs.container.AsyncResponse;
import javax.ws.rs.core.Response;

import org.hawkular.metrics.core.api.MetricAlreadyExistsException;

/**
* An implementation of {@code EntityCreatedCallback} for metric entities.
*
* @author Thomas Segismont
*/
public class MetricCreatedCallback extends EntityCreatedCallback<MetricAlreadyExistsException> {

public MetricCreatedCallback(
AsyncResponse asyncResponse,
URI location,
Function<MetricAlreadyExistsException, Response> alreadyExistsResponseBuilder
) {
super(asyncResponse, location, MetricAlreadyExistsException.class, alreadyExistsResponseBuilder);
}
}

0 comments on commit 65b7e16

Please sign in to comment.