/**
* 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";
}
}