JSF 2.X UI Component use of ValueExpression - jsf-2

I am trying to build a simple component to understand how and why JSF 2.X works the way it does. I have been using the newer annotations and have been trying to piece together a clear example.
So I have built my component and deployed it in a xhtml file as follows:
<kal:day value="K.Day" title="Kalendar" model="#{kalendarDay}"/>
The within the UIComponent I do the following:
ValueExpression ve = getValueExpression("model");
if (ve != null)
{
System.out.println("model expression "+ve.getExpressionString());
model = (KalendarDay) ve.getValue(getFacesContext().getELContext());
System.out.println("model "+model);
}
The expression "#{kalendarDay}" is correctly displayed indicating that the value has been successfully transmitted between the page and the component. However the evaluation of the expression results in "null".
This seems to indicate that the backing bean is unavailable at this point, although the page correctly validates and deploys. So I am 95% certain that the bean is there at run time.
So perhaps this is a phase thing? Should I be evaluating this in the decode of the renderer and setting the value in the attributes map? I am still a little confused about the combination of actual values and value expressions.
So my question is where should I fetch and evaluate the valueExpression for model and should I store the result of the evaluation in the UIComponent or should I simply evaluate it every time?
SSCCE files below I think these are the only required files to demonstrate the problem
Bean Interface -----
/**
*
*/
package com.istana.kalendar.fixture;
import java.util.Date;
/**
* #author User
*
*/
public interface KalendarDay
{
public Date getDate();
}
Bean Implementation ---
/**
*
*/
package com.istana.kalendar.session.wui;
import java.util.Calendar;
import java.util.Date;
import javax.ejb.Stateful;
import javax.inject.Named;
import com.istana.kalendar.fixture.KalendarDay;
/**
* #author User
*
*/
#Named ("kalendarDay")
#Stateful
public class KalKalendarDay
implements KalendarDay
{
private Calendar m_date = Calendar.getInstance();
/* (non-Javadoc)
* #see com.istana.kalendar.fixture.KalendarDay#getDate()
*/
#Override
public Date getDate()
{
return m_date.getTime();
}
}
UIComponent ---
/**
*
*/
package com.istana.kalendar.fixture.jsf;
import javax.el.ValueExpression;
import javax.faces.component.FacesComponent;
import javax.faces.component.UIOutput;
import com.istana.kalendar.fixture.KalendarDay;
/**
* #author User
*
*/
#FacesComponent (value=UIDay.COMPONENT_TYPE)
public class UIDay extends UIOutput
{
static final
public String COMPONENT_TYPE = "com.istana.kalendar.fixture.jsf.Day";
static final
public String COMPONENT_FAMILY = "com.istana.kalendar.fixture.jsf.Day";
private KalendarDay m_model;
private String m_title;
#Override
public String getRendererType()
{
return UIDayRenderer.RENDERER_TYPE;
}
#Override
public String getFamily()
{
return COMPONENT_FAMILY;
}
public KalendarDay getModel()
{
KalendarDay model = (KalendarDay) getStateHelper().eval("model");
System.out.println("model "+model);
return model;
}
public void setModel(KalendarDay model)
{
getStateHelper().put("model",model);
}
public String getTitle()
{
return (String) getStateHelper().eval("title");
}
public void setTitle(String title)
{
getStateHelper().put("title",title);
}
}
UIComponentRenderer ---
/**
*
*/
package com.istana.kalendar.fixture.jsf;
import java.io.IOException;
import javax.el.ValueExpression;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.render.FacesRenderer;
import javax.faces.render.Renderer;
import com.istana.kalendar.fixture.KalendarDay;
/**
* #author User
*
*/
#FacesRenderer (componentFamily = UIDay.COMPONENT_FAMILY
,rendererType = UIDayRenderer.RENDERER_TYPE
)
public class UIDayRenderer extends Renderer
{
static final
public String RENDERER_TYPE = "com.istana.kalendar.fixture.jsf.DayRenderer";
#Override
public void encodeBegin (FacesContext context,UIComponent component)
throws IOException
{
UIDay uic = (UIDay) component;
ResponseWriter writer = context.getResponseWriter();
writer.startElement("p", uic);
/*
* This is the call that triggers the println
*/
writer.write("Day - "+uic.getModel().getDate());
}
#Override
public void encodeEnd (FacesContext context,UIComponent component)
throws IOException
{
ResponseWriter writer = context.getResponseWriter();
writer.endElement("p");
writer.flush();
}
}
kalendar.taglib.xml ---
<facelet-taglib
id="kalendar"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facelettaglibrary_2_0.xsd"
version="2.0"
>
<namespace>http://istana.com/kalendar</namespace>
<tag>
<tag-name>day</tag-name>
<component>
<component-type>com.istana.kalendar.fixture.jsf.Day</component-type>
</component>
</tag>
</facelet-taglib>

I'm not sure why it's null, but the symptoms indicate that the #{kalendarDay} is been specified during view render time while you're trying to evaluate it during the view build time.
So perhaps this is a phase thing? Should I be evaluating this in the decode of the renderer and setting the value in the attributes map? I am still a little confused about the combination of actual values and value expressions.
You should use the encodeXxx() methods of the component or the associated renderer (if any) to generate HTML based on the component's attributes/properties.
You should use the decode() method of the component or the associated renderer (if any) to set component's attributes/properties based on HTTP request parameters which are been sent along with a HTML form submit.
So my question is where should I fetch and evaluate the valueExpression for model and should I store the result of the evaluation in the UIComponent or should I simply evaluate it every time?
Since JSF 2.x it's recommended to explicitly specify a getter and setter for component attributes which in turn delegates to UIComponent#getStateHelper().
public String getValue() {
return (String) getStateHelper().eval("value");
}
public void setValue(String value) {
getStateHelper().put("value", value);
}
public String getTitle() {
return (String) getStateHelper().eval("title");
}
public void setTitle(String title) {
getStateHelper().put("title", title);
}
public Object getModel() {
return getStateHelper().eval("model");
}
public void setModel(Object model) {
getStateHelper().put("model", model);
}
That's all you basically need (note that the getter and setter must exactly match the attribute name as per Javabeans specification). Then in the encodeXxx() method(s) just call getModel() to get (and evaluate) the value of the model attribute.

Related

jQAssistant: org.neo4j.driver.exceptions.ClientException: Unable to convert [C to Neo4j Value - known error?

I'm starting with jQAssist scanning (version 1.9.1). Most of the Maven projects can be scanned without problem, but there is a problem with some tests.
I get this stack trace:
[WARNING] Cannot scan class '/foo/bar/util/validation/constraints/VerfahrensspracheTest$Geschaeft.class'.
org.neo4j.driver.exceptions.ClientException: Unable to convert [C to Neo4j Value.
at org.neo4j.driver.Values.value (Values.java:134)
at org.neo4j.driver.Values.value (Values.java:297)
at org.neo4j.driver.Values.value (Values.java:119)
at org.neo4j.driver.Values.parameters (Values.java:401)
at com.buschmais.xo.neo4j.remote.impl.datastore.AbstractRemoteDatastorePropertyManager.flush (AbstractRemoteDatastorePropertyManager.java:68)
at com.buschmais.xo.neo4j.remote.impl.datastore.RemoteDatastoreEntityManager.flush (RemoteDatastoreEntityManager.java:235)
at com.buschmais.xo.impl.cache.CacheSynchronizationService.flush (CacheSynchronizationService.java:50)
at com.buschmais.xo.impl.cache.CacheSynchronizationService.flush (CacheSynchronizationService.java:34)
at com.buschmais.xo.impl.query.XOQueryImpl.flush (XOQueryImpl.java:158)
at com.buschmais.xo.impl.query.XOQueryImpl.execute (XOQueryImpl.java:138)
etc. etc.
The test in question contains an inner class:
public class VerfahrensspracheTest extends BeanValidationTestBase<VerfahrensspracheTest.Geschaeft> {
/**
* Tests {#link Verfahrenssprache} with English.
*/
#Test
public void testEnglish() {
// Some test
}
/**
* Some bean for testing.
*/
#SuppressWarnings("javadoc")
static class Geschaeft extends PrettyPrintObject {
/**
* Creates this with valid values.
*/
public Geschaeft() {
this.sprache1 = foo.bar.util.type.language.Verfahrenssprache.DEUTSCH;
this.sprache2 = foo.bar.util.type.language.Verfahrenssprache.ENGLISCH.getISOCode();
this.sprache3 = Maps.newHashMap();
this.sprache3.put(foo.bar.util.type.language.Verfahrenssprache.DEUTSCH, "Deutsch");
this.sprache3.put(foo.bar.util.type.language.Verfahrenssprache.FRANZOESISCH, "Français");
this.sprache3.put(foo.bar.util.type.language.Verfahrenssprache.ITALIENISCH, "Italiano");
}
#Verfahrenssprache
public foo.bar.util.type.language.Verfahrenssprache sprache1;
#Verfahrenssprache(englishAllowed = true)
public String sprache2;
#Verfahrenssprache
public Map<foo.bar.util.type.language.Verfahrenssprache, String> sprache3;
}
}
Is there a problem with my inner class or is there a problem with jQAssistant?

Binding between an Object and a SimpleIntegerProperty

I have a combo box over my GUI in JavaFX.
This Combo Box is composed of a complex type elements :
public class DureeChoiceBoxElement extends ObservableValueBase<DureeChoiceBoxElement> {
private IntegerProperty duree;
#Override
public String toString() {
return duree.get() + " an";
}
}
I want to map (or bind) the selected complex element with my model which contains the simple type :
public class Pel {
private IntegerProperty duree = new SimpleIntegerProperty(1);
public Property<Number> dureeProperty() {
return duree;
}
public void setDuree(Integer duree) {
this.duree.setValue(duree);
}
public Integer getDuree() {
return duree.getValue();
}
}
How to do it ?
I tried in the controller with :
public class PelController {
#FXML
private ChoiceBox<DureeChoiceBoxElement> duree;
//etc..
pel.dureeProperty().bind(createElapsedBindingByBindingsAPI2(duree.getValue()));
/*
* #return an ObjectBinding of immutable TimeElapsed objects for the player
*/
private ObjectBinding<Property<Number>> createElapsedBindingByBindingsAPI2(
final DureeChoiceBoxElement dureeChoiceBoxElement) {
return Bindings.createObjectBinding(new Callable<Property<Number>>() {
#Override
public IntegerProperty call() throws Exception {
return dureeChoiceBoxElement.dureeProperty();
}
}, dureeChoiceBoxElement.dureeProperty());
}
}
But it doesn't work (even not compile). I want to say that "Bind this simple property to this complex Object calling the method I give you through the method named "createElapsedBindingByBindingsAPI2(..)".
It is logical read but I didn't managed to make it works anyway.
That's poor ....
Any help please :).
Example that (obviously) works with legacy code style (Swing coding) :
duree.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<DureeChoiceBoxElement>() {
#Override
public void changed(ObservableValue<? extends DureeChoiceBoxElement> observable,
DureeChoiceBoxElement oldValue, DureeChoiceBoxElement newValue) {
// changement durée
log.debug("Durée sélectionnée : {}", duree.getSelectionModel().getSelectedItem().getDuree());
log.debug("Durée bindée ? : {}", pel.getDuree());
pel.setDuree(duree.getSelectionModel().getSelectedItem().getDuree());
}
});
Like this my model is set to selected item. But it implies some boilerplate code. Any better idea based on high level bindings of JavaFX ?

Displaying Many Jira issues using Issue Navigator

My question is similar to "Displaying Jira issues using Issue Navigator" But my concern is that sometimes my list of issues is quite long and providing the user with a link to the Issue Navigator only works if my link is shorter than the max length of URLs.
Is there another way? Cookies? POST data? Perhaps programmatically creating and sharing a filter on the fly, and returning a link to the Issue Navigator that uses this filter? (But at some point I'd want to delete these filters so I don't have so many lying around.)
I do have the JQL query for getting the same list of issues, but it takes a very (very) long time to run and my code has already done the work of finding out what the result is -- I don't want the user to wait twice for the same thing (once while I'm generating my snazzy graphical servlet view and a second time when they want to see the same results in the Issue Navigator).
The easiest way to implement this is to ensure that the list of issues is cached somewhere within one of your application components. Each separate list of issues should be identified by its own unique ID (the magicKey below) that you define yourself.
You would then write your own JQL function that looks up your pre-calculated list of issues by that magic key, which then converts the list of issues into the format that the Issue Navigator requires.
The buildUriEncodedJqlQuery() method can be used to create such an example JQL query dynamically. For example, if the magic key is 1234, it would yield this JQL query: issue in myJqlFunction(1234).
You'd then feed the user a URL that looks like this:
String url = "/secure/IssueNavigator.jspa?mode=hide&reset=true&jqlQuery=" + MyJqlFunction.buildUriEncodedJqlQuery(magicKey);
The end result is that the user will be placed in the Issue Navigator looking at exactly the list of issues your code has provided.
This code also specifically prevents the user from saving your JQL function as part of a saved filter, since it's assumed that the issue cache in your application will not be permanent. If that's not correct, you will want to empty out the sanitiseOperand part.
In atlassian-plugins.xml:
<jql-function key="myJqlFunction" name="My JQL Function"
class="com.mycompany.MyJqlFunction">
</jql-function>
JQL function:
import com.atlassian.crowd.embedded.api.User;
import com.atlassian.jira.JiraDataType;
import com.atlassian.jira.JiraDataTypes;
import com.atlassian.jira.jql.operand.QueryLiteral;
import com.atlassian.jira.jql.query.QueryCreationContext;
import com.atlassian.jira.plugin.jql.function.ClauseSanitisingJqlFunction;
import com.atlassian.jira.plugin.jql.function.JqlFunction;
import com.atlassian.jira.plugin.jql.function.JqlFunctionModuleDescriptor;
import com.atlassian.jira.util.MessageSet;
import com.atlassian.jira.util.MessageSetImpl;
import com.atlassian.jira.util.NotNull;
import com.atlassian.query.clause.TerminalClause;
import com.atlassian.query.operand.FunctionOperand;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class MyJqlFunction implements JqlFunction, ClauseSanitisingJqlFunction
{
private static final String JQL_FUNCTION_NAME = "myJqlFunctionName";
private static final int JQL_FUNCTION_MIN_ARG_COUNT = 1;
private static final int JQL_FUNCTION_MAGIC_KEY_INDEX = 0;
public MyJqlFunction()
{
// inject your app's other components here
}
#Override
public void init(#NotNull JqlFunctionModuleDescriptor moduleDescriptor)
{
}
#Override
public JiraDataType getDataType()
{
return JiraDataTypes.ISSUE;
}
#Override
public String getFunctionName()
{
return JQL_FUNCTION_NAME;
}
#Override
public int getMinimumNumberOfExpectedArguments()
{
return JQL_FUNCTION_MIN_ARG_COUNT;
}
#Override
public boolean isList()
{
return true;
}
/**
* This function generates a URL-escaped JQL query that corresponds to the supplied magic key.
*
* #param magicKey
* #return
*/
public static String buildUriEncodedJqlQuery(String magicKey)
{
return "issue%20in%20" + JQL_FUNCTION_NAME + "(%22"
+ magicKey + "%22%)";
}
#Override
public List<QueryLiteral> getValues(#NotNull QueryCreationContext queryCreationContext,
#NotNull FunctionOperand operand,
#NotNull TerminalClause terminalClause)
{
User searchUser = queryCreationContext.getUser();
MessageSet messages = new MessageSetImpl();
List<QueryLiteral> values = internalGetValues(searchUser,
operand,
terminalClause,
messages,
!queryCreationContext.isSecurityOverriden());
return values;
}
private List<QueryLiteral> internalGetValues(#NotNull User searchUser,
#NotNull FunctionOperand operand,
#NotNull TerminalClause terminalClause,
#NotNull MessageSet messages,
#NotNull Boolean checkSecurity)
{
List<QueryLiteral> result = new ArrayList<QueryLiteral>();
if (searchUser==null)
{
// handle anon user
}
List<String> args = operand.getArgs();
if (wasSanitised(args))
{
messages.addErrorMessage("this function can't be used as part of a saved filter etc");
return result;
}
if (args.size() < getMinimumNumberOfExpectedArguments())
{
messages.addErrorMessage("too few arguments, etc");
return result;
}
final String magicKey = args.get(JQL_FUNCTION_MAGIC_KEY_INDEX);
// You need to implement this part yourself! This is where you use the supplied
// magicKey to fetch a list of issues from your own internal data source.
List<String> myIssueKeys = myCache.get(magicKey);
for (String id : myIssueKeys)
{
result.add(new QueryLiteral(operand, id));
}
return result;
}
#Override
public MessageSet validate(User searcher, #NotNull FunctionOperand operand, #NotNull TerminalClause terminalClause)
{
MessageSet messages = new MessageSetImpl();
internalGetValues(searcher, operand, terminalClause, messages, true);
return messages;
}
#Override
public FunctionOperand sanitiseOperand(User paramUser, #NotNull FunctionOperand operand)
{
// prevent the user from saving this as a filter, since the results are presumed to be dynamic
return new FunctionOperand(operand.getName(), Arrays.asList(""));
}
private boolean wasSanitised(List<String> args)
{
return (args.size() == 0 || args.get(JQL_FUNCTION_MAGIC_KEY_INDEX).isEmpty());
}
}

Trim submitted value in Mojarra TextRenderer

We have many XHTML pages, each page has some <h:inputText>, <h:inputSecret>, etc.. input components.
I am trying to find out a generic way to trim all those values before form submission.
I implemented a TrimInputTextRenderer class,
XHTML page:
<h:inputText value="#{userBean.emailAddress}" trim="true">
<f:validator validatorId="emailAddressValidator"/>
</h:inputText>
TrimInputTextRenderer.java:
package com.vyan.web.component;
import java.io.IOException;
import java.util.List;
import javax.faces.component.UIComponent;
import javax.faces.component.UIInput;
import javax.faces.component.UIOutput;
import javax.faces.context.FacesContext;
import javax.faces.render.FacesRenderer;
import com.sun.faces.renderkit.html_basic.TextRenderer;
#FacesRenderer(componentFamily="javax.faces.Input", rendererType="javax.faces.Text")
public class TrimInputTextRenderer extends TextRenderer {
private void encodeBeginForChildren(FacesContext context, UIComponent component) {
if (component instanceof UIInput) {
component = (UIInput)component;
String componentValue = ((UIOutput)component).getValue().toString();
componentValue = (componentValue!=null)?componentValue.trim():componentValue;
((UIInput)component).setValue(componentValue);
}
// Encoding recursively all childrens.
if (component.isRendered() && component.getChildCount() > 0)
encodeBeginForChildrens(context, component.getChildren());
}
private void encodeBeginForChildrens(FacesContext context, List<UIComponent> children) {
for (UIComponent comp : children) {
if (comp instanceof UIInput) {
comp = (UIInput) comp;
((UIInput) comp).setValue(((UIInput) comp).getValue()
.toString().trim());
}
// Encoding recursively all childrens.
if (comp.isRendered() && comp.getChildCount() > 0)
encodeBeginForChildrens(context, comp.getChildren());
}
}
#Override
public void encodeBegin(FacesContext context, UIComponent component) throws IOException {
String trimValue = (String) component.getAttributes().get("trim");
if (trimValue!= null && Boolean.valueOf(trimValue)) {
//super.write(string);
encodeBeginForChildren(context, component);
}
super.encodeBegin(context, component);
}
}
faces-config.xml:
<render-kit>
<renderer>
<component-family>javax.faces.Input</component-family>
<renderer-type>javax.faces.Text</renderer-type>
<renderer-class>com.vyan.web.component.TrimInputTextRenderer</renderer-class>
</renderer>
</render-kit>
The problem is, everything works fine, the custom renderer trims the values.
But we need the component values in validator & managed bean.
Those were getting executed before the method TrimInputTextRenderer#encodeBegin() method call.
So in validator & managed bean, we are getting non-trimmed values.
Is there a way it can be executed before validation phase,
or do i need to implement this logic some where?
This logic needs to be implemented in the decode() method or, easier, the setSubmittedValue().
#Override
public void setSubmittedValue(UIComponent component, Object value) {
if (value instanceof String) {
value = ((String) value).trim();
}
super.setSubmittedValue(component, value);
}
The easier and non-implementation-dependent alternative to this Mojarra-specific renderer extension was been a #FacesConverter(forClass=String), by the way.

How to sort the choices in a Wicket dropdown according to the current user locale?

I have the following issue:
a drop down with a list of elements
each of these elements has a fixed key, which is used by the IChoiceRenderer implementation to look up the localized version of the key (it's a standard, utility renderer implemented in a different package)
the list of localized keys is in a properties file, linked to the panel which instantiates the dropdown.
Is there an elegant/reusable solution to have the dropdown display its elements sorted alphabetically ?
In the end, I think using the render is probably the best approach. To make it reusable and efficient, I isolated this in a Behavior.
Here's the code:
import org.apache.wicket.Component;
import org.apache.wicket.behavior.Behavior;
import org.apache.wicket.markup.html.form.AbstractChoice;
import org.apache.wicket.markup.html.form.IChoiceRenderer;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import static java.util.Arrays.sort;
/**
* This {#link Behavior} can only be used on {#link AbstractChoice} subclasses. It will sort the choices
* according to their "natural display order" (i.e. the natural order of the display values of the choices).
* This assumes that the display value implements {#link Comparable}. If this is not the case, you should
* provide a comparator for the display value. An instance of this class <em>cannot be shared</em> between components.
* Because the rendering can be costly, the sort-computation is done only once, by default,
* unless you set to <code>false</code> the <code>sortOnlyOnce</code> argument in the constructor.
*
* #author donckels (created on 2012-06-07)
*/
#SuppressWarnings({"unchecked"})
public class OrderedChoiceBehavior extends Behavior {
// ----- instance fields -----
private Comparator displayValueComparator;
private boolean sortOnlyOnce = true;
private boolean sorted;
// ----- constructors -----
public OrderedChoiceBehavior() {
}
public OrderedChoiceBehavior(boolean sortOnlyOnce) {
this.sortOnlyOnce = sortOnlyOnce;
}
public OrderedChoiceBehavior(boolean sortOnlyOnce, Comparator displayValueComparator) {
this.sortOnlyOnce = sortOnlyOnce;
this.displayValueComparator = displayValueComparator;
}
// ----- public methods -----
#Override
public void beforeRender(Component component) {
if (this.sorted && this.sortOnlyOnce) { return;}
AbstractChoice owner = (AbstractChoice) component;
IChoiceRenderer choiceRenderer = owner.getChoiceRenderer();
List choices = owner.getChoices();
// Temporary data structure: store the actual rendered value with its initial index
Object[][] displayValuesWithIndex = new Object[choices.size()][2];
for (int i = 0, valuesSize = choices.size(); i < valuesSize; i++) {
Object value = choices.get(i);
displayValuesWithIndex[i][0] = choiceRenderer.getDisplayValue(value);
displayValuesWithIndex[i][1] = i;
}
sort(displayValuesWithIndex, new DisplayValueWithIndexComparator());
List valuesCopy = new ArrayList(choices);
for (int i = 0, length = displayValuesWithIndex.length; i < length; i++) {
Object[] displayValueWithIndex = displayValuesWithIndex[i];
int originalIndex = (Integer) displayValueWithIndex[1];
choices.set(i, valuesCopy.get(originalIndex));
}
this.sorted = true;
}
public Comparator getDisplayValueComparator() {
return this.displayValueComparator;
}
// ----- inner classes -----
private class DisplayValueWithIndexComparator implements Comparator<Object[]> {
// ----- Comparator -----
public int compare(Object[] left, Object[] right) {
Object leftDisplayValue = left[0];
Object rightDisplayValue = right[0];
if (null == leftDisplayValue) { return -1;}
if (null == rightDisplayValue) { return 1;}
if (null == getDisplayValueComparator()) {
return ((Comparable) leftDisplayValue).compareTo(rightDisplayValue);
} else {
return getDisplayValueComparator().compare(leftDisplayValue, rightDisplayValue);
}
}
}
}
Use this extension of DropDownChoice using Java's Collator (basically locale sensitive sorting - take national characters and national sorting rules into account)
Code tested with Wicket 6 and Java 5+:
import java.text.Collator;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import org.apache.wicket.Session;
import org.apache.wicket.markup.html.form.DropDownChoice;
import org.apache.wicket.markup.html.form.IChoiceRenderer;
import org.apache.wicket.model.IModel;
import com.google.common.collect.Ordering;
/**
* DropDownChoice which sort its choices (or in HTML's terminology select's options) according it's localized value
* and using current locale based Collator so it's sorted how it should be in particular language (ie. including national characters,
* using right order).
*
* #author Michal Bernhard michal#bernhard.cz 2013
*
* #param <T>
*/
public class OrderedDropDownChoice<T> extends DropDownChoice<T> {
public OrderedDropDownChoice(String id, IModel<? extends List<? extends T>> choices, IChoiceRenderer<? super T> renderer) {
super(id, choices, renderer);
}
public OrderedDropDownChoice(String id, IModel<? extends List<? extends T>> choices) {
super(id, choices);
}
public OrderedDropDownChoice(String id) {
super(id);
}
public OrderedDropDownChoice(
String id,
IModel<T> model,
IModel<? extends List<? extends T>> choices,
IChoiceRenderer<? super T> renderer) {
super(id, model, choices, renderer);
}
#Override
public List<? extends T> getChoices() {
List<? extends T> unsortedChoices = super.getChoices();
List<? extends T> sortedChoices = Ordering.from(displayValueAlphabeticComparator()).sortedCopy(unsortedChoices);
return sortedChoices;
}
private Collator localeBasedTertiaryCollator() {
Locale currentLocale = Session.get().getLocale();
Collator collator = Collator.getInstance(currentLocale);
collator.setStrength(Collator.TERTIARY);
return collator;
}
private Comparator<T> displayValueAlphabeticComparator() {
final IChoiceRenderer<? super T> renderer = getChoiceRenderer();
return new Comparator<T>() {
#Override
public int compare(T o1, T o2) {
Object o1DisplayValue = renderer.getDisplayValue(o1);
Object o2DisplayValue = renderer.getDisplayValue(o2);
return localeBasedTertiaryCollator().compare(o1DisplayValue, o2DisplayValue);
}
};
}
}
Copied from https://gist.github.com/michalbcz/7236242
If you want a Wicket-based solution you can try to sort the list with something like that:
public class ChoiceRendererComparator<T> implements Comparator<T> {
private final IChoiceRenderer<T> renderer;
public ChoiceRendererComparator(IChoiceRenderer<T> renderer) {
this.renderer = renderer;
}
#SuppressWarnings("unchecked")
public int compare(T o1, T o2) {
return ((Comparable<Object>) renderer.getDisplayValue(o1)).compareTo(renderer.getDisplayValue(o2));
}
}
Usage:
List<Entity> list = ...
IChoiceRenderer<Entity> renderer = ...
Collections.sort(list, new ChoiceRendererComparator<Entity>(renderer));
DropDownChoice<Entity> dropdown = new DropDownChoice<Entity>("dropdown", list, renderer);
The solution we use at my company is Javascript based, we set a special css class on the dropdowns we want to be sorted, and a little jQuery trick does the sort.
Facing the same problem, I moved part of the localisation data from my XMLs to the database, implemented a matching Resolver and was able to use the localized Strings for sorting.
The table design and hibernate configuration was kind of tricky and is described here: Hibernate #ElementCollection - Better solution needed.
The ResourceLoader is along these lines:
public class DataBaseStringResourceLoader extends ComponentStringResourceLoader {
private static final transient Logger logger = Logger
.getLogger(DataBaseStringResourceLoader.class);
#Inject
private ISomeDAO someDao;
#Inject
private IOtherDao otherDao;
#Inject
private IThisDAO thisDao;
#Inject
private IThatDAO thatDao;
#Override
public String loadStringResource(Class<?> clazz, String key, Locale locale,
String style, String variation) {
String resource = loadFromDB(key, new Locale(locale.getLanguage()));
if (resource == null) {
resource = super.loadStringResource(clazz, key, locale, style, variation);
}
return resource;
}
private String loadFromDB(String key, Locale locale) {
String resource = null;
if (locale.getLanguage() != Locale.GERMAN.getLanguage()
&& locale.getLanguage() != Locale.ENGLISH.getLanguage()) {
locale = Locale.ENGLISH;
}
if (key.startsWith("some") || key.startsWith("other")
|| key.startsWith("this") || key.startsWith("that")) {
Integer id = Integer.valueOf(key.substring(key.indexOf(".") + 1));
ILocalizedObject master;
if (key.startsWith("some")) {
master = someDao.findById(id);
} else if (key.startsWith("other")) {
master = otherDao.findById(id);
} else if (key.startsWith("this") ){
master = thisDao.findById(id);
} else {
master = thatDao.findById(id);
}
if (master != null && master.getNames().get(locale) != null) {
resource = master.getNames().get(locale).getName();
} else if (master == null) {
logger.debug("For key " + key + " there is no master.");
}
}
return resource;
}
[...]
}

Resources