blob: b2c4321f2c466c2110d006b0ad252cdcfb5d55e9 [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 DateTimeOffset.
*
* Details about parsing of time strings to value objects can be found in the
* {@link org.apache.olingo.odata2.api.edm.EdmSimpleType} documentation.
*/
public class EdmDateTimeOffset 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}{1,7})?)?"
+ "(Z|([-+]\\p{Digit}{1,2}:\\p{Digit}{2}))?");
private static final Pattern JSON_PATTERN = Pattern.compile(
"/Date\\((-?\\p{Digit}+)(?:(\\+|-)(\\p{Digit}{1,4}))?\\)/");
private static final EdmDateTimeOffset instance = new EdmDateTimeOffset();
public static EdmDateTimeOffset 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 {
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() > 16 && value.toLowerCase().startsWith("datetimeoffset'") && value.endsWith("'")) {
return internalValueOfString(value.substring(15, value.length() - 1), EdmLiteralKind.DEFAULT, facets,
returnType);
} else {
throw new EdmSimpleTypeException(EdmSimpleTypeException.LITERAL_ILLEGAL_CONTENT.addContent(value));
}
}
Calendar dateTimeValue = null;
long millis = 0;
if (literalKind == EdmLiteralKind.JSON) {
final Matcher matcher = JSON_PATTERN.matcher(value);
if (matcher.matches()) {
try {
millis = Long.parseLong(matcher.group(1));
} catch (final NumberFormatException e) {
throw new EdmSimpleTypeException(EdmSimpleTypeException.LITERAL_ILLEGAL_CONTENT.addContent(value), e);
}
String timeZone = "GMT";
if (matcher.group(2) != null) {
final int offsetInMinutes = Integer.parseInt(matcher.group(3));
if (offsetInMinutes >= 24 * 60) {
throw new EdmSimpleTypeException(EdmSimpleTypeException.LITERAL_ILLEGAL_CONTENT.addContent(value));
}
if (offsetInMinutes != 0) {
timeZone += matcher.group(2) + String.valueOf(offsetInMinutes / 60)
+ ":" + String.format("%02d", offsetInMinutes % 60);
// Convert the local-time milliseconds to UTC.
millis -= ("+".equals(matcher.group(2)) ? 1 : -1) * offsetInMinutes * 60 * 1000;
}
}
dateTimeValue = Calendar.getInstance(TimeZone.getTimeZone(timeZone));
}
}
int nanoSeconds = 0;
if (dateTimeValue == null) {
final Matcher matcher = PATTERN.matcher(value);
if (!matcher.matches()) {
throw new EdmSimpleTypeException(EdmSimpleTypeException.LITERAL_ILLEGAL_CONTENT.addContent(value));
}
final String timeZoneOffset =
matcher.group(1) != null && matcher.group(2) != null && !matcher.group(2).matches("[-+]0+:0+") ? matcher
.group(2) : null;
dateTimeValue = Calendar.getInstance(TimeZone.getTimeZone("GMT" + timeZoneOffset));
if (dateTimeValue.get(Calendar.ZONE_OFFSET) == 0 && timeZoneOffset != null) {
throw new EdmSimpleTypeException(EdmSimpleTypeException.LITERAL_ILLEGAL_CONTENT.addContent(value));
}
dateTimeValue.clear();
final Timestamp timestamp = EdmDateTime.getInstance().internalValueOfString(
value.substring(0, matcher.group(1) == null ? value.length() : matcher.start(1)),
EdmLiteralKind.DEFAULT, facets, Timestamp.class);
millis = timestamp.getTime() - dateTimeValue.get(Calendar.ZONE_OFFSET);
nanoSeconds = timestamp.getNanos();
if (nanoSeconds % (1000 * 1000) != 0 && !returnType.isAssignableFrom(Timestamp.class)) {
throw new EdmSimpleTypeException(EdmSimpleTypeException.LITERAL_ILLEGAL_CONTENT.addContent(value));
}
}
if (returnType.isAssignableFrom(Calendar.class)) {
dateTimeValue.clear();
dateTimeValue.setTimeInMillis(millis);
return returnType.cast(dateTimeValue);
} else 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(Timestamp.class)) {
Timestamp timestamp = new Timestamp(millis);
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 milliSeconds; // number of milliseconds since 1970-01-01T00:00:00Z
int offset; // offset in milliseconds from GMT to the requested time zone
if (value instanceof Date) {
milliSeconds = ((Date) value).getTime();
// Although java.util.Date, as stated in its documentation,
// "is intended to reflect coordinated universal time (UTC)",
// its toString() method uses the default time zone. And so do we.
Calendar dateTimeValue = Calendar.getInstance();
dateTimeValue.setTime((Date) value);
offset = dateTimeValue.get(Calendar.ZONE_OFFSET) + dateTimeValue.get(Calendar.DST_OFFSET);
} else if (value instanceof Calendar) {
final Calendar dateTimeValue = (Calendar) ((Calendar) value).clone();
milliSeconds = dateTimeValue.getTimeInMillis();
offset = dateTimeValue.get(Calendar.ZONE_OFFSET) + dateTimeValue.get(Calendar.DST_OFFSET);
} else if (value instanceof Long) {
milliSeconds = (Long) value;
offset = 0;
} else {
throw new EdmSimpleTypeException(EdmSimpleTypeException.VALUE_TYPE_NOT_SUPPORTED.addContent(value.getClass()));
}
milliSeconds += offset; // Convert from UTC to local time.
final int offsetInMinutes = offset / 60 / 1000;
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(" + milliSeconds + (offset == 0 ? "" : String.format("%+05d", offsetInMinutes)) + ")/";
}
} else {
final String localTimeString =
EdmDateTime.getInstance().valueToString(
value instanceof Timestamp ? value : milliSeconds, EdmLiteralKind.DEFAULT, facets);
final int offsetHours = offsetInMinutes / 60;
final int offsetMinutes = Math.abs(offsetInMinutes % 60);
final String offsetString = offset == 0 ? "Z" : String.format("%+03d:%02d", offsetHours, offsetMinutes);
return localTimeString + offsetString;
}
}
@Override
public String toUriLiteral(final String literal) throws EdmSimpleTypeException {
return "datetimeoffset'" + literal + "'";
}
}