/** * Copyright 2014 55 Minutes (http://www.55minutes.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package fiftyfive.wicket.model; import fiftyfive.wicket.util.HtmlUtils; import org.apache.wicket.Component; import org.apache.wicket.Page; import org.apache.wicket.model.AbstractReadOnlyModel; import org.apache.wicket.model.IModel; import org.apache.wicket.model.StringResourceModel; import org.apache.wicket.request.mapper.parameter.PageParameters; import org.apache.wicket.util.lang.Args; /** * Selects an appropriate localized string based on whether a count value * is zero, one or greater than one. This makes it easier to produce singluar * and plural phrases. * <p> * For example, consider the case where we would like to set one of these * three feedback messages. Notice how the phrasing is slightly different * depending on the number of items. Furthermore in the latter two examples * we would like to embed a hyperlink to a bookmarkable page. * <ul> * <li>There are no friends in your network.</li> * <li>There is <u>one friend</u> in your network.</li> * <li>There are <u>5 friends</u> in your network.</li> * </ul> * <p> * Normally in Wicket you would accomplish this using tedious if/else * statements and string concatenation, conditionally visible * fragments/containers, or the incredibly arcane syntax of Java's * {@link java.text.ChoiceFormat ChoiceFormat} (and note that you'd have to * carefully escape the "<" and ">" if you want to include HTML markup * as we need for the hyperlinks in this example). All of these * approaches are clumsy, and it would be hard for a non-programmer to localize * or edit the strings. * <p> * With CountMessageModel there is a much easier way: simply declare three * separate localizable strings in the properties file of your page or panel. * <pre class="example"> * friends.zero = There are no friends in your network. * friends.one = There is <a href="${href}">one friend</a> in your network. * friends.many = There are <a href="${href}">${count} friends</a> in your network.</pre> * <p> * And your Java would be: * <pre class="example"> * new CountMessageModel("friends", pageOrPanelObject, numFriends) * setLink(FriendsPage.class);</pre> * <p> * CountMessageModel selects the appropriate localized string depending on the * value of {@code numFriends}, and subtitutes values for * <code>${href}</code> and <code>${count}</code> automatically. * <p> * You can now use this model in a label (also consider * {@link fiftyfive.wicket.basic.CountLabel CountLabel} as a shortcut for this * common use case), or call {@link #getObject getObject()} to get the * interpolated string value for use in a feedback message. * * @since 2.0 */ public class CountMessageModel extends AbstractReadOnlyModel<String> { private Component component; private StringResourceModel stringModel; private IModel<? extends Number> countModel; private Class<? extends Page> linkPage; private PageParameters linkParams; /** * Constructs a CountMessageModel that will choose the appropriate * localized string from a properties file. * * @param messageKey The key that will be consulted in the properties file. * The key will have ".zero", ".one" or ".many" appended * to it depending on the value of the {@code count}. * @param component The Wicket component that will be used for finding * the properties file. Usually this is the page or panel * where you plan to use the model. * @param count The number that will be used in the message. This will * be used to substitute any <code>${count}</code> expressions * in the message. */ public CountMessageModel(String messageKey, Component component, IModel<? extends Number> count) { super(); Args.notNull(messageKey, "messageKey"); Args.notNull(component, "component"); Args.notNull(count, "count"); this.component = component; this.countModel = count; this.stringModel = new StringResourceModel( messageKey + "${resourceSuffix}", component, new AbstractReadOnlyModel<CountMessageModel>() { public CountMessageModel getObject() { return CountMessageModel.this; } } ); } /** * Detaches the count model passed into the constructor and other * internal state. */ @Override public void detach() { this.countModel.detach(); this.stringModel.detach(); super.detach(); } /** * Returns the message after all interpolation has been performed. */ public String getObject() { return this.stringModel.getObject(); } /** * Sets the bookmarkable page that will be used to generate a link within * the message. A special <code>${href}</code> variable will be exposed * for use within the message. * * @return {@code this} to allow chaining */ public CountMessageModel setLink(Class<? extends Page> page) { return setLink(page, null); } /** * Sets the bookmarkable page class and page parameters that will be used * to generate a link within the message. A special <code>${href}</code> * variable will be exposed for use within the message. * * @return {@code this} to allow chaining */ public CountMessageModel setLink(Class<? extends Page> page, PageParameters params) { this.linkPage = page; this.linkParams = params; return this; } /** * Returns the current value of the count model. This is exposed as * <code>${count}</code> within the message. */ public int getCount() { Number num = this.countModel.getObject(); return num != null ? num.intValue() : 0; } /** * Returns the URL to the bookmarkable page specified in a previous call * to {@link #setLink(Class,PageParameters) setLink()}. This is exposed as * <code>${href}</code> within the message. If {@code setLink()} was not * called, then this value will be {@code null}. */ public CharSequence getHref() { if(this.linkPage != null) { return HtmlUtils.escapeAttribute( this.component.urlFor(this.linkPage, this.linkParams) ); } return null; } /** * Returns either ".zero", ".one", or ".many" depending on the current * value of the count model. */ public String getResourceSuffix() { int count = getCount(); if(0 == count) return ".zero"; if(1 == count) return ".one"; return ".many"; } }