/* * This program is free software; you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software * Foundation. * * You should have received a copy of the GNU Lesser General Public License along with this * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html * or from the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Lesser General Public License for more details. * * Copyright (c) 2006 - 2013 Pentaho Corporation and Contributors. All rights reserved. */ package org.pentaho.reporting.libraries.formula.typing; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.pentaho.reporting.libraries.base.config.Configuration; import org.pentaho.reporting.libraries.base.util.IOUtils; import org.pentaho.reporting.libraries.formula.EvaluationException; import org.pentaho.reporting.libraries.formula.FormulaContext; import org.pentaho.reporting.libraries.formula.LocalizationContext; import org.pentaho.reporting.libraries.formula.lvalues.DefaultDataTable; import org.pentaho.reporting.libraries.formula.lvalues.LValue; import org.pentaho.reporting.libraries.formula.lvalues.StaticValue; import org.pentaho.reporting.libraries.formula.lvalues.TypeValuePair; import org.pentaho.reporting.libraries.formula.typing.coretypes.AnyType; import org.pentaho.reporting.libraries.formula.typing.coretypes.DateTimeType; import org.pentaho.reporting.libraries.formula.typing.coretypes.LogicalType; import org.pentaho.reporting.libraries.formula.typing.coretypes.NumberType; import org.pentaho.reporting.libraries.formula.typing.coretypes.TextType; import org.pentaho.reporting.libraries.formula.typing.sequence.AnyNumberSequence; import org.pentaho.reporting.libraries.formula.typing.sequence.AnySequence; import org.pentaho.reporting.libraries.formula.typing.sequence.DefaultNumberSequence; import org.pentaho.reporting.libraries.formula.util.DateUtil; import org.pentaho.reporting.libraries.formula.util.HSSFDateUtil; import org.pentaho.reporting.libraries.formula.util.NumberUtil; import java.lang.reflect.Array; import java.math.BigDecimal; import java.sql.Clob; import java.sql.Time; import java.text.DateFormat; import java.text.NumberFormat; import java.text.ParsePosition; import java.util.Collection; import java.util.Date; import java.util.Iterator; import java.util.List; /** * Creation-Date: 02.11.2006, 12:46:08 * * @author Thomas Morgner */ public class DefaultTypeRegistry implements TypeRegistry { private static final Log logger = LogFactory.getLog( DefaultTypeRegistry.class ); private static class ArrayConverterCallback implements ArrayCallback { private Object retval; private Type targetType; private ArrayConverterCallback( final Object retval, final Type targetType ) { this.retval = retval; this.targetType = targetType; } public LValue getRaw( final int row, final int column ) { return new StaticValue( retval, targetType ); } public Object getValue( final int row, final int column ) throws EvaluationException { if ( row == 0 && column == 0 ) { return retval; } return null; } public Type getType( final int row, final int column ) throws EvaluationException { if ( row == 0 && column == 0 ) { return targetType; } return null; } public int getColumnCount() { return 1; } public int getRowCount() { return 1; } } private static final BigDecimal NUM_TRUE = new BigDecimal( "1" ); private static final BigDecimal NUM_FALSE = new BigDecimal( "0" ); private static final BigDecimal ZERO = NUM_FALSE; private FormulaContext context; public DefaultTypeRegistry() { } /** * Returns an comparator for the given types. * * @param type1 * @param type2 * @return */ public ExtendedComparator getComparator( final Type type1, final Type type2 ) { final DefaultComparator comparator = new DefaultComparator(); comparator.inititalize( context ); return comparator; } /** * converts the object of the given type into a number. If the object is not convertible, a NumberFormatException is * thrown. If the given value is null or not parsable as number, return null. * * @param sourceType * @param value * @return * @throws NumberFormatException if the type cannot be represented as number. */ public Number convertToNumber( final Type sourceType, final Object value ) throws EvaluationException { final LocalizationContext localizationContext = context.getLocalizationContext(); if ( value == null ) { // there's no point in digging deeper - there *is* no value .. throw TypeConversionException.getInstance(); } final boolean isAnyType = sourceType.isFlagSet( Type.ANY_TYPE ); if ( sourceType.isFlagSet( Type.NUMERIC_TYPE ) || isAnyType ) { if ( sourceType.isFlagSet( Type.DATETIME_TYPE ) || sourceType.isFlagSet( Type.TIME_TYPE ) || sourceType.isFlagSet( Type.DATE_TYPE ) || isAnyType ) { if ( value instanceof Date ) { final BigDecimal serial = HSSFDateUtil.getExcelDate( (Date) value ); return DateUtil.normalizeDate( serial, sourceType ); } } if ( value instanceof Number ) { return (Number) value; } } if ( sourceType.isFlagSet( Type.LOGICAL_TYPE ) || isAnyType ) { if ( value instanceof Boolean ) { if ( Boolean.TRUE.equals( value ) ) { return NUM_TRUE; } else { return NUM_FALSE; } } } if ( sourceType.isFlagSet( Type.TEXT_TYPE ) || isAnyType ) { final String val = computeStringValue( value ); // first, try to parse the value as a big-decimal. try { return new BigDecimal( val ); } catch ( NumberFormatException e ) { // ignore .. } for ( final DateFormat df : localizationContext.getDateFormats( DateTimeType.DATETIME_TYPE ) ) { final Date date = parse( df, val ); if ( date != null ) { return HSSFDateUtil.getExcelDate( date ); } } for ( final DateFormat df : localizationContext.getDateFormats( DateTimeType.DATE_TYPE ) ) { final Date date = parse( df, val ); if ( date != null ) { return HSSFDateUtil.getExcelDate( date ); } } for ( final DateFormat df : localizationContext.getDateFormats( DateTimeType.TIME_TYPE ) ) { final Date date = parse( df, val ); if ( date != null ) { return HSSFDateUtil.getExcelDate( date ); } } // then checking for numbers for ( final NumberFormat format : localizationContext.getNumberFormats() ) { final Number number = parse( format, val ); if ( number != null ) { return number; } } } throw TypeConversionException.getInstance(); } private static Number parse( final NumberFormat format, final String source ) { final ParsePosition parsePosition = new ParsePosition( 0 ); final Number result = format.parse( source, parsePosition ); if ( parsePosition.getIndex() == 0 || parsePosition.getIndex() != source.length() ) { return null; } return result; } private static Date parse( final DateFormat format, final String source ) { final ParsePosition parsePosition = new ParsePosition( 0 ); final Date result = format.parse( source, parsePosition ); if ( parsePosition.getIndex() == 0 || parsePosition.getIndex() != source.length() ) { return null; } return result; } /** * @param configuration * @param formulaContext * @deprecated Use the single-argument function instead. */ public void initialize( final Configuration configuration, final FormulaContext formulaContext ) { this.initialize( formulaContext ); } public void initialize( final FormulaContext formulaContext ) { if ( formulaContext == null ) { throw new NullPointerException(); } this.context = formulaContext; } public String convertToText( final Type type1, final Object value ) throws EvaluationException { if ( value == null ) { return ""; } // already converted or compatible if ( type1.isFlagSet( Type.TEXT_TYPE ) ) { // no need to check whatever it is a String return computeStringValue( value ); } if ( type1.isFlagSet( Type.LOGICAL_TYPE ) ) { if ( value instanceof Boolean ) { final Boolean b = (Boolean) value; if ( Boolean.TRUE.equals( b ) ) { return "TRUE"; } else { return "FALSE"; } } else { throw TypeConversionException.getInstance(); } } // 2 types of numeric : numbers and dates if ( type1.isFlagSet( Type.NUMERIC_TYPE ) ) { final LocalizationContext localizationContext = context.getLocalizationContext(); if ( type1.isFlagSet( Type.DATETIME_TYPE ) || type1.isFlagSet( Type.DATE_TYPE ) || type1 .isFlagSet( Type.TIME_TYPE ) ) { final Date d = convertToDate( type1, value ); final List dateFormats = localizationContext.getDateFormats( type1 ); if ( dateFormats != null && dateFormats.size() >= 1 ) { final DateFormat format = (DateFormat) dateFormats.get( 0 ); return format.format( d ); } else { // fallback return DateFormat.getDateTimeInstance ( DateFormat.FULL, DateFormat.FULL, localizationContext.getLocale() ).format( d ); } } else { try { final Number n = convertToNumber( type1, value ); final List<NumberFormat> numberFormats = localizationContext.getNumberFormats(); if ( numberFormats.isEmpty() ) { // use the canonical format .. return NumberFormat.getNumberInstance( localizationContext.getLocale() ).format( n ); } else { numberFormats.get( 0 ).format( n ); } } catch ( EvaluationException nfe ) { // ignore .. } } } return computeStringValue( value ); } private String computeStringValue( final Object retval ) throws EvaluationException { if ( retval instanceof Clob ) { try { return IOUtils.getInstance().readClob( (Clob) retval ); } catch ( Exception e ) { return null; } } if ( retval instanceof String ) { return (String) retval; } if ( retval != null ) { return unwrap( retval, new StringBuilder() ).toString(); } return null; } private StringBuilder unwrap( final Object retval, final StringBuilder b ) throws EvaluationException { if ( retval.getClass().isArray() ) { return unwrapArray( retval, b ); } if ( retval instanceof Sequence ) { return unwrapSequence( (Sequence) retval, b ); } if ( retval instanceof ArrayCallback ) { return unwrapArrayCallback( (ArrayCallback) retval, b ); } if ( retval instanceof Collection ) { return unwrapCollection( (Collection<?>) retval, b ); } return b.append( retval ); } private StringBuilder unwrapCollection( final Collection<?> retval, final StringBuilder b ) throws EvaluationException { final Iterator<?> it = retval.iterator(); while ( it.hasNext() ) { unwrap( it.next(), b ); if ( it.hasNext() ) { b.append( ", " ); } } return b; } private StringBuilder unwrapSequence( final Sequence retval, final StringBuilder b ) throws EvaluationException { while ( retval.hasNext() ) { unwrap( retval.next(), b ); if ( retval.hasNext() ) { b.append( ", " ); } } return b; } private StringBuilder unwrapArrayCallback( final ArrayCallback retval, final StringBuilder b ) throws EvaluationException { int rc = retval.getRowCount(); int cc = retval.getColumnCount(); for ( int r = 0; r < rc; r += 1 ) { for ( int c = 0; c < cc; c += 1 ) { if ( r != 0 || c != 0 ) { b.append( ", " ); } unwrap( retval.getValue( r, c ), b ); } } return b; } private StringBuilder unwrapArray( final Object retval, final StringBuilder b ) throws EvaluationException { int length = Array.getLength( retval ); for ( int i = 0; i < length; i += 1 ) { if ( i != 0 ) { b.append( ", " ); } unwrap( Array.get( retval, i ), b ); } return b; } public Boolean convertToLogical( final Type type1, final Object value ) throws TypeConversionException { if ( value == null ) { return Boolean.FALSE; } // already converted or compatible if ( type1.isFlagSet( Type.LOGICAL_TYPE ) || type1.isFlagSet( Type.ANY_TYPE ) ) { if ( value instanceof Boolean ) { return (Boolean) value; } // fallback if ( "true".equalsIgnoreCase( String.valueOf( value ) ) ) { return Boolean.TRUE; } return Boolean.FALSE; } if ( type1.isFlagSet( Type.NUMERIC_TYPE ) ) { // no need to check between different types of numeric if ( value instanceof Number ) { final Number num = (Number) value; if ( !ZERO.equals( num ) ) { return Boolean.TRUE; } } // fallback return Boolean.FALSE; } if ( type1.isFlagSet( Type.TEXT_TYPE ) ) { // no need to convert it to String try { final String str = computeStringValue( value ); if ( "TRUE".equalsIgnoreCase( str ) ) { return Boolean.TRUE; } else if ( "FALSE".equalsIgnoreCase( str ) ) { return Boolean.FALSE; } } catch ( final EvaluationException e ) { throw TypeConversionException.getInstance(); } } throw TypeConversionException.getInstance(); } public Date convertToDate( final Type type1, final Object value ) throws EvaluationException { if ( type1.isFlagSet( Type.NUMERIC_TYPE ) || type1.isFlagSet( Type.ANY_TYPE ) ) { if ( type1.isFlagSet( Type.DATE_TYPE ) || type1.isFlagSet( Type.DATETIME_TYPE ) || type1.isFlagSet( Type.TIME_TYPE ) || type1.isFlagSet( Type.ANY_TYPE ) ) { if ( value instanceof Date ) { return DateUtil.normalizeDate( (Date) value, type1 ); } } } final Number serial = convertToNumber( type1, value ); final BigDecimal bd = NumberUtil.getAsBigDecimal( serial ); return HSSFDateUtil.getJavaDate( bd ); } public ArrayCallback convertToArray( final Type type, final Object value ) throws EvaluationException { if ( value instanceof ArrayCallback ) { return (ArrayCallback) value; } if ( value == null ) { return new DefaultDataTable().getAsArray(); } final Class valueType = value.getClass(); if ( valueType.isArray() == false ) { if ( value instanceof Collection ) { final Collection colVal = (Collection) value; final DefaultDataTable table = new DefaultDataTable(); final Iterator iterator = colVal.iterator(); int i = 0; while ( iterator.hasNext() ) { table.setObject( i, 0, new StaticValue( iterator.next() ) ); i += 1; } return table.getAsArray(); } return new ArrayConverterCallback( value, type ); } final Class componentType = valueType.getComponentType(); if ( componentType.isArray() ) { final DefaultDataTable table = new DefaultDataTable(); final int length = Array.getLength( value ); for ( int row = 0; row < length; row++ ) { final Object innerArray = Array.get( value, row ); final int innerLength = Array.getLength( innerArray ); for ( int col = 0; col < innerLength; col++ ) { table.setObject( row, col, new StaticValue( Array.get( innerArray, col ) ) ); } } return table.getAsArray(); } final DefaultDataTable table = new DefaultDataTable(); final int length = Array.getLength( value ); for ( int i = 0; i < length; i++ ) { table.setObject( i, 0, new StaticValue( Array.get( value, i ) ) ); } return table.getAsArray(); } /** * A internal method that converts the given value-pair into a sequence. * * @param targetType * @param valuePair * @return * @throws TypeConversionException if there was a error while converting types. */ private TypeValuePair convertToSequence( final Type targetType, final TypeValuePair valuePair ) throws EvaluationException { if ( targetType.isFlagSet( Type.NUMERIC_SEQUENCE_TYPE ) ) { return new TypeValuePair ( targetType, convertToNumberSequence( valuePair.getType(), valuePair.getValue(), true ) ); } return new TypeValuePair( targetType, convertToSequence( valuePair.getType(), valuePair.getValue() ) ); } public Sequence convertToSequence( final Type type, final Object value ) throws EvaluationException { // sclar if ( type.isFlagSet( Type.SCALAR_TYPE ) ) { return new AnySequence( new StaticValue( value, type ), context ); } // else already a sequence else if ( type.isFlagSet( Type.SEQUENCE_TYPE ) ) { if ( value instanceof Sequence ) { return (Sequence) value; } else { logger.warn( "Assertation failure: Type declared to be a sequence, but no sequence found inside." ); throw TypeConversionException.getInstance(); } } // else an array source else if ( type.isFlagSet( Type.ARRAY_TYPE ) ) { if ( value instanceof ArrayCallback ) { return new AnySequence( (ArrayCallback) value, context ); } else if ( value instanceof Object[] ) { return new AnySequence( convertToArray( type, value ), context ); } else { logger.warn( "Assertation failure: Type declared to be array, but no array callback found inside." ); throw TypeConversionException.getInstance(); } } throw TypeConversionException.getInstance(); } public NumberSequence convertToNumberSequence( final Type type, final Object value, final boolean strict ) throws EvaluationException { // sequence array if ( type.isFlagSet( Type.NUMERIC_SEQUENCE_TYPE ) ) { if ( value instanceof DefaultNumberSequence ) { return (NumberSequence) value; } else { // a empty sequence ... return new DefaultNumberSequence( context ); } } // array else if ( type.isFlagSet( Type.ARRAY_TYPE ) ) { if ( value instanceof ArrayCallback ) { if ( strict ) { return new DefaultNumberSequence( (ArrayCallback) value, context ); } else { return new AnyNumberSequence( (ArrayCallback) value, context ); } } else { logger.warn( "Assertation failure: Type declared to be array, but no array callback found inside." ); throw TypeConversionException.getInstance(); } } // else scalar if ( type.isFlagSet( Type.SCALAR_TYPE ) || type.isFlagSet( Type.NUMERIC_TYPE ) ) { return new DefaultNumberSequence ( new StaticValue( convertToNumber( type, value ), NumberType.GENERIC_NUMBER ), context ); } else { return new DefaultNumberSequence( context ); } } /** * Checks whether the target type would accept the specified value object and value type.<br/> This method is called * for auto conversion of fonction parameters using the conversion type declared by the function metadata. * * @param targetType * @param valuePair * @noinspection ObjectEquality is tested at the end of the method for performance reasons only. We just want to * detect whether a new object has been created or not. */ public TypeValuePair convertTo( final Type targetType, final TypeValuePair valuePair ) throws EvaluationException { if ( targetType.isFlagSet( Type.ARRAY_TYPE ) ) { if ( valuePair.getType().isFlagSet( Type.ARRAY_TYPE ) ) { return valuePair; } else if ( targetType.isFlagSet( Type.SEQUENCE_TYPE ) ) { return convertTo( targetType, valuePair ); } else { final Object o = valuePair.getValue(); if ( o != null && o.getClass().isArray() ) { return new TypeValuePair( targetType, convertToArray( valuePair.getType(), o ) ); } else { final Object retval = convertPlainToPlain( targetType, valuePair.getType(), valuePair.getValue() ); return new TypeValuePair( targetType, new ArrayConverterCallback( retval, targetType ) ); } } } else if ( targetType.isFlagSet( Type.SEQUENCE_TYPE ) ) { if ( valuePair.getType().isFlagSet( Type.ARRAY_TYPE ) ) { return convertToSequence( targetType, valuePair ); } else if ( targetType.isFlagSet( Type.SEQUENCE_TYPE ) ) { return valuePair; } else { final Object retval = convertPlainToPlain( targetType, valuePair.getType(), valuePair.getValue() ); final ArrayConverterCallback converterCallback = new ArrayConverterCallback( retval, targetType ); return convertToSequence( targetType, new TypeValuePair( AnyType.ANY_ARRAY, converterCallback ) ); } } // else scalar final Object value = valuePair.getValue(); final Object o = convertPlainToPlain( targetType, valuePair.getType(), value ); if ( value == o ) { return valuePair; } return new TypeValuePair( targetType, o ); } private Object convertPlainToPlain( final Type targetType, final Type sourceType, final Object value ) throws EvaluationException { if ( targetType.isFlagSet( Type.NUMERIC_TYPE ) ) { if ( targetType.isFlagSet( Type.LOGICAL_TYPE ) ) { if ( sourceType.isFlagSet( Type.LOGICAL_TYPE ) ) { return value; } return convertToLogical( sourceType, value ); } if ( value instanceof Date ) { if ( targetType.isFlagSet( Type.DATE_TYPE ) || targetType.isFlagSet( Type.DATETIME_TYPE ) || targetType.isFlagSet( Type.TIME_TYPE ) ) { final Date toJavaDate = (Date) value; return DateUtil.normalizeDate( toJavaDate, targetType, false ); } } final Number serial = convertToNumber( sourceType, value ); if ( targetType.isFlagSet( Type.DATE_TYPE ) || targetType.isFlagSet( Type.DATETIME_TYPE ) || targetType.isFlagSet( Type.TIME_TYPE ) ) { final BigDecimal fromAsBigDecimal = NumberUtil.getAsBigDecimal( serial ); final BigDecimal normalizedSerial = DateUtil.normalizeDate( fromAsBigDecimal, targetType ); final Date toJavaDate = HSSFDateUtil.getJavaDate( normalizedSerial ); return DateUtil.normalizeDate( toJavaDate, targetType, false ); } return serial; } else if ( targetType.isFlagSet( Type.TEXT_TYPE ) ) { return convertToText( sourceType, value ); } // Unknown type - ignore it, crash later :) return value; } public Type guessTypeOfObject( final Object o ) { if ( o instanceof Number ) { return NumberType.GENERIC_NUMBER; } else if ( o instanceof Time ) { return DateTimeType.TIME_TYPE; } else if ( o instanceof java.sql.Date ) { return DateTimeType.DATE_TYPE; } else if ( o instanceof Date ) { return DateTimeType.DATETIME_TYPE; } else if ( o instanceof Boolean ) { return LogicalType.TYPE; } else if ( o instanceof String ) { return TextType.TYPE; } return AnyType.TYPE; } }