/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.kie.dmn.feel.util;
import org.kie.dmn.feel.lang.EvaluationContext;
import org.kie.dmn.feel.lang.FEELProperty;
import org.kie.dmn.feel.lang.ast.InfixOpNode;
import org.kie.dmn.feel.runtime.Range;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.MathContext;
import java.time.Duration;
import java.time.Period;
import java.time.temporal.ChronoField;
import java.time.temporal.Temporal;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BiPredicate;
import java.util.regex.Pattern;
import java.util.stream.Stream;
public class EvalHelper {
public static final Logger LOG = LoggerFactory.getLogger( EvalHelper.class );
private static final Pattern SPACES_PATTERN = Pattern.compile( "[\\s\u00A0]+" );
public static String normalizeVariableName(String name) {
return SPACES_PATTERN.matcher( name.trim() ).replaceAll( " " );
}
public static BigDecimal getBigDecimalOrNull(Object value) {
if ( !(value instanceof Number || value instanceof String) ) {
return null;
}
if ( !BigDecimal.class.isAssignableFrom( value.getClass() ) ) {
if ( value instanceof Long || value instanceof Integer || value instanceof Short || value instanceof Byte ||
value instanceof AtomicLong || value instanceof AtomicInteger ) {
value = new BigDecimal( ((Number) value).longValue(), MathContext.DECIMAL128 );
} else if ( value instanceof BigInteger ) {
value = new BigDecimal( ((BigInteger) value).toString(), MathContext.DECIMAL128 );
} else if ( value instanceof String ) {
// we need to remove leading zeros to prevent octal conversion
value = new BigDecimal( ((String) value).replaceFirst("^0+(?!$)", ""), MathContext.DECIMAL128 );
} else {
value = new BigDecimal( ((Number) value).doubleValue(), MathContext.DECIMAL128 );
}
}
return (BigDecimal) value;
}
public static Object coerceNumber(Object value) {
if ( value instanceof Number && !(value instanceof BigDecimal) ) {
return getBigDecimalOrNull( value );
} else {
return value;
}
}
public static Boolean getBooleanOrNull(Object value) {
if ( value == null || !(value instanceof Boolean) ) {
return null;
}
return (Boolean) value;
}
public static String unescapeString(String text) {
if ( text == null ) {
return null;
}
if ( text.length() >= 2 && text.startsWith( "\"" ) && text.endsWith( "\"" ) ) {
// remove the quotes
text = text.substring( 1, text.length() - 1 );
}
if ( text.indexOf( '\\' ) >= 0 ) {
// might require un-escaping
StringBuilder r = new StringBuilder();
for ( int i = 0; i < text.length(); i++ ) {
char c = text.charAt( i );
if ( c == '\\' ) {
if ( text.length() > i + 1 ) {
i++;
char cn = text.charAt( i );
switch ( cn ) {
case 'b':
r.append( '\b' );
break;
case 't':
r.append( '\t' );
break;
case 'n':
r.append( '\n' );
break;
case 'f':
r.append( '\f' );
break;
case 'r':
r.append( '\r' );
break;
case '"':
r.append( '"' );
break;
case '\'':
r.append( '\'' );
break;
case '\\':
r.append( '\\' );
break;
case 'u': {
if ( text.length() >= i + 5 ) {
// escape unicode
String hex = text.substring( i + 1, i + 5 );
char[] chars = Character.toChars( Integer.parseInt( hex, 16 ) );
r.append( chars );
i += 4;
} else {
// not really unicode
r.append( "\\" ).append( cn );
}
break;
}
}
}
} else {
r.append( c );
}
}
text = r.toString();
}
return text;
}
public static Object getValue(Object current, String property)
throws IllegalAccessException, InvocationTargetException {
if ( current == null ) {
return null;
} else if ( current instanceof Map ) {
current = ((Map) current).get( property );
} else if ( current instanceof Period ) {
switch ( property ) {
case "years":
current = ((Period) current).getYears();
break;
case "months":
current = ((Period) current).getMonths()%12;
break;
case "days":
current = ((Period) current).getDays()%30;
break;
default:
return null;
}
} else if ( current instanceof Duration ) {
switch ( property ) {
case "days":
current = ((Duration) current).toDays();
break;
case "hours":
current = ((Duration) current).toHours()%24;
break;
case "minutes":
current = ((Duration) current).toMinutes()%60;
break;
case "seconds":
current = ((Duration) current).getSeconds()%60;
break;
default:
return null;
}
} else if ( current instanceof Temporal ) {
switch ( property ) {
case "year":
current = ((Temporal) current).get( ChronoField.YEAR );
break;
case "month":
current = ((Temporal) current).get( ChronoField.MONTH_OF_YEAR );
break;
case "day":
current = ((Temporal) current).get( ChronoField.DAY_OF_MONTH );
break;
case "hour":
current = ((Temporal) current).get( ChronoField.HOUR_OF_DAY );
break;
case "minute":
current = ((Temporal) current).get( ChronoField.MINUTE_OF_HOUR );
break;
case "second":
current = ((Temporal) current).get( ChronoField.SECOND_OF_MINUTE );
break;
case "time offset":
case "timezone":
current = Duration.ofSeconds( ((Temporal) current).get( ChronoField.OFFSET_SECONDS ) );
break;
default:
return null;
}
} else {
Method getter = getGenericAccessor( current.getClass(), property );
if ( getter != null ) {
current = getter.invoke( current );
} else {
return null;
}
}
return coerceNumber( current );
}
/**
* FEEL annotated or else Java accessor.
* @param clazz
* @param field
* @return
*/
public static Method getGenericAccessor(Class<?> clazz, String field) {
LOG.trace( "getGenericAccessor({}, {})", clazz, field );
return Stream.of( clazz.getMethods() )
.filter( m -> Optional.ofNullable( m.getAnnotation( FEELProperty.class ) )
.map( ann -> ann.value().equals( field ) )
.orElse( false )
)
.findFirst()
.orElse( getAccessor( clazz, field ) );
}
/**
* JavaBean -spec compliant accessor.
* @param clazz
* @param field
* @return
*/
public static Method getAccessor(Class<?> clazz, String field) {
LOG.trace( "getAccessor({}, {})", clazz, field );
try {
return clazz.getMethod( "get" + ucFirst( field ) );
} catch ( NoSuchMethodException e ) {
try {
return clazz.getMethod( field );
} catch ( NoSuchMethodException e1 ) {
try {
return clazz.getMethod( "is" + ucFirst( field ) );
} catch ( NoSuchMethodException e2 ) {
return null;
}
}
}
}
/**
* Inverse of {@link #getAccessor(Class, String)}
*/
public static Optional<String> propertyFromAccessor(Method accessor) {
if ( accessor.getParameterCount() != 0 || accessor.getReturnType().equals( Void.class ) ) {
return Optional.empty();
}
String methodName = accessor.getName();
if ( methodName.startsWith( "get" ) ) {
return Optional.of( lcFirst( methodName.substring( 3, methodName.length() ) ) );
} else if ( methodName.startsWith( "is" ) ) {
return Optional.of( lcFirst( methodName.substring( 2, methodName.length() ) ) );
} else {
return Optional.of( lcFirst( methodName ) );
}
}
public static String ucFirst(final String name) {
return name.toUpperCase().charAt( 0 ) + name.substring( 1 );
}
public static String lcFirst(final String name) {
return name.toLowerCase().charAt( 0 ) + name.substring( 1 );
}
/**
* Compares left and right operands using the given predicate and returns TRUE/FALSE accordingly
*
* @param left
* @param right
* @param ctx
* @param op
* @return
*/
public static Boolean compare(Object left, Object right, EvaluationContext ctx, BiPredicate<Comparable, Comparable> op) {
if ( left == null || right == null ) {
return null;
} else if ( (left instanceof Period && right instanceof Period ) ) {
// periods have special compare semantics in FEEL as it ignores "days". Only months and years are compared
Period lp = (Period) left;
Period rp = (Period) right;
Integer l = lp.getYears() * 12 + lp.getMonths();
Integer r = rp.getYears() * 12 + rp.getMonths();
return op.test( l, r );
} else if ( (left instanceof String && right instanceof String) ||
(left instanceof Number && right instanceof Number) ||
(left instanceof Boolean && right instanceof Boolean) ||
(left instanceof Comparable && left.getClass().isAssignableFrom( right.getClass() )) ) {
Comparable l = (Comparable) left;
Comparable r = (Comparable) right;
return op.test( l, r );
}
return null;
}
/**
* Compares left and right for equality applying FEEL semantics to specific data types
*
* @param left
* @param right
* @param ctx
* @return
*/
public static Boolean isEqual(Object left, Object right, EvaluationContext ctx ) {
if ( left == null || right == null ) {
return left == right;
}
// spec defines that "a=[a]", i.e., singleton collections should be treated as the single element
// and vice-versa
if( left instanceof Collection && !(right instanceof Collection) && ((Collection)left).size() == 1 ) {
left = ((Collection)left).toArray()[0];
} else if( right instanceof Collection && !(left instanceof Collection) && ((Collection)right).size()==1 ) {
right = ((Collection) right).toArray()[0];
}
if( left instanceof Range && right instanceof Range ) {
return isEqual( (Range)left, (Range) right );
} else if( left instanceof Iterable && right instanceof Iterable ) {
return isEqual( (Iterable)left, (Iterable) right );
} else if( left instanceof Map && right instanceof Map ) {
return isEqual( (Map)left, (Map) right );
}
return compare( left, right, ctx, (l, r) -> l.compareTo( r ) == 0 );
}
private static Boolean isEqual(Range left, Range right) {
return left.equals( right );
}
private static Boolean isEqual(Iterable left, Iterable right) {
Iterator li = left.iterator();
Iterator ri = right.iterator();
while( li.hasNext() && ri.hasNext() ) {
Object l = li.next();
Object r = ri.next();
if ( !isEqual( l, r ) ) return false;
}
return li.hasNext() == ri.hasNext();
}
private static Boolean isEqual(Map<?,?> left, Map<?,?> right) {
if( left.size() != right.size() ) {
return false;
}
for( Map.Entry le : left.entrySet() ) {
Object l = le.getValue();
Object r = right.get( le.getKey() );
if ( !isEqual( l, r ) ) return false;
}
return true;
}
private static Boolean isEqual(Object l, Object r) {
if( l instanceof Iterable && r instanceof Iterable && !isEqual( (Iterable) l, (Iterable) r ) ) {
return false;
} else if( l instanceof Map && r instanceof Map && !isEqual( (Map) l, (Map) r ) ) {
return false;
} else if( l != null && r != null && !l.equals( r ) ) {
return false;
} else if( ( l == null || r == null ) && l != r ) {
return false;
}
return true;
}
}