From 2e9d6eaa0147ba9b1c5ad37477a4fc3bf3618ede Mon Sep 17 00:00:00 2001 From: Aron Roberts Date: Wed, 14 Jul 2010 03:49:37 +0000 Subject: [PATCH] CSPACE-2408,CSPACE-2418: Changed datatypes for three calendar date fields in Movement records to xs:dateTime, to prototype storage of dates as date types, rather than strings. Provides a uniform date and time representation - a widely used ISO 8601 format - to be provided by clients as values in calendar date fields, as early work on CSPACE-2418. Dates round-trip correctly, but are stored in MySQL in a local time zone, rather than in UTC. This is potentially problematic, and may require further investigation. Two utility classes included in this commit likely belong in 'common' package, or in a future DateAndTime service, rather than in Movement, and they - or similar classes - will likely be migrated there in a future check-in. --- .../resources/schemas/movements_common.xsd | 6 +- services/movement/client/pom.xml | 4 +- .../client/MovementClientDateTimeUtils.java | 216 ++++++++++++++++++ .../client/test/MovementServiceTest.java | 8 +- .../nuxeo/MovementDocumentModelHandler.java | 78 ++++++- .../nuxeo/MovementServiceDateTimeUtils.java | 194 ++++++++++++++++ 6 files changed, 496 insertions(+), 10 deletions(-) create mode 100644 services/movement/client/src/main/java/org/collectionspace/services/client/MovementClientDateTimeUtils.java create mode 100644 services/movement/service/src/main/java/org/collectionspace/services/movement/nuxeo/MovementServiceDateTimeUtils.java diff --git a/services/movement/3rdparty/nuxeo-platform-cs-movement/src/main/resources/schemas/movements_common.xsd b/services/movement/3rdparty/nuxeo-platform-cs-movement/src/main/resources/schemas/movements_common.xsd index 693ec14c8..5cdf43fcb 100644 --- a/services/movement/3rdparty/nuxeo-platform-cs-movement/src/main/resources/schemas/movements_common.xsd +++ b/services/movement/3rdparty/nuxeo-platform-cs-movement/src/main/resources/schemas/movements_common.xsd @@ -24,7 +24,7 @@ - + @@ -38,8 +38,8 @@ - - + + diff --git a/services/movement/client/pom.xml b/services/movement/client/pom.xml index 8b6fe1fbf..bbc2e396c 100644 --- a/services/movement/client/pom.xml +++ b/services/movement/client/pom.xml @@ -18,12 +18,12 @@ org.slf4j slf4j-api - test + provided org.slf4j slf4j-log4j12 - test + provided diff --git a/services/movement/client/src/main/java/org/collectionspace/services/client/MovementClientDateTimeUtils.java b/services/movement/client/src/main/java/org/collectionspace/services/client/MovementClientDateTimeUtils.java new file mode 100644 index 000000000..8fc7ee267 --- /dev/null +++ b/services/movement/client/src/main/java/org/collectionspace/services/client/MovementClientDateTimeUtils.java @@ -0,0 +1,216 @@ +/** + * This document is a part of the source code and related artifacts + * for CollectionSpace, an open source collections management system + * for museums and related institutions: + * + * http://www.collectionspace.org + * http://wiki.collectionspace.org + * + * Copyright (c) 2009 Regents of the University of California + * + * Licensed under the Educational Community License (ECL), Version 2.0. + * You may not use this file except in compliance with this License. + * + * You may obtain a copy of the ECL 2.0 License at + * https://source.collectionspace.org/collection-space/LICENSE.txt + */ +package org.collectionspace.services.client; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +import javax.xml.datatype.DatatypeConfigurationException; +import javax.xml.datatype.DatatypeFactory; +import javax.xml.datatype.XMLGregorianCalendar; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * MovementClientDateTimeUtils.java + * + * $LastChangedRevision: 2107 $ + * $LastChangedDate: 2010-05-17 18:22:27 -0700 (Mon, 17 May 2010) $ + * + */ +public class MovementClientDateTimeUtils { + + private static final Logger logger = + LoggerFactory.getLogger(MovementClientDateTimeUtils.class); + + final static String UTC_TIMEZONE_IDENTIFIER = "UTC"; + final static String ISO_8601_UTC_TIMESTAMP_PATTERN = "yyyy-MM-dd'T'HH:mm:ss'Z'"; + + // FIXME The methods below are not specific to the Movement service + // or its client code. + // + // At present, they may redundantly be included in or referenced from + // several classes in the Movement service module, in its 'service' + // and/or 'client' sub-modules. + // + // However, these methods, and any associated constants and imports + // above, should instead be moved to the Date and Time service or + // into another common package, where they can be shared by multiple services. + + /** + * Returns the default time zone. + * + * @return The default time zone + */ + public static TimeZone defaultTimeZone() { + return TimeZone.getDefault(); + } + + /** + * Returns a calendar date, representing the current date and time instance + * in the default time zone. + * + * @return The current date and time instance in the default time zone + */ + public static GregorianCalendar currentDateAndTime() { + return currentDateAndTime(defaultTimeZone()); + } + + /** + * Returns the UTC time zone. + * + * @return The UTC time zone. Defaults to the closely-related GMT time zone, + * if for some reason the UTC time zone identifier cannot be understood. + */ + public static TimeZone UTCTimeZone() { + return TimeZone.getTimeZone(UTC_TIMEZONE_IDENTIFIER); + } + + /** + * Returns a calendar date, representing the current date and time instance + * in the UTC time zone. + * + * @return The current date and time instance in the UTC time zone. + */ + public static GregorianCalendar currentDateAndTimeUTC() { + return currentDateAndTime(UTCTimeZone()); + } + + /** + * Returns a calendar date, representing the current date and time instance + * in the specified time zone. + * + * @return The current date and time instance in the specified time zone. + * If the time zone is null, will return the current time and + * date in the time zone intrinsic to a new Calendar instance. + */ + public static GregorianCalendar currentDateAndTime(TimeZone tz) { + GregorianCalendar gcal = new GregorianCalendar(); + if (tz != null) { + gcal.setTimeZone(tz); + } + Date now = new Date(); + gcal.setTime(now); + return gcal; + } + + /** + * Returns a String representing the current date and time instance. + * in the UTC time zone, formatted as an ISO 8601 timestamp. + * + * @return A String representing the current date and time instance. + */ + public static String timestampUTC() { + return formatAsISO8601Timestamp(currentDateAndTime(UTCTimeZone())); + } + + /** + * Returns a representation of a calendar date and time instance, + * as an ISO 8601-formatted timestamp in the UTC time zone. + * + * @param cal A calendar date and time instance + * + * @return A representation of that calendar date and time instance, + * as an ISO 8601-formatted timestamp in the UTC time zone. + */ + public static String formatAsISO8601Timestamp(GregorianCalendar cal) { + return formatCalendarDate(cal, UTCTimeZone(), ISO8601TimestampFormatter()); + } + + /** + * Formats a provided calendar date using a provided date formatter, + * in the default system time zone. + * + * @param date A calendar date to format. + * @param df A date formatter to apply. + * + * @return A formatted date string, or the empty string + * if one or more of the parameter values were invalid. + */ + public static String formatCalendarDate(GregorianCalendar gcal, DateFormat df) { + return formatCalendarDate(gcal, TimeZone.getDefault(), df); + } + + /** + * Formats a provided calendar date using a provided date formatter, + * in a provided time zone. + * + * @param date A calendar date to format. + * @param tz The time zone qualifier for the calendar date to format. + * @param df A date formatter to apply. + * + * @return A formatted date string, or the empty string + * if one or more of the parameter values were invalid. + */ + public static String formatCalendarDate(GregorianCalendar gcal, TimeZone tz, DateFormat df) { + String formattedDate = ""; + if (gcal == null) { + logger.warn("Null calendar date was provided when a non-null calendar date was required."); + return formattedDate; + } + if (tz == null) { + logger.warn("Null time zone was provided when a non-null time zone was required."); + return formattedDate; + } + if (df == null) { + logger.warn("Null date formatter was provided when a non-null date formatter was required."); + return formattedDate; + } + gcal.setTimeZone(tz); + Date date = gcal.getTime(); + df.setTimeZone(tz); + formattedDate = df.format(date); + return formattedDate; + } + + /** + * Returns a date formatter for an ISO 8601 timestamp pattern. + * + * @return A date formatter for an ISO 8601 timestamp pattern. + * This pattern is specified as a class constant above. + */ + public static DateFormat ISO8601TimestampFormatter() { + return getDateFormatter(ISO_8601_UTC_TIMESTAMP_PATTERN); + } + + /** + * Returns a date formatter for a provided date or date/time pattern. + * + * @param pattern A date or date/time pattern. + * + * @return A date formatter using that pattern, or null + * if the pattern was null, empty, or invalid. + */ + public static DateFormat getDateFormatter(String pattern) { + DateFormat df = null; + if (pattern == null || pattern.trim().isEmpty()) { + logger.warn("Null or empty date pattern string was provided " + + "when a non-null, non-empty date pattern string was required."); + return df; + } + try { + df = new SimpleDateFormat(pattern); + } catch (IllegalArgumentException iae) { + logger.warn("Invalid date pattern string: " + pattern); + } + return df; + } +} diff --git a/services/movement/client/src/test/java/org/collectionspace/services/client/test/MovementServiceTest.java b/services/movement/client/src/test/java/org/collectionspace/services/client/test/MovementServiceTest.java index bebc7bc5e..1ad9bd8d0 100644 --- a/services/movement/client/src/test/java/org/collectionspace/services/client/test/MovementServiceTest.java +++ b/services/movement/client/src/test/java/org/collectionspace/services/client/test/MovementServiceTest.java @@ -33,6 +33,7 @@ import javax.ws.rs.core.Response; import org.collectionspace.services.client.CollectionSpaceClient; import org.collectionspace.services.client.MovementClient; +import org.collectionspace.services.client.MovementClientDateTimeUtils; import org.collectionspace.services.jaxb.AbstractCommonList; import org.collectionspace.services.movement.MovementsCommon; import org.collectionspace.services.movement.MovementsCommonList; @@ -732,12 +733,13 @@ public class MovementServiceTest extends AbstractServiceTestImpl { */ private MultipartOutput createInstance(String movementReferenceNumber) { MovementsCommon movement = new MovementsCommon(); + String timestampUTC = MovementClientDateTimeUtils.timestampUTC(); // FIXME: Values of currentLocation, normalLocation, // and movementContact should be refNames. movement.setCurrentLocation("currentLocation value"); movement.setCurrentLocationFitness("currentLocationFitness value"); movement.setCurrentLocationNote("currentLocationNote value"); - movement.setLocationDate(timestampUTC()); + movement.setLocationDate(timestampUTC); movement.setNormalLocation("normalLocation value"); movement.setMovementContact("movementContact value"); MovementMethodsList movementMethodsList = new MovementMethodsList(); @@ -750,8 +752,8 @@ public class MovementServiceTest extends AbstractServiceTestImpl { movement.setMovementMethods(movementMethodsList); movement.setMovementNote("movementNote value"); movement.setMovementReferenceNumber(movementReferenceNumber); - movement.setPlannedRemovalDate("plannedRemovalDate value"); - movement.setRemovalDate("removalDate value"); + movement.setPlannedRemovalDate(timestampUTC); + movement.setRemovalDate(timestampUTC); movement.setReasonForMove("reasonForMove value"); MultipartOutput multipart = new MultipartOutput(); OutputPart commonPart = diff --git a/services/movement/service/src/main/java/org/collectionspace/services/movement/nuxeo/MovementDocumentModelHandler.java b/services/movement/service/src/main/java/org/collectionspace/services/movement/nuxeo/MovementDocumentModelHandler.java index c7cf06b04..f06a615de 100644 --- a/services/movement/service/src/main/java/org/collectionspace/services/movement/nuxeo/MovementDocumentModelHandler.java +++ b/services/movement/service/src/main/java/org/collectionspace/services/movement/nuxeo/MovementDocumentModelHandler.java @@ -23,11 +23,17 @@ */ package org.collectionspace.services.movement.nuxeo; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.GregorianCalendar; import java.util.Iterator; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; import org.collectionspace.services.MovementJAXBSchema; import org.collectionspace.services.common.document.DocumentWrapper; +import org.collectionspace.services.common.service.ObjectPartType; import org.collectionspace.services.movement.MovementsCommon; import org.collectionspace.services.movement.MovementsCommonList; import org.collectionspace.services.movement.MovementsCommonList.MovementListItem; @@ -46,6 +52,10 @@ public class MovementDocumentModelHandler /** The logger. */ private final Logger logger = LoggerFactory.getLogger(MovementDocumentModelHandler.class); + + private static final String COMMON_PART_LABEL = "movements_common"; + + private static final ArrayList DATE_TIME_FIELDS = dateTimeFields(); /** The Movement. */ private MovementsCommon Movement; @@ -94,6 +104,29 @@ public class MovementDocumentModelHandler this.MovementList = MovementList; } + /* (non-Javadoc) + * @see org.collectionspace.services.nuxeo.client.java.RemoteDocumentModelHandlerImpl#extractPart(org.nuxeo.ecm.core.api.DocumentModel, java.lang.String, org.collectionspace.services.common.service.ObjectPartType) + */ + @Override + protected Map extractPart(DocumentModel docModel, String schema, ObjectPartType partMeta) + throws Exception { + Map unQObjectProperties = super.extractPart(docModel, schema, partMeta); + + // For each dateTime field in the common part, return an + // appropriately formatted representation of its value. + if (partMeta.getLabel().equalsIgnoreCase(COMMON_PART_LABEL)) { + for(Entry entry : unQObjectProperties.entrySet()){ + if (isDateTimeType(entry)) { + entry.setValue( + MovementServiceDateTimeUtils.formatAsISO8601Timestamp( + (GregorianCalendar) entry.getValue())); + } + } + } + + return unQObjectProperties; + } + /** * Extract common part. * @@ -136,8 +169,9 @@ public class MovementDocumentModelHandler MovementListItem ilistItem = new MovementListItem(); ilistItem.setMovementReferenceNumber((String) docModel.getProperty(getServiceContext().getCommonPartLabel(), MovementJAXBSchema.MOVEMENT_REFERENCE_NUMBER)); - ilistItem.setLocationDate((String) docModel.getProperty(getServiceContext().getCommonPartLabel(), - MovementJAXBSchema.LOCATION_DATE)); + GregorianCalendar gcal = (GregorianCalendar) docModel.getProperty(getServiceContext().getCommonPartLabel(), + MovementJAXBSchema.LOCATION_DATE); + ilistItem.setLocationDate(MovementServiceDateTimeUtils.formatAsISO8601Timestamp(gcal)); String id = NuxeoUtils.extractId(docModel.getPathAsString()); ilistItem.setUri(getServiceContextPath() + id); ilistItem.setCsid(id); @@ -157,6 +191,46 @@ public class MovementDocumentModelHandler public String getQProperty(String prop) { return MovementConstants.NUXEO_SCHEMA_NAME + ":" + prop; } + + private boolean isDateTimeType(Entry entry) { + boolean isDateTimeType = false; + + // Approach 1: Check the name of this property against a list of + // dateTime field names. + if (DATE_TIME_FIELDS.contains(entry.getKey())){ + isDateTimeType = true; + } + + // Approach 2: Check the data type of this property's value. + /* + if (entry.getValue() instanceof Calendar) { + isDateTimeType = true; + } + * + */ + + return isDateTimeType; + } + + /** + * Returns a list of the names of dateTime fields (e.g. fields whose + * XML Schema datatype is xs:dateTime). + * + * @return A list of names of dateTime fields. + */ + private static ArrayList dateTimeFields() { + // FIXME Rather than hard-coding directly here, + // identify these fields from configuration, such as: + // * Metadata returned from Nuxeo, if available. + // * Examination of the XSD schema for this document type. + // * External configuration (e.g. date fields as tenant bindings props, via + // org.collectionspace.services.common.context.ServiceBindingUtils) + ArrayList dateTimeTypeFields = new ArrayList(); + dateTimeTypeFields.add(MovementJAXBSchema.LOCATION_DATE); + dateTimeTypeFields.add(MovementJAXBSchema.PLANNED_REMOVAL_DATE); + dateTimeTypeFields.add(MovementJAXBSchema.REMOVAL_DATE); + return dateTimeTypeFields; + } } diff --git a/services/movement/service/src/main/java/org/collectionspace/services/movement/nuxeo/MovementServiceDateTimeUtils.java b/services/movement/service/src/main/java/org/collectionspace/services/movement/nuxeo/MovementServiceDateTimeUtils.java new file mode 100644 index 000000000..7877e2154 --- /dev/null +++ b/services/movement/service/src/main/java/org/collectionspace/services/movement/nuxeo/MovementServiceDateTimeUtils.java @@ -0,0 +1,194 @@ +/** + * This document is a part of the source code and related artifacts + * for CollectionSpace, an open source collections management system + * for museums and related institutions: + * + * http://www.collectionspace.org + * http://wiki.collectionspace.org + * + * Copyright (c) 2009 Regents of the University of California + * + * Licensed under the Educational Community License (ECL), Version 2.0. + * You may not use this file except in compliance with this License. + * + * You may obtain a copy of the ECL 2.0 License at + * https://source.collectionspace.org/collection-space/LICENSE.txt + */ +package org.collectionspace.services.movement.nuxeo; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * MovementClientDateTimeUtils.java + * + * $LastChangedRevision: 2107 $ + * $LastChangedDate: 2010-05-17 18:22:27 -0700 (Mon, 17 May 2010) $ + * + */ +public class MovementServiceDateTimeUtils { + + private static final Logger logger = + LoggerFactory.getLogger(MovementServiceDateTimeUtils.class); + + final static String UTC_TIMEZONE_IDENTIFIER = "UTC"; + final static String ISO_8601_UTC_TIMESTAMP_PATTERN = "yyyy-MM-dd'T'HH:mm:ss'Z'"; + + // FIXME The methods below are not specific to the Movement service + // or its client code. + // + // At present, they may redundantly be included in or referenced from + // several classes in the Movement service module, in its 'service' + // and/or 'client' sub-modules. + // + // However, these methods, and any associated constants and imports + // above, should instead be moved to the Date and Time service or + // into another common package, where they can be shared by multiple services. + + /** + * Returns the UTC time zone. + * + * @return The UTC time zone. Defaults to the closely-related GMT time zone, + * if for some reason the UTC time zone identifier cannot be understood. + */ + public static TimeZone UTCTimeZone() { + return TimeZone.getTimeZone(UTC_TIMEZONE_IDENTIFIER); + } + + /** + * Returns a calendar date, representing the current date and time instance + * in the UTC time zone. + * + * @return The current date and time instance in the UTC time zone. + */ + public static GregorianCalendar currentDateAndTimeUTC() { + return currentDateAndTime(UTCTimeZone()); + } + + /** + * Returns a calendar date, representing the current date and time instance + * in the specified time zone. + * + * @return The current date and time instance in the specified time zone. + * If the time zone is null, will return the current time and + * date in the time zone intrinsic to a new Calendar instance. + */ + public static GregorianCalendar currentDateAndTime(TimeZone tz) { + GregorianCalendar gcal = new GregorianCalendar(); + if (tz != null) { + gcal.setTimeZone(tz); + } + Date now = new Date(); + gcal.setTime(now); + return gcal; + } + + /** + * Returns a String representing the current date and time instance. + * in the UTC time zone, formatted as an ISO 8601 timestamp. + * + * @return A String representing the current date and time instance. + */ + public static String timestampUTC() { + return formatAsISO8601Timestamp(currentDateAndTime(UTCTimeZone())); + } + + /** + * Returns a representation of a calendar date and time instance, + * as an ISO 8601-formatted timestamp in the UTC time zone. + * + * @param cal A calendar date and time instance + * + * @return A representation of that calendar date and time instance, + * as an ISO 8601-formatted timestamp in the UTC time zone. + */ + public static String formatAsISO8601Timestamp(GregorianCalendar cal) { + return formatCalendarDate(cal, UTCTimeZone(), ISO8601TimestampFormatter()); + } + + /** + * Formats a provided calendar date using a provided date formatter, + * in the default system time zone. + * + * @param date A calendar date to format. + * @param df A date formatter to apply. + * + * @return A formatted date string, or the empty string + * if one or more of the parameter values were invalid. + */ + public static String formatCalendarDate(GregorianCalendar gcal, DateFormat df) { + return formatCalendarDate(gcal, TimeZone.getDefault(), df); + } + + /** + * Formats a provided calendar date using a provided date formatter, + * in a provided time zone. + * + * @param date A calendar date to format. + * @param tz The time zone qualifier for the calendar date to format. + * @param df A date formatter to apply. + * + * @return A formatted date string, or the empty string + * if one or more of the parameter values were invalid. + */ + public static String formatCalendarDate(GregorianCalendar gcal, TimeZone tz, DateFormat df) { + String formattedDate = ""; + if (gcal == null) { + logger.warn("Null calendar date was provided when a non-null calendar date was required."); + return formattedDate; + } + if (tz == null) { + logger.warn("Null time zone was provided when a non-null time zone was required."); + return formattedDate; + } + if (df == null) { + logger.warn("Null date formatter was provided when a non-null date formatter was required."); + return formattedDate; + } + gcal.setTimeZone(tz); + Date date = gcal.getTime(); + df.setTimeZone(tz); + formattedDate = df.format(date); + return formattedDate; + } + + /** + * Returns a date formatter for an ISO 8601 timestamp pattern. + * + * @return A date formatter for an ISO 8601 timestamp pattern. + * This pattern is specified as a class constant above. + */ + public static DateFormat ISO8601TimestampFormatter() { + return getDateFormatter(ISO_8601_UTC_TIMESTAMP_PATTERN); + } + + /** + * Returns a date formatter for a provided date or date/time pattern. + * + * @param pattern A date or date/time pattern. + * + * @return A date formatter using that pattern, or null + * if the pattern was null, empty, or invalid. + */ + public static DateFormat getDateFormatter(String pattern) { + DateFormat df = null; + if (pattern == null || pattern.trim().isEmpty()) { + logger.warn("Null or empty date pattern string was provided " + + "when a non-null, non-empty date pattern string was required."); + return df; + } + try { + df = new SimpleDateFormat(pattern); + } catch (IllegalArgumentException iae) { + logger.warn("Invalid date pattern string: " + pattern); + } + return df; + } + +} -- 2.47.3