gt:             if (!imageName.contains(".")) {
context: /**
   * Copyright (c) 2015 Bosch Software Innovations GmbH and others.
   *
   * All rights reserved. This program and the accompanying materials
   * are made available under the terms of the Eclipse Public License v1.0
   * which accompanies this distribution, and is available at
   * http://www.eclipse.org/legal/epl-v10.html
   */
  package org.eclipse.hawkbit.repository.jpa.rsql;
  
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collection;
  import java.util.Collections;
  import java.util.List;
  import java.util.Map;
  import java.util.Optional;
  import java.util.Set;
  import java.util.stream.Collectors;
  
  import org.eclipse.hawkbit.repository.TargetFields;
  import org.eclipse.hawkbit.repository.TargetManagement;
  import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException;
  import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException;
  import org.eclipse.hawkbit.repository.jpa.rsql.ParseExceptionWrapper.TokenWrapper;
  import org.eclipse.hawkbit.repository.rsql.RsqlValidationOracle;
  import org.eclipse.hawkbit.repository.rsql.SuggestToken;
  import org.eclipse.hawkbit.repository.rsql.SuggestionContext;
  import org.eclipse.hawkbit.repository.rsql.SyntaxErrorContext;
  import org.eclipse.hawkbit.repository.rsql.ValidationOracleContext;
  import org.eclipse.persistence.exceptions.ConversionException;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  import org.springframework.beans.factory.annotation.Autowired;
  import org.springframework.data.domain.PageRequest;
  import org.springframework.orm.jpa.JpaSystemException;
  import org.springframework.util.CollectionUtils;
  
  import com.google.common.collect.ArrayListMultimap;
  import com.google.common.collect.Multimap;
  
  import cz.jirutka.rsql.parser.ParseException;
  import cz.jirutka.rsql.parser.RSQLParserException;
  
  /**
   * An implementation of {@link RsqlValidationOracle} which retrieves the
   * exception using the {@link ParseException} to retrieve the suggestions.
   * 
   * The suggestion only works when there are syntax errors existing because the
   * information about current and next tokens in the RSQL syntax are from the
   * {@link ParseException}.
   * 
   * There is a feature request on the GitHub project
   * <a href="https://github.com/jirutka/rsql-parser/issues/22">https://github.com
   * /jirutka/rsql-parser/issues/22</a>
   * 
   */
  public class RsqlParserValidationOracle implements RsqlValidationOracle {
  
      private static final Logger LOGGER = LoggerFactory.getLogger(RsqlParserValidationOracle.class);
  
      @Autowired
      private TargetManagement targetManagement;
  
      @Override
      public ValidationOracleContext suggest(final String rsqlQuery, final int cursorPosition) {
  
          final List<SuggestToken> expectedTokens = new ArrayList<>();
          final ValidationOracleContext context = new ValidationOracleContext();
          context.setSyntaxError(true);
          final SuggestionContext suggestionContext = new SuggestionContext();
          context.setSuggestionContext(suggestionContext);
          final SyntaxErrorContext errorContext = new SyntaxErrorContext();
          context.setSyntaxErrorContext(errorContext);
  
          try {
              targetManagement.findByRsql(new PageRequest(0, 1), rsqlQuery);
              context.setSyntaxError(false);
              suggestionContext.getSuggestions().addAll(getLogicalOperatorSuggestion(rsqlQuery));
          } catch (final RSQLParameterSyntaxException | RSQLParserException ex) {
              setExceptionDetails(new Exception(ex.getCause().getCause()), expectedTokens);
              errorContext.setErrorMessage(getCustomMessage(ex.getCause().getMessage(), expectedTokens));
              suggestionContext.setSuggestions(expectedTokens);
              LOGGER.trace("Syntax exception on parsing :", ex);
          } catch (final RSQLParameterUnsupportedFieldException | IllegalArgumentException ex) {
              errorContext.setErrorMessage(getCustomMessage(ex.getMessage(), null));
              LOGGER.trace("Illegal argument on parsing :", ex);
          } catch (@SuppressWarnings("squid:S1166") final ConversionException | JpaSystemException e) {
              // noop
          }
          return context;
      }
  
      private static Collection<? extends SuggestToken> getLogicalOperatorSuggestion(final String rsqlQuery) {
          if (!rsqlQuery.endsWith(" ")) {
              return Collections.emptyList();
          }
          if (rsqlQuery.endsWith(" ")) {
              final int currentQueryLength = rsqlQuery.length();
              // only return and/or suggestion when there is a space at the end
              final Collection<String> tokenImages = TokenDescription.getTokenImage(TokenDescription.LOGICAL_OP);
              final List<SuggestToken> logicalOps = new ArrayList<>(tokenImages.size());
              for (final String tokenImage : tokenImages) {
                  logicalOps.add(new SuggestToken(currentQueryLength, currentQueryLength + tokenImage.length(), null,
                          tokenImage));
              }
              return logicalOps;
          }
          return Collections.emptyList();
      }
  
      private static void setExceptionDetails(final Exception ex, final List<SuggestToken> expectedTokens) {
          expectedTokens.addAll(getNextTokens(ex));
      }
  
      private static List<SuggestToken> getNextTokens(final Exception e) {
          final ParseException parseException = findParseException(e);
          if (parseException == null) {
              return Collections.emptyList();
          }
          final List<SuggestToken> listTokens = new ArrayList<>();
          final ParseExceptionWrapper parseExceptionWrapper = new ParseExceptionWrapper(parseException);
          final int[][] expectedTokenSequence = parseExceptionWrapper.getExpectedTokenSequence();
          final TokenWrapper currentToken = parseExceptionWrapper.getCurrentToken();
          if (currentToken == null) {
              return Collections.emptyList();
          }
          final TokenWrapper nextToken = currentToken.getNext();
          final int currentTokenKind = currentToken.getKind();
          final String currentTokenImageName = currentToken.getImage();
          final int nextTokenBeginColumn = nextToken.getBeginColumn();
          final int currentTokenEndColumn = currentToken.getEndColumn();
  
          // token == 5 is the field token, reverse engineering.
          if (currentTokenKind == 5) {
              final Optional<List<SuggestToken>> handleFieldTokenSuggestion = handleFieldTokenSuggestion(
                      currentTokenImageName, nextTokenBeginColumn, currentTokenEndColumn);
              if (handleFieldTokenSuggestion.isPresent()) {
                  return handleFieldTokenSuggestion.get();
              }
          }
  
          for (final int[] is : expectedTokenSequence) {
              addSuggestionOnTokenImage(listTokens, nextTokenBeginColumn, currentTokenEndColumn, is);
          }
          return listTokens;
      }
  
      private static void addSuggestionOnTokenImage(final List<SuggestToken> listTokens, final int nextTokenBeginColumn,
              final int currentTokenEndColumn, final int[] is) {
          for (final int i : is) {
              final Collection<String> tokenImage = TokenDescription.getTokenImage(i);
              if (!CollectionUtils.isEmpty(tokenImage)) {
                  tokenImage.forEach(image -> listTokens.add(new SuggestToken(currentTokenEndColumn + 1,
                          nextTokenBeginColumn + image.length(), null, image)));
              }
          }
      }
  
      private static Optional<List<SuggestToken>> handleFieldTokenSuggestion(final String currentTokenImageName,
              final int nextTokenBeginColumn, final int currentTokenEndColumn) {
          final boolean containsDot = currentTokenImageName.indexOf('.') != -1;
          if (shouldSuggestTopLevelFieldNames(currentTokenImageName, containsDot)) {
              return Optional
                      .of(FieldNameDescription.toTopSuggestToken(nextTokenBeginColumn - currentTokenImageName.length(),
                              nextTokenBeginColumn + currentTokenImageName.length(), currentTokenImageName));
          } else if (shouldSuggestDotToken(currentTokenImageName, containsDot)) {
              return Optional
                      .of(Arrays.asList(new SuggestToken(currentTokenEndColumn, nextTokenBeginColumn + 1, null, ".")));
          } else if (shouldSuggestSubTokenFieldNames(currentTokenImageName, containsDot)) {
              return handleSubtokenSuggestion(currentTokenImageName, nextTokenBeginColumn);
          }
          return Optional.empty();
      }
  
      private static boolean shouldSuggestSubTokenFieldNames(final String currentTokenImageName,
              final boolean containsDot) {
          return containsDot && !FieldNameDescription.containsValue(currentTokenImageName);
      }
  
      private static boolean shouldSuggestDotToken(final String currentTokenImageName, final boolean containsDot) {
          return !containsDot && FieldNameDescription.hasSubEntries(currentTokenImageName);
      }
  
      private static boolean shouldSuggestTopLevelFieldNames(final String currentTokenImageName,
              final boolean containsDot) {
          return !containsDot && !FieldNameDescription.containsValue(currentTokenImageName)
                  && !FieldNameDescription.hasSubEntries(currentTokenImageName);
      }
  
      private static Optional<List<SuggestToken>> handleSubtokenSuggestion(final String currentTokenImageName,
              final int nextTokenBeginColumn) {
          final String[] split = currentTokenImageName.split("\\.");
          for (final String string : split) {
              if (FieldNameDescription.containsValue(string)) {
                  final String subTokenImage = split.length > 1 ? split[1] : null;
                  final int subTokenBegin = nextTokenBeginColumn + currentTokenImageName.indexOf('.') + 1;
                  return Optional.of(FieldNameDescription.toSubSuggestToken(subTokenBegin, subTokenBegin + 1, string,
                          subTokenImage));
              }
          }
          return Optional.empty();
      }
  
      private static ParseException findParseException(final Throwable e) {
          if (e instanceof ParseException) {
              return (ParseException) e;
          } else if (e.getCause() != null) {
              return findParseException(e.getCause());
          }
          return null;
      }
  
      private static String getCustomMessage(final String message, final List<SuggestToken> expectedTokens) {
          String builder = message;
  
          if (!message.contains(":")) {
              return builder;
          }
  
          builder = message.substring(message.indexOf(':') + 1, message.length());
          if (builder.indexOf("Was expecting") != -1) {
              builder = builder.substring(0, builder.lastIndexOf("Was expecting"));
          }
  
          if (!CollectionUtils.isEmpty(expectedTokens)) {
              final StringBuilder tokens = new StringBuilder();
              expectedTokens.stream().forEach(value -> tokens.append(value.getSuggestion() + ","));
              builder = builder.concat("Was expecting :" + tokens.toString().substring(0, tokens.length() - 1));
          }
          builder = builder.replace('\r', ' ');
          builder = builder.replace('\n', ' ');
          builder = builder.replaceAll(">", " ");
          builder = builder.replaceAll("<", " ");
  
          return builder;
      }
  
      // Token map with logical and comparator operator that are used for context
      // sensitive help on search query.
      private static final class TokenDescription {
  
          private static final Multimap<Integer, String> TOKEN_MAP = ArrayListMultimap.create();
  
          private static final int LOGICAL_OP = 8;
          private static final int COMPARATOR = 12;
  
          static {
              TOKEN_MAP.put(LOGICAL_OP, "and");
              TOKEN_MAP.put(LOGICAL_OP, "or");
              TOKEN_MAP.put(COMPARATOR, "==");
              TOKEN_MAP.put(COMPARATOR, "!=");
              TOKEN_MAP.put(COMPARATOR, "=ge=");
              TOKEN_MAP.put(COMPARATOR, "=le=");
              TOKEN_MAP.put(COMPARATOR, "=gt=");
              TOKEN_MAP.put(COMPARATOR, "=lt=");
              TOKEN_MAP.put(COMPARATOR, "=in=");
              TOKEN_MAP.put(COMPARATOR, "=out=");
          }
  
          private TokenDescription() {
  
          }
  
          private static Collection<String> getTokenImage(final int tokenIndex) {
              return TOKEN_MAP.get(tokenIndex);
          }
  
      }
  
      private static final class FieldNameDescription {
  
          private static final Set<String> FIELD_NAMES = Arrays.stream(TargetFields.values())
                  .map(field -> field.toString().toLowerCase()).collect(Collectors.toSet());
  
          private static final Map<String, List<String>> SUB_NAMES = Arrays.stream(TargetFields.values()).collect(
                  Collectors.toMap(field -> field.toString().toLowerCase(), TargetFields::getSubEntityAttributes));
  
          private FieldNameDescription() {
  
          }
  
          private static boolean hasSubEntries(final String tokenImageName) {
              String tmpTokenName = tokenImageName;
              if (tokenImageName.contains(".")) {
                  final String[] split = tokenImageName.split("\\.");
                  if (split.length <= 0) {
                      return false;
                  }
                  tmpTokenName = split[0];
              }
              final String finalTmpTokenName = tmpTokenName;
              return Arrays.stream(TargetFields.values())
                      .filter(field -> field.toString().equalsIgnoreCase(finalTmpTokenName))
                      .map(TargetFields::getSubEntityAttributes).flatMap(List::stream).count() > 0;
          }
  
          private static List<SuggestToken> toTopSuggestToken(final int beginToken, final int endToken,
                  final String tokenImageName) {
              return FIELD_NAMES.stream()
                      .map(field -> new SuggestToken(beginToken, endToken, tokenImageName, field.toLowerCase()))
                      .collect(Collectors.toList());
          }
  
          private static List<SuggestToken> toSubSuggestToken(final int beginToken, final int endToken,
                  final String topToken, final String tokenImageName) {
              return Arrays.stream(TargetFields.values()).filter(field -> field.toString().equalsIgnoreCase(topToken))
                      .map(TargetFields::getSubEntityAttributes).flatMap(List::stream)
                      .map(subentity -> new SuggestToken(beginToken, endToken, tokenImageName, subentity))
                      .collect(Collectors.toList());
          }
  
          private static boolean containsValue(final String imageName) {
  