How to organize complexity with JSP and Snippetory

In habours a large toolset is used to deal with complexityTo display data with template engines like JSP or all the others we often face the problem to organize pretty complex logic. Whenever we face a problem in programming outside of the templates we have tons of advice and support how to solve it by pattern, code analysis tools, test tools, hard typed languages,inheritance, and so on. Searching for similar help in working with templates reveals almost nothing. Ok there are two things:

  1. MVC will do the job. On one hand there are so many versions of MVC out there, that whatever you do, you’ll find some recommending this. On the other hand most of those concentrates on the phase before the template is entered: Request handling and dispatching.
  2. Very much information, what a template engine should not be capable of. Even though anyone knows what should be removed from the template only very few people go the consequent way and use a solution without scripting at all.

Now let’s stop complaining and jump over to the original topic: How to organize complexity with templating. First I’ve defined a little example:

With my colleague Johannes Sebastian Prahler (called JSP for some reason) I’m working on a shop. It’s a mature project with pretty high complexity on almost every aspect. For the moment we use JSP with  scriptlets but it’s hard to organize the complexity. JSP would like to move JSTL/EL while I’d prefer Snippetory. Of course.

Me:
What we want to do is presenting 2 numbers: The number of units and the number of
packs. (Where number of units = number of packs * units per pack)

JSP:
That`s simple:

<div class="units">${entry.quantityInUnits}</div>
<div class="packs">${entry.quantityInPacks}</div>

No, you said numbers. We`ll need to format them.

<fmt:setLocale value="${user.locale}"/>
...
<div class="units">
 <fmt:formatNumber type="number" pattern="#,##0" 
      value="${entry.quantityInUnits}" />
</div>
<div class="packs">
 <fmt:formatNumber type="number" pattern="#,##0" 
      value="${entry.quantityInPacks}" />
</div>

Me:
Now, in Snippetory we don't tell what has to be done. Templates only describe how
data is bound to the text. We can define a default behavior for integers and
another default formatting for number types that support decimals.

<div class="units">$units(number="#,##0")</div>
<div class="packs">$packs(number="#,##0")</div>

However, we'll need some java code in addition:

entryTpl.set("units", entry.getQuantityInUnits());
entryTpl.set("packs", entry.getQuantityInPacks());

JSP:
Now we're in cart we have to edit the number.

<div class="units">
  <input type="text" name="units_$entryId" value="<fmt:formatNumber type="number" 
      pattern="#,##0" value="${entry.quantityInUnits}" />" />
</div>
<div class="packs">
 <fmt:formatNumber type="number" pattern="#,##0" 
      value="${entry.quantityInPacks}" />
</div>

Me:
As we now found a place where changes seem to likely I put the display of that numbers into sub templates. This makes me pretty flexible how to use them. Every single sub template can be globally reused if this makes sense.

<div class="units">
  <t:units>
  <input name="units_$entryId" value="$amount(number="#,##0")" />
  </t:units>
</div>
<div class="packs">
  <t:packs>$amount(number="#,##0")</t:packs>
</div>

Of course the java code gets a bit more complicated that way.

entryTpl.get("units")
    .set("amount", entry.getQuantityInUnits())
    .set("entryId", entryId)
    .render();
entryTpl.get("packs").set("amount", entry.getQuantityInPacks()).render();

JSP:
No. Oh. Some times it's just the other way around. The units are fix and the packs are editable.

<c:choose> 
  <c:when test="${editUnits}" > 
    <div class="units">
      <input type="text" name="units_$entryId" value="<fmt:formatNumber 
          type="number" pattern="#,##0" value="${entry.quantityInUnits}" />" 
      />
    </div>
    <div class="packs">
      <fmt:formatNumber type="number" pattern="#,##0" 
          value="${entry.quantityInPacks}" />
    </div>
  </c:when> 
  <c:otherwise> 
    <div class="units">
     <fmt:formatNumber type="number" pattern="#,##0" 
        value="${entry.quantityIUnits}" />
    </div>
    <div class="packs">
      <input type="text" name="packs_$entryId" value="<fmt:formatNumber type="number" 
          pattern="#,##0" value="${entry.quantityIPacks}" />" />
    </div>
  </c:otherwise> 
</c:choose> 

Me:
Fortunately I was prepared. So I need only a minor change to the template. The name gets more parametrize so the control can be used for both, units and packs.
I expect from a template to get me a quick and rough overview how a page works. For this, I thinks it should be focused on one simple but realistic case. Having each and every special case in the same place makes it hard for me get to the point.

<div class="units">
  <t:units>
  <input name="$type()_$entryId" value="$amount(number="#,##0")" />
  </t:units>
</div>
<div class="packs">
  <t:packs>$amount(number="#,##0")</t:packs>
</div>

Most of the complexity happens on the java side. But here I can introduce a few methods to handle it. In Snippetory everything is about building text. That's why the builder pattern shows up about everywhere. This helps to get concise and readable code.

...
    if (editUnits) {
      entryTpl.set("units", input(entry.getQuantityInUnits(), "units"));
      entryTpl.set("packs", amount(entry.getQuantityInPacks())):
    } else {
      entryTpl.set("units", amount(entry.getQuantityInUnits()));
      entryTpl.set("packs", input(entry.getQuantityInPacks(), "packs")):
    }
...
  public Template input(BigDecimal amount, String type) {
    return entryTpl.get("units")
        .set("amount", amount)
        .set("entryId", entryId)
        .set("type", type);
  }

  public Template amount(BigDecimal amount) {
    return entryTpl.get("packs").set("amount", amount);
  }

JSP:
Now we need to implement the bundle prices. The rendering of the bundles is pretty complicated, so I'll put it into a tag file let's call it bundle.tag.

<%@ attribute name="quantity" required="true" %>
<%@ attribute name="type" required="true" %>
<c:set var="unit">
  <c:choose> 
    <c:when test="${editUnits}" > 
      <c:out value="entry.product.packName">
        <jsp:attribute name="default">
          <shop:message key="CART_PACKS" />
        </jsp:attribute>
      </c:out>
    </c:when> 
    <c:otherwise>
      <shop:message key="CART_PIECES" />
    </c:otherwise> 
  </c:choose>   
</c:set>
<select name="${type}_${entryId}">
  <c:forEach var="price" items="${prices}">
    <jsp:element name="option">
      <jsp:attribute name="value">
        <fmt:formatNumber type="number" pattern="#,##0.00" value="${price.quantity}" />
      </jsp:attribute>
      <c:if test="${quantity eq price.quantity}">
        <jsp:attribute name="selected">selected</jsp:attribute>
      </c:if>
    </jsp:element>
    <jsp:body>
      <c:set var="msg">
        <%-- 10 pieces for 10,00€ --%>
        <shop:message key="CART_BUNDLE_SELECTOR">
          <shop:msgAttrib name="QUANTITY">
            <jsp:attribute name="value">
              <fmt:formatNumber type="number" pattern="#,##0" value="${price.quantity}" />
            </jsp:attribute>
          </shop:msgAttrib>
          <shop:msgAttrib name="PRICE">
            <jsp:attribute name="value">
              <fmt:formatNumber type="currency" pattern="#,##0.00" value="${price.price}" />
            </jsp:attribute>
          </shop:msgAttrib>
          <shop:msgAttrib name="UNIT" value="${unit}" />
        </shop:message>
      </c:set>
    </jsp:body>
  </c:forEach>
</select>

Hey, now seen how powerful JSP is. Reusable can be put to tag files and this way be expressed completely in the template engine. In addition we can put the editor selection into another tag (let's call it getEditor.tag)

<%@ attribute name="quantity" required="true" %>
<%@ attribute name="type" required="true" %>
<%@ attribute name="editable" required="true" %>
<c:choose> 
  <c:when test="${editable}" > 
    <c:choose> 
      <%-- unfortunately constants are not supported here --%>
      <c:when test="${entry.product.priceType eq 3}" > 
        <my:bundle quantity="${quantity}" type="${type}" />
      </c:when> 
      <c:otherwise>
        <input type="text" name="${type}_${entryId}" value="<fmt:formatNumber 
            type="number" pattern="#,##0" value="${quantity}" />" />
      </c:otherwise> 
    </c:choose>   
  </c:when> 
  <c:otherwise>
    <fmt:formatNumber type="number" pattern="#,##0" value="${quantity}" />
  </c:otherwise> 
</c:choose>   

So the main part gets again a bit simpler.

<div class="units">
  <my:getEditor type="units" quantity="${entry.quantityInUnits}" editable="${editUnits eq true}" />
</div>
<div class="packs">
  <my:getEditor type="packs" quantity="${entry.quantityIPacks}" editable="${editUnits eq false}" />
</div>

Me:
Man, you're joking, aren't you? You pick up 5 lines of template code, blow it 10 times in size and tell me how cute reuse is? People tell me java is wordy, but for this you need JSP! Now look at Snippetory. That simple piece of template can be placed where we like to have it. At the end of the main template, in its own file or in a template of reusable components according to the metaphor repository pattern

<t:bundlePrices>
<select name="$type()_$entryId">
  <t:prices>
  <option value="$amount" $selected{select="selected"}>
    /// 10 pieces for 10,00€
    $(msg="CART_BUNDLE_SELECTOR")
  </option>
  </t:prices>
</select>
</t:bundlePrices>

Of course, I have to implement the stuff of the bundle tag, too. But I do that with Java. I'll write a bundle method that's call from the input method. The rest of the code stays untouched.

  public Template bundle(BigDecimal amount, String type) {
    Template pricesTpl = entryTpl.get("bundlePrices");
    pricesTpl.set("entryId", entryId).set("type", type);
    String unitName = getUnitName(entry.getProduct());
    for (PriceEntry price: prices) {
      Template priceTpl = priceTpl.get("prices").set("amount", price.getQuantity());
      priceTpl.set("UNIT", unitName).set("QUANTITY", price.getQuantity()).set("PRICE", price.getPrice());
      if (price.getQuantity().equals(amount)) {
         priceTpl.get("selected").render();
      }
      priceTpl.render();
    }

    return pricesTpl;
  }
  public String getUnitName(Product prod) {
    if (editUnits) {
      String packName= entry.getProduct().getPackName();
      if (packName!= null && !packName.isEmpty()) {
        return packName;
      }
      return ctx.msg("CART_PACKS");
    }
    return ctx.msg("CART_PIECES");
  }
  public Template input(BigDecimal amount, String type) {
    if (entry.getProduct().getPriceType() == PriceTypes.BUNDLE) {
      return bundle(amount, type);
    }
    return entryTpl.get("units")
        .set("amount", amount)
        .set("entryId", entryId)
        .set("type", type);
  }
About these ads