/* * 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.usergrid.persistence.qakka.api; import com.fasterxml.jackson.databind.util.ByteBufferBackedInputStream; import com.google.common.base.Preconditions; import com.google.common.io.ByteStreams; import org.apache.usergrid.persistence.qakka.core.*; import org.apache.usergrid.persistence.qakka.exceptions.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; import javax.ws.rs.*; import javax.ws.rs.NotFoundException; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.StreamingOutput; import java.nio.ByteBuffer; import java.util.Collections; import java.util.List; import java.util.UUID; @Path("queues") public class QueueResource { private static final Logger logger = LoggerFactory.getLogger( QueueResource.class ); private final QueueManager queueManager; private final QueueMessageManager queueMessageManager; private final URIStrategy uriStrategy; private final Regions regions; @Inject public QueueResource( QueueManager queueManager, QueueMessageManager queueMessageManager, URIStrategy uriStrategy, Regions regions ) { this.queueManager = queueManager; this.queueMessageManager = queueMessageManager; this.uriStrategy = uriStrategy; this.regions = regions; } @POST @Consumes({MediaType.APPLICATION_JSON}) @Produces({MediaType.APPLICATION_JSON}) public Response createQueue(Queue queue ) throws Exception { Preconditions.checkArgument(queue != null, "Queue configuration is required"); Preconditions.checkArgument(!QakkaUtils.isNullOrEmpty(queue.getName()), "Queue name is required"); queueManager.createQueue(queue); ApiResponse apiResponse = new ApiResponse(); apiResponse.setQueues( Collections.singletonList(queue) ); return Response.created( uriStrategy.queueURI( queue.getName() )).entity(apiResponse).build(); } @PUT @Path( "{queueName}/config" ) @Consumes({MediaType.APPLICATION_JSON}) @Produces({MediaType.APPLICATION_JSON}) public Response updateQueueConfig( @PathParam("queueName") String queueName, Queue queue) { Preconditions.checkArgument(!QakkaUtils.isNullOrEmpty(queueName), "Queue name is required"); Preconditions.checkArgument(queue != null, "Queue configuration is required"); queue.setName(queueName); queueManager.updateQueueConfig(queue); ApiResponse apiResponse = new ApiResponse(); apiResponse.setQueues( Collections.singletonList(queue) ); return Response.ok().entity(apiResponse).build(); } @DELETE @Path( "{queueName}" ) @Produces({MediaType.APPLICATION_JSON}) public Response deleteQueue( @PathParam("queueName") String queueName, @QueryParam("confirm") @DefaultValue("false") Boolean confirmedParam) { Preconditions.checkArgument(!QakkaUtils.isNullOrEmpty(queueName), "Queue name is required"); Preconditions.checkArgument(confirmedParam != null, "Confirm parameter is required"); ApiResponse apiResponse = new ApiResponse(); if ( confirmedParam ) { queueManager.deleteQueue( queueName ); return Response.ok().entity( apiResponse ).build(); } apiResponse.setMessage( "confirm parameter must be true" ); return Response.status( Response.Status.BAD_REQUEST ).entity( apiResponse ).build(); } @GET @Path( "{queueName}/config" ) @Produces({MediaType.APPLICATION_JSON}) public Response getQueueConfig( @PathParam("queueName") String queueName) { Preconditions.checkArgument(!QakkaUtils.isNullOrEmpty(queueName), "Queue name is required"); ApiResponse apiResponse = new ApiResponse(); Queue queue = queueManager.getQueueConfig( queueName ); if ( queue != null ) { apiResponse.setQueues( Collections.singletonList(queue) ); return Response.ok().entity(apiResponse).build(); } return Response.status( Response.Status.NOT_FOUND ).build(); } @GET @Produces({MediaType.APPLICATION_JSON}) public List<String> getListOfQueues() { // TODO: create design to handle large number of queues, e.g. paging and/or hierarchy of queues // TODO: create design to support multi-tenant usage, authentication, etc. return queueManager.getListOfQueues(); } @GET @Path( "{queueName}/stats" ) @Produces({MediaType.APPLICATION_JSON}) public Response getQueueStats( @PathParam("queueName") String queueName) throws Exception { // TODO: implement GET /queues/{queueName}/stats throw new UnsupportedOperationException(); } Long convertDelayParameter(String delayParam) { Long delayMs = 0L; if (!QakkaUtils.isNullOrEmpty(delayParam)) { switch (delayParam.toUpperCase()) { case "NONE": case "": delayMs = 0L; break; default: try { delayMs = Long.parseLong(delayParam); } catch (Exception e) { throw new IllegalArgumentException("Invalid delay parameter"); } break; } } return delayMs; } Long convertExpirationParameter(String expirationParam) throws IllegalArgumentException { Long expirationSecs = null; if (!QakkaUtils.isNullOrEmpty(expirationParam)) { switch (expirationParam.toUpperCase()) { case "NEVER": case "": expirationSecs = null; break; default: try { expirationSecs = Long.parseLong(expirationParam); } catch (Exception e) { throw new IllegalArgumentException("Invalid expiration parameter"); } break; } } return expirationSecs; } /** * Send a queue message with a JSON payload. * * @param queueName Name of queue to target (queue must exist) * @param regionsParam Comma-separated list of regions to send to * @param delayParam Delay (ms) before sending message (not yet supported) * @param expirationParam Time (ms) after which message will expire (not yet supported) * @param messageBody JSON payload in string form */ @POST @Path( "{queueName}/messages" ) @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public Response sendMessageJson( @PathParam("queueName") String queueName, @QueryParam("regions" ) @DefaultValue("") String regionsParam, @QueryParam("delay") @DefaultValue("") String delayParam, @QueryParam("expiration") @DefaultValue("") String expirationParam, String messageBody) throws Exception { return sendMessage( queueName, regionsParam, delayParam, expirationParam, MediaType.APPLICATION_JSON, ByteBuffer.wrap( messageBody.getBytes() ) ); } /** * Send a queue message with a binary data payload. * * @param queueName Name of queue to target (queue must exist) * @param regionsParam Comma-separated list of regions to send to * @param delayParam Delay (ms) before sending message (not yet supported) * @param expirationParam Time (ms) after which message will expire (not yet supported) * @param actualContentType Content type of messageBody data (if not application/octet-stream) * @param messageBody Binary data that is the payload of the queue message */ @POST @Path( "{queueName}/messages" ) @Consumes(MediaType.APPLICATION_OCTET_STREAM) @Produces(MediaType.APPLICATION_JSON) public Response sendMessageBinary( @PathParam("queueName") String queueName, @QueryParam("regions" ) @DefaultValue("") String regionsParam, @QueryParam("delay") @DefaultValue("") String delayParam, @QueryParam("expiration") @DefaultValue("") String expirationParam, @QueryParam("contentType") String actualContentType, byte[] messageBody) throws Exception { String contentType = actualContentType != null ? actualContentType : MediaType.APPLICATION_OCTET_STREAM; return sendMessage( queueName, regionsParam, delayParam, expirationParam, contentType, ByteBuffer.wrap( messageBody ) ); } private Response sendMessage( String queueName, String regionsParam, String delayParam, String expirationParam, String contentType, ByteBuffer byteBuffer) { if ( queueManager.getQueueConfig( queueName ) == null ) { throw new NotFoundException( "Queue " + queueName + " not found" ) ; } Preconditions.checkArgument( !QakkaUtils.isNullOrEmpty( queueName ), "Queue name is required" ); // if regions, delay or expiration are empty string, would get the defaults from the queue if (regionsParam.equals( "" )) { regionsParam = Regions.LOCAL; } Long delayMs = convertDelayParameter( delayParam ); Long expirationSecs = convertExpirationParameter( expirationParam ); List<String> regionList = regions.getRegions( regionsParam ); queueMessageManager.sendMessages( queueName, regionList, delayMs, expirationSecs, contentType, byteBuffer ); ApiResponse apiResponse = new ApiResponse(); apiResponse.setCount( 1 ); return Response.ok().entity( apiResponse ).build(); } @GET @Path( "{queueName}/messages" ) @Produces({MediaType.APPLICATION_JSON}) public Response getNextMessages( @PathParam("queueName") String queueName, @QueryParam("count") @DefaultValue("1") String countParam) throws Exception { Preconditions.checkArgument( !QakkaUtils.isNullOrEmpty( queueName ), "Queue name is required" ); int count = 1; try { count = Integer.parseInt( countParam ); } catch (Exception e) { throw new IllegalArgumentException( "Invalid count parameter" ); } if (count <= 0) { // invalid count throw new IllegalArgumentException( "Count must be >= 1" ); } List<QueueMessage> messages = queueMessageManager.getNextMessages( queueName, count ); ApiResponse apiResponse = new ApiResponse(); if (messages != null && !messages.isEmpty()) { apiResponse.setQueueMessages( messages ); } else { // always return queueMessages field apiResponse.setQueueMessages( Collections.EMPTY_LIST ); } apiResponse.setCount( apiResponse.getQueueMessages().size() ); return Response.ok().entity( apiResponse ).build(); } @DELETE @Path( "{queueName}/messages/{queueMessageId}" ) @Produces({MediaType.APPLICATION_JSON}) public Response ackMessage( @PathParam("queueName") String queueName, @PathParam("queueMessageId") String queueMessageId) throws Exception { Preconditions.checkArgument( !QakkaUtils.isNullOrEmpty( queueName ), "Queue name is required" ); UUID messageUuid; try { messageUuid = UUID.fromString( queueMessageId ); } catch (Exception e) { throw new IllegalArgumentException( "Invalid queue message UUID" ); } queueMessageManager.ackMessage( queueName, messageUuid ); ApiResponse apiResponse = new ApiResponse(); return Response.ok().entity( apiResponse ).build(); } @GET @Path( "{queueName}/data/{queueMessageId}" ) public Response getMessageData( @PathParam("queueName") String queueName, @PathParam("queueMessageId") String queueMessageIdParam ) { Preconditions.checkArgument(!QakkaUtils.isNullOrEmpty(queueName), "Queue name is required"); UUID queueMessageId; try { queueMessageId = UUID.fromString(queueMessageIdParam); } catch (Exception e) { throw new IllegalArgumentException("Invalid queue message UUID"); } QueueMessage message = queueMessageManager.getMessage( queueName, queueMessageId ); if ( message == null ) { throw new NotFoundException( "Message not found for queueName: " + queueName + " queue message id: " + queueMessageId ); } ByteBuffer messageData = queueMessageManager.getMessageData( message.getMessageId() ); if ( messageData == null ) { throw new NotFoundException( "Message data not found queueName: " + queueName + " queue message id: " + queueMessageId + " message id: " + message.getMessageId() ); } ByteBufferBackedInputStream input = new ByteBufferBackedInputStream( messageData ); StreamingOutput stream = output -> { try { ByteStreams.copy(input, output); } catch (Exception e) { throw new WebApplicationException(e); } }; return Response.ok( stream ).header( "Content-Type", message.getContentType() ).build(); } // @PUT // @Path( "{queueName}/messages/{queueMessageId}" ) // @Produces({MediaType.APPLICATION_JSON}) // public Response requeueMessage( @PathParam("queueName") String queueName, // @PathParam("queueMessageId") String queueMessageIdParam, // @QueryParam("delay") @DefaultValue("") String delayParam) throws Exception { // // Preconditions.checkArgument(!QakkaUtils.isNullOrEmpty(queueName), "Queue name is required"); // // UUID queueMessageId; // try { // queueMessageId = UUID.fromString(queueMessageIdParam); // } // catch (Exception e) { // throw new IllegalArgumentException("Invalid message UUID"); // } // Long delayMs = convertDelayParameter(delayParam); // // queueMessageManager.requeueMessage(queueName, queueMessageId, delayMs); // // ApiResponse apiResponse = new ApiResponse(); // return Response.ok().entity(apiResponse).build(); // } // // // @DELETE // @Path( "{queueName}/messages" ) // @Produces({MediaType.APPLICATION_JSON}) // public Response clearMessages( @PathParam("queueName") String queueName, // @QueryParam("confirm") @DefaultValue("false") Boolean confirmed) throws Exception { // // // TODO: implement DELETE /queues/{queueName}/messages" // throw new UnsupportedOperationException(); // } }