cancel
Showing results for 
Search instead for 
Did you mean: 

Custom Data Type

califa198
Champ in-the-making
Champ in-the-making
Hello,

I wanna know if its possible to make a custom data type. i would like to make a new data type for properties in which i can put 2 text fields for 1 data type, is this possible? where can i find information regarding this? it should be a data type just like d:text or d:datetime.

Anything would be useful, thanks

califa
17 REPLIES 17

iblanco
Confirmed Champ
Confirmed Champ
Well this is my converter:

package es.binovo.alfresco.web.ui.repo.converter;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.Locale;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;
import javax.faces.convert.ConverterException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class Long2DecimalConverter implements Converter {

   // By default 2 digits for the fraction
   private short fractionDigits;

   private static Log logger = LogFactory.getLog(Long2DecimalConverter.class);

   public Long2DecimalConverter() {
      this.fractionDigits = 2;
      if (logger.isDebugEnabled()) {
         logger.debug("Constructed new Long2Deciaml converter.");
      }
   }

   public Object getAsObject(FacesContext context, UIComponent component,
         String strValue) throws ConverterException {

      if (logger.isDebugEnabled()) {
         logger.debug("getting '" + strValue + "' string as long.");
      }
      Locale locale = context.getViewRoot().getLocale();
      return string2Long(strValue, locale);
   }

   public String getAsString(FacesContext context, UIComponent component,
         Object value) throws ConverterException {

      if (logger.isDebugEnabled()) {
         logger.debug("getting '" + value + "' object as String.");
      }
      Locale locale = context.getViewRoot().getLocale();
      return long2String((Long) value, locale);
   }

   public short getFractionDigits() {
      return fractionDigits;
   }

   String long2String(Long value, Locale locale) {
      String strValue = null;
      if (value != null && value.toString().length() > 0) {
         BigDecimal decimalValue = new BigDecimal(value.toString());
         decimalValue = decimalValue.movePointLeft(this.fractionDigits);
         DecimalFormat decimal = (DecimalFormat) NumberFormat
               .getInstance(locale);
         decimal.setMaximumFractionDigits(this.fractionDigits);
         strValue = decimal.format(decimalValue);
      }
      if (logger.isDebugEnabled()) {
         logger.debug("returning '" + strValue + "' string.");
      }
      return strValue;
   }

   public void setFractionDigits(short fractionDigits) {
      // Long can not hold over 18 full digits (max is
      // 9,223,372,036,854,775,807)
      if (logger.isDebugEnabled()) {
         logger.debug("Setting fraction digits to " + fractionDigits + ".");
      }
      if (fractionDigits > 18 || fractionDigits < 0) {
         if (logger.isErrorEnabled()) {
            logger.error(fractionDigits
                  + " is not a valid fraction digit number."
                  + " Allowed values are between 0 and 18.");
         }
         throw new IllegalArgumentException(
               "Fraction digits must be between 0 and 18.");
      }
      this.fractionDigits = fractionDigits;
   }

   /**
    * Parses a string and compounds the corresponding long value based on the
    * locale an the number of fraction digits to keep in the long.
    *
    * @param strValue
    *            String to parse
    * @param locale
    *            Locale to use in the parsing
    * @return
    */
   long string2Long(String strValue, Locale locale) {
      long longValue;
      DecimalFormat decimal = (DecimalFormat) NumberFormat
            .getInstance(locale);
      decimal.setParseBigDecimal(true);
      BigDecimal decimalValue = null;
      try {
         decimalValue = (BigDecimal) decimal.parseObject(strValue);
      } catch (ParseException e) {
         if (logger.isErrorEnabled()) {
            logger.error("String value " + strValue
                  + " can not be parsed to a BigDecimal object.");
         }
         throw new ConverterException(e);
      }
      // prepare to convert to long
      decimalValue = decimalValue.movePointRight(this.fractionDigits);
      
      // Truncate non supported decimals
      decimalValue = decimalValue.setScale(0, RoundingMode.DOWN);

      try {
         longValue = decimalValue.longValueExact();
      } catch (ArithmeticException e) {
         // Long value can not hold the number
         if (logger.isErrorEnabled()) {
            logger.error("Decimal value " + decimalValue
                  + " is not exact or can not be hold in a long.");
         }
         throw new ConverterException(e);
      }
      if (logger.isDebugEnabled()) {
         logger.debug("returning '" + longValue + "' long.");
      }
      return longValue;
   }

}

It works like a charm (as far as I have tested it). You can set the number of digits that will be considered the decimal part. But I have not been able to especify the number of fractions to use in the web-client config.

This is my faces-config-custom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE faces-config PUBLIC "-//Sun Microsystems, Inc.//DTD JavaServer Faces Config 1.1//EN"
                              "http://java.sun.com/dtd/web-facesconfig_1_1.dtd">
<faces-config>
   <converter>
      <converter-id>PriceConverter</converter-id>
      <converter-class>es.binovo.alfresco.web.ui.repo.converter.Long2DecimalConverter</converter-class>
      <property>
         <property-name>fractionDigits</property-name>
         <property-class>java.lang.Short</property-class>
      </property>
   </converter>
</faces-config>

If i do not specify the "property" element it takes by default 2 fraction digits and it works fine, but if I include the "property" element I am not able to specify it in the web-client configuraction and it gets automatically 2 digits for the fraction (useless). This is the corresponding part of my web-client-config-custom.xml file:


   <config evaluator="node-type" condition="eun:ofertaProyecto">
      <property-sheet>
         <show-property name="eun:descuentoOferta" display-label="Descuento" converter="PriceConverter" fractionDigits="3"/>
         <show-property name="eun:portesOferta" display-label="Portes" converter="PriceConverter" fractionDigits="3"/>
         <show-property name="eun:tipoIvaOferta" display-label="Tipo IVA" converter="PriceConverter" fractionDigits="3"/>
         <show-property name="eun:formaPagoOferta" display-label="Forma de pago" />
         <show-property name="eun:plazoEntregaOferta" display-label="Plazo de entrega" />
         <show-property name="eun:validezOferta" display-label="Valido hasta" />
      </property-sheet>
   </config>

Alfresco (Community 3.0.0 Stable) does not complain at all but it just uses 0 fraction digits. Is there a way to do it or simply Alfresco is unable to receive parameters for the converter ?

If it is impossible I guess I'll have to extend the class for each use case, horrible and hackish but it surely works.

I have opened a new topic to discuss this.

[Offtopic]: By the way, how do I grant some points to the helpful replies in the forum ?

billerby
Champ on-the-rise
Champ on-the-rise
Nice to see you got an (almost) working converter (no parameters). I think the points system is constructed in the way that only the author of the thread can give points for helpful replies.

/Erik - still without points :cry:

iblanco
Confirmed Champ
Confirmed Champ
Smiley Sad The converter works but the components validation is not locale aware, so if I don't write the numbers in english the validation does not post the changes, and if I'm using Alfresco in Spanish, as my converter is locale aware does not interpret the number right.

That is quite discouraging and strange, I can't believe that nor Alfresco nor anyone in the community is using Alfresco with fixed precision numbers in a language other than english.

I suppose I have 3 options:

- Forget Locale awareness.
- Create my own component.
- Represent all numeric values as Strings and build a converter in order to manage locale related conversions my own.

The first one sucks, as Alfresco is supposed to be locale aware for most of things.
The second one seems complex.
The third one seems not very efficient, but its quite easy. The problem is that probably there won't be the possibility to validate data correctnes before posting.

I don't know what to do, I might give a go to the second one.

iblanco
Confirmed Champ
Confirmed Champ
I extended the standard TextFieldGenerator:

public class NumberLocaleAwareTextFieldGenerator extends TextFieldGenerator {

   private static Log logger = LogFactory
         .getLog(NumberLocaleAwareTextFieldGenerator.class);

   @Override
   protected void setupConstraints(FacesContext context,
         UIPropertySheet propertySheet, PropertySheetItem property,
         PropertyDefinition propertyDef, UIComponent component) {

      // if the property type is a number based type and the property
      // sheet is in edit mode and validation is turned, on add the
      // validateIsNumber validation function
      if (propertySheet.inEditMode() && propertySheet.isValidationEnabled()
            && propertyDef != null) {
         // check the type of the property is a number
         if (propertyDef.getDataType().getName().equals(
               DataTypeDefinition.DOUBLE)
               || propertyDef.getDataType().getName().equals(
                     DataTypeDefinition.FLOAT)
               || propertyDef.getDataType().getName().equals(
                     DataTypeDefinition.INT)
               || propertyDef.getDataType().getName().equals(
                     DataTypeDefinition.LONG)) {
            List<String> params = new ArrayList<String>(5);

            // Get the current locale
            Locale locale = context.getViewRoot().getLocale();
            DecimalFormatSymbols symbols = new DecimalFormatSymbols(locale);
            char decimalSeparator = symbols.getDecimalSeparator();
            char thousandSeparator = symbols.getGroupingSeparator();

            if (logger.isDebugEnabled()) {
               logger.debug("Preparing number validation for locale " + locale +
                     " with decimal separator '" + decimalSeparator
                     + "' and thousand separator '" + thousandSeparator +"'.");
            }

            // add the value parameter
            String value = "document.getElementById('"
                  + component.getClientId(context) + "')";
            params.add(value);

            // add the validation failed message to show
            String msg = Application.getMessage(context,
                  "validation_is_number");
            addStringConstraintParam(params, MessageFormat.format(msg,
                  new Object[] { property.getResolvedDisplayLabel() }));

            // Tell Javascript about locale related separators
            addStringConstraintParam(params, String.valueOf(decimalSeparator));
            addStringConstraintParam(params, String.valueOf(thousandSeparator));

            // add the validation case to the property sheet
            propertySheet.addClientValidation(new ClientValidation(
                  "validateIsLocalisedNumber", params, false));
         }
      }
   }
}

And added a couple of new functions to validation.js:

/**
* Function taken from:
*     http://simonwillison.net/2006/Jan/20/escape/
*/
function regexp_escape(text)
{
   if (!arguments.callee.sRE) {
       var specials = [
         '/', '.', '*', '+', '?', '|',
         '(', ')', '[', ']', '{', '}', '\\'
       ];
       arguments.callee.sRE = new RegExp(
         '(\\' + specials.join('|\\') + ')', 'g'
       );
     }
   return text.replace(arguments.callee.sRE, '\\$1');
}

/**
* Ensures the value of the 'control' is a number
* as specified by the supplied locale related separators.
*
* @return true if the value is a number
*/
function validateIsLocalisedNumber(control, message, decimalSeparator, thousandSeparator, showMessage)
{
   var result = true;
  
   var decimalRegexp = new RegExp("^[-]?([0-9]*|[0-9]{1,3}("
         + regexp_escape(thousandSeparator) + "[0-9]{3})*)"
         + "(" + regexp_escape(decimalSeparator)  + "[0-9]+)?$");
   var value = control.value;
  
   if (!decimalRegexp.test(value)) {
      informUser(control, message, showMessage);
      return false;
   }  
   return true;
}

This works, but seems quite hackish for a such a simple thing….

billerby
Champ on-the-rise
Champ on-the-rise
To make it less hackish, I would generate the JavaScript in your component instead of modifying  an Alfresco core file, avoiding the need to repatch when upgrading Alfresco.

/Erik

iblanco
Confirmed Champ
Confirmed Champ
You are right Erik:

I'm trying to do it but I do not know very well how to do it. I have tried overriding the createComponent method of the textFieldGenerator:

   @Override
   protected UIComponent createComponent(FacesContext context,
         UIPropertySheet propertySheet, PropertySheetItem item) {
      ResponseWriter out = context.getResponseWriter();
      if (propertySheet.inEditMode()) {
         try {
            if (logger.isDebugEnabled()) {
               logger.debug("Including custom validation JavaScript file.");
            }
            out.write("\n<script type='text/javascript' src='");
            out.write(context.getExternalContext().getRequestContextPath());
            out.write("/scripts/custom/validation.js");
            out.write("'></script>\n");            
         } catch (IOException e) {
            if (logger.isErrorEnabled()) {
               logger.error("Unable to include custom validtaion JavaScript file.");
            }
            if (logger.isDebugEnabled()) {
               logger.debug(e);
            }
         }
      }
      UIComponent component = super.createComponent(context, propertySheet,
            item);
      return component;
   }

But this approach has a serious problems, the script file is included as many times as the generator is called (once per each property using the generator). It works, but It seems at least horrible or problematic.

It seems like the proper place to include this scrip tag would be "UIPropertySheet" class but I suppose I would have to do a complete new generator in order for it to use the extended version of the UIPropertySheet. I can't find an easy way to add the script in a "once by request/page render" way.

Is it possible, at all? Any suggestion? Thanks.

iblanco
Confirmed Champ
Confirmed Champ
Smiley Tongue I've got it

It's not the nicest way on the world of doing it, but its less hackish, it works and it doesn't touch Alfresco's core classes.

I used the HTML separator offered by Alfresco and incrusted the required Script through it including the separator in the property page (only in edit mode)

faces-config-custom.xml has:

   <!– Generates the required JavaScript in order for NumberLocaleAwareTextFieldGenerator to work.–>
   <managed-bean>
      <managed-bean-name>LocaleAwareNumberValidatorGenerator</managed-bean-name>
      <managed-bean-class>org.alfresco.web.bean.generator.HtmlSeparatorGenerator</managed-bean-class>
      <managed-bean-scope>request</managed-bean-scope>
      <managed-property>
         <property-name>html</property-name>
         <value>
            <![CDATA[
<script type="text/javascript">        
/**
* Function taken from:
*     http://simonwillison.net/2006/Jan/20/escape/
*/
function regexp_escape(text)
{
   if (!arguments.callee.sRE) {
       var specials = [
         '/', '.', '*', '+', '?', '|',
         '(', ')', '[', ']', '{', '}', '\\'
       ];
       arguments.callee.sRE = new RegExp(
         '(\\' + specials.join('|\\') + ')', 'g'
       );
     }
   return text.replace(arguments.callee.sRE, '\\$1');
}

/**
* Ensures the value of the 'control' is a number
* as specified by the supplied locale related separators.
*
* @return true if the value is a number
*/
function validateIsLocalisedNumber(control, message, decimalSeparator, thousandSeparator, showMessage)
{
   var result = true;
  
   var decimalRegexp = new RegExp("^[-]?([0-9]*|[0-9]{1,3}("
         + regexp_escape(thousandSeparator) + "[0-9]{3})*)"
         + "(" + regexp_escape(decimalSeparator)  + "[0-9]+)?$");
   var value = control.value;
  
   if (!decimalRegexp.test(value)) {
      informUser(control, message, showMessage);
      return false;
   }  
   return true;
}
</script>      
         ]]>
         </value>
      </managed-property>
   </managed-bean>

And web-client-config-custom.xml has:

   <config evaluator="node-type" condition="eun:ofertaConcurso">
      <property-sheet>
         <separator name="sep" show-in-view-mode="false" component-generator="LocaleAwareNumberValidatorGenerator" />      
         <show-property name="eun:descuentoOferta" display-label="Descuento" component-generator="NumberLocaleAwareTextFieldGenerator" converter="TwoDecimalConverter" />
         <show-property name="eun:portesOferta" display-label="Portes" component-generator="NumberLocaleAwareTextFieldGenerator" converter="TwoDecimalConverter" />
         <show-property name="eun:tipoIvaOferta" display-label="Tipo IVA" component-generator="NumberLocaleAwareTextFieldGenerator" converter="TwoDecimalConverter" />
         <show-property name="eun:formaPagoOferta" display-label="Forma de pago" />
         <show-property name="eun:plazoEntregaOferta" display-label="Plazo de entrega" />
         <show-property name="eun:validezOferta" display-label="Valido hasta" />
      </property-sheet>
   </config>

Great, I'm quite happy now…

billerby
Champ on-the-rise
Champ on-the-rise
Great, that was a nice feature I wasn't aware of. That can come to my use soon Smiley Happy

Thanks
/Erik