/******************************************************************************* * Copyright (c) 2014 Open Door Logistics (www.opendoorlogistics.com) * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Lesser Public License v3 * which accompanies this distribution, and is available at http://www.gnu.org/licenses/lgpl.txt ******************************************************************************/ package com.opendoorlogistics.core.tables; import java.awt.Color; import java.awt.image.BufferedImage; import java.awt.image.RenderedImage; import java.io.File; import java.text.NumberFormat; import java.time.LocalDate; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.opendoorlogistics.api.standardcomponents.map.MapTileProvider; import com.opendoorlogistics.api.tables.ODLColumnType; import com.opendoorlogistics.api.tables.ODLTime; import com.opendoorlogistics.core.geometry.ODLGeomImpl; import com.opendoorlogistics.core.geometry.ODLLoadedGeometry; import com.opendoorlogistics.core.geometry.ODLShapefileLinkGeom; import com.opendoorlogistics.core.geometry.ShapefileLink; import com.opendoorlogistics.core.utils.Colours; import com.opendoorlogistics.core.utils.NullComparer; import com.opendoorlogistics.core.utils.Numbers; import com.opendoorlogistics.core.utils.images.ImageUtils; import com.opendoorlogistics.core.utils.strings.StandardisedCache; import com.opendoorlogistics.core.utils.strings.Strings; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.io.WKTReader; /** * Contains all the logic to process different values supported by column type, * e.g. conversion, comparison etc... * * @author Phil * */ public class ColumnValueProcessor { //private static final Pattern STRICT_DOUBLE_TESTER = Pattern.compile(".*[a-zA-Z_].*"); private ColumnValueProcessor() { } private static final WKTReader wktReader = new WKTReader(); public static Class<?> getJavaClass(ODLColumnType colType) { // Each column type must have its own java class or the bean mapping gets confused... switch (colType) { case STRING: return String.class; case LONG: return Long.class; case DOUBLE: return Double.class; case COLOUR: return Color.class; case IMAGE: return BufferedImage.class; case GEOM: return ODLGeomImpl.class; case TIME: return ODLTime.class; case DATE: return LocalDate.class; case MAP_TILE_PROVIDER: return MapTileProvider.class; case FILE_DIRECTORY: return File.class; default: throw new RuntimeException(); } } private static Pattern daysHourPattern = Pattern.compile("\\s*(\\d+)\\s*d[a-z]*\\s*(\\d+)\\s*", Pattern.CASE_INSENSITIVE); private static ODLTime parseTime(String time) { // Possibilities // 1d11:11:11.111 // 1d11:11:11 // 11:11:11.111 // 11:11:11 // 11:11 if (time == null) { return null; } time = time.toLowerCase().trim(); String[] split = time.split(":"); if (split.length < 2) { // must have at least one : return null; } // the first will either be days and hours or just days Long days = 0L; Long hours = 0L; Matcher daysHours = daysHourPattern.matcher(split[0]); if (daysHours.matches()) { days = Numbers.toLong(daysHours.group(1)); hours = Numbers.toLong(daysHours.group(2)); if (days == null || hours == null) { return null; } } else { hours = Numbers.toLong(split[0]); if (hours == null) { return null; } } // get minutes (optional) Long minutes = 0L; if (split.length > 1) { minutes = Numbers.toLong(split[1]); if (minutes == null) { return null; } } // get seconds and milliseconds (optional) Long seconds = 0L; Long millis = 0L; if (split.length > 2) { String[] split2 = split[2].split("\\."); seconds = Numbers.toLong(split2[0]); if (split2.length == 1) { } else if (split2.length == 2) { millis = Numbers.toLong(split2[1]); if (millis == null) { return null; } } else { return null; } if (seconds == null) { return null; } } long value = 0; if (days != null) { value += ODLTime.MILLIS_IN_DAY * days; } if (hours != null) { value += ODLTime.MILLIS_IN_HOUR * hours; } if (minutes != null) { if (minutes < 0 || minutes > 59) { return null; } value += ODLTime.MILLIS_IN_MIN * minutes; } if (seconds != null) { if (seconds < 0 || seconds > 59) { return null; } value += ODLTime.MILLIS_IN_SEC * seconds; } if (millis != null) { if (millis < 0 || millis > 999) { return null; } value += millis; } return new ODLTime(value); } public static Object convertToMe(ODLColumnType convertToMe, Object other) { if (other == null) { // null always converts to null return null; } if(ColumnValueProcessor.getJavaClass(convertToMe).isAssignableFrom(other.getClass())){ return other; } // if (other.getClass() == ColumnValueProcessor.getJavaClass(convertToMe)) { // // check for same class // return other; // } if(convertToMe.isEngineType() ){ // no conversion between engine types return null; } // treat boolean as integer if (other.getClass() == Boolean.class || other.getClass() == Boolean.TYPE) { other = (Boolean) other ? 1 : 0; } if (ODLGeomImpl.class.isInstance(other)) { return convertToMe(convertToMe, other, ODLColumnType.GEOM); } if (convertToMe == ODLColumnType.GEOM) { if (Geometry.class.isInstance(other)) { return new ODLLoadedGeometry((Geometry) other); } else if (ShapefileLink.class.isInstance(other)) { return new ODLShapefileLinkGeom((ShapefileLink) other); } } // do conversion .. find most suitable supported type to avoid string // parsing Class<?> otherCls = other.getClass(); if (ODLTime.class.isInstance(other)) { return convertToMe(convertToMe, other, ODLColumnType.TIME); } if (Numbers.isFloatingPoint(otherCls)) { return convertToMe(convertToMe, ((Number) other).doubleValue(), ODLColumnType.DOUBLE); } if (Numbers.isInteger(otherCls)) { return convertToMe(convertToMe, ((Number) other).longValue(), ODLColumnType.LONG); } if (Color.class.isAssignableFrom(otherCls)) { return convertToMe(convertToMe, other, ODLColumnType.COLOUR); } // just try parsing, will throw exception if fails return convertToMe(convertToMe, other.toString(), ODLColumnType.STRING); } public static Object convertToMe(ODLColumnType convertToMe, Object other, ODLColumnType otherType) { return convertToMe(convertToMe, other, otherType, false); } /** * Strict formatting test for a double (probably needs to be stricter!) * @param s * @return */ private static boolean cannotBeDouble(String s) { // Does the string start with 00, 01, 02, ... , 09? Useful for detecting // French zip codes which are 5 digit, starting with 0 sometimes and should // not be converted to numeric. String stdVal = Strings.std(s); if (stdVal.length() >= 2 && stdVal.charAt(0) == '0' && Character.isDigit(stdVal.charAt(1))) { return true; } // A more elaborate version of this should probably use java regular expressions and take into account order. // For the moment we just include any numberic characters irrespective of order, remembering: // , used for decimal point in some countries // scientific notation 1.2E+3 s = s.trim().toLowerCase(); int n = s.length(); for(int i = 0 ; i< n ; i++){ char c = s.charAt(i); if(!Character.isDigit(c) && (c!='.') && (c!=',') && (c!='e') && (c!='+')){ return true; } } return false; } public static Object convertToMe(ODLColumnType convertToMe, Object other, ODLColumnType otherType, boolean onlyConvertStringIfFormatMatches) { if (otherType == convertToMe) { return other; } if (other == null) { // null always converts to null return null; } // NOTE - the automatic type identification when loading an excel // without a schema // uses this convertToMe, so we shouldn't return default values for // unparsable strings switch (convertToMe) { case DATE: try { switch (otherType) { case LONG: case DOUBLE: return LocalDate.ofEpochDay(((Number) other).longValue()); case STRING: return LocalDate.parse(((String)other).trim()); default: return null; } } catch (Exception e) { return null; } case GEOM: // conversion to geom only supported from wkt string or // shapefilelink try { ShapefileLink link = ShapefileLink.parse(other.toString()); if (link != null) { return new ODLShapefileLinkGeom(link); } Geometry geometry = wktReader.read(other.toString()); return geometry != null ? new ODLLoadedGeometry(geometry) : null; } catch (Throwable e) { return null; } case COLOUR: switch (otherType) { case DOUBLE: case LONG: case TIME: int intVal = ((Number) other).intValue(); return new Color(intVal); case STRING: String sOther = ((String) other).trim(); Color ret = Colours.getColourByName(sOther); if (ret != null) { return ret; } if (sOther.startsWith("#") == false) { if (onlyConvertStringIfFormatMatches) { // must start with # in this case... return null; } sOther = "#" + sOther; } try { return Color.decode(sOther); } catch (Throwable e) { return null; } default: return null; } case DOUBLE: switch (otherType) { case COLOUR: return (double) ((Color) other).getRGB(); case LONG: case TIME: return ((Number) other).doubleValue(); case DATE: return (double)((LocalDate)other).toEpochDay(); case STRING: // Always trim whitespace String sOther = ((String) other).trim(); if (onlyConvertStringIfFormatMatches && cannotBeDouble((String) other)) { return null; } try { // Test if we have a . in the number and if so, use java's // parsedouble which always uses . double number = 0; if (sOther.indexOf(".") != -1) { number = Double.parseDouble(sOther); } else { // If not, use the number format which takes account of // localisation and will use commas in the correct // country. NumberFormat nf = NumberFormat.getInstance(); number = nf.parse((String) sOther).doubleValue(); return number; } return number; } catch (Throwable e) { return null; } default: return null; } case LONG: switch (otherType) { case COLOUR: return (long) ((Color) other).getRGB(); case DOUBLE: case TIME: return ((Number) other).longValue(); case DATE: return ((LocalDate)other).toEpochDay(); case STRING: if (onlyConvertStringIfFormatMatches) { if(cannotBeDouble((String) other)){ return null; }else{ // use our more strict conversion return Numbers.toLong((String) other, true); } } // use our more lenient conversion which also tests for "true", "yes" etc... return Numbers.toLong( other, false); default: return null; } case STRING: switch (otherType) { case GEOM: return ((ODLGeomImpl) other).toText(); case COLOUR: return Colours.toHexString((Color) other); case IMAGE: return ImageUtils.imageToBase64String((RenderedImage) other, "png"); default: return other.toString(); } case IMAGE: switch (otherType) { case STRING: return ImageUtils.base64StringToImage((String) other); default: return null; } case TIME: switch (otherType) { case LONG: return new ODLTime(((Number) other).longValue()); case DOUBLE: return new ODLTime((long) Math.round(((Double) other).doubleValue())); case STRING: return parseTime(other.toString()); default: return null; } default: return null; } } public static boolean isNumeric(ODLColumnType type) { switch (type) { case DOUBLE: case LONG: return true; default: return false; } } public static boolean isBatchKeyCompatible(ODLColumnType type) { switch (type) { case COLOUR: case DOUBLE: case LONG: case STRING: case TIME: return true; default: return false; } } /** * Compare values of the same column type * * @param type * @param valueA * @param valueB * @return */ public static int compareSameType(ODLColumnType type, Object val1, Object val2) { int diff = NullComparer.compare(val1, val2); switch (type) { case STRING: diff = ((String) val1).compareTo((String) val2); break; case LONG: diff = ((Long) val1).compareTo((Long) val2); break; case DOUBLE: diff = ((Double) val1).compareTo((Double) val2); break; case COLOUR: diff = Colours.compare((Color) val1, (Color) val2); break; case TIME: diff = ((ODLTime) val1).compareTo((ODLTime) val2); break; default: throw new RuntimeException(); } return diff; } /** * Check if 2 values are equal when they're the same type * @param type * @param val1 * @param val2 * @return */ public static boolean isEqualSameType(ODLColumnType type, Object val1, Object val2) { if (val1 == val2) { return true; } if (NullComparer.compare(val1, val2) != 0) { return false; } if (val1 != null) { switch (type) { case LONG: case DOUBLE: case STRING: case COLOUR: case TIME: case DATE: if (val1.equals(val2) == false) { return false; } break; default: // convert to string and compare if (val1.toString().equals(val2.toString()) == false) { return false; } break; } } return true; } public static boolean isEqual(Object a, Object b) { return isEqual(a, b, null); } public static boolean isEqual(Object a, Object b, StandardisedCache stdCache) { boolean equals = false; if (a == null && b == null) { // both null are equal equals = true; } else if ((a == null && b != null) || (a != null && b == null)) { // one null and one not null are not equal equals = false; } else if (a.getClass() == b.getClass()) { // do same class-internal comparison if (a.getClass() == String.class) { equals = Strings.equalsStd(a.toString(), b.toString()); } else { equals = a.equals(b); } } else if (Number.class.isInstance(a) && Number.class.isInstance(b)) { // try treating both as double if they are both numbers Double da = Numbers.toDouble(a); Double db = Numbers.toDouble(b); if (da == null || db == null) { return false; } equals = da.equals(db); } else { // test the string-representation values equals = Strings.equalsStd(a.toString(), b.toString(), stdCache); } return equals; } private static boolean isEmpty(Object o) { return o == null || o.toString().length() == 0; } public static int compareValues(Object val1, Object val2, boolean isNumeric) { int diff; boolean empty1 = isEmpty(val1); boolean empty2 = isEmpty(val2); diff = Boolean.compare(empty1, empty2); // if both empty then return 0 if (diff == 0 && empty1) { return 0; } if (diff == 0 && val1 != null) { if (isNumeric) { Double d1 = (Double) ColumnValueProcessor.convertToMe(ODLColumnType.DOUBLE, val1); Double d2 = (Double) ColumnValueProcessor.convertToMe(ODLColumnType.DOUBLE, val2); diff = d1.compareTo(d2); } else if (ODLTime.class.isInstance(val1) && ODLTime.class.isInstance(val2)) { diff = ((ODLTime) val1).compareTo((ODLTime) val2); } else { // ignore case in string compare String s1 = val1.toString().toLowerCase(); String s2 = val2.toString().toLowerCase(); diff = s1.compareTo(s2); } } return diff; } public static void main(String[] args) { for (String s : new String[] { "15:00:12", "1.2", "1" ,"15,4,3"}) { System.out.println(s + " cannotBeDouble=" + cannotBeDouble(s) + " converToMe=" + convertToMe(ODLColumnType.DOUBLE, s, ODLColumnType.STRING, true)); } } }