blob: fdd07023011cff18fe41fc3b39e6409cbc6f48df [file] [log] [blame]
/*******************************************************************************
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.olingo.odata2.core.edm;
import java.sql.Timestamp;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.olingo.odata2.api.edm.EdmFacets;
import org.apache.olingo.odata2.api.edm.EdmLiteralKind;
import org.apache.olingo.odata2.api.edm.EdmSimpleTypeException;
/**
* Implementation of the EDM simple type DateTime.
*
*/
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,9}?)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();
String valueString;
if (literalKind == EdmLiteralKind.URI) {
//OLINGO-883 prefix is case insensitve so we need to check with lower case if we want to use startsWith()
if (value.length() > 10 && value.toLowerCase().startsWith("datetime'") && value.endsWith("'")) {
valueString = value.substring(9, value.length() - 1);
} else {
throw new EdmSimpleTypeException(EdmSimpleTypeException.LITERAL_ILLEGAL_CONTENT.addContent(value));
}
} else {
valueString = value;
}
final Matcher matcher = PATTERN.matcher(valueString);
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)));
int nanoSeconds = 0;
if (matcher.group(7) != null) {
if (matcher.group(7).length() == 1 || matcher.group(7).length() > 10) {
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));
}
nanoSeconds = Integer.parseInt(decimals + "000000000".substring(decimals.length()));
if (!(returnType.isAssignableFrom(Timestamp.class))) {
if (nanoSeconds % (1000 * 1000) == 0) {
dateTimeValue.set(Calendar.MILLISECOND, nanoSeconds / (1000 * 1000));
} else {
throw new EdmSimpleTypeException(EdmSimpleTypeException.LITERAL_ILLEGAL_CONTENT.addContent(value));
}
}
}
// 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);
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 if (returnType.isAssignableFrom(Timestamp.class)) {
Timestamp timestamp = new Timestamp(dateTimeValue.getTimeInMillis());
if (literalKind != EdmLiteralKind.JSON) {
timestamp.setNanos(nanoSeconds);
}
return returnType.cast(timestamp);
} else {
throw new EdmSimpleTypeException(EdmSimpleTypeException.VALUE_TYPE_NOT_SUPPORTED.addContent(returnType));
}
}
@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) {
if (value instanceof Timestamp && ((Timestamp) value).getNanos() % (1000 * 1000) != 0) {
throw new EdmSimpleTypeException(EdmSimpleTypeException.VALUE_ILLEGAL_CONTENT.addContent(value));
} else {
return "/Date(" + timeInMillis + ")/";
}
}
Calendar dateTimeValue = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
dateTimeValue.setTimeInMillis(timeInMillis);
StringBuilder result = new StringBuilder(29); // 29 characters are enough for nanosecond 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));
final int fractionalSecs = value instanceof Timestamp ?
((Timestamp) value).getNanos() :
dateTimeValue.get(Calendar.MILLISECOND);
try {
appendFractionalSeconds(result, fractionalSecs, value instanceof Timestamp, 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));
}
/**
* Appends the given milli- or nanoseconds to the given string builder, performance-optimized.
* @param result a {@link StringBuilder}
* @param fractionalSeconds fractional seconds (nonnegative and assumed to be in the valid range)
* @param isNano whether the value is to be interpreted as nanoseconds (milliseconds if false)
* @param facets the EDM facets containing an upper limit for decimal digits (optional, defaults to zero)
* @throws IllegalArgumentException if precision is not met
*/
protected static void appendFractionalSeconds(StringBuilder result, final int fractionalSeconds,
final boolean isNano, final EdmFacets facets) throws IllegalArgumentException {
int significantDigits = 0;
if (fractionalSeconds > 0) {
// Determine the number of significant digits.
significantDigits = isNano ? 9 : 3;
int output = fractionalSeconds;
while (output % 10 == 0) {
output /= 10;
significantDigits--;
}
result.append('.');
for (int d = 100 * (isNano ? 1000 * 1000 : 1); d > 0; d /= 10) {
final byte digit = (byte) (fractionalSeconds % (d * 10) / d);
if (digit > 0 || fractionalSeconds % d > 0) {
result.append((char) ('0' + digit));
}
}
}
// Check precision constraint.
final Integer precision = facets == null || facets.getPrecision() == null ? null : facets.getPrecision();
if (precision != null) {
if (precision < significantDigits) {
throw new IllegalArgumentException();
} else {
// Add additional zeroes if the precision is larger than the number of significant digits.
if (significantDigits == 0 && precision > 0) {
result.append('.');
}
for (int i = significantDigits; i < precision; i++) {
result.append('0');
}
}
}
}
@Override
public String toUriLiteral(final String literal) throws EdmSimpleTypeException {
return "datetime'" + literal + "'";
}
}