<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Micah Silverman</title>
    <description>I&apos;m a technology junkie, maker, fitness enthusiast and life long chaotic neutral. I love java, motor cycling, jogging, spring boot, rowing, mongodb, biking, node.js, hiking, clojure and lifting, to name a few.
</description>
    <link>https://afitnerd.com/</link>
    <atom:link href="https://afitnerd.com/feed.xml" rel="self" type="application/rss+xml" />
    <pubDate>Wed, 11 Dec 2024 21:48:12 +0000</pubDate>
    <lastBuildDate>Wed, 11 Dec 2024 21:48:12 +0000</lastBuildDate>
    <generator>Jekyll v3.10.0</generator>
    
      <item>
        <title>Use a Rules Engine to Write Better Code</title>
        <description>&lt;p&gt;&lt;img src=&quot;/images/rules-engine/rules.jpeg&quot; alt=&quot;robot&quot; width=&quot;400&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Recently, I wrote an API that I am very proud of. It’s got 97% test coverage. It aggregates a number of other APIs to accomplish a complicated flow in just two calls. It makes use of two-factor authentication for verification purposes.&lt;/p&gt;

&lt;p&gt;I didn’t like how long and branchy my controller code was, though. I started to search around for design patterns or approaches that would enable me to get rid of all the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;if&lt;/code&gt;s. I found this &lt;a href=&quot;https://www.baeldung.com/java-replace-if-statements&quot;&gt;excellent post on Baeldung&lt;/a&gt; that led me to the Rule Engine pattern. From the post: &lt;strong&gt;A &lt;em&gt;RuleEngine&lt;/em&gt; evaluates the &lt;em&gt;Rules&lt;/em&gt; and returns the result based on the input.&lt;/strong&gt; In the example, each rule’s evaluate method returns &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;true&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;false&lt;/code&gt;. Once any of the rules evaluates to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;true&lt;/code&gt;, processing in the &lt;em&gt;RuleEngine&lt;/em&gt; stops. If none of the rules evaluates to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;true&lt;/code&gt;, then an Exception is thrown.&lt;/p&gt;

&lt;p&gt;This was very close to what I needed. In my case, the behavior of the &lt;em&gt;RuleEngine&lt;/em&gt; needed to be slightly different. Each API call (sometimes to different APIs) returned either a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;success&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;failure&lt;/code&gt; result. If any API call returns a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;failure&lt;/code&gt;, processing stops. If every API call is succesful, then an overall &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;success&lt;/code&gt; result is returned. I called my implementation a &lt;em&gt;StepsEngine&lt;/em&gt; where each of a number of &lt;em&gt;Steps&lt;/em&gt; is evaluated.&lt;/p&gt;

&lt;p&gt;The rest of this post reviews how the code started and where it went. I made a separate project from the production one I described above that still illustrates the approach well. You can find it &lt;a href=&quot;https://github.com/dogeared/rules-engine&quot;&gt;here on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;hitting-every-branch-on-the-way-down&quot;&gt;Hitting Every Branch on the Way Down&lt;/h2&gt;

&lt;p&gt;I’m a huge fan of anime in general and, in particular, of all the &lt;a href=&quot;https://en.wikipedia.org/wiki/Hayao_Miyazaki#Studio_Ghibli&quot;&gt;Miyazaki films from Studio Ghibli&lt;/a&gt;. It turns out a kind soul has made a &lt;a href=&quot;https://ghibliapi.herokuapp.com/&quot;&gt;Studio Ghibli API&lt;/a&gt;! This is perfect to demonstrate the &lt;em&gt;StepsEngine&lt;/em&gt; approach.&lt;/p&gt;

&lt;p&gt;Before we get there, let’s take a look at where the project started. Like any good API, I want to break down my API calls into small chunks in a service. Here’s my &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GhibliService&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;GhibliService&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;

    &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;BASE_URL&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;https://ghibliapi.herokuapp.com&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;PEOPLE_ENDPOINT&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;BASE_URL&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/people&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;SPECIES_ENDPOINT&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;BASE_URL&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/species&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;FILMS_ENDPOINT&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;BASE_URL&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/films&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;nc&quot;&gt;PeopleResponse&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;listPeople&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;PersonResponse&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;findPersonByName&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;FilmsResponse&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;listMoviesByUrls&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;filmUrls&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;SpeciesResponse&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;findSpeciesByUrl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;speciesUrl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;For the purposes of demonstration, I want to make a controller endpoint that aggregates a number of calls to the Ghibli API. When a request is made to this endpoint it will:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;search the Ghibli API for a person using a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;name&lt;/code&gt; that is passed in&lt;/li&gt;
  &lt;li&gt;if the person is found, a list of the movies that person appears in is retrieved from the Ghiblie API&lt;/li&gt;
  &lt;li&gt;the species that person belongs to is then retrieved from the Ghibli API.&lt;/li&gt;
  &lt;li&gt;if all goes well, all of the previous results are returned in a single json response.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; in the Ghibli API, a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Person&lt;/code&gt; is any character from a movie, including humans, gods, spirits and animals.&lt;/p&gt;

&lt;p&gt;Here’s my first pass at the controller method:&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@PostMapping&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;PERSON_ENDPOINT&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ServiceHttpResponse&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;findPerson&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nd&quot;&gt;@RequestBody&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;KeyValueFieldsRequest&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;HttpServletResponse&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;PersonResponse&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;personResponse&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ghibliService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;findPersonByName&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getByName&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;personResponse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getStatus&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ServiceHttpResponse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;Status&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;FAILURE&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setStatusAndReturn&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;personResponse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;Person&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;person&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;personResponse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getPerson&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;FilmsResponse&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;filmsResponse&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ghibliService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;listMoviesByUrls&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;person&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getFilmUrls&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;filmsResponse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getStatus&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ServiceHttpResponse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;Status&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;FAILURE&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setStatusAndReturn&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;filmsResponse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Film&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;films&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;filmsResponse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getFilms&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;SpeciesResponse&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;speciesResponse&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ghibliService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;findSpeciesByUrl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;person&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getSpeciesUrl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;speciesResponse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getStatus&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ServiceHttpResponse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;Status&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;FAILURE&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setStatusAndReturn&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;speciesResponse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;Species&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;species&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;speciesResponse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getSpecies&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setStatusAndReturn&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CompositeResponse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;ServiceHttpResponse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;Status&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;SUCCESS&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;HttpStatus&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;SC_OK&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; 
        &lt;span class=&quot;s&quot;&gt;&quot;success&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;person&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;films&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;species&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;It’s not too bad. But, there’s a lot of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;if&lt;/code&gt;s. In the code I wrote for my real API, there were a lot more of them!&lt;/p&gt;

&lt;p&gt;Notice that in between some of the if statements, the result from one service call is passed into the next service call. Also, after each service call, a status check is done. If one of the service calls fails, then the controller will respond with that failure immediately.&lt;/p&gt;

&lt;p&gt;The uniform nature of the responses works well. There are specific response types, like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PersonResponse&lt;/code&gt;, each of which implements the more general base &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ServiceHttpResponse&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We can easily test this controller for each of the failure possibilities along with the success outcome using Spring Boot’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MockMvc&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;testing-the-twists-and-turns&quot;&gt;Testing the Twists and Turns&lt;/h2&gt;

&lt;p&gt;In my example code, I am still using Junit 4. While Spring Boot has moved on to Junit 5, I have not yet. Don’t Judge me! (I will get there).&lt;/p&gt;

&lt;p&gt;In order to make Junit 4 work with the latest version of Spring Boot, you need the “vintage” engine in your project. Here’s the snippet from my &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pom.xml&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-xml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.junit.vintage&lt;span class=&quot;nt&quot;&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;junit-vintage-engine&lt;span class=&quot;nt&quot;&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;scope&amp;gt;&lt;/span&gt;test&lt;span class=&quot;nt&quot;&gt;&amp;lt;/scope&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;exclusions&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;exclusion&amp;gt;&lt;/span&gt;
            &lt;span class=&quot;nt&quot;&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.hamcrest&lt;span class=&quot;nt&quot;&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
            &lt;span class=&quot;nt&quot;&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;hamcrest-core&lt;span class=&quot;nt&quot;&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;/exclusion&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/exclusions&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Gross, I know!&lt;/p&gt;

&lt;p&gt;Here’s the real meat of the tests: the set up.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setupRequest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;Status&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;personResponseStatus&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Status&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;filmsResponseStatus&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; 
    &lt;span class=&quot;nc&quot;&gt;Status&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;speciesResponseStatus&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;PersonResponse&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;personResponse&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;personResponseStatus&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Status&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;SUCCESS&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;?&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;PersonResponse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;nc&quot;&gt;Status&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;SUCCESS&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;HttpStatus&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;SC_OK&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;success&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;person&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;PersonResponse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Status&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;FAILURE&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;HttpStatus&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;SC_NOT_FOUND&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;failure&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;FilmsResponse&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;filmsResponse&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;filmsResponseStatus&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Status&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;SUCCESS&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;?&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;FilmsResponse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Status&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;SUCCESS&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;HttpStatus&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;SC_OK&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;success&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;films&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;FilmsResponse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Status&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;FAILURE&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;HttpStatus&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;SC_BAD_REQUEST&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;failure&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;SpeciesResponse&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;speciesResponse&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 
        &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;speciesResponseStatus&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Status&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;SUCCESS&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;?&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;SpeciesResponse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;nc&quot;&gt;Status&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;SUCCESS&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;HttpStatus&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;SC_OK&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;success&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;species&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;SpeciesResponse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;nc&quot;&gt;Status&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;FAILURE&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;HttpStatus&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;SC_BAD_REQUEST&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;failure&quot;&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;when&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ghibliService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;findPersonByName&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;NAME&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;thenReturn&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;personResponse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;when&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ghibliService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;listFilmsByUrls&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;FILM_URLS&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;thenReturn&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;filmsResponse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;when&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ghibliService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;findSpeciesByUrl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;SPECIES_URL&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;))&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;thenReturn&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;speciesResponse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;This method takes three parameters to indicate the expected status of the service calls in the chain of calls that are made in the controller. This makes it easy to have multiple tests with
different conditions for success and failure. The general rule of thumb with this setup is that if the associated service call is expected to succeed, then &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Status.SUCCESS&lt;/code&gt; is passed in. If it’s expected to fail, then &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Status.FAILURE&lt;/code&gt; is passed in. If it’s not expected that the associated service call will be reached (due to a previous failure), &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;null&lt;/code&gt; is passed in.&lt;/p&gt;

&lt;p&gt;Since all the tests against the controller will be with the same POST request, there’s another helper method to keep the tests small and focused:&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ResultActions&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;doPerform&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mvc&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;perform&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;API_URI&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;API_VERSION_URI&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;PERSON_ENDPOINT&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;contentType&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;MediaType&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;APPLICATION_JSON&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mapper&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;writeValueAsString&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;))&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If you’re not already familiar with Spring’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MockMvc&lt;/code&gt; testing features, it provides a compact domain specific language (DSL) for interacting with the controller.&lt;/p&gt;

&lt;p&gt;In the above code, I use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MockMvc&lt;/code&gt; object’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;perform&lt;/code&gt; method. I pass &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;perform&lt;/code&gt; a call to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;post&lt;/code&gt; with the endpoint already defined in the controller. I’ve also set up the request content as an object, which can be be easily serialized to a string using the mapper built into Spring Boot.&lt;/p&gt;

&lt;p&gt;Here’s what one of the tests look like:&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Test&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;personEndpoint_Fail_atFindByPerson&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;setupRequest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Status&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;FAILURE&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;doPerform&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;andExpect&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;isNotFound&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;());&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;verify&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ghibliService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;times&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;findPersonByName&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;NAME&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;verify&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ghibliService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;times&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;listFilmsByUrls&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;FILM_URLS&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Notice that the first parameter in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setupRequest&lt;/code&gt; call is a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FAILURE&lt;/code&gt; status and the other two parameters are &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;null&lt;/code&gt;. This lines up with our expectation of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;findPersonByName&lt;/code&gt; call on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GhibliService&lt;/code&gt; failing for this test.&lt;/p&gt;

&lt;p&gt;Here’s a snippet from the controller method:&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;o&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;nc&quot;&gt;PersonResponse&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;personResponse&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ghibliService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;findPersonByName&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getByName&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;));&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;personResponse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getStatus&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ServiceHttpResponse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;Status&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;FAILURE&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setStatusAndReturn&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;personResponse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;findPersonByName&lt;/code&gt; call fails, none of the other service calls will be called as the controller returns at that point. The calls to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;verify&lt;/code&gt; at the end of the test confirm this outcome. We expect that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;findByPersonName&lt;/code&gt; has been called once and that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;listFilmsByUrls&lt;/code&gt; has not been called at all.&lt;/p&gt;

&lt;p&gt;Each test exercises the next service call with the previous service calls succeeding. Here’s the second-to-last test:&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@Test&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;personEndpoint_Fail_atFindSpecies&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;setupRequest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Status&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;SUCCESS&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Status&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;SUCCESS&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Status&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;FAILURE&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;doPerform&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;andExpect&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;isBadRequest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;());&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;verify&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ghibliService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;times&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;findPersonByName&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;NAME&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;verify&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ghibliService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;times&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;listFilmsByUrls&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;FILM_URLS&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;verify&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ghibliService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;times&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;findSpeciesByUrl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;SPECIES_URL&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Here you can see that the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;findByPersonName&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;listFilmsByUrls&lt;/code&gt; service calls will succeed while &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;findBySpeciesUrl&lt;/code&gt; service call will fail. We verify that each of the service calls have been executed at the end.&lt;/p&gt;

&lt;p&gt;The final test is almost exactly the same as the above test with the exception of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setupRequest&lt;/code&gt; call:&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;setupRequest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Status&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;SUCCESS&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Status&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;SUCCESS&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Status&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;SUCCESS&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In the final test, the expectation is that all the service calls will succeed.&lt;/p&gt;

&lt;p&gt;The point of all these tests is that after we refactor the controller code to take advantage of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;StepsEngine&lt;/code&gt;, the tests should all still pass.&lt;/p&gt;

&lt;h2 id=&quot;step-it-up-to-the-next-level&quot;&gt;Step it up to the Next Level&lt;/h2&gt;

&lt;p&gt;The goal here is to get rid of all the branching in the controller, while still keeping the code readable and having all the tests pass.&lt;/p&gt;

&lt;p&gt;Spoiler Alert! Here’s the final form of the controller method:&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@PostMapping&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;PERSON_BY_ENGINE_ENDPOINT&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ServiceHttpResponse&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;findPersonEngineStyle&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@RequestBody&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;KeyValueFieldsRequest&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;HttpServletResponse&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;StepsEngine&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;stepsEngine&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;setupEngine&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;ServiceHttpResponse&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;serviceHttpResponse&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;stepsEngine&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;process&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setStatusAndReturn&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;serviceHttpResponse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Three lines! Not too shabby! And, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setupEngine&lt;/code&gt; method is pretty lean too:&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;StepsEngine&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setupEngine&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;StepsEngine&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;stepsEngine&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;StepsEngine&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;stepsEngine&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;addStep&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PersonResponseStep&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ghibliService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;));&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;stepsEngine&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;addStep&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;FilmsResponseStep&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ghibliService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;));&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;stepsEngine&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;addStep&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SpeciesResponseStep&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ghibliService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;));&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;stepsEngine&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;addStep&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CompositeResponseStep&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;());&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;stepsEngine&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Examining the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setupEngine&lt;/code&gt; method, you can see the order of the steps:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PersonResponseStep&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FilmsResponseStep&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SpeciesResponseStep&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CompositeResponseStep&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If any of these steps fails, we expect that our controller will return an appropriate failure response. If all the steps succeed, we expcet that our controller will return the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CompositeResponse&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;How is all this accomplished with our 3-line controller method? It seems like magic! It’s really a combination of the Java 8 streams api and Java generics that allows the system to work. Let’s look deeper.&lt;/p&gt;

&lt;p&gt;At the heart of the system is the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Step&lt;/code&gt; interface:&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Step&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;

    &lt;span class=&quot;nc&quot;&gt;Status&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;evaluate&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;KeyValueFieldsRequest&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;ServiceHttpResponse&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getResponse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Every step, when evaluated, takes a uniform request object as a parameter and returns a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Status&lt;/code&gt;. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Status&lt;/code&gt; is an enum with two values: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SUCCESS&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FAILURE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There’s also a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;getResponse&lt;/code&gt; method that returns a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ServiceHttpResponse&lt;/code&gt; object. There are a number of implementations of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ServiceHttpResponse&lt;/code&gt; that we’ll review below.&lt;/p&gt;

&lt;p&gt;Here’s the first step implementation:&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PersonResponseStep&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;implements&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Step&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;GhibliService&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ghibliService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PersonResponse&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;personResponse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;PersonResponseStep&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;GhibliService&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ghibliService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;ghibliService&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ghibliService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Override&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Status&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;evaluate&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;KeyValueFieldsRequest&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;personResponse&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ghibliService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;findPersonByName&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getByName&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;personResponse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getStatus&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Override&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ServiceHttpResponse&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getResponse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;personResponse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The step requires the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GhibliService&lt;/code&gt;, so it’s passed into the constructor. This pattern works well when you have a number of services that interact with different APIs.&lt;/p&gt;

&lt;p&gt;Let’s take a look at the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;StepsEngine&lt;/code&gt; itself to see just how the engine works.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;StepsEngine&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Step&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;steps&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ArrayList&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;gt;();&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;addStep&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Step&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;step&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;step&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ServiceHttpResponse&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;process&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;KeyValueFieldsRequest&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;Step&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;step&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;  &lt;span class=&quot;n&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;stream&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;s&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;evaluate&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Status&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;FAILURE&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;findFirst&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;c1&quot;&gt;// if nothing fails, last step must be success&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;orElse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;));&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;step&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getResponse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Here’s where the magic of the streams interface (introduced in Java 8) comes into play.&lt;/p&gt;

&lt;p&gt;There’s a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;List&lt;/code&gt; of steps that gets filled out through the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;addStep&lt;/code&gt; method.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;process&lt;/code&gt; method takes in the request object that all the steps must know how to handle.&lt;/p&gt;

&lt;p&gt;The line with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.filter&lt;/code&gt; calls the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;evaluate&lt;/code&gt; method of each of the steps and returns that step if the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Status&lt;/code&gt; is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FAILURE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The call to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;findFirst()&lt;/code&gt; ensures that the stream is interrupted if a FAILURE occurs. The step that failed is assigned to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;step&lt;/code&gt; variable.&lt;/p&gt;

&lt;p&gt;Finally, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;orElse&lt;/code&gt; is hit when all steps have run successfully. In that case, the last step is assigned to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;step&lt;/code&gt; variable. It is therefore important that you define a step that represents your overall success state and that it be the last step in your list.&lt;/p&gt;

&lt;p&gt;After the stream is done - whether the result is success of failure - the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ServiceHttpResponse&lt;/code&gt; from the assigned step is returned.&lt;/p&gt;

&lt;p&gt;This is all pretty straightforward so far, but there’s a problem that you run into almost right away. That problem is state.&lt;/p&gt;

&lt;p&gt;Look at this snippet from the original controller method:&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;        &lt;span class=&quot;nc&quot;&gt;PersonResponse&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;personResponse&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ghibliService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;findPersonByName&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getByName&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;...&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;Person&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;person&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;personResponse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getPerson&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;FilmsResponse&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;filmsResponse&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ghibliService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;listFilmsByUrls&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;person&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getFilmUrls&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Here, we’re passing the result of one service call into the method of the next service call.&lt;/p&gt;

&lt;p&gt;In order for our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;StepsEngine&lt;/code&gt; to function properly, we’re going to need to save state.&lt;/p&gt;

&lt;p&gt;Here’s the updated &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Step&lt;/code&gt; interface:&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Step&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@SuppressWarnings&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;unchecked&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;fetchState&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;getStateContainer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;saveState&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Object&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;getStateContainer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;nc&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Object&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getStateContainer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setStateContainer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Object&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;stateContainer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;Status&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;evaluate&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;KeyValueFieldsRequest&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;ServiceHttpResponse&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getResponse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Map&amp;lt;String, Object&amp;gt;&lt;/code&gt; is a handy and cheap way to save state along the way. It accomodates saving various types of objects in a flow that can have many steps.&lt;/p&gt;

&lt;p&gt;However, casting can get annoying, so I use some generics magic to alleviate that. That’s what the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fetchState&lt;/code&gt; method is for.&lt;/p&gt;

&lt;p&gt;Since all the steps must now implement &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;getStateContainer&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setStateContainer&lt;/code&gt;, it’s useful to introduce an abstract class that implements those two methods. The various step classes can then just extend that abstract class.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;abstract&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BasicStep&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;implements&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Step&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Object&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;stateContainer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Override&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setStateContainer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Object&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;stateContainer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;stateContainer&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;stateContainer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Override&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Object&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;getStateContainer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;stateContainer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now, Let’s take a look at the refactored &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PersonResponseStep&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PersonResponseStep&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BasicStep&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;PERSON_KEY&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;person&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;o&quot;&gt;...&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Override&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Status&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;evaluate&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;KeyValueFieldsRequest&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;personResponse&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ghibliService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;findPersonByName&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getByName&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;saveState&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;PERSON_KEY&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;personResponse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getPerson&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;());&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;personResponse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getStatus&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Each step is responsible for defining the key that will be used in the state container. In this case, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;saveState&lt;/code&gt; method is called with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PERSON_KEY&lt;/code&gt; and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Person&lt;/code&gt; object from the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PersonResponse&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Take a look at the next step in the chain, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FilmsResponseStep&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;FilmsResponseStep&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;BasicStep&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;FILMS_KEY&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;films&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;o&quot;&gt;...&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@Override&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Status&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;evaluate&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;KeyValueFieldsRequest&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;Person&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;person&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fetchState&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;PERSON_KEY&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;filmsResponse&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ghibliService&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;listFilmsByUrls&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;person&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getFilmUrls&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;());&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;saveState&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;FILMS_KEY&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;filmsResponse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getFilms&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;());&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;filmsResponse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getStatus&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In its &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;evaluate&lt;/code&gt; method, it uses &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fetchState&lt;/code&gt; to get the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Person&lt;/code&gt; from the state container. Because of the generics used in the definition of the method in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Step&lt;/code&gt; interface, a cast
is not required here.&lt;/p&gt;

&lt;p&gt;Now the service method can be called with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Person&lt;/code&gt; object retrieved from the state container. This step adds to the state container with its defined key &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FILMS_KEY&lt;/code&gt; after the service call.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;StepsEngine&lt;/code&gt; along with the generified &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;stateContainer&lt;/code&gt; makes the whole system hang together.&lt;/p&gt;

&lt;h2 id=&quot;revisiting-the-tests-with-the-engine&quot;&gt;Revisiting the Tests with the Engine&lt;/h2&gt;

&lt;p&gt;Now that we’ve refactored the pieces of the original controller method to use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;StepsEngine&lt;/code&gt;, all our tests should still pass. As a reminder, here’s what the controller method now looks like:&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nd&quot;&gt;@PostMapping&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;PERSON_BY_ENGINE_ENDPOINT&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ServiceHttpResponse&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;findPersonEngineStyle&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;nd&quot;&gt;@RequestBody&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;KeyValueFieldsRequest&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;HttpServletResponse&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;StepsEngine&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;stepsEngine&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;setupEngine&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;nc&quot;&gt;ServiceHttpResponse&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;serviceHttpResponse&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;stepsEngine&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;process&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setStatusAndReturn&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;serviceHttpResponse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If you remember from before, I showed you the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;doPerform&lt;/code&gt; method that sets up the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MockMvc&lt;/code&gt; request to call the controller endpoint. In order to support both the steps engine and branching approaches in the controller, I have two endpoints: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PERSON_ENDPOINT&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PERSON_BY_ENGINE_ENDPOINT&lt;/code&gt;. In the controller test, I have two setup methods each of which calls a base method to get everything set up with one endpoint or the other:&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ResultActions&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;doPerformBase&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;endpoint&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mvc&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;perform&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;endpoint&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;contentType&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;MediaType&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;APPLICATION_JSON&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mapper&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;writeValueAsString&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;))&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ResultActions&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;doPerform&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;doPerformBase&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;API_URI&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;API_VERSION_URI&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;PERSON_ENDPOINT&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ResultActions&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;doPerformByEngine&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;doPerformBase&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;API_URI&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;API_VERSION_URI&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;PERSON_BY_ENGINE_ENDPOINT&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now I can run tests against both endpoints and they should all pass. And, as you can see below, they all do!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/rules-engine/ghibli_alltests.png&quot; alt=&quot;ghibli all tests&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;get-your-engines-running&quot;&gt;Get Your Engines running&lt;/h2&gt;

&lt;p&gt;Hopefully you’ve found this post useful in how to reduce branching in your controllers. There are a number of pros and cons to the Rules Engine approach taken here:&lt;/p&gt;

&lt;p&gt;Pros:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;very compact controller code&lt;/li&gt;
  &lt;li&gt;encapsulation of steps in their own classes&lt;/li&gt;
  &lt;li&gt;easy to test&lt;/li&gt;
  &lt;li&gt;works great in situations with multiple external apis with lots of steps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cons:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;more classes (one per step)&lt;/li&gt;
  &lt;li&gt;need to maintain state&lt;/li&gt;
  &lt;li&gt;might require more hunting around for code to understand the flow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can find my demo project &lt;a href=&quot;https://github.com/dogeared/rules-engine&quot;&gt;here on Github&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The original post that this work is based on is on &lt;a href=&quot;https://www.baeldung.com/java-replace-if-statements&quot;&gt;Baeldung&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You can find me on twitter: &lt;a href=&quot;https://twitter.com/afitnerd&quot;&gt;@afitnerd&lt;/a&gt;&lt;/p&gt;
</description>
        <pubDate>Wed, 17 Feb 2021 13:00:00 +0000</pubDate>
        <link>https://afitnerd.com/2021/02/17/rules-engine-ftw/</link>
        <guid isPermaLink="true">https://afitnerd.com/2021/02/17/rules-engine-ftw/</guid>
        
        <category>programming</category>
        
        <category>java</category>
        
        <category>spring boot</category>
        
        
        <category>programming</category>
        
        <category>java</category>
        
        <category>spring boot</category>
        
      </item>
    
      <item>
        <title>Man in the Middle Yourself to See What Your IoT is Up To</title>
        <description>&lt;p&gt;&lt;img src=&quot;/images/mitm-yourself/safe.jpg&quot; alt=&quot;robot&quot; width=&quot;400&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Can you name all of the Internet connected devices in your house right now? You might be surprised to learn what’s connected and what’s not and maybe even what you’ve forgotten about!&lt;/p&gt;

&lt;p&gt;Take my mom, for example. She has a MacBook, an iPad, a Galaxy S8, an Amazon FireTV Stick, an Amazon Echo Show 8. And, that’s just what I know about. Like many of us, she may also have a smart tv. She has a medic alert fob that she wears around her neck. You know, the “Help! I’ve fallen and I can’t get up!” I bet that has an IP address or, at least the base station does.&lt;/p&gt;

&lt;p&gt;In my house, I favor devices that I can hook up to Alexa to control. Every now and then, though, I end up with something that can’t (yet) integrate with Alexa. I recently got a mini-split air conditioner installed to keep my office - which sits over a garage - cool in the summer without taxing the rest of the house. The unit I chose has an app, but it does not integrate with Alexa. I’ll save the brand-name for a follow up post, because using the tools and techniques I talk about in this post, I discovered a serious security flaw in the app!&lt;/p&gt;

&lt;p&gt;One of my favorite posts of all time is about reverse-engineering the &lt;a href=&quot;https://medium.com/@nolanbrown/the-process-of-reverse-engineering-the-august-lock-api-9dbd12ab65cb&quot;&gt;August Lock API&lt;/a&gt;. In this post, the author uses a reverse-proxy, called &lt;a href=&quot;https://www.charlesproxy.com/&quot;&gt;Charles Proxy&lt;/a&gt; to affect a man-in-the-middle attack on his own network to discover the calls that the August App was making to its servers.&lt;/p&gt;

&lt;p&gt;In this post, I will cover a mobile app proxy called &lt;a href=&quot;https://play.google.com/store/apps/details?id=com.guoshi.httpcanary&amp;amp;hl=en_US&quot;&gt;HttpCanary&lt;/a&gt; as well as &lt;a href=&quot;&quot;&gt;Charles Proxy&lt;/a&gt;. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HttpCanary&lt;/code&gt; is only available for Android, so apologies in advance if you’re on an iOS device. Charles Proxy runs on your Mac, Windows or Linux desktop.&lt;/p&gt;

&lt;h2 id=&quot;insomnia-is-a-good-time-to-hack&quot;&gt;Insomnia is a Good Time to Hack&lt;/h2&gt;

&lt;p&gt;Earlier in the week, I was struck with some bad insomnia. This happens from time to time with me. After watching 3 episodes of season 2 of Umbrella Academy, I got to thinking about how I could Alexa-ify my mini-split air conditioner. It had an app that required me to register for an account. I can use the app off my network, which means that both the app and the AC connect to some central server somewhere to communicate.&lt;/p&gt;

&lt;p&gt;I was already familiar with Charles Proxy, but I (lazily) didn’t want to get up out of bed to get my laptop. So, I did a quick search and found HttpCanary. Most reverse proxies available for Android require you to root your device. HttpCanary, ingeniously, installed a certificate and sets up a VPN all within bounds of stock Android. Once I had it configured, I launched the AC app, logged out, logged in again, turned it on and off, and changed the temperature. Switching back to HttpCanary, I could see all this traffic.&lt;/p&gt;

&lt;p&gt;If an app is using a service properly, it will have &lt;a href=&quot;https://owasp.org/www-community/controls/Certificate_and_Public_Key_Pinning&quot;&gt;certificate pinning&lt;/a&gt; configured. This makes it harder for the reverse proxy to do its job. Ordinarily, you’d need a rooted-device with some additional software. You can do this both on Android (rooted) or iOS (Jailbreak). Fortunately, HttpCanary can install a root certificate in Firefox, so it’s easy to snoop on all of its network traffic. My AC app did &lt;strong&gt;not&lt;/strong&gt; have certificate pinning properly configured, and so I was able to use HttpCanary with it without issue. More on that in the next post!&lt;/p&gt;

&lt;h3 id=&quot;httpcanary-for-capturing-in-bed&quot;&gt;HttpCanary for Capturing in Bed&lt;/h3&gt;

&lt;p&gt;In HttpCanary, you can search for apps that you want to monitor. You then click on the paper airplane icon in the lower right to enable its vpn and start it listening. The cool thing is that on my Samsung (and maybe other Android 10 devices?), I can have canary in a floating window and see all the traffic it’s capturing.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://afitnerd.com/images/mitm-yourself/canary-float.png&quot; alt=&quot;canary-float&quot; width=&quot;400&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Here’s a fullscreen view of the app:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://afitnerd.com/images/mitm-yourself/canary-full.png&quot; alt=&quot;canary-full&quot; width=&quot;400&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;use-charles-proxy-for-the-big-guns&quot;&gt;Use Charles Proxy for the Big Guns&lt;/h3&gt;

&lt;p&gt;Charles Proxy is a paid app, but it has a free trial. If you really want to dig into what your IoT devices are doing, Charles Proxy is for you. In the screenshot below, You can see some traffic I’ve captured from afitnerd.com&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://afitnerd.com/images/mitm-yourself/afitnerd-charles.png&quot; alt=&quot;afitnerd-charles&quot; width=&quot;400&quot; /&gt;&lt;/p&gt;

&lt;p&gt;I can easily configure my mobile device to use the proxy server running on my laptop. On Android, you go to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Settings &amp;gt; Connections &amp;gt; Wi-Fi&lt;/code&gt;. Touch the gear icon next to your Wi-Fi connection and choose &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Advanced&lt;/code&gt;. Then, choose &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Proxy &amp;gt; Manual&lt;/code&gt; and enter in the IP address of the machine your Charles Proxy is running on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: If you’re not sure of the IP Address where Charles Proxy is running, you can choose &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Help &amp;gt; Local IP Address&lt;/code&gt; from the menu and it will tell you. Also, Charles Proxy runs on port &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;8888&lt;/code&gt; by default.&lt;/p&gt;

&lt;h2 id=&quot;whats-next-in-iot-hacking&quot;&gt;What’s Next in IoT Hacking&lt;/h2&gt;

&lt;p&gt;This week I will disclose my security findings to the maker of my AC unit. Hopefully they will respond and indicate that they are fixing the issue. Keep an eye out for the next post, where I’ll dig into hacking my AC unit with Charles Proxy and HttpCanary.&lt;/p&gt;
</description>
        <pubDate>Sun, 09 Aug 2020 13:00:00 +0000</pubDate>
        <link>https://afitnerd.com/2020/08/09/man-in-the-middle-yourself/</link>
        <guid isPermaLink="true">https://afitnerd.com/2020/08/09/man-in-the-middle-yourself/</guid>
        
        <category>tech</category>
        
        <category>diy</category>
        
        
        <category>technology</category>
        
      </item>
    
      <item>
        <title>We&apos;re in a Golden Age of Home Automation</title>
        <description>&lt;p&gt;&lt;img src=&quot;/images/golden-age-home-automation/robot.jpg&quot; alt=&quot;robot&quot; width=&quot;400&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;no-more-robots&quot;&gt;No More Robots&lt;/h2&gt;

&lt;p&gt;A frequent lament of my daughter is, “No more robots, Dad!” I was a very early adopter of home automation, which included things like &lt;a href=&quot;https://en.wikipedia.org/wiki/ReplayTV&quot;&gt;ReplayTV&lt;/a&gt; and &lt;a href=&quot;https://en.wikipedia.org/wiki/X10_(industry_standard)&quot;&gt;X10&lt;/a&gt; in the late ’90s and early aughts.&lt;/p&gt;

&lt;p&gt;I had the very first &lt;a href=&quot;https://en.wikipedia.org/wiki/Roku#First_generation&quot;&gt;Roku&lt;/a&gt; and we cut cable and switched to Internet-only service around 2008. It was almost as expensive to have &lt;em&gt;just&lt;/em&gt; Internet back then as it was to have Internet, TV and phone service bundled together. I’d always thought that the TV would tie into home automation and some early attempts included showing caller id on-screen via cable boxes.&lt;/p&gt;

&lt;p&gt;Fast forward a few years and around 2014, I bought two devices: a first generation Amazon FireTV Stick and a first generation Amazon Echo Dot. I was travleing a lot and the limitations of the Roku made me want to look at a new platform. I wanted to be able to install a VPN client so that I could access my home media server when on the road. The Roku was completely closed and proprietary while the FireTV platform was a custom build of Android. This enabled me to &lt;a href=&quot;https://en.wikipedia.org/wiki/Sideloading&quot;&gt;side load&lt;/a&gt; the OpenVPN client. I was intrigued by the Echo platform, but I was more interested in home automation than in listening to music. So, the Echo Dot was perfect. At this time, the &lt;a href=&quot;https://en.wikipedia.org/wiki/Philips_Hue&quot;&gt;Philips Hue&lt;/a&gt; platform was already released, so I bought my first modern home automation kit: two Philips Hue dimmable bulbs and a Philips Hue bridge. Combined with the Echo Dot, I could now say: “Alexa, turn on my light 50%”. This was a revelation to me. My wife and daughter were much less impressed with this than I was.&lt;/p&gt;

&lt;p&gt;I now have six different brands of devices all of which I can issue voice commands to from anywhere in my house. This includes everything from: “Alexa, turn on the fireplace” and “Alexa, tell Simplisafe goodnight” to “Alexa, turn on the bedroom fan 50%” and “Alexa, tell the Deebot to vacuum”.&lt;/p&gt;

&lt;h2 id=&quot;down-with-vendor-lock-in-long-live-vendor-lock-in&quot;&gt;Down with Vendor Lock-in, Long Live Vendor Lock-in!&lt;/h2&gt;

&lt;p&gt;What Amazon, Google and Apple (to a lesser degree) did with home automation is nothing short of brilliant. They made it so that you could use a variety of smart-speaker enabled plugs, switches, bulbs and other types of devices. No vendor lock-in. However, it became much more likely that you’d settle on one brand as the “nerve center” of all this automation. I ended up with Amazon. I’ve been told by friends (and have not verified for myself) that Google’s Home line of smart speakers is more sophisticated than Amazon’s Echo line. Even if that’s true, it’s unlikely that I would switch any time soon (I have an unopened Google Home mini in my storage closet. I’ll send it to the first person to @afitnerd me on Twitter and venmo me the cost of shipping).&lt;/p&gt;

&lt;p&gt;So, I am pretty locked in to Amazon. But, I am free to range around all the devices that support the Amazon platform. And, most of the devices I buy these days support Amazon and Google, at least.&lt;/p&gt;

&lt;h3 id=&quot;the-philips-hue-case-study&quot;&gt;The Philips Hue Case Study&lt;/h3&gt;

&lt;p&gt;I got my first Philips Hue kit nearly at the time it dropped. We used those bulbs for years. Every now and then I’d have to reset the hub, but it was infrequent enough that it didn’t bother me. I never bought additional Hue bulbs because they were expensive compared to other other brands coming onto the smart bulb scene. That didn’t matter since the GE bulbs I got later on integrated with Amazon Alexa just as easily as the Philips Hue bulbs had.&lt;/p&gt;

&lt;p&gt;On April 30th, 2020, Philips not only discontinued support for the v1 hub, they made it lose all Internet connectivity, making it inoperable with any smart devices. You could still control the bulbs with the mobile app, as long as the hub and the app were on the same network. The v1 hub was introduced in 2012 and eight years is a long time in Internet years. There were a number of &lt;a href=&quot;https://en.wikipedia.org/wiki/Philips_Hue#Security_concern&quot;&gt;security concerns&lt;/a&gt; with the v1 hub, which is why Philips said they were dropping ongoing support for it. The v2 hub was released in 2015, had a faster processor and would have continued support from Philips (or, so they said).&lt;/p&gt;

&lt;p&gt;But, fool me once… - I didn’t like that Philips might brick future devices I would buy from them. True to their promise, on May 1st, I could no longer control my bulbs from my Amazon devices. I reached out to Philips, hoping to get a discount on a v2 bridge. It’s currently priced at $58 on Amazon. To their credit, the Philips Twitter account was very responsive:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://afitnerd.com/images/golden-age-home-automation/hue.png&quot; alt=&quot;hue&quot; width=&quot;400&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The bottom line, though, was that there were no promos I could use to offset the cost of the v2 bridge in order to keep using my Hue bulbs. Well, I couldn’t fault them too much. In 6 years, I’d only ever bought the hub and the two bulbs. I might have been more upset or might have bitten-the-bullet and bought the v2 hub, but for the fact that there were so many other options!&lt;/p&gt;

&lt;p&gt;I hunted around and bought a &lt;a href=&quot;https://www.bestbuy.com/site/c-by-ge-soft-white-a-19-4-pack-smart-plug-white/6373503.p?skuId=6373503&quot;&gt;C by GE&lt;/a&gt; starter kit. It was on sale at the time for $36. I was impressed that GE built their hub into the smart plug that could connect to my network wirelessly and could control up to 50 bulbs. So, for 28% less than the cost of a Hue v2 bridge, I got four dimmable bulbs and a smartplug. I’ve since bought another 4 bulbs and easily connected it to my existing smart-plug-bridge.&lt;/p&gt;

&lt;p&gt;This is a perfect example of avoiding vendor lock-in thanks to the architecture that Amazon and Google provided for their smart devices.&lt;/p&gt;

&lt;h2 id=&quot;upward-and-onward-with-home-automation&quot;&gt;Upward and Onward with Home Automation&lt;/h2&gt;

&lt;p&gt;Currently, I have a boat load of switches, plugs and devices in my house that I control via the 6 various Amazon devices I have as well. Here’s a list (&lt;strong&gt;note&lt;/strong&gt;: these are affiliate links and I get some cents if you follow them and make a purchase):&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.amazon.com/gp/product/B07N8RPRF7/ref=as_li_tl?ie=UTF8&amp;amp;tag=dogeared08-20&amp;amp;camp=1789&amp;amp;creative=9325&amp;amp;linkCode=as2&amp;amp;creativeASIN=B07N8RPRF7&amp;amp;linkId=aae324cac418c726cd6fb4ec792573f9&quot;&gt;Amazon Echo Dot&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.amazon.com/gp/product/B07PF1Y28C/ref=as_li_tl?ie=UTF8&amp;amp;tag=dogeared08-20&amp;amp;camp=1789&amp;amp;creative=9325&amp;amp;linkCode=as2&amp;amp;creativeASIN=B07PF1Y28C&amp;amp;linkId=61d74cc3d11e509cea59f5cd35edb1d4&quot;&gt;Amazon Echo Show 8&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.amazon.com/gp/product/B07KGVB6D6/ref=as_li_tl?ie=UTF8&amp;amp;tag=dogeared08-20&amp;amp;camp=1789&amp;amp;creative=9325&amp;amp;linkCode=as2&amp;amp;creativeASIN=B07KGVB6D6&amp;amp;linkId=77b437caaa337873d87253f2a55f14d3&quot;&gt;Amazon Cube&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.amazon.com/gp/product/B079QHML21/ref=as_li_tl?ie=UTF8&amp;amp;tag=dogeared08-20&amp;amp;camp=1789&amp;amp;creative=9325&amp;amp;linkCode=as2&amp;amp;creativeASIN=B079QHML21&amp;amp;linkId=f583c2ffb276e954969e5192960e7ace&quot;&gt;Amazon FireTV Stick 4K&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.amazon.com/gp/product/B077HW9XM7/ref=as_li_tl?ie=UTF8&amp;amp;tag=dogeared08-20&amp;amp;camp=1789&amp;amp;creative=9325&amp;amp;linkCode=as2&amp;amp;creativeASIN=B077HW9XM7&amp;amp;linkId=e85f58a2ed6ca047f1c0a835902bee03&quot;&gt;Ecovacs DEEBOT N79S&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;(https://www.bestbuy.com/site/c-by-ge-soft-white-a-19-4-pack-smart-plug-white/6373503.p?skuId=6373503)&quot;&gt;C by GE Smart Room Starter Pack&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.amazon.com/gp/product/B01KB0O2E8/ref=as_li_tl?ie=UTF8&amp;amp;camp=1789&amp;amp;creative=9325&amp;amp;creativeASIN=B01KB0O2E8&amp;amp;linkCode=as2&amp;amp;tag=dogeared08-20&amp;amp;linkId=897fc775d52e6f5f47cf40df15b5a281&quot;&gt;C by GE C-Life and C-Sleep Smart LED Light Bulb Combo 4-Pack&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.amazon.com/gp/product/B07YBQKZTK/ref=as_li_tl?ie=UTF8&amp;amp;tag=dogeared08-20&amp;amp;camp=1789&amp;amp;creative=9325&amp;amp;linkCode=as2&amp;amp;creativeASIN=B07YBQKZTK&amp;amp;linkId=822d0f6e783bf39b14b865ceb837623c&quot;&gt;Smart Plug with USB Port 4 Pack&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.amazon.com/gp/product/B079MFTYMV/ref=as_li_tl?ie=UTF8&amp;amp;camp=1789&amp;amp;creative=9325&amp;amp;creativeASIN=B079MFTYMV&amp;amp;linkCode=as2&amp;amp;tag=dogeared08-20&amp;amp;linkId=cb3cabf15cf0c454218eea9cdb31f008&quot;&gt;Gosund Mini WiFi Outlet 4-Pack&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.amazon.com/gp/product/B07N9BCJRW/ref=as_li_tl?ie=UTF8&amp;amp;tag=dogeared08-20&amp;amp;camp=1789&amp;amp;creative=9325&amp;amp;linkCode=as2&amp;amp;creativeASIN=B07N9BCJRW&amp;amp;linkId=63767f28d14701958bac6d9d7c688e97&quot;&gt;Voice Control Your Fireplace&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.amazon.com/gp/product/B01MU9SH77/ref=as_li_tl?ie=UTF8&amp;amp;tag=dogeared08-20&amp;amp;camp=1789&amp;amp;creative=9325&amp;amp;linkCode=as2&amp;amp;creativeASIN=B01MU9SH77&amp;amp;linkId=f45010c8bd5f046672d8603571f3a570&quot;&gt;Leviton Decora Smart Wi-Fi Switch&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.amazon.com/gp/product/B01NASBN1V/ref=as_li_tl?ie=UTF8&amp;amp;tag=dogeared08-20&amp;amp;camp=1789&amp;amp;creative=9325&amp;amp;linkCode=as2&amp;amp;creativeASIN=B01NASBN1V&amp;amp;linkId=613319b6637ef7d0a61207dac47df4d3&quot;&gt;Leviton Decora Smart Wi-Fi Dimmer&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I hope you dug my little tour of home automation. I’d love to hear your experiences. At me at @afitnerd on twitter or comment on this post.&lt;/p&gt;
</description>
        <pubDate>Sat, 01 Aug 2020 13:00:00 +0000</pubDate>
        <link>https://afitnerd.com/2020/08/01/golden-age-home-automation/</link>
        <guid isPermaLink="true">https://afitnerd.com/2020/08/01/golden-age-home-automation/</guid>
        
        <category>tech</category>
        
        <category>diy</category>
        
        
        <category>technology</category>
        
      </item>
    
      <item>
        <title>Travel Necessities of the Modern Road Warrior</title>
        <description>&lt;p&gt;&lt;img src=&quot;/images/road-warrior/tplink.png&quot; alt=&quot;tplink&quot; width=&quot;300&quot; /&gt;&lt;/p&gt;

&lt;p&gt;I’ve now been a full time Developer Evangelist for 4 years (at two different companies). Over this period of time, my travel requirements have steadily increased. At this point I am averaging travel two out of four weeks every month and over the next two months, I will be in ten different locations, including international travel to Australia.&lt;/p&gt;

&lt;p&gt;Like most people in Developer Evangelism (or Developer Relations or Developer Advocacy), my travel expenses are covered by the company I work for. So, clean, decent hotels are the baseline. I am very particular about my “creature comforts” when I travel, and most hotels - even great ones - tend to fall short.&lt;/p&gt;

&lt;p&gt;In this post, I share with you my essentials for travel along with links. What follows is 100% opinion. YMMV!&lt;/p&gt;

&lt;h2 id=&quot;what-you-need-to-get-there&quot;&gt;What you Need to Get There&lt;/h2&gt;

&lt;p&gt;At $85 for five years, &lt;a href=&quot;https://www.tsa.gov/precheck&quot;&gt;TSA Precheck&lt;/a&gt; is well worth the cost. If you’re not familiar with the service, you make an appointment at a local office or at an airport. You get a background check and you get finger-printed. Once approved, you get to go on a faster line at the airport. And, more importantly, you don’t have to remove your belt or lightweight jacket. Nor do you have to take your laptop out of its case. Recently, I’ve also purchased &lt;a href=&quot;https://www.clearme.com&quot;&gt;Clear&lt;/a&gt;. This gets you through security even faster. When you register for Clear, your retina and finger prints are scanned. At the airport, you go to the Clear kiosks and scan your retina or fingerprints. Once verified, you’re escorted to the front of the pre-check line and you need only show your boarding pass to the TSA Agent. You don’t even have to show your id! Clear is more expensive at $179 / year and it’s not available at all airports.&lt;/p&gt;

&lt;p&gt;As often as possible, I try not to check luggage. I use carry-on luggage. I like to get on and off the plane and out of the airport as quickly as possible.&lt;/p&gt;

&lt;p&gt;I’ve gone through a number of iterations with carry-on luggage. In the past, I’ve been super cheap - and paid the (actual) price. That is, luggage that falls apart, or has wheels that lock up or wear out, or is not designed well for efficient packing. I recently purchased the Bigger Carry-On by &lt;a href=&quot;https://www.awaytravel.com/suitcases/bigger-carry-on/navy&quot;&gt;Away&lt;/a&gt; and it’s superb. The wheels are smoothy-smooth. The zippers are tough, but easy to open and close. Inside, the right section has a metal-framed mat that sits over your clothes and has straps across it that when tightened, compress everything down. The left section is a spacious open compartment for loose items, like shoes. I chose the battery, which has a 10,000mAh ejectable battery pack. It’s perfect for quick charging while waiting to board at the airport. It comes in at $245 and (most importantly) has a &lt;a href=&quot;https://www.awaytravel.com/warranty&quot;&gt;lifetime warranty&lt;/a&gt;. I’ve spent way more than this on a bunch of cheapo luggage with basically no warranty.&lt;/p&gt;

&lt;h2 id=&quot;what-you-need-when-youre-there&quot;&gt;What you Need when You’re There&lt;/h2&gt;

&lt;p&gt;When I arrive at a hotel, I like to make it my own. It usually takes me 5 - 10 minutes to “customize” the room. But once I’m done, everything is just the way I like it.&lt;/p&gt;

&lt;h3 id=&quot;hotels-have-shitty-internet&quot;&gt;Hotels have Shitty Internet&lt;/h3&gt;

&lt;p&gt;Hotel WiFi tends to be slow at best, and insecure at worst. Using a VPN on hotel WiFi can make it even slower. Fortunately, most hotels have a wired connection. That’s why I travel with a &lt;a href=&quot;https://amzn.to/2SiuAWt&quot;&gt;TP-Link&lt;/a&gt; compact wireless router. This serves two purposes. The first is that I get better speed. The second is that all my devices are all ready to connect to the WPA2 protected network. This alleviates the need to navigate the usually terrible captive portal flow on devices like my Amazon FireTV Stick.&lt;/p&gt;

&lt;p&gt;This brings me to the next point.&lt;/p&gt;

&lt;h3 id=&quot;hotels-have-shitty-tv&quot;&gt;Hotels have Shitty TV&lt;/h3&gt;

&lt;p&gt;There. I said it! As much as I love Mario Lopez, I’m tired of him being the first thing I see when I turn on a hotel tv. I cut cable a long time ago. I’m a creature of habit and I don’t like to have to endure commercials and E! entertainment just because I am at a hotel. One of the first things I do is pop my &lt;a href=&quot;https://amzn.to/2Y1k4Iv&quot;&gt;Amazon FireTV Stick&lt;/a&gt; into an available port on the TV.&lt;/p&gt;

&lt;p&gt;My favorite hotels have a module - often embedded in furniture - that allows you to plug an HDMI device right in. Often times, I have to go hunting around the back of the TV for an available port.&lt;/p&gt;

&lt;p&gt;Occasionally, a hotel will have any access to ports on the TV blocked off. That’s why I bought this cheap &lt;a href=&quot;https://amzn.to/2XNZQT6&quot;&gt;HDMI coupler&lt;/a&gt;. In these cases, I can just hijack the HDMI cable coming out of the cable box and connect it to my FireTV without needing access to the back of the TV.&lt;/p&gt;

&lt;h3 id=&quot;hotels-are-mad-dry&quot;&gt;Hotels are Mad Dry&lt;/h3&gt;

&lt;p&gt;I rarely get sick. But, I’ve been getting sicker more often as I amp up my travel schedule. Being trapped in a metal test tube with everyone else’s germs and then going to a dusty, dry hotel - well, it’s enough to turn anyone into a germophobe.&lt;/p&gt;

&lt;p&gt;I found this great little &lt;a href=&quot;https://amzn.to/2XHNTOO&quot;&gt;travel humidifier&lt;/a&gt; that Feng Shui’s up the hotel room
a bit. It may be psychosomatic, but I don’t get dry throat or cough when I use it on the road.&lt;/p&gt;

&lt;p&gt;As an aside, I use the &lt;a href=&quot;https://www.getquip.com/rf?referral_code=tqmmhjkv&quot;&gt;Quip&lt;/a&gt; toothbrush (full disclosure - that is an affiliate link). It turns out that the plastic tube the Quip comes in is perfect for keeping long body of the humidifier in. You just have to be careful to let it dry out from time to time so you don’t end up with a science experiment.&lt;/p&gt;

&lt;h3 id=&quot;hotel-power-outlets-are-infuriating&quot;&gt;Hotel Power Outlets are Infuriating&lt;/h3&gt;

&lt;p&gt;It seems that most of the hotels I stay at - even really nice ones fall into two camps with regard to power outlets. Either there aren’t enough or there’s a bunch located across the room in some inconvenient spot.&lt;/p&gt;

&lt;p&gt;This is something that’s improving as a number of hotels I’ve stayed at recently have clocks or lamps with power outlets and USB ports.&lt;/p&gt;

&lt;p&gt;I travel with this &lt;a href=&quot;https://amzn.to/2xOSYWb&quot;&gt;5 outlet&lt;/a&gt; wonder. In addition to the 5 3-prong power outlets, it has a retractable cord, 2 USB-A ports and even a USB-C port. I will sacrifice a lamp on the night table to have this plugged in.&lt;/p&gt;

&lt;h3 id=&quot;honorable-mentions&quot;&gt;Honorable Mentions&lt;/h3&gt;

&lt;p&gt;There’s a whole bunch of other kit I have with me to make traveling easy and functional.&lt;/p&gt;

&lt;p&gt;I use the &lt;a href=&quot;https://amzn.to/2XM5uVJ&quot;&gt;eBags Professional Slim Laptop Backpack&lt;/a&gt;. It’s got a ton of storage while still being reasonably slim. It always fits under the seat on the airplane.&lt;/p&gt;

&lt;p&gt;I love the Omnicharge product line and I’ve backed their products on Kickstarter. The one I use now is the &lt;a href=&quot;https://amzn.to/2LoT7sl&quot;&gt;Omni 20+&lt;/a&gt;. It’s beefy 20,000mAh battery is in a lean enclosure with an unholy number of ports, including USB-C, USB-A and a standard 3-prong 120V outlet. You can plug nearly anything into it and charge it via nearly any of its ports. I can run my 15” MacBook Pro off it for nearly 2 hours. It even has 10W wireless charging. I can just place my phone on top of it and bam - it’s charging.&lt;/p&gt;

&lt;p&gt;I always have a couple of &lt;a href=&quot;https://amzn.to/2XOQZRh&quot;&gt;retractable ethernet cables&lt;/a&gt; on me to use when I’m onsite with a customer or in the hotel room using my WiFi router.&lt;/p&gt;

&lt;p&gt;Getting the right size travel containers for shampoo, body wash, etc. can be a pain. It needs to be 3oz or less to be in my carry-on. It needs to be totally leak proof. These &lt;a href=&quot;https://amzn.to/2XHO8tc&quot;&gt;silicon travel tubes&lt;/a&gt; do the trick nicely.&lt;/p&gt;

&lt;h2 id=&quot;the-full-travel-kit&quot;&gt;The Full Travel Kit&lt;/h2&gt;

&lt;p&gt;I hope this post helps you if you travel a lot as I do.&lt;/p&gt;

&lt;p&gt;Here’s the full list of my travel gear:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.awaytravel.com/suitcases/bigger-carry-on&quot;&gt;The Bigger Carry-on from Away&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://amzn.to/2XM5uVJ&quot;&gt;eBags Professional Slim Laptop Backpack&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://amzn.to/2LoT7sl&quot;&gt;Omni 20+ AC/DC/USB-C Portable Power Bank&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://amzn.to/2xOSYWb&quot;&gt;Travel Power Strip 5 Outlet Surge Protector with Retractable Cord Smart USB Ports and Type-C Port&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://amzn.to/2SiuAWt&quot;&gt;TP-Link AC750 Wireless Portable Nano Travel Router&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://amzn.to/2XOQZRh&quot;&gt;Cable Matters 2-Pack Retractable Ethernet Cable&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://amzn.to/2Y1k4Iv&quot;&gt;Amazon Fire TV Stick&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://getquip.com/rf?referral_code=tqmmhjkv&quot;&gt;Quip Electric toothbrush&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://amzn.to/2XHNTOO&quot;&gt;USB Cool Mist Humidifier Travel Air Humidifier&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://amzn.to/2Y5kgqe&quot;&gt;AmazonBasics Hanging Travel Toiletry Kit Bag&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://amzn.to/2XHO8tc&quot;&gt;Gear Silicone Squeezable Travel Tubes&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://amzn.to/2XNZQT6&quot;&gt;AmazonBasics HDMI Coupler (2 Pack)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’ve also organized this on &lt;a href=&quot;https://kit.com/dogeared/travel-kit&quot;&gt;kit&lt;/a&gt; where you can see all the items in one place.&lt;/p&gt;

&lt;p&gt;Feel free to shoot me any questions at: micah@afitnerd.com or tweet at me: @afitnerd&lt;/p&gt;

</description>
        <pubDate>Fri, 12 Jul 2019 13:00:00 +0000</pubDate>
        <link>https://afitnerd.com/2019/07/12/road-warrior-kit/</link>
        <guid isPermaLink="true">https://afitnerd.com/2019/07/12/road-warrior-kit/</guid>
        
        <category>travel</category>
        
        
        <category>technology</category>
        
      </item>
    
      <item>
        <title>Omni Charge Delivers Power</title>
        <description>&lt;p&gt;As a Samsung Galaxy S8 owner and software developer with a 2016 MacBook Pro,
Apple has done me a huge favor in moving over to USB-C for all its ports.&lt;/p&gt;

&lt;p&gt;Of course, they completely screwed over their loyal iPhone customers in doing
so. But, that’s a post for another time.&lt;/p&gt;

&lt;p&gt;USB-C is the perfect standard for modern devices as the same port can be used
both to charge the pack itself and to charge the external device.&lt;/p&gt;

&lt;p&gt;This post is all about my quest for the perfect battery backup.&lt;/p&gt;

&lt;p&gt;Let’s start with the punchline: The &lt;a href=&quot;https://www.indiegogo.com/projects/omnicharge-most-powerful-dual-usb-c-power-bank-smartphone-powerbank&quot;&gt;omni 20 usb-c&lt;/a&gt;
is it.&lt;/p&gt;

&lt;p&gt;Over the years, I’ve gone through a &lt;em&gt;lot&lt;/em&gt; of battery backups. I quickly 
discarded the type that you get from conferences as not viable for my needs. 
Those are usually around 2500mAh and, aside from being barely able to fully 
charge a modern mobile device even once, they don’t hold their own charge, so 
you end up with a dead battery backup when you need it most.&lt;/p&gt;

&lt;p&gt;For quite a while I was very happy with my &lt;a href=&quot;https://www.amazon.com/16750mAh-Portable-Charger-Nintendo-External/dp/B01KHDFXCE&quot;&gt;iMuto 16.750mAh&lt;/a&gt; backup.
They’re now pitching it as a “portable chargeer for Nintendo Switch”. 
Can’t say I blame them. That Nintendo Switch is so hot right now…&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://afitnerd.com/images/omnicharge/omni1.jpg&quot; alt=&quot;iMuto 1&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://afitnerd.com/images/omnicharge/omni2.jpg&quot; alt=&quot;iMuto 2&quot; /&gt;&lt;/p&gt;

&lt;p&gt;This battery backup was the first I’d found with a native USB-C port. It charged
up super quick using a USB-C wall charger and fast-charged my Nexus 6P and
later, my Samsung S8. It holds its charge for a very long time. It’s rugged and
the simple LED display lets you know how much juice you have left.&lt;/p&gt;

&lt;p&gt;Then, I got the new MacBook Pro with 4 USB-C ports. I was excited to see what
the iMuto could do, if anything. The MBP has pretty heavy duty power 
requirements and at a max output of 15 Watts, the iMuto just wasn’t up to the 
task.&lt;/p&gt;

&lt;p&gt;Then, the Internet spoke. I became interested in the omnicharge when they
announced their &lt;a href=&quot;https://www.omnicharge.co/products/omni-20&quot;&gt;120V inverter battery backup&lt;/a&gt;.
It was originally on Indiegogo and, at the time, I decided not to buy it. While
it’s more general purpose, I heard that they were coming out with a USB-C model.
That would fit better with the amount of travel that I do. For any of my
non-USB-C devices, like my iPad mini, I bought &lt;a href=&quot;https://www.amazon.com/gp/product/B075WY6VQ8/ref=oh_aui_detailpage_o07_s00?ie=UTF8&amp;amp;psc=1&quot;&gt;USB-C cables&lt;/a&gt;. 
No dongles for me. I’m all in on USB-C.&lt;/p&gt;

&lt;h2 id=&quot;the-unboxening&quot;&gt;The Unboxening&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;https://afitnerd.com/images/omnicharge/unbox1.jpg&quot; alt=&quot;omni 1&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://afitnerd.com/images/omnicharge/unbox2.jpg&quot; alt=&quot;omni 2&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://afitnerd.com/images/omnicharge/unbox3.jpg&quot; alt=&quot;omni 3&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://afitnerd.com/images/omnicharge/unbox4.jpg&quot; alt=&quot;omni 4&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href=&quot;https://www.omnicharge.co/omni-20-usb-c&quot;&gt;omni 20 usb-c&lt;/a&gt; comes in a 
minimalist box. Inside, it has a small set of stickers, a square card with a url 
for the &lt;a href=&quot;http://www.omnicharge.co/downloads/usb-c-manual.pdf&quot;&gt;user manual&lt;/a&gt; and 
the battery backup itself nestled in squishy foam.&lt;/p&gt;

&lt;h2 id=&quot;setup-and-testing&quot;&gt;Setup and Testing&lt;/h2&gt;

&lt;p&gt;The manual recommends that you perform an initial “calibration” by draining
the battery to 0% and then charging it to 100%. Out of the box, it showed 18%.
I connected my Samsung S8, iPad mini and, the iMuto 16,750mAh battery pack to
it and waited until it was dead.&lt;/p&gt;

&lt;p&gt;I then connected it to my USB-C native 29W Apple power supply and a little over
2.5 hours later, it was back to 100%. The manual says it will take 3 hours to
charge, so - right on track there.&lt;/p&gt;

&lt;p&gt;Next, I wanted to see how long it would power my 15” MacBook Pro under load.
This is a more “real world” test than the usual “ideal conditions” type of test.&lt;/p&gt;

&lt;p&gt;The test was simple - How long would it keep my MBP at 100% with the 
charging indicator showing.&lt;/p&gt;

&lt;p&gt;When I say, “under load”, I mean it! I started the test at the beginning of a
7-hour screenshare, during which time I was teaching a course. In addition to
screen sharing, I was running Powerpoint and I was connected to a virtual 
machine environment - effectively screesharing a remote screen! And, I was
driving two external 27” displays in addition to the built-in display.&lt;/p&gt;

&lt;p&gt;The omni 20 usb-c lasted for 1 hour, 45 minutes. I’m quite pleased with that
result as the conditions that I would normally be using the battery backup under
would be much less severe. My most common use for this will be on a long flight,
where I’d have the screen brightness lower than normal and I would not be
sharing my screen or driving external displays.&lt;/p&gt;

&lt;h2 id=&quot;some-stats&quot;&gt;Some Stats&lt;/h2&gt;

&lt;p&gt;For comparison, my old iMuto weights 10.5 ounces. The omni 20 usb-c weighs 17.6
ounces. The omni is just about the same thickness as the iMuto, although it’s
considerably wider. It fits in my backpack well and the extra 7 ounces is worth
it.&lt;/p&gt;

&lt;p&gt;The omni 20 usb-c can produce 45W, which is plenty to charge the MBP.&lt;/p&gt;

&lt;p&gt;Its tiny OLED display is packed with great information including available
watt-hours, battery temperature, activated ports and expected time remaining or
charge time.&lt;/p&gt;

&lt;p&gt;It has two USB-C ports and two USB-A ports.&lt;/p&gt;

&lt;p&gt;Full stats on the omni 20 usb-c can be found &lt;a href=&quot;https://www.omnicharge.co/omni-20-usb-c&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Full stats on the iMuto 16,750 can be found &lt;a href=&quot;https://www.amazon.com/16750mAh-Portable-Charger-Nintendo-External/dp/B01KHDFXCE&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;power-nerds-unite&quot;&gt;Power Nerds, Unite!&lt;/h2&gt;

&lt;p&gt;I am accumulating quite a bit of USB-C powered devices. The most recent is a
Nintendo Switch. While I have not yet tried to use the omni 20 with it, I am
sure it will work like a champ when I want it to.&lt;/p&gt;

&lt;p&gt;Do you nerd out over power backup solutions too? I’d love to hear your
experiences in the comments section below.&lt;/p&gt;
</description>
        <pubDate>Sat, 03 Feb 2018 13:00:00 +0000</pubDate>
        <link>https://afitnerd.com/2018/02/03/omnicharge-ftw/</link>
        <guid isPermaLink="true">https://afitnerd.com/2018/02/03/omnicharge-ftw/</guid>
        
        <category>usbc</category>
        
        <category>usb-c</category>
        
        <category>battery</category>
        
        <category>charge</category>
        
        <category>charging</category>
        
        
        <category>technology</category>
        
      </item>
    
      <item>
        <title>What if Spring Boot Handled Forms Like JSON?</title>
        <description>&lt;p&gt;What follows is an unexpected journey to some of the inner workings of Spring
Boot. In particular, how it handles materializing POJOs from an incoming HTTP
request. It all started with an innocent look at the Slack API…&lt;/p&gt;

&lt;p&gt;All of the code mentioned in this post can be found in the
&lt;a href=&quot;https://github.com/dogeared/slack-slash-command-example&quot;&gt;slack-slash-command-example&lt;/a&gt;
github repo.&lt;/p&gt;

&lt;p&gt;I use &lt;a href=&quot;https://slack.com/&quot;&gt;Slack&lt;/a&gt;. A lot. I’m currently in 12 slack orgs.
One of them is even a paid org! I thought I’d play around with the Slack API
and I started with
&lt;a href=&quot;https://api.slack.com/slash-commands&quot;&gt;Slack Slash commands&lt;/a&gt;, as it seemed like
the easiest point of entry.&lt;/p&gt;

&lt;p&gt;I also live and breathe &lt;a href=&quot;https://projects.spring.io/spring-boot/&quot;&gt;Spring Boot&lt;/a&gt;.
It’s so easy to write APIs with Spring Boot, that this seemed like the most
natural place to start. Here’s the simplest one-file Spring Boot API app:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-java&quot; data-lang=&quot;java&quot;&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;nd&quot;&gt;@SpringBootApplication&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@RestController&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@RequestMapping&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/api/v1&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SlackApplication&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;SpringApplication&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;SlackApplication&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@RequestMapping&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/slack&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nd&quot;&gt;@ResponseBody&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Object&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;slack&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nd&quot;&gt;@RequestBody&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Object&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;slackSlashCommand&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;slackSlashCommand&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Thanks to the magic of Spring Boot and its creation of a fully executable jar,
I can easily fire up this example:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;target/slack-slash-command-example-0.0.1-SNAPSHOT.jar
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;On Windows, you may have to run:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;java -jar target/slack-slash-command-example-0.0.1-SNAPSHOT.jar
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I can hit my API like so
(I am using &lt;a href=&quot;https://httpie.org/&quot;&gt;HTTPie&lt;/a&gt;, a modern curl replacement):&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;http POST localhost:8080/api/v1/slack &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;token&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;token &lt;span class=&quot;nv&quot;&gt;team_id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;team_id &lt;span class=&quot;nv&quot;&gt;team_domain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;team_domain &lt;span class=&quot;nv&quot;&gt;channel_id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;channel_id &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;channel_name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;channel_name &lt;span class=&quot;nv&quot;&gt;user_id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;user_id &lt;span class=&quot;nv&quot;&gt;user_name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;user_name &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;command &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;text &lt;span class=&quot;nv&quot;&gt;response_url&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;response_url

HTTP/1.1 200
Content-Type: application/json&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;charset&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;UTF-8
Date: Wed, 24 May 2017 14:39:48 GMT
Transfer-Encoding: chunked

&lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;s2&quot;&gt;&quot;channel_id&quot;&lt;/span&gt;: &lt;span class=&quot;s2&quot;&gt;&quot;channel_id&quot;&lt;/span&gt;,
    &lt;span class=&quot;s2&quot;&gt;&quot;channel_name&quot;&lt;/span&gt;: &lt;span class=&quot;s2&quot;&gt;&quot;channel_name&quot;&lt;/span&gt;,
    &lt;span class=&quot;s2&quot;&gt;&quot;command&quot;&lt;/span&gt;: &lt;span class=&quot;s2&quot;&gt;&quot;command&quot;&lt;/span&gt;,
    &lt;span class=&quot;s2&quot;&gt;&quot;response_url&quot;&lt;/span&gt;: &lt;span class=&quot;s2&quot;&gt;&quot;response_url&quot;&lt;/span&gt;,
    &lt;span class=&quot;s2&quot;&gt;&quot;team_domain&quot;&lt;/span&gt;: &lt;span class=&quot;s2&quot;&gt;&quot;team_domain&quot;&lt;/span&gt;,
    &lt;span class=&quot;s2&quot;&gt;&quot;team_id&quot;&lt;/span&gt;: &lt;span class=&quot;s2&quot;&gt;&quot;team_id&quot;&lt;/span&gt;,
    &lt;span class=&quot;s2&quot;&gt;&quot;text&quot;&lt;/span&gt;: &lt;span class=&quot;s2&quot;&gt;&quot;text&quot;&lt;/span&gt;,
    &lt;span class=&quot;s2&quot;&gt;&quot;token&quot;&lt;/span&gt;: &lt;span class=&quot;s2&quot;&gt;&quot;token&quot;&lt;/span&gt;,
    &lt;span class=&quot;s2&quot;&gt;&quot;user_id&quot;&lt;/span&gt;: &lt;span class=&quot;s2&quot;&gt;&quot;user_id&quot;&lt;/span&gt;,
    &lt;span class=&quot;s2&quot;&gt;&quot;user_name&quot;&lt;/span&gt;: &lt;span class=&quot;s2&quot;&gt;&quot;user_name&quot;&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;NOTE: Contrary to the way the above command looks, the data is sent over as
JSON with a Content-Type header of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;application/json&lt;/code&gt;. Thank you, HTTPie.&lt;/p&gt;

&lt;p&gt;Well, that was easy!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;EXCEPT, this doesn’t work with the Slack Slash Command API at all.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Looking more closely at the &lt;a href=&quot;https://api.slack.com/slash-commands&quot;&gt;API&lt;/a&gt;, I see
that Slack does the POST with an old-school &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;application/x-www-form-urlencoded&lt;/code&gt;
content type.&lt;/p&gt;

&lt;p&gt;“OK”, I thought, “Spring Boot is pretty smart, let me try sending over the
params as form input rather than JSON.”&lt;/p&gt;

&lt;p&gt;I can hit my API again, with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;application/x-www-form-urlencoded&lt;/code&gt; by simply
adding &lt;span style=&quot;white-space:nowrap;&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--form&lt;/code&gt;&lt;/span&gt; to the command line:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;http &lt;span class=&quot;nt&quot;&gt;--form&lt;/span&gt; POST localhost:8080/api/v1/slack &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;token&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;token &lt;span class=&quot;nv&quot;&gt;team_id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;team_id &lt;span class=&quot;nv&quot;&gt;team_domain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;team_domain &lt;span class=&quot;nv&quot;&gt;channel_id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;channel_id &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;channel_name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;channel_name &lt;span class=&quot;nv&quot;&gt;user_id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;user_id &lt;span class=&quot;nv&quot;&gt;user_name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;user_name &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;command &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;text &lt;span class=&quot;nv&quot;&gt;response_url&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;response_url

HTTP/1.1 415
Content-Type: application/json&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;charset&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;UTF-8
Date: Wed, 24 May 2017 14:55:34 GMT
Transfer-Encoding: chunked

&lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;s2&quot;&gt;&quot;error&quot;&lt;/span&gt;: &lt;span class=&quot;s2&quot;&gt;&quot;Unsupported Media Type&quot;&lt;/span&gt;,
  &lt;span class=&quot;s2&quot;&gt;&quot;exception&quot;&lt;/span&gt;: &lt;span class=&quot;s2&quot;&gt;&quot;org.springframework.web.HttpMediaTypeNotSupportedException&quot;&lt;/span&gt;,
  &lt;span class=&quot;s2&quot;&gt;&quot;message&quot;&lt;/span&gt;: &lt;span class=&quot;s2&quot;&gt;&quot;Content type &apos;application/x-www-form-urlencoded;charset=utf-8&apos; not supported&quot;&lt;/span&gt;,
  &lt;span class=&quot;s2&quot;&gt;&quot;path&quot;&lt;/span&gt;: &lt;span class=&quot;s2&quot;&gt;&quot;/api/v1/slack&quot;&lt;/span&gt;,
  &lt;span class=&quot;s2&quot;&gt;&quot;status&quot;&lt;/span&gt;: 415,
  &lt;span class=&quot;s2&quot;&gt;&quot;timestamp&quot;&lt;/span&gt;: 1495637734535
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Hrmph. I’ve gotten so used to modern APIs using JSON (ahem, Slack - what gives?)
that I wasn’t sure how to handle regular old form input in an API. To the
Google!&lt;/p&gt;

&lt;h2 id=&quot;modelattributes-webdatabinders-and-httpmessageconverters---oh-my&quot;&gt;ModelAttributes, WebDataBinders and HttpMessageConverters - Oh My!&lt;/h2&gt;

&lt;p&gt;Turns out, Spring Boot creates Form beans with the &lt;a href=&quot;http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/bind/WebDataBinder.html&quot;&gt;WebDataBinder&lt;/a&gt;.
Non-Form beans are created using an
&lt;a href=&quot;https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/http/converter/HttpMessageConverter.html&quot;&gt;HttpMessageConverter&lt;/a&gt;. 
Spring Boot exposes the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MappingJackson2HttpMessageConverter&lt;/code&gt; which handles the
JSON mapping magic.&lt;/p&gt;

&lt;p&gt;First thing I wanted to do was to stop using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Map&amp;lt;String, Object&amp;gt;&lt;/code&gt; datatypes and
to make a proper model object. Using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Map&amp;lt;String, Object&amp;gt;&lt;/code&gt; is a handy trick
when you’re interacting with an external API and you’re not sure exactly what
you are going to get from it. So, I created the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormSlackSlashCommand&lt;/code&gt; model:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-java&quot; data-lang=&quot;java&quot;&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;FormSlackSlashCommand&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;token&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@JsonProperty&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;team_id&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;teamId&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@JsonProperty&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;team_domain&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;teamDomain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@JsonProperty&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;channel_id&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;channelId&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@JsonProperty&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;channel_name&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;channelName&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@JsonProperty&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;user_id&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;userId&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@JsonProperty&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;user_name&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;userName&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@JsonProperty&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;response_url&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;responseUrl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;o&quot;&gt;...&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// setters and getters&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Hold on, there! If this POJO is used for Form handling, why all the
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@JsonProperty&lt;/code&gt; annotations? Well, I want to be able to return this POJO as
JSON, with properties that conform to the Slack API. These annotations don’t
get in the way of our Form processing in any way. We’ll see how they come into
play shortly.&lt;/p&gt;

&lt;p&gt;Here’s an updated Controller:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-java&quot; data-lang=&quot;java&quot;&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;nd&quot;&gt;@RestController&lt;/span&gt;
&lt;span class=&quot;nd&quot;&gt;@RequestMapping&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/api/v1&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SlackController&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Logger&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;log&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;LoggerFactory&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getLogger&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;SlackController&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@RequestMapping&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/slack&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;method&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;RequestMethod&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;POST&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;consumes&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MediaType&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;APPLICATION_FORM_URLENCODED_VALUE&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;produces&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MediaType&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;APPLICATION_JSON_VALUE&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nd&quot;&gt;@ResponseBody&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;FormSlackSlashCommand&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;slack&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nd&quot;&gt;@ModelAttribute&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;FormSlackSlashCommand&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;slackSlashCommand&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;info&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;slackSlashCommand: {}&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;slackSlashCommand&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;slackSlashCommand&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Notice the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;consumes&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;produces&lt;/code&gt; attributes of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@RequestMapping&lt;/code&gt;
annotation. This controller method will take HTTP Form input and respond with
JSON.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@ModelAttribute&lt;/code&gt; annotation ensures that the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WebDataBinder&lt;/code&gt; is engaged to
materialize the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormSlackSlashCommand&lt;/code&gt; object. Looks like we’re done! I’ll just
hit the endpoint again to make sure:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;http &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; POST localhost:8080/api/v1/slack &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;token&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;token &lt;span class=&quot;nv&quot;&gt;team_id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;team_id &lt;span class=&quot;nv&quot;&gt;team_domain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;team_domain &lt;span class=&quot;nv&quot;&gt;channel_id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;channel_id &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;channel_name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;channel_name &lt;span class=&quot;nv&quot;&gt;user_id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;user_id &lt;span class=&quot;nv&quot;&gt;user_name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;user_name &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;command &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;text&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;text &lt;span class=&quot;nv&quot;&gt;response_url&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;response_url

HTTP/1.1 200
Content-Type: application/json&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;charset&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;UTF-8
Date: Thu, 25 May 2017 03:55:49 GMT
Transfer-Encoding: chunked

&lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;s2&quot;&gt;&quot;channel_id&quot;&lt;/span&gt;: null,
    &lt;span class=&quot;s2&quot;&gt;&quot;channel_name&quot;&lt;/span&gt;: null,
    &lt;span class=&quot;s2&quot;&gt;&quot;command&quot;&lt;/span&gt;: &lt;span class=&quot;s2&quot;&gt;&quot;command&quot;&lt;/span&gt;,
    &lt;span class=&quot;s2&quot;&gt;&quot;response_url&quot;&lt;/span&gt;: null,
    &lt;span class=&quot;s2&quot;&gt;&quot;team_domain&quot;&lt;/span&gt;: null,
    &lt;span class=&quot;s2&quot;&gt;&quot;team_id&quot;&lt;/span&gt;: null,
    &lt;span class=&quot;s2&quot;&gt;&quot;text&quot;&lt;/span&gt;: &lt;span class=&quot;s2&quot;&gt;&quot;text&quot;&lt;/span&gt;,
    &lt;span class=&quot;s2&quot;&gt;&quot;token&quot;&lt;/span&gt;: &lt;span class=&quot;s2&quot;&gt;&quot;token&quot;&lt;/span&gt;,
    &lt;span class=&quot;s2&quot;&gt;&quot;user_id&quot;&lt;/span&gt;: null,
    &lt;span class=&quot;s2&quot;&gt;&quot;user_name&quot;&lt;/span&gt;: null
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;What the what?!? Well, Spring Boot currently just doesn’t handle Form
submissions as flexibly as it does JSON. It automatically handles the “simple”
attributes, like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;command&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;text&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;token&lt;/code&gt; using the corresponding setters
in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormSlackSlashCommand&lt;/code&gt;. But, there is no corresponding setter to deal
with attributes like: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;channel_id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here’s a two-year-old Jira issue that proposes adding an annotation for
handling Form input in a similar way that the Jackson mapper handles JSON
input: &lt;a href=&quot;https://jira.spring.io/browse/SPR-13433&quot;&gt;https://jira.spring.io/browse/SPR-13433&lt;/a&gt;
(I humbly request your votes on this issue!)&lt;/p&gt;

&lt;p&gt;Following this discovery, I came up with three approaches to address
handling Form input more flexibly with what’s currently on the truck for Spring
Boot. Many thanks to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;r/java&lt;/code&gt; community for the feedback in the
&lt;a href=&quot;https://www.reddit.com/r/java/comments/6coo2x/what_if_spring_handled_form_posts_automatically/&quot;&gt;comments&lt;/a&gt; of my post there.&lt;/p&gt;

&lt;p&gt;Below follows an explanation of these approaches, from my least favorite to most
favorite (and, more importantly, from worst to best approach).&lt;/p&gt;

&lt;h2 id=&quot;first-approach-nasty-java-automatic-marshaling&quot;&gt;First Approach: Nasty Java, Automatic Marshaling&lt;/h2&gt;

&lt;p&gt;For, my first swipe at a solution, I wanted to have the least amount of
additional configuration code. Rather than deal with custom converters, I
wanted to hook into the existing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WebDataBinder&lt;/code&gt;. To do this, I needed to
break some Java syntax conventions. In order to keep the primary code “clean”,
I created a superclass for the sole purpose of properly materializing our
Object in the controller.&lt;/p&gt;

&lt;p&gt;I give you &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AbstractFormSlackSlashCommand&lt;/code&gt;:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-java&quot; data-lang=&quot;java&quot;&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;c1&quot;&gt;// workaround for customize x-www-form-urlencoded&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;abstract&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AbstractFormSlackSlashCommand&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setTeam_id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;teamId&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;setTeamId&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;teamId&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setTeam_domain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;teamDomain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;setTeamDomain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;teamDomain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setChannel_id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;channelId&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;setChannelId&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;channelId&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setChannel_name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;channelName&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;setChannelName&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;channelName&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setUser_id&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;userId&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;setUserId&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;userId&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setUser_name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;userName&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;setUserName&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;userName&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setResponse_url&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;responseUrl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;setResponseUrl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;responseUrl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;abstract&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setTeamId&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;teamId&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;abstract&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setTeamDomain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;teamDomain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;abstract&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setChannelId&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;channelId&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;abstract&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setChannelName&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;channelName&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;abstract&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setUserId&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;userId&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;abstract&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setUserName&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;userName&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;abstract&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;setResponseUrl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;responseUrl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormSlackSlashCommand&lt;/code&gt; class is the same as it was, except that now it
extends &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AbstractFormSlackSlashCommand&lt;/code&gt;:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-java&quot; data-lang=&quot;java&quot;&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
3
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;FormSlackSlashCommand&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AbstractFormSlackSlashCommand&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;o&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Now, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WebDataBinder&lt;/code&gt; can handle incoming form parameters such as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;team_id&lt;/code&gt;
thanks to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setTeam_id&lt;/code&gt; method.&lt;/p&gt;

&lt;p&gt;Aside from a lot of boilerplate code, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AbstractFormSlackSlashCommand&lt;/code&gt; has the
drawback of violating Java method naming conventions.&lt;/p&gt;

&lt;p&gt;On to the next approach!&lt;/p&gt;

&lt;h2 id=&quot;second-approach-custom-handlermethodargumentresolver&quot;&gt;Second Approach: Custom HandlerMethodArgumentResolver&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;https://www.reddit.com/user/sazzer&quot;&gt;u/sazzer&lt;/a&gt; posted the excellent suggestion
of creating an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HandleMethodArgumentResolver&lt;/code&gt; to materialize the Object in the
controller. I hadn’t heard or read about a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HandleMethodArgumentResolver&lt;/code&gt;
prior to this and it turned out to be a solid approach.&lt;/p&gt;

&lt;p&gt;Here’s the updated controller:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-java&quot; data-lang=&quot;java&quot;&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
3
4
5
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nd&quot;&gt;@ResponseBody&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SlackSlashCommand&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;slack&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;SlackSlashCommand&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;slackSlashCommand&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;info&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;slackSlashCommand: {}&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;slackSlashCommand&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;slackSlashCommand&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Notice that there’s no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@ModelAttribute&lt;/code&gt; annotation. That’s because we don’t
want the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WebDataBinder&lt;/code&gt; to materialize the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SlackSlashCommand&lt;/code&gt;. We’ll have
a custom &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HandlerMethodArgumentResolver&lt;/code&gt; do that for us.&lt;/p&gt;

&lt;p&gt;Note: In order to exercise all of the approaches in the
&lt;a href=&quot;https://github.com/dogeared/slack-slash-command-example&quot;&gt;slack-slash-command-example&lt;/a&gt;,
the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SlackSlashCommand&lt;/code&gt; class is basically the same as the
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormSlackSlashCommand&lt;/code&gt; class, except that it does not extend
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AbstractFormSlackSlashCommand&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here’s the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HandleMethodArgumentResolver&lt;/code&gt;:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-java&quot; data-lang=&quot;java&quot;&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SlackSlashCommandMethodArgumentResolver&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;implements&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;HandlerMethodArgumentResolver&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Override&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;boolean&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;supportsParameter&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;MethodParameter&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;methodParameter&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;methodParameter&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getParameterType&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;equals&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;SlackSlashCommand&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Override&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Object&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;resolveArgument&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;MethodParameter&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;methodParameter&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ModelAndViewContainer&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;modelAndViewContainer&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;NativeWebRequest&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nativeWebRequest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;WebDataBinderFactory&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;webDataBinderFactory&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;

        &lt;span class=&quot;nc&quot;&gt;SlackSlashCommand&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ret&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SlackSlashCommand&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;

        &lt;span class=&quot;n&quot;&gt;ret&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;setChannelId&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nativeWebRequest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getParameter&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;channel_id&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;));&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;ret&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;setChannelName&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nativeWebRequest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getParameter&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;channel_name&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;));&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;ret&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;setCommand&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nativeWebRequest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getParameter&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;command&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;));&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;ret&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;setResponseUrl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nativeWebRequest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getParameter&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;response_url&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;));&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;ret&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;setTeamDomain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nativeWebRequest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getParameter&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;team_domain&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;));&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;ret&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;setTeamId&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nativeWebRequest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getParameter&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;team_id&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;));&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;ret&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;setText&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nativeWebRequest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getParameter&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;text&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;));&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;ret&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;setToken&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nativeWebRequest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getParameter&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;token&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;));&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;ret&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;setUserId&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nativeWebRequest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getParameter&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;user_id&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;));&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;ret&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;setUserName&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nativeWebRequest&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getParameter&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;user_name&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;));&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ret&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;boolean&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;isNotSet&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;supportsParameter&lt;/code&gt; method determines whether or not the resolver will be
used to return a particular Object, in this case, a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SlackSlashCommand&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;resolveArgument&lt;/code&gt; method uses the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NativeWebRequest&lt;/code&gt; to obtain all of the
form parameters and create a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SlackSlashCommand&lt;/code&gt; object.&lt;/p&gt;

&lt;p&gt;The last piece of the puzzle is to ensure that Spring Boot uses this resolver.
That’s handled in a configuration:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-java&quot; data-lang=&quot;java&quot;&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
3
4
5
6
7
8
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;nd&quot;&gt;@Configuration&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SlackSlashCommandMethodArgumentResolverConfig&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;WebMvcConfigurerAdapter&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Override&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;addArgumentResolvers&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;HandlerMethodArgumentResolver&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;argumentResolvers&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;argumentResolvers&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SlackSlashCommandMethodArgumentResolver&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;());&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;This is a solid approach. However, there’s a lot of manual labor involved in
materializing the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SlackSlashCommand&lt;/code&gt; Object. Also, it’s a little more idiomatic
for a Spring Boot controller to have the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@ModelAttribute&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@RequestBody&lt;/code&gt;
annotation on an Object that’s part of the request.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://www.reddit.com/user/sazzer&quot;&gt;u/sazzer&lt;/a&gt; also suggested using reflection
to easily convert the form parameters into setter methods on the object.&lt;/p&gt;

&lt;p&gt;That led to my last and favorite approach.&lt;/p&gt;

&lt;h2 id=&quot;third-approach-custom-httpmessageconverter&quot;&gt;Third Approach: Custom HttpMessageConverter&lt;/h2&gt;

&lt;p&gt;This approach has the least amount of boilerplate code and reuses existing
components available to Spring Boot.&lt;/p&gt;

&lt;p&gt;As I said toward the beginning of the post, there are a bunch of built in
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HttpMessageConverter&lt;/code&gt; classes. The most common one is the
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MappingJackson2HttpMessageConverter&lt;/code&gt;, which handles JSON.&lt;/p&gt;

&lt;p&gt;As you would expect of Spring Boot, it’s easy to create a custom
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HttpMessageConverter&lt;/code&gt;.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-java&quot; data-lang=&quot;java&quot;&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SlackSlashCommandConverter&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;AbstractHttpMessageConverter&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;SlackSlashCommand&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;// no need to reinvent the wheel for parsing the query string&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;FormHttpMessageConverter&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;formHttpMessageConverter&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;FormHttpMessageConverter&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ObjectMapper&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mapper&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ObjectMapper&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Override&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;boolean&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;supports&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Class&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;?&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;clazz&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;SlackSlashCommand&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;clazz&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Override&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SlackSlashCommand&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;readInternal&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Class&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;?&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SlackSlashCommand&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;clazz&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;HttpInputMessage&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;inputMessage&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;IOException&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;HttpMessageNotReadableException&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;vals&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;formHttpMessageConverter&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;read&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;inputMessage&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;toSingleValueMap&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mapper&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;convertValue&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;vals&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SlackSlashCommand&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Override&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;protected&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;writeInternal&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;SlackSlashCommand&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;slackSlashCommand&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;HttpOutputMessage&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;outputMessage&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;IOException&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;HttpMessageNotWritableException&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;

    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;readInternal&lt;/code&gt; method takes an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HttpInputMessage&lt;/code&gt; as one if its parameters.
This is essentially a raw &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;InputStream&lt;/code&gt; of the incoming form parameters.&lt;/p&gt;

&lt;p&gt;At first, I thought I would have to manually read the stream and parse the
query string. Gross! Then, I remembered that the existing
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FormHttpMessageConverter&lt;/code&gt; takes an incoming form and resolves it to a
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MultiValueMap&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Based on the Slack Slash Command API, I also knew that all I needed was a single
value map. Line 14 of the above code takes care of that.&lt;/p&gt;

&lt;p&gt;At this point, I could manually create the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SlackSlashCommand&lt;/code&gt; object, like I
did in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SlackSlashCommandMethodArgumentResolver&lt;/code&gt;. However, the available
Jackson &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ObjectMapper&lt;/code&gt; is all ready to take a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Map&lt;/code&gt; and materialize it to an
Object for us. As a bonus, it will make use of the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@JsonProperty&lt;/code&gt; annotations
found in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SlackSlashCommand&lt;/code&gt; class. Whoa - that’s a lot of power in that
one line on line 16!&lt;/p&gt;

&lt;p&gt;Just two more bits tie this approach into a neat bow.&lt;/p&gt;

&lt;p&gt;First, the controller method now needs to use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@RequestBody&lt;/code&gt; annotation so
that the collection of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HttpMessageConverter&lt;/code&gt;s can be inspected and used.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-java&quot; data-lang=&quot;java&quot;&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
3
4
5
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nd&quot;&gt;@ResponseBody&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SlackSlashCommand&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;slack&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nd&quot;&gt;@RequestBody&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SlackSlashCommand&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;slackSlashCommand&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;info&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;slackSlashCommand: {}&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;slackSlashCommand&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;slackSlashCommand&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Now, that looks like a proper Spring boot controller method!&lt;/p&gt;

&lt;p&gt;We do need to register the custom &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HttpMessageConverter&lt;/code&gt; with Spring Boot. This
is accomplished with a configuration as before:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-java&quot; data-lang=&quot;java&quot;&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
3
4
5
6
7
8
9
10
11
12
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;nd&quot;&gt;@Configuration&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SlackSlashCommandConverterConfig&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;WebMvcConfigurerAdapter&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;

    &lt;span class=&quot;nd&quot;&gt;@Override&lt;/span&gt;
    &lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;configureMessageConverters&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;HttpMessageConverter&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;?&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;converters&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;SlackSlashCommandConverter&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;slackSlashCommandConverter&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;SlackSlashCommandConverter&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
        &lt;span class=&quot;nc&quot;&gt;MediaType&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mediaType&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;MediaType&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;application&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;x-www-form-urlencoded&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Charset&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;forName&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;UTF-8&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;));&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;slackSlashCommandConverter&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;setSupportedMediaTypes&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;Arrays&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;asList&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mediaType&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;));&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;converters&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;slackSlashCommandConverter&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;kd&quot;&gt;super&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;configureMessageConverters&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;converters&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;This instantiates our &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SlackSlashCommandConverter&lt;/code&gt;, ensures that it supports
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;x-www-form-urlencoded&lt;/code&gt; requests and adds it to the list of
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HttpMessageConverter&lt;/code&gt;s.&lt;/p&gt;

&lt;p&gt;The only drawback of this approach is that the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@RequestBody&lt;/code&gt; annotation is
typically used for non-form based requests. However, I think that the
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE&lt;/code&gt; attribute makes it
clear what’s going on.&lt;/p&gt;

&lt;h2 id=&quot;the-best-approach-of-all&quot;&gt;The Best Approach of All&lt;/h2&gt;

&lt;p&gt;I hope you’ve enjoyed our trek down workaround lane.&lt;/p&gt;

&lt;p&gt;If &lt;a href=&quot;https://jira.spring.io/browse/SPR-13433&quot;&gt;https://jira.spring.io/browse/SPR-13433&lt;/a&gt;
is implemented (Did I mention about voting on it?), then none of the above
approaches would be necessary. You’d simply annotate your POJO in a similar way
to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@JsonProperty&lt;/code&gt; to indicate how to map incoming form params to its fields.&lt;/p&gt;

&lt;p&gt;In your controller, you would simply use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@ModelAttribute&lt;/code&gt; annotation and
you’d be done.&lt;/p&gt;

&lt;p&gt;That would truly make Form handling as civilized as JSON handling already is in
Spring Boot.&lt;/p&gt;

&lt;p&gt;All of the code mentioned in this post can be found in the
&lt;a href=&quot;https://github.com/dogeared/slack-slash-command-example&quot;&gt;slack-slash-command-example&lt;/a&gt;
github repo.&lt;/p&gt;
</description>
        <pubDate>Wed, 24 May 2017 13:52:41 +0000</pubDate>
        <link>https://afitnerd.com/2017/05/24/what-if-spring-boot-handled-forms-like-json/</link>
        <guid isPermaLink="true">https://afitnerd.com/2017/05/24/what-if-spring-boot-handled-forms-like-json/</guid>
        
        <category>programming</category>
        
        <category>java</category>
        
        <category>spring boot</category>
        
        
        <category>programming</category>
        
        <category>java</category>
        
        <category>spring boot</category>
        
      </item>
    
      <item>
        <title>A Fit Nerd&apos;s new home</title>
        <description>&lt;p&gt;A few days away from 6 years ago I started this blog and hosted it on
&lt;a href=&quot;https://wordpress.org/&quot;&gt;wordpress&lt;/a&gt;. Wordpress has an awesome community, so many
plugins and is (relatively) easy to setup. But, I’ve always found it a bit
heavyweight for a blog.&lt;/p&gt;

&lt;p&gt;As a developer and contributor to many open source projects, I live and breath
markdown. So, it seems like a more natural fit to use &lt;a href=&quot;&quot;&gt;Jekyll&lt;/a&gt; for the blog
tech and to just host it on Github pages. It’s free and thanks to the
&lt;a href=&quot;https://github.com/joshgerdes/jekyll-uno&quot;&gt;Jekyll-uno&lt;/a&gt; plugin, looks great on
desktop and mobile.&lt;/p&gt;

&lt;p&gt;I am going to port over my posts from over the years, but it will take a little
while. Since I’ve been a fair weather blogger (at best), it won’t take too long.&lt;/p&gt;

&lt;p&gt;You can see the old posts
&lt;a href=&quot;http://old.afitnerd.com&quot; target=&quot;_blank&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In the meantime, I ported over one of my personal favs from back in the day:
my weekend project of restoring a &lt;a href=&quot;/2011/10/16/weekend-project-fix-dark-tower/&quot;&gt;Dark Tower&lt;/a&gt; game to its original glory.&lt;/p&gt;
</description>
        <pubDate>Sun, 30 Apr 2017 20:52:41 +0000</pubDate>
        <link>https://afitnerd.com/2017/04/30/new-home/</link>
        <guid isPermaLink="true">https://afitnerd.com/2017/04/30/new-home/</guid>
        
        <category>info</category>
        
        
        <category>info</category>
        
      </item>
    
      <item>
        <title>Killing Distractions, Mac Automator Style</title>
        <description>&lt;p&gt;It has been a long long while since I’ve posted anything here. Life just kind of got in the way. Lame. Frankly, I’ve gotten a bit away from the “fit” part of afitnerd.com. Here’s what I am excited about: I recently took a position as the Java Developer Evangelist for &lt;a href=&quot;https://www.stormpath.com&quot; target=&quot;_blank&quot;&gt;Stormpath&lt;/a&gt;. This is such a wonderful mix of two of my favorite things - programming and writing - I sometimes can’t believe my good fortune! Shameless plug alert: I really love Stormpath and what we are up to. Check us out.  And, I’ve begun putting the “fit” back in. I’m beginning to run again, starting with 3 to 5 sessions per week on the elliptical.&lt;/p&gt;

&lt;p&gt;I am very easily distracted. I wanted to set myself up for success in my new position. When I use the &lt;a href=&quot;http://pomodorotechnique.com/&quot; target=&quot;_blank&quot;&gt;pomodoro technique&lt;/a&gt;, I’m in a much better place.&lt;/p&gt;

&lt;p&gt;Recently, I started using a combination of Google Chrome “people” (personas) and killing apps to try to keep distractions away while working in a pomodoro. I created a persona called “Distractions”. Here’s what the process of getting rid of distractions looks like for me:&lt;/p&gt;

&lt;ol&gt;
	&lt;li&gt;Close the &quot;Distractions&quot; Google Chrome window&lt;/li&gt;
	&lt;li&gt;Quit a number of apps that are distracting when I am trying to work&lt;/li&gt;
        &lt;ul&gt;
       	        &lt;li&gt;&lt;a href=&quot;http://youneedabudget.com&quot; target=&quot;_blank&quot;&gt;YNAB&lt;/a&gt; - awesome budgeting software.&lt;/li&gt;
                &lt;li&gt;&lt;a href=&quot;https://calendar.sunrise.am/&quot; target=&quot;_blank&quot;&gt;Sunrise Calendar&lt;/a&gt; - awesome calendar app.&lt;/li&gt;
                &lt;li&gt;&lt;a href=&quot;https://www.omnigroup.com/omnifocus&quot; target=&quot;_blank&quot;&gt;Omnifocus&lt;/a&gt; - essential project and task management.&lt;/li&gt;
                &lt;li&gt;&lt;a href=&quot;https://telegram.org/&quot; target=&quot;_blank&quot;&gt;Telegram&lt;/a&gt; - great individual and group messaging app.&lt;/li&gt;
                &lt;li&gt;&lt;a href=&quot;https://www.apple.com/support/mac-apps/messages/&quot; target=&quot;_blank&quot;&gt;Messages&lt;/a&gt; - Apple messaging app that allows me to send SMS messages from the desktop.&lt;/li&gt;
                &lt;li&gt;&lt;a href=&quot;https://www.hipchat.com/&quot; target=&quot;_blank&quot;&gt;Hipchat&lt;/a&gt; - awesome messaging app for work. Annoyingly, disregards &quot;Do Not Disturb&quot; setting on Mac.&lt;/li&gt;
         &lt;/ul&gt;
	&lt;li&gt;Turn on Do Not Disturb mode&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;During the pomodoro break, I reverse the process.&lt;/p&gt;

&lt;p&gt;On top of that, I like to have certain apps on certain &lt;a href=&quot;https://support.apple.com/kb/PH18757?locale=en_US&quot; target=&quot;_blank&quot;&gt;spaces&lt;/a&gt; on my two-display setup. This meant CMD+Tab to the app or jump to the space, and then start it up (or kill it, depending on where I am at). With only 5 minutes in a pomodoro break, all this switching around and launching can add up.&lt;/p&gt;

&lt;p&gt;This post is all about how I automated this process using an application that’s been in OS X for a very long time: &lt;a href=&quot;https://support.apple.com/en-ph/HT2488&quot; target=&quot;_blank&quot;&gt;Automator&lt;/a&gt;. Atomator’s been built-in since the &lt;a href=&quot;https://en.wikipedia.org/wiki/Mac_OS_X_Tiger&quot; target=&quot;_blank&quot;&gt;Tiger&lt;/a&gt; release in 2005! And, I’ve completely neglected it even though I’ve been a full-time OS X user since 2008. Full disclosure here: I am an Automator and Applescript n00b. The Applescript examples that I am going to share with you work on my setup running Yosemite. YMMV!&lt;/p&gt;

&lt;p&gt;There were a couple of challenges to get this working the way I wanted. First, I had to be able to launch certain apps on certain spaces. I also needed a way to open and close certain Google Chrome personas without exiting Chrome altogether.&lt;/p&gt;

&lt;p&gt;Let’s take a look at the close distractions script first:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;on run &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;input, parameters&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

    tell application &lt;span class=&quot;s2&quot;&gt;&quot;Messages&quot;&lt;/span&gt;
    	quit
    end tell

    tell application &lt;span class=&quot;s2&quot;&gt;&quot;HipChat&quot;&lt;/span&gt;
    	quit
    end tell

    tell application &lt;span class=&quot;s2&quot;&gt;&quot;Telegram&quot;&lt;/span&gt;
    	quit
    end

    tell application &lt;span class=&quot;s2&quot;&gt;&quot;YNAB&quot;&lt;/span&gt;
    	quit
    end

    tell application &lt;span class=&quot;s2&quot;&gt;&quot;Sunrise&quot;&lt;/span&gt;
    	quit
    end

    tell application &lt;span class=&quot;s2&quot;&gt;&quot;Omnifocus&quot;&lt;/span&gt;
    	quit
    end

    tell application &lt;span class=&quot;s2&quot;&gt;&quot;Google Chrome&quot;&lt;/span&gt;
    	activate
    	tell application &lt;span class=&quot;s2&quot;&gt;&quot;System Events&quot;&lt;/span&gt;
    		tell process &lt;span class=&quot;s2&quot;&gt;&quot;Google Chrome&quot;&lt;/span&gt;
    			click menu item &lt;span class=&quot;s2&quot;&gt;&quot;Distractions&quot;&lt;/span&gt; of menu 1 of menu bar item &lt;span class=&quot;s2&quot;&gt;&quot;People&quot;&lt;/span&gt; of menu bar 1
    			click menu item &lt;span class=&quot;s2&quot;&gt;&quot;Close Window&quot;&lt;/span&gt; of menu 1 of menu bar item &lt;span class=&quot;s2&quot;&gt;&quot;File&quot;&lt;/span&gt; of menu bar 1
    		end tell
    	end tell
    end tell

	&lt;span class=&quot;k&quot;&gt;return &lt;/span&gt;input
end run
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Lines 3 - 25 are pretty self explanatory. For each named application, tell that application to quit. There were a couple of applications that Automator didn’t know about even though they were running. I probably just didn’t have the name exactly right. Automator gave me a picker to select the app and once I did that, the proper app would quit when I ran the script. Some secret sauce going on behind the scenes there.&lt;/p&gt;

&lt;p&gt;Lines 27 - 35 are where things get interesting. In this case, I am activating the Google Chrome application and then interacting with the menu to accomplish the goal of closing the “Distractions” persona.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/images/2015/08/close_distractions.gif&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;/images/2015/08/close_distractions.gif&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After testing this script in the Applescript editor, I was ready to create an Automator script and bind a key sequence to it. In the Automator app, I chose to create a new workflow of the “Service” type. I then dragged over the “Run Applescript” item from the library on the left. In addition to pasting in the above script, I selected some key settings that make the keyboard shortcut work the way I want it to.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/images/2015/08/close_distractions.png&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;/images/2015/08/close_distractions.png&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Toward the top of the pane, there are two dropdowns. This service automation does not receive any input AND I want it to be available in “any application”.&lt;/p&gt;

&lt;p&gt;I always wondered what the universal menu item “Services” that I see on all mac apps was for. Now I know - a little at least. Once I saved the automation, the workflow I had created showed up in the services menu of (nearly) any app I run.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/images/2015/08/services.png&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;/images/2015/08/services.png&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Having these workflows available in any application is critical to being able to being able to trigger them by a key sequence. In the Keyboard System Preference pane, there’s a Shortcuts tab. On that tab, you can bind key sequences to variety of types, including Services.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/images/2015/08/keyboard_shortcuts.png&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;/images/2015/08/keyboard_shortcuts.png&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I bound the “close_distractions” workflow to the key sequence:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;CTRL+ALT+CMD+,&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;I bound the “open_distractions” workflow to the key sequence:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;CTRL+ALT+CMD+.&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Let’s take a look at the “open distractions” workflow code. Again, some of this is very specific to my setup. YMMV.&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;on run &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;input, parameters&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;do &lt;/span&gt;shell script &lt;span class=&quot;s2&quot;&gt;&quot;/Applications/Mission&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\\&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; Control.app/Contents/MacOS/Mission&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\\&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; Control&quot;&lt;/span&gt;
    delay 0.5
    tell application &lt;span class=&quot;s2&quot;&gt;&quot;System Events&quot;&lt;/span&gt; to click &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;first button whose value of attribute &lt;span class=&quot;s2&quot;&gt;&quot;AXDescription&quot;&lt;/span&gt; is &lt;span class=&quot;s2&quot;&gt;&quot;exit to Desktop 4&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; of list 2 of group 1 of process &lt;span class=&quot;s2&quot;&gt;&quot;Dock&quot;&lt;/span&gt;
    delay 0.5

    tell application &lt;span class=&quot;s2&quot;&gt;&quot;Google Chrome&quot;&lt;/span&gt;
    	activate
    	tell application &lt;span class=&quot;s2&quot;&gt;&quot;System Events&quot;&lt;/span&gt;
    		tell process &lt;span class=&quot;s2&quot;&gt;&quot;Google Chrome&quot;&lt;/span&gt;
    			click menu item &lt;span class=&quot;s2&quot;&gt;&quot;Distractions&quot;&lt;/span&gt; of menu 1 of menu bar item &lt;span class=&quot;s2&quot;&gt;&quot;People&quot;&lt;/span&gt; of menu bar 1
    		end tell
    	end tell
    end tell

    &lt;span class=&quot;k&quot;&gt;do &lt;/span&gt;shell script &lt;span class=&quot;s2&quot;&gt;&quot;/Applications/Mission&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\\&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; Control.app/Contents/MacOS/Mission&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\\&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; Control&quot;&lt;/span&gt;
    delay 0.5
    tell application &lt;span class=&quot;s2&quot;&gt;&quot;System Events&quot;&lt;/span&gt; to click &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;first button whose value of attribute &lt;span class=&quot;s2&quot;&gt;&quot;AXDescription&quot;&lt;/span&gt; is &lt;span class=&quot;s2&quot;&gt;&quot;exit to Desktop 5&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; of list 2 of group 1 of process &lt;span class=&quot;s2&quot;&gt;&quot;Dock&quot;&lt;/span&gt;
    delay 0.5

    tell application &lt;span class=&quot;s2&quot;&gt;&quot;Messages&quot;&lt;/span&gt;
    	activate
    end tell

    tell application &lt;span class=&quot;s2&quot;&gt;&quot;HipChat&quot;&lt;/span&gt;
    	activate
    end tell

    tell application &lt;span class=&quot;s2&quot;&gt;&quot;Telegram&quot;&lt;/span&gt;
    	activate
    end tell


    &lt;span class=&quot;k&quot;&gt;do &lt;/span&gt;shell script &lt;span class=&quot;s2&quot;&gt;&quot;/Applications/Mission&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\\&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; Control.app/Contents/MacOS/Mission&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\\&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; Control&quot;&lt;/span&gt;
    delay 0.5
    tell application &lt;span class=&quot;s2&quot;&gt;&quot;System Events&quot;&lt;/span&gt; to click &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;first button whose value of attribute &lt;span class=&quot;s2&quot;&gt;&quot;AXDescription&quot;&lt;/span&gt; is &lt;span class=&quot;s2&quot;&gt;&quot;exit to Desktop 1&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; of list 1 of group 1 of process &lt;span class=&quot;s2&quot;&gt;&quot;Dock&quot;&lt;/span&gt;
    delay 0.5

    tell application &lt;span class=&quot;s2&quot;&gt;&quot;Omnifocus&quot;&lt;/span&gt;
    	activate
    end tell

    tell application &lt;span class=&quot;s2&quot;&gt;&quot;YNAB&quot;&lt;/span&gt;
    	activate
    end tell

    tell application &lt;span class=&quot;s2&quot;&gt;&quot;Sunrise&quot;&lt;/span&gt;
    	activate
    end tell

	&lt;span class=&quot;k&quot;&gt;return &lt;/span&gt;input
end run
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Before I break down this script, let me tell you a little bit about my setup. I have dual 27” HDMI displays. The first display has 3 spaces and the second display has 2 spaces. On Desktop 1, I keep all my “productivity” apps. (The quotes are because I am very easily distracted by these apps if they are always running). These include apps like YNAB, Sunrise Calendar, and Omnifocus. Desktop 2 is my workspace. I usually have &lt;a href=&quot;https://www.iterm2.com/&quot; target=&quot;_blank&quot;&gt;iTerm2&lt;/a&gt; and &lt;a href=&quot;https://www.jetbrains.com/idea/&quot; target=&quot;_blank&quot;&gt;IntelliJ&lt;/a&gt; running on this desktop. Desktop 3 is a scratch space for apps I fire up and tear down quickly. Desktop 4 is for my browsers. And Desktop 5 is for my messaging apps, like Messages, Telegram and Hipchat.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/images/2015/08/desktop_1.png&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;/images/2015/08/desktop_1.png&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/images/2015/08/desktop_2.png&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;/images/2015/08/desktop_2.png&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Part of what I wanted to accomplish in this automation was having the apps launched on the correct space. Lines 3 - 6 in the code block above accomplish this. Line 3 gets us into Mission Control. Line 5 clicks on the appropriate Desktop. In this case, Desktop 4.&lt;/p&gt;

&lt;p&gt;Once I am on Desktop 4, I can launch my Distractions Google Chrome persona, which is what lines 8 - 15 does.&lt;/p&gt;

&lt;p&gt;Next, I switch to Desktop 5 and launch my messaging apps.&lt;/p&gt;

&lt;p&gt;And, finally, I switch to Desktop 1 and launch my productivity apps.&lt;/p&gt;

&lt;p&gt;I also bound the key sequence:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;CTRL+ALT+CMD+]&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;to toggle Do Not Disturb mode.&lt;/p&gt;

&lt;p&gt;Now, just before I start a pomodoro, I hit:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;CTRL+ALT+CMD+,
CTRL+ALT+CMD+]&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;And, when I am in the pomodoro break, I hit:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;CTRL+ALT+CMD+.
CTRL+ALT+CMD+]&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;During my testing of these scripts, I had to add certain applications to a list of applications allowed to interact with the Accessibility features of OS X. I probably should have kept better track, but it included Automator and Google Chrome.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/images/2015/08/accessibility.png&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;/images/2015/08/accessibility.png&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I started looking into toggling Do Not Disturb mode as part of the key sequence for open and close distractions, but I bailed on that. I also started to look into hooking into &lt;a href=&quot;http://www.irradiatedsoftware.com/sizeup/&quot; target=&quot;_blank&quot;&gt;SizeUp&lt;/a&gt; so that I could lay out the messaging windows the way I like as part of the open distractions script, but I bailed on that too. Getting to the point I am at - which is very functional - took about an hour of Googling and experimentation.&lt;/p&gt;

&lt;p&gt;Any feedback on how to make it more awesome or to correct any bone-headed things I am doing is much appreciated.&lt;/p&gt;
</description>
        <pubDate>Mon, 10 Aug 2015 20:52:41 +0000</pubDate>
        <link>https://afitnerd.com/2015/08/10/killing-distractions-mac-automator-style/</link>
        <guid isPermaLink="true">https://afitnerd.com/2015/08/10/killing-distractions-mac-automator-style/</guid>
        
        <category>tech</category>
        
        <category>productivity</category>
        
        
        <category>tech</category>
        
        <category>productivity</category>
        
      </item>
    
      <item>
        <title>Pomodoro &amp; HipChat: Automate Availability</title>
        <description>&lt;p&gt;The &lt;a href=&quot;http://www.pomodorotechnique.com/&quot; target=&quot;_blank&quot;&gt;Pomodoro Technique&lt;/a&gt; is a way to enhance focus while working. There is a high cost in brainpower to stop doing what you are focused on, look at the tweet that just came in, and then try to start up where you left off. The idea of working in “pomodoros” is that you work uninterrupted for 25 minutes. Then you get a 5 minute break. After you do 4 of those cycles, you get a 10 minute break. Speaking from my own experience, I get a lot more done in 4 hours of pomodoros than 8 hours of work disrupted by various notifications.&lt;/p&gt;

&lt;p&gt;At &lt;a href=&quot;https://www.workmarket.com&quot; target=&quot;_blank&quot;&gt;Work Market&lt;/a&gt;, we rely on &lt;a href=&quot;https://hipchat.com&quot; target=&quot;_blank&quot;&gt;HipChat&lt;/a&gt; to communicate with each other throughout the day. This is a very handy way to connect our two offices and the various remote workers that we may have at any given time. However, when I am in a pomodoro, I don’t want to be disturbed by HipChat.&lt;/p&gt;

&lt;p&gt;Following the excellent post &lt;a href=&quot;http://ertw.com/blog/2012/05/02/controlling-hipchat-status-through-applescript/&quot; target=&quot;_blank&quot;&gt;here&lt;/a&gt;, I am able to automatically set my status to “Do Not Disturb” when I start a pomodoro and back to “Available” when the pomodoro is done.&lt;/p&gt;

&lt;p&gt;I’ve taken it one step further by sending a notification to the main room we all use in HipChat to let my coworkers know what I am up to. This is a little more communicative than just having my status abruptly change with no other context.&lt;/p&gt;

&lt;p&gt;The key is making use of the HipChat &lt;a href=&quot;https://www.hipchat.com/docs/api&quot; target=&quot;_blank&quot;&gt;API&lt;/a&gt; in conjunction with AppleScript.&lt;/p&gt;

&lt;p&gt;Firstly, I created a shell script named HipChat.Pomodoro.sh that takes a message as an argument and sends it to the specified HipChat room using the API:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
3
4
5
6
7
8
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;c&quot;&gt;#! /bin/bash&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;ROOM_ID&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=[&lt;/span&gt;HipChat Room ID]
&lt;span class=&quot;nv&quot;&gt;AUTH_TOKEN&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=[&lt;/span&gt;HipChat Auth Token]
&lt;span class=&quot;nv&quot;&gt;MESSAGE&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$1&lt;/span&gt;
curl &lt;span class=&quot;nt&quot;&gt;-s&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;room_id=&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$ROOM_ID&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;amp;from=Pomodoro&amp;amp;notify=1&amp;amp;color=yellow&amp;amp;message_format=text&quot;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--data-urlencode&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;message=(pomodoro) &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$MESSAGE&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  https://api.hipchat.com/v1/rooms/message?auth_token&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$AUTH_TOKEN&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; /dev/null
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;You’ll need to put in your own room id and auth token from HipChat.&lt;/p&gt;

&lt;p&gt;You can test this right from the command line:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;./HipChat.Pomodoro.sh &lt;span class=&quot;s2&quot;&gt;&quot;Test Message&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Next, I updated the settings in the &lt;a href=&quot;https://github.com/ugol/pomodoro&quot; target=&quot;_blank&quot;&gt;Pomodoro&lt;/a&gt; app preferences to make use of the script:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/images/2013/08/Screen-Shot-2013-08-19-at-1.57.16-AM.png&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;/images/2013/08/Screen-Shot-2013-08-19-at-1.57.16-AM-300x282.png&quot; alt=&quot;&quot; title=&quot;Screen Shot 2013-08-19 at 1.57.16 AM&quot; width=&quot;300&quot; height=&quot;282&quot; class=&quot;aligncenter size-medium wp-image-949&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I added the call to my script after the AppleScript from Sean’s original post. I have the Start, Interrupt Over, Reset and End events handled&lt;/p&gt;

&lt;p&gt;Start:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
3
4
5
6
7
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;tell application &lt;span class=&quot;s2&quot;&gt;&quot;System Events&quot;&lt;/span&gt; to tell UI element &lt;span class=&quot;s2&quot;&gt;&quot;HipChat&quot;&lt;/span&gt; of list 1 of process &lt;span class=&quot;s2&quot;&gt;&quot;Dock&quot;&lt;/span&gt;
  perform action &lt;span class=&quot;s2&quot;&gt;&quot;AXShowMenu&quot;&lt;/span&gt;
  delay 0.5
  click menu item &lt;span class=&quot;s2&quot;&gt;&quot;Status&quot;&lt;/span&gt; of menu 1
  click menu item &lt;span class=&quot;s2&quot;&gt;&quot;Do Not Disturb&quot;&lt;/span&gt; of menu 1 of menu item &lt;span class=&quot;s2&quot;&gt;&quot;Status&quot;&lt;/span&gt; of menu 1
end tell
&lt;span class=&quot;k&quot;&gt;do &lt;/span&gt;shell script &lt;span class=&quot;s2&quot;&gt;&quot;/Users/dogeared/Hipchat.Pomodoro.sh &apos;Micah is starting a Pomodoro. He will be back in 25 minutes.&apos;&quot;&lt;/span&gt;
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Interrupt Over:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
3
4
5
6
7
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;tell application &lt;span class=&quot;s2&quot;&gt;&quot;System Events&quot;&lt;/span&gt; to tell UI element &lt;span class=&quot;s2&quot;&gt;&quot;HipChat&quot;&lt;/span&gt; of list 1 of process &lt;span class=&quot;s2&quot;&gt;&quot;Dock&quot;&lt;/span&gt;
  perform action &lt;span class=&quot;s2&quot;&gt;&quot;AXShowMenu&quot;&lt;/span&gt;
  delay 0.5
  click menu item &lt;span class=&quot;s2&quot;&gt;&quot;Status&quot;&lt;/span&gt; of menu 1
  click menu item &lt;span class=&quot;s2&quot;&gt;&quot;Available&quot;&lt;/span&gt; of menu 1 of menu item &lt;span class=&quot;s2&quot;&gt;&quot;Status&quot;&lt;/span&gt; of menu 1
end tell
&lt;span class=&quot;k&quot;&gt;do &lt;/span&gt;shell script &lt;span class=&quot;s2&quot;&gt;&quot;/Users/dogeared/HipChat.Pomodoro.sh &apos;Too many distractions. (pomodoro) fail for Micah!&apos;&quot;&lt;/span&gt;
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Reset:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
3
4
5
6
7
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;tell application &lt;span class=&quot;s2&quot;&gt;&quot;System Events&quot;&lt;/span&gt; to tell UI element &lt;span class=&quot;s2&quot;&gt;&quot;HipChat&quot;&lt;/span&gt; of list 1 of process &lt;span class=&quot;s2&quot;&gt;&quot;Dock&quot;&lt;/span&gt;
  perform action &lt;span class=&quot;s2&quot;&gt;&quot;AXShowMenu&quot;&lt;/span&gt;
  delay 0.5
  click menu item &lt;span class=&quot;s2&quot;&gt;&quot;Status&quot;&lt;/span&gt; of menu 1
  click menu item &lt;span class=&quot;s2&quot;&gt;&quot;Available&quot;&lt;/span&gt; of menu 1 of menu item &lt;span class=&quot;s2&quot;&gt;&quot;Status&quot;&lt;/span&gt; of menu 1
end tell
&lt;span class=&quot;k&quot;&gt;do &lt;/span&gt;shell script &lt;span class=&quot;s2&quot;&gt;&quot;/Users/dogeared/HipChat.Pomodoro.sh &apos;Micah bailed! (timeforthat)&apos;&quot;&lt;/span&gt;
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;End:&lt;/p&gt;

&lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-bash&quot; data-lang=&quot;bash&quot;&gt;&lt;table class=&quot;rouge-table&quot;&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td class=&quot;gutter gl&quot;&gt;&lt;pre class=&quot;lineno&quot;&gt;1
2
3
4
5
6
7
&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;tell application &lt;span class=&quot;s2&quot;&gt;&quot;System Events&quot;&lt;/span&gt; to tell UI element &lt;span class=&quot;s2&quot;&gt;&quot;HipChat&quot;&lt;/span&gt; of list 1 of process &lt;span class=&quot;s2&quot;&gt;&quot;Dock&quot;&lt;/span&gt;
  perform action &lt;span class=&quot;s2&quot;&gt;&quot;AXShowMenu&quot;&lt;/span&gt;
  delay 0.5
  click menu item &lt;span class=&quot;s2&quot;&gt;&quot;Status&quot;&lt;/span&gt; of menu 1
  click menu item &lt;span class=&quot;s2&quot;&gt;&quot;Available&quot;&lt;/span&gt; of menu 1 of menu item &lt;span class=&quot;s2&quot;&gt;&quot;Status&quot;&lt;/span&gt; of menu 1
end tell
&lt;span class=&quot;k&quot;&gt;do &lt;/span&gt;shell script &lt;span class=&quot;s2&quot;&gt;&quot;/Users/dogeared/HipChat.Pomodoro.sh &apos;And... Micah is back! (thumbsup)&apos;&quot;&lt;/span&gt;
&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;

&lt;p&gt;Note: The Pomodoro app’s script tab in preferences is finicky. The standard copy-and-paste shortcut keys do not work, but right-clicking and choosing copy or paste does work.&lt;/p&gt;

&lt;p&gt;You will need to enable access for assistive devices in System Preferences -&amp;gt; Accessibility in order for the Pomodoro automation to work.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/images/2013/08/Screen-Shot-2013-08-19-at-9.21.39-AM.png&quot;&gt;&lt;img src=&quot;/images/2013/08/Screen-Shot-2013-08-19-at-9.21.39-AM.png&quot; alt=&quot;&quot; title=&quot;Screen Shot 2013-08-19 at 9.21.39 AM&quot; width=&quot;668&quot; height=&quot;476&quot; class=&quot;aligncenter size-full wp-image-978&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here are some screenshots from HipChat. In the first one, I started a pomodoro and reset in the middle of it. The second one shows a complete pomodoro.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/images/2013/08/Screen-Shot-2013-08-19-at-2.10.27-AM1.png&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;/images/2013/08/Screen-Shot-2013-08-19-at-2.10.27-AM1-1024x52.png&quot; alt=&quot;&quot; title=&quot;Screen Shot 2013-08-19 at 2.10.27 AM&quot; width=&quot;1024&quot; height=&quot;52&quot; class=&quot;aligncenter size-large wp-image-969&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/images/2013/08/Screen-Shot-2013-08-19-at-2.10.42-AM1.png&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;/images/2013/08/Screen-Shot-2013-08-19-at-2.10.42-AM1-1024x48.png&quot; alt=&quot;&quot; title=&quot;Screen Shot 2013-08-19 at 2.10.42 AM&quot; width=&quot;1024&quot; height=&quot;48&quot; class=&quot;aligncenter size-large wp-image-970&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
</description>
        <pubDate>Mon, 19 Aug 2013 06:33:41 +0000</pubDate>
        <link>https://afitnerd.com/2013/08/19/pomodoro-hipchat-automate-availability/</link>
        <guid isPermaLink="true">https://afitnerd.com/2013/08/19/pomodoro-hipchat-automate-availability/</guid>
        
        <category>tech</category>
        
        <category>productivity</category>
        
        
        <category>tech</category>
        
        <category>productivity</category>
        
      </item>
    
      <item>
        <title>Weekend Project: Fix Dark Tower</title>
        <description>&lt;p&gt;This post is about replacing the keypad on the game Dark Tower. If you&apos;re not interested in my nostalgic blathering, click &lt;a href=&quot;#howto&quot;&gt;here&lt;/a&gt; to get right to the instructions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nostalgic Blathering&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For those of you not familiar (because you&apos;re too young or live under a rock), &lt;a href=&quot;http://en.wikipedia.org/wiki/Dark_Tower_(game)&quot; target=&quot;_blank&quot;&gt;Dark Tower&lt;/a&gt; was one of the first interactive electronic board games that was massed produced. It was released in 1981 and I got one for my 13th birthday. It blew my mind - not only because it was fun to play - but because of the possibilities that electronics could bring to gaming. I had been hacking around on &lt;a href=&quot;http://en.wikipedia.org/wiki/Commodore_PET&quot; target=&quot;_blank&quot;&gt;Pet&lt;/a&gt; computers for about 2 years by that point. While there were some character based green-screen games for the Pet, Dark Tower was the first mixed media game I had ever played. You set up a physical &lt;a href=&quot;http://www.google.com/search?q=dark+tower+game&amp;amp;hl=en&amp;amp;prmd=imvns&amp;amp;source=lnms&amp;amp;tbm=isch&amp;amp;ei=EAubTsGuE-rv0gHl3PnpBA&amp;amp;sa=X&amp;amp;oi=mode_link&amp;amp;ct=mode&amp;amp;cd=2&amp;amp;ved=0CD4Q_AUoAQ&amp;amp;biw=1400&amp;amp;bih=737#hl=en&amp;amp;tbm=isch&amp;amp;sa=1&amp;amp;q=dark+tower+game+board&amp;amp;pbx=1&amp;amp;oq=dark+tower+game+board&amp;amp;aq=f&amp;amp;aqi=&amp;amp;aql=1&amp;amp;gs_sm=e&amp;amp;gs_upl=56646l57429l0l57772l6l6l0l3l0l1l171l484l0.3l3l0&amp;amp;bav=on.2,or.r_gc.r_pw.,cf.osb&amp;amp;fp=638d9931109a0b28&amp;amp;biw=1400&amp;amp;bih=737&quot; target=&quot;_blank&quot;&gt;map board&lt;/a&gt;, complete with plastic buildings, flags and character pieces. You monitored your progress through the game with a cardboard status indicator which used re-purposed &lt;a href=&quot;http://en.wikipedia.org/wiki/Battleship_(game)&quot; target=&quot;_blank&quot;&gt;battleship&lt;/a&gt; red peg markers. On the peg board, you kept track of how many warriors, gold, food, keys and other items you had during game play. At the center of it all was the Dark Tower. It sported a 12 button keypad and a display that tells you what had happened on each turn of the game. Display is a generous word in this context as it really was just a vertical row of three lights behind a plastic drum with pictures. The drum turned to the correct position and the correct light illuminated behind the relevant picture for what just occurred in the game. The game came in a &lt;a href=&quot;http://www.google.com/search?q=dark+tower+game&amp;amp;hl=en&amp;amp;prmd=imvns&amp;amp;source=lnms&amp;amp;tbm=isch&amp;amp;ei=EAubTsGuE-rv0gHl3PnpBA&amp;amp;sa=X&amp;amp;oi=mode_link&amp;amp;ct=mode&amp;amp;cd=2&amp;amp;ved=0CD4Q_AUoAQ&amp;amp;biw=1400&amp;amp;bih=737#hl=en&amp;amp;tbm=isch&amp;amp;sa=1&amp;amp;q=dark+tower+game+box&amp;amp;oq=dark+tower+game+box&amp;amp;aq=f&amp;amp;aqi=&amp;amp;aql=&amp;amp;gs_sm=e&amp;amp;gs_upl=9629l10581l0l10699l4l3l0l0l0l2l213l394l0.1.1l3l0&amp;amp;bav=on.2,or.r_gc.r_pw.,cf.osb&amp;amp;fp=638d9931109a0b28&amp;amp;biw=1400&amp;amp;bih=737&quot; target=&quot;_blank&quot;&gt;huge cardboard box&lt;/a&gt; with artwork on it that fit right in with the &lt;a href=&quot;http://en.wikipedia.org/wiki/Dungeons_%26_Dragons&quot; target=&quot;_blank&quot;&gt;Dungeons &amp;amp; Dragons&lt;/a&gt; mindset of the early &apos;80s.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Keypad Problem&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Complete Dark Tower game sets in good working condition go for as much as $400 on Ebay these days (average price is probably $100). Back In 2003, my brother bought me a complete working Dark Tower set from an antique shop in New York City for my 35th Birthday. My brother, sister and I (as well as others) played the hell out of the game for 2 years before the keypad crapped out. The original keypad was constructed of 3 thin plastic sheets (see pictures &lt;a href=&quot;#keypads&quot;&gt;below&lt;/a&gt;). Two of the sheets have metal contacts and are separated from each other by a blank plastic sheet with holes in it. When you press a &quot;key&quot;, the two metal contacts touch through the plastic sheet in the metal. Although pretty tough, it is this simple construction that tends to wear out first on these games.&lt;/p&gt;
&lt;p&gt;When my siblings and I got together, we always had a hankering to play Dark Tower. So, we decided to try to fix the keypad. With a little copper tape and one solder joint, we were able to get the keypad working again. This fix lasted us another eight years - pretty good for our MacGyver-like fix. At a recent company game night, the keypad crapped out again in the middle of a game. This was after I had introduced the game to a bunch of twenty-somethings who had never heard of it before and who were enjoying it quite a lot. I thought, &quot;This shall not stand, man!&quot; That thought was quickly replaced with a &quot;WTF&quot; as I looked online and found Dark Tower repairs costing $70 - $100. Thankfully, I have been given the gift (and curse) of being a compulsive tinkerer and DIYer.&lt;/p&gt;
&lt;p&gt;This weekend with the help of Adafruit.com&apos;s &lt;a href=&quot;https://www.adafruit.com/products/419&quot; target=&quot;_blank&quot;&gt;keypad&lt;/a&gt; (2 of them - 1 had to be sacrificed so you wouldn&apos;t have to), some scribbled notes, some nail-biting-trimming of said keypad with a scissor and a wire wrapping tool, my brother and I completely replaced the keypad and have a fully functional Dark Tower again. As an added bonus, the Adafruit keypad has a satisfying click when pressed - a feature missing from the 1980s pure membrane keypad. While I had hoped I would be able to simply plug in this new keypad, the universe laughed. If you want to skip the background on how these keypads work and just get to the steps, click &lt;a href=&quot;#howto&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How Membrane Keypads work&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For a 12-button keypad, you only need seven pins to determine which key was pressed (3 columns and 4 rows). Each key press combines two of the seven pins when contact is made. When I first hooked up the new keypad, I quickly realized that the pin mappings were very different as almost none of the keys worked. This is the reason that I bought two of the Adafruit keypads. I ripped one of them apart so that I could see exactly how each of the keys connected to the pins. I was able to do the same for the original keypad, which was pretty much toast anyway. I&apos;m going to refer to the original keypad by number, just like the new keypad has and just like a standard telephone dialing keypad has. &lt;/p&gt;
&lt;pre&gt;
Key: (x,y) &amp;lt;========= pins
       z   &amp;lt;========= number on keypad

          Old Keypad                        New Keypad
=============================     =============================
| (5,2)     (4,2)     (3,2) |     | (3,7)     (2,7)     (1,7) |
|   1         2         3   |     |   1         2         3   |
|                           |     |                           |
| (5,1)     (4,1)     (3,1) |     | (3,6)     (2,6)     (1,6) |
|   4         5         6   |     |   4         5         6   |
|                           |     |                           |
| (5,6)     (4,6)     (3,6) |     | (3,5)     (2,5)     (1,5) |
|   7         8         9   |     |   7         8         9   |
|                           |     |                           |
| (5,7)     (4,7)     (3,7) |     | (3,4)     (2,4)     (1,4) |
|   *         0         #   |     |   *         0         #   |
=============================     =============================
&lt;/pre&gt;
&lt;p&gt;&lt;a name=&quot;howto&quot;&gt;&lt;strong&gt;Cable Pin Mappings&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;With the two pin maps above, I could now create a cable to properly connect the new keypad to the internal connector of the Dark Tower. Here is the pin map:&lt;/p&gt;
&lt;pre&gt;
 DT Connector     New Keypad
   1 =============== 6
   2 =============== 7
   3 =============== 1
   4 =============== 2
   5 =============== 3
   6 =============== 5
   7 =============== 4
&lt;/pre&gt;

&lt;p&gt;NOTE: I arbitrarily chose “pin 1” to be the top of the internal connector with the Dark Tower laying on its side as pictured &lt;a href=&quot;#dtside&quot;&gt;below&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a name=&quot;keypads&quot;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Here are pictures of the two keypads, closed, open, and with pin explanations:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/images/2011/10/OldAndNewKeyboardsClosed.jpg&quot;&gt;&lt;img src=&quot;/images/2011/10/OldAndNewKeyboardsClosed.jpg&quot; alt=&quot;&quot; title=&quot;OldAndNewKeyboardsClosed&quot; width=&quot;409&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/images/2011/10/OldAndNewKeyboardsOpen.jpg&quot;&gt;&lt;img src=&quot;/images/2011/10/OldAndNewKeyboardsOpen.jpg&quot; alt=&quot;&quot; title=&quot;OldAndNewKeyboardsOpen&quot; width=&quot;436&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/images/2011/10/OldKeyboardPinoutSmall.jpg&quot;&gt;&lt;img src=&quot;/images/2011/10/OldKeyboardPinoutSmall.jpg&quot; alt=&quot;&quot; title=&quot;OldKeyboardPinoutSmall&quot; width=&quot;770&quot; height=&quot;323&quot; class=&quot;alignnone size-full wp-image-543&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/images/2011/10/NewKeyboardPinoutSmall.jpg&quot;&gt;&lt;img src=&quot;/images/2011/10/NewKeyboardPinoutSmall.jpg&quot; alt=&quot;&quot; title=&quot;NewKeyboardPinoutSmall&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test Run: prototype&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The first thing I wanted to do was test out my pin mappings. I used relatively heavy gauge wire connect the new keypad to the connector in the Dark Tower using the mappings above.&lt;/p&gt;

&lt;p&gt;&lt;a name=&quot;dtside&quot;&gt;&lt;/a&gt;
&lt;a href=&quot;/images/2011/10/DTPinMapTest.jpg&quot;&gt;&lt;img src=&quot;/images/2011/10/DTPinMapTest.jpg&quot; alt=&quot;&quot; title=&quot;DTPinMapTest&quot; width=&quot;436&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And… it worked!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make the Cable&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The new keypads each came with a 7 pin header. I thought I would simply solder wires to the headers in the appropriate order outlined above, and then plug one end into the socket in the Dark Tower and the other end into the keypad. Sadly, the universe laughed again. My soldering skills are adequate at best. What I had forgotten is that the pins on these headers are meant to break away. That way, you can have a 2-pin header or a 10-pin header - whatever you need. As I began soldering the first wire onto the first pin, the heat melted the connecting plastic in the middle and the pin just fell off from the rest of the bunch. No to be deterred, I remembered that I in my college days, I used a super handy &lt;a href=&quot;http://www.radioshack.com/product/index.jsp?productId=2103243&quot; target=&quot;_blank&quot;&gt;wire wrapping tool&lt;/a&gt; and very thin &lt;a href=&quot;http://www.radioshack.com/product/index.jsp?productId=2062642&quot; target=&quot;_blank&quot;&gt;wrapping wire&lt;/a&gt;. This tool made it very easy for the lazy and soldering inept (me, basically) to still put together complex projects involving connecting up electronic components without having to solder them. One issue I found with wire wrapping on the square pins of the header was that it is easy to break the wire if you&apos;re not careful. After a few attempts, I got the technique down, which involves wrapping a little more slowly and moving the wrapping tool out as you wrap. I built my cable, but you can see that for the 7th pin, I used a bit of heavier gauge wire since I had destroyed the original pin from the header.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/images/2011/10/KeyboardConnectorCable.jpg&quot;&gt;&lt;img src=&quot;/images/2011/10/KeyboardConnectorCable.jpg&quot; alt=&quot;&quot; title=&quot;KeyboardConnectorCable&quot; width=&quot;436&quot; class=&quot;alignnone&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test Run: for realz&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The next step was to test to the cable in the Dark Tower:&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/images/2011/10/DTNewPinTest.jpg&quot;&gt;&lt;img src=&quot;/images/2011/10/DTNewPinTest.jpg&quot; alt=&quot;&quot; title=&quot;DTNewPinTest&quot; width=&quot;436&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And… it failed. The first time it failed because I had screwed up the pin connections. The second header only having 6 pins threw me off (pictured above is the final working cable). I re-wrapped all the wires (so much easier than de-soldering!) and tried again. This time, all of the keys worked except the bottom row. The reason for this, we found, was that pin 7 was not making good contact in the connector on the Dark Tower (hey, the thing’s 30 years old - cut it some slack). Jamming a small piece of the heavier gauge wire in with the header on pin 7 got it working. I realized that I needed to ensure that the header wouldn’t fall out of the internal connector. I thought about gluing it in, but I wanted a less permanent solution. So, I jammed small pieces of the heavier gauge wire in at pin 1 and pin 3 on the internal connector. We’ll see how this holds up long term. I may need to come up with a better solution for keeping the cable plugged in internally. (After the picture below was taken, I trimmed the bits of wire so there is no exposed copper).&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/images/2011/10/InternalConnector.jpg&quot;&gt;&lt;img src=&quot;/images/2011/10/InternalConnector.jpg&quot; alt=&quot;&quot; title=&quot;InternalConnector&quot; width=&quot;436&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In order for the keypad to fit back into the dark tower, I had to trim it. I did this by measuring against the plastic bezel that it sits in and by holding the keypad up to a bright light to ensure that I would not be cutting through any of the traces. I then put electrical tape carefully around the exposed header pins on my cable. I needed to put the original keypad decal on top of the new keypad and to put the whole together with the plastic bezel. I thought about using adhesives, but since the whole thing is held in with a fitted plastic outline from the bottom, I decided to just layer everything and let the pressure hold it together. This way, if any future adjustments need to be made, I won’t have to damage anything getting it apart again.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/images/2011/10/InternalHookup.jpg&quot;&gt;&lt;img src=&quot;/images/2011/10/InternalHookup.jpg&quot; alt=&quot;&quot; title=&quot;InternalHookup&quot; width=&quot;436&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Closing Time&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It was finally time to close the patient up. The bottom of the new keypad could not be trimmed at all since all the traces connect there. By trimming the top, I was able to at least make it flush with the bottom of the Dark Tower. This resulted in a slight warping of the outer tower once assembled and there is some pressure being put on the ribbon cable, but neither of those issues are show stoppers.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/images/2011/10/DTBottom.jpg&quot;&gt;&lt;img src=&quot;/images/2011/10/DTBottom.jpg&quot; alt=&quot;&quot; title=&quot;DTBottom&quot; width=&quot;436&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once everything was closed up, we did another test where we exercised all the keys. Everything worked great. The tactile feedback of a click when a key is pressed is great. The keys are not 100% lined up with the original overlay, but it’s close enough that neither my brother, nephew or I hit the wrong key by mistake when we played. Here, you can see me spanking some Brigands.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/images/2011/10/Warriors.jpg&quot;&gt;&lt;img src=&quot;/images/2011/10/Warriors.jpg&quot; alt=&quot;&quot; title=&quot;Warriors&quot; width=&quot;436&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;/images/2011/10/Brigands.jpg&quot;&gt;&lt;img src=&quot;/images/2011/10/Brigands.jpg&quot; alt=&quot;&quot; title=&quot;Brigands&quot; width=&quot;436&quot; /&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DIY Satisfaction&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So, for $12.06 in parts from Adafruit (includes 2 keypads, tax and shipping) and $11.60 in parts from Radio Shack (the wire wrap tool and the wrapping wire - which I will use again), I was able to bring back to life a classic (and favorite) game. I’ll be bringing it back to game night in November at work and we should be able to get a full game in this time!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Parts List and tools&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.adafruit.com/products/419&quot; target=&quot;_blank&quot;&gt;Adafruit keypad&lt;/a&gt;
&lt;li&gt;&lt;a href=&quot;http://www.radioshack.com/product/index.jsp?productId=2103243&quot; target=&quot;_blank&quot;&gt;Wire wrapping tool&lt;/a&gt;
&lt;li&gt;&lt;a href=&quot;http://www.radioshack.com/product/index.jsp?productId=2062642&quot; target=&quot;_blank&quot;&gt;Wrapping wire&lt;/a&gt;
&lt;li&gt;electrical tape
&lt;li&gt;scissors
&lt;li&gt;Philips head screwdriver
&amp;lt;/ul&amp;gt;
&lt;/li&gt;&lt;/li&gt;&lt;/li&gt;&lt;/li&gt;&lt;/li&gt;&lt;/li&gt;&lt;/ul&gt;
</description>
        <pubDate>Sun, 16 Oct 2011 20:52:41 +0000</pubDate>
        <link>https://afitnerd.com/2011/10/16/weekend-project-fix-dark-tower/</link>
        <guid isPermaLink="true">https://afitnerd.com/2011/10/16/weekend-project-fix-dark-tower/</guid>
        
        <category>diy</category>
        
        
        <category>diy</category>
        
      </item>
    
  </channel>
</rss>
