| /******************************************************************************* |
| * 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 + "'"; |
| } |
| } |