/******************************************************************************* * 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 -= (matcher.group(2).equals("+") ? 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); 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 + "'"; } }