public UIComponent buildWidget(
      String elementName, Map<String, String> attributes, UIMetawidget metawidget) {

    // Not for RichFaces?

    if (TRUE.equals(attributes.get(HIDDEN))) {
      return null;
    }

    // Note: we tried implementing lookups using org.richfaces.ComboBox, but that
    // allows manual input and if you set enableManualInput=false it behaves a
    // bit screwy for our liking (ie. if you hit backspace the browser goes back)

    if (attributes.containsKey(FACES_LOOKUP) || attributes.containsKey(LOOKUP)) {
      return null;
    }

    // Lookup the class

    Class<?> clazz = WidgetBuilderUtils.getActualClassOrType(attributes, null);

    if (clazz == null) {
      return null;
    }

    // Primitives

    if (clazz.isPrimitive()) {
      // Not for RichFaces

      if (boolean.class.equals(clazz) || char.class.equals(clazz)) {
        return null;
      }

      // Ranged

      UIComponent ranged = createRanged(attributes);

      if (ranged != null) {
        return ranged;
      }

      // Not-ranged

      HtmlInputNumberSpinner spinner = VERSION_SPECIFIC_WIDGET_BUILDER.createInputNumberSpinner();

      // May be ranged in one dimension only. In which case, explictly range the *other*
      // dimension because RichFaces defaults to 0-100

      String minimumValue = attributes.get(MINIMUM_VALUE);

      if (minimumValue != null && !"".equals(minimumValue)) {
        spinner.setMinValue(minimumValue);
      } else if (byte.class.equals(clazz)) {
        spinner.setMinValue(String.valueOf(Byte.MIN_VALUE));
      } else if (short.class.equals(clazz)) {
        spinner.setMinValue(String.valueOf(Short.MIN_VALUE));
      } else if (int.class.equals(clazz)) {
        spinner.setMinValue(String.valueOf(Integer.MIN_VALUE));
      } else if (long.class.equals(clazz)) {
        spinner.setMinValue(String.valueOf(Long.MIN_VALUE));
      } else if (float.class.equals(clazz)) {
        spinner.setMinValue(String.valueOf(-Float.MAX_VALUE));
      } else if (double.class.equals(clazz)) {
        spinner.setMinValue(String.valueOf(-Double.MAX_VALUE));
      }

      String maximumValue = attributes.get(MAXIMUM_VALUE);

      if (maximumValue != null && !"".equals(maximumValue)) {
        spinner.setMaxValue(maximumValue);
      } else if (byte.class.equals(clazz)) {
        spinner.setMaxValue(String.valueOf(Byte.MAX_VALUE));
      } else if (short.class.equals(clazz)) {
        spinner.setMaxValue(String.valueOf(Short.MAX_VALUE));
      } else if (int.class.equals(clazz)) {
        spinner.setMaxValue(String.valueOf(Integer.MAX_VALUE));
      } else if (long.class.equals(clazz)) {
        spinner.setMaxValue(String.valueOf(Long.MAX_VALUE));
      } else if (float.class.equals(clazz)) {
        spinner.setMaxValue(String.valueOf(Float.MAX_VALUE));
      } else if (double.class.equals(clazz)) {
        spinner.setMaxValue(String.valueOf(Double.MAX_VALUE));
      }

      // Wraps around?

      spinner.setCycled(false);

      // Stepped

      if (float.class.equals(clazz) || double.class.equals(clazz)) {
        spinner.setStep("0.1");
      }

      return spinner;
    }

    // Dates
    //
    // Note: when http://jira.jboss.org/jira/browse/RF-2023 gets implemented, that
    // would allow external, app-level configuration of this Calendar

    if (Date.class.isAssignableFrom(clazz)) {
      UICalendar calendar =
          FacesUtils.createComponent(HtmlCalendar.COMPONENT_TYPE, "org.richfaces.CalendarRenderer");

      if (attributes.containsKey(DATETIME_PATTERN)) {
        calendar.setDatePattern(attributes.get(DATETIME_PATTERN));
      }

      if (attributes.containsKey(LOCALE)) {
        calendar.setLocale(new Locale(attributes.get(LOCALE)));
      }

      if (attributes.containsKey(TIME_ZONE)) {
        calendar.setTimeZone(TimeZone.getTimeZone(attributes.get(TIME_ZONE)));
      }

      return calendar;
    }

    // Object primitives

    if (Number.class.isAssignableFrom(clazz)) {
      // Ranged

      UIComponent ranged = createRanged(attributes);

      if (ranged != null) {
        return ranged;
      }

      // Not-ranged
      //
      // Until https://jira.jboss.org/jira/browse/RF-4450 is fixed, do not use
      // UIInputNumberSpinner for nullable numbers
    }

    // RichFaces version-specific

    return VERSION_SPECIFIC_WIDGET_BUILDER.buildWidget(elementName, attributes, metawidget);
  }
  public Converter getConverter(ValueHolder valueHolder, Map<String, String> attributes) {

    // Use existing Converter (if any)

    Converter converter = valueHolder.getConverter();

    if (converter != null) {
      return converter;
    }

    // Create from id

    FacesContext context = FacesContext.getCurrentInstance();
    String converterId = attributes.get(FACES_CONVERTER);

    if (converterId != null) {

      converter = context.getApplication().createConverter(converterId);

    } else if (valueHolder instanceof UISelectOne || valueHolder instanceof UISelectMany) {

      // Create from parameterized type (eg. a Date converter for List<Date>)

      String parameterizedType = WidgetBuilderUtils.getComponentType(attributes);

      if (parameterizedType != null) {
        Class<?> parameterizedClass = ClassUtils.niceForName(parameterizedType);

        // The parameterized type might be null, or might not be concrete
        // enough to be instantiatable (eg. List<? extends Foo>)

        if (parameterizedClass != null) {
          converter = context.getApplication().createConverter(parameterizedClass);
        }
      }

    } else {

      // Create implicit converters
      //
      // JSF does not appear to implicitly hook up DateTimeConverters without either an
      // explicit f:convertDateTime tag or a registered java.util.Date converter. Adding one
      // fixes both POSTback and display of read-only dates (otherwise JSF uses Date.toString)
      //
      // JSF *does* appear to implicitly hook up NumberConverters.

      String type = attributes.get(TYPE);

      if (type != null) {

        Class<?> clazz = ClassUtils.niceForName(type);

        if (clazz != null) {

          if (clazz.isAssignableFrom(Date.class)) {
            converter = getDateTimeConverter(converter);
          }
        }
      }
    }

    // Support for DateTimeConverter

    if (attributes.containsKey(DATE_STYLE)) {
      converter = getDateTimeConverter(converter);
      ((DateTimeConverter) converter).setDateStyle(attributes.get(DATE_STYLE));
    }

    if (attributes.containsKey(DATETIME_PATTERN)) {
      converter = getDateTimeConverter(converter);
      ((DateTimeConverter) converter).setPattern(attributes.get(DATETIME_PATTERN));
    }

    if (attributes.containsKey(TIME_STYLE)) {
      converter = getDateTimeConverter(converter);
      ((DateTimeConverter) converter).setTimeStyle(attributes.get(TIME_STYLE));
    }

    if (attributes.containsKey(TIME_ZONE)) {
      converter = getDateTimeConverter(converter);
      ((DateTimeConverter) converter).setTimeZone(TimeZone.getTimeZone(attributes.get(TIME_ZONE)));
    }

    if (attributes.containsKey(DATETIME_TYPE)) {
      converter = getDateTimeConverter(converter);
      ((DateTimeConverter) converter).setType(attributes.get(DATETIME_TYPE));
    }

    // Support for NumberConverter

    if (attributes.containsKey(CURRENCY_CODE)) {
      converter = getNumberConverter(converter);
      ((NumberConverter) converter).setCurrencyCode(attributes.get(CURRENCY_CODE));
    }

    if (attributes.containsKey(CURRENCY_SYMBOL)) {
      converter = getNumberConverter(converter);
      ((NumberConverter) converter).setCurrencySymbol(attributes.get(CURRENCY_SYMBOL));
    }

    if (attributes.containsKey(NUMBER_USES_GROUPING_SEPARATORS)) {
      converter = getNumberConverter(converter);
      ((NumberConverter) converter)
          .setGroupingUsed(Boolean.parseBoolean(attributes.get(NUMBER_USES_GROUPING_SEPARATORS)));
    }

    if (attributes.containsKey(MINIMUM_INTEGER_DIGITS)) {
      converter = getNumberConverter(converter);
      ((NumberConverter) converter)
          .setMinIntegerDigits(Integer.parseInt(attributes.get(MINIMUM_INTEGER_DIGITS)));
    }

    if (attributes.containsKey(MAXIMUM_INTEGER_DIGITS)) {
      converter = getNumberConverter(converter);
      ((NumberConverter) converter)
          .setMaxIntegerDigits(Integer.parseInt(attributes.get(MAXIMUM_INTEGER_DIGITS)));
    }

    if (attributes.containsKey(MINIMUM_FRACTIONAL_DIGITS)) {
      converter = getNumberConverter(converter);
      ((NumberConverter) converter)
          .setMinFractionDigits(Integer.parseInt(attributes.get(MINIMUM_FRACTIONAL_DIGITS)));
    }

    if (attributes.containsKey(MAXIMUM_FRACTIONAL_DIGITS)) {
      converter = getNumberConverter(converter);
      ((NumberConverter) converter)
          .setMaxFractionDigits(Integer.parseInt(attributes.get(MAXIMUM_FRACTIONAL_DIGITS)));
    }

    if (attributes.containsKey(NUMBER_PATTERN)) {
      converter = getNumberConverter(converter);
      ((NumberConverter) converter).setPattern(attributes.get(NUMBER_PATTERN));
    }

    if (attributes.containsKey(NUMBER_TYPE)) {
      converter = getNumberConverter(converter);
      ((NumberConverter) converter).setType(attributes.get(NUMBER_TYPE));
    }

    // Locale (applies to both DateTimeConverter and NumberConverter)

    if (attributes.containsKey(LOCALE)) {
      if (converter instanceof NumberConverter) {
        ((NumberConverter) converter).setLocale(new Locale(attributes.get(LOCALE)));
      } else {
        converter = getDateTimeConverter(converter);
        ((DateTimeConverter) converter).setLocale(new Locale(attributes.get(LOCALE)));
      }
    }

    // Return it

    return converter;
  }