package er.rest; import java.math.BigDecimal; import java.text.Format; import java.text.SimpleDateFormat; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; import java.time.temporal.TemporalAccessor; import java.time.temporal.TemporalQuery; import java.util.Calendar; import java.util.Date; import com.webobjects.eoaccess.EOAttribute; import com.webobjects.eoaccess.EOEntityClassDescription; import com.webobjects.eocontrol.EOClassDescription; import com.webobjects.eocontrol.EOKeyValueCoding; import com.webobjects.foundation.NSData; import com.webobjects.foundation.NSKeyValueCoding; import com.webobjects.foundation.NSPropertyListSerialization; import com.webobjects.foundation.NSTimestamp; import com.webobjects.foundation.NSTimestampFormatter; import com.webobjects.foundation._NSUtilities; import er.extensions.crypting.ERXCryptoString; import er.extensions.foundation.ERXProperties; import er.extensions.foundation.ERXValueUtilities; /** * Miscellaneous rest-related utility methods. * * @property er.rest.dateFormat * @property er.rest.timestampFormat * @property er.rest.rfcDateFormat (default "rfc822") * * @author mschrag */ public class ERXRestUtils { protected final static DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ISO_DATE; protected final static DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ISO_DATE_TIME; protected final static DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ISO_TIME; /** * Returns whether or not the given object represents a primitive in REST. * * @param obj the object to check * @return whether or not the given object represents a primitive in REST */ public static boolean isPrimitive(Object obj) { return obj == null || ((obj instanceof Class) ? ERXRestUtils.isPrimitive((Class<?>) obj) : ERXRestUtils.isPrimitive(obj.getClass())); } /** * Returns whether or not the given class represents a primitive in REST. * * @param valueType the class to check * @return whether or not the given class represents a primitive in REST */ public static boolean isPrimitive(Class<?> valueType) { boolean primitive = false; if (String.class.isAssignableFrom(valueType)) { primitive = true; } else if (Boolean.class.isAssignableFrom(valueType)) { primitive = true; } else if (Character.class.isAssignableFrom(valueType)) { primitive = true; } else if (Byte.class.isAssignableFrom(valueType)) { primitive = true; } else if (BigDecimal.class.isAssignableFrom(valueType)) { primitive = true; } else if (Integer.class.isAssignableFrom(valueType)) { primitive = true; } else if (Short.class.isAssignableFrom(valueType)) { primitive = true; } else if (Long.class.isAssignableFrom(valueType)) { primitive = true; } else if (Float.class.isAssignableFrom(valueType)) { primitive = true; } else if (Double.class.isAssignableFrom(valueType)) { primitive = true; } else if (Date.class.isAssignableFrom(valueType)) { primitive = true; } else if (Calendar.class.isAssignableFrom(valueType)) { primitive = true; } else if (org.joda.time.LocalDateTime.class.isAssignableFrom(valueType)) { primitive = true; } else if (org.joda.time.LocalDate.class.isAssignableFrom(valueType)) { primitive = true; } else if (Enum.class.isAssignableFrom(valueType)) { primitive = true; } else if (NSKeyValueCoding.Null.class.isAssignableFrom(valueType)) { primitive = true; } else if (ERXCryptoString.class.isAssignableFrom(valueType)) { primitive = true; } else if (LocalDate.class.isAssignableFrom(valueType)) { primitive = true; } else if (LocalDateTime.class.isAssignableFrom(valueType)) { primitive = true; } else if (LocalTime.class.isAssignableFrom(valueType)) { primitive = true; } else if (OffsetDateTime.class.isAssignableFrom(valueType)) { primitive = true; } return primitive; } /** * Convert the given object to a String (using REST formats). * * @param value the value to convert * @param context the REST context * @return the REST-formatted string */ public static String coerceValueToString(Object value, ERXRestContext context) { String formattedValue; if (value == null || value instanceof NSKeyValueCoding.Null) { formattedValue = null; } else if (value instanceof NSTimestamp) { NSTimestamp timestamp = (NSTimestamp) value; String rfcFormat = ERXProperties.stringForKeyWithDefault("er.rest.rfcDateFormat", "rfc822"); if ("rfc3339".equals(rfcFormat)) { formattedValue = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").format(new Date(timestamp.getTime())); formattedValue = formattedValue.substring(0, formattedValue.length()-2) + ":" + formattedValue.substring(formattedValue.length()-2); } else { formattedValue = ERXRestUtils.timestampFormat(false, context).format(timestamp); } } else if (value instanceof Date) { formattedValue = ERXRestUtils.dateFormat(false, context).format(value); } else if (value instanceof org.joda.time.LocalDateTime) { formattedValue = ERXRestUtils.jodaLocalDateTimeFormat(false, context).print((org.joda.time.LocalDateTime)value); } else if (value instanceof org.joda.time.LocalDate) { formattedValue = ERXRestUtils.jodaLocalDateFormat(false, context).print((org.joda.time.LocalDate)value); } else if (value instanceof LocalDate) { formattedValue = DATE_FORMATTER.format((TemporalAccessor)value); } else if (value instanceof LocalDateTime) { formattedValue = DATE_TIME_FORMATTER.format((TemporalAccessor)value); } else if (value instanceof LocalTime) { formattedValue = TIME_FORMATTER.format((TemporalAccessor)value); } else if (value instanceof OffsetDateTime) { formattedValue = DATE_TIME_FORMATTER.format((TemporalAccessor)value); } else if (value instanceof NSData && ((NSData)value).length() == 24) { formattedValue = NSPropertyListSerialization.stringFromPropertyList(value); } else { formattedValue = value.toString(); } return formattedValue; } // this "spaces" attribute is stupid, i know ... this whole api is stupid. it's a quick hack for now to accommodate someone very near and dear to my heart ... yes i'm talking to you. protected static Format timestampFormat(boolean spaces, ERXRestContext context) { Format timestampFormatter = (Format)context.userInfoForKey("er.rest.timestampFormatter"); if (timestampFormatter == null) { String timestampFormat = (String)context.userInfoForKey("er.rest.timestampFormat"); if (timestampFormat == null) { timestampFormat = ERXProperties.stringForKey("er.rest.timestampFormat"); if (timestampFormat == null) { if (spaces) { timestampFormat = ERXProperties.stringForKeyWithDefault("er.rest.timestampFormat.secondary", "%Y-%m-%d %H:%M:%S %Z"); } else { timestampFormat = ERXProperties.stringForKeyWithDefault("er.rest.timestampFormat.primary", "%Y-%m-%dT%H:%M:%SZ"); } } } timestampFormatter = new NSTimestampFormatter(timestampFormat); } return timestampFormatter; } protected static Format dateFormat(boolean spaces, ERXRestContext context) { Format dateFormatter = (Format)context.userInfoForKey("er.rest.dateFormatter"); if (dateFormatter == null) { String dateFormat = (String)context.userInfoForKey("er.rest.dateFormat"); if (dateFormat == null) { dateFormat = ERXProperties.stringForKey("er.rest.dateFormat"); if (dateFormat == null) { if (spaces) { dateFormat = ERXProperties.stringForKeyWithDefault("er.rest.dateFormat.secondary", "yyyy-MM-dd HH:mm:ss z"); } else { dateFormat = ERXProperties.stringForKeyWithDefault("er.rest.dateFormat.primary", "yyyy-MM-dd'T'HH:mm:ss'Z'"); } } } dateFormatter = new SimpleDateFormat(dateFormat); } return dateFormatter; } protected static org.joda.time.format.DateTimeFormatter jodaLocalDateFormat(boolean spaces, ERXRestContext context) { org.joda.time.format.DateTimeFormatter dateFormatter = (org.joda.time.format.DateTimeFormatter)context.userInfoForKey("er.rest.jodaFormatter"); if (dateFormatter == null) { String dateFormat = (String)context.userInfoForKey("er.rest.jodaFormat"); if (dateFormat == null) { dateFormat = ERXProperties.stringForKey("er.rest.jodaFormat"); if (dateFormat == null) { if (spaces) { dateFormat = ERXProperties.stringForKeyWithDefault("er.rest.jodaFormat.secondary", "yyyy-MM-dd HH:mm:ss z"); } else { dateFormat = ERXProperties.stringForKeyWithDefault("er.rest.jodaFormat.primary", "yyyy-MM-dd'T'HH:mm:ss'Z'"); } } } dateFormatter = org.joda.time.format.DateTimeFormat.forPattern(dateFormat); } return dateFormatter; } protected static org.joda.time.format.DateTimeFormatter jodaLocalDateTimeFormat(boolean spaces, ERXRestContext context) { org.joda.time.format.DateTimeFormatter dateFormatter = (org.joda.time.format.DateTimeFormatter)context.userInfoForKey("er.rest.jodaTimeFormatter"); if (dateFormatter == null) { String dateFormat = (String)context.userInfoForKey("er.rest.jodaFormatTime"); if (dateFormat == null) { dateFormat = ERXProperties.stringForKey("er.rest.jodaFormatTime"); if (dateFormat == null) { if (spaces) { dateFormat = ERXProperties.stringForKeyWithDefault("er.rest.jodaFormat.secondary", "yyyy-MM-dd HH:mm:ss z"); } else { dateFormat = ERXProperties.stringForKeyWithDefault("er.rest.jodaFormat.primary", "yyyy-MM-dd'T'HH:mm:ss'Z'"); } } } dateFormatter = org.joda.time.format.DateTimeFormat.forPattern(dateFormat); } return dateFormatter; } @SuppressWarnings("unchecked") public static Object coerceValueToTypeNamed(Object value, String valueTypeName, ERXRestContext context, boolean resolveEntities) { Object parsedValue; Class<?> valueType = _NSUtilities.classWithName(valueTypeName); // test primitives first, since we can't return a null for them if (valueType != null && int.class.isAssignableFrom(valueType)) { parsedValue = ERXValueUtilities.intValueWithDefault(value, 0); } else if (valueType != null && boolean.class.isAssignableFrom(valueType)) { parsedValue = ERXValueUtilities.booleanValueWithDefault(value, false); } else if (valueType != null && char.class.isAssignableFrom(valueType)) { parsedValue = (char)ERXValueUtilities.intValueWithDefault(value, 0); } else if (valueType != null && byte.class.isAssignableFrom(valueType)) { parsedValue = (byte)ERXValueUtilities.intValueWithDefault(value, 0); } else if (valueType != null && long.class.isAssignableFrom(valueType)) { parsedValue = ERXValueUtilities.longValueWithDefault(value, 0); } else if (valueType != null && float.class.isAssignableFrom(valueType)) { parsedValue = ERXValueUtilities.floatValueWithDefault(value, 0); } else if (valueType != null && double.class.isAssignableFrom(valueType)) { parsedValue = ERXValueUtilities.doubleValueWithDefault(value, 0); } else if (valueType != null && short.class.isAssignableFrom(valueType)) { parsedValue = (short)ERXValueUtilities.intValueWithDefault(value, 0); } else if (ERXValueUtilities.isNull(value)) { parsedValue = null; } else if (valueType != null && String.class.isAssignableFrom(valueType)) { parsedValue = String.valueOf(value); } else if (valueType != null && Boolean.class.isAssignableFrom(valueType)) { parsedValue = ERXValueUtilities.BooleanValueWithDefault(value, null); } else if (valueType != null && Character.class.isAssignableFrom(valueType)) { parsedValue = Character.valueOf(((String) value).charAt(0)); // MS: Presumes String } else if (valueType != null && Byte.class.isAssignableFrom(valueType)) { parsedValue = Byte.valueOf((String) value); // MS: Presumes String } else if (valueType != null && BigDecimal.class.isAssignableFrom(valueType)) { parsedValue = ERXValueUtilities.DoubleValueWithDefault(value, null); } else if (valueType != null && Integer.class.isAssignableFrom(valueType)) { parsedValue = ERXValueUtilities.IntegerValueWithDefault(value, null); } else if (valueType != null && Short.class.isAssignableFrom(valueType)) { parsedValue = Short.valueOf((String) value); // MS: Presumes String } else if (valueType != null && Long.class.isAssignableFrom(valueType)) { parsedValue = ERXValueUtilities.LongValueWithDefault(value, null); } else if (valueType != null && Float.class.isAssignableFrom(valueType)) { parsedValue = ERXValueUtilities.FloatValueWithDefault(value, null); } else if (valueType != null && Double.class.isAssignableFrom(valueType)) { parsedValue = ERXValueUtilities.DoubleValueWithDefault(value, null); } else if (valueType != null && NSData.class.isAssignableFrom(valueType)) { parsedValue = ERXValueUtilities.dataValueWithDefault(value, null); } else if (valueType != null && NSTimestamp.class.isAssignableFrom(valueType)) { if (value instanceof NSTimestamp) { parsedValue = value; } else { String strValue = (String) value; boolean spaces = strValue.indexOf(' ') != -1; String rfcFormat = ERXProperties.stringForKeyWithDefault("er.rest.rfcDateFormat", "rfc822"); if ("rfc3339".equals(rfcFormat)) { SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ"); java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("(.*[\\-,\\+]{1}[0-9]{1,2}):([0-9]{1,2})"); java.util.regex.Matcher matcher = pattern.matcher(strValue); if (matcher.matches()) { try { strValue = matcher.group(1) + matcher.group(2); parsedValue = formatter.parseObject(strValue); if (parsedValue instanceof java.util.Date) { parsedValue = new NSTimestamp((Date)parsedValue); } } catch (Throwable t) { String msg = "Failed to parse '" + strValue + "' as a timestamp"; if (formatter != null) { msg += " (example: " + formatter.format(new NSTimestamp()) + ")"; } msg += "."; throw new IllegalArgumentException(msg, t); } } else { strValue = null; throw new IllegalArgumentException(strValue + " didn't match the " + pattern.pattern() + " pattern", new Throwable()); } } else { Format formatter = null; try { formatter = ERXRestUtils.timestampFormat(spaces, context); parsedValue = formatter.parseObject(strValue); } catch (Throwable t) { String msg = "Failed to parse '" + strValue + "' as a timestamp"; if (formatter != null) { msg += " (example: " + formatter.format(new NSTimestamp()) + ")"; } msg += "."; throw new IllegalArgumentException(msg, t); } } } } else if (valueType != null && Date.class.isAssignableFrom(valueType)) { if (value instanceof NSTimestamp) { parsedValue = value; } else { String strValue = (String) value; Format formatter = null; try { boolean spaces = strValue.indexOf(' ') != -1; formatter = ERXRestUtils.dateFormat(spaces, context); parsedValue = formatter.parseObject(strValue); } catch (Throwable t) { String msg = "Failed to parse '" + strValue + "' as a timestamp"; if (formatter != null) { msg += " (example: " + formatter.format(new Date()) + ")"; } msg += "."; throw new IllegalArgumentException(msg, t); } } } else if (valueType != null && org.joda.time.LocalDateTime.class.isAssignableFrom(valueType)) { if (value instanceof NSTimestamp) { parsedValue = value; } else { String strValue = (String) value; org.joda.time.format.DateTimeFormatter formatter = null; try { boolean spaces = strValue.indexOf(' ') != -1; formatter = ERXRestUtils.jodaLocalDateTimeFormat(spaces, context); parsedValue = new org.joda.time.LocalDateTime(formatter.parseDateTime(strValue)); } catch (Throwable t) { String msg = "Failed to parse '" + strValue + "' as a timestamp"; if (formatter != null) { msg += " (example: " + formatter.print(new org.joda.time.LocalDateTime()) + ")"; } msg += "."; throw new IllegalArgumentException(msg, t); } } } else if (valueType != null && org.joda.time.LocalDate.class.isAssignableFrom(valueType)) { if (value instanceof NSTimestamp) { parsedValue = value; } else { String strValue = (String) value; org.joda.time.format.DateTimeFormatter formatter = null; try { boolean spaces = strValue.indexOf(' ') != -1; formatter = ERXRestUtils.jodaLocalDateFormat(spaces, context); parsedValue = new org.joda.time.LocalDate(formatter.parseDateTime(strValue)); } catch (Throwable t) { String msg = "Failed to parse '" + strValue + "' as a timestamp"; if (formatter != null) { msg += " (example: " + formatter.print(new org.joda.time.LocalDate()) + ")"; } msg += "."; throw new IllegalArgumentException(msg, t); } } } else if (valueType != null && TemporalAccessor.class.isAssignableFrom(valueType)) { if (value instanceof NSTimestamp) { parsedValue = value; } else { TemporalQuery<?> query = null; Class<? extends Class> valueTypeClass = valueType.getClass(); if (valueTypeClass.equals(LocalDate.class)) { query = LocalDate::from; } else if (valueTypeClass.equals(LocalDateTime.class)) { query = LocalDateTime::from; } else if (valueTypeClass.equals(LocalTime.class)) { query = LocalTime::from; } else if (valueTypeClass.equals(OffsetDateTime.class)) { query = OffsetDateTime::from; } String strValue = (String) value; try { parsedValue = DATE_TIME_FORMATTER.parse(strValue, query); } catch (Throwable t) { String msg = "Failed to parse '" + strValue + "' as a java time object " + valueTypeClass + "."; throw new IllegalArgumentException(msg, t); } } } else if (valueType != null && Enum.class.isAssignableFrom(valueType)) { parsedValue = ERXValueUtilities.enumValueWithDefault(value, (Class<? extends Enum>) valueType, null); } else if (valueType != null && ERXCryptoString.class.isAssignableFrom(valueType)) { parsedValue = new ERXCryptoString(value.toString()); } else if (resolveEntities) { EOClassDescription entity = ERXRestClassDescriptionFactory.classDescriptionForEntityName(valueTypeName); if (entity != null) { parsedValue = IERXRestDelegate.Factory.delegateForClassDescription(entity).objectOfEntityWithID(entity, value, context); } else { throw new IllegalArgumentException("Unknown value type '" + valueTypeName + "'."); } } else { throw new IllegalArgumentException("Unable to parse the value '" + value + "' into a '" + valueTypeName + "'."); } return parsedValue; } /** * Parses the given String and returns an object. * * @param value * the value of the attribute * @param parentEntity * the entity * @param parentObject * the parent object * @param attributeName * the name of the property * @param context * the REST context * @return a parsed version of the String */ public static Object coerceValueToAttributeType(Object value, EOClassDescription parentEntity, Object parentObject, String attributeName, ERXRestContext context) { NSKeyValueCoding._KeyBinding binding = NSKeyValueCoding.DefaultImplementation._keyGetBindingForKey(parentObject, attributeName); Class<?> valueType = binding.valueType(); try { Object parsedValue; if (value == null || ERXValueUtilities.isNull(value) || (value instanceof String && ((String) value).length() == 0)) { if (parentEntity != null) { if (parentEntity instanceof EOEntityClassDescription) { EOAttribute attribute = ((EOEntityClassDescription)parentEntity).entity().attributeNamed(attributeName); if (attribute != null && !attribute.allowsNull() && String.class.isAssignableFrom(valueType)) { parsedValue = ""; } else { parsedValue = EOKeyValueCoding.NullValue; } } else { parsedValue = NSKeyValueCoding.NullValue; } } else { parsedValue = NSKeyValueCoding.NullValue; } } else if (valueType == Object.class) { parsedValue = value; } else { parsedValue = ERXRestUtils.coerceValueToTypeNamed(value, valueType.getName(), context, false); } return parsedValue; } catch (Throwable e) { throw new IllegalArgumentException("Failed to parse attribute " + attributeName + " for entity " + ((parentEntity == null) ? "unknown" : parentEntity.entityName()), e); } } }