Introduction
JSF supports parameter substitution out-of-the-box, but only so far as using numbered indices, which aren’t insanely easy to read or remember. For example,
1 2 3 4 |
<h:outputFormat value="{0} comes out as {1}"> <f:param value="This"/> <f:param value="That"/> </h:outputFormat> |
This outputs “This comes out as that.” – fine for your everyday message, but when combined with DB-driven resource bundles and dynamic parameters, this provides a world of difficulties within the opportunities provided by localisation.
With thousands of sentences containing 0’s and 1’s comes great potential for trouble. It would be preferable to refer to the parameters by name – this makes them more resilient to change, especially in terms of changing the order of the parameters, and makes the sentences easier to read.
1 2 3 4 |
<h:outputText value="{theFirst} comes out as {theSecond}"> <f:param name="theFirst" value="This"/> <f:param name="theSecond" value="That"/> </h:outputFormat> |
It turns out we can accomplish that fairly easily, by implementing a new Renderer for the outputText tag. This renderer can actually be applied to both the outputText / outputFormat tags, but I’m using it on the straight outputText for simplicity’s sake.
Renderer
To register our new renderer, let’s call it za.co.knowles.renderer.CustomOutputTextRenderer , all we have to do is add a section to the faces-config.xml .
1 2 3 4 5 6 7 8 |
<render-kit> <renderer> <display-name>Custom outputText renderer</display-name> <component-family>javax.faces.Output</component-family> <renderer-type>javax.faces.Text</renderer-type> <renderer-class>za.co.knowles.renderer.CustomOutputTextRenderer</renderer-class> </renderer> </render-kit> |
This automatically becomes the renderer for all outputText tags.
Our renderer extends the supplied Renderer class, and all we need to override is the getEndTextToRender method. This method is called just before the text is rendered to the screen, so if we modify the text with our substitution and call the super-class method then our ends are met with minimal fuss.
1 2 3 4 5 6 7 8 9 |
@Override protected void getEndTextToRender(FacesContext context, UIComponent component, String currentValue) throws IOException { List<Parameter> params = getParameterComponents(component); String finalString = Substitute.substitute(currentValue, params); super.getEndTextToRender(context, component, finalString); } |
We extract the parameters from the original UIComponent , then pass them on to our substitution code. To extract the parameters, we iterate through the children and extract their names and values, then put them in a POJO we’ve created called Parameter that stores names and values.
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 |
/** * Extract the list of parameters from the children of the component * * @param component the UI component we are examining * @return the list of Parameters in order. */ protected static List<Parameter> getParameterComponents(final UIComponent component) { if (component.getChildCount() == 0) { return new ArrayList<>(0); } final List<Parameter> params = new ArrayList<>(component.getChildCount()); for (UIComponent child : component.getChildren()) { if (child instanceof UIParameter) { String name = ((UIParameter) child).getName(); String value = ((UIParameter) child).getValue() != null ? ((UIParameter) child).getValue().toString() : null; params.add(new Parameter(name, value)); } } return params; } |
At this point we have the original text (potentially with {‘s and }’s indicating substitution) and we have a range of (or no) parameters. This is now straight String manipulation, decoupled from anything JSF specific – we extract the parameters with a RegEx, then replace them with the parameter values we pass in.
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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
public class Substitute { /** * Attempt to substitute the supplied parameters into the base text, by name or index. * * @param baseText text containing substitutions * @param params parameters * @return the substituted text */ public static String substitute(String baseText, List<Parameter> params) { if (params == null || params.isEmpty()) { return baseText; } String finalString = baseText; Set<String> matches = getReplacements(baseText); for (String match : matches) { String result = getMatch(match, params); if (result == null) { continue; } String markUp = markUp(match); finalString = finalString.replaceAll(markUp, result); } return finalString; } /** * Mark up the string so that it can be used within a regex with the { and } symbols indicating repetition. * @param match un-marked up string * @return marked up string */ private static String markUp(String match) { return match.replaceAll("\\{", "\\\\{").replaceAll("\\}", "\\\\}"); } /** * See if the parameters contain a match for the supplied text. * @param match value to be substituted include braces * @param params extracted parameters * @return the final value, or null if there was no match */ private static String getMatch(String match, List<Parameter> params) { String value = match.substring(1, match.length() - 1); for (Parameter param : params) { String name = param.getName(); if (name != null && name.equalsIgnoreCase(value)) { return param.getValue(); } } try { int index = Integer.parseInt(value); return params.get(index).getValue(); } catch (Exception ex) { return null; } } /** * Extract all of the items that should be replaced from the String * @param baseString the base string * @return the set of potential replacements */ private static Set<String> getReplacements(String baseString) { Pattern pattern = Pattern.compile("\\{[^\\{]+\\}"); Matcher matcher = pattern.matcher(baseString); Set<String> replacements = new HashSet<>(); while (matcher.find()) { String bit = baseString.substring(matcher.start(), matcher.end()); replacements.add(bit); } return replacements; } } |
Leave a Reply