5 Replies Latest reply on Oct 2, 2008 5:29 PM by luxspes

    Converters are not called by selectOneMenu validation

      I am having problems biding a selectOneMenu  to a List<Map<String,Object>> if I write this (I started a new thread because I think this is a different problem):


      <h:selectOneMenu name="selectName" value="#{controller.mapProperty['key']}" >
      <s:selectItems  value="#{controller.sourceList}" var="item" label="#{item['label']}"/>                                                                            
      </h:selectOneMenu>
      
      



      I am getting a string (Map.toString()) in #{controller.mapProperty['key']} instead of a Map. So I decided to write a converter:


      @Name("mapConverter")
      @Converter(forClass=java.util.Map.class)
      public class MapConverter implements javax.faces.convert.Converter, Serializable {
           
           @Logger
           Log log;
           
           private List<javax.faces.model.SelectItem> getItems(UIComponent component){
                UISelectOne selectOne = (UISelectOne)component;
                UISelectItems selectItems =  (UISelectItems) selectOne.getChildren().get(0);
                List<SelectItem> items = (List<SelectItem>) selectItems.getValue();          
                log.info("Component #0 children #1 values #2 first #3",component,selectOne.getChildren(),items,items.get(0).getValue());
                return items;
           }
           
           private List<Map> getMaps(UIComponent component){
                List<SelectItem> items = getItems(component);
                List<Map> maps = new ArrayList<Map>();
                for(SelectItem item:items){
                     Map mapToAdd = (Map) item.getValue();
                     log.info("Map to add #0",mapToAdd);
                     maps.add(mapToAdd);
                }
                return maps;
           }
           
           private Map findMap(UIComponent component,String value){
                List<Map> maps = getMaps(component);
                for(Map map:maps){
                     if(map.get("value").toString().equals(value))
                          return map;
                }
                log.info("Not found #0 in #1",value,maps);
                return null;
           }
      
           public Object getAsObject(FacesContext context, UIComponent component, String value) { 
                Map object = findMap(component,value);
                return value;
           }
      
           public String getAsString(FacesContext context, UIComponent component, Object value) {
                // TODO Auto-generated method stub
                if(value instanceof Map){
                     Map map = (Map) value;
                     String string = map.get("value").toString();
                     log.info("Map #0 a #1", map,string);
                     return string;
                }
                throw new ConverterException("Unexpected class:"+value.getClass());
           }
      
      }
      
      



      I know it is a very naive implementation (it assumes a value key in the map) but it should work... well it does not. It crashes. And I think it is because of a bug inside selectOneMenu. I'll write about it in my next post.

        • 1. Re: Converters are not called by selectOneMenu validation

          So, since my converter, instead of helping me caused a crash (yes, I know it will bind the value  to #{controller.mapProperty['key']} instead of a full java.util.Map but that is fine for me) , I started looking at the code in javax.faces.component.UISelectOne for answers.


          The error is enqueued here in the validateValue method of UISelectOne :


          protected void validateValue(FacesContext context, Object value) {
          
                  // Skip validation if it is not necessary
                  super.validateValue(context, value);
          
                  if (!isValid() || (value == null)) {
                      return;
                  }
          
                  // Ensure that the value matches one of the available options
                  boolean found = matchValue(value, new SelectItemsIterator(this));
          
                  // Enqueue an error message if an invalid value was specified
                  if (!found) {
                      FacesMessage message =
                          MessageFactory.getMessage(context, INVALID_MESSAGE_ID,
                               MessageFactory.getLabel(context, this));
                      context.addMessage(getClientId(context), message);
                      setValid(false);
                  }
              }
          



          But the interesting part is in the matchValue method called by validateValue:


              private boolean matchValue(Object value, Iterator items) {
          
                  while (items.hasNext()) {
                      SelectItem item = (SelectItem) items.next();
                      if (item instanceof SelectItemGroup) {
                          SelectItem subitems[] =
                              ((SelectItemGroup) item).getSelectItems();
                          if ((subitems != null) && (subitems.length > 0)) {
                              if (matchValue(value, new ArrayIterator(subitems))) {
                                  return (true);
                              }
                          }
                      } else {
                          //Coerce the item value type before comparing values.
                          Class type = value.getClass();
                          Object newValue;
                          try {
                              newValue = getFacesContext().getApplication().
                                  getExpressionFactory().coerceToType(item.getValue(), type);
                          } catch (Exception e) {
                              // this should catch an ELException, but there is a bug
                              // in ExpressionFactory.coerceToType() in GF
                              newValue = null;
                          }
                          if (value.equals(newValue)) {
                              return (true);
                          }
                      }
                  }
                  return (false);
          
              }
          
          



          UISelectOne is enqueuing an error message saying there is an invalid value, because my converter is not being called in matchValue, instead it is calling coerceToType:


           newValue = getFacesContext().getApplication().
                                  getExpressionFactory().coerceToType(item.getValue(), type);
          



          and therefore, by the time it arrives to:


             if (value.equals(newValue)) {
                              return (true);
             }
          



          if value has a value of 5, newValue has a value of: {5="Option1"}. newValue was not created using my converter because coerceToType just didn't call it. (BTW I took a look at the MyFaces code and their JSF implementation does seem to call the converter, if you want I can post it here too).


          Now, this is of course terrible (I tried updating to the latest Sun JSF mojarra version, but this bug is still there), but is not the problem of the Seam development team (is a problem for Mojarra team). Why am I posting it here then?


          Well, because the EntityConverter and the EnumConverter that come with Seam do work... why do they work? how could they work? IMO they shouldn't, so... why do they?


          What I want to know is... is the Seam code dealing with this problem in some special way? and, if it is... how is it dealing with it? I have been trying to find clues in the code but I can not find anything that looks like a workaround for this...


          Sorry for this very lengthly posts... but I couldn't find a shorter way to explain all this.


          So.. Any hints? Any recommended workarounds?


          Regards,

          • 2. Re: Converters are not called by selectOneMenu validation

            Oh, I forgot to say that, to call my converter I had to do this (note the converter="mapConverter"):


            <h:selectOneMenu converter="mapConverter" name="selectName" value="#{controller.mapProperty['key']}" >
            <s:selectItems  value="#{controller.sourceList}" var="item" label="#{item['label']}"/>                                                                            
            </h:selectOneMenu>
            
            



            It seems that the forClass in @Converter(forClass=java.util.Map.class) is pointless. if I don't call it explicitly in selectOneMenu my converter is completly ignored.

            • 3. Re: Converters are not called by selectOneMenu validation

              Here is the MyFaces implementation:


              protected void validateValue(FacesContext context, Object value)
                  {
                      super.validateValue(context, value);
              
                      if (!isValid() || value == null)
                      {
                          return;
                      }
              
                      _SelectItemsUtil._ValueConverter converter = new _SelectItemsUtil._ValueConverter()
                      {
                          public Object getConvertedValue(FacesContext context, String value)
                          {
                              return UISelectOne.this.getConvertedValue(context, value);
                          }
                      };
              
                      // selected value must match to one of the available options
                      if (!_SelectItemsUtil.matchValue(context, value, new _SelectItemsIterator(this), converter))
                      {
                          _MessageUtils.addErrorMessage(context, this, INVALID_MESSAGE_ID, new Object[]
                          { _MessageUtils.getLabel(context, this) });
                          setValid(false);
                      }
                  }
              



              as you can see, it creates a _ValueConverter and the passes it as a parameter in SelectItemsUtil.matchValue, if you take a look at the code in there, it does seem to be using converters for this (as it should)


              public static boolean matchValue(FacesContext context, Object value,
                                  Iterator<SelectItem> selectItemsIter, _ValueConverter converter)
                  {
                      while (selectItemsIter.hasNext())
                      {
                          SelectItem item = selectItemsIter.next();
                          if (item instanceof SelectItemGroup)
                          {
                              SelectItemGroup itemgroup = (SelectItemGroup) item;
                              SelectItem[] selectItems = itemgroup.getSelectItems();
                              if (selectItems != null
                                              && selectItems.length > 0
                                              && matchValue(context, value, Arrays.asList(
                                                              selectItems).iterator(), converter))
                              {
                                  return true;
                              }
                          }
                          else
                          {
                              Object itemValue = item.getValue();
                              if(converter != null && itemValue instanceof String)
                              {
                                  itemValue = converter.getConvertedValue(context, (String)itemValue);
                              }
                              if (value==itemValue || value.equals(itemValue))
                              {
                                  return true;
                              }
                          }
                      }
                      return false;
                  }
              



              But now the thing is that since MyFaces implementation uses converters, and Mojara doesn't... how can one write code that works in Mojarra? what is the trick?

              • 4. Re: Converters are not called by selectOneMenu validation
                pmuir

                So talk to the JSF guys, they don't monitor this forum.

                • 5. Re: Converters are not called by selectOneMenu validation

                  Been there, done that. What I want to know is why my map converter couldn't workaround this issue but your entity converter seems to work fine. Did you know about this problem when you created the entity converter ? Did you do something about it?