Tapestry Training -- From The Source

Let me help you get your team up to speed in Tapestry ... fast. Visit howardlewisship.com for details on training, mentoring and support!

Friday, December 06, 2013

Tapestry Quicky: ConditionalComment

Here's a quicky I just put together for a client to generate IE conditional comments. This isn't a feature supported by Tapestry's JavaScriptSupport (for libraries; it is support for CSS files).

Fortunately, this is something that comes together in Tapestry in almost no code:

This is what it looks like in a template:

And here's the markup it produces:

And that's the whole point ... the URLs for the referenced JavaScript libraries are now Tapestry assets, and will have access to Tapestry's asset pipeline as unique resources, including GZip compression and minimization.

It is always important to me that Tapestry not get in the way: frameworks that lock you down and prevent you from accomplishing your goals should not be used. Tapestry has a long history, and there are certainly many areas that could have been implemented differently, or simply not implemented at all, but it's nice that most edge cases, like this one, have a simple solution.

Monday, November 11, 2013

Improving Clojure Feedback : Stack Traces

Perhaps no subject in software development is as important to me as feedback, which to my mind, is the language, library, or framework providing information back to the developer when things go wrong. A language, library, or framework that provides poor feedback is creating barriers to its own adoption.

There's a possibly apocryphal legend about early FORTRAN compilers. Back in the day (thankfully, even earlier than my personal back in the day), you would code up your FORTRAN code on punch cards and deliver them to an operator. At some convenient time (for the operator), the compiler would be started and fed the stack of cards. Assuming the cards had not been bent, folded, spindled, or mutilated ... and that the program itself contained no errors, you would get a compiled program somewhere. If anything failed, you would simply get the error message "FAIL". Nothing else.

That's bad feedback.

It's important to feel that you are in control of your own tools; that's part of the love/hate relationship so many developers have with their IDEs: the IDEs certainly enable a lot of productivity ... but what you always end up remembering is when the IDE gets in the way of some really critical, really difficult stuff. Or when it crashes mysteriously at a critical moment. Or when it just won't dosomething and won't tell you why not.

Likewise, any tool (or language, library, or framework) that provides bad feedback, especially when things go wrong, is building a perception that it is hard to use overall. We're a fickle bunch ... we'll be off to find the Next Big Thing instead.

Which brings us to my favorite programming language, Clojure. When things are going right in Clojure they are going very, veryright. However, when things go wrong in Clojure it can be very painful.

First of all, Clojure's basic mechanism for any kind of error is to simply throw an exception. Since Clojure is a Lisp, the difference between compile time and runtime is not so easily distinguished ... a running Clojure program is often loading source and compiling code as well as running the application.

And what happens to exceptions? They are thrown in one piece of code and, more often than not, caught, wrapped, and re-thrown in containing stack frames. Ultimately, this whole stack of exceptions gets spewed out to the console.

This isn't quite bad as that FORTRAN compiler; however, even on my big 30" cinema display, a Clojure compilation failure always sends me scrolling backwards in my terminal window to see the actual error and (with more than a pinch of luck) the file name and line number containing the error.

It's much worse when it comes to runtime failures in my own code; it is likely that the exceptions will be nested even deeper than the number of levels intituted by the Clojure compiler and REPL. Beyond that is the issue of laziness ... program execution goes a little topsy-turvey from your code due to laziness, as what code is executing is typically driven by what particular bit of lazily-computed data is needed at any particular instant. That takes a little getting used to for new Clojure developers.

And so, deep in your code, you passed a string when a keyword was expected, or you passed a map when a seq was expected, or any number of other similar situations ... and now you are faced with an exception and its stack trace.

Here's the ugly not-so-secret of Clojure: it's a light and tasty frosting on top of a cake of sawdust and rusty gears: Java. Every Clojure function must be converted into a Java class in order to execute within the JVM. Java places somewhat arbitrary limits on the class and method names: Where Clojure loves dashes in function names, Java doesn't allow them. Likewise, Clojure loves all kind of other punctuation, with functions named "+" or "string?", that Java forbids as well.

Clojure manglesthe Clojure namespace and function names to fit into the Java world, and nothing in the exception reporting chain does anything to make the result any prettier. Here's an example:

All the information is available ... but you have to work to get at it: You need to mentally de-mangle the Java class names back to Clojure namespaces and function names. You have to ignore a lot of stack frames that are really the infrastructure supporting Clojure on top of Java. You have to assemble the meaningful stack trace (the last and deepest one) from the parts it extends off of the prior stack traces. You have to ignore the numeric ids tacked on to the end of names to help ensure uniqueness.

This is not great feedback.

What if we could provide just a little bit better feedback? What if we could do a lot of that name-demangling automatically, and present the same data in a more helpful way? That's what I've been working on as part of the io.aviso:pretty library for Clojure.

Pretty can be used to format that same exception just a little bit nicer:

The exact format is still in-flux because it is currently so wide; fortunate, the interesting stuff is always to the left!

This same approach appears in the related io.aviso:twixt library, modified to take advantage of how much more richly data can be presented in a web page:

However, this is just the start. Providing truly useful feedback is more than just putting a patch on exception reporting. It requires attention to detail throughout the tool. This has been the case for Apache Tapestry, where a significant amount of the framework is about detecting and reporting errors in a useful way.

Twixt has a set of utilities to address this as well; a way of tracking what the application is doing so that exceptions can be reported. The end result is an "operations trace" that can nest to an arbitrary depth and is used to reconstruct how the application arrived at the point of failure.

This extra layer of code can pay big dividends when things go wrong; the nesting of operations can be critical to understanding the underlying problem; the operations trace combines the very local scope of the failure with a larger scope that explains why the failed code was invoked in the first place.

So I'm very excited by the possibilities, but to provide truly universal, useful feedback in Clojure may not be something that can be effectively added in from the outside: it has to be a priority for the core Clojure developers, and bred into the fabric of the Clojure compiler and runtime. That's something I'd love to see ... or help with.

Wednesday, October 16, 2013

Gainesville in February 2014

I'll be spending about three weeks in Gainesville, Florida this coming February (2014). For the wife and kids, its about visiting the grandparents, hiking, fishing, and playing. For me, its mostly about work, sigh. In any case, I'm told that Gainesville has gotten quite the start up culture going recently ... I'd love an opportunity to spread the word about Tapestry, Clojure, or AngularJS (or Spock, or Geb, or any other presentation I have in my bag of tricks). Think of this as a chance to put your "brown bag lunch" on steroids for a day.

Just get in contact with me if you are interested; see my contact information on my home page.

Monday, September 23, 2013

Named Parameters for Clojure

Clojure can simulate named parameters, or a mix of positional and named parameters. This is really old news, dating back to Clojure 1.2 if evidence serves, but I always have trouble finding this exact syntax, and it is not fully obvious.

Say you want to accept a certain number of optional parameters with names, and perhaps, defaults. It turns out you can combine rest parameters (the ones that come after a &) with map destructuring.

In the simple case, you don't care what the possible options are, and you don't have any defaults.

The keys and values you pass to this function, say (named-parameters :foo 1 :bar 2), are collected together as symbol params.

If you don't provide an even number of values (that is, the same number of keys and values), you'll get a reasonable exception, such as java.lang.IllegalArgumentException: No value supplied for key: :bar

Easy-peasey ... but you need to extract values from the params map to use them inside the function, e.g.: (:foo params). It would be nicer to have them as symbols, just like with normal positional parameters. This is also easy, by leveraging more of the features of map destructuring:

The :keys identifies the keywords expected in the map; it works backwards from the symbol name, foo, to the expected keyword, :foo.

There's also a :syms (for when the keys are expected to be symbols) and :strs (for when the keys are expected to be strings).

The :or identifies default values for each symbol. The end result is that we can rely on defaults from :or or provide our own values when invoking the function:

And since this is Clojure, you can combine all of these things together quite easily ... some positional parameters, some named, some identified by keywords, others identified by symbols.

Thursday, July 25, 2013

Tapestry 5.4: An Avalanche of Fixed Issues

I've been working through the Tapestry issue list over the last couple of days; I'm closing a lot of duplicate and invalid bugs, and fixing some low-hanging fruit. Still, the number of fixed bugs already is impressive:

Bug

  • [TAP5-800] - Server side error during provideCompletions event for Autocompleter mixin is not reported on the client side properly
  • [TAP5-860] - JavaScript errors when adding validations to checkboxes
  • [TAP5-986] - A request can fail with an NPE in some cases, when a Tapestry page is acting as the servlet container error page
  • [TAP5-1010] - Fix Finnish validation message translation - corrected file attached
  • [TAP5-1193] - tapestry.js prevents using the back/forward browser cache
  • [TAP5-1200] - Nested Ajax calls result in a call to $T(null)
  • [TAP5-1569] - Grid should implement the ClientElement interface
  • [TAP5-1601] - Sometime a method that references a field with a conduit will not be instrumented, resulting in an NPE accessing the field itself
  • [TAP5-1668] - JavaDoc for @Parameter.value() should be clearer about the empty string
  • [TAP5-1691] - AssetPathConstructorImpl should URL-encode the application version
  • [TAP5-1704] - Localizing the "Today" and "None" labels in the core datefield component
  • [TAP5-1729] - Sometimes YUICompressor can fail with java.util.EmptyStackException
  • [TAP5-1734] - Race condition loading JavaScript libraries with ProgressiveDisplay
  • [TAP5-1735] - Most packages lack package-level javadocs
  • [TAP5-1742] - AfterRender() in Loop component should not short circuit
  • [TAP5-1762] - Some components do not have include a description of their parameters in their JavaDoc pages
  • [TAP5-1765] - PerThread scope is not honored when service is created using autobuild
  • [TAP5-1768] - @ActivationRequestParameter does not encode to be URL friendly
  • [TAP5-1770] - PageTester causes StringIndexOutOfBoundsException for any page request path with query parameter
  • [TAP5-1773] - FormFieldFocus mixin passes control name, not client id, to JavaScriptSupport.autofocus()
  • [TAP5-1779] - Tapestry allows directory listing of assets via client browser
  • [TAP5-1784] - Extra comma in tapestry-messages_de.js causes Internet Explorer to fail to work
  • [TAP5-1785] - Exceptions while compressing JavaScript are not fully reported
  • [TAP5-1787] - TextField should be usable with HTML5 type values (such as "number", "email", etc.)
  • [TAP5-1788] - Service id 'environment' has already been defined by org.apache.tapestry5.services.TapestryModule with Spring 3.1
  • [TAP5-1791] - On some JDKs, the complex regular expression used by ComponentEventLinkEncoderImpl will cause a stack overflow
  • [TAP5-1798] - Grid and BeanDisplay should ignore properties that are actually static fields
  • [TAP5-1822] - LinkSecurity should be public, not internal, as it is used with the public Link interface
  • [TAP5-1825] - Incorrect order of parameter in localization messages within ValidationMessages Italian lang
  • [TAP5-1831] - DelegatingInjectionResources does not pass generic type information to its first delegate
  • [TAP5-1836] - "LocalhostOnly" WhitelistAnalyzer check "0:0:0:0:0:0:0:1%0" ip address instead "0:0:0:0:0:0:0:1"
  • [TAP5-1844] - Datefield not fires onchange event on changes
  • [TAP5-1848] - tapestry-jpa ignores persistence provider from persistence.xml
  • [TAP5-1854] - AjaxComponentEventRequestHandler doesn't handle the case where a response has already be returned, and may append an empty JSON Object to the response
  • [TAP5-1860] - Access to protected component fields does not always reflect in subclasses
  • [TAP5-1868] - SRSCachingInterceptor returns compressed version of asset for all clients once it was compressed for some client
  • [TAP5-1870] - javascript added while in the render phase of a component from an ajax request is never executed
  • [TAP5-1873] - JavaScript execution exception is not logged
  • [TAP5-1880] - GZip compression should be disabled if the request is over http 1.0
  • [TAP5-1881] - TypeCoercion from Number to Boolean returns false for any number that is an even multiple of 256
  • [TAP5-1887] - Client-side JavaScript error if console.info/debug/... is available but not a function
  • [TAP5-1890] - PlaceholderBlock should implement RenderCommand
  • [TAP5-1892] - FormFragment validates non-displayed fragments (reopen)
  • [TAP5-1903] - Client-side exception when a Zone containing a Form with an Upload component is re-rendered
  • [TAP5-1906] - Interaction between client-side validation and submit buttons can result in a server-side error parsing JSON array
  • [TAP5-1907] - Client exception in IE9 when partial page render introduces stylesheets
  • [TAP5-1911] - Tree leaf is still not selectable
  • [TAP5-1926] - PropertyDisplayBlocks Date's display block is not thread safe
  • [TAP5-1929] - High contention in method InternalComponentResourcesImpl.postRenderCleanup() and NamedSet.getValues()
  • [TAP5-1931] - Alerts can not be dismissed in IE8.
  • [TAP5-1933] - Email validation should use a different regular expression
  • [TAP5-1935] - ContextResource uses a deprecated File.toURL
  • [TAP5-1937] - removeCookieValue with custom path
  • [TAP5-1938] - The ValueEncoder for JPA entity types should encode transient instances as null rather than throw an exception
  • [TAP5-1945] - Clojure integration breaks if tapestry-core is on the classpath
  • [TAP5-1949] - Alerts component does not show alerts added from a component that occurs later in the template
  • [TAP5-1950] - ResourceStreamer sends SC_NOT_MODIFIED as an error, but it should be just status
  • [TAP5-1952] - JavaScript Compressor should log warnings as well as errors, and identify the input clearly
  • [TAP5-1956] - Tapestry.ZoneManager creates memory leaks
  • [TAP5-1960] - @javax.annotations.Inject on a field, without @Named, does not inject service resources
  • [TAP5-1963] - Original exception lost in CommitAfterWorker upon abort
  • [TAP5-1964] - In production mode, placeholder timestamp needs to be limited to one-second accuracy
  • [TAP5-1969] - Excessive warnings from YUICompressor
  • [TAP5-1973] - :443 added to URLs when using the Link.toAbsoluteURI(true)
  • [TAP5-1976] - XML Parser adds attributes with default values and produces invalid HTML5 markup
  • [TAP5-1977] - Memory leak (perm gen) in component reloading
  • [TAP5-1979] - Changing the implementation of a method after adding method advice does not work; the original implementation remains
  • [TAP5-1983] - PerThreadManager does not cleanup on shutdown, can lead to memory leaks when application redeployed
  • [TAP5-1984] - Adding alerts doesn't work during ajax rendering
  • [TAP5-1985] - Clicking Alert component's "dismiss all" link scrolls to top of page
  • [TAP5-1988] - Tapestry Security Violations
  • [TAP5-1991] - YUICompressor should be less verbose about common warnings
  • [TAP5-1992] - Switch YUICompressor dependency back to com.yahoo version 2.4.7
  • [TAP5-1995] - Tapestry5 Application can not be deployed as Tomcat7 HotDeploy Package
  • [TAP5-2001] - Race condition while loading javascript file via ajax
  • [TAP5-2004] - Tapestry-Ioc fails to build on Windows (ReloadTest.java)
  • [TAP5-2008] - Serialized object data stored on the client should be HMAC signed and validated
  • [TAP5-2009] - Downgrade bundled Prototype version back to 1.7
  • [TAP5-2010] - Broken links in Javadoc pages
  • [TAP5-2014] - Zone highlight leaves behind an explicit background-color which overrides css background-color
  • [TAP5-2015] - Tomcat .war path is not decoded properly
  • [TAP5-2017] - Triggerfragment Mixin does not work when used within a table element for IE7
  • [TAP5-2025] - Duplicate generated ids
  • [TAP5-2034] - When the application operates with a context path, asset URLs are incorrectly formed
  • [TAP5-2037] - ValidationTracker/Flash persistence race condition with AJAX
  • [TAP5-2040] - TapestryAppInitializer should have access to system properties; this is a reversion in 5.4
  • [TAP5-2041] - Links within subheadings are invisible on Javadoc pages
  • [TAP5-2045] - Set default CSS class for Label component to be "control-label", but allow overrides
  • [TAP5-2047] - ElementWrapper#find jQuery implementation is broken if there is no match
  • [TAP5-2049] - Tapestry should provide locking semantics for attributes stored in the session, to prevent multiple simultaneous requests (due to Ajax) from conflicting
  • [TAP5-2052] - tapestry-ioc has a compile dependency on tapestry-test
  • [TAP5-2053] - Use of 'transient' property in alert options breaks JavaScript processors
  • [TAP5-2054] - `dom.js` implementations break when minified
  • [TAP5-2057] - CSS URL rewriting is incomplete
  • [TAP5-2061] - core_**.properties got wrong encoding
  • [TAP5-2062] - Ajax alerts not rendered after a JS call to dismissOne()
  • [TAP5-2064] - Add 'info' style to info alerts
  • [TAP5-2068] - Use <button>, not <a>, tags for the close buttons on alerts
  • [TAP5-2071] - Tapestry should output valid HTML5 when using the HTML5 doctype in templates
  • [TAP5-2073] - AbstractEventContext always emits "null" regardless of what values are defined by subclass
  • [TAP5-2079] - Tapestry should handle the case where the context path is (incorrectly) "/" (not the empty string)
  • [TAP5-2081] - datefield: cannot blank out date value via text input
  • [TAP5-2082] - When the last alert that is removed is transient, the outer container is not removed
  • [TAP5-2084] - Form should decode its link parameters
  • [TAP5-2087] - Checkboxes in forms always return "on" (considered as "checked" server-side)
  • [TAP5-2089] - JavaScript errors when using Autocomplete mixin
  • [TAP5-2092] - Tapestry-mongodb has a compile dependency on tapestry-test
  • [TAP5-2095] - Module assets should not be sent a far-future expires header
  • [TAP5-2101] - BeanEditor should always provide a new BeanValidationContext (JSR-303)
  • [TAP5-2103] - ElementWrapper#text() jQuery implementation does not work
  • [TAP5-2106] - Tapestry incorrectly rewrites CSS urls in a variety of cases
  • [TAP5-2110] - MinLength and MaxLength validators don't import "t5/core/validation"
  • [TAP5-2119] - Depenencies excluded in Gradle build files are not excluded in the generated Maven POMs
  • [TAP5-2120] - StringIndexOutOfBoundsException computing relative paths for certain Resources
  • [TAP5-2121] - Broken image (for Tapestry Logo) in generated documentation page
  • [TAP5-2122] - visible() on empty <div> is false; prevents valiation of Palette component
  • [TAP5-2124] - events.zone.refresh must be triggered directly on zone element, can fail otherwise
  • [TAP5-2125] - Ajax form submissions via jQuery may pass t:formdata as "t:formdata[]" if there are multiple values
  • [TAP5-2126] - TAP5-2063 fix (multi-valued parameters in Link) conflicts with use of ActivationRequestParameter and PageLink.parameters parameter
  • [TAP5-2132] - Generation of documentation for t5/core/dom should use the split (for jQuery, for Prototype) files, not the combined source file
  • [TAP5-2137] - Tree component renders empty nodes as expandable, AssertionError when expanded
  • [TAP5-2141] - Ajax form submission should ignore fields with no name
  • [TAP5-2142] - Exceptions thrown during Ajax processing or validation force a traditional form submission
  • [TAP5-2148] - VersionUtils should close the stream it opens

Improvement

  • [TAP5-336] - The Cookies service interface could be simplified using a builder pattern
  • [TAP5-672] - Translation for Vietnamese
  • [TAP5-1008] - The order in which zones are added to the MultiZoneUpdate should be honored on the client side, to allow nested zones to be updated all in a single request
  • [TAP5-1268] - Have Tapestry's core library contribute to the global message catalog; move all validation messages and component catalogs to the single file
  • [TAP5-1394] - Adding method to Cookies api that lets you set path, domain, and maxAge at once.
  • [TAP5-1405] - XHR requests should be easily callable from javascript and not rely on a zone
  • [TAP5-1560] - Remove internal dependancies on deprecated features
  • [TAP5-1570] - Zone elements in the client should trigger events so that the application can react to changes
  • [TAP5-1748] - Alerts component should render informal parameters
  • [TAP5-1756] - Let the asset path prefix be configurable
  • [TAP5-1775] - Improve javascript performance while creating zone events
  • [TAP5-1781] - Improve javascript load time of pages improving onDomLoadedCallback
  • [TAP5-1801] - Component fields should not need to be private, merely non-public
  • [TAP5-1805] - Selectable Tree component look and feel is lacking in user affordances
  • [TAP5-1808] - Change Form to (by default) immediately render markup when there are validation errors, to avoid creating the session
  • [TAP5-1816] - Add CSS rule for DIV.t-exception-container to default.css that sets a very high z-index
  • [TAP5-1824] - New translations for Norwegian BokmÃ¥l
  • [TAP5-1827] - KaptchaField should have a parameter to allow it to operate as a visible text field rather than a password field
  • [TAP5-1832] - Tapestry could do an even better job of filtering unnecessary stack frames from the exception report
  • [TAP5-1840] - Add method Request.isSessionInvalidated()
  • [TAP5-1847] - An extension component template (with a root t:extend element) should allow t:block elements to be nested
  • [TAP5-1849] - The text "Dismiss All" used in client-side Alerts is not localized or localizable
  • [TAP5-1878] - Default for parameter visible of component KaptchaField should change from false to true
  • [TAP5-1889] - Improve integration test execution speed
  • [TAP5-1905] - Not serializable: org.apache.tapestry5.tree.DefaultTreeExpansionModel - can't persist sessions across cluster
  • [TAP5-1914] - Alerts needs a parameter to show/hide "Dismiss all"
  • [TAP5-1940] - Typo in interface LocalizationSetter.setNonPeristentLocaleFromLocaleName()
  • [TAP5-1970] - JSON API improvements
  • [TAP5-1987] - ProgressiveDisplay update parameter as a symbol
  • [TAP5-1990] - Link interface should be more fluid to allow setting of anchor, etc., more concisely
  • [TAP5-1993] - Support returning an URL for an ajax request
  • [TAP5-1996] - Add Severity.SUCCESS enum for alerts
  • [TAP5-2013] - "Parameters" table often too wide in component javadocs
  • [TAP5-2022] - Add PropertyAccess.getAnnotation
  • [TAP5-2035] - Make it easier to choose another slf4j backend
  • [TAP5-2039] - Element.attribute() should treat "class" specially, appending the new value to the old
  • [TAP5-2046] - Should use OperationTracker to track type of request (ajax component event, traditional component event, page render request)
  • [TAP5-2050] - Grid sort icons should be added via CSS instead of rendering an embedded img element
  • [TAP5-2051] - Fix Plastic objects memory leaks
  • [TAP5-2058] - Support X-Forwarded-Proto to identify a Request secured (https)
  • [TAP5-2063] - Add support for multivalued parameters in Link
  • [TAP5-2102] - Allow supplying EntityManager properties via TapestryPersistenceUnitInfo
  • [TAP5-2131] - Update Javadoc CSS to reflect changes in the html generate by JDK7

New Feature

  • [TAP5-999] - Implement an agnostic tapestry.js layer + adapters to allow developers to switch from prototype to jquery
  • [TAP5-1168] - Add @Operation annotation that can be used to automatically track a component or service method invocation using the OperationTracker
  • [TAP5-1794] - Allow for configuring Selenium-based integration tests using an annotation
  • [TAP5-1807] - In development mode, Tapestry should include a <meta> tag to identify the active page
  • [TAP5-1809] - Option to render a particular page instance in its current state (without page activation)
  • [TAP5-1833] - Merge functionality of Tynamo.org's tapestry-exceptionpage module with the built-in ExceptionHandler
  • [TAP5-1843] - Allow all of the properties of the Messages services to be accessed
  • [TAP5-1857] - Add a SubmitMode (for Submit and LinkSubmit components) for unconditionally submitting the form
  • [TAP5-1944] - Add basic integration between Tapestry and Clojure
  • [TAP5-2055] - Polish translations
  • [TAP5-2065] - Introduce support for MongoDB access as a service
  • [TAP5-2094] - E-Tags support

Task

  • [TAP5-1518] - Remove ClassFactory / ClassFab / Javassist Dependency
  • [TAP5-1780] - Upgrade Selenium dependency to 2.14.0
  • [TAP5-1790] - Update Tapestry build to create a binary release archive (in addition to a source and javadoc archive)
  • [TAP5-1792] - Upgrade Tapestry/Spring integration to Spring 3.1.0.RELEASE
  • [TAP5-1810] - Remove deprecated "suppress redirects from action" code
  • [TAP5-1826] - Setup the Sonar plugin to let analysis.apache.org analyze the source base
  • [TAP5-1852] - Upgrade Plastic to use ASM 4.0
  • [TAP5-1909] - Update Underscore JavaScript library to latest version 1.3.3
  • [TAP5-1922] - Upgrade Hibernate dependency to latest, Hibernate 4.1.2.Final
  • [TAP5-1965] - Replace use of Request.getContextPath() with a symbol defined at application startup
  • [TAP5-1989] - Upgrade bundled Prototype to version 1.7.1

Thursday, July 18, 2013

Closing in on Tapestry 5.4

We're getting very close to a beta release of Tapestry 5.4. I've spent the last couple of weeks converting an existing client's very large application (120+ pages, tons of components, lots of client-side JavaScript) from 5.3 to the current 5.4 alpha, and it's been a lot of work ... but also a lot of reward.

Before the switch, the application was already using jQuery, Bootstrap, and a limited amount of Backbone; under Tapestry 5.3, that means that Prototype and Scriptaculous were also in the mix ... and there were quite a few conflicts between Prototype and jQuery.

It's not just that Prototype and jQuery both want to use window.$ as their point of entry; the conflicts can be deeper and more subtle. For example, I had a nasty time with a Boostrap modal dialog that didn't dismiss correctly. After a lot of debugging, I found out that jQuery treats a method on an element as an event handler for the event with the matching name; Bootstrap was triggering a hide event, and jQuery was invoking the hide() Element method added by Prototype. That kind of thing has been my life under the mixed stack.

Tapestry 5.4 manages this better; and with the new abstraction layer it is possible to turn off Prototype completely and use just jQuery. That removes these conflicts ... and also speeds things up, but reducing the amount of JavaScript code downloaded to the client.

The transition from 5.3 to 5.4 was a chance to review and improve all that code. Here's a few observations.

The application has some very complex client-side forms; well beyond the abilities of Tapestry to manage using the FormInjector and FormFragment components. Instead, we use Backbone is a kind of hybrid mode, where the Backbone Model or Collection is persisted to a hidden field, so that the data collected or edited is transmitted to the server as part of an over-all form submission.

Of course, this means creating a lot of content, including form control elements, on the fly. One of the main problem was integrating Tapestry's client-side validation for the newly created fields.

Under 5.3, this required examining and often hacking (or monkey patching) the Tapestry client-side code, which create FormEventManager and FieldEventManager objects for each form or form control element ... and there was lots of hackery to tap into the form's submission cycle to perform validations.

Under 5.4 it is much easier, when creating the new fields, we can specify the desired validations via attributes:

The data-validation attribute indicates the field participates in validation; it will have events triggered on it when the enclosing form submits. The t5/core/validation module supplies the code that handles fields with data-optionality="required" and ensures that a value is provided, displaying the data-required-message if the field is left blank.

That's the pattern throughout 5.4; data- attributes are used to identify where special behavior is desired, and a well documented system of events is used handle the processing of the behavior.

Very little on the server side changed with the upgrade to 5.4; but revisiting and rewriting all that JavaScript was more than enough work!

In addition, it was an opportunity to convert all the code to CoffeeScript. CoffeeScript is my preferred way of writing client-side code: it is more concise and readable than JavaScript, but the compiled output is still quite readable. It also has great features like the block strings (from the example code above) which largely eliminates the need for a separate template engine.

Working in CoffeeScript and Tapestry so much led to some quick evolution of the CoffeeScript support built into Tapestry (in the optional tapestry-wro4j module). Initially, any change to any CoffeeScript file forced all CoffeeScript files to be recompiled; when using the sluggish Rhino JavaScript engine to run the CoffeeScript compiler, we could see 10 - 20 seconds per source file!

I made some improvements, adding a special development-mode in-memory cache that would only recompile the CoffeeScript if the underlying source file content had changed. Since application restarts are rare in Tapestry, this was sufficient. I eventually added a file-system cache so that compilation could be avoided, even over restarts of the application.

I also started using Less instead of CSS. Less is a meta-language that compiles down to CSS, much like the relationship between CoffeeScript and JavaScript ... but I think even more dramatically. Compilation is pretty fast, based on the Less4J library.

I found that the out-of-the-box support for Less4J supplied by WRO4J was insufficient: it didn't do a great job with @import; a change to an imported source file didn't force a recompilation. I've addressed that with a custom (for Tapestry) wrapper that properly tracks dependencies.

So where does that leave us? There's still a huge amount of work to do before Tapestry 5.4 is ready for release, but it's mostly fixing bugs and other rough edges. As too often happens, the reality of earning a living have made me postpone some of my ideas for a later release.

I think that Tapestry is aging, if not gracefully, then at least comfortably, into a growing age where rich, single-page applications built with Backbone, AngularJS, or something else are the norm, and not the exception. I'm the first to admit that Tapestry was designed for a simpler time when Ajax was seasoning, not the meat-and-potatoes of the application. There's a lot of baggage in there, particularly related to forms and form controls. Yet, even so, there's some amazingly useful parts to Tapestry that apply equally well to modern applications:

Asset Pipeline
Tapestry's asset pipeline does much of the work normally associated with a command-line build step, but does it at runtime, with the benefit of being live for development. Tapestry can not only compile files from one type to another (e.g., CoffeeScript to JavaScript), but can also aggregate many small files into a single combined file and pass it through a minimizer. Further, Tapestry builds URLs that incorporate a content checksum. This checksum means that we can have the browser aggressively cache the file (using both E-Tags and a far-future expires header) as any change to the file will change the checksum as well, resulting in what looks to the client like an entirely new resource. Further, Tapestry caches both the straight-up file, and the GZip compressed version of the file. All of this means that Tapestry provides not just great throughput by processing requests quickly, but also great performance by reducing the number of requests from clients as well.
Asynchronous Module Definition (AMD) Support
Tapestry 5.4 introduces direct support for AMD via RequireJS. At the minimum, this means that JavaScript can be loaded in parallel ... but the real benefit is how AMD encourages you to organize your code in a manageable way. In addition, Tapestry ensures that your modules can be JavaScript or CoffeeScript, and provides a simple way on the server to override any built-in modules provided by Tapestry, should the need arise. Better yet, Tapestry has a server-side API for invoking client-side functions exported by AMD modules; these is a very clean and very useful way to knit together server-side and client-side logic.
Superior Feedback and Exception Reporting
When working on other stacks, the thing I miss the most now is Tapestry's exception reporting. This is cleanly integrated in Tapestry, where a server side failure will result in a detailed HTML report on the client. For Ajax requests, this report is presented in a floating <iframe>. The wealth of information available from the server side and reported directly in the client makes developing complex applications with involved server-side interaction much easier.

As Tapestry 5.4 transitions to beta, and to an eventual final release, I'm still targeted on the future ... and I'm focused on making Tapestry be a great choice for single-page apps as well as traditional applications. My head is bubbling with ideas to make this happen.

Thursday, May 23, 2013

Once more ... feedback please!

You've probably heard about "Not Invented Here" syndrome: the drive among developers to create something of their own, rather than just use an off-the-shelf library or component. It's almost universally painted as a bad thing, a sign of immaturity, or even arrogance.

But there's a flip side to this: every bit of code ever written contains within it tradeoffs: speed versus maintainability is a common tradeoff that everyone has seen. Perhaps the code is insufficiently flexible in the face of real-world requirements, but is really well tested for what it does cover. These choices reflect the developer's principles applied to the code. In fact, it is rare for it to be an easy give-and-take between two simple goals; more likely, there's lots of conflicting goals in the code, in the requirements, and in the developer's head. "Not Invented Here" can also mean "Not Reflecting My Principles".

Tapestry has it own set of guiding principals: Simplicity, Consistency, Efficiency, and Feedback ... and as a reusable framework, Feedback is very important. Feedback may be the most important principle when things go wrong. A framework that obscures problems, through bad feedback, is a framework that shouldn't be used.

Which brings us back to "Not Invented Here". Only in an impossibly perfect world would there be some ideal blob of code out there, ready to be reused, with zero impedance mismatch issues. In fact, when you bring in other people's code, you are forced to mesh your goals and principals with theirs. In my case, I'm adding support to Tapestry for converting Less files to CSS, using WRO4J (Web Resource Optimizer for Java). They've been working on WRO4J for several years, it makes sense to reuse their code, and it would be arrogant to think I could whip something better together under any kind of time constraints.

In fact, it's actually been pretty smooth sailing ... until we tripped across a violation of Tapestry's Feedback principle. As soon as I tested an error case, where the Less source file was not valid I hit bad feedback. Can you spot what's wrong with this exception report?

Oops! Looks like someone didn't get the memo about the importance of toString(). See, that Feedback principal is important to me, but for the majority of developers, useful feedback is too often an afterthought. I don't want to single out WRO4J here ... I'm pretty disdainful about feedback in nearly all software: open source or proprietary.

So what are our options here?

  • Write our own wrappers around the Less Processor, and throw out WRO4J
  • Beg the WRO4J guys to implement a real toString(), and wait for the next release
  • Fork WRO4J in the short term, and hope they'll take a patch in the long term
  • Patch around this reporting problem

Obviously, we should find a way to patch the reporting problem; we don't want to throw out the baby with the bath water. Fortunately, Tapestry provides the necessary hooks to override how it presents objects inside the exception report; it's all about providing a mapping from a Java type to a matching implementation of ObjectRenderer. Because of Tapestry's IoC container, this is actually quite straight forward:

And with those changes, the exception is presented quite differently:

Well, those are actually the raw ANTLR parser errors, but at least that's enough to help you find location of the problem ... whereas, with the bad feedback, you would only know that there was an issue somewhere in your Less source file.

Friday, February 01, 2013

Crafting Code in Clojure

The other day, I was working on a little bit of code in Clojure, just touching up some exception reporting, when I was suddenly struck by one of the fundamental reasons that Clojure is so enjoyable to code in. Clojure is craftable: that is, in Clojure you have the option to craft at your code to make it more concise, easier to read, and easier to maintain. That is not the case for all, or perhaps even most, programming languages.

In my case, I was constructing an error message where I needed to convert the keys of two maps into a comma-seperated string (I don't like to say "you guessed wrong" without saying "here's what you could have said").

What I want my code to do is easily expressed as an informal recipe:

  • Extract all the keys from both maps
  • Remove any duplicates
  • Convert the keys to strings
  • Sort the strings into ascending order
  • Build and return one big string, by concatinating all the key strings, using ", " as a seperator
  • Return "<none>" if both maps are empty

If I was writing this in Java, it would look something like this:

There's enough looping and conditionals in this code (along with tip-toeing around Java Generics) that its easier to look at its test specifiction (written in Spock) to see what it is supposed to do:

The first pass at a Clojure version is already simpler than the Java version ...

I couldn't resist using the clojure.string/join function, rather than building the string directly (which would be slightly tedious in Clojure). In many ways, this is a lot like the Java version; we're using let to create local symbols for each step in the process in just the same way that the Java version defines local variables for each step.

However, there's room for improvement here. Let's start to craft.

For example, let's assume that both maps being empty is rare, or at least, that the cost of sorting an empty list is low (it is!). Our code becomes much more readable if we merge it into one big let:

Now we're getting somewhere. I think this version makes it much more clear what is going on that the prior Clojure version, or the Java version.

However, if you've written enough code, you know one of the basic rules of all programming: names are hard. Anything that frees you from having to come up with names is generally a Good Thing. In Java, we have endless names: not just for methods and variables, but for classes and interfaces ... even packages. Long years of coding Java has made me dread naming things, because names never quite encompass what a thing does, and often become outdated as code evolves.

So, what names can we get rid of, and how? Well, if we look at the structure of our code, we can see that each step creates a value that is passed to the next expression as the final parameter. So all-keys is passed as the last parameter of the (map) expression, resulting in key-names, and then key-names is passed as the last parameter of the (sort) expression. In fact, ignoring the empty check for a moment, the sorted-names value is passed to the (s/join) expression as the last parameter as well.

This is a very important concept in Clojure; you may have heard people trying to express that you code in Clojure in terms of a "flow" of data through a series of expressions. We'll, you've just seen a very small example of this.

In fact, it is no simple coincidence that the last parameter is so important; this represents a careful and reasoned alignment of the parameters of many different functions in clojure.core and elsewhere, to ensure that flow can be passed as that final parameter, because it becomes central to the ability to combine functions and expressions together with minimal fuss.

We can use the ->> macro (pronounced "thread last") to rebuild our flow without having to come up with names for each step:

The ->> macro juggles our expressions into an appropriate order; without it we'd have to deeply nest our expressions in an unreadable way: (sort (map str (set (concat (keys map1) (keys map2))))). Even with a short flow of expressions, that's hard to parse and interpret, so ->> is an invaluable and frequently used tool in the Clojure toolbox.

We can continue to craft; the first expression (that builds the set from the keys), can itself be broken apart into a few smaller steps. This is really to get us ready to do something a bit more dramatic:

This is getting ever closer to our original recipe; you can more clearly see the extraction of keys from the maps before building the set (which is only used to ensure key uniqueness), before continuing on to convert keys from objects to strings, sort them, and combine the final result.

In fact, we're going to go beyond our original brief, and support any number of input maps, not just two:

The mapcat function is like map, but expects that each invocation will create a collection; mapcat concatinates all those collections together ... just what we want to assemble a collection of all the keys of all the input maps.

At this point, we don't have much more to go ... but can we get rid of the sorted-names symbol? In fact, we can: what if part of our flow replaced the empty list with a list containing just the string "<none>"? It would look like this:

... and that's about as far as I care to take it; a clean flow starting with the maps, and going through a series of expressions to transform those input maps into a final result. But what's really important here is just how fast and easy it is to start with an idea in Clojure and refine it from something clumsy (such as the initial too-much-like-Java version) into something elegant and surgically precise, such as the final version.

That's simply not something you can do in less expressive languages such as Java. For example, Tapestry certainly does quite a number of wonderful things, and supports some very concise and elegant code (especially in green code) ... but that is the result of organizing large amounts of code in service of specific goals. We're talking tons of interfaces, a complete Inversion-Of-Control container, and runtime bytecode manipulation to support that level of conciseness. That's the hallmark of a quite consequential framework.

That isn't crafting code; that's a big engineering effort. It isn't local and invisible, it tends to be global and intrusive.

In Java, your only approach to simplifying code in one place is build up a lot of complexity somewhere else.

That is simply not the case in Clojure; by adopting, leveraging, and extending the wonderful patterns already present in the language and its carefully designed standard library, you can reach a high level of readability. You are no longer coding to make the compiler happy, you are in control, because the Clojure languge gives you the tools you need to be in control. And that can be intoxicating.

The source code for this blog post is available on GitHub.

Wednesday, January 30, 2013

Red Code, Green Code, My Code, Your Code

I had a very odd interchange with my friend Merlyn over lunch; he started talking about red code vs. green code (in the context of supporting both callbacks and promises inside NodeJS). At first I thought he was referring to code coverage of those lines of code ... the implication being that supporting multiple paths of execution may lead to laziness in testing every possible path (with some code going "red" in the code coverage report).

But that wasn't it at all: "red" code referred to framework code, "green" code referred to end-user code, leveraging the framework.

Odder still, Merlyn insisted that he first heard this term from ... me, a few years ago. That's what being in the baby-raising camp can do to you ... I know longer have any idea of what I've said or thought in the past. Actually, this isn't new for me ... I've always had a very vague memory for anything not code.

In any case, this red vs. green terminology is a useful concept ... certainly I move the earth in Tapestry's red code to make end-user's green code as simple as possible.

So, I want to do three things:

  • It's definitely worth reflecting on the fact that all code is not created equal, and that long-lived, reusable code ("red") will inevitably grow in complexity to a level that would not be acceptable in client ("green") code.
  • Let's promote this term, because it is so handy!
  • If I didn't create the term myself, let's track down the originator and thank them!

Wednesday, January 02, 2013

Tapestry 5.4: jQuery Support now in place

I've spent the last several months significantly reworking Tapestry 5's client-side JavaScript support, in an effort to move away form the tight binding to Prototype. After all of that refactoring, recoding, repositioning, and just-plain-hacking, I was able to do most of the job of introducing jQuery support in just a few hours, yesterday and today.

I'll be producing another preview release pretty soon, or you can get the lastest from Tapestry's master branch.

Currently, the code to switch over from using Prototype to jQuery looks like this (it will get simpler soon):

The first part overrides the provider to be "jquery" (the default provider is "prototype"). In Tapestry terms, the "infastructure framework" provides the APIs for DOM queries, DOM manipulation, and Ajax request handling. If you don't like jQuery, you can easily create your own provider for your favorite framework.

Part of 5.4 is trying to manage all these different compatibility issues at a slightly higher level than configuration symbols; that's the Compatibility service with its Traits. Those two traits disable Scriptaculous support (which isn't needed by jQuery, and won't work without Prototype), and disables support for Tapestry 5.3-style initializers.

Underneath the covers, the way the switch between Prototype and jQuery works is quite simple:

The ModuleManager service is the code that handles requests for modules from the client. It has a configuration that is used to handle edge cases, such as treating traditional JavaScript libraries as if they were AMD modules. The code above sets up a server-side override for the module t5/core/dom. All the Tapestry client-side modules use dom; none of them uses Prototype or jQuery directly (except for a couple that access Twitter Bootstrap functionality).

These contributions ensure that when the client-side requests the t5/core/dom module, what will be sent back will be either the Prototype-specific implementation, or the jQuery-specific implementation. Without this contribution, we'd see a 404 error when the dom module was requested. Instead, the client-side doesn't have any idea that dom is special.

The primary job of the dom module is to wrap DOM elements inside a new object, thereby providing a new API that allows various kinds of manipulation, as well as listening to events, or triggering them. The API is a bit of a mashup between Prototype and jQuery, but leans pretty heavily towards jQuery-style operations and naming. dom's secondary job is to support Ajax requests, plus adding event handlers to the document object.

In practice, this can be very concise and readable (partially, thanks to CoffeeScript):

In this listing (which supports some of the behavior of the Tapestry Zone component), dom holds the export from module t5/core/dom; it waits for the events.zone.update event to be triggered; the callback is invoked with this set to the wrapper around the element where the event was triggered.

The event here is also a wrapper; its a minimal mix of Prototype and jQuery: I like the memo property from Prototype, so that's present. The event handler triggers a pair of before and after events, and updates the content of the zone. Why the before and after events? By default they do nothing, but it would be simple to add handlers to perform some kind of animation when content is added.

In any case, because all Tapestry client-side modules code against this API, they don't know or care whether the page has loaded Prototype, jQuery, or something else. If you are writing Tapestry components for reuse, coding against the dom API will help ensure that your component will work correctly in all kinds of Tapestry applications. However, when coding an application,. you reserve the right to choose what the infrastructure framework should be: you should feel free to use the infrastructure framework directly ... unless you find the dom API to be easier.