From 79b1ae8728db34a023565d35f3201579fc19f388 Mon Sep 17 00:00:00 2001 From: Michael Hirsch Date: Fri, 26 Aug 2016 13:26:37 +0200 Subject: [PATCH 01/11] introduce autocomplete textfield with RSQL suggestion Signed-off-by: Michael Hirsch --- .../repository/rsql/RsqlValidationOracle.java | 34 ++ .../hawkbit/repository/rsql/SuggestToken.java | 66 ++++ .../repository/rsql/SuggestionContext.java | 69 ++++ .../repository/rsql/SyntaxErrorContext.java | 56 ++++ .../rsql/ValidationOracleContext.java | 46 +++ .../RepositoryApplicationConfiguration.java | 8 + .../jpa/rsql/ParseExceptionWrapper.java | 187 +++++++++++ .../jpa/rsql/RsqlParserValidationOracle.java | 301 ++++++++++++++++++ .../rsql/RsqlParserValidationOracleTest.java | 81 +++++ .../eclipse/hawkbit/ui/AppWidgetSet.gwt.xml | 43 +-- .../AutoCompleteTextFieldComponent.java | 255 +++++++++++++++ .../CreateOrUpdateFilterHeader.java | 162 ++-------- .../FilterManagementView.java | 3 +- .../FilterQueryValidation.java | 182 ----------- .../filtermanagement/TargetFilterHeader.java | 1 + .../filtermanagement/TargetFilterTable.java | 1 - .../TextFieldSuggestionBox.gwt.xml | 17 + .../TextFieldSuggestionBox.java | 83 +++++ .../ui/filtermanagement/TokenDescription.java | 30 -- .../ui/filtermanagement/ValidationResult.java | 73 ----- .../AutoCompleteTextFieldConnector.java | 137 ++++++++ .../client/SuggestTokenDto.java | 68 ++++ .../client/SuggestionContextDto.java | 63 ++++ .../client/SuggestionsSelectList.java | 116 +++++++ .../TextFieldSuggestionBoxClientRpc.java | 32 ++ .../TextFieldSuggestionBoxServerRpc.java | 34 ++ .../customstyles/target-filter-query.scss | 47 ++- pom.xml | 2 +- 28 files changed, 1735 insertions(+), 462 deletions(-) create mode 100644 hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/RsqlValidationOracle.java create mode 100644 hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/SuggestToken.java create mode 100644 hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/SuggestionContext.java create mode 100644 hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/SyntaxErrorContext.java create mode 100644 hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/ValidationOracleContext.java create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/ParseExceptionWrapper.java create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RsqlParserValidationOracle.java create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RsqlParserValidationOracleTest.java create mode 100644 hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/AutoCompleteTextFieldComponent.java delete mode 100644 hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/FilterQueryValidation.java create mode 100644 hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/TextFieldSuggestionBox.gwt.xml create mode 100644 hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/TextFieldSuggestionBox.java delete mode 100644 hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/TokenDescription.java delete mode 100644 hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/ValidationResult.java create mode 100644 hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/client/AutoCompleteTextFieldConnector.java create mode 100644 hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/client/SuggestTokenDto.java create mode 100644 hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/client/SuggestionContextDto.java create mode 100644 hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/client/SuggestionsSelectList.java create mode 100644 hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/client/TextFieldSuggestionBoxClientRpc.java create mode 100644 hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/client/TextFieldSuggestionBoxServerRpc.java diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/RsqlValidationOracle.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/RsqlValidationOracle.java new file mode 100644 index 000000000..bb4b16817 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/RsqlValidationOracle.java @@ -0,0 +1,34 @@ +/** + * 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.rsql; + +/** + * An interface declaration which validates an RSQL based query syntax and + * allows providing suggestions e.g. in case of syntax errors or current cursor + * position. + */ +@FunctionalInterface +public interface RsqlValidationOracle { + + /** + * Parses and validates an given RSQL based query syntax and provides + * suggestion based on syntax error and cursor positioning. + * + * @param rsqlQuery + * an RSQL based query string to parse. + * @param cursorPosition + * the position of the cursor to retrieve suggestions at the + * position. {@code -1} indicates for no cursor suggestion + * @return a validation oracle context providing information about syntax + * errors and possible suggestions for fixing the syntax error or at + * the cursor position to replace tokens + */ + ValidationOracleContext suggest(final String rsqlQuery, final int cursorPosition); + +} diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/SuggestToken.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/SuggestToken.java new file mode 100644 index 000000000..55f009adf --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/SuggestToken.java @@ -0,0 +1,66 @@ +/** + * 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.rsql; + +/** + * A suggestion which contains the start and the end character position of the + * suggested token of the suggestion of the token and the actual suggestion. + */ +public class SuggestToken { + + private final int start; + private final int end; + private final String suggestion; + private final String tokenImageName; + + /** + * Constructor. + * + * @param start + * the character position of the start of the token + * @param end + * the character position of the end of the token + * @param the + * token image name which is currently parsed, e.g. entered image + * name is {@code na} which the suggestion can be based on and + * e.g. filter the suggestions. + * @param tokenImageName + * the entered name of the token, e.g. could be the beginning of + * the suggestion like 'na' or 'name' + * @param suggestion + * the token suggestion + */ + public SuggestToken(final int start, final int end, final String tokenImageName, final String suggestion) { + this.start = start; + this.end = end; + this.tokenImageName = tokenImageName; + this.suggestion = suggestion; + } + + public int getStart() { + return start; + } + + public int getEnd() { + return end; + } + + public String getSuggestion() { + return suggestion; + } + + public String getTokenImageName() { + return tokenImageName; + } + + @Override + public String toString() { + return "SuggestToken [start=" + start + ", end=" + end + ", suggestion=" + suggestion + "]"; + } +} diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/SuggestionContext.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/SuggestionContext.java new file mode 100644 index 000000000..2dd6531a6 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/SuggestionContext.java @@ -0,0 +1,69 @@ +/** + * 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.rsql; + +import java.util.ArrayList; +import java.util.List; + +/** + * The context which holds suggestions for the current cursor position. + */ +public class SuggestionContext { + + private String rsqlQuery; + private int cursorPosition; + private List suggestions = new ArrayList<>(); + + /** + * Default constructor. + */ + public SuggestionContext() { + // nothing to initialize + } + + /** + * Constructor. + * + * @param rsqlQuery + * the original RSQL based query the suggestions based on + * @param cursorPosition + * the current cursor position + * @param suggestions + * the suggestions for the current cursor position + */ + public SuggestionContext(final String rsqlQuery, final int cursorPosition, final List suggestions) { + this.rsqlQuery = rsqlQuery; + this.cursorPosition = cursorPosition; + this.suggestions = suggestions; + } + + public List getSuggestions() { + return suggestions; + } + + public int getCursorPosition() { + return cursorPosition; + } + + public String getRsqlQuery() { + return rsqlQuery; + } + + public void setRsqlQuery(final String rsqlQuery) { + this.rsqlQuery = rsqlQuery; + } + + public void setCursorPosition(final int cursorPosition) { + this.cursorPosition = cursorPosition; + } + + public void setSuggestions(final List suggestions) { + this.suggestions = suggestions; + } +} diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/SyntaxErrorContext.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/SyntaxErrorContext.java new file mode 100644 index 000000000..107ff755a --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/SyntaxErrorContext.java @@ -0,0 +1,56 @@ +/** + * 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.rsql; + +/** + * An syntax error context object which holds the character position of the + * syntax error and message. + */ +public class SyntaxErrorContext { + + private int characterPosition = -1; + private String errorMessage; + + /** + * Default constructor. + */ + public SyntaxErrorContext() { + // nothing to initialize + } + + /** + * Constructor. + * + * @param characterPosition + * the position of the character within the RSQL query string the + * error occurs. + * @param errorMessage + * the error message with further information + */ + public SyntaxErrorContext(final int characterPosition, final String errorMessage) { + this.characterPosition = characterPosition; + this.errorMessage = errorMessage; + } + + public int getCharacterPosition() { + return characterPosition; + } + + public void setCharacterPosition(final int characterPosition) { + this.characterPosition = characterPosition; + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(final String errorMessage) { + this.errorMessage = errorMessage; + } +} diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/ValidationOracleContext.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/ValidationOracleContext.java new file mode 100644 index 000000000..a6a71cf35 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/ValidationOracleContext.java @@ -0,0 +1,46 @@ +/** + * 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.rsql; + +/** + * A context object which contains information about validation and suggestions + * of a parsed RSQL query. + */ +public class ValidationOracleContext { + + private boolean syntaxError; + + private SuggestionContext suggestionContext; + + private SyntaxErrorContext syntaxErrorContext; + + public boolean isSyntaxError() { + return syntaxError; + } + + public SuggestionContext getSuggestionContext() { + return suggestionContext; + } + + public SyntaxErrorContext getSyntaxErrorContext() { + return syntaxErrorContext; + } + + public void setSyntaxError(final boolean syntaxError) { + this.syntaxError = syntaxError; + } + + public void setSuggestionContext(final SuggestionContext suggestionContext) { + this.suggestionContext = suggestionContext; + } + + public void setSyntaxErrorContext(final SyntaxErrorContext syntaxErrorContext) { + this.syntaxErrorContext = syntaxErrorContext; + } +} diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/RepositoryApplicationConfiguration.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/RepositoryApplicationConfiguration.java index a62de9a06..c38612095 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/RepositoryApplicationConfiguration.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/RepositoryApplicationConfiguration.java @@ -52,6 +52,8 @@ import org.eclipse.hawkbit.repository.jpa.model.helper.SystemManagementHolder; import org.eclipse.hawkbit.repository.jpa.model.helper.SystemSecurityContextHolder; import org.eclipse.hawkbit.repository.jpa.model.helper.TenantAwareHolder; import org.eclipse.hawkbit.repository.jpa.model.helper.TenantConfigurationManagementHolder; +import org.eclipse.hawkbit.repository.jpa.rsql.RsqlParserValidationOracle; +import org.eclipse.hawkbit.repository.rsql.RsqlValidationOracle; import org.eclipse.hawkbit.security.SecurityTokenGenerator; import org.eclipse.hawkbit.security.SystemSecurityContext; import org.eclipse.hawkbit.tenancy.TenantAware; @@ -92,6 +94,12 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { @Autowired private EventBus eventBus; + @Bean + @ConditionalOnMissingBean + public RsqlValidationOracle rsqlValidationOracle() { + return new RsqlParserValidationOracle(); + } + /** * @return the {@link SystemSecurityContext} singleton bean which make it * accessible in beans which cannot access the service directly, diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/ParseExceptionWrapper.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/ParseExceptionWrapper.java new file mode 100644 index 000000000..08978eaaa --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/ParseExceptionWrapper.java @@ -0,0 +1,187 @@ +/** + * 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.lang.reflect.Field; +import java.util.Arrays; + +import com.google.common.base.Throwables; + +import cz.jirutka.rsql.parser.ParseException; + +/** + * A {@link ParseException} wrapper which allows to access the parsing + * information from the exception using reflection due there is no other access + * of this information. See issue for requesting feature + * {@link https://github.com/jirutka/rsql-parser/issues/22} + */ +public class ParseExceptionWrapper { + + private static final String FIELD_EXPECTED_TOKEN_SEQ = "expectedTokenSequences"; + private static final String FIELD_CURRENT_TOKEN = "currentToken"; + + private final ParseException parseException; + private final Class parseExceptionClass; + private Field expectedTokenSequenceField; + private Field currentTokenField; + + /** + * Constructor. + * + * @param parseException + * the original parsing exception object to access its field + * using reflection + */ + public ParseExceptionWrapper(final ParseException parseException) { + this.parseException = parseException; + parseExceptionClass = parseException.getClass(); + + try { + expectedTokenSequenceField = getAccessibleField(parseExceptionClass, FIELD_EXPECTED_TOKEN_SEQ); + } catch (@SuppressWarnings("squid:S1166") final NoSuchFieldException e) { + expectedTokenSequenceField = null; + } + + try { + currentTokenField = getAccessibleField(parseExceptionClass, FIELD_CURRENT_TOKEN); + } catch (@SuppressWarnings("squid:S1166") final NoSuchFieldException e) { + currentTokenField = null; + } + } + + public int[][] getExpectedTokenSequence() { + if (expectedTokenSequenceField == null) { + return new int[0][0]; + } + return (int[][]) getValue(expectedTokenSequenceField, parseException); + } + + public TokenWrapper getCurrentToken() { + if (currentTokenField == null) { + return null; + } + return new TokenWrapper(getValue(currentTokenField, parseException)); + } + + @Override + public String toString() { + return "ParseExceptionWrapper [getExpectedTokenSequence()=" + Arrays.toString(getExpectedTokenSequence()) + + ", getCurrentToken()=" + getCurrentToken() + "]"; + } + + private static Field getAccessibleField(final Class clazz, final String field) throws NoSuchFieldException { + final Field declaredField = clazz.getDeclaredField(field); + declaredField.setAccessible(true); + return declaredField; + } + + private static Object getValue(final Field field, final Object instance) { + try { + return field.get(instance); + } catch (IllegalArgumentException | IllegalAccessException e) { + throw Throwables.propagate(e); + } + } + + /** + * A {@link TokenWrapper} which wraps the + * {@code cz.jirutka.rsql.parser.Token} class of the {@link ParseException} + * which otherwise is not accessible. + */ + public static final class TokenWrapper { + + private static final String FIELD_NEXT = "next"; + private static final String FIELD_KIND = "kind"; + private static final String FIELD_IMAGE = "image"; + private static final String FIELD_BEGIN_COL = "beginColumn"; + private static final String FIELD_END_COL = "endColumn"; + + private final Object tokenInstance; + + private Field nextTokenField; + private Field kindTokenField; + private Field imageTokenField; + private Field beginColumnTokenField; + private Field endColumnTokenField; + + private TokenWrapper(final Object tokenField) { + this.tokenInstance = tokenField; + + try { + nextTokenField = getAccessibleField(tokenField.getClass(), FIELD_NEXT); + } catch (@SuppressWarnings("squid:S1166") final NoSuchFieldException e) { + nextTokenField = null; + } + try { + kindTokenField = getAccessibleField(tokenField.getClass(), FIELD_KIND); + } catch (@SuppressWarnings("squid:S1166") final NoSuchFieldException e) { + kindTokenField = null; + } + + try { + imageTokenField = getAccessibleField(tokenField.getClass(), FIELD_IMAGE); + } catch (@SuppressWarnings("squid:S1166") final NoSuchFieldException e) { + imageTokenField = null; + } + + try { + beginColumnTokenField = getAccessibleField(tokenField.getClass(), FIELD_BEGIN_COL); + } catch (@SuppressWarnings("squid:S1166") final NoSuchFieldException e) { + beginColumnTokenField = null; + } + + try { + endColumnTokenField = getAccessibleField(tokenField.getClass(), FIELD_END_COL); + } catch (@SuppressWarnings("squid:S1166") final NoSuchFieldException e) { + endColumnTokenField = null; + } + } + + public TokenWrapper getNext() { + final Object nextToken = getValue(nextTokenField, tokenInstance); + return nextToken != null ? new TokenWrapper(nextToken) : null; + + } + + public int getKind() { + if (kindTokenField == null) { + return 0; + } + return (int) getValue(kindTokenField, tokenInstance); + } + + public String getImage() { + if (imageTokenField == null) { + return null; + } + return (String) getValue(imageTokenField, tokenInstance); + } + + public int getBeginColumn() { + if (beginColumnTokenField == null) { + return 0; + } + return (int) getValue(beginColumnTokenField, tokenInstance); + } + + public int getEndColumn() { + if (endColumnTokenField == null) { + return 0; + } + return (int) getValue(endColumnTokenField, tokenInstance); + } + + @Override + public String toString() { + return "TokenWrapper [tokenInstance=" + tokenInstance + ", getNext()=" + getNext() + ", getKind()=" + + getKind() + ", getImage()=" + getImage() + ", getBeginColumn()=" + getBeginColumn() + + ", getEndColumn()=" + getEndColumn() + "]"; + } + } +} diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RsqlParserValidationOracle.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RsqlParserValidationOracle.java new file mode 100644 index 000000000..8eb21400f --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RsqlParserValidationOracle.java @@ -0,0 +1,301 @@ +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 com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Lists; +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 + * {@link https://github.com/jirutka/rsql-parser/issues/22}. + */ +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 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.findTargetsAll(rsqlQuery, new PageRequest(0, 1)); + 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 getLogicalOperatorSuggestion(final String rsqlQuery) { + final int currentQueryLength = rsqlQuery.length(); + // only return and/or suggestion when there is a space at the end + if (rsqlQuery.endsWith(" ")) { + final Collection tokenImages = TokenDescription.getTokenImage(TokenDescription.LOGICAL_OP); + final List 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 expectedTokens) { + expectedTokens.addAll(getNextTokens(ex)); + } + + private static List getNextTokens(final Exception e) { + final ParseException parseException = findParseException(e); + if (parseException != null) { + final List listTokens = new ArrayList<>(); + final ParseExceptionWrapper parseExceptionWrapper = new ParseExceptionWrapper(parseException); + final int[][] expectedTokenSequence = parseExceptionWrapper.getExpectedTokenSequence(); + final TokenWrapper currentToken = parseExceptionWrapper.getCurrentToken(); + 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> handleFieldTokenSuggestion = handleFieldTokenSuggestion( + currentTokenImageName, nextTokenBeginColumn, currentTokenEndColumn); + if (handleFieldTokenSuggestion.isPresent()) { + return handleFieldTokenSuggestion.get(); + } + } + + for (final int[] is : expectedTokenSequence) { + for (final int i : is) { + final Collection tokenImage = TokenDescription.getTokenImage(i); + if (tokenImage != null && !tokenImage.isEmpty()) { + tokenImage.forEach(image -> listTokens.add(new SuggestToken(currentTokenEndColumn + 1, + nextTokenBeginColumn + image.length(), null, image))); + } + } + } + return listTokens; + } + return Collections.emptyList(); + + } + + private static Optional> 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( + Lists.newArrayList(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> 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 != null && e instanceof ParseException) { + return (ParseException) e; + } else if (e != null && e.getCause() != null) { + return findParseException(e.getCause()); + } + return null; + } + + private static String getCustomMessage(final String message, final List expectedTokens) { + String builder = message; + if (message.contains(":")) { + builder = message.substring(message.indexOf(':') + 1, message.length()); + if (builder.indexOf("Was expecting") != -1) { + builder = builder.substring(0, builder.lastIndexOf("Was expecting")); + } + if (null != expectedTokens && !expectedTokens.isEmpty()) { + 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; + } + + private static final class TokenDescription { + + private static final Multimap 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 getTokenImage(final int tokenIndex) { + return TOKEN_MAP.get(tokenIndex); + } + + } + + private static final class FieldNameDescription { + + private static final Set FIELD_NAMES = Arrays.stream(TargetFields.values()) + .map(field -> field.toString().toLowerCase()).collect(Collectors.toSet()); + + private static final Map> SUB_NAMES = Arrays.stream(TargetFields.values()).collect( + Collectors.toMap(field -> field.toString().toLowerCase(), field -> field.getSubEntityAttributes())); + + private FieldNameDescription() { + + } + + private static boolean hasSubEntries(final String tokenImageName) { + final String tmpTokenName; + if (tokenImageName.contains(".")) { + final String[] split = tokenImageName.split("\\."); + if (split.length > 0) { + tmpTokenName = split[0]; + } else { + return false; + } + } else { + tmpTokenName = tokenImageName; + } + return Arrays.stream(TargetFields.values()).filter(field -> field.toString().equalsIgnoreCase(tmpTokenName)) + .map(field -> field.getSubEntityAttributes()).flatMap(subentities -> subentities.stream()) + .count() > 0; + } + + private static List 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 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(field -> field.getSubEntityAttributes()).flatMap(list -> list.stream()) + .map(subentity -> new SuggestToken(beginToken, endToken, tokenImageName, subentity)) + .collect(Collectors.toList()); + } + + private static boolean containsValue(final String imageName) { + if (imageName.contains(".")) { + final String[] split = imageName.split("\\."); + if (split.length > 1 && FIELD_NAMES.contains(split[0].toLowerCase())) { + return SUB_NAMES.get(split[0].toLowerCase()).stream() + .filter(subname -> new String(split[0] + "." + subname).equalsIgnoreCase(imageName)) + .count() > 0; + } + } + return FIELD_NAMES.stream().filter(value -> value.equalsIgnoreCase(imageName)).count() > 0; + } + } + +} diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RsqlParserValidationOracleTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RsqlParserValidationOracleTest.java new file mode 100644 index 000000000..c1b43eecd --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RsqlParserValidationOracleTest.java @@ -0,0 +1,81 @@ +/** + * 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 static org.fest.assertions.api.Assertions.assertThat; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.eclipse.hawkbit.repository.TargetFields; +import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest; +import org.eclipse.hawkbit.repository.rsql.RsqlValidationOracle; +import org.eclipse.hawkbit.repository.rsql.ValidationOracleContext; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import ru.yandex.qatools.allure.annotations.Description; +import ru.yandex.qatools.allure.annotations.Features; +import ru.yandex.qatools.allure.annotations.Stories; + +@Features("Component Tests - Repository") +@Stories("RSQL filter suggestion") +public class RsqlParserValidationOracleTest extends AbstractJpaIntegrationTest { + + @Autowired + private RsqlValidationOracle rsqlValidationOracle; + + private static final String[] OP_SUGGESTIONS = new String[] { "==", "!=", "=ge=", "=le=", "=gt=", "=lt=", "=in=", + "=out=" }; + private static final String[] FIELD_SUGGESTIONS = Arrays.stream(TargetFields.values()) + .map(field -> field.name().toLowerCase()).toArray(size -> new String[size]); + private static final String[] AND_OR_SUGGESTIONS = new String[] { "and", "or" }; + private static final String[] NAME_VERSION_SUGGESTIONS = new String[] { "name", "version" }; + + @Test + @Description("Verifies that suggestions contains all possible field names") + public void suggestionContainsAllFieldNames() { + final String rsqlQuery = "na"; + final List currentSuggestions = getSuggestions(rsqlQuery); + assertThat(currentSuggestions).containsOnly(FIELD_SUGGESTIONS); + } + + @Test + @Description("Verifies that suggestions only contains the allowed operators") + public void suggestionContainsOnlyOperators() { + final String rsqlQuery = "name"; + final List currentSuggestions = getSuggestions(rsqlQuery); + assertThat(currentSuggestions).containsOnly(OP_SUGGESTIONS); + } + + @Test + @Description("Verifies that suggestions only contains operator to combine RSQL filters (and, or)") + public void suggestionContainsOnlyAndOrOperator() { + final String rsqlQuery = "name==a "; + final List currentSuggestions = getSuggestions(rsqlQuery); + assertThat(currentSuggestions).containsOnly(AND_OR_SUGGESTIONS); + } + + @Test + @Description("Verifies that sub suggestions are shown") + public void suggestionContainsSubFieldSuggestions() { + final String rsqlQuery = "assignedds."; + final List currentSuggestions = getSuggestions(rsqlQuery); + assertThat(currentSuggestions).containsOnly(NAME_VERSION_SUGGESTIONS); + } + + private List getSuggestions(final String rsqlQuery) { + final ValidationOracleContext suggest = rsqlValidationOracle.suggest(rsqlQuery, -1); + final List currentSuggestions = suggest.getSuggestionContext().getSuggestions().stream() + .map(suggestion -> suggestion.getSuggestion()).collect(Collectors.toList()); + return currentSuggestions; + } + +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/AppWidgetSet.gwt.xml b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/AppWidgetSet.gwt.xml index c6ee9b5b5..2141eb6b1 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/AppWidgetSet.gwt.xml +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/AppWidgetSet.gwt.xml @@ -1,44 +1,33 @@ - + - + - - + + - + - + - + - + - + - + - + + - + + - + diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/AutoCompleteTextFieldComponent.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/AutoCompleteTextFieldComponent.java new file mode 100644 index 000000000..e3d766391 --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/AutoCompleteTextFieldComponent.java @@ -0,0 +1,255 @@ +/** + * 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.ui.filtermanagement; + +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.Executor; + +import javax.annotation.PostConstruct; + +import org.eclipse.hawkbit.repository.rsql.RsqlValidationOracle; +import org.eclipse.hawkbit.ui.common.builder.TextFieldBuilder; +import org.eclipse.hawkbit.ui.filtermanagement.event.CustomFilterUIEvent; +import org.eclipse.hawkbit.ui.filtermanagement.state.FilterManagementUIState; +import org.eclipse.hawkbit.ui.utils.SPUIComponentIdProvider; +import org.eclipse.hawkbit.ui.utils.SPUILabelDefinitions; +import org.eclipse.hawkbit.ui.utils.SPUIStyleDefinitions; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.vaadin.spring.events.EventBus; + +import com.vaadin.event.ShortcutListener; +import com.vaadin.server.FontAwesome; +import com.vaadin.shared.ui.label.ContentMode; +import com.vaadin.spring.annotation.SpringComponent; +import com.vaadin.spring.annotation.ViewScope; +import com.vaadin.ui.AbstractTextField.TextChangeEventMode; +import com.vaadin.ui.Alignment; +import com.vaadin.ui.HorizontalLayout; +import com.vaadin.ui.Label; +import com.vaadin.ui.TextField; +import com.vaadin.ui.UI; + +/** + * An textfield with the {@link TextFieldSuggestionBox} extension which shows + * suggestions in a suggestion-pop-up window while typing. + */ +@SpringComponent +@ViewScope +public class AutoCompleteTextFieldComponent extends HorizontalLayout { + + private static final long serialVersionUID = 1L; + + @Autowired + private FilterManagementUIState filterManagementUIState; + + @Autowired + private transient EventBus.SessionEventBus eventBus; + + @Autowired + private transient RsqlValidationOracle rsqlValidationOracle; + + @Autowired + @Qualifier("uiExecutor") + private transient Executor executor; + + private transient List listeners = new LinkedList<>(); + + private final Label validationIcon; + private final TextField queryTextField; + + /** + * Constructor. + */ + public AutoCompleteTextFieldComponent() { + + queryTextField = createSearchField(); + validationIcon = createStatusIcon(); + + setSizeUndefined(); + setSpacing(true); + addStyleName("custom-search-layout"); + addComponents(validationIcon, queryTextField); + setComponentAlignment(validationIcon, Alignment.TOP_CENTER); + } + + /** + * Called by the spring-framework when this bean has be post-constructed. + */ + @PostConstruct + public void postConstruct() { + new TextFieldSuggestionBox(rsqlValidationOracle, this).extend(queryTextField); + } + + /** + * Clears the textfield and resets the validation icon. + */ + public void clear() { + queryTextField.clear(); + validationIcon.setValue(FontAwesome.CHECK_CIRCLE.getHtml()); + validationIcon.setStyleName("hide-status-label"); + } + + @Override + public void focus() { + queryTextField.focus(); + } + + /** + * Adds the given listener + * + * @param textChangeListener + * the listener to be called in case of text changed + */ + public void addTextChangeListener(final FilterQueryChangeListener textChangeListener) { + listeners.add(textChangeListener); + } + + public void setValue(final String textValue) { + queryTextField.setValue(textValue); + } + + public String getValue() { + return queryTextField.getValue(); + } + + /** + * Called when the filter-query has been changed in the textfield, e.g. from + * client-side. + * + * @param currentText + * the current text of the textfield which has been changed + * @param valid + * {@code boolean} if the current text is RSQL syntax valid + * otherwise {@code false} + * @param validationMessage + * a message shown in case of syntax errors as tooltip + */ + public void onQueryFilterChange(final String currentText, final boolean valid, final String validationMessage) { + if (valid) { + showValidationSuccesIcon(currentText); + } else { + showValidationFailureIcon(validationMessage); + } + listeners.forEach(listener -> listener.queryChanged(valid, currentText)); + } + + /** + * Shows the validation success icon in the textfield + * + * @param text + * the text to store in the UI state object + */ + public void showValidationSuccesIcon(final String text) { + validationIcon.setValue(FontAwesome.CHECK_CIRCLE.getHtml()); + validationIcon.setStyleName(SPUIStyleDefinitions.SUCCESS_ICON); + filterManagementUIState.setFilterQueryValue(text); + filterManagementUIState.setIsFilterByInvalidFilterQuery(Boolean.FALSE); + } + + /** + * Shows the validation error icon in the textfield + * + * @param validationMessage + * the validation message which should be added to the error-icon + * tooltip + */ + public void showValidationFailureIcon(final String validationMessage) { + validationIcon.setValue(FontAwesome.TIMES_CIRCLE.getHtml()); + validationIcon.setStyleName(SPUIStyleDefinitions.ERROR_ICON); + validationIcon.setDescription(validationMessage); + filterManagementUIState.setFilterQueryValue(null); + filterManagementUIState.setIsFilterByInvalidFilterQuery(Boolean.TRUE); + } + + public boolean isValidationError() { + return validationIcon.getStyleName().equals(SPUIStyleDefinitions.ERROR_ICON); + } + + private TextField createSearchField() { + final TextField textField = new TextFieldBuilder().immediate(true).id("custom.query.text.Id") + .maxLengthAllowed(SPUILabelDefinitions.TARGET_FILTER_QUERY_TEXT_FIELD_LENGTH).buildTextComponent(); + textField.addStyleName("target-filter-textfield"); + textField.setWidth(900.0F, Unit.PIXELS); + textField.setTextChangeEventMode(TextChangeEventMode.EAGER); + textField.setImmediate(true); + textField.setTextChangeTimeout(100); + textField.addShortcutListener(new EnterShortCutListener()); + return textField; + } + + private static Label createStatusIcon() { + final Label statusIcon = new Label(); + statusIcon.setImmediate(true); + statusIcon.setContentMode(ContentMode.HTML); + statusIcon.setSizeFull(); + setInitialStatusIconStyle(statusIcon); + statusIcon.setId(SPUIComponentIdProvider.VALIDATION_STATUS_ICON_ID); + return statusIcon; + } + + private static void setInitialStatusIconStyle(final Label statusIcon) { + statusIcon.setValue(FontAwesome.CHECK_CIRCLE.getHtml()); + statusIcon.setStyleName("hide-status-label"); + } + + class StatusCircledAsync implements Runnable { + private final UI current; + + public StatusCircledAsync(final UI current) { + this.current = current; + } + + @Override + public void run() { + UI.setCurrent(current); + eventBus.publish(this, CustomFilterUIEvent.FILTER_TARGET_BY_QUERY); + } + } + + private final class EnterShortCutListener extends ShortcutListener { + + private static final long serialVersionUID = 1L; + + public EnterShortCutListener() { + super("Enter", KeyCode.ENTER, new int[0]); + } + + @Override + public void handleAction(final Object sender, final Object target) { + if (!isValidationError()) { + showValidationInProgress(); + executor.execute(new StatusCircledAsync(UI.getCurrent())); + } + } + + private void showValidationInProgress() { + validationIcon.setValue(null); + validationIcon.addStyleName("show-status-label"); + validationIcon.setStyleName(SPUIStyleDefinitions.TARGET_FILTER_SEARCH_PROGRESS_INDICATOR_STYLE); + } + } + + /** + * Change listener on the textfield. + */ + @FunctionalInterface + public interface FilterQueryChangeListener { + /** + * Called when the text has been changed and validated. + * + * @param valid + * indicates if the entered query text is valid + * @param query + * the entered query text + */ + void queryChanged(final boolean valid, final String query); + } +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/CreateOrUpdateFilterHeader.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/CreateOrUpdateFilterHeader.java index b6f186188..34607da2b 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/CreateOrUpdateFilterHeader.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/CreateOrUpdateFilterHeader.java @@ -8,7 +8,7 @@ */ package org.eclipse.hawkbit.ui.filtermanagement; -import java.util.concurrent.Executor; +import java.util.Optional; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; @@ -23,6 +23,7 @@ import org.eclipse.hawkbit.ui.common.builder.TextFieldBuilder; import org.eclipse.hawkbit.ui.components.SPUIButton; import org.eclipse.hawkbit.ui.components.SPUIComponentProvider; import org.eclipse.hawkbit.ui.decorators.SPUIButtonStyleSmallNoBorder; +import org.eclipse.hawkbit.ui.filtermanagement.AutoCompleteTextFieldComponent.FilterQueryChangeListener; import org.eclipse.hawkbit.ui.filtermanagement.event.CustomFilterUIEvent; import org.eclipse.hawkbit.ui.filtermanagement.state.FilterManagementUIState; import org.eclipse.hawkbit.ui.utils.I18N; @@ -31,7 +32,6 @@ import org.eclipse.hawkbit.ui.utils.SPUILabelDefinitions; import org.eclipse.hawkbit.ui.utils.SPUIStyleDefinitions; import org.eclipse.hawkbit.ui.utils.UINotification; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; import org.vaadin.spring.events.EventBus; import org.vaadin.spring.events.EventScope; import org.vaadin.spring.events.annotation.EventBusListenerMethod; @@ -40,16 +40,11 @@ import com.google.common.base.Strings; import com.vaadin.event.FieldEvents.BlurEvent; import com.vaadin.event.FieldEvents.BlurListener; import com.vaadin.event.FieldEvents.TextChangeEvent; -import com.vaadin.event.FieldEvents.TextChangeListener; import com.vaadin.event.LayoutEvents.LayoutClickEvent; import com.vaadin.event.LayoutEvents.LayoutClickListener; -import com.vaadin.event.ShortcutAction.KeyCode; import com.vaadin.server.FontAwesome; -import com.vaadin.shared.ui.label.ContentMode; import com.vaadin.spring.annotation.SpringComponent; import com.vaadin.spring.annotation.ViewScope; -import com.vaadin.ui.AbstractField; -import com.vaadin.ui.AbstractTextField.TextChangeEventMode; import com.vaadin.ui.Alignment; import com.vaadin.ui.Button; import com.vaadin.ui.Button.ClickEvent; @@ -57,7 +52,6 @@ import com.vaadin.ui.HorizontalLayout; import com.vaadin.ui.Label; import com.vaadin.ui.Link; import com.vaadin.ui.TextField; -import com.vaadin.ui.UI; import com.vaadin.ui.VerticalLayout; import com.vaadin.ui.themes.ValoTheme; @@ -94,11 +88,10 @@ public class CreateOrUpdateFilterHeader extends VerticalLayout implements Button private transient UiProperties uiProperties; @Autowired - @Qualifier("uiExecutor") - private transient Executor executor; + private transient EntityFactory entityFactory; @Autowired - private transient EntityFactory entityFactory; + private AutoCompleteTextFieldComponent queryTextField; private HorizontalLayout breadcrumbLayout; @@ -108,8 +101,6 @@ public class CreateOrUpdateFilterHeader extends VerticalLayout implements Button private Label headerCaption; - private TextField queryTextField; - private TextField nameTextField; private Label nameLabel; @@ -120,10 +111,6 @@ public class CreateOrUpdateFilterHeader extends VerticalLayout implements Button private Link helpLink; - private Label validationIcon; - - private HorizontalLayout searchLayout; - private String oldFilterName; private String oldFilterQuery; @@ -136,8 +123,6 @@ public class CreateOrUpdateFilterHeader extends VerticalLayout implements Button private LayoutClickListener nameLayoutClickListner; - private boolean validationFailed = false; - /** * Initialize the Campaign Status History Header. */ @@ -170,8 +155,6 @@ public class CreateOrUpdateFilterHeader extends VerticalLayout implements Button } else if (custFUIEvent == CustomFilterUIEvent.CREATE_NEW_FILTER_CLICK) { setUpCaptionLayout(true); resetComponents(); - } else if (custFUIEvent == CustomFilterUIEvent.UPDATE_TARGET_FILTER_SEARCH_ICON) { - UI.getCurrent().access(() -> updateStatusIconAfterTablePopulated()); } } @@ -183,38 +166,22 @@ public class CreateOrUpdateFilterHeader extends VerticalLayout implements Button oldFilterQuery = filterManagementUIState.getTfQuery().get().getQuery(); } breadcrumbName.setValue(nameLabel.getValue()); - showValidationSuccesIcon(); + queryTextField.showValidationSuccesIcon(filterManagementUIState.getFilterQueryValue()); titleFilterIconsLayout.addStyleName(SPUIStyleDefinitions.TARGET_FILTER_CAPTION_LAYOUT); headerCaption.setVisible(false); setUpCaptionLayout(false); } private void resetComponents() { + queryTextField.clear(); + queryTextField.focus(); headerCaption.setVisible(true); breadcrumbName.setValue(headerCaption.getValue()); nameLabel.setValue(""); - queryTextField.setValue(""); - setInitialStatusIconStyle(validationIcon); - validationFailed = false; saveButton.setEnabled(false); titleFilterIconsLayout.removeStyleName(SPUIStyleDefinitions.TARGET_FILTER_CAPTION_LAYOUT); } - private Label createStatusIcon() { - final Label statusIcon = new Label(); - statusIcon.setImmediate(true); - statusIcon.setContentMode(ContentMode.HTML); - statusIcon.setSizeFull(); - setInitialStatusIconStyle(statusIcon); - statusIcon.setId(SPUIComponentIdProvider.VALIDATION_STATUS_ICON_ID); - return statusIcon; - } - - private void setInitialStatusIconStyle(final Label statusIcon) { - statusIcon.setValue(FontAwesome.CHECK_CIRCLE.getHtml()); - statusIcon.setStyleName("hide-status-label"); - } - private void createComponents() { breadcrumbButton = createBreadcrumbButton(); @@ -227,10 +194,6 @@ public class CreateOrUpdateFilterHeader extends VerticalLayout implements Button nameTextField = createNameTextField(); nameTextField.setWidth(380, Unit.PIXELS); - queryTextField = createSearchField(); - addSearchLisenter(); - - validationIcon = createStatusIcon(); saveButton = createSaveButton(); helpLink = SPUIComponentProvider.getHelpLink(uiProperties.getLinks().getDocumentation().getTargetfilterView()); @@ -282,6 +245,14 @@ public class CreateOrUpdateFilterHeader extends VerticalLayout implements Button } } }; + + queryTextField.addTextChangeListener(new FilterQueryChangeListener() { + @Override + public void queryChanged(final boolean valid, final String query) { + enableDisableSaveButton(!valid, query); + } + }); + } private void onFilterNameChange(final TextChangeEvent event) { @@ -318,15 +289,6 @@ public class CreateOrUpdateFilterHeader extends VerticalLayout implements Button titleFilterLayout.setComponentAlignment(titleFilterIconsLayout, Alignment.TOP_LEFT); titleFilterLayout.setComponentAlignment(closeIcon, Alignment.TOP_RIGHT); - validationIcon = createStatusIcon(); - - searchLayout = new HorizontalLayout(); - searchLayout.setSizeUndefined(); - searchLayout.setSpacing(false); - searchLayout.addComponents(validationIcon, queryTextField); - searchLayout.addStyleName("custom-search-layout"); - searchLayout.setComponentAlignment(validationIcon, Alignment.TOP_CENTER); - final HorizontalLayout iconLayout = new HorizontalLayout(); iconLayout.setSizeUndefined(); iconLayout.setSpacing(false); @@ -335,7 +297,7 @@ public class CreateOrUpdateFilterHeader extends VerticalLayout implements Button final HorizontalLayout queryLayout = new HorizontalLayout(); queryLayout.setSizeUndefined(); queryLayout.setSpacing(true); - queryLayout.addComponents(searchLayout, iconLayout); + queryLayout.addComponents(queryTextField, iconLayout); addComponent(breadcrumbLayout); addComponent(titleFilterLayout); @@ -358,58 +320,6 @@ public class CreateOrUpdateFilterHeader extends VerticalLayout implements Button } } - private void addSearchLisenter() { - queryTextField.addTextChangeListener(new TextChangeListener() { - private static final long serialVersionUID = -6668604418942689391L; - - @Override - public void textChange(final TextChangeEvent event) { - validationIcon.addStyleName("show-status-label"); - showValidationInProgress(); - onQueryChange(event.getText()); - executor.execute(new StatusCircledAsync(UI.getCurrent())); - } - - }); - } - - class StatusCircledAsync implements Runnable { - private final UI current; - - public StatusCircledAsync(final UI current) { - this.current = current; - } - - @Override - public void run() { - UI.setCurrent(current); - eventBus.publish(this, CustomFilterUIEvent.FILTER_TARGET_BY_QUERY); - } - } - - private void onQueryChange(final String input) { - if (!Strings.isNullOrEmpty(input)) { - final ValidationResult validationResult = FilterQueryValidation.getExpectedTokens(input); - if (!validationResult.getIsValidationFailed()) { - filterManagementUIState.setFilterQueryValue(input); - filterManagementUIState.setIsFilterByInvalidFilterQuery(Boolean.FALSE); - validationFailed = false; - } else { - validationFailed = true; - filterManagementUIState.setFilterQueryValue(null); - filterManagementUIState.setIsFilterByInvalidFilterQuery(Boolean.TRUE); - validationIcon.setDescription(validationResult.getMessage()); - showValidationFailureIcon(); - } - enableDisableSaveButton(validationFailed, input); - } else { - setInitialStatusIconStyle(validationIcon); - filterManagementUIState.setFilterQueryValue(null); - filterManagementUIState.setIsFilterByInvalidFilterQuery(Boolean.TRUE); - } - queryTextField.setValue(input); - } - private void enableDisableSaveButton(final boolean validationFailed, final String query) { if (validationFailed || (isNameAndQueryEmpty(nameTextField.getValue(), query) || (query.equals(oldFilterQuery) && nameTextField.getValue().equals(oldFilterName)))) { @@ -428,21 +338,6 @@ public class CreateOrUpdateFilterHeader extends VerticalLayout implements Button return false; } - private void showValidationSuccesIcon() { - validationIcon.setValue(FontAwesome.CHECK_CIRCLE.getHtml()); - validationIcon.setStyleName(SPUIStyleDefinitions.SUCCESS_ICON); - } - - private void showValidationFailureIcon() { - validationIcon.setValue(FontAwesome.TIMES_CIRCLE.getHtml()); - validationIcon.setStyleName(SPUIStyleDefinitions.ERROR_ICON); - } - - private void showValidationInProgress() { - validationIcon.setValue(null); - validationIcon.setStyleName(SPUIStyleDefinitions.TARGET_FILTER_SEARCH_PROGRESS_INDICATOR_STYLE); - } - private SPUIButton createSearchResetIcon() { final SPUIButton button = (SPUIButton) SPUIComponentProvider.getButton("create.custom.filter.close.Id", "", "", null, false, FontAwesome.TIMES, SPUIButtonStyleSmallNoBorder.class); @@ -450,18 +345,6 @@ public class CreateOrUpdateFilterHeader extends VerticalLayout implements Button return button; } - private static TextField createSearchField() { - final TextField textField = new TextFieldBuilder().immediate(true).id("custom.query.text.Id") - .maxLengthAllowed(SPUILabelDefinitions.TARGET_FILTER_QUERY_TEXT_FIELD_LENGTH).buildTextComponent(); - textField.addStyleName("target-filter-textfield"); - textField.setWidth(900.0F, Unit.PIXELS); - textField.setTextChangeEventMode(TextChangeEventMode.LAZY); - textField.setTextChangeTimeout(1000); - - textField.addShortcutListener(new AbstractField.FocusShortcut(textField, KeyCode.ENTER)); - return textField; - } - private void closeFilterLayout() { filterManagementUIState.setFilterQueryValue(null); filterManagementUIState.setCreateFilterBtnClicked(false); @@ -508,7 +391,11 @@ public class CreateOrUpdateFilterHeader extends VerticalLayout implements Button } private void updateCustomFilter() { - final TargetFilterQuery targetFilterQuery = filterManagementUIState.getTfQuery().get(); + final Optional tfQuery = filterManagementUIState.getTfQuery(); + if (!tfQuery.isPresent()) { + return; + } + final TargetFilterQuery targetFilterQuery = tfQuery.get(); targetFilterQuery.setName(nameTextField.getValue()); targetFilterQuery.setQuery(queryTextField.getValue()); final TargetFilterQuery updatedTargetFilter = targetFilterQueryManagement @@ -545,13 +432,6 @@ public class CreateOrUpdateFilterHeader extends VerticalLayout implements Button return true; } - private void updateStatusIconAfterTablePopulated() { - queryTextField.focus(); - if (!validationFailed && !Strings.isNullOrEmpty(queryTextField.getValue())) { - showValidationSuccesIcon(); - } - } - private void showCustomFiltersView() { eventBus.publish(this, CustomFilterUIEvent.SHOW_FILTER_MANAGEMENT); } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/FilterManagementView.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/FilterManagementView.java index 40e6306b0..88f7c003d 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/FilterManagementView.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/FilterManagementView.java @@ -94,8 +94,7 @@ public class FilterManagementView extends VerticalLayout implements View { void onEvent(final CustomFilterUIEvent custFilterUIEvent) { if (custFilterUIEvent == CustomFilterUIEvent.TARGET_FILTER_DETAIL_VIEW) { viewTargetFilterDetailLayout(); - } else if (custFilterUIEvent == CustomFilterUIEvent.CREATE_NEW_FILTER_CLICK - || custFilterUIEvent == CustomFilterUIEvent.FILTER_TARGET_BY_QUERY) { + } else if (custFilterUIEvent == CustomFilterUIEvent.CREATE_NEW_FILTER_CLICK) { this.getUI().access(() -> viewCreateTargetFilterLayout()); } else if (custFilterUIEvent == CustomFilterUIEvent.EXIT_CREATE_OR_UPDATE_FILTRER_VIEW || custFilterUIEvent == CustomFilterUIEvent.SHOW_FILTER_MANAGEMENT) { diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/FilterQueryValidation.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/FilterQueryValidation.java deleted file mode 100644 index 6d1f1a7ab..000000000 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/FilterQueryValidation.java +++ /dev/null @@ -1,182 +0,0 @@ -/** - * 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.ui.filtermanagement; - -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -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.ui.utils.SpringContextHelper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.data.domain.PageRequest; - -import cz.jirutka.rsql.parser.ParseException; -import cz.jirutka.rsql.parser.RSQLParserException; - -/** - * - * Validates the target filter query. - * - */ -public final class FilterQueryValidation { - - private static final Logger LOGGER = LoggerFactory.getLogger(FilterQueryValidation.class); - - private FilterQueryValidation() { - - } - - /** - * method for get ExpectedTokens. - * - * @param input - * @param entityManager - * @return - */ - public static ValidationResult getExpectedTokens(final String input) { - - final TokenDescription tokenDesc = new TokenDescription(); - final ValidationResult result = new ValidationResult(); - final List expectedTokens = new ArrayList<>(); - try { - - final TargetManagement management = SpringContextHelper.getBean(TargetManagement.class); - management.findTargetsAll(input, new PageRequest(0, 100)); - } catch (final RSQLParameterSyntaxException ex) { - setExceptionDetails(new Exception(ex.getCause().getCause()), expectedTokens, result, tokenDesc); - result.setMessage(getCustomMessage(ex.getCause().getMessage(), result.getExpectedTokens())); - result.setIsValidationFailed(Boolean.TRUE); - LOGGER.trace("Syntax exception on parsing :", ex); - } catch (final RSQLParserException ex) { - setExceptionDetails(ex, expectedTokens, result, tokenDesc); - result.setMessage(getCustomMessage(ex.getMessage(), result.getExpectedTokens())); - result.setIsValidationFailed(Boolean.TRUE); - LOGGER.trace("Exception on parsing :", ex); - } catch (final IllegalArgumentException ex) { - result.setMessage(getCustomMessage(ex.getMessage(), null)); - result.setIsValidationFailed(Boolean.TRUE); - LOGGER.trace("Illegal argument on parsing :", ex); - } catch (final RSQLParameterUnsupportedFieldException ex) { - result.setMessage(getCustomMessage(ex.getMessage(), null)); - result.setIsValidationFailed(Boolean.TRUE); - LOGGER.trace("Unsupported field on parsing :", ex); - } - return result; - - } - - private static void setExceptionDetails(final Exception ex, final List expectedTokens, - final ValidationResult result, final TokenDescription tokenDesc) { - for (final Integer node : getNextTokens(ex)) { - if (node != 12) { - expectedTokens.add(tokenDesc.getTokenImage()[node]); - } - } - final List customExpectTokenList = processExpectedTokens(getNextTokens(ex)); - if (!customExpectTokenList.isEmpty()) { - result.setExpectedTokens(customExpectTokenList); - } else { - result.setExpectedTokens(expectedTokens); - } - } - - /** - * method for process ExpectedTokens. - * - * @param expectedTokens - * @return - */ - // Exception squid:S2095 - see - // https://jira.sonarsource.com/browse/SONARJAVA-1478 - @SuppressWarnings({ "squid:S2095" }) - public static List processExpectedTokens(final List expectedTokens) { - final List expectToken = new ArrayList<>(); - if (expectedTokens.size() == 2 && expectedTokens.contains(9) && expectedTokens.contains(4)) { - final List expectedFieldList = Arrays.stream(TargetFields.values()).map(v -> v.name().toLowerCase()) - .collect(Collectors.toList()); - expectToken.addAll(expectedFieldList); - expectToken.add("assignedds.name"); - expectToken.add("assignedds.version"); - } - return expectToken; - } - - /** - * Method To Get Next Token. - * - * @param e - * . - * @return list. - */ - public static List getNextTokens(final Exception e) { - Throwable parseException = e.getCause(); - final List listTokens = new ArrayList<>(); - if (parseException != null) { - do { - if (parseException instanceof ParseException) { - try { - Field declaredField; - declaredField = parseException.getClass().getDeclaredField("expectedTokenSequences"); - int[][] tokens; - tokens = (int[][]) declaredField.get(parseException); - for (final int[] is : tokens) { - for (final int i : is) { - listTokens.add(i); - } - } - return listTokens; - } catch (SecurityException | NoSuchFieldException | IllegalArgumentException - | IllegalAccessException ex) { - LOGGER.info("Exception on parsing :", ex); - } - - } else { - return listTokens; - } - } while ((parseException = parseException.getCause()) != null); - } - return Collections.emptyList(); - } - - /** - * To Get Custom Message. - * - * @param message - * @param expectedTokens - * @return String. - */ - public static String getCustomMessage(final String message, final List expectedTokens) { - String builder = message; - if (message.contains(":")) { - builder = message.substring(message.indexOf(':') + 1, message.length()); - if (builder.indexOf("Was expecting") != -1) { - builder = builder.substring(0, builder.lastIndexOf("Was expecting")); - } - if (null != expectedTokens && !expectedTokens.isEmpty()) { - final StringBuilder tokens = new StringBuilder(); - expectedTokens.stream().forEach(value -> tokens.append(value + ",")); - 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; - } - -} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/TargetFilterHeader.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/TargetFilterHeader.java index ada3e600a..a58b40a98 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/TargetFilterHeader.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/TargetFilterHeader.java @@ -114,6 +114,7 @@ public class TargetFilterHeader extends VerticalLayout { } private void addNewFilter() { + filterManagementUIState.setTfQuery(null); filterManagementUIState.setFilterQueryValue(null); filterManagementUIState.setCreateFilterBtnClicked(true); eventBus.publish(this, CustomFilterUIEvent.CREATE_NEW_FILTER_CLICK); diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/TargetFilterTable.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/TargetFilterTable.java index 5ee3be084..010e37bde 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/TargetFilterTable.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/TargetFilterTable.java @@ -225,7 +225,6 @@ public class TargetFilterTable extends Table { final String targetFilterName = (String) ((Button) event.getComponent()).getData(); final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement .findTargetFilterQueryByName(targetFilterName); - filterManagementUIState.setTfQuery(targetFilterQuery); filterManagementUIState.setFilterQueryValue(targetFilterQuery.getQuery()); filterManagementUIState.setEditViewDisplayed(true); diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/TextFieldSuggestionBox.gwt.xml b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/TextFieldSuggestionBox.gwt.xml new file mode 100644 index 000000000..43f76950d --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/TextFieldSuggestionBox.gwt.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/TextFieldSuggestionBox.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/TextFieldSuggestionBox.java new file mode 100644 index 000000000..3c3b63bee --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/TextFieldSuggestionBox.java @@ -0,0 +1,83 @@ +/** + * 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.ui.filtermanagement; + +import java.util.stream.Collectors; + +import org.eclipse.hawkbit.repository.rsql.RsqlValidationOracle; +import org.eclipse.hawkbit.repository.rsql.SuggestionContext; +import org.eclipse.hawkbit.repository.rsql.ValidationOracleContext; +import org.eclipse.hawkbit.ui.filtermanagement.client.SuggestTokenDto; +import org.eclipse.hawkbit.ui.filtermanagement.client.SuggestionContextDto; +import org.eclipse.hawkbit.ui.filtermanagement.client.TextFieldSuggestionBoxClientRpc; +import org.eclipse.hawkbit.ui.filtermanagement.client.TextFieldSuggestionBoxServerRpc; + +import com.vaadin.server.AbstractExtension; +import com.vaadin.ui.TextField; + +/** + * Extension for the AutoCompleteTexfield. + * + */ +public class TextFieldSuggestionBox extends AbstractExtension implements TextFieldSuggestionBoxServerRpc { + + private static final long serialVersionUID = 1L; + private final transient RsqlValidationOracle rsqlValidationOracle; + private final AutoCompleteTextFieldComponent autoCompleteTextFieldComponent; + + /** + * Constructor. + * + * @param autoCompleteTextFieldComponent + * @param rsqlValidationOracle + * the suggestion oracle where to retrieve the suggestions from + */ + public TextFieldSuggestionBox(final RsqlValidationOracle rsqlValidationOracle, + final AutoCompleteTextFieldComponent autoCompleteTextFieldComponent) { + this.rsqlValidationOracle = rsqlValidationOracle; + this.autoCompleteTextFieldComponent = autoCompleteTextFieldComponent; + + registerRpc(this, TextFieldSuggestionBoxServerRpc.class); + } + + /** + * Add this extension to the target connector. This method is protected to + * allow subclasses to require a more specific type of target. + * + * @param target + * the connector to attach this extension to + */ + public void extend(final TextField target) { + super.extend(target); + } + + @Override + public void suggest(final String text, final int cursor) { + final ValidationOracleContext suggest = rsqlValidationOracle.suggest(text, cursor); + updateValidationIcon(suggest, text); + getRpcProxy(TextFieldSuggestionBoxClientRpc.class).showSuggestions(mapToDto(suggest.getSuggestionContext())); + } + + private static SuggestionContextDto mapToDto(final SuggestionContext suggestionContext) { + return new SuggestionContextDto(suggestionContext.getCursorPosition(), + suggestionContext.getSuggestions().stream() + .filter(suggestion -> suggestion.getTokenImageName() == null + || suggestion.getSuggestion().contains(suggestion.getTokenImageName())) + .map(suggestion -> new SuggestTokenDto(suggestion.getStart(), suggestion.getEnd(), + suggestion.getSuggestion())) + .collect(Collectors.toList())); + + } + + private void updateValidationIcon(final ValidationOracleContext suggest, final String text) { + final String errorMessage = (suggest.getSyntaxErrorContext() != null) + ? suggest.getSyntaxErrorContext().getErrorMessage() : null; + autoCompleteTextFieldComponent.onQueryFilterChange(text, !suggest.isSyntaxError(), errorMessage); + } +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/TokenDescription.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/TokenDescription.java deleted file mode 100644 index 81f222cee..000000000 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/TokenDescription.java +++ /dev/null @@ -1,30 +0,0 @@ -/** - * 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.ui.filtermanagement; - -import java.util.Arrays; - -/** - * - * Available token details. - * - * - * - */ -public class TokenDescription { - - /** Literal token values. */ - private static final String[] TOKEN_IMAGE = { "", "\" \"", "\"\\t\"", "", "", - "", "", "", "", "\"(\"", "\")\"", "<==>|", ">=|<=", }; - - public String[] getTokenImage() { - return Arrays.copyOf(TOKEN_IMAGE, TOKEN_IMAGE.length); - } - -} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/ValidationResult.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/ValidationResult.java deleted file mode 100644 index 68d80fc62..000000000 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/ValidationResult.java +++ /dev/null @@ -1,73 +0,0 @@ -/** - * 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.ui.filtermanagement; - -import java.util.ArrayList; -import java.util.List; - -/** - * Query validation result with expected token on error. - * - * - * - */ -public class ValidationResult { - - private List expectedTokens = new ArrayList<>(); - - private String message; - - private Boolean isValidationFailed = Boolean.FALSE; - - /** - * @return the isValidationFailed - */ - public Boolean getIsValidationFailed() { - return isValidationFailed; - } - - /** - * @param isValidationFailed - * the isValidationFailed to set - */ - public void setIsValidationFailed(final Boolean isValidationFailed) { - this.isValidationFailed = isValidationFailed; - } - - /** - * @return the expectedTokens - */ - public List getExpectedTokens() { - return expectedTokens; - } - - /** - * @param expectedTokens - * the expectedTokens to set - */ - public void setExpectedTokens(final List expectedTokens) { - this.expectedTokens = expectedTokens; - } - - /** - * @return the message - */ - public String getMessage() { - return message; - } - - /** - * @param message - * the message to set - */ - public void setMessage(final String message) { - this.message = message; - } - -} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/client/AutoCompleteTextFieldConnector.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/client/AutoCompleteTextFieldConnector.java new file mode 100644 index 000000000..949b2a39d --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/client/AutoCompleteTextFieldConnector.java @@ -0,0 +1,137 @@ +/** + * 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.ui.filtermanagement.client; + +import java.util.List; + +import org.eclipse.hawkbit.ui.filtermanagement.TextFieldSuggestionBox; + +import com.google.gwt.event.dom.client.KeyCodes; +import com.google.gwt.event.dom.client.KeyUpEvent; +import com.google.gwt.event.dom.client.KeyUpHandler; +import com.google.gwt.user.client.ui.MenuItem; +import com.vaadin.client.ComponentConnector; +import com.vaadin.client.ServerConnector; +import com.vaadin.client.extensions.AbstractExtensionConnector; +import com.vaadin.client.ui.VOverlay; +import com.vaadin.client.ui.VTextField; +import com.vaadin.shared.ui.Connect; + +/** + * Connector for the AutoCompleteTextField which automatically listens to + * key-events to show pop-up panel with entered suggestions based on the + * {@link TextFieldSuggestionBoxServerRpc} call. + * + */ +@SuppressWarnings({ "deprecation", "squid:CallToDeprecatedMethod" }) +// need to use VOverlay because otherwise it's not in the correct theme +// widget @see com.vaadin.client.ui.VOverlay.getOverlayContainer() +@Connect(TextFieldSuggestionBox.class) +public class AutoCompleteTextFieldConnector extends AbstractExtensionConnector { + + private static final long serialVersionUID = 1L; + + private final transient SuggestionsSelectList select = new SuggestionsSelectList(); + private transient VTextField textFieldWidget; + + private final TextFieldSuggestionBoxServerRpc rpc = getRpcProxy(TextFieldSuggestionBoxServerRpc.class); + + private final transient VOverlay panel = new VOverlay(true, false, true); + + @Override + protected void init() { + super.init(); + + registerRpc(TextFieldSuggestionBoxClientRpc.class, new TextFieldSuggestionBoxClientRpc() { + private static final long serialVersionUID = 1L; + + @Override + public void showSuggestions(final SuggestionContextDto suggestContext) { + select.clearItems(); + if (suggestContext != null) { + final List suggestions = suggestContext.getSuggestions(); + if (suggestions != null && !suggestions.isEmpty()) { + select.addItems(suggestions, textFieldWidget, panel, rpc); + panel.showRelativeTo(textFieldWidget); + select.moveSelectionDown(); + return; + } + } + panel.hide(); + } + }); + } + + @Override + protected void extend(final ServerConnector target) { + textFieldWidget = (VTextField) ((ComponentConnector) target).getWidget(); + textFieldWidget.setImmediate(true); + textFieldWidget.textChangeEventMode = "EAGER"; + panel.setWidget(select); + panel.setStyleName("suggestion-popup"); + panel.setOwner(textFieldWidget); + + textFieldWidget.addKeyUpHandler(new KeyUpHandler() { + @Override + public void onKeyUp(final KeyUpEvent event) { + if (panel.isAttached()) { + handlePanelEventDelegation(event); + } else { + doAskForSuggestion(); + } + } + }); + } + + private void handlePanelEventDelegation(final KeyUpEvent event) { + switch (event.getNativeKeyCode()) { + case KeyCodes.KEY_DOWN: + arrowKeyDown(event); + break; + case KeyCodes.KEY_UP: + arrorKeyUp(event); + break; + case KeyCodes.KEY_ESCAPE: + escapeKey(); + break; + case KeyCodes.KEY_ENTER: + enterKey(); + break; + default: + doAskForSuggestion(); + } + } + + private void escapeKey() { + panel.hide(); + } + + private void enterKey() { + final MenuItem item = select.getSelectedItem(); + if (item != null) { + item.getScheduledCommand().execute(); + } + } + + private void arrorKeyUp(final KeyUpEvent event) { + select.moveSelectionUp(); + event.preventDefault(); + event.stopPropagation(); + } + + private void arrowKeyDown(final KeyUpEvent event) { + select.moveSelectionDown(); + event.preventDefault(); + event.stopPropagation(); + } + + private void doAskForSuggestion() { + rpc.suggest(textFieldWidget.getValue(), textFieldWidget.getCursorPos()); + } +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/client/SuggestTokenDto.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/client/SuggestTokenDto.java new file mode 100644 index 000000000..8291fa0bf --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/client/SuggestTokenDto.java @@ -0,0 +1,68 @@ +package org.eclipse.hawkbit.ui.filtermanagement.client; + +import java.io.Serializable; + +/** + * A suggestion which contains the start and the end character position of the + * suggested token of the suggestion of the token and the actual suggestion. + */ +public class SuggestTokenDto implements Serializable { + + private static final long serialVersionUID = 1L; + + private int start; + private int end; + private String suggestion; + + /** + * Default constructor. + */ + public SuggestTokenDto() { + // necessary for java serialization with GWT. + } + + /** + * Constructor. + * + * @param start + * the character position of the start of the token + * @param end + * the character position of the end of the token + * @param suggestion + * the token suggestion + */ + public SuggestTokenDto(final int start, final int end, final String suggestion) { + this.start = start; + this.end = end; + this.suggestion = suggestion; + } + + public int getStart() { + return start; + } + + public int getEnd() { + return end; + } + + public String getSuggestion() { + return suggestion; + } + + public void setStart(final int start) { + this.start = start; + } + + public void setEnd(final int end) { + this.end = end; + } + + public void setSuggestion(final String suggestion) { + this.suggestion = suggestion; + } + + @Override + public String toString() { + return "SuggestTokenDto [start=" + start + ", end=" + end + ", suggestion=" + suggestion + "]"; + } +} \ No newline at end of file diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/client/SuggestionContextDto.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/client/SuggestionContextDto.java new file mode 100644 index 000000000..ec5326d28 --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/client/SuggestionContextDto.java @@ -0,0 +1,63 @@ +/** + * 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.ui.filtermanagement.client; + +import java.io.Serializable; +import java.util.List; + +public class SuggestionContextDto implements Serializable { + + private static final long serialVersionUID = 1L; + + private int cursorPosition; + private List suggestions; + + /** + * Default constructor. + */ + public SuggestionContextDto() { + // necessary for java serialization with GWT. + } + + /** + * Constructor. + * + * @param rsqlQuery + * the original RSQL based query the suggestions based on + * @param cursorPosition + * the current cursor position + * @param suggestions + * the suggestions for the current cursor position + */ + public SuggestionContextDto(final int cursorPosition, final List suggestions) { + this.cursorPosition = cursorPosition; + this.suggestions = suggestions; + } + + public List getSuggestions() { + return suggestions; + } + + public int getCursorPosition() { + return cursorPosition; + } + + public void setCursorPosition(final int cursorPosition) { + this.cursorPosition = cursorPosition; + } + + public void setSuggestions(final List suggestions) { + this.suggestions = suggestions; + } + + @Override + public String toString() { + return "SuggestionContextDto [cursorPosition=" + cursorPosition + ", suggestions=" + suggestions + "]"; + } +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/client/SuggestionsSelectList.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/client/SuggestionsSelectList.java new file mode 100644 index 000000000..e51c1c8fe --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/client/SuggestionsSelectList.java @@ -0,0 +1,116 @@ +/** + * 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.ui.filtermanagement.client; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.google.gwt.aria.client.Roles; +import com.google.gwt.core.client.Scheduler.ScheduledCommand; +import com.google.gwt.user.client.ui.MenuBar; +import com.google.gwt.user.client.ui.MenuItem; +import com.google.gwt.user.client.ui.PopupPanel; +import com.vaadin.client.WidgetUtil; +import com.vaadin.client.ui.VTextField; + +/** + * The suggestion list within the suggestion pop-up panel. + */ +public class SuggestionsSelectList extends MenuBar { + + public static final String CLASSNAME = "autocomplete"; + private final Map tokenMap = new HashMap<>(); + + /** + * Constructor. + */ + public SuggestionsSelectList() { + super(true); + setFocusOnHoverEnabled(false); + } + + /** + * Adds suggestions to the suggestion menu bar. + * + * @param suggestions + * the suggestions to be added + * @param textFieldWidget + * the text field which the suggestion is attached to to bring + * back the focus after selection + * @param popupPanel + * pop-up panel where the menu bar is shown to hide it after + * selection + * @param suggestionServerRpc + * server RPC to ask for new suggestion after a selection + */ + public void addItems(final List suggestions, final VTextField textFieldWidget, + final PopupPanel popupPanel, final TextFieldSuggestionBoxServerRpc suggestionServerRpc) { + for (int index = 0; index < suggestions.size(); index++) { + final SuggestTokenDto suggestToken = suggestions.get(index); + final MenuItem mi = new MenuItem(suggestToken.getSuggestion(), true, new ScheduledCommand() { + @Override + public void execute() { + final String tmpSuggestion = suggestToken.getSuggestion(); + final TokenStartEnd tokenStartEnd = tokenMap.get(tmpSuggestion); + final String text = textFieldWidget.getValue(); + final StringBuilder builder = new StringBuilder(text); + builder.replace(tokenStartEnd.getStart(), tokenStartEnd.getEnd() + 1, tmpSuggestion); + textFieldWidget.setValue(builder.toString(), true); + popupPanel.hide(); + textFieldWidget.setFocus(true); + suggestionServerRpc.suggest(builder.toString(), textFieldWidget.getCursorPos()); + } + }); + tokenMap.put(suggestToken.getSuggestion(), + new TokenStartEnd(suggestToken.getStart(), suggestToken.getEnd())); + Roles.getListitemRole().set(mi.getElement()); + WidgetUtil.sinkOnloadForImages(mi.getElement()); + addItem(mi); + } + } + + @Override + public void setStyleName(final String style) { + super.setStyleName(style + "-" + CLASSNAME); + } + + @Override + public MenuItem getSelectedItem() { + return super.getSelectedItem(); + } + + /** + * Suggestion Token start and end index. + * + */ + public static final class TokenStartEnd { + final int start; + final int end; + + /** + * Constructor. + * + * @param start + * @param end + */ + public TokenStartEnd(final int start, final int end) { + this.start = start; + this.end = end; + } + + public int getStart() { + return start; + } + + public int getEnd() { + return end; + } + } +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/client/TextFieldSuggestionBoxClientRpc.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/client/TextFieldSuggestionBoxClientRpc.java new file mode 100644 index 000000000..d4385db6f --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/client/TextFieldSuggestionBoxClientRpc.java @@ -0,0 +1,32 @@ +/** + * 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.ui.filtermanagement.client; + +import com.vaadin.shared.communication.ClientRpc; + +/** + * Client RPC for the AutocompleteTextField. The Client RPC interface is used to + * make server to client calls in Vaadin. Only void methods are allowed in + * ClientRpc calls. + * + */ +@FunctionalInterface +public interface TextFieldSuggestionBoxClientRpc extends ClientRpc { + + /** + * Notifies the client about showing the given suggestions in the suggestion + * box. + * + * @param suggestionContext + * the suggestion context which contains all informations about + * showing suggestions + */ + void showSuggestions(final SuggestionContextDto suggestionContext); + +} diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/client/TextFieldSuggestionBoxServerRpc.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/client/TextFieldSuggestionBoxServerRpc.java new file mode 100644 index 000000000..51388a0a3 --- /dev/null +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/client/TextFieldSuggestionBoxServerRpc.java @@ -0,0 +1,34 @@ +/** + * 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.ui.filtermanagement.client; + +import com.vaadin.shared.communication.ServerRpc; + +/** + * Server RPC for the AutoCompleteTextField. The Server RPC interface is used to + * make client to server calls in Vaadin. Only void methods are allowed in + * ServerRpc calls. + */ +@FunctionalInterface +public interface TextFieldSuggestionBoxServerRpc extends ServerRpc { + + /** + * Parses the given RSQL based query and try finding suggestions at the + * current given cursor position. When suggestions are possible the + * {@link TextFieldSuggestionBoxClientRpc#showSuggestions(org.eclipse.hawkbit.rsql.SuggestionContext)} + * is called as a callback mechanism back to the client. + * + * @param text + * the current entered text e.g. in a text field to retrieve + * suggestion for + * @param cursor + * the current cursor position + */ + void suggest(final String text, final int cursor); +} diff --git a/hawkbit-ui/src/main/resources/VAADIN/themes/hawkbit/customstyles/target-filter-query.scss b/hawkbit-ui/src/main/resources/VAADIN/themes/hawkbit/customstyles/target-filter-query.scss index 33ae2dd85..102f6a3c9 100644 --- a/hawkbit-ui/src/main/resources/VAADIN/themes/hawkbit/customstyles/target-filter-query.scss +++ b/hawkbit-ui/src/main/resources/VAADIN/themes/hawkbit/customstyles/target-filter-query.scss @@ -8,6 +8,39 @@ */ @mixin target-filter-query { +.gwt-MenuBar-autocomplete { + cursor: default; + } + + .gwt-MenuBar-autocomplete .gwt-MenuItem{ + border-radius: 3px !important; + cursor: pointer !important; + font-weight: 400 !important; + line-height: 27px !important; + padding: 0 20px 0 10px !important; + position: relative !important; + white-space: nowrap !important; + } + +.gwt-MenuBar-autocomplete .gwt-MenuItem-selected { + background-color: #197de1 !important; + background-image: linear-gradient(to bottom, #1b87e3 2%, #166ed5 98%) !important; + color: #ecf2f8 !important; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.05) !important; +} +.suggestion-popup{ + backface-visibility: hidden; + background-color: white; + border-radius: 4px; + box-shadow: 0 4px 10px 0 rgba(0, 0, 0, 0.1), 0 3px 5px 0 rgba(0, 0, 0, 0.05), 0 0 0 1px rgba(0, 0, 0, 0.09); + box-sizing: border-box; + color: #474747; + padding: 4px; + position: relative; + z-index: 1; +} + + .caption-header-layout{ padding-left:10px; } @@ -23,15 +56,17 @@ .target-filter-textfield, .target-filter-textfield:focus{ border:none !important; box-shadow: none !important; - height:26px !important; + height: 26px !important; } .error-icon{ + margin-left: 5px; color:$success-icon-color !important; padding-left:2px !important; } .success-icon{ + margin-left: 5px; color:$error-icon-color !important; padding-left:2px !important; } @@ -42,10 +77,12 @@ } .target-filter-spinner{ - @include valo-spinner( - $size: $v-font-size--small, - $color: $bosch-color-light-blue - ); + margin-top: 5px; + margin-left: 5px; + @include valo-spinner( + $size:16px, + $speed:500ms + ); } .hide-status-label { diff --git a/pom.xml b/pom.xml index 31a175bd6..6bfa81534 100644 --- a/pom.xml +++ b/pom.xml @@ -105,7 +105,7 @@ 3.4 4.1 20141113 - 2.0.0 + 2.1.0 From 9be214770290548ed4e1a112e211809915438255 Mon Sep 17 00:00:00 2001 From: Gaurav Date: Tue, 6 Sep 2016 13:41:23 +0200 Subject: [PATCH 02/11] Ordered the shortcut listener on the press of Enter key for querying the filtered data. Signed-off-by: Gaurav --- .../ui/filtermanagement/AutoCompleteTextFieldComponent.java | 1 + 1 file changed, 1 insertion(+) diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/AutoCompleteTextFieldComponent.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/AutoCompleteTextFieldComponent.java index e3d766391..9752d97c7 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/AutoCompleteTextFieldComponent.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/AutoCompleteTextFieldComponent.java @@ -86,6 +86,7 @@ public class AutoCompleteTextFieldComponent extends HorizontalLayout { @PostConstruct public void postConstruct() { new TextFieldSuggestionBox(rsqlValidationOracle, this).extend(queryTextField); + queryTextField.addShortcutListener(new EnterShortCutListener()); } /** From a42edd14ada7d959edefa5cf133fdee914d7a095 Mon Sep 17 00:00:00 2001 From: Gaurav Date: Tue, 6 Sep 2016 13:41:23 +0200 Subject: [PATCH 03/11] Fix the issue on press of Enter key for querying the filtered data but not on while selecting any suggestion by pressing enter. Also fixed the infinite looping of the progress spinner. Signed-off-by: Gaurav --- .../AutoCompleteTextFieldComponent.java | 53 +++++++++++-------- .../TextFieldSuggestionBox.java | 10 ++++ .../AutoCompleteTextFieldConnector.java | 2 + .../TextFieldSuggestionBoxServerRpc.java | 12 ++++- 4 files changed, 54 insertions(+), 23 deletions(-) diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/AutoCompleteTextFieldComponent.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/AutoCompleteTextFieldComponent.java index e3d766391..24f69d2fc 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/AutoCompleteTextFieldComponent.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/AutoCompleteTextFieldComponent.java @@ -13,6 +13,7 @@ import java.util.List; import java.util.concurrent.Executor; import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; import org.eclipse.hawkbit.repository.rsql.RsqlValidationOracle; import org.eclipse.hawkbit.ui.common.builder.TextFieldBuilder; @@ -24,8 +25,9 @@ import org.eclipse.hawkbit.ui.utils.SPUIStyleDefinitions; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.vaadin.spring.events.EventBus; +import org.vaadin.spring.events.EventScope; +import org.vaadin.spring.events.annotation.EventBusListenerMethod; -import com.vaadin.event.ShortcutListener; import com.vaadin.server.FontAwesome; import com.vaadin.shared.ui.label.ContentMode; import com.vaadin.spring.annotation.SpringComponent; @@ -85,9 +87,27 @@ public class AutoCompleteTextFieldComponent extends HorizontalLayout { */ @PostConstruct public void postConstruct() { + eventBus.subscribe(this); new TextFieldSuggestionBox(rsqlValidationOracle, this).extend(queryTextField); } + @PreDestroy + void destroy() { + eventBus.unsubscribe(this); + } + + @EventBusListenerMethod(scope = EventScope.SESSION) + void onEvent(final CustomFilterUIEvent custFUIEvent) { + if (custFUIEvent == CustomFilterUIEvent.UPDATE_TARGET_FILTER_SEARCH_ICON) { + validationIcon.setValue(FontAwesome.CHECK_CIRCLE.getHtml()); + if (!isValidationError()) { + validationIcon.setStyleName(SPUIStyleDefinitions.SUCCESS_ICON); + } else { + validationIcon.setStyleName(SPUIStyleDefinitions.ERROR_ICON); + } + } + } + /** * Clears the textfield and resets the validation icon. */ @@ -181,7 +201,6 @@ public class AutoCompleteTextFieldComponent extends HorizontalLayout { textField.setTextChangeEventMode(TextChangeEventMode.EAGER); textField.setImmediate(true); textField.setTextChangeTimeout(100); - textField.addShortcutListener(new EnterShortCutListener()); return textField; } @@ -214,27 +233,17 @@ public class AutoCompleteTextFieldComponent extends HorizontalLayout { } } - private final class EnterShortCutListener extends ShortcutListener { + /** + * Sets the spinner as progress indicator. + */ + public void showValidationInProgress() { + validationIcon.setValue(null); + validationIcon.addStyleName("show-status-label"); + validationIcon.setStyleName(SPUIStyleDefinitions.TARGET_FILTER_SEARCH_PROGRESS_INDICATOR_STYLE); + } - private static final long serialVersionUID = 1L; - - public EnterShortCutListener() { - super("Enter", KeyCode.ENTER, new int[0]); - } - - @Override - public void handleAction(final Object sender, final Object target) { - if (!isValidationError()) { - showValidationInProgress(); - executor.execute(new StatusCircledAsync(UI.getCurrent())); - } - } - - private void showValidationInProgress() { - validationIcon.setValue(null); - validationIcon.addStyleName("show-status-label"); - validationIcon.setStyleName(SPUIStyleDefinitions.TARGET_FILTER_SEARCH_PROGRESS_INDICATOR_STYLE); - } + public Executor getExecutor() { + return executor; } /** diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/TextFieldSuggestionBox.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/TextFieldSuggestionBox.java index 3c3b63bee..51dae8561 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/TextFieldSuggestionBox.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/TextFieldSuggestionBox.java @@ -20,6 +20,7 @@ import org.eclipse.hawkbit.ui.filtermanagement.client.TextFieldSuggestionBoxServ import com.vaadin.server.AbstractExtension; import com.vaadin.ui.TextField; +import com.vaadin.ui.UI; /** * Extension for the AutoCompleteTexfield. @@ -64,6 +65,15 @@ public class TextFieldSuggestionBox extends AbstractExtension implements TextFie getRpcProxy(TextFieldSuggestionBoxClientRpc.class).showSuggestions(mapToDto(suggest.getSuggestionContext())); } + @Override + public void executeQuery(final String text, final int cursor) { + if (!autoCompleteTextFieldComponent.isValidationError()) { + autoCompleteTextFieldComponent.showValidationInProgress(); + autoCompleteTextFieldComponent.getExecutor() + .execute(autoCompleteTextFieldComponent.new StatusCircledAsync(UI.getCurrent())); + } + } + private static SuggestionContextDto mapToDto(final SuggestionContext suggestionContext) { return new SuggestionContextDto(suggestionContext.getCursorPosition(), suggestionContext.getSuggestions().stream() diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/client/AutoCompleteTextFieldConnector.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/client/AutoCompleteTextFieldConnector.java index 949b2a39d..8b5812bf1 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/client/AutoCompleteTextFieldConnector.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/client/AutoCompleteTextFieldConnector.java @@ -82,6 +82,8 @@ public class AutoCompleteTextFieldConnector extends AbstractExtensionConnector { public void onKeyUp(final KeyUpEvent event) { if (panel.isAttached()) { handlePanelEventDelegation(event); + } else if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER) { + rpc.executeQuery(textFieldWidget.getValue(), textFieldWidget.getCursorPos()); } else { doAskForSuggestion(); } diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/client/TextFieldSuggestionBoxServerRpc.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/client/TextFieldSuggestionBoxServerRpc.java index 51388a0a3..c1fc5ca8b 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/client/TextFieldSuggestionBoxServerRpc.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/client/TextFieldSuggestionBoxServerRpc.java @@ -15,7 +15,6 @@ import com.vaadin.shared.communication.ServerRpc; * make client to server calls in Vaadin. Only void methods are allowed in * ServerRpc calls. */ -@FunctionalInterface public interface TextFieldSuggestionBoxServerRpc extends ServerRpc { /** @@ -31,4 +30,15 @@ public interface TextFieldSuggestionBoxServerRpc extends ServerRpc { * the current cursor position */ void suggest(final String text, final int cursor); + + /** + * Executes the query text to get the filtered data. + * + * @param text + * the current entered text e.g. in a text field to retrieve + * suggestion for + * @param cursor + * the current cursor position + */ + void executeQuery(final String text, final int cursor); } From 903e4103aacd611f5f5306e1ebed6adc973844b6 Mon Sep 17 00:00:00 2001 From: Gaurav Date: Wed, 7 Sep 2016 12:39:03 +0200 Subject: [PATCH 04/11] Fixed the compilation error. Signed-off-by: Gaurav --- .../ui/filtermanagement/AutoCompleteTextFieldComponent.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/AutoCompleteTextFieldComponent.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/AutoCompleteTextFieldComponent.java index 7154442a4..24f69d2fc 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/AutoCompleteTextFieldComponent.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/AutoCompleteTextFieldComponent.java @@ -89,7 +89,6 @@ public class AutoCompleteTextFieldComponent extends HorizontalLayout { public void postConstruct() { eventBus.subscribe(this); new TextFieldSuggestionBox(rsqlValidationOracle, this).extend(queryTextField); -<<<<<<< HEAD } @PreDestroy @@ -107,9 +106,6 @@ public class AutoCompleteTextFieldComponent extends HorizontalLayout { validationIcon.setStyleName(SPUIStyleDefinitions.ERROR_ICON); } } -======= - queryTextField.addShortcutListener(new EnterShortCutListener()); ->>>>>>> branch 'feature_rsql_parser_suggestion' of https://github.com/bsinno/hawkbit/ } /** From 88fb28b587f4c20122ffbddd82e728511dc6c617 Mon Sep 17 00:00:00 2001 From: Gaurav Date: Wed, 7 Sep 2016 16:38:35 +0200 Subject: [PATCH 05/11] Fix for the targetFilterQuery not getting set while updating the filter. Signed-off-by: Gaurav --- .../eclipse/hawkbit/ui/filtermanagement/TargetFilterTable.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/TargetFilterTable.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/TargetFilterTable.java index 010e37bde..caf6976fd 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/TargetFilterTable.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/TargetFilterTable.java @@ -226,7 +226,7 @@ public class TargetFilterTable extends Table { final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement .findTargetFilterQueryByName(targetFilterName); filterManagementUIState.setFilterQueryValue(targetFilterQuery.getQuery()); - + filterManagementUIState.setTfQuery(targetFilterQuery); filterManagementUIState.setEditViewDisplayed(true); eventBus.publish(this, CustomFilterUIEvent.TARGET_FILTER_DETAIL_VIEW); } From 9bb8459f3704470b16712c66e62bd7ab0a2543d4 Mon Sep 17 00:00:00 2001 From: Michael Hirsch Date: Mon, 19 Sep 2016 13:23:18 +0200 Subject: [PATCH 06/11] add missing license header Signed-off-by: Michael Hirsch --- .../repository/jpa/rsql/RsqlParserValidationOracle.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RsqlParserValidationOracle.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RsqlParserValidationOracle.java index 8eb21400f..aa31bb4da 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RsqlParserValidationOracle.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RsqlParserValidationOracle.java @@ -1,3 +1,11 @@ +/** + * 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; From 848632bd0bee9a92b9e758e5a8eeaa5584eadb66 Mon Sep 17 00:00:00 2001 From: Michael Hirsch Date: Mon, 19 Sep 2016 15:00:25 +0200 Subject: [PATCH 07/11] add missing license header Signed-off-by: Michael Hirsch --- .../java/org/eclipse/hawkbit/ui/AppWidgetSet.gwt.xml | 12 ++++++++++-- .../filtermanagement/TextFieldSuggestionBox.gwt.xml | 10 ++++++++++ .../ui/filtermanagement/client/SuggestTokenDto.java | 8 ++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/AppWidgetSet.gwt.xml b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/AppWidgetSet.gwt.xml index 2141eb6b1..5479eab26 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/AppWidgetSet.gwt.xml +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/AppWidgetSet.gwt.xml @@ -1,6 +1,14 @@ - + diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/TextFieldSuggestionBox.gwt.xml b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/TextFieldSuggestionBox.gwt.xml index 43f76950d..c87a64a8a 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/TextFieldSuggestionBox.gwt.xml +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/TextFieldSuggestionBox.gwt.xml @@ -1,4 +1,14 @@ + diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/client/SuggestTokenDto.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/client/SuggestTokenDto.java index 8291fa0bf..9dbe07d36 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/client/SuggestTokenDto.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/client/SuggestTokenDto.java @@ -1,3 +1,11 @@ +/** + * 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.ui.filtermanagement.client; import java.io.Serializable; From ae583e49380fbdb7f41cf05db8fde5e592507f6d Mon Sep 17 00:00:00 2001 From: Jonathan Philip Knoblauch Date: Wed, 21 Sep 2016 17:47:19 +0200 Subject: [PATCH 08/11] Refactoring and adapted CSS styling to HawkBit Signed-off-by: Jonathan Philip Knoblauch --- .../org/eclipse/hawkbit/repository/rsql/SuggestToken.java | 7 ++----- .../repository/jpa/rsql/ParseExceptionWrapper.java | 3 ++- .../repository/jpa/rsql/RsqlParserValidationOracle.java | 4 +++- .../ui/filtermanagement/CreateOrUpdateFilterHeader.java | 8 +------- .../themes/hawkbit/customstyles/target-filter-query.scss | 2 +- 5 files changed, 9 insertions(+), 15 deletions(-) diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/SuggestToken.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/SuggestToken.java index 55f009adf..db560f17f 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/SuggestToken.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/SuggestToken.java @@ -26,10 +26,6 @@ public class SuggestToken { * the character position of the start of the token * @param end * the character position of the end of the token - * @param the - * token image name which is currently parsed, e.g. entered image - * name is {@code na} which the suggestion can be based on and - * e.g. filter the suggestions. * @param tokenImageName * the entered name of the token, e.g. could be the beginning of * the suggestion like 'na' or 'name' @@ -61,6 +57,7 @@ public class SuggestToken { @Override public String toString() { - return "SuggestToken [start=" + start + ", end=" + end + ", suggestion=" + suggestion + "]"; + return "SuggestToken [start=" + start + ", end=" + end + ", suggestion=" + suggestion + ", tokenImageName=" + + tokenImageName + "]"; } } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/ParseExceptionWrapper.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/ParseExceptionWrapper.java index 08978eaaa..b2f5a6e42 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/ParseExceptionWrapper.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/ParseExceptionWrapper.java @@ -19,7 +19,8 @@ import cz.jirutka.rsql.parser.ParseException; * A {@link ParseException} wrapper which allows to access the parsing * information from the exception using reflection due there is no other access * of this information. See issue for requesting feature - * {@link https://github.com/jirutka/rsql-parser/issues/22} + * https://github.com + * /jirutka/rsql-parser/issues/22 */ public class ParseExceptionWrapper { diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RsqlParserValidationOracle.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RsqlParserValidationOracle.java index aa31bb4da..641b9a291 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RsqlParserValidationOracle.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RsqlParserValidationOracle.java @@ -51,7 +51,9 @@ import cz.jirutka.rsql.parser.RSQLParserException; * {@link ParseException}. * * There is a feature request on the GitHub project - * {@link https://github.com/jirutka/rsql-parser/issues/22}. + * https://github.com + * /jirutka/rsql-parser/issues/22 + * */ public class RsqlParserValidationOracle implements RsqlValidationOracle { diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/CreateOrUpdateFilterHeader.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/CreateOrUpdateFilterHeader.java index 777185750..fd86f138f 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/CreateOrUpdateFilterHeader.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/CreateOrUpdateFilterHeader.java @@ -27,9 +27,9 @@ import org.eclipse.hawkbit.ui.filtermanagement.AutoCompleteTextFieldComponent.Fi import org.eclipse.hawkbit.ui.filtermanagement.event.CustomFilterUIEvent; import org.eclipse.hawkbit.ui.filtermanagement.state.FilterManagementUIState; import org.eclipse.hawkbit.ui.utils.I18N; -import org.eclipse.hawkbit.ui.utils.UIComponentIdProvider; import org.eclipse.hawkbit.ui.utils.SPUILabelDefinitions; import org.eclipse.hawkbit.ui.utils.SPUIStyleDefinitions; +import org.eclipse.hawkbit.ui.utils.UIComponentIdProvider; import org.eclipse.hawkbit.ui.utils.UINotification; import org.springframework.beans.factory.annotation.Autowired; import org.vaadin.spring.events.EventBus; @@ -363,12 +363,6 @@ public class CreateOrUpdateFilterHeader extends VerticalLayout implements Button return saveButton; } - /* - * (non-Javadoc) - * - * @see com.vaadin.ui.Button.ClickListener#buttonClick(com.vaadin.ui.Button. - * ClickEvent) - */ @Override public void buttonClick(final ClickEvent event) { if (UIComponentIdProvider.CUSTOM_FILTER_SAVE_ICON.equals(event.getComponent().getId()) diff --git a/hawkbit-ui/src/main/resources/VAADIN/themes/hawkbit/customstyles/target-filter-query.scss b/hawkbit-ui/src/main/resources/VAADIN/themes/hawkbit/customstyles/target-filter-query.scss index 102f6a3c9..2344b35cc 100644 --- a/hawkbit-ui/src/main/resources/VAADIN/themes/hawkbit/customstyles/target-filter-query.scss +++ b/hawkbit-ui/src/main/resources/VAADIN/themes/hawkbit/customstyles/target-filter-query.scss @@ -23,7 +23,7 @@ } .gwt-MenuBar-autocomplete .gwt-MenuItem-selected { - background-color: #197de1 !important; + background-color: $hawkbit-primary-color; background-image: linear-gradient(to bottom, #1b87e3 2%, #166ed5 98%) !important; color: #ecf2f8 !important; text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.05) !important; From 97df8620ae819009a8483008b76b19a6ebc327fa Mon Sep 17 00:00:00 2001 From: Gaurav Date: Mon, 26 Sep 2016 16:38:51 +0200 Subject: [PATCH 09/11] Added the search icon for executing the filter query. Signed-off-by: Gaurav --- .../CreateOrUpdateFilterHeader.java | 24 ++++++++++++++++++- .../ui/utils/UIComponentIdProvider.java | 4 ++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/CreateOrUpdateFilterHeader.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/CreateOrUpdateFilterHeader.java index fd86f138f..b9e98c9f0 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/CreateOrUpdateFilterHeader.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/CreateOrUpdateFilterHeader.java @@ -52,6 +52,7 @@ import com.vaadin.ui.HorizontalLayout; import com.vaadin.ui.Label; import com.vaadin.ui.Link; import com.vaadin.ui.TextField; +import com.vaadin.ui.UI; import com.vaadin.ui.VerticalLayout; import com.vaadin.ui.themes.ValoTheme; @@ -111,6 +112,8 @@ public class CreateOrUpdateFilterHeader extends VerticalLayout implements Button private Link helpLink; + private Button searchIcon; + private String oldFilterName; private String oldFilterQuery; @@ -195,6 +198,7 @@ public class CreateOrUpdateFilterHeader extends VerticalLayout implements Button nameTextField.setWidth(380, Unit.PIXELS); saveButton = createSaveButton(); + searchIcon = createSearchIcon(); helpLink = SPUIComponentProvider.getHelpLink(uiProperties.getLinks().getDocumentation().getTargetfilterView()); @@ -292,7 +296,7 @@ public class CreateOrUpdateFilterHeader extends VerticalLayout implements Button final HorizontalLayout iconLayout = new HorizontalLayout(); iconLayout.setSizeUndefined(); iconLayout.setSpacing(false); - iconLayout.addComponents(helpLink, saveButton); + iconLayout.addComponents(helpLink, searchIcon, saveButton); final HorizontalLayout queryLayout = new HorizontalLayout(); queryLayout.setSizeUndefined(); @@ -324,10 +328,12 @@ public class CreateOrUpdateFilterHeader extends VerticalLayout implements Button if (validationFailed || (isNameAndQueryEmpty(nameTextField.getValue(), query) || (query.equals(oldFilterQuery) && nameTextField.getValue().equals(oldFilterName)))) { saveButton.setEnabled(false); + searchIcon.setEnabled(false); } else { if (hasSavePermission()) { saveButton.setEnabled(true); } + searchIcon.setEnabled(true); } } @@ -363,6 +369,22 @@ public class CreateOrUpdateFilterHeader extends VerticalLayout implements Button return saveButton; } + private Button createSearchIcon() { + searchIcon = SPUIComponentProvider.getButton(UIComponentIdProvider.FILTER_SEARCH_ICON_ID, "", "", null, false, + FontAwesome.SEARCH, SPUIButtonStyleSmallNoBorder.class); + searchIcon.addClickListener(event -> onSearchIconClick()); + searchIcon.setEnabled(false); + searchIcon.setData(false); + return searchIcon; + } + + private void onSearchIconClick() { + if (!queryTextField.isValidationError()) { + queryTextField.showValidationInProgress(); + queryTextField.getExecutor().execute(queryTextField.new StatusCircledAsync(UI.getCurrent())); + } + } + @Override public void buttonClick(final ClickEvent event) { if (UIComponentIdProvider.CUSTOM_FILTER_SAVE_ICON.equals(event.getComponent().getId()) diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIComponentIdProvider.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIComponentIdProvider.java index e3475d20d..09540f83e 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIComponentIdProvider.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/utils/UIComponentIdProvider.java @@ -876,6 +876,10 @@ public final class UIComponentIdProvider { * ID for download anonymous checkbox */ public static final String DOWNLOAD_ANONYMOUS_CHECKBOX = "downloadanonymouscheckbox"; + /** + * Id of custom filter query search Icon. + */ + public static final String FILTER_SEARCH_ICON_ID = "filter.search.icon"; /** * /* Private Constructor. From fa3cee62bf44f06aa68f530ca4b46840adc54f15 Mon Sep 17 00:00:00 2001 From: Jonathan Philip Knoblauch Date: Fri, 30 Sep 2016 13:04:58 +0200 Subject: [PATCH 10/11] Refactoring after review Signed-off-by: Jonathan Philip Knoblauch --- .../jpa/rsql/RsqlParserValidationOracle.java | 129 +++++++++--------- 1 file changed, 68 insertions(+), 61 deletions(-) diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RsqlParserValidationOracle.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RsqlParserValidationOracle.java index 641b9a291..a3076b6f0 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RsqlParserValidationOracle.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RsqlParserValidationOracle.java @@ -112,39 +112,38 @@ public class RsqlParserValidationOracle implements RsqlValidationOracle { private static List getNextTokens(final Exception e) { final ParseException parseException = findParseException(e); - if (parseException != null) { - final List listTokens = new ArrayList<>(); - final ParseExceptionWrapper parseExceptionWrapper = new ParseExceptionWrapper(parseException); - final int[][] expectedTokenSequence = parseExceptionWrapper.getExpectedTokenSequence(); - final TokenWrapper currentToken = parseExceptionWrapper.getCurrentToken(); - 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> handleFieldTokenSuggestion = handleFieldTokenSuggestion( - currentTokenImageName, nextTokenBeginColumn, currentTokenEndColumn); - if (handleFieldTokenSuggestion.isPresent()) { - return handleFieldTokenSuggestion.get(); - } - } - - for (final int[] is : expectedTokenSequence) { - for (final int i : is) { - final Collection tokenImage = TokenDescription.getTokenImage(i); - if (tokenImage != null && !tokenImage.isEmpty()) { - tokenImage.forEach(image -> listTokens.add(new SuggestToken(currentTokenEndColumn + 1, - nextTokenBeginColumn + image.length(), null, image))); - } - } - } - return listTokens; + if (parseException == null) { + return Collections.emptyList(); } - return Collections.emptyList(); + final List listTokens = new ArrayList<>(); + final ParseExceptionWrapper parseExceptionWrapper = new ParseExceptionWrapper(parseException); + final int[][] expectedTokenSequence = parseExceptionWrapper.getExpectedTokenSequence(); + final TokenWrapper currentToken = parseExceptionWrapper.getCurrentToken(); + 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> handleFieldTokenSuggestion = handleFieldTokenSuggestion( + currentTokenImageName, nextTokenBeginColumn, currentTokenEndColumn); + if (handleFieldTokenSuggestion.isPresent()) { + return handleFieldTokenSuggestion.get(); + } + } + + for (final int[] is : expectedTokenSequence) { + for (final int i : is) { + final Collection tokenImage = TokenDescription.getTokenImage(i); + if (tokenImage != null && !tokenImage.isEmpty()) { + tokenImage.forEach(image -> listTokens.add(new SuggestToken(currentTokenEndColumn + 1, + nextTokenBeginColumn + image.length(), null, image))); + } + } + } + return listTokens; } private static Optional> handleFieldTokenSuggestion(final String currentTokenImageName, @@ -193,9 +192,9 @@ public class RsqlParserValidationOracle implements RsqlValidationOracle { } private static ParseException findParseException(final Throwable e) { - if (e != null && e instanceof ParseException) { + if (e instanceof ParseException) { return (ParseException) e; - } else if (e != null && e.getCause() != null) { + } else if (e.getCause() != null) { return findParseException(e.getCause()); } return null; @@ -203,24 +202,31 @@ public class RsqlParserValidationOracle implements RsqlValidationOracle { private static String getCustomMessage(final String message, final List expectedTokens) { String builder = message; - if (message.contains(":")) { - builder = message.substring(message.indexOf(':') + 1, message.length()); - if (builder.indexOf("Was expecting") != -1) { - builder = builder.substring(0, builder.lastIndexOf("Was expecting")); - } - if (null != expectedTokens && !expectedTokens.isEmpty()) { - 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("<", " "); + + 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 (expectedTokens != null && !expectedTokens.isEmpty()) { + 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 TOKEN_MAP = ArrayListMultimap.create(); @@ -264,18 +270,17 @@ public class RsqlParserValidationOracle implements RsqlValidationOracle { } private static boolean hasSubEntries(final String tokenImageName) { - final String tmpTokenName; + String tmpTokenName = tokenImageName; if (tokenImageName.contains(".")) { final String[] split = tokenImageName.split("\\."); - if (split.length > 0) { - tmpTokenName = split[0]; - } else { + if (split.length <= 0) { return false; } - } else { - tmpTokenName = tokenImageName; + tmpTokenName = split[0]; } - return Arrays.stream(TargetFields.values()).filter(field -> field.toString().equalsIgnoreCase(tmpTokenName)) + final String finalTmpTokenName = tmpTokenName; + return Arrays.stream(TargetFields.values()) + .filter(field -> field.toString().equalsIgnoreCase(finalTmpTokenName)) .map(field -> field.getSubEntityAttributes()).flatMap(subentities -> subentities.stream()) .count() > 0; } @@ -296,16 +301,18 @@ public class RsqlParserValidationOracle implements RsqlValidationOracle { } private static boolean containsValue(final String imageName) { - if (imageName.contains(".")) { - final String[] split = imageName.split("\\."); - if (split.length > 1 && FIELD_NAMES.contains(split[0].toLowerCase())) { - return SUB_NAMES.get(split[0].toLowerCase()).stream() - .filter(subname -> new String(split[0] + "." + subname).equalsIgnoreCase(imageName)) - .count() > 0; - } + if (!imageName.contains(".")) { + return FIELD_NAMES.stream().filter(value -> value.equalsIgnoreCase(imageName)).count() > 0; + } + final String[] split = imageName.split("\\."); + if (split.length > 1 && FIELD_NAMES.contains(split[0].toLowerCase())) { + return SUB_NAMES.get(split[0].toLowerCase()).stream() + .filter(subname -> new String(split[0] + "." + subname).equalsIgnoreCase(imageName)) + .count() > 0; } return FIELD_NAMES.stream().filter(value -> value.equalsIgnoreCase(imageName)).count() > 0; } + } } From 8c7af7d28dca14b0814b4b959c1d17ae1b1ae45b Mon Sep 17 00:00:00 2001 From: Jonathan Philip Knoblauch Date: Tue, 4 Oct 2016 10:16:08 +0200 Subject: [PATCH 11/11] More refactoring (added early returns) Signed-off-by: Jonathan Philip Knoblauch --- .../jpa/rsql/RsqlParserValidationOracle.java | 7 +++++-- .../CreateOrUpdateFilterHeader.java | 10 +++++++--- .../client/AutoCompleteTextFieldConnector.java | 18 ++++++++++-------- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RsqlParserValidationOracle.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RsqlParserValidationOracle.java index a3076b6f0..adec49389 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RsqlParserValidationOracle.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RsqlParserValidationOracle.java @@ -92,9 +92,12 @@ public class RsqlParserValidationOracle implements RsqlValidationOracle { } private static Collection getLogicalOperatorSuggestion(final String rsqlQuery) { - final int currentQueryLength = rsqlQuery.length(); - // only return and/or suggestion when there is a space at the end + 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 tokenImages = TokenDescription.getTokenImage(TokenDescription.LOGICAL_OP); final List logicalOps = new ArrayList<>(tokenImages.size()); for (final String tokenImage : tokenImages) { diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/CreateOrUpdateFilterHeader.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/CreateOrUpdateFilterHeader.java index b9e98c9f0..f508aa436 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/CreateOrUpdateFilterHeader.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/CreateOrUpdateFilterHeader.java @@ -379,10 +379,14 @@ public class CreateOrUpdateFilterHeader extends VerticalLayout implements Button } private void onSearchIconClick() { - if (!queryTextField.isValidationError()) { - queryTextField.showValidationInProgress(); - queryTextField.getExecutor().execute(queryTextField.new StatusCircledAsync(UI.getCurrent())); + + if (queryTextField.isValidationError()) { + return; } + + queryTextField.showValidationInProgress(); + queryTextField.getExecutor().execute(queryTextField.new StatusCircledAsync(UI.getCurrent())); + } @Override diff --git a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/client/AutoCompleteTextFieldConnector.java b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/client/AutoCompleteTextFieldConnector.java index 8b5812bf1..80f7714b7 100644 --- a/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/client/AutoCompleteTextFieldConnector.java +++ b/hawkbit-ui/src/main/java/org/eclipse/hawkbit/ui/filtermanagement/client/AutoCompleteTextFieldConnector.java @@ -54,14 +54,16 @@ public class AutoCompleteTextFieldConnector extends AbstractExtensionConnector { @Override public void showSuggestions(final SuggestionContextDto suggestContext) { select.clearItems(); - if (suggestContext != null) { - final List suggestions = suggestContext.getSuggestions(); - if (suggestions != null && !suggestions.isEmpty()) { - select.addItems(suggestions, textFieldWidget, panel, rpc); - panel.showRelativeTo(textFieldWidget); - select.moveSelectionDown(); - return; - } + if (suggestContext == null) { + panel.hide(); + return; + } + final List suggestions = suggestContext.getSuggestions(); + if (suggestions != null && !suggestions.isEmpty()) { + select.addItems(suggestions, textFieldWidget, panel, rpc); + panel.showRelativeTo(textFieldWidget); + select.moveSelectionDown(); + return; } panel.hide(); }