/*
 * 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.server.core.uri.parser;

import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.olingo.commons.api.edm.Edm;
import org.apache.olingo.commons.api.edm.EdmEntityType;
import org.apache.olingo.commons.api.edm.EdmFunction;
import org.apache.olingo.commons.api.edm.EdmKeyPropertyRef;
import org.apache.olingo.commons.api.edm.EdmNavigationProperty;
import org.apache.olingo.commons.api.edm.EdmParameter;
import org.apache.olingo.commons.api.edm.EdmPrimitiveType;
import org.apache.olingo.commons.api.edm.EdmPrimitiveTypeException;
import org.apache.olingo.commons.api.edm.EdmPrimitiveTypeKind;
import org.apache.olingo.commons.api.edm.EdmProperty;
import org.apache.olingo.commons.api.edm.EdmStructuredType;
import org.apache.olingo.commons.api.edm.EdmType;
import org.apache.olingo.commons.api.edm.EdmTypeDefinition;
import org.apache.olingo.commons.api.edm.FullQualifiedName;
import org.apache.olingo.commons.api.edm.constants.EdmTypeKind;
import org.apache.olingo.server.api.OData;
import org.apache.olingo.server.api.uri.UriParameter;
import org.apache.olingo.server.api.uri.UriResourcePartTyped;
import org.apache.olingo.server.api.uri.queryoption.AliasQueryOption;
import org.apache.olingo.server.api.uri.queryoption.expression.Expression;
import org.apache.olingo.server.api.uri.queryoption.expression.Literal;
import org.apache.olingo.server.core.ODataImpl;
import org.apache.olingo.server.core.uri.UriParameterImpl;
import org.apache.olingo.server.core.uri.UriResourceTypedImpl;
import org.apache.olingo.server.core.uri.UriResourceWithKeysImpl;
import org.apache.olingo.server.core.uri.parser.UriTokenizer.TokenKind;
import org.apache.olingo.server.core.uri.queryoption.AliasQueryOptionImpl;
import org.apache.olingo.server.core.uri.queryoption.expression.LiteralImpl;
import org.apache.olingo.server.core.uri.validator.UriValidationException;

public class ParserHelper {

  private static final OData odata = new ODataImpl();

  protected static final Map<TokenKind, EdmPrimitiveTypeKind> tokenToPrimitiveType;
  static {
    /* Enum and null are not present in the map. These have to be handled differently. */
    Map<TokenKind, EdmPrimitiveTypeKind> temp = new EnumMap<>(TokenKind.class);
    temp.put(TokenKind.BooleanValue, EdmPrimitiveTypeKind.Boolean);
    temp.put(TokenKind.StringValue, EdmPrimitiveTypeKind.String);
    // Very large integer values are of type Edm.Decimal but this is handled elsewhere.
    temp.put(TokenKind.IntegerValue, EdmPrimitiveTypeKind.Int64);
    temp.put(TokenKind.GuidValue, EdmPrimitiveTypeKind.Guid);
    temp.put(TokenKind.DateValue, EdmPrimitiveTypeKind.Date);
    temp.put(TokenKind.DateTimeOffsetValue, EdmPrimitiveTypeKind.DateTimeOffset);
    temp.put(TokenKind.TimeOfDayValue, EdmPrimitiveTypeKind.TimeOfDay);
    temp.put(TokenKind.DecimalValue, EdmPrimitiveTypeKind.Decimal);
    temp.put(TokenKind.DoubleValue, EdmPrimitiveTypeKind.Double);
    temp.put(TokenKind.DurationValue, EdmPrimitiveTypeKind.Duration);
    temp.put(TokenKind.BinaryValue, EdmPrimitiveTypeKind.Binary);

    temp.put(TokenKind.GeographyPoint, EdmPrimitiveTypeKind.GeographyPoint);
    temp.put(TokenKind.GeometryPoint, EdmPrimitiveTypeKind.GeometryPoint);
    temp.put(TokenKind.GeographyLineString, EdmPrimitiveTypeKind.GeographyLineString);
    temp.put(TokenKind.GeometryLineString, EdmPrimitiveTypeKind.GeometryLineString);
    temp.put(TokenKind.GeographyPolygon, EdmPrimitiveTypeKind.GeographyPolygon);
    temp.put(TokenKind.GeometryPolygon, EdmPrimitiveTypeKind.GeometryPolygon);
    temp.put(TokenKind.GeographyMultiPoint, EdmPrimitiveTypeKind.GeographyMultiPoint);
    temp.put(TokenKind.GeometryMultiPoint, EdmPrimitiveTypeKind.GeometryMultiPoint);
    temp.put(TokenKind.GeographyMultiLineString, EdmPrimitiveTypeKind.GeographyMultiLineString);
    temp.put(TokenKind.GeometryMultiLineString, EdmPrimitiveTypeKind.GeometryMultiLineString);
    temp.put(TokenKind.GeographyMultiPolygon, EdmPrimitiveTypeKind.GeographyMultiPolygon);
    temp.put(TokenKind.GeometryMultiPolygon, EdmPrimitiveTypeKind.GeometryMultiPolygon);
    temp.put(TokenKind.GeographyCollection, EdmPrimitiveTypeKind.GeographyCollection);
    temp.put(TokenKind.GeometryCollection, EdmPrimitiveTypeKind.GeometryCollection);

    tokenToPrimitiveType = Collections.unmodifiableMap(temp);
  }

  protected static void requireNext(UriTokenizer tokenizer, final TokenKind required) throws UriParserException {
    if (!tokenizer.next(required)) {
      throw new UriParserSyntaxException("Expected token '" + required.toString() + "' not found.",
          UriParserSyntaxException.MessageKeys.SYNTAX);
    }
  }

  protected static void requireTokenEnd(UriTokenizer tokenizer) throws UriParserException {
    requireNext(tokenizer, TokenKind.EOF);
  }

  protected static boolean bws(UriTokenizer tokenizer) {
    return tokenizer.nextWhitespace();
  }
  
  protected static TokenKind next(UriTokenizer tokenizer, final TokenKind... kinds) {
    for (final TokenKind kind : kinds) {
      if (tokenizer.next(kind)) {
        return kind;
      }
    }
    return null;
  }

  protected static TokenKind nextPrimitiveValue(UriTokenizer tokenizer) {
    return next(tokenizer,
        TokenKind.NULL,
        TokenKind.BooleanValue,
        TokenKind.StringValue,

        // The order of the next seven expressions is important in order to avoid
        // finding partly parsed tokens (counter-intuitive as it may be, even a GUID may start with digits ...).
        TokenKind.GuidValue,
        TokenKind.DoubleValue,
        TokenKind.DecimalValue,
        TokenKind.DateTimeOffsetValue,
        TokenKind.DateValue,
        TokenKind.TimeOfDayValue,
        TokenKind.IntegerValue,

        TokenKind.DurationValue,
        TokenKind.BinaryValue,
        TokenKind.EnumValue,

        // Geography and geometry literals are defined to be primitive,
        // although they contain several parts with their own meaning.
        TokenKind.GeographyPoint,
        TokenKind.GeometryPoint,
        TokenKind.GeographyLineString,
        TokenKind.GeometryLineString,
        TokenKind.GeographyPolygon,
        TokenKind.GeometryPolygon,
        TokenKind.GeographyMultiPoint,
        TokenKind.GeometryMultiPoint,
        TokenKind.GeographyMultiLineString,
        TokenKind.GeometryMultiLineString,
        TokenKind.GeographyMultiPolygon,
        TokenKind.GeometryMultiPolygon,
        TokenKind.GeographyCollection,
        TokenKind.GeometryCollection);
  }

  protected static List<UriParameter> parseFunctionParameters(UriTokenizer tokenizer,
      final Edm edm, final EdmType referringType, final boolean withComplex,
      final Map<String, AliasQueryOption> aliases)
      throws UriParserException, UriValidationException {
    List<UriParameter> parameters = new ArrayList<>();
    Set<String> parameterNames = new HashSet<>();
    ParserHelper.requireNext(tokenizer, TokenKind.OPEN);
    if (tokenizer.next(TokenKind.CLOSE)) {
      return parameters;
    }
    do {
      ParserHelper.requireNext(tokenizer, TokenKind.ODataIdentifier);
      final String name = tokenizer.getText();
      if (parameterNames.contains(name)) {
        throw new UriParserSemanticException("Duplicated function parameter " + name,
            UriParserSemanticException.MessageKeys.INVALID_KEY_VALUE, name);
      }
      parameterNames.add(name);
      ParserHelper.requireNext(tokenizer, TokenKind.EQ);
      if (tokenizer.next(TokenKind.COMMA) || tokenizer.next(TokenKind.CLOSE) || tokenizer.next(TokenKind.EOF)) {
        throw new UriParserSyntaxException("Parameter value expected.", UriParserSyntaxException.MessageKeys.SYNTAX);
      }
      UriParameterImpl parameter = new UriParameterImpl().setName(name);
      if (tokenizer.next(TokenKind.ParameterAliasName)) {
        final String aliasName = tokenizer.getText();
        parameter.setAlias(aliasName)
            .setExpression(aliases.containsKey(aliasName) ? aliases.get(aliasName).getValue() : null);
      } else if (tokenizer.next(TokenKind.jsonArrayOrObject)) {
        if (withComplex) {
          parameter.setText(tokenizer.getText());
        } else {
          throw new UriParserSemanticException("A JSON array or object is not allowed as parameter value.",
              UriParserSemanticException.MessageKeys.COMPLEX_PARAMETER_IN_RESOURCE_PATH, tokenizer.getText());
        }
      } else if (withComplex) {
        final Expression expression = new ExpressionParser(edm, odata).parse(tokenizer, referringType, null, aliases);
        parameter.setText(expression instanceof Literal ?
            "null".equals(((Literal) expression).getText()) ? null : ((Literal) expression).getText() :
            null)
            .setExpression(expression instanceof Literal ? null : expression);
      } else if (nextPrimitiveValue(tokenizer) == null) {
        throw new UriParserSemanticException("Wrong parameter value.",
            UriParserSemanticException.MessageKeys.INVALID_KEY_VALUE, "");
      } else {
        final String literalValue = tokenizer.getText();
        parameter.setText("null".equals(literalValue) ? null : literalValue);
      }
      parameters.add(parameter);
    } while (tokenizer.next(TokenKind.COMMA));
    ParserHelper.requireNext(tokenizer, TokenKind.CLOSE);
    return parameters;
  }

  protected static void validateFunctionParameters(final EdmFunction function, final List<UriParameter> parameters,
      final Edm edm, final EdmType referringType, final Map<String, AliasQueryOption> aliases)
      throws UriParserException, UriValidationException {
    for (final UriParameter parameter : parameters) {
      final String parameterName = parameter.getName();
      final EdmParameter edmParameter = function.getParameter(parameterName);
      final boolean isNullable = edmParameter.isNullable();
      if (parameter.getText() == null && parameter.getExpression() == null && !isNullable) {
        if (parameter.getAlias() == null) {
          // No alias, value is explicitly null.
          throw new UriValidationException("Missing non-nullable parameter " + parameterName,
              UriValidationException.MessageKeys.MISSING_PARAMETER, parameterName);
        } else {
          final String valueForAlias = aliases.containsKey(parameter.getAlias()) ?
              parseAliasValue(parameter.getAlias(),
                  edmParameter.getType(), edmParameter.isNullable(), edmParameter.isCollection(),
                  edm, referringType, aliases).getText() :
              null;
          // Alias value is missing or explicitly null.
          if (valueForAlias == null) {
            throw new UriValidationException("Missing alias for " + parameterName,
                UriValidationException.MessageKeys.MISSING_ALIAS, parameter.getAlias());
          }
        }
      }
    }
  }

  protected static AliasQueryOption parseAliasValue(final String name, final EdmType type, final boolean isNullable,
      final boolean isCollection, final Edm edm, final EdmType referringType,
      final Map<String, AliasQueryOption> aliases) throws UriParserException, UriValidationException {
    final EdmTypeKind kind = type == null ? null : type.getKind();
    final AliasQueryOption alias = aliases.get(name);
    if (alias != null && alias.getText() != null) {
      UriTokenizer aliasTokenizer = new UriTokenizer(alias.getText());
      if (kind == null
          || !((isCollection || kind == EdmTypeKind.COMPLEX || kind == EdmTypeKind.ENTITY ?
          aliasTokenizer.next(TokenKind.jsonArrayOrObject) :
          nextPrimitiveTypeValue(aliasTokenizer, (EdmPrimitiveType) type, isNullable))
          && aliasTokenizer.next(TokenKind.EOF))) {
        // The alias value is not an allowed literal value, so parse it again as expression.
        aliasTokenizer = new UriTokenizer(alias.getText());
        // Don't pass on the current alias to avoid circular references.
        Map<String, AliasQueryOption> aliasesInner = new HashMap<>(aliases);
        aliasesInner.remove(name);
        final Expression expression = new ExpressionParser(edm, odata)
            .parse(aliasTokenizer, referringType, null, aliasesInner);
        final EdmType expressionType = ExpressionParser.getType(expression);
        if (aliasTokenizer.next(TokenKind.EOF)
            && (expressionType == null || type == null || expressionType.equals(type))) {
          ((AliasQueryOptionImpl) alias).setAliasValue(expression);
        } else {
          throw new UriParserSemanticException("Illegal value for alias '" + alias.getName() + "'.",
              UriParserSemanticException.MessageKeys.UNKNOWN_PART, alias.getText());
        }
      }
    }
    return alias;
  }

  protected static List<UriParameter> parseNavigationKeyPredicate(UriTokenizer tokenizer,
      final EdmNavigationProperty navigationProperty,
      final Edm edm, final EdmType referringType, final Map<String, AliasQueryOption> aliases)
      throws UriParserException, UriValidationException {
    if (tokenizer.next(TokenKind.OPEN)) {
      if (navigationProperty.isCollection()) {
        return parseKeyPredicate(tokenizer, navigationProperty.getType(), navigationProperty.getPartner(),
            edm, referringType, aliases);
      } else {
        throw new UriParserSemanticException("A key is not allowed on non-collection navigation properties.",
            UriParserSemanticException.MessageKeys.KEY_NOT_ALLOWED);
      }
    }
    return null;
  }

  protected static List<UriParameter> parseKeyPredicate(UriTokenizer tokenizer, final EdmEntityType edmEntityType,
      final EdmNavigationProperty partner,
      final Edm edm, final EdmType referringType, final Map<String, AliasQueryOption> aliases)
      throws UriParserException, UriValidationException {
    final List<EdmKeyPropertyRef> keyPropertyRefs = edmEntityType.getKeyPropertyRefs();
    if (tokenizer.next(TokenKind.CLOSE)) {
      throw new UriParserSemanticException(
          "Expected " + keyPropertyRefs.size() + " key predicates but got none.",
          UriParserSemanticException.MessageKeys.WRONG_NUMBER_OF_KEY_PROPERTIES,
          Integer.toString(keyPropertyRefs.size()), "0");
    }
    List<UriParameter> keys = new ArrayList<>();
    Map<String, String> referencedNames = new HashMap<>();

    if (partner != null) {
      // Prepare list of potentially missing keys to be filled from referential constraints.
      for (final String name : edmEntityType.getKeyPredicateNames()) {
        final String referencedName = partner.getReferencingPropertyName(name);
        if (referencedName != null) {
          referencedNames.put(name, referencedName);
        }
      }
      for (final EdmKeyPropertyRef candidate : keyPropertyRefs) {
    	  final UriParameter simpleKey = simpleKey(tokenizer, candidate, edm, referringType, aliases);
          if (simpleKey != null) {
            keys.add(simpleKey);
          }
      }
    }

    if (keyPropertyRefs.size() - referencedNames.size() == 1) {
      for (final EdmKeyPropertyRef candidate : keyPropertyRefs) {
       if (referencedNames.get(candidate.getName()) == null) {
          final UriParameter simpleKey = simpleKey(tokenizer, candidate, edm, referringType, aliases);
          if (simpleKey != null) {
            keys.add(simpleKey);
          }
          break;
        }
      }
    }
    if (keys.isEmpty()) {
      if (tokenizer.next(TokenKind.ODataIdentifier)) {
        keys.addAll(compoundKey(tokenizer, edmEntityType, edm, referringType, aliases));
      } else {
        throw new UriParserSemanticException("The key value is not valid.",
            UriParserSemanticException.MessageKeys.INVALID_KEY_VALUE, "");
      }
    }

    if (keys.size() < keyPropertyRefs.size() && partner != null) {
      // Fill missing keys from referential constraints.
      for (final String name : edmEntityType.getKeyPredicateNames()) {
        boolean found = false;
        for (final UriParameter key : keys) {
          if (name.equals(key.getName())) {
            found = true;
            break;
          }
        }
        if (!found && referencedNames.get(name) != null) {
          keys.add(0, new UriParameterImpl().setName(name).setReferencedProperty(referencedNames.get(name)));
        }
      }
    }

    // Check that all key predicates are filled from the URI.
    if (keys.size() < keyPropertyRefs.size()) {
      throw new UriParserSemanticException(
          "Expected " + keyPropertyRefs.size() + " key predicates but found " + keys.size() + ".",
          UriParserSemanticException.MessageKeys.WRONG_NUMBER_OF_KEY_PROPERTIES,
          Integer.toString(keyPropertyRefs.size()), Integer.toString(keys.size()));
    } else {
      return keys;
    }
  }

  private static UriParameter simpleKey(UriTokenizer tokenizer, final EdmKeyPropertyRef edmKeyPropertyRef,
      final Edm edm, final EdmType referringType, final Map<String, AliasQueryOption> aliases)
      throws UriParserException, UriValidationException {
    final EdmProperty edmProperty = edmKeyPropertyRef == null ? null : edmKeyPropertyRef.getProperty();
    if (nextPrimitiveTypeValue(tokenizer,
        edmProperty == null ? null : (EdmPrimitiveType) edmProperty.getType(),
        edmProperty == null ? false : edmProperty.isNullable())) {
      final String literalValue = tokenizer.getText();
      ParserHelper.requireNext(tokenizer, TokenKind.CLOSE);
      return createUriParameter(edmProperty, edmKeyPropertyRef.getName(), literalValue, edm, referringType, aliases);
    } else {
      return null;
    }
  }

  private static List<UriParameter> compoundKey(UriTokenizer tokenizer, final EdmEntityType edmEntityType,
      final Edm edm, final EdmType referringType, final Map<String, AliasQueryOption> aliases)
      throws UriParserException, UriValidationException {

    List<UriParameter> parameters = new ArrayList<>();
    List<String> parameterNames = new ArrayList<>();

    // To validate that each key predicate is exactly specified once, we use a list to pick from.
    List<String> remainingKeyNames = new ArrayList<String>(edmEntityType.getKeyPredicateNames());

    // At least one key predicate is mandatory.  Try to fetch all.
    boolean hasComma = false;
    do {
      final String keyPredicateName = tokenizer.getText();
      if (parameterNames.contains(keyPredicateName)) {
        throw new UriValidationException("Duplicated key property " + keyPredicateName,
            UriValidationException.MessageKeys.DOUBLE_KEY_PROPERTY, keyPredicateName);
      }
      if (remainingKeyNames.isEmpty()) {
        throw new UriParserSemanticException("Too many key properties.",
            UriParserSemanticException.MessageKeys.WRONG_NUMBER_OF_KEY_PROPERTIES,
            Integer.toString(parameters.size()), Integer.toString(parameters.size() + 1));
      }
      if (!remainingKeyNames.remove(keyPredicateName)) {
        throw new UriValidationException("Unknown key property " + keyPredicateName,
            UriValidationException.MessageKeys.INVALID_KEY_PROPERTY, keyPredicateName);
      }
      parameters.add(keyValuePair(tokenizer, keyPredicateName, edmEntityType, edm, referringType, aliases));
      parameterNames.add(keyPredicateName);
      hasComma = tokenizer.next(TokenKind.COMMA);
      if (hasComma) {
        ParserHelper.requireNext(tokenizer, TokenKind.ODataIdentifier);
      }
    } while (hasComma);
    ParserHelper.requireNext(tokenizer, TokenKind.CLOSE);

    return parameters;
  }

  private static UriParameter keyValuePair(UriTokenizer tokenizer,
      final String keyPredicateName, final EdmEntityType edmEntityType,
      final Edm edm, final EdmType referringType, final Map<String, AliasQueryOption> aliases)
      throws UriParserException, UriValidationException {
    final EdmKeyPropertyRef keyPropertyRef = edmEntityType.getKeyPropertyRef(keyPredicateName);
    final EdmProperty edmProperty = keyPropertyRef == null ? null : keyPropertyRef.getProperty();
    if (edmProperty == null) {
      throw new UriValidationException(keyPredicateName + " is not a valid key property name.",
          UriValidationException.MessageKeys.INVALID_KEY_PROPERTY, keyPredicateName);
    }
    ParserHelper.requireNext(tokenizer, TokenKind.EQ);
    if (tokenizer.next(TokenKind.COMMA) || tokenizer.next(TokenKind.CLOSE) || tokenizer.next(TokenKind.EOF)) {
      throw new UriParserSyntaxException("Key value expected.", UriParserSyntaxException.MessageKeys.SYNTAX);
    }
    if (nextPrimitiveTypeValue(tokenizer, (EdmPrimitiveType) edmProperty.getType(), edmProperty.isNullable())) {
      return createUriParameter(edmProperty, keyPredicateName, tokenizer.getText(), edm, referringType, aliases);
    } else {
      throw new UriParserSemanticException(keyPredicateName + " has not a valid  key value.",
          UriParserSemanticException.MessageKeys.INVALID_KEY_VALUE, keyPredicateName);
    }
  }

  private static UriParameter createUriParameter(final EdmProperty edmProperty, final String parameterName,
      final String literalValue, final Edm edm, final EdmType referringType,
      final Map<String, AliasQueryOption> aliases) throws UriParserException, UriValidationException {
    final AliasQueryOption alias = literalValue.startsWith("@") ?
        getKeyAlias(literalValue, edmProperty, edm, referringType, aliases) :
        null;
    final String value = alias == null ? literalValue : alias.getText();
    final EdmPrimitiveType primitiveType = (EdmPrimitiveType) edmProperty.getType();
    try {
      if (!(primitiveType.validate(primitiveType.fromUriLiteral(value), edmProperty.isNullable(),
          edmProperty.getMaxLength(), edmProperty.getPrecision(), edmProperty.getScale(), edmProperty.isUnicode()))) {
        throw new UriValidationException("Invalid key property",
            UriValidationException.MessageKeys.INVALID_KEY_PROPERTY, parameterName);
      }
    } catch (final EdmPrimitiveTypeException e) {
      throw new UriValidationException("Invalid key property", e,
          UriValidationException.MessageKeys.INVALID_KEY_PROPERTY, parameterName);
    }

    return new UriParameterImpl()
        .setName(parameterName)
        .setText("null".equals(literalValue) ? null : literalValue)
        .setAlias(alias == null ? null : literalValue)
        .setExpression(alias == null ? null :
            alias.getValue() == null ? new LiteralImpl(value, primitiveType) : alias.getValue());
  }

  private static AliasQueryOption getKeyAlias(final String name, final EdmProperty edmProperty,
      final Edm edm, final EdmType referringType, final Map<String, AliasQueryOption> aliases)
      throws UriParserException, UriValidationException {
    if (aliases.containsKey(name)) {
      return parseAliasValue(name,
          edmProperty.getType(), edmProperty.isNullable(), edmProperty.isCollection(),
          edm, referringType, aliases);
    } else {
      throw new UriValidationException("Alias '" + name + "' for key value not found.",
          UriValidationException.MessageKeys.MISSING_ALIAS, name);
    }
  }

  private static boolean nextPrimitiveTypeValue(UriTokenizer tokenizer,
      final EdmPrimitiveType primitiveType, final boolean nullable) {
    final EdmPrimitiveType type = primitiveType instanceof EdmTypeDefinition ?
        ((EdmTypeDefinition) primitiveType).getUnderlyingType() :
        primitiveType;
    if (tokenizer.next(TokenKind.ParameterAliasName)) {
      return true;
    } else if (nullable && tokenizer.next(TokenKind.NULL)) {
      return true;

    // Special handling for frequently-used types and types with more than one token kind.
    } else if (odata.createPrimitiveTypeInstance(EdmPrimitiveTypeKind.Boolean).equals(type)) {
      return tokenizer.next(TokenKind.BooleanValue);
    } else if (odata.createPrimitiveTypeInstance(EdmPrimitiveTypeKind.String).equals(type)) {
      return tokenizer.next(TokenKind.StringValue);
    } else if (odata.createPrimitiveTypeInstance(EdmPrimitiveTypeKind.SByte).equals(type)
        || odata.createPrimitiveTypeInstance(EdmPrimitiveTypeKind.Byte).equals(type)
        || odata.createPrimitiveTypeInstance(EdmPrimitiveTypeKind.Int16).equals(type)
        || odata.createPrimitiveTypeInstance(EdmPrimitiveTypeKind.Int32).equals(type)
        || odata.createPrimitiveTypeInstance(EdmPrimitiveTypeKind.Int64).equals(type)) {
      return tokenizer.next(TokenKind.IntegerValue);
    } else if (odata.createPrimitiveTypeInstance(EdmPrimitiveTypeKind.Guid).equals(type)) {
      return tokenizer.next(TokenKind.GuidValue);
    } else if (odata.createPrimitiveTypeInstance(EdmPrimitiveTypeKind.Decimal).equals(type)) {
      // The order is important.
      // A decimal value should not be parsed as integer and let the tokenizer stop at the decimal point.
      return tokenizer.next(TokenKind.DecimalValue)
          || tokenizer.next(TokenKind.IntegerValue);
    } else if (odata.createPrimitiveTypeInstance(EdmPrimitiveTypeKind.Double).equals(type)
        || odata.createPrimitiveTypeInstance(EdmPrimitiveTypeKind.Single).equals(type)) {
      // The order is important.
      // A floating-point value should not be parsed as decimal and let the tokenizer stop at 'E'.
      // A decimal value should not be parsed as integer and let the tokenizer stop at the decimal point.
      return tokenizer.next(TokenKind.DoubleValue)
          || tokenizer.next(TokenKind.DecimalValue)
          || tokenizer.next(TokenKind.IntegerValue);
    } else if (type.getKind() == EdmTypeKind.ENUM) {
      return tokenizer.next(TokenKind.EnumValue);
    } else {
      // Check the types that have not been checked already above.
      for (final Entry<TokenKind, EdmPrimitiveTypeKind> entry : tokenToPrimitiveType.entrySet()) {
        final EdmPrimitiveTypeKind kind = entry.getValue();
        if ((kind == EdmPrimitiveTypeKind.Date || kind == EdmPrimitiveTypeKind.DateTimeOffset
            || kind == EdmPrimitiveTypeKind.TimeOfDay || kind == EdmPrimitiveTypeKind.Duration
            || kind == EdmPrimitiveTypeKind.Binary
            || kind.isGeospatial())
            && odata.createPrimitiveTypeInstance(kind).equals(type)) {
          return tokenizer.next(entry.getKey());
        }
      }
      return false;
    }
  }

  protected static List<String> getParameterNames(final List<UriParameter> parameters) {
    List<String> names = new ArrayList<>();
    for (final UriParameter parameter : parameters) {
      names.add(parameter.getName());
    }
    return names;
  }

  protected static EdmStructuredType parseTypeCast(UriTokenizer tokenizer, final Edm edm,
      final EdmStructuredType referencedType) throws UriParserException {
    if (tokenizer.next(TokenKind.QualifiedName)) {
      final FullQualifiedName qualifiedName = new FullQualifiedName(tokenizer.getText());
      final EdmStructuredType type = referencedType.getKind() == EdmTypeKind.ENTITY ?
          edm.getEntityType(qualifiedName) :
          edm.getComplexType(qualifiedName);
      if (type == null) {
        throw new UriParserSemanticException("Type '" + qualifiedName + "' not found.",
            UriParserSemanticException.MessageKeys.UNKNOWN_PART, qualifiedName.getFullQualifiedNameAsString());
      } else {
        if (!type.compatibleTo(referencedType)) {
          throw new UriParserSemanticException("The type cast '" + qualifiedName + "' is not compatible.",
              UriParserSemanticException.MessageKeys.INCOMPATIBLE_TYPE_FILTER, type.getName());
        }
      }
      return type;
    }
    return null;
  }

  protected static EdmType getTypeInformation(final UriResourcePartTyped resourcePart) {
    EdmType type = null;
    if (resourcePart instanceof UriResourceWithKeysImpl) {
      final UriResourceWithKeysImpl lastPartWithKeys = (UriResourceWithKeysImpl) resourcePart;
      if (lastPartWithKeys.getTypeFilterOnEntry() != null) {
        type = lastPartWithKeys.getTypeFilterOnEntry();
      } else if (lastPartWithKeys.getTypeFilterOnCollection() != null) {
        type = lastPartWithKeys.getTypeFilterOnCollection();
      } else {
        type = lastPartWithKeys.getType();
      }

    } else if (resourcePart instanceof UriResourceTypedImpl) {
      final UriResourceTypedImpl lastPartTyped = (UriResourceTypedImpl) resourcePart;
      type = lastPartTyped.getTypeFilter() == null ?
          lastPartTyped.getType() :
          lastPartTyped.getTypeFilter();
    } else {
      type = resourcePart.getType();
    }

    return type;
  }

  protected static int parseNonNegativeInteger(final String optionName, final String optionValue,
      final boolean zeroAllowed) throws UriParserException {
    int value;
    try {
      value = Integer.parseInt(optionValue);
    } catch (final NumberFormatException e) {
      throw new UriParserSyntaxException("Illegal value of '" + optionName + "' option!", e,
          UriParserSyntaxException.MessageKeys.WRONG_VALUE_FOR_SYSTEM_QUERY_OPTION,
          optionName, optionValue);
    }
    if (value > 0 || value == 0 && zeroAllowed) {
      return value;
    } else {
      throw new UriParserSyntaxException("Illegal value of '" + optionName + "' option!",
          UriParserSyntaxException.MessageKeys.WRONG_VALUE_FOR_SYSTEM_QUERY_OPTION,
          optionName, optionValue);
    }
  }
  
  protected static void validateFunctionParameterFacets(final EdmFunction function, 
      final List<UriParameter> parameters, Edm edm, Map<String, AliasQueryOption> aliases) 
          throws UriParserException, UriValidationException {
    for (UriParameter parameter : parameters) {
      EdmParameter edmParameter = function.getParameter(parameter.getName());
      final EdmType type = edmParameter.getType();
      final EdmTypeKind kind = type.getKind();
      if ((kind == EdmTypeKind.PRIMITIVE || kind == EdmTypeKind.DEFINITION || kind == EdmTypeKind.ENUM)
          && !edmParameter.isCollection()) {
        final EdmPrimitiveType primitiveType = (EdmPrimitiveType) type;
        String text = null;
        try {
          text = parameter.getAlias() == null ?
              parameter.getText() :
                aliases.containsKey(parameter.getAlias()) ?
                    parseAliasValue(parameter.getAlias(),
                        edmParameter.getType(), edmParameter.isNullable(), edmParameter.isCollection(),
                        edm, type, aliases).getText() : null;
                        if (edmParameter.getMapping() == null) {
                          primitiveType.valueOfString(primitiveType.fromUriLiteral(text),
                              edmParameter.isNullable(), edmParameter.getMaxLength(), edmParameter.getPrecision(), 
                              edmParameter.getScale(), true, primitiveType.getDefaultType());
                        } else {
                          primitiveType.valueOfString(primitiveType.fromUriLiteral(text),
                              edmParameter.isNullable(), edmParameter.getMaxLength(), edmParameter.getPrecision(), 
                              edmParameter.getScale(), true, edmParameter.getMapping().getMappedJavaClass());
                        }
        } catch (final EdmPrimitiveTypeException e) {
          throw new UriValidationException(
              "Invalid value '" + text + "' for parameter " + parameter.getName(), e,
              UriValidationException.MessageKeys.INVALID_VALUE_FOR_PROPERTY, parameter.getName());
        }
      }
    }
  }
}
