Saturday, September 25, 2021

Periodic Internet Speed Tracking on MAC

This is a quick little guide to setup a periodic job on MAC for measuring internet speed (Download & Upload) and capture the output as a csv file. The job will execute whenever your machine is on and keep collecting internet speed statistics in a csv file. Please follow below steps for setting this up.

[Note: Tested and working fine on MacBook Pro with macOS Catalina 10.15.7]

Install Brew

Brew is a utility for MAC that allows installing packages. We need this to install the speedtest utility package. To setup brew, fire up a terminal and copy-paste below single line command -

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"

It will ask for your permission and password a few times.

Install Speedtest Command Line Utility

Execute below three commands in that order to install speedtest utility

brew tap teamookla/speedtest

brew update

brew install speedtest --force

Create Shell Script 'speedtest.sh'

Open your favorite editor and copy below contents in a new file. Save the file to your Desktop with name 'speedtest.sh'. Make sure NOT to have the .txt extension appended to it automatically by macOS.

#!/bin/bash
/usr/local/bin/speedtest -p no | grep -i 'Download\|Upload' > .tmp-bandwidth.txt 
if [ $? -ne 0 ]; then
  echo `date`,'0','0'
else
  download_speed=`cat .tmp-bandwidth.txt | grep -i 'download' | awk -F" " '{ print $3}'`
  upload_speed=`cat .tmp-bandwidth.txt | grep -i 'upload' | awk -F" " '{ print $3}'`
  echo `date`,$download_speed,$upload_speed
fi

Schedule the Script

We use cron scheduler to run this script periodically. Cron is a simple command line utility present on most *nix systems & macs that allows scheduling tasks.

Set the cron schedule

To schedule script to run every 10 minutes, copy-paste below single-line command. (Entering this command may trigger a pop-up asking permissions, say 'OK' in that case.) 

echo "*/10 * * * * cd ~/Desktop && bash speedtest.sh >> speedtest-results.csv" | crontab -

You can change the job execution interval minutes to a desired frequency by replacing '10' in above command.

Allow cron access disk

To be able to access the shell script and write to the CSV file, cron needs to be given permissions. Simply follow below steps -

  1. Open 'System Preferences' and then Click "Security & Privacy"


  2. Select 'Full Disk Access' from the list and at the bottom, click on 'Click the lock to make changes'. Enter your mac account password when prompted.


  3. Now you need to add 'cron' in the right side list. Click the '+' icon.


  4. This will open up a file browser. Now press "Command + Shift + G" and enter '/usr/sbin/cron' in the textbox and click 'Go', and you are done!


The Output

At this point your cron is set to execute and generate speed test report for you on your desktop as speedtest-results.csv

The generated file has three columns in that order -
Timetamp, Download speed in Mpbs, Upload speed in Mpbs. 
The file contents should look like below in a simple text editor
Sat Sep 25 19:57:45 IST 2021,18.79,7.26
Sat Sep 25 20:07:43 IST 2021,8.00,6.19
Sat Sep 25 20:17:43 IST 2021,11.13,7.01

If internet is down or any error occurs, the bandwidth will be shown as 0,0 for that particular time 

You can also open this file in Microsoft Excel and generate a nice graph out of it!




Sunday, May 2, 2021

DIY: Fully Automated Drip Irrigation For Your Home Plants With Alexa/Google

A Drip Emitter at the end
of Feeder Pipe

It's about a month my wife Shruti and I assembled this automated drip irrigation and now thought of writing this blog as it turned out to be an extremely successful experiment. We hardly have to do any manual intervention now, the system fully autonomous and the plants are flourishing like never before.

Earlier, it was a daily manual boring task to water all 25 plants we have in pots in our home terrace. It was not only time consuming but also each plant got a varied amount of water every time due to the manual process. Mostly, it was excessive water spilling out or leaking through the pot holes at bottom. The water requirements also change based on the season, and in summer we had to water them twice - morning and evening. And it's altogether a different story when you want to travel away from home for some days.

Then we decided to put a full stop to this manual work. Thanks to ready availability of many things online these days, it turned out to be just a matter of ordering few things and assembling them correctly. Of course assembling was tedious hard work, which took about a full day. Then it took about a week or two until we had to tune the flow to each of the plants as per individual water requirement. After that, it's pretty much on its own. 

Why?

  • No more manual watering of plants
  • Saves time
  • Saves lot of water
  • Good for plants as it avoids over or under watering
  • No more worrying about watering your plants when you are out of station for few days
  • Cost effective
  • Can be timer based as well as remotely controlled from your phone from anywhere in the world!

Things you need


  1. A Drip Kit :
    We used this kit on Amazon India. It is good comprehensive kit that has almost everything you need for drip irrigation piping. Good quality components and working as expected. Comes with a quick short user guide. You also have a choice of size (10, 20, 30, 50.. upto 150) based on number of plants you intend to water. Costed us Rs 602 for 30 plants drip kit. 





  2. Adjustable Drip Emitters:
    The drip emitters that come with above kit are 'fixed-flow' ones, meaning water flow cannot be finely controlled. So we additionally ordered these adjustable drip emitters. Cost: Rs 182 for 50 pieces.




    Items below this will be only needed if you want full automation while you don't have a water tap connection nearby. If you have water tap, then an electronic timer valve might be useful


  3. Submersible Pump:
    A submersible pump to force water through the pipes. There are many many alternatives online, we went with this and have no complaints. Cost: Rs 545.







  4. Smart Plug:
    You can go for a smart plug like this one for scheduling pump on-off times or remote operating the pump. Cost Rs 899. Go for a smart plug compatible with Amazon Alexa and Google so that it eases further automation steps.





Total Cost

Summarizing from above list, it costed us total Rs 2228 in all.
  1. Drip Kit (for 30 plants) Rs 602
  2. Adjustable Drip Emitters Rs 182
  3. Submersible Pump Rs 545
  4. Smart Plug Rs 899

Assembling

I won't add too much details here about assembling the irrigation kit components. The user manual that comes with the kit has it. 

We used two parallel
main pipes for two
rows of pots


Just few tips here -

  •  - Do a proper measurement and design of main pipe layout, feeder pipe lengths needed etc before embarking on assembling.
  •  - Inserting the feeder pipe in the main supply pipe and attaching the drip emitter to the feeder pipe are the most physically tiring tasks. It will hurt your fingers and wrists like hell, you have to take intermittent rest.
  •  - Use the PVC glue that comes with the kit at pipe joins. If you still see minor leakages, IGNORE. After some days, water residue will automatically fix those. Really!







Automation

Submersible Pump
in Bucket
Since we did not have a running water tap connection in our terrace, we had to rely on a bucket as a water reservoir. Connect your drip assembly to the pump and submerge it in a bucketful of water. Connect the power supply of the pump to the Smart Plug. Amazon Alexa app can be used to configure and connect the Smart Plug to WiFi and in turn to internet. Follow the Smart Plug's user guide for configuration. 
Bigger the reservoir, more number of days it will last once filled up. Occasionally you can also add some natural fertilizer to the water.




Watering Schedule

Three watering schedules


Steps in a Routine
After much experimentation and tuning we finally settled on a 3-times-per-day watering schedule, each lasting for 30 seconds - 7am, 1pm and 7pm. The schedules can be easily configured in the Amazon Alexa app as Routines

Each routine has three steps - Switch on the plug, wait 30 seconds and then switch off the plug.

While manual watering required a full bucket daily, now a bucket of water lasts for two days in peak summer. May be in monsoon a bucket may last for 3 or 4 days as we will tweak schedule to lower frequency and smaller watering periods.




And here we go ...





We also had few small plants in an isolated window. Here is what we did for it, using mostly throwaway material. Although not fully automated, this makes life easy, and plants are happy too!

Wednesday, November 18, 2020

School In The Heavens


School In The Heavens

Blessed are the kids who get to attend such beautiful divine school in their childhood! This pencil sketch is of a primary school in peak rainy season. It is located in a remote village named Palsambe in Gaganbawda, the place of highest rainfall in Maharashtra state, India.




Canvas Size: A4

Frame Size: 31 cm x 22.5 cm

Sketch Size: 28 cm x13 cm




The sketch was completed on 14th Nov 2020. It took about 40 hours over about 2 months as I struggled to find time to complete it through my busy work schedule.

Timelapse



My Toolkit: It's drawn using world class color pencils from Derwent that guarantee that color remains the same for 100 years in museum conditions. Good quality tools are vital in crafting accurate and durable art.



The sketch is based on a photo I took of the school on 3 Jul 2016, however I have exercised freedom to do some modifications I thought would make the sketch better. At this particular time in 2016, the school was probably freshly colored, and also being peak rainy season it was lush green all around.



Coincidentally, I happened to visit the same school again on 8 Nov 2020, while I was still working on this sketch close to its completion. It was bright sunny day, the rains were already over by this time and the once bright colors of the school were also faded over years. I hope someone plans to color the school again and next rainy season it restores its beauty from the past ... 

Sunday, June 21, 2020

Solar Powered Desk

This will be the first one in my Smart Home series posts. Now that I have to work from my home desk, for don't know how many days, decided to revamp my desk with some electronic tweaks. My home desk may not be aesthetically the best one, but its awesome when it comes to its utility functions. 


It occupies a small corner of my bedroom and is pluggable and moveable. Prima facie, it looks like a usual work desk, as I have tried best hiding most of its complexities to make it look clean, no clutter of wires. 

I have been experimenting with this for few years now, trying and improving on many ideas, improvising based on observations and it's still ongoing. Its primary source of power is Solar power, with grid electricity as a backup option.






The Source

The desk obtains all its energy from these 140 watt solar panels mounted just outside of the window. Its a combination of three solar panels (50W+40W+50W) connected in parallel with total surface area of 6x2.5 feet. The window is facing West, and it's not top floor. So it gets direct sunlight only in second half of the day. The direct sunlight time is further limited by an early Sunset, thanks to the hills. Nonetheless the panels generate enough power for my desk, even in an ambient light. Average usual voltage at the panels is about 18V during the day.












Power Storage

The solar panels charge a 12V 65Ah battery, located right under the desk. I hate flooded lead-acid batteries due to the high maintenance and the hazardous fumes they generate. So I have chosen a dry battery, with no health hazard concerns and zero maintenance. Currently I am using an Exide sealed lead-acid battery. Usual lifespan of it is about 3 to 5 years.












Solar Charge Controller

Charging the battery directly from the solar panel can damage the battery and reduce its life. So a Charge Controller is must. I have tried and tested various solar charge controllers over years and finally settled down on this one ordered from Amazon. Its higher capacity one with microprocessor based control and can handle upto 260W for 12V and 520W for 24V solar panels. It has best heat dissipation, doesn't get heated at all. The built-in USB outlets are added advantage. In addition to the built-in LCD display, I have added an LED based voltage display, as you see at the bottom, so that the voltage is easily visible even during low light and at night.

It's extremely important to use right type and thickness of wires at right place to minimize losses and avoid risk of fire due to overheated wires. I am using 8 mm copper wires from battery to the controller and 4mm copper wires from solar to the controller and from controller to the load

The Load

So where all this energy collected being used? Its mainly used for lights, fans and charging all type of gadgets. All of them requiring DC only. The setup does not involve an inverter at all. I have always been avoiding introducing inverters due to their power inefficiencies. All the lights and fans are custom designed ones to run on 12V or 5V DC. Power is distributed to all consuming widgets from a small custom built hub where it's easy to add/remove connections as needed. The hub includes an additional fuse as a safety measure if some short circuit happens.

Charging all Electronic Gadgets:

Effectively it means that all my gadgets run purely on solar power. It includes all phones, tablets, smart watches, kindle, LED wall clock and even a tyre inflator that I use to pump air into my bicycle tyres.

Apart from the USB ports available in the solar charge controller, I have built couple of USB points right into the desk. This is a simple high ampere USB car adapter with added switch and an LED indicator.






Flexi Personal Fan:

This tiny custom built DC fan runs on 5V. Suffices a single person, can be turned to focus in any direction, safe even if you put your fingers through it while it's running. The blades are cut from a thin plastic sheet (Lid of a Shrikhand container to be specific :) ) and molded using a little heat treatment to give the right angles to the blade for optimum airflow. The blades are mounted on a DC motor from an old non-functional CD player. It is powered by a simple IC 7805 Voltage Regulator to convert 12V from battery to 5V necessary for the fan. The IC is mounted on same motor body itself so that the motor body acts as heatsink for the IC that can get super hot while stepping down the voltage.



A Static Fan:

There is another static fan that I once ordered from ebay. This one is a USB fan running on 5V.

Reading Lights

A 24 LED strip reverse mounted behind this fan acts as reading lights, the LEDs are not directly visible when you are at desk and generate nice ambient light for reading purpose.



Room Lighting:


Three different 12V LED panels with total of 156 LEDs generate enough light not only for desk but for whole room. All are mounted on big metal heatsinks and placed so as to produce ambient light with no direct sight. A separate 3 LED strip also acts as a night lamp.

Switch Panel:

Most lights/fans are controlled from this single switch panel at the back of the desk, not visible but easily reachable. It comprises of small DC switches mounted on a throwaway shaving-blades cartridge.

Charging Laptops:

When it comes to laptops, its not easy to charge them without inverter as most laptops need 19V as input and battery produces only 12V. I tried a small portable single output inverter for few days, but it gave up soon causing fluctuating output voltage. A portable car inverter is the next option I am going to try out now.





And finally the desk is equipped with power from the grid as a backup option, again, not so easily visible but reachable.










Some more pictures:

 




Sunday, March 15, 2020

Stay Home, Stay Safe, Stay Fit! - Corona Lockdown


Are you a regular gym goer and stuck at home due to Corona lockdown? No worries, here are some bodyweight workouts that you can do at home. This is just a compilation of exercises targeting specific muscles. You can plan your weekly home workout by targeting a muscle or two each day and choose few exercises for each.


Note: Video/links are just some references I could find online. Not created by me. Most exercises need no equipments, except few marked with * need a simple home installable pull-up bar. I recommend you get this one.

CARDIO
  1. Walk
  2. Running
  3. Burpees
LEGS
ABS / Core
CHEST
BACK
BICEPS / FOREARMS
TRICEPS
SHOULDERS
  1. Handstand (with wall support)
  2. Pike push-ups
  3. Archer push-ups
  4. 'Jor' (Hindu push-ups)
  5. Pseudo Planche Push-Up
  6. Handstand push-ups
Stay Home, Stay Safe, Eat Right and Stay Fit!!!

Saturday, October 15, 2016

Spring MVC like processing of AMQP messages

Requirement


At K2 when we were trying to build a micro-services based solution, we decided to use AMQP messages instead of REST over HTTP (reasons out of scope of this post). Obviously to make it developer friendly it was most important to have mechanism in place to intercept and process messages in most non-intrusive, transparent and a developer familiar way.

What we wanted to do was to process AMQP messages in a very similar way the Http Requests are processed using spring-webmvc annotations such as @RequestMapping, @RequestParam etc. But, instead of the Http Request my source object will be an AMQP message. The AMQP message request will have two headers, for example -
method="POST"
url="/api/myobjects/{someParam}"
and the payload may optionally contain data in JSON format. If you have noticed, this is nothing but HTTP REST api mapped to AMQP message. We wanted to be able to write a Spring Controller like handler, for example -
@Controller
pubic class MyObjectHandler {

  @RequestMapping(value="/api/myobjects/{someParam}", method="POST")
  public MyObject createMyObject(@Payload MyObject myObj, @PathParam String someParam) {
    //... some processing
    return myObj;
  }
  // ...more handlers
}

The Alternatives

We had looked at spring-amqp/rabbitmq annotations and also spring integration annotations. They are close to what we wanted, but would not allow routing to handler methods based on header parameters, especially the REST url. The spring integration would also lead to extremely complex xmls.
We posted a question on stackoverflow to seek some expert advise. The quick response from Gary Russell (Lead developer - Spring Integration and Spring AMQP) acted as a starting point to the final solution we built.

End Solution's overview from its User's perspective

Before we get to the actual solution, let us see how an end message handler would look like to a developer who uses this, this will give you a clear idea of what we are trying to do. Lets call the handlers a MessageProcessor (equivalent to Spring Controller). CustomerService is an example MessageProcessor that will have handler methods that will be invoked based on the url and method headers in the incoming AMQP message.

@Service
@MessageProcessor
public class CustomerService {
  // ...
  @MessageMapping(value = "/api/customer/{customerId}", method = MessageMethod.GET)
  public Customer getCustomer(@UrlPathParam int customerId) {
    return repo.findOne(customerId);
  }
       
  @MessageMapping(value = "/api/customer/{customerId}/credentials", method = MessageMethod.POST)
  public Customer addCustomer(@Header String status, @Headers Map headers, Customer customer) {
    //... some custome headers processing
         return repo.save(customer);
  }
       
  @MessageMapping(value = "/api/customer/customprocess", method = MessageMethod.POST)
  public Customer processMyMessage(@Message message) {
    //... process the raw message in some custom way
    return customer;
  }
}
Note: Not all URLs in above example are REST compliant, they just clarify purpose and use of the MessageProcessor.

Input Headers For Routing


As you see in example above, Handlers can be mapped to the incoming messages based on two headers in the message. These headers are mandatory in incoming message.
  • url : REST style url which can also have path parameters. e.g. /api/tenant/{tenantId}
  • method : HTTP method. Supported types: GET, POST, PUT, DELETE

Supported Annotations

  • @MessageProcessor A custom defined annotation. The message handlers must be annotated with @MessageProcessor along with any other spring bean annotation such as @Bean, @Service, @Controller etc
  • @MessageMapping A custom defined annotation. Maps a method to incoming message based on two above mentioned headers - url and method. These two headers are matched with value and method attributes respectively as shown in example code above

Supported Handler Method Parameters

  • @UrlPathParameter A custom defined annotation. Method arguments annotated with this annotation will be resolved against the variables in url path
  • @Header Method arguments annotated with this annotation will be resolved against incoming message headers
  • @Headers annotated argument that must also be assignable to java.util.Map for getting access to all headers.
  • Parameter of type Message will be automatically resolved to the received Message without need of any annotation
  • Parameter of type MessageResponse (A custom defined class) will be automatically resolved to an instance of MessageResponse supplied by the framework. This can be used by the handlers to add custom headers in the response if required. The default success headers (i.e. statusCode=200, status="OK", message="OK") can also be overridden by adding them in headers in MessageResponse.
  • A non-annotated argument that is not of the type Message is considered to be the payload. You can make that explicit by annotating the parameter with @Payload. If the incoming message has a header 'contentType' with value as 'application/json', then the payload considered to be json string and is transformed to the argument object

Response Processing

The response is sent to the queue as defined by the 'amqp_replyTo' header in the incoming message
  • If the return type of message handler method is void, no processing will be done.
  • If the return type is of type java.lang.String or primitive type (e.g. int, float) or primitive wrapper (e.g. Integer, Float) it will be returned as response message payload as is.
  • In all other cases, the response object will be converted to its json representation and the json string will be sent as a part of response message.

Response Status and Error Handling

The response statuses are similar to as that of HTTP status codes. See StatusCode
  • If the handler handles the incoming message with no errors/exceptions the response message will be added with default success headers. i.e. statusCode=200, status="OK", message="OK". Handlers can override theses statuses by adding them to MessageResponse as mentioned above
  • If the handler throws a ServiceException the response status headers will be added based on those specified in the ServiceException thrown. The message payload can also be added in such cases by specifying it via ServiceException.setPayload()
  • If the handler throws any other exception than ServiceException, then the response message will have status headers as statusCode=500, status="INTERNAL_SERVER_ERROR", message=< String message as received in the exception > The payload will be a map containing error data.

The Solution


The solution re-uses many spring classes apart from some custom implementations. Below I will be showing vital components of the solution. 


Important Classes


MessageHandler

The most important class is the MessageHandler. MessageHandler itself is a spring bean that on initialization scans through all classes and finds out @MessageProcessors and builds metadata of all the methods that are annotated with @MessageMapping.

This class also is responsible for receiving AMQP messages using @RabbitListener. Once it receives an AMQP messages, it looks up the metadata for an appropriate @MessageMapping method of a @MessageProcessor and invokes it. Of course you must have appropriate RabbitConnectionConfiguration in your spring context, skipping that for brevity.

The MessageHandler class looks as below :
/*Import statements moved at the end of this code snippet */

@Component
public class MessageHandler {
  
  public static final String RESP_HEADER_KEY_STATUS_CODE = "statusCode";
  public static final String RESP_HEADER_KEY_STATUS = "status";
  public static final String RESP_HEADER_KEY_MESSAGE = "message";
  
  public static final String HEADER_KEY_METHOD = "method";
  public static final String HEADER_KEY_URL = "url";

  @Autowired
  private ApplicationContext context;
  
  private Set<String> supportedMethods;
  
  /**
   * Map of all discovered handlers. It is maintained as a Map of Map. The outer map has the discovered 
   * url as key. The inner Map contains discovered handler methods against the HTTP method (GET/POST/PUT/DELETE)
   */
  private Map<String/*url*/, Map<MessageMethod, HandlerMethod>> handlers = new HashMap<>();
  
  private PathMatcher pathMatcher = new AntPathMatcher();
  private ObjectMapper objectMapper = new ObjectMapper(); //For json conversions
  private HandlerMethodArgumentResolverComposite resolvers = new HandlerMethodArgumentResolverComposite();
  private ConversionService conversionService = new DefaultFormattingConversionService();
  
  private MessageConverter messageConverter;
  
  private static Logger logger = LoggerFactory.getLogger(MessageHandler2.class);
  
  @PostConstruct
  public void init() {
    logger.info("Initializing MessageHandler");
    initConverters();
    initArgumentResolvers();
    
    for (String beanName : BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, Object.class)) {
      
      //For every bean annotated with @MessageProcessor, detect the handler methods
      if (context.findAnnotationOnBean(beanName, MessageProcessor.class) != null) {
        logger.info("Found message processor: {}", beanName);
        detectHandlerMethods(beanName);
      }
    }
    supportedMethods = Arrays.asList(MessageMethod.values()).stream().map(method -> method.name()).collect(Collectors.toSet());
    logger.info("Mapped handler methods : {}", handlers);
    logger.info("Ready to process incoming messages");
  }
  
  /**
   * Processes incoming message by finding and invoking appropriate message handler.
   * @param message incoming message. Required headers are <tt>url</tt> and <tt>method</tt>
   * @return message processing result object
   * @throws Exception if any error processing message
   */
  @SuppressWarnings("rawtypes")
  @SendTo
  @RabbitListener(bindings = 
    {@QueueBinding(
      value = @Queue(value = "your.queue.here", durable = "true", autoDelete = "false"),
      exchange = @Exchange(value = "your.exchange.here", durable = "true", autoDelete = "false"),
      key = "your.key.here"), 
    }
  )
  public Message handleMessage(Message<?> message) throws Exception {
    if (logger.isDebugEnabled()) {
      logger.debug("Received message: {}", message);
      message.getHeaders().forEach((k, v) -> logger.debug("Header: "+ k + "="+ v));
    }
    
    String url = (String)message.getHeaders().get(HEADER_KEY_URL);
    String strMethod = (String)message.getHeaders().get(HEADER_KEY_METHOD);
    if (url == null || strMethod == null) {
      String errMsg = "Required headers '" + HEADER_KEY_URL +"' and/or '" + HEADER_KEY_METHOD + "' not found in incoming message";
      return generateErrorResponse(StatusCode.BAD_REQUEST, StatusCode.BAD_REQUEST.name(), errMsg, 
          getErrorMap(new ServiceException(errMsg), null), message);
    }
    
    int indexOfQuestionMark = url.indexOf("?"); //Ignore the '?' and query string further if present. Not needed for matching.
    if (indexOfQuestionMark != -1) {
      url = url.substring(0, indexOfQuestionMark);
    }
    if (!supportedMethods.contains(strMethod)) {
      String errMsg = "Unsupported Method : " + strMethod;
      return generateErrorResponse(StatusCode.BAD_REQUEST, StatusCode.BAD_REQUEST.name(), errMsg, 
          getErrorMap(new ServiceException(errMsg), null), message);
    }
    
    MessageMethod method = MessageMethod.valueOf(strMethod);
    logger.debug("Finding handler for method={} and url={}", method, url);
    HandlerInvocationDetails handlerDetails = lookupMatchingHandler(url, method);
    
    if (handlerDetails == null || handlerDetails.handlerMethod == null) {
      String errMsg = "No handler found for url: " + url + " & method: " + strMethod;
      return generateErrorResponse(StatusCode.NOT_FOUND, StatusCode.NOT_FOUND.name(), errMsg, 
          getErrorMap(new ServiceException(errMsg), null), message);
    }
    MessageWrapper<?> messageWrapper = new MessageWrapper<>(message);
    
    if (handlerDetails.uriTemplateVariables != null) { //Put the resolved URL template variables
      messageWrapper.getData().put(UrlPathVariableMethodArgumentResolver.URI_TEMPLATE_VARIABLES_KEY, handlerDetails.uriTemplateVariables);
    }
    
    logger.debug("Found Handler: {}", handlerDetails.handlerMethod);

    InvocableHandlerMethod invocable = new InvocableHandlerMethod(handlerDetails.handlerMethod);
    invocable.setMessageMethodArgumentResolvers(resolvers);
    
    try {
      Object returnValue = invocable.invoke(messageWrapper);
      MethodParameter returnType = handlerDetails.handlerMethod.getReturnType();
      
      Message returnMsg = processReturnValue(returnValue, returnType, messageWrapper);
      logger.debug("Return value type={} val={}", (returnValue == null ? null :  returnValue.getClass().getName()), returnValue);
      return returnMsg;
    } catch (ServiceException ex) {
      logger.error("Error while processing message {}", messageWrapper, ex);
      return generateErrorResponse(ex.getStatusCode(), ex.getStatus(), ex.getMessage(), getErrorMap(ex, ex.getPayload()), message);
    } catch (Throwable th) { //Any other Exception than ServiceException
      logger.error("Error while processing message {}", messageWrapper, th);
      return generateErrorResponse(StatusCode.INTERNAL_SERVER_ERROR, StatusCode.INTERNAL_SERVER_ERROR.name(), th.getMessage(), getErrorMap(th, null), message);
    }

  }

  /**
   * Initialize all the method argument resolvers. Resolvers map the handler method arguments to correct values from 
   * incoming Message
   */
  private void initArgumentResolvers() {
    logger.info("Initializing argument resolvers");
    ConfigurableBeanFactory beanFactory =
        (ClassUtils.isAssignableValue(ConfigurableApplicationContext.class, context)) ?
            ((ConfigurableApplicationContext) context).getBeanFactory() : null;
            
    List<HandlerMethodArgumentResolver> argumentResolvers = new ArrayList<>();
    argumentResolvers.add(new HeadersMethodArgumentResolver());
    argumentResolvers.add(new HeaderMethodArgumentResolver(conversionService, beanFactory));
    argumentResolvers.add(new MessageMethodArgumentResolver());
    argumentResolvers.add(new UrlPathVariableMethodArgumentResolver(conversionService));
    argumentResolvers.add(new MessageResponseArgumentResolver());
    argumentResolvers.add(new PayloadArgumentResolver(this.messageConverter, new NoOpValidator())); //Must be the last resolver. @Payload annotation is optional
    resolvers.addResolvers(argumentResolvers);
  }

  /**
   * Initialize all the message Payload converters. These are used by PayloadArgumentResolver which is last resolver in the 
   * argumentResolvers list.
   */
  private void initConverters() {
    Collection<MessageConverter> converters = new ArrayList<>();
    converters.add(new StringMessageConverter());
    converters.add(new ByteArrayMessageConverter());
    converters.add(new JsonMessageConverter());
    this.messageConverter = new CompositeMessageConverter(converters);
  }

  /**
   * Detects all the handler methods defined by @MessageMapping annotations
   * @param beanName name of the bean with @MessageProcessor annotation
   */
  private void detectHandlerMethods(String beanName) {
    Class<?> handlerType = context.getType(beanName);
    Class<?> userType = ClassUtils.getUserClass(handlerType); //To take care of CGLIB generated classes
    Object handlerBean = context.getBean(beanName);
    
    HandlerMethodSelector.selectMethods(userType, (method) -> {
      MessageMapping annotation = AnnotationUtils.findAnnotation(method, MessageMapping.class);
      if (annotation != null) {
        addHandler(handlerBean, method);
      }
      return annotation != null ? true : false; //Ignored
    });
  }

  /**
   * Add the identified handler in the handlers map
   * @param handlerBean the handler bean with @MessageProcessor annotation
   * @param method the handler method with @MessageMapping annotation
   */
  private void addHandler(Object handlerBean, Method method) {
    MessageMapping annotation = method.getAnnotation(MessageMapping.class);
    HandlerMethod handlerMethod = new HandlerMethod(handlerBean, method);
    
    Map<MessageMethod, HandlerMethod> methodwiseHandlers = getMethodwiseHandlers(annotation);
    HandlerMethod oldMapping = methodwiseHandlers.put(annotation.method(), handlerMethod);
    
    if (oldMapping != null) {
      logger.warn("WARN: Duplicate handler detected! Ignoring older. Old={}, New={}", oldMapping, handlerMethod);
    }
  }

  /**
   * Get all the handlers already found for 'annotation.value()' defined url. Creates and returns an empty one if not already there.
   * @param annotation the @MessageMapping annotation found on the method
   */
  private Map<MessageMethod, HandlerMethod> getMethodwiseHandlers(MessageMapping annotation) {
    Map<MessageMethod, HandlerMethod> urlHandlers = handlers.get(annotation.value());
    if (urlHandlers == null) {
      urlHandlers = new HashMap<>();
      handlers.put(annotation.value(), urlHandlers);
    }
    return urlHandlers;
  }
  
  /**
   * Process the result obtained by invoking the matched message handler
   * @param returnValue the value to be returned
   * @param returnType type of return value as in the message handler method signature
   * @param message the incoming message
   * @return if return type is void, returns null. If the return type is primitive or primitive wrapper or String, returns as is. 
   * Otherwise the returned object is converted to json represented String and returned
   * @throws JsonProcessingException in case of failure transforming response object to json
   */
  @SuppressWarnings("rawtypes")
  private Message processReturnValue(Object returnValue, MethodParameter returnType, MessageWrapper<?> message) throws JsonProcessingException {
    
    if (void.class.equals(returnType.getParameterType())) {
      return null; // No response will be sent
    }
    
    if (Message.class.isAssignableFrom(returnType.getParameterType())) { //If of type Message, return as is.
      // Do not manipulate message
      return (Message)returnValue;
    }
    
    return MessageBuilder
        .withPayload(getTransformedPayload(returnValue))
        .copyHeaders(message.getMessageResponse().getHeaders())
        .copyHeadersIfAbsent(message.getHeaders()) //Copy all headers from incoming message to outgoing.
        .copyHeaders(getMissingResponseStatusHeadersIfAny(message.getMessageResponse().getHeaders()))
        .build();
  }

  private Map<String, ?> getMissingResponseStatusHeadersIfAny(Map<String, ?> msgResponseHeaders) {
    Map<String, Object> successHeaders = new HashMap<>();
    if (!msgResponseHeaders.containsKey(RESP_HEADER_KEY_STATUS_CODE)) {
      successHeaders.put(RESP_HEADER_KEY_STATUS_CODE, StatusCode.OK.getCode());
    }
    if (!msgResponseHeaders.containsKey(RESP_HEADER_KEY_STATUS)) {
      successHeaders.put(RESP_HEADER_KEY_STATUS, StatusCode.OK.name());
    }
    if (!msgResponseHeaders.containsKey(RESP_HEADER_KEY_MESSAGE)) {
      successHeaders.put(RESP_HEADER_KEY_MESSAGE, StatusCode.OK.name());
    }
    return successHeaders;
  }

  /**
   * Looks up from the handlers map, a handler that matches a given url and method (GET/PUT etc)
   * @param lookupUrl the url as received in the incoming message header 'url'
   * @param lookupMethod the method as received in the incoming message header 'method'
   * @return all details required for invoking the matched handler
   */
  private HandlerInvocationDetails lookupMatchingHandler(String lookupUrl, MessageMethod lookupMethod) {
    
    Map<MessageMethod, HandlerMethod> methodwiseHandlers = handlers.get(lookupUrl);
    if (methodwiseHandlers != null) { //Exact match
      logger.debug("Found exact match for requesst url [{}]. Available method handlers:{}", lookupUrl, methodwiseHandlers);
      return new HandlerInvocationDetails(methodwiseHandlers.get(lookupMethod));
    }

    //Non-exact matches
    List<String> matchingPatterns = new ArrayList<>();
    for (String registeredPattern : handlers.keySet()) {
      if (pathMatcher.match(registeredPattern, lookupUrl)) {
        matchingPatterns.add(registeredPattern);
      }
    }
    
    String bestPatternMatch = null;
    Comparator<String> patternComparator = pathMatcher.getPatternComparator(lookupUrl);
    if (!matchingPatterns.isEmpty()) {
      Collections.sort(matchingPatterns, patternComparator);
      if (logger.isDebugEnabled()) {
        logger.debug("Matching patterns for request url [{}] are {}", lookupUrl, matchingPatterns);
      }
      bestPatternMatch = matchingPatterns.get(0);
    }
    
    logger.debug("bestPatternMatch={}", bestPatternMatch);
    
    if (bestPatternMatch != null) {
      methodwiseHandlers = handlers.get(bestPatternMatch);
      String pathWithinMapping = pathMatcher.extractPathWithinPattern(bestPatternMatch, lookupUrl);
      logger.debug("pathWithinMapping={}", pathWithinMapping);

      // There might be multiple 'best patterns', let's make sure we have the correct URI template variables
      // for all of them
      Map<String, Object> uriTemplateVariables = new LinkedHashMap<>();
      for (String matchingPattern : matchingPatterns) {
        if (patternComparator.compare(bestPatternMatch, matchingPattern) == 0) {
          Map<String, String> vars = pathMatcher.extractUriTemplateVariables(matchingPattern, lookupUrl);
          uriTemplateVariables.putAll(vars);
        }
      }
      logger.debug("uriTemplateVariables={}", uriTemplateVariables);
      if (logger.isDebugEnabled()) {
        logger.debug("Resolved URI Template variables for request [{}] are {}", lookupUrl, uriTemplateVariables);
      }
      return new HandlerInvocationDetails(methodwiseHandlers.get(lookupMethod), uriTemplateVariables);
    }
    return null;
  }
  
  /**
   * Generate an error response.
   * @param statusCode HTTP equivalent status code
   * @param status short status code. may be used by the receiver for comparison etc.
   * @param strMessage human understandable error message, could be longer and descriptive, may include stack trace etc.
   * @param payload optional payload.
   * @param message the incoming message. Headers from this will also be copied to 
   * @return Error message with input information. The message contains below headers to indicate error response
   * "statusCode", "status", "message". All headers from input message will also be copied to this.
   * @throws JsonProcessingException 
   */
  @SuppressWarnings("rawtypes")
  private Message generateErrorResponse(StatusCode statusCode, String status, String strMessage, 
      Object payload, Message<?> message) throws JsonProcessingException {
    Map<String, Object> headers = new HashMap<>();
    message.getHeaders().forEach((name, value) -> {
      headers.put(name, value);
    });
    //Overwrite these if already existed.
    headers.put(RESP_HEADER_KEY_STATUS_CODE, statusCode != null ? statusCode.getCode() : StatusCode.INTERNAL_SERVER_ERROR);
    headers.put(RESP_HEADER_KEY_STATUS, status);
    headers.put(RESP_HEADER_KEY_MESSAGE, strMessage);
    
    Message msg = MessageBuilder.withPayload(payload == null ? "" : getTransformedPayload(payload))
        .copyHeaders(headers)
        .build();
    return msg;
  }
  
  private Object getTransformedPayload(Object payload) throws JsonProcessingException {
    if (payload == null) {
      return "";
    }
    if (payload.getClass().isPrimitive() 
        || ClassUtils.isPrimitiveWrapper(payload.getClass()) 
        || String.class.equals(payload.getClass())) { 
      //return as is
      return payload;
    }
    return objectMapper.writeValueAsString(payload);
  }

  private Map<String, Object> getErrorMap(Throwable th, Object payload) {
    Map<String, Object> errorMap = new LinkedHashMap<>();
    errorMap.put("error", true);
    errorMap.put("type", th.getClass().getName());
    errorMap.put("message", th.getMessage());
    errorMap.put("payload", payload);
    errorMap.put("stackTrace", th.getStackTrace());
    errorMap.put("cause", th.getCause() == null ? null : getErrorMap(th.getCause(), null));
    return errorMap;
  }

  /**
   * Everything required for handler invocation is accumulated here
   */
  private class HandlerInvocationDetails {
    
    private HandlerMethod handlerMethod;
    
    private Map<String, Object> uriTemplateVariables;
    
    public HandlerInvocationDetails(HandlerMethod handlerMethod) {
      this(handlerMethod, new LinkedHashMap<String, Object>());
    }

    public HandlerInvocationDetails(HandlerMethod handlerMethod, Map<String, Object> uriTemplateVariables) {
      this.handlerMethod = handlerMethod;
      this.uriTemplateVariables = uriTemplateVariables;
    }

  }
  
  /**
   * Pass-through validator that does nothing
   */
  private static final class NoOpValidator implements Validator {

    @Override
    public boolean supports(Class<?> clazz) {
      return false;
    }

    @Override
    public void validate(Object target, Errors errors) {
    }
  }

}

/* Imports used in this class -
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import javax.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.convert.ConversionService;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.messaging.Message;
import org.springframework.messaging.converter.ByteArrayMessageConverter;
import org.springframework.messaging.converter.CompositeMessageConverter;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.messaging.converter.StringMessageConverter;
import org.springframework.messaging.handler.HandlerMethod;
import org.springframework.messaging.handler.HandlerMethodSelector;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.handler.annotation.support.HeaderMethodArgumentResolver;
import org.springframework.messaging.handler.annotation.support.HeadersMethodArgumentResolver;
import org.springframework.messaging.handler.annotation.support.MessageMethodArgumentResolver;
import org.springframework.messaging.handler.annotation.support.PayloadArgumentResolver;
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolverComposite;
import org.springframework.messaging.handler.invocation.InvocableHandlerMethod;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.ClassUtils;
import org.springframework.util.PathMatcher;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
*/

Listing below some of the non-trivial classes used by above MessageHandler -

JsonMessageConverter

A MessageConverter that supports MIME type "application/json" with the payload converted to and from a json.
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.converter.AbstractMessageConverter;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.util.MimeType;
import com.fasterxml.jackson.databind.ObjectMapper;

public class JsonMessageConverter extends AbstractMessageConverter {

  private final Charset defaultCharset;
  
  private final ObjectMapper objectMapper;

  public JsonMessageConverter() {
    this(Charset.forName("UTF-8"));
  }

  public JsonMessageConverter(Charset defaultCharset) {
    super(new MimeType("application", "json", defaultCharset));
    this.defaultCharset = defaultCharset;
    objectMapper = new ObjectMapper();
  }

  @Override
  protected boolean supports(Class<?> clazz) {
    return true;
  }

  @Override
  public Object convertFromInternal(Message<?> message, Class<?> targetClass) {
    Charset charset = getContentTypeCharset(getMimeType(message.getHeaders()));
    Object payload = message.getPayload();
    String strJson = (payload instanceof String) ? (String) payload : new String((byte[]) payload, charset);
    try {
      return objectMapper.readValue(strJson, targetClass);
    } catch (Exception e) {
      throw new MessageProcessingException(e);
    }
  }

  @Override
  public Object convertToInternal(Object payload, MessageHeaders headers) {
    throw new UnsupportedOperationException();
  }

  private Charset getContentTypeCharset(MimeType mimeType) {
    if (mimeType != null && mimeType.getCharSet() != null) {
      return mimeType.getCharSet();
    } else {
      return this.defaultCharset;
    }
  }
}

UrlPathVariableMethodArgumentResolver

Resolves method parameters annotated with @UrlPathParam
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.ConversionService;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandlingException;
import org.springframework.messaging.handler.annotation.ValueConstants;
import org.springframework.messaging.handler.annotation.support.AbstractNamedValueMethodArgumentResolver;

public class UrlPathVariableMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver {

  public static final String URI_TEMPLATE_VARIABLES_KEY = ".uriTemplateVariables";

 public UrlPathVariableMethodArgumentResolver(ConversionService cs) {
  super(cs, null);
 }

 @Override
 public boolean supportsParameter(MethodParameter parameter) {
  return parameter.hasParameterAnnotation(UrlPathParam.class);
 }

 @Override
 protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
   UrlPathParam annotation = parameter.getParameterAnnotation(UrlPathParam.class);
  return new UrlPathVariableNamedValueInfo(annotation, parameter.getParameterName());
 }

 @Override
 protected Object resolveArgumentInternal(MethodParameter parameter, Message<?> message, String name)
   throws Exception {
   
  MessageWrapper<?> messageWrapper = (MessageWrapper<?>) message;
  @SuppressWarnings("unchecked")
                Map<String, Object> vars = (Map<String, Object>) messageWrapper.getData().get(URI_TEMPLATE_VARIABLES_KEY);

  return (vars != null) ? vars.get(name) : null;
 }

 @Override
 protected void handleMissingValue(String name, MethodParameter parameter, Message<?> message) {
  throw new MessageHandlingException(message, "Missing path template variable '" + name +
    "' for method parameter type [" + parameter.getParameterType() + "]");
 }

 private static class UrlPathVariableNamedValueInfo extends NamedValueInfo {
  private UrlPathVariableNamedValueInfo(UrlPathParam annotation, String paramName) {
   super(paramName, true, ValueConstants.DEFAULT_NONE);
  }
 }
}
MessageResponseArgumentResolver

Resolves method parameters of type MessageResponse
import org.springframework.core.MethodParameter;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;

public class MessageResponseArgumentResolver implements HandlerMethodArgumentResolver {

  @Override
  public boolean supportsParameter(MethodParameter parameter) {
    return parameter.getParameterType().equals(MessageResponse.class);
  }

  @Override
  public Object resolveArgument(MethodParameter parameter, Message<?> message) throws Exception {
    
    MessageWrapper<?> messageWrapper = (MessageWrapper<?>) message;
    return messageWrapper.getMessageResponse();
  }
}

MessageResponse

The response to be used by @MessageProcessor methods when they want to add headers in the AMQP response message in addition to returning a response object. Also allows suppressing a response AMQP message to be sent by setting the sendResponse flag to false. The default value is true.
public class MessageResponse {
  
  private Map<String, Object> headers;

  public MessageResponse() {
    headers = new HashMap<>();
  }

  public void addHeader(String name, Object value) {
    headers.put(name, value);
  }
  
  public Object getHeader(String name) {
    return headers.get(name);
  }

  public Map<String, Object> getHeaders() {
    return Collections.unmodifiableMap(headers);
  }
}

MessageWrapper

A Message compatible wrapper for Message, to allow adding additional data
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;

/**
 * @param <T> Message payload type
 */
public class MessageWrapper<T> implements Message<T> {
  
  private Message<T> message;
  private Map<String, Object> data = new LinkedHashMap<>();
  private MessageResponse response = new MessageResponse();
  
  public MessageWrapper(Message<T> message) {
    this.message = message;
  }

  @Override
  public T getPayload() {
    return message.getPayload();
  }

  @Override
  public MessageHeaders getHeaders() {
    return message.getHeaders();
  }
  
  public Map<String, Object> getData() {
    return data;
  }
  
  public MessageResponse getMessageResponse() {
    return response;
  }
}

Custom defined annotations


MessageProcessor
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MessageProcessor {
}

MessageMapping
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MessageMapping {
  
  /**
   * URL value with support for path parameters
   */
  String value();

  /**
   * Method - GET/PUT/POST/DELETE
   */
  MessageMethod method();
}

UrlPathParam
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface UrlPathParam {
}
* Some obvious imports are excluded from code snippets above for brevity. Some obvious custom Enum definitions are also excluded.

Thanks for reading till the end :) Your views, suggestions are welcome. Also please share if you have done it/plan to do it in any better way!