/******************************************************************************* * Copyright 2013 SAP AG * * 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 com.sap.core.odata.core.edm; import java.util.Calendar; import java.util.Date; import java.util.TimeZone; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.sap.core.odata.api.edm.EdmFacets; import com.sap.core.odata.api.edm.EdmLiteralKind; import com.sap.core.odata.api.edm.EdmSimpleTypeException; /** * Implementation of the EDM simple type DateTime. * @author SAP AG */ public class EdmDateTime extends AbstractSimpleType { private static final Pattern PATTERN = Pattern.compile( "(\\p{Digit}{1,4})-(\\p{Digit}{1,2})-(\\p{Digit}{1,2})" + "T(\\p{Digit}{1,2}):(\\p{Digit}{1,2})(?::(\\p{Digit}{1,2})(\\.(\\p{Digit}{0,3}?)0*)?)?"); private static final Pattern JSON_PATTERN = Pattern.compile("/Date\\((-?\\p{Digit}+)\\)/"); private static final EdmDateTime instance = new EdmDateTime(); public static EdmDateTime getInstance() { return instance; } @Override public Class<?> getDefaultType() { return Calendar.class; } @Override protected <T> T internalValueOfString(final String value, final EdmLiteralKind literalKind, final EdmFacets facets, final Class<T> returnType) throws EdmSimpleTypeException { // In JSON, we allow also the XML literal form, so there is on purpose // no exception if the JSON pattern does not match. if (literalKind == EdmLiteralKind.JSON) { final Matcher matcher = JSON_PATTERN.matcher(value); if (matcher.matches()) { long millis; try { millis = Long.parseLong(matcher.group(1)); } catch (final NumberFormatException e) { throw new EdmSimpleTypeException(EdmSimpleTypeException.LITERAL_ILLEGAL_CONTENT.addContent(value), e); } if (returnType.isAssignableFrom(Long.class)) { return returnType.cast(millis); } else if (returnType.isAssignableFrom(Date.class)) { return returnType.cast(new Date(millis)); } else if (returnType.isAssignableFrom(Calendar.class)) { Calendar dateTimeValue = Calendar.getInstance(TimeZone.getTimeZone("GMT")); dateTimeValue.clear(); dateTimeValue.setTimeInMillis(millis); return returnType.cast(dateTimeValue); } else { throw new EdmSimpleTypeException(EdmSimpleTypeException.VALUE_TYPE_NOT_SUPPORTED.addContent(returnType)); } } } Calendar dateTimeValue = Calendar.getInstance(TimeZone.getTimeZone("GMT")); dateTimeValue.clear(); if (literalKind == EdmLiteralKind.URI) { if (value.length() > 10 && value.startsWith("datetime'") && value.endsWith("'")) { parseLiteral(value.substring(9, value.length() - 1), facets, dateTimeValue); } else { throw new EdmSimpleTypeException(EdmSimpleTypeException.LITERAL_ILLEGAL_CONTENT.addContent(value)); } } else { parseLiteral(value, facets, dateTimeValue); } if (returnType.isAssignableFrom(Calendar.class)) { return returnType.cast(dateTimeValue); } else if (returnType.isAssignableFrom(Long.class)) { return returnType.cast(dateTimeValue.getTimeInMillis()); } else if (returnType.isAssignableFrom(Date.class)) { return returnType.cast(dateTimeValue.getTime()); } else { throw new EdmSimpleTypeException(EdmSimpleTypeException.VALUE_TYPE_NOT_SUPPORTED.addContent(returnType)); } } /** * Parses a formatted date/time value and sets the values of a * {@link Calendar} object accordingly. * @param value the formatted date/time value as String * @param facets additional constraints for parsing (optional) * @param dateTimeValue the Calendar object to be set to the parsed value * @throws EdmSimpleTypeException */ protected static void parseLiteral(final String value, final EdmFacets facets, final Calendar dateTimeValue) throws EdmSimpleTypeException { final Matcher matcher = PATTERN.matcher(value); if (!matcher.matches()) { throw new EdmSimpleTypeException(EdmSimpleTypeException.LITERAL_ILLEGAL_CONTENT.addContent(value)); } dateTimeValue.set( Short.parseShort(matcher.group(1)), Byte.parseByte(matcher.group(2)) - 1, // month is zero-based Byte.parseByte(matcher.group(3)), Byte.parseByte(matcher.group(4)), Byte.parseByte(matcher.group(5)), matcher.group(6) == null ? 0 : Byte.parseByte(matcher.group(6))); if (matcher.group(7) != null) { if (matcher.group(7).length() == 1 || matcher.group(7).length() > 8) { throw new EdmSimpleTypeException(EdmSimpleTypeException.LITERAL_ILLEGAL_CONTENT.addContent(value)); } final String decimals = matcher.group(8); if (facets != null && facets.getPrecision() != null && facets.getPrecision() < decimals.length()) { throw new EdmSimpleTypeException(EdmSimpleTypeException.LITERAL_FACETS_NOT_MATCHED.addContent(value, facets)); } final String milliSeconds = decimals + "000".substring(decimals.length()); dateTimeValue.set(Calendar.MILLISECOND, Short.parseShort(milliSeconds)); } // The Calendar class does not check any values until a get method is called, // so we do just that to validate the fields set above, not because we want // to return something else. For strict checks, the lenient mode is switched // off temporarily. dateTimeValue.setLenient(false); try { dateTimeValue.get(Calendar.MILLISECOND); } catch (final IllegalArgumentException e) { throw new EdmSimpleTypeException(EdmSimpleTypeException.LITERAL_ILLEGAL_CONTENT.addContent(value), e); } dateTimeValue.setLenient(true); } @Override protected <T> String internalValueToString(final T value, final EdmLiteralKind literalKind, final EdmFacets facets) throws EdmSimpleTypeException { long timeInMillis; if (value instanceof Date) { timeInMillis = ((Date) value).getTime(); } else if (value instanceof Calendar) { timeInMillis = ((Calendar) value).getTimeInMillis(); } else if (value instanceof Long) { timeInMillis = ((Long) value).longValue(); } else { throw new EdmSimpleTypeException(EdmSimpleTypeException.VALUE_TYPE_NOT_SUPPORTED.addContent(value.getClass())); } if (literalKind == EdmLiteralKind.JSON) { return "/Date(" + timeInMillis + ")/"; } Calendar dateTimeValue = Calendar.getInstance(TimeZone.getTimeZone("GMT")); dateTimeValue.setTimeInMillis(timeInMillis); StringBuilder result = new StringBuilder(23); // 23 characters are enough for millisecond precision. final int year = dateTimeValue.get(Calendar.YEAR); appendTwoDigits(result, year / 100); appendTwoDigits(result, year % 100); result.append('-'); appendTwoDigits(result, dateTimeValue.get(Calendar.MONTH) + 1); // month is zero-based result.append('-'); appendTwoDigits(result, dateTimeValue.get(Calendar.DAY_OF_MONTH)); result.append('T'); appendTwoDigits(result, dateTimeValue.get(Calendar.HOUR_OF_DAY)); result.append(':'); appendTwoDigits(result, dateTimeValue.get(Calendar.MINUTE)); result.append(':'); appendTwoDigits(result, dateTimeValue.get(Calendar.SECOND)); try { appendMilliseconds(result, timeInMillis, facets); } catch (final IllegalArgumentException e) { throw new EdmSimpleTypeException(EdmSimpleTypeException.VALUE_FACETS_NOT_MATCHED.addContent(value, facets), e); } return result.toString(); } /** * Appends the given number to the given string builder, * assuming that the number has at most two digits, performance-optimized. * @param result a {@link StringBuilder} * @param number an integer that must satisfy <code>0 <= number <= 99</code> */ private static void appendTwoDigits(final StringBuilder result, final int number) { result.append((char) ('0' + number / 10)); result.append((char) ('0' + number % 10)); } protected static void appendMilliseconds(final StringBuilder result, final long milliseconds, final EdmFacets facets) throws IllegalArgumentException { final int digits = milliseconds % 1000 == 0 ? 0 : milliseconds % 100 == 0 ? 1 : milliseconds % 10 == 0 ? 2 : 3; if (digits > 0) { result.append('.'); for (int d = 100; d > 0; d /= 10) { final byte digit = (byte) (milliseconds % (d * 10) / d); if (digit > 0 || milliseconds % d > 0) { result.append((char) ('0' + digit)); } } } if (facets != null && facets.getPrecision() != null) { final int precision = facets.getPrecision(); if (digits > precision) { throw new IllegalArgumentException(); } if (digits == 0 && precision > 0) { result.append('.'); } for (int i = digits; i < precision; i++) { result.append('0'); } } } @Override public String toUriLiteral(final String literal) throws EdmSimpleTypeException { return "datetime'" + literal + "'"; } }