From 52a532150fb117fd4f40bbb748025608d48a5650 Mon Sep 17 00:00:00 2001 From: Aron Roberts Date: Wed, 18 Aug 2010 01:38:25 +0000 Subject: [PATCH] CSPACE-2418: Services can now accept, as input to date fields in their record types, dates in a variety of representation formats, specified in per-tenant configuration. This will permit free text entry of dates in formats familiar to users in various countries, such as 'MM/dd/yyyy' (USA), 'dd/MM/yyyy' (UK), and 'dd.MM.yyyy' (one of several formats used in Denmark). No validation is yet performed, and dates in unrecognized formats, that cannot be parsed and converted to the ISO 8601-based formats required by Nuxeo and MySQL, are silently dropped, as they were even before this check-in. This is addressed by a new bug, CSPACE-2653. --- .../main/config/services/tenant-bindings.xml | 12 +- .../common/datetime/DateTimeFormatUtils.java | 185 +++++++++++++++++- .../common/document/DocumentUtils.java | 47 ++++- .../java/RemoteDocumentModelHandlerImpl.java | 12 +- ...RemoteSubItemDocumentModelHandlerImpl.java | 3 +- .../nuxeo/MovementValidatorHandler.java | 10 +- 6 files changed, 236 insertions(+), 33 deletions(-) diff --git a/services/common/src/main/config/services/tenant-bindings.xml b/services/common/src/main/config/services/tenant-bindings.xml index f42e066e7..e11388c01 100644 --- a/services/common/src/main/config/services/tenant-bindings.xml +++ b/services/common/src/main/config/services/tenant-bindings.xml @@ -19,9 +19,9 @@ - datePatternMM/dd/YYYY - datePatterndd.MM.YYYY - + datePatternMM/dd/yyyy + datePatterndd.MM.yyyy + @@ -1125,9 +1125,9 @@ - datePatternMM/dd/YYYY - datePatterndd.MM.YYYY - + datePatternMM/dd/yyyy + datePatterndd.MM.yyyy + diff --git a/services/common/src/main/java/org/collectionspace/services/common/datetime/DateTimeFormatUtils.java b/services/common/src/main/java/org/collectionspace/services/common/datetime/DateTimeFormatUtils.java index b01e5b80b..55f17f8cf 100644 --- a/services/common/src/main/java/org/collectionspace/services/common/datetime/DateTimeFormatUtils.java +++ b/services/common/src/main/java/org/collectionspace/services/common/datetime/DateTimeFormatUtils.java @@ -23,7 +23,9 @@ import java.text.SimpleDateFormat; import java.util.Date; import java.util.GregorianCalendar; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.TimeZone; import org.collectionspace.services.common.ServiceMain; @@ -47,15 +49,71 @@ public class DateTimeFormatUtils { private static final Logger logger = LoggerFactory.getLogger(DateTimeFormatUtils.class); final static String DATE_FORMAT_PATTERN_PROPERTY_NAME = "datePattern"; + final static String ISO_8601_FLOATING_DATE_PATTERN = "yyyy-MM-dd"; final static String ISO_8601_UTC_TIMESTAMP_PATTERN = "yyyy-MM-dd'T'HH:mm:ss'Z'"; + static Map> dateFormatters = new HashMap>(); + static Map> datePatterns = new HashMap>(); + // FIXME: // - Add a method to return the default set of ISO 8601-based date patterns, // irrespective of per-tenant configuration. - // - Consider cacheing the lists of per-tenant date format patterns - // and refresh the cached copies whenever tenant bindings are read. // - Methods below related to per-tenant configuration of date formats might // be moved to their own class. + // - Investigate whether the map of per-tenant date formatters might best be + // maintained within a singleton class. + + /** + * Returns a list of the date formatters permitted in a service context. + * + * @param ctx a service context. + * + * @return a list of date formatters permitted in the service context. + * Returns an empty list of date formatters if the service context is null. + */ + public static List getDateFormattersForTenant(ServiceContext ctx) { + List formatters = new ArrayList(); + if (ctx == null) { + return formatters; + } + return getDateFormattersForTenant(ctx.getTenantId()); + } + + /** + * Returns a list of the date formatters permitted for a tenant, specified + * by tenant ID. + * + * @param tenantId a tenant ID. + * + * @return a list of date formatters permitted for the tenant. + * Returns an empty list of date formatters if the tenant ID is null or empty. + */ + public static List getDateFormattersForTenant(String tenantId) { + List formatters = new ArrayList(); + if (tenantId == null || tenantId.trim().isEmpty()) { + return formatters; + } + // If a list of date formatters for this tenant already exists, return it. + if (dateFormatters != null && dateFormatters.containsKey(tenantId)) { + formatters = dateFormatters.get(tenantId); + if (formatters != null && formatters.size() > 0) { + return formatters; + } + } + // Otherwise, generate that list and cache it for re-use. + List patterns = getDateFormatPatternsForTenant(tenantId); + DateFormat df = null; + for (String pattern : patterns) { + df = getDateFormatter(pattern); + if (df != null) { + formatters.add(df); + } + } + if (dateFormatters != null) { + dateFormatters.put(tenantId, formatters); + } + return formatters; + } /** * Returns a list of the date format patterns permitted in a service context. @@ -84,28 +142,54 @@ public class DateTimeFormatUtils { * @param tenantId a tenant ID. * * @return a list of date format patterns permitted for the tenant. + * Returns an empty list of patterns if the tenant ID is null or empty. */ public static List getDateFormatPatternsForTenant(String tenantId) { List patterns = new ArrayList(); if (tenantId == null || tenantId.trim().isEmpty()) { return patterns; } + // If a list of date patterns for this tenant already exists, return it. + if (datePatterns != null && datePatterns.containsKey(tenantId)) { + patterns = datePatterns.get(tenantId); + if (patterns != null && patterns.size() > 0) { + return patterns; + } + } + // Otherwise, generate that list and cache it for re-use. TenantBindingConfigReaderImpl tReader = ServiceMain.getInstance().getTenantBindingConfigReader(); TenantBindingType tenantBinding = tReader.getTenantBinding(tenantId); patterns = TenantBindingUtils.getPropertyValues(tenantBinding, DATE_FORMAT_PATTERN_PROPERTY_NAME); - return validatePatterns(patterns); + patterns = validatePatterns(patterns); + if (datePatterns != null) { + datePatterns.put(tenantId, patterns); + } + return patterns; } + /** + * Validates a list of date or date/time patterns, checking each pattern + * to determine whether it can be used to instantiate a date formatter. + * + * These patterns must conform to the format for date and time pattern strings + * specified in the Javadocs for the Java language class, java.text.SimpleDateFormat. + * + * @param patterns a list of date or date/time patterns. + * + * @return a list of valid patterns, excluding any patterns + * that could not be used to instantiate a date formatter. + */ public static List validatePatterns(List patterns) { if (patterns == null) { return new ArrayList(); } + DateFormat df = new SimpleDateFormat(); List validPatterns = new ArrayList(); for (String pattern : patterns) { try { - DateFormat df = getDateFormatter(pattern); + df = getDateFormatter(pattern); validPatterns.add(pattern); } catch (IllegalArgumentException iae) { logger.warn("Invalid " + DATE_FORMAT_PATTERN_PROPERTY_NAME + " property: " + pattern); @@ -114,6 +198,38 @@ public class DateTimeFormatUtils { return validPatterns; } + /** + * Returns an ISO 8601 timestamp representation of a presumptive date or + * date/time string. Applies the set of date formatters for a supplied tenant + * to attempt to parse the string. + * + * @param str a String, possibly a date or date/time String. + * @param tenantId a tenant ID. + * + * @return an ISO 8601 timestamp representation of that String. + * If the String cannot be parsed by the date formatters + * for the supplied tenant, return the original string. + */ + public static String toIso8601Timestamp(String dateStr, String tenantId) { + Date date = null; + List formatters = getDateFormattersForTenant(tenantId); +// for (DateFormat formatter : getDateFormattersForTenant(tenantId)) { + for (DateFormat formatter : formatters) { + date = parseDate(dateStr, formatter); + if (date != null) { + break; + } + } + if (date == null) { + return dateStr; + } else { + GregorianCalendar gcal = new GregorianCalendar(); + gcal.setTime(date); + String isoStr = formatAsISO8601Timestamp(gcal); + return isoStr; + } + } + /** * Returns a representation of a calendar date and time instance, * as an ISO 8601-formatted timestamp in the UTC time zone. @@ -176,10 +292,9 @@ public class DateTimeFormatUtils { /** * Identifies whether a presumptive date or date/time can be parsed - * by a date parser, using a specified format pattern. + * by a date parser, using a supplied format pattern. * * @param str a String, possibly a date or date/time String. - * * @param pattern A date or date/time pattern. * * @return true, if the String can be parsed, using the pattern; @@ -190,6 +305,9 @@ public class DateTimeFormatUtils { if (pattern == null || pattern.trim().isEmpty()) { return false; } + if (str == null || str.trim().isEmpty()) { + return false; + } DateFormat df = null; try { df = new SimpleDateFormat(pattern); @@ -202,6 +320,59 @@ public class DateTimeFormatUtils { return true; } + /** + * Parses a presumptive date or date/time, using a supplied format pattern. + * + * @param str a String, possibly a date or date/time String. + * @param pattern A date or date/time pattern. + * + * @return A date value, resulting from parsing the String using the + * supplied pattern. Returns null if the parsing attempt fails. + */ + public static Date parseDate(String str, String pattern) { + if (pattern == null || pattern.trim().isEmpty()) { + return null; + } + if (str == null || str.trim().isEmpty()) { + return null; + } + DateFormat df = null; + Date date = null; + try { + df = new SimpleDateFormat(pattern); + date = parseDate(str, df); + } catch (IllegalArgumentException iae) { + return null; + } + return date; + } + + /** + * Parses a presumptive date or date/time, using a supplied format pattern. + * + * @param str a String, possibly a date or date/time String. + * @param df A date formatter. + * + * @return A date value, resulting from parsing the String using the + * supplied formatter. Returns null if the parsing attempt fails. + */ + public static Date parseDate(String str, DateFormat df) { + if (df == null) { + return null; + } + if (str == null || str.trim().isEmpty()) { + return null; + } + Date date = null; + try { + df.setLenient(false); + date = df.parse(str); + } catch (ParseException pe) { + return null; + } + return date; + } + /** * Returns a date formatter for a provided date or date/time pattern. * @@ -218,9 +389,11 @@ public class DateTimeFormatUtils { } try { df = new SimpleDateFormat(pattern); + df.setLenient(false); } catch (IllegalArgumentException iae) { logger.warn("Invalid date pattern string '" + pattern + "': " + iae.getMessage()); } return df; } + } diff --git a/services/common/src/main/java/org/collectionspace/services/common/document/DocumentUtils.java b/services/common/src/main/java/org/collectionspace/services/common/document/DocumentUtils.java index b99991d04..b65235f94 100644 --- a/services/common/src/main/java/org/collectionspace/services/common/document/DocumentUtils.java +++ b/services/common/src/main/java/org/collectionspace/services/common/document/DocumentUtils.java @@ -23,6 +23,8 @@ */ package org.collectionspace.services.common.document; +import java.util.Calendar; + import java.lang.reflect.Array; import java.io.File; @@ -52,6 +54,8 @@ import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerException; import org.collectionspace.services.common.ServiceMain; +import org.collectionspace.services.common.context.ServiceContext; +import org.collectionspace.services.common.datetime.DateTimeFormatUtils; import org.collectionspace.services.common.service.ObjectPartContentType; import org.collectionspace.services.common.service.ObjectPartType; import org.collectionspace.services.common.service.XmlContentType; @@ -69,8 +73,10 @@ import org.nuxeo.ecm.core.schema.types.ComplexType; import org.nuxeo.ecm.core.schema.types.Field; import org.nuxeo.ecm.core.schema.types.ListType; import org.nuxeo.ecm.core.schema.types.Schema; +import org.nuxeo.ecm.core.schema.types.SimpleType; import org.nuxeo.ecm.core.schema.types.Type; import org.nuxeo.ecm.core.schema.types.JavaTypes; +import org.nuxeo.ecm.core.schema.types.primitives.DateType; import org.nuxeo.ecm.core.schema.types.primitives.StringType; import org.nuxeo.ecm.core.schema.types.FieldImpl; import org.nuxeo.ecm.core.schema.types.QName; @@ -572,7 +578,7 @@ public class DocumentUtils { parent.appendChild(element); // extract the element content if (type.isSimpleType()) { - element.setTextContent(type.encode(value)); + element.setTextContent(type.encode(value)); } else if (type.isComplexType()) { ComplexType ctype = (ComplexType) type; if (ctype.getName().equals(TypeConstants.CONTENT)) { @@ -830,6 +836,22 @@ public class DocumentUtils { } + /* + * Identifies whether a property type is a date type. + * + * @param type a type. + * @return true, if is a date type; + * false, if it is not a date type. + */ + private static boolean isDateType(Type type) { + SimpleType st = (SimpleType) type; + if (st.getPrimitiveType() instanceof DateType) { + return true; + } else { + return false; + } + } + /** * Insert multi values. * @@ -1021,7 +1043,7 @@ public class DocumentUtils { * @return the map */ public static Map parseProperties(ObjectPartType partMeta, - Document document) { + Document document, ServiceContext ctx) { Map result = null; String schemaName = partMeta.getLabel(); Schema schema = getSchemaFromName(schemaName); @@ -1029,7 +1051,7 @@ public class DocumentUtils { org.dom4j.io.DOMReader xmlReader = new org.dom4j.io.DOMReader(); org.dom4j.Document dom4jDocument = xmlReader.read(document); try { - result = loadSchema(schema, dom4jDocument.getRootElement()); + result = loadSchema(schema, dom4jDocument.getRootElement(), ctx); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); @@ -1047,7 +1069,7 @@ public class DocumentUtils { * @throws Exception the exception */ @SuppressWarnings("unchecked") - static private Map loadSchema(Schema schema, org.dom4j.Element schemaElement) + static private Map loadSchema(Schema schema, org.dom4j.Element schemaElement, ServiceContext ctx) throws Exception { String schemaName1 = schemaElement.attributeValue(ExportConstants.NAME_ATTR); String schemaName = schema.getName(); @@ -1059,7 +1081,7 @@ public class DocumentUtils { String name = element.getName(); Field field = schema.getField(name); if (field != null) { - Object value = getElementData(element, field.getType()); + Object value = getElementData(element, field.getType(), ctx); data.put(name, value); } else { if (logger.isDebugEnabled() == true) { @@ -1080,18 +1102,25 @@ public class DocumentUtils { * @return the element data */ @SuppressWarnings("unchecked") - static private Object getElementData(org.dom4j.Element element, Type type) { + static private Object getElementData(org.dom4j.Element element, Type type, + ServiceContext ctx) { Object result = null; if (type.isSimpleType()) { - result = type.decode(element.getText()); + // Convert incoming date values to a canonical date representation, + if (isDateType(type)) { + result = DateTimeFormatUtils.toIso8601Timestamp((String) element.getText(), + ctx.getTenantId()); + } else { + result = type.decode(element.getText()); + } } else if (type.isListType()) { ListType ltype = (ListType) type; List list = new ArrayList(); Iterator it = element.elementIterator(); while (it.hasNext()) { org.dom4j.Element el = it.next(); - list.add(getElementData(el, ltype.getFieldType())); + list.add(getElementData(el, ltype.getFieldType(), ctx)); } Type ftype = ltype.getFieldType(); if (ftype.isSimpleType()) { // these are stored as arrays @@ -1132,7 +1161,7 @@ public class DocumentUtils { org.dom4j.Element el = it.next(); String name = el.getName(); Object value = getElementData(el, ctype.getField( - el.getName()).getType()); + el.getName()).getType(), ctx); map.put(name, value); } result = map; diff --git a/services/common/src/main/java/org/collectionspace/services/nuxeo/client/java/RemoteDocumentModelHandlerImpl.java b/services/common/src/main/java/org/collectionspace/services/nuxeo/client/java/RemoteDocumentModelHandlerImpl.java index fc067ec94..b5d5947a3 100644 --- a/services/common/src/main/java/org/collectionspace/services/nuxeo/client/java/RemoteDocumentModelHandlerImpl.java +++ b/services/common/src/main/java/org/collectionspace/services/nuxeo/client/java/RemoteDocumentModelHandlerImpl.java @@ -25,7 +25,6 @@ package org.collectionspace.services.nuxeo.client.java; import java.io.InputStream; import java.util.Collection; -import java.util.Iterator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -56,11 +55,7 @@ import org.nuxeo.ecm.core.api.DocumentModelList; import org.nuxeo.ecm.core.api.model.Property; import org.nuxeo.ecm.core.api.model.PropertyException; -import org.nuxeo.ecm.core.schema.types.ComplexType; -import org.nuxeo.ecm.core.schema.types.Field; -import org.nuxeo.ecm.core.schema.types.ListType; import org.nuxeo.ecm.core.schema.types.Schema; -import org.nuxeo.ecm.core.schema.types.Type; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -213,7 +208,7 @@ public abstract class RemoteDocumentModelHandlerImpl if (partMeta == null) { continue; } - fillPart(part, docModel, partMeta, action); + fillPart(part, docModel, partMeta, action, ctx); }//rof } @@ -225,7 +220,8 @@ public abstract class RemoteDocumentModelHandlerImpl * @param partMeta metadata for the object to fill * @throws Exception */ - protected void fillPart(InputPart part, DocumentModel docModel, ObjectPartType partMeta, Action action) + protected void fillPart(InputPart part, DocumentModel docModel, + ObjectPartType partMeta, Action action, ServiceContext ctx) throws Exception { InputStream payload = part.getBody(InputStream.class, null); @@ -240,7 +236,7 @@ public abstract class RemoteDocumentModelHandlerImpl //TODO: callback to handler if registered to validate the //document // Map objectProps = DocumentUtils.parseProperties(document.getFirstChild()); - Map objectProps = DocumentUtils.parseProperties(partMeta, document); + Map objectProps = DocumentUtils.parseProperties(partMeta, document, ctx); if (action == Action.UPDATE) { this.filterReadOnlyPropertiesForPart(objectProps, partMeta); } diff --git a/services/common/src/main/java/org/collectionspace/services/nuxeo/client/java/RemoteSubItemDocumentModelHandlerImpl.java b/services/common/src/main/java/org/collectionspace/services/nuxeo/client/java/RemoteSubItemDocumentModelHandlerImpl.java index 2b9f88051..baabb0adb 100644 --- a/services/common/src/main/java/org/collectionspace/services/nuxeo/client/java/RemoteSubItemDocumentModelHandlerImpl.java +++ b/services/common/src/main/java/org/collectionspace/services/nuxeo/client/java/RemoteSubItemDocumentModelHandlerImpl.java @@ -27,6 +27,7 @@ import java.io.InputStream; import java.util.HashMap; import java.util.Map; +import org.collectionspace.services.common.context.ServiceContext; import org.collectionspace.services.common.document.DocumentUtils; import org.collectionspace.services.common.service.ObjectPartType; @@ -66,7 +67,7 @@ public abstract class RemoteSubItemDocumentModelHandlerImpl extends */ @Override protected void fillPart(InputPart part, DocumentModel docModel, - ObjectPartType partMeta, Action action) + ObjectPartType partMeta, Action action, ServiceContext ctx) throws Exception { InputStream payload = part.getBody(InputStream.class, null); diff --git a/services/movement/service/src/main/java/org/collectionspace/services/movement/nuxeo/MovementValidatorHandler.java b/services/movement/service/src/main/java/org/collectionspace/services/movement/nuxeo/MovementValidatorHandler.java index 0bd4dd934..b01efb893 100644 --- a/services/movement/service/src/main/java/org/collectionspace/services/movement/nuxeo/MovementValidatorHandler.java +++ b/services/movement/service/src/main/java/org/collectionspace/services/movement/nuxeo/MovementValidatorHandler.java @@ -24,7 +24,10 @@ public class MovementValidatorHandler implements ValidatorHandler { if(logger.isDebugEnabled()) { logger.debug("validate() action=" + action.name()); } + + /* try { + MultipartServiceContext mctx = (MultipartServiceContext) ctx; MovementsCommon mc = (MovementsCommon) mctx.getInputPart(mctx.getCommonPartLabel(), MovementsCommon.class); @@ -39,8 +42,9 @@ public class MovementValidatorHandler implements ValidatorHandler { // in the incoming payload are date fields whose values we // might wish to validate, and of extracting their values, // than hard-coding them here. + // + // See DocumentUtils.parseProperties() for one possible approach. - /* boolean validDateFormat = false; String locDate = mc.getLocationDate(); for (String pattern : patterns) { @@ -52,8 +56,6 @@ public class MovementValidatorHandler implements ValidatorHandler { invalid = true; msgBldr.append("\nlocationDate : unrecognized date format '" + locDate + "'"); } - * - */ if(action.equals(Action.CREATE)) { //create specific validation here @@ -71,6 +73,8 @@ public class MovementValidatorHandler implements ValidatorHandler { } catch (Exception e) { throw new InvalidDocumentException(e); } + * + */ } -- 2.47.3