Bare-knuckle web
    development
                   XP Days Ukraine
     Johannes Brodwall, Chief scientist
                     Exilesoft Global
• Bare-knuckle
             philosophy
• Demonstration of bare-
     knuckle web in Java
    • Further directions
Part I:
The bare-knuckle
     philosophy
High impact with
   low ceremony
•    Framework light
      • Test-driven
    • No calculators
Light on framework
Frameworks solve 80%
         of the job…
… and makes the rest 10
         times as hard
“Why did Hibernate
suddenly slow down?”
“How do I implement a
 custom SOAP header
      with JAX-WS?”
“How to do X with Spring”
@AutoWire + package scan
      with 100s of beans
Test-driven
No more architecture than
           what’s needed
Fast feedback cycle – also
             in the future
Don’t use a
calculator…
Part II:
Demo: Phonebook
        web app
Test driving
WebDriver browser = createWebDriver();

browser.get(url);
browser.findElement(By.linkText("Add contact")).click();

browser.findEleme(By.name("fullName")).sendKeys("Vader");
browser.findEleme(By.name("phoneNumber")).sendKeys("27");
browser.findEleme(By.name("saveContact")).click();

browser.findElement(By.linkText("Find contact")).click();
browser.findElem(By.name("nameQuery")).sendKeys("vader");
browser.findElement(By.name("nameQuery")).submit();

assertThat(browser.findElem(By.id("contacts")).getText())
    .contains("555-33274-7827");
Server server = new Server(0);
server.setHandler(
     new WebAppContext("src/main/webapp", "/contacts"));
server.start();

int port = server.getConnectors()[0].getLocalPort();
String url = "http://localhost:" + port + "/contacts";
<web-app version="2.5“>

<servlet>
  <servlet-name>contactServlet</servlet-name>
  <servlet-class>
     com.exilesoft.bareknuckleweb.ContactServlet
  </servlet-class>
</servlet>

<servlet-mapping>
  <servlet-name>contactServlet</servlet-name>
  <url-pattern>contact/*</url-pattern>
</servlet-mapping>


</web-app>
public class ContactServlet extends HttpServlet {
}
@Test
public void shouldShowAddForm() throws Exception {
    ContactServlet servlet = new ContactServlet();
    HttpServletRequest req = mock(HttpServletRequest.class);
    HttpServletResponse resp = mock(HttpServletResponse.class);
    StringWriter html = new StringWriter();

    when(resp.getWriter()).thenReturn(new PrintWriter(html));
    when(req.getPathInfo()).thenReturn("/create.html");

    servlet.doGet(req, resp);

    verify(resp).setContentType("text/html");
    assertThat(html.toString())
        .contains("<form method='post'")
        .contains("<input type='text' name='fullName'")
        .contains("<input type='text' name='phoneNumber'")
        .contains("<input type='submit' name='createContact'");
}
Refactoring
void showFindPage(String q, PrintWriter writer) {
    Document doc = Xml.read("contact/index.html");
    doc.selectFirst("[name=nameQuery]").val(nameQuery);
    Element contactsEl = doc.select("#contacts");
    Element template = contactsEl.select(".contact");
    contactsEl.clear();
    for (Contact contact : contactRepository.find(q)) {
        contactsEl.add(
            template.copy().text(contact.print()));
    }
    doc.write(writer);
}
.NET
[TestMethod]
public void ShouldFindSavedContacts()
{
    var server = new WebServer();
    server.Start("http://localhost:12380/");

    var url = "http://localhost:12380";
    var browser = new SimpleBrowser.WebDriver.SimpleBrowserDriver();

    browser.Url = url + "/contacts";

    browser.FindElement(By.LinkText("Add contact")).Click();

    browser.FindElement(By.Name("fullName")).SendKeys("Darth Vader");
    browser.FindElement(By.Name("phoneNumber")).SendKeys("555-33274-7827");
    browser.FindElement(By.Name("saveContact")).Click();

    browser.FindElement(By.LinkText("Find contact")).Click();
    browser.FindElement(By.Name("nameQuery")).SendKeys("vader");
    browser.FindElement(By.Name("nameQuery")).Submit();

    browser.FindElement(By.Id("contacts")).Text.Should()
       .Contain("555-33274-7827");
}
public class WebServer
{
    public void Start(string baseAddress)
    {
        var config =
           new HttpSelfHostConfiguration(baseAddress);
        config.Routes.MapHttpRoute(
               "web Default",
                "{controller}/{id}",
                new { id = RouteParameter.Optional });

        using (var server = new HttpSelfHostServer(config))
        {
            server.OpenAsync().Wait();
            Console.WriteLine("Press Enter to quit.");
            Console.ReadLine();
        }
    }
}
Part III:
Further directions
Norwegian
agricultural
  authority
Java web application with
     an MVC architecture
Controllers:
             • Create a view
•   Retrieve model from repo
        • Set model on view
              • Render view
View example:
@Override
public void render(HttpServletResponse resp) throws IOException {
    Match document = $("html",
            head(),
            $("img").attr("src", "/sms-varsel/Sparebank1.jpg"),
            $("h1", "Internet bank simulator"),
            $("form").attr("method", "post").append(
                    hiddenField(this.bankNum, "bankNum"),
                    hiddenField(this.customerId, "customerId"),
                    $("h2", "Set Mobile Phone Number"),
                    phoneNumberField(this.phoneNumber),
                    $("h2", "Account numbers"),
                    accountNumbersField(this.accountNumbers),
                    $("h2", "Payment account"),
                    paymentAccountField(this.defaultAccount),
                    $("h2", "Save changes"),
                    $("div",

$("input").attr("type", "submit").attr("value", "Store")).attr("nam
e", "update")));
    resp.setContentType("text/html");
    resp.setCharacterEncoding("UTF-8");
    resp.getWriter().write(document.toString());
}
Match document = $("html",
      head(),
      $("img").attr("src", "/logo.jpg"),
      $("h1", “Page name"),
      $("form").attr("method", "post").append(
          hiddenField(this.bankNum, "bankNum"),
          hiddenField(this.customerId, "customerId"),
          $("h2", "Save changes"),
          $("div",
              $("input").attr("type", "submit")
                        .attr("value", "Store"))
                        .attr("name", "update")));
Norwegian Power
   Transmission
System Operator
Universal repository
     Universal service
Commands and Queries
    One domain model
No Spring – 100 KLOC
Single-jar deployment
   • Includes scripts
     • Includes Jetty
public class StatnettWebServer {
    private final org.eclipse.jetty.server.Server server;
    public ContactWebServer(int port) {
        server = new Server(port);
        server.setHandler(new WebAppContext(“…", "/statnett"));
    }

    void start() throws Exception {
        server.start();
    }

    String getUrl() {
        int port = server.getConnectors()[0].getLocalPort();
        return "http://localhost:" + port + "/contacts";
    }

    public static void main(String[] args) throws Exception {
        StatnettWebServer server = new StatnettWebServer(10080);
        server.start();
        System.out.println(server.getUrl());
    }
}
SpareBank1
10 web service
        clients
HttpURLConnection
           JOOX
@Override
public String getCountryByIp(String ipAddress) throws
Exception {
    Document soapRequest =
        soapElement("S:Envelope",
            $("S:Body",
                wsxElement("wsx:GetGeoIP",
                    $("wsx:IPAddress", ipAddress))));
    Document soapResponse
       endpoint.postRequest(getSOAPAction(), soapRequest);
    return $(soapResponse).xpath("/Envelope/Body/*")
              .xpath("GetGeoIPResult/CountryName").text();
}
public Document postRequest(String soapAction, Document soapRequest) {
    HttpURLConnection conn = (HttpURLConnection) url.openConnection();
    connection.setDoInput(true);
    connection.setDoOutput(true);
    connection.addRequestProperty("SOAPAction", soapAction);
    connection.addRequestProperty("Content-Type", "text/xml");
    $(soapRequest).write(connection.getOutputStream());

     int responseCode = connection.getResponseCode();
     if (isErrorResponse(responseCode)) {
         String response = toString(connection.getErrorStream());
         String responseContentType = connection.getContentType();
         if (responseContentType.startsWith("text/xml")) {
             return response;
         }
         throw new ServiceCommunicationException(
                 "On POST to " + url + ": " + responseCode + " "
                 + connection.getResponseMessage() + ": " + response);
     }
     return $(connection.getInputStream()).document();
d}
Conclusion:
YAGNI
No calculator
      until…
Don’t use a framework
    you couldn’t have
       written yourself
Thank you
           jbr@exilesoft.com

 http://johannesbrodwall.com
           http://exilesoft.com

    http://twitter.com/jhannes

Bare-knuckle web development

  • 1.
    Bare-knuckle web development XP Days Ukraine Johannes Brodwall, Chief scientist Exilesoft Global
  • 2.
    • Bare-knuckle philosophy • Demonstration of bare- knuckle web in Java • Further directions
  • 3.
  • 4.
  • 5.
    High impact with low ceremony
  • 6.
    Framework light • Test-driven • No calculators
  • 7.
  • 8.
    Frameworks solve 80% of the job…
  • 9.
    … and makesthe rest 10 times as hard
  • 10.
  • 11.
    “How do Iimplement a custom SOAP header with JAX-WS?”
  • 12.
    “How to doX with Spring”
  • 13.
    @AutoWire + packagescan with 100s of beans
  • 14.
  • 15.
    No more architecturethan what’s needed
  • 16.
    Fast feedback cycle– also in the future
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
    WebDriver browser =createWebDriver(); browser.get(url); browser.findElement(By.linkText("Add contact")).click(); browser.findEleme(By.name("fullName")).sendKeys("Vader"); browser.findEleme(By.name("phoneNumber")).sendKeys("27"); browser.findEleme(By.name("saveContact")).click(); browser.findElement(By.linkText("Find contact")).click(); browser.findElem(By.name("nameQuery")).sendKeys("vader"); browser.findElement(By.name("nameQuery")).submit(); assertThat(browser.findElem(By.id("contacts")).getText()) .contains("555-33274-7827");
  • 22.
    Server server =new Server(0); server.setHandler( new WebAppContext("src/main/webapp", "/contacts")); server.start(); int port = server.getConnectors()[0].getLocalPort(); String url = "http://localhost:" + port + "/contacts";
  • 23.
    <web-app version="2.5“> <servlet> <servlet-name>contactServlet</servlet-name> <servlet-class> com.exilesoft.bareknuckleweb.ContactServlet </servlet-class> </servlet> <servlet-mapping> <servlet-name>contactServlet</servlet-name> <url-pattern>contact/*</url-pattern> </servlet-mapping> </web-app>
  • 24.
    public class ContactServletextends HttpServlet { }
  • 25.
    @Test public void shouldShowAddForm()throws Exception { ContactServlet servlet = new ContactServlet(); HttpServletRequest req = mock(HttpServletRequest.class); HttpServletResponse resp = mock(HttpServletResponse.class); StringWriter html = new StringWriter(); when(resp.getWriter()).thenReturn(new PrintWriter(html)); when(req.getPathInfo()).thenReturn("/create.html"); servlet.doGet(req, resp); verify(resp).setContentType("text/html"); assertThat(html.toString()) .contains("<form method='post'") .contains("<input type='text' name='fullName'") .contains("<input type='text' name='phoneNumber'") .contains("<input type='submit' name='createContact'"); }
  • 26.
  • 27.
    void showFindPage(String q,PrintWriter writer) { Document doc = Xml.read("contact/index.html"); doc.selectFirst("[name=nameQuery]").val(nameQuery); Element contactsEl = doc.select("#contacts"); Element template = contactsEl.select(".contact"); contactsEl.clear(); for (Contact contact : contactRepository.find(q)) { contactsEl.add( template.copy().text(contact.print())); } doc.write(writer); }
  • 28.
  • 29.
    [TestMethod] public void ShouldFindSavedContacts() { var server = new WebServer(); server.Start("http://localhost:12380/"); var url = "http://localhost:12380"; var browser = new SimpleBrowser.WebDriver.SimpleBrowserDriver(); browser.Url = url + "/contacts"; browser.FindElement(By.LinkText("Add contact")).Click(); browser.FindElement(By.Name("fullName")).SendKeys("Darth Vader"); browser.FindElement(By.Name("phoneNumber")).SendKeys("555-33274-7827"); browser.FindElement(By.Name("saveContact")).Click(); browser.FindElement(By.LinkText("Find contact")).Click(); browser.FindElement(By.Name("nameQuery")).SendKeys("vader"); browser.FindElement(By.Name("nameQuery")).Submit(); browser.FindElement(By.Id("contacts")).Text.Should() .Contain("555-33274-7827"); }
  • 30.
    public class WebServer { public void Start(string baseAddress) { var config = new HttpSelfHostConfiguration(baseAddress); config.Routes.MapHttpRoute( "web Default", "{controller}/{id}", new { id = RouteParameter.Optional }); using (var server = new HttpSelfHostServer(config)) { server.OpenAsync().Wait(); Console.WriteLine("Press Enter to quit."); Console.ReadLine(); } } }
  • 31.
  • 32.
  • 33.
  • 34.
    Java web applicationwith an MVC architecture
  • 35.
    Controllers: • Create a view • Retrieve model from repo • Set model on view • Render view
  • 36.
  • 37.
    @Override public void render(HttpServletResponseresp) throws IOException { Match document = $("html", head(), $("img").attr("src", "/sms-varsel/Sparebank1.jpg"), $("h1", "Internet bank simulator"), $("form").attr("method", "post").append( hiddenField(this.bankNum, "bankNum"), hiddenField(this.customerId, "customerId"), $("h2", "Set Mobile Phone Number"), phoneNumberField(this.phoneNumber), $("h2", "Account numbers"), accountNumbersField(this.accountNumbers), $("h2", "Payment account"), paymentAccountField(this.defaultAccount), $("h2", "Save changes"), $("div", $("input").attr("type", "submit").attr("value", "Store")).attr("nam e", "update"))); resp.setContentType("text/html"); resp.setCharacterEncoding("UTF-8"); resp.getWriter().write(document.toString()); }
  • 38.
    Match document =$("html", head(), $("img").attr("src", "/logo.jpg"), $("h1", “Page name"), $("form").attr("method", "post").append( hiddenField(this.bankNum, "bankNum"), hiddenField(this.customerId, "customerId"), $("h2", "Save changes"), $("div", $("input").attr("type", "submit") .attr("value", "Store")) .attr("name", "update")));
  • 39.
    Norwegian Power Transmission System Operator
  • 40.
    Universal repository Universal service Commands and Queries One domain model
  • 41.
    No Spring –100 KLOC
  • 42.
    Single-jar deployment • Includes scripts • Includes Jetty
  • 43.
    public class StatnettWebServer{ private final org.eclipse.jetty.server.Server server; public ContactWebServer(int port) { server = new Server(port); server.setHandler(new WebAppContext(“…", "/statnett")); } void start() throws Exception { server.start(); } String getUrl() { int port = server.getConnectors()[0].getLocalPort(); return "http://localhost:" + port + "/contacts"; } public static void main(String[] args) throws Exception { StatnettWebServer server = new StatnettWebServer(10080); server.start(); System.out.println(server.getUrl()); } }
  • 44.
  • 45.
  • 46.
  • 47.
    @Override public String getCountryByIp(StringipAddress) throws Exception { Document soapRequest = soapElement("S:Envelope", $("S:Body", wsxElement("wsx:GetGeoIP", $("wsx:IPAddress", ipAddress)))); Document soapResponse endpoint.postRequest(getSOAPAction(), soapRequest); return $(soapResponse).xpath("/Envelope/Body/*") .xpath("GetGeoIPResult/CountryName").text(); }
  • 48.
    public Document postRequest(StringsoapAction, Document soapRequest) { HttpURLConnection conn = (HttpURLConnection) url.openConnection(); connection.setDoInput(true); connection.setDoOutput(true); connection.addRequestProperty("SOAPAction", soapAction); connection.addRequestProperty("Content-Type", "text/xml"); $(soapRequest).write(connection.getOutputStream()); int responseCode = connection.getResponseCode(); if (isErrorResponse(responseCode)) { String response = toString(connection.getErrorStream()); String responseContentType = connection.getContentType(); if (responseContentType.startsWith("text/xml")) { return response; } throw new ServiceCommunicationException( "On POST to " + url + ": " + responseCode + " " + connection.getResponseMessage() + ": " + response); } return $(connection.getInputStream()).document(); d}
  • 49.
  • 50.
  • 51.
  • 52.
    Don’t use aframework you couldn’t have written yourself
  • 53.
    Thank you [email protected] http://johannesbrodwall.com http://exilesoft.com http://twitter.com/jhannes