Indexed Properties
This how-to will cover how to use both numeric indexed properties and string-indexed or mapped properties. Indexed properties are hard to define, but easy to show by example. Imagine we wanted to edit information about a bug on a page. We might create a form with fields like "bug.name", "bug.description", "bug.priority" etc. Now imagine we want to edit multiple bugs at once, on the same page. We could write a form (and an ActionBean) with an awful lot of properties, like "bug1.name", "bug2.name", etc. but that's way too much work. So instead we use a notation that is understood by Stripes (and lots of other tools), and call our form fields bug[0].name and bug[1].name and so on.
To accomplish this there are two aspects to consider: How to generate the names of the fields on the form and how to code an ActionBean that can receive them. Both are actually quite simple.
Numeric Indexed Properties on the JSP
Essentially constructing the names on the JSP is up to you. This is good and bad news. It means slightly more work for you (the developer) - but it really is only a tiny bit more work. On the upside it means a lot more flexibility. Some of the advantages of this approach are:
- Indexes can be embedded anywhere inside a property name
- A single property can have multiple indexing (e.g. bugs[0].watchers[3].name)
- The source of the index can be anything - allowing interopation with any looping tag that gives you access to it's index, including the c:for* tags, the display:table tag etc.
The following is an example fragment of a JSP using indexed properties (it is a simplified version of a page from the Bugzooky Sample Application):
<stripes:form action="/bugzooky/EditPeople.action"> <table class="display"> <tr> <th>ID</th> <th>Username</th> <th>First Name</th> <th>Last Name</th> <th>Email</th> </tr> <c:forEach items="${personManager.allPeople}" var="person" varStatus="loop"> <tr> <td> ${person.id} <stripes:hidden name="people[${loop.index}].id" value="${person.id}"/> </td> <td> <stripes:text name="people[${loop.index}].username" value="${person.username}"/> </td> <td> <stripes:text name="people[${loop.index}].firstName" value="${person.firstName}"/> </td> <td> <stripes:text name="people[${loop.index}].lastName" value="${person.lastName}"/> </td> <td> <stripes:text name="people[${loop.index}].email" value="${person.email}"/> </td> </tr> <c:set var="newIndex" value="${loop.index + 1}" scope="page"/> </c:forEach> <%-- And now, an empty row, to allow the adding of new users. --%> <tr> <td></td> <td></td> <td> <stripes:text name="people[${newIndex}].username"/> </td> <td> <stripes:text name="people[${newIndex}].firstName"/> </td> <td> <stripes:text name="people[${newIndex}].lastName"/> </td> <td> <stripes:text name="people[${newIndex}].email"/> </td> </tr> </table> <div class="buttons"> <stripes:submit name="Save" value="Save Changes"/> </div> </stripes:form>
It's pretty easy with EL. Using the c:forEach tag the varStatus (which contains the index of the current iteration) is assigned to the name loop. Then, in the form fields the loop.index is inserted into the form field name using EL. E.g. people[${loop.index}].username will translate at runtime into people[0].username, people[1].username etc.
Numeric Indexed Properties using Lists in the ActionBean
This is an area where Stripes really shines. The relevant piece of the ActionBean which corresponds to the above form is:
private List<Person> people; @ValidateNestedProperties ({ @Validate(field="username", required=true, minlength=3, maxlength=15), @Validate(field="firstName", required=true, maxlength=25), @Validate(field="lastName", required=true, maxlength=25), @Validate(field="email", mask="[\\w\\.]+@[\\w\\.]+\\.\\w+") }) public List<Person> getPeople() { return people; } public void setPeople(List<Person> people) { this.people = people; }
As you can see, all that is involved is declaring a List (or implementation thereof) property, and providing a getter and setter for the List that have the appropriate generic types. Stripes will introspect your bean at run time and figure out that the "people" property is a list property and will fill in the list for you, instantiating person objects and binding properties to them. You don't even have to instantiate your list - Stripes will do that for you too.
Sets and other non-indexed collections
Stripes explicitly does not support indexed properties for Sets and other collections where there either is no guaranteed ordering, or the ordering is intrinsic to the items in the collection (e.g. SortedSets) - i.e. collections without external indexing. The reason for this is that it is not possible to guarantee consistent ordering in these collections - adding or changing an item can fundamentally alter the ordering of the collection and make numeric indexing invalid, and even dangerous.
For this reason Stripes does not, and will not, support use of Sets as indexed properties directly. Having read the above, if you still want to use a Set the recommended solution is to initialize either a Map (preferably keyed by an id field) or a List in your ActionBean, populate it with the items from the Set and provide a pair of accessor methods for the Map/List. The Map solution is safer since it allows you to access by a stable key as opposed to a numeric index. If you prefer the List approach is it recommended to use Collections.unmodifiable() to ensure that no items can be added or removed from the List, thus ensuring stable ordering.
Validation of Indexed Properties
You might have noticed that the getPeople() method above was annotated, not with validations that make sense for a list, but with validations that make sense for each item in the list - in this case a Person object. Stripes performs validations for each index or row in the list as if it were a regular property. This is true for both numeric/List indexed properties and Map properties, regardless of whether the List/Map is a property of the ActionBean or a sub-property nested within a property of the ActionBean.
For example, imagine we had a page to edit a Person and that the Person has a property 'pets' which is a List of Pet objects, and each Pet has a List of Nicknames. To validate these properties we write the validations without any indexing - just as if they were regular properties:
@ValidateNestedProperties({ @Validate(field="phoneNumber", required=true), @Validate(field="pets.age", required=true, maxvalue=100), @Validate(field="pets.nicknames.name", required=true, maxlength=50), }) private Person person;
There is, however, one big change in validation. Required field validations are only applied if at least one value with the same index was supplied. To understand this it is easier to think of indexed properties as a mechanism for creating multi-row forms. And this change means that rows in the form that are completely empty are ignored.
For example, in the Bugzooky sample application the Administer Bugzooky page shows the form we have been using as an example above. Look back at the JSP example and you'll notice that an extra row is put in at the end, with no values in it. If the user does not enter anything in this row, the browser will submit it, but because all the fields are empty, Stripes will ignore it and raise no errors. However, if the user entered any field, say username, and left the others blank then a host of validation error messages would show up.
Indexed Properties with Maps (and arbitrary key types)
Just like you can use numeric indices to construct Lists in ActionBeans, you can also construct Maps using any type that Stripes knows how to convert to (e.g. Numbers, Strings, Dates, custom types etc.). Stripes will use the generic information present on the getter/setter for the Map to determine both the type of the key, and the type of the value.
The syntax is very similar to the List example above (in fact with numbers, it is identical). The value between the square brackets should be quoted if it is a String or Character (single and double quotes are both accepted). Otherwise the value can be provided without quotes.
The following example shows how you might use indexed properties with a Map to capture a large set of options or parameters with as little effort as possible. In this case the Map key is the String name of the parameter and the Map value is the numerical value of the parameter.
<stripes:form ... > ... <table> <c:forEach items="${toolParams}" var="toolParam"> <tr> <td>${toolParam.name}:</td> <td><stripes:text name="toolParameters['${toolParam.name}']"/></td> </tr> </c:forEach> </table> ... </stripes:form>
The relevant section of the ActionBean:
private Map<String,Double> toolParameters; public Map<String,Double> getToolParameters() { return toolParameters; } public void setToolParameters(Map<String,Double> toolParameters) { this.toolParameters = toolParameters; }
Advanced Indexed Properties
There are a few things worth knowing about how Stripes handles indexed properties:
- Map keys can be any type that Stripes can convert to (either because there is a registered TypeConverter for the type, or because it has a usable public String constructor)
- The items in Lists and values in Maps can be of any type Stripes can convert to, OR any complex type with nested properties
- Stripes will happily instantiate Lists and Maps for you (including SortedMaps)
- Stripes will happily instantiate complex types in Lists or Maps in order to set nested properties, as long as there is a public, no-arg constructor
- You can chain indexed properties
The last point is worth explaining in more detail. What is means is that you can have ActionBean properties that looks like this:
public Map<Date,List<Appointment>> getAppoinments() { return appointments; } public void setAppoinments(Map<Date,List<Appointment>> appointments) { this.appointments = appointments; }
and have input fields that look like this:
Note: <stripes:text name="appointments[${date}][${idx}].note"/>