introduce autocomplete textfield with RSQL suggestion
Signed-off-by: Michael Hirsch <michael.hirsch@bosch-si.com>
This commit is contained in:
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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 + "]";
|
||||
}
|
||||
}
|
||||
@@ -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<SuggestToken> 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<SuggestToken> suggestions) {
|
||||
this.rsqlQuery = rsqlQuery;
|
||||
this.cursorPosition = cursorPosition;
|
||||
this.suggestions = suggestions;
|
||||
}
|
||||
|
||||
public List<SuggestToken> 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<SuggestToken> suggestions) {
|
||||
this.suggestions = suggestions;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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<? extends ParseException> 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() + "]";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<SuggestToken> expectedTokens = new ArrayList<>();
|
||||
final ValidationOracleContext context = new ValidationOracleContext();
|
||||
context.setSyntaxError(true);
|
||||
final SuggestionContext suggestionContext = new SuggestionContext();
|
||||
context.setSuggestionContext(suggestionContext);
|
||||
final SyntaxErrorContext errorContext = new SyntaxErrorContext();
|
||||
context.setSyntaxErrorContext(errorContext);
|
||||
|
||||
try {
|
||||
targetManagement.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<? extends SuggestToken> 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<String> tokenImages = TokenDescription.getTokenImage(TokenDescription.LOGICAL_OP);
|
||||
final List<SuggestToken> logicalOps = new ArrayList<>(tokenImages.size());
|
||||
for (final String tokenImage : tokenImages) {
|
||||
logicalOps.add(new SuggestToken(currentQueryLength, currentQueryLength + tokenImage.length(), null,
|
||||
tokenImage));
|
||||
}
|
||||
return logicalOps;
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
private static void setExceptionDetails(final Exception ex, final List<SuggestToken> expectedTokens) {
|
||||
expectedTokens.addAll(getNextTokens(ex));
|
||||
}
|
||||
|
||||
private static List<SuggestToken> getNextTokens(final Exception e) {
|
||||
final ParseException parseException = findParseException(e);
|
||||
if (parseException != null) {
|
||||
final List<SuggestToken> 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<List<SuggestToken>> handleFieldTokenSuggestion = handleFieldTokenSuggestion(
|
||||
currentTokenImageName, nextTokenBeginColumn, currentTokenEndColumn);
|
||||
if (handleFieldTokenSuggestion.isPresent()) {
|
||||
return handleFieldTokenSuggestion.get();
|
||||
}
|
||||
}
|
||||
|
||||
for (final int[] is : expectedTokenSequence) {
|
||||
for (final int i : is) {
|
||||
final Collection<String> 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<List<SuggestToken>> handleFieldTokenSuggestion(final String currentTokenImageName,
|
||||
final int nextTokenBeginColumn, final int currentTokenEndColumn) {
|
||||
final boolean containsDot = currentTokenImageName.indexOf('.') != -1;
|
||||
if (shouldSuggestTopLevelFieldNames(currentTokenImageName, containsDot)) {
|
||||
return Optional
|
||||
.of(FieldNameDescription.toTopSuggestToken(nextTokenBeginColumn - currentTokenImageName.length(),
|
||||
nextTokenBeginColumn + currentTokenImageName.length(), currentTokenImageName));
|
||||
} else if (shouldSuggestDotToken(currentTokenImageName, containsDot)) {
|
||||
return Optional.of(
|
||||
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<List<SuggestToken>> handleSubtokenSuggestion(final String currentTokenImageName,
|
||||
final int nextTokenBeginColumn) {
|
||||
final String[] split = currentTokenImageName.split("\\.");
|
||||
for (final String string : split) {
|
||||
if (FieldNameDescription.containsValue(string)) {
|
||||
final String subTokenImage = split.length > 1 ? split[1] : null;
|
||||
final int subTokenBegin = nextTokenBeginColumn + currentTokenImageName.indexOf('.') + 1;
|
||||
return Optional.of(FieldNameDescription.toSubSuggestToken(subTokenBegin, subTokenBegin + 1, string,
|
||||
subTokenImage));
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private static ParseException findParseException(final Throwable e) {
|
||||
if (e != 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<SuggestToken> 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<Integer, String> TOKEN_MAP = ArrayListMultimap.create();
|
||||
|
||||
private static final int LOGICAL_OP = 8;
|
||||
private static final int COMPARATOR = 12;
|
||||
|
||||
static {
|
||||
TOKEN_MAP.put(LOGICAL_OP, "and");
|
||||
TOKEN_MAP.put(LOGICAL_OP, "or");
|
||||
TOKEN_MAP.put(COMPARATOR, "==");
|
||||
TOKEN_MAP.put(COMPARATOR, "!=");
|
||||
TOKEN_MAP.put(COMPARATOR, "=ge=");
|
||||
TOKEN_MAP.put(COMPARATOR, "=le=");
|
||||
TOKEN_MAP.put(COMPARATOR, "=gt=");
|
||||
TOKEN_MAP.put(COMPARATOR, "=lt=");
|
||||
TOKEN_MAP.put(COMPARATOR, "=in=");
|
||||
TOKEN_MAP.put(COMPARATOR, "=out=");
|
||||
}
|
||||
|
||||
private TokenDescription() {
|
||||
|
||||
}
|
||||
|
||||
private static Collection<String> getTokenImage(final int tokenIndex) {
|
||||
return TOKEN_MAP.get(tokenIndex);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class FieldNameDescription {
|
||||
|
||||
private static final Set<String> FIELD_NAMES = Arrays.stream(TargetFields.values())
|
||||
.map(field -> field.toString().toLowerCase()).collect(Collectors.toSet());
|
||||
|
||||
private static final Map<String, List<String>> SUB_NAMES = Arrays.stream(TargetFields.values()).collect(
|
||||
Collectors.toMap(field -> field.toString().toLowerCase(), 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<SuggestToken> toTopSuggestToken(final int beginToken, final int endToken,
|
||||
final String tokenImageName) {
|
||||
return FIELD_NAMES.stream()
|
||||
.map(field -> new SuggestToken(beginToken, endToken, tokenImageName, field.toLowerCase()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private static List<SuggestToken> toSubSuggestToken(final int beginToken, final int endToken,
|
||||
final String topToken, final String tokenImageName) {
|
||||
return Arrays.stream(TargetFields.values()).filter(field -> field.toString().equalsIgnoreCase(topToken))
|
||||
.map(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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<String> 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<String> 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<String> 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<String> currentSuggestions = getSuggestions(rsqlQuery);
|
||||
assertThat(currentSuggestions).containsOnly(NAME_VERSION_SUGGESTIONS);
|
||||
}
|
||||
|
||||
private List<String> getSuggestions(final String rsqlQuery) {
|
||||
final ValidationOracleContext suggest = rsqlValidationOracle.suggest(rsqlQuery, -1);
|
||||
final List<String> currentSuggestions = suggest.getSuggestionContext().getSuggestions().stream()
|
||||
.map(suggestion -> suggestion.getSuggestion()).collect(Collectors.toList());
|
||||
return currentSuggestions;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,44 +1,33 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
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
|
||||
|
||||
-->
|
||||
<!-- 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 -->
|
||||
<!DOCTYPE module PUBLIC "-//Google Inc.//DTD Google Web Toolkit 2.5.1//EN" "http://google-web-toolkit.googlecode.com/svn/tags/2.5.1/distro-source/core/src/gwt-module.dtd">
|
||||
<module>
|
||||
<!-- This file is automatically updated based on new dependencies by the
|
||||
goal "vaadin:update-widgetset". -->
|
||||
<!-- This file is automatically updated based on new dependencies by the goal "vaadin:update-widgetset". -->
|
||||
|
||||
<!-- Inherit DefaultWidgetSet -->
|
||||
<inherits name="com.vaadin.DefaultWidgetSet" />
|
||||
<!-- Inherit DefaultWidgetSet -->
|
||||
<inherits name="com.vaadin.DefaultWidgetSet" />
|
||||
|
||||
<inherits
|
||||
name="org.vaadin.hene.flexibleoptiongroup.widgetset.FlexibleOptionGroupWidgetset" />
|
||||
<inherits name="org.vaadin.hene.flexibleoptiongroup.widgetset.FlexibleOptionGroupWidgetset" />
|
||||
|
||||
<inherits name="org.vaadin.tokenfield.TokenfieldWidgetset" />
|
||||
<inherits name="org.vaadin.tokenfield.TokenfieldWidgetset" />
|
||||
|
||||
|
||||
<inherits
|
||||
name="org.vaadin.alump.distributionbar.gwt.DistributionBarWidgetset" />
|
||||
<inherits name="org.vaadin.alump.distributionbar.gwt.DistributionBarWidgetset" />
|
||||
|
||||
|
||||
|
||||
<inherits name="org.vaadin.peter.contextmenu.ContextmenuWidgetset" />
|
||||
<inherits name="org.vaadin.peter.contextmenu.ContextmenuWidgetset" />
|
||||
|
||||
<inherits
|
||||
name="org.vaadin.hene.flexibleoptiongroup.widgetset.FlexibleOptionGroupWidgetset" />
|
||||
<inherits name="org.vaadin.hene.flexibleoptiongroup.widgetset.FlexibleOptionGroupWidgetset" />
|
||||
|
||||
<inherits
|
||||
name="org.vaadin.alump.distributionbar.gwt.DistributionBarWidgetset" />
|
||||
<inherits name="org.vaadin.alump.distributionbar.gwt.DistributionBarWidgetset" />
|
||||
|
||||
<inherits name="org.eclipse.hawkbit.ui.customrenderers.CustomRendererWidgetSet" />
|
||||
<inherits name="org.eclipse.hawkbit.ui.customrenderers.CustomRendererWidgetSet" />
|
||||
<inherits name="org.eclipse.hawkbit.ui.filtermanagement.TextFieldSuggestionBox" />
|
||||
|
||||
<inherits name="org.vaadin.hene.flexibleoptiongroup.widgetset.FlexibleOptionGroupWidgetset" />
|
||||
|
||||
<inherits name="org.vaadin.hene.flexibleoptiongroup.widgetset.FlexibleOptionGroupWidgetset" />
|
||||
|
||||
<inherits name="org.vaadin.alump.distributionbar.gwt.DistributionBarWidgetset" />
|
||||
<inherits name="org.vaadin.alump.distributionbar.gwt.DistributionBarWidgetset" />
|
||||
</module>
|
||||
|
||||
@@ -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<FilterQueryChangeListener> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<TargetFilterQuery> 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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<String> 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<String> expectedTokens,
|
||||
final ValidationResult result, final TokenDescription tokenDesc) {
|
||||
for (final Integer node : getNextTokens(ex)) {
|
||||
if (node != 12) {
|
||||
expectedTokens.add(tokenDesc.getTokenImage()[node]);
|
||||
}
|
||||
}
|
||||
final List<String> 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<String> processExpectedTokens(final List<Integer> expectedTokens) {
|
||||
final List<String> expectToken = new ArrayList<>();
|
||||
if (expectedTokens.size() == 2 && expectedTokens.contains(9) && expectedTokens.contains(4)) {
|
||||
final List<String> 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<Integer> getNextTokens(final Exception e) {
|
||||
Throwable parseException = e.getCause();
|
||||
final List<Integer> 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<String> 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module>
|
||||
<inherits name="com.vaadin.DefaultWidgetSet" />
|
||||
|
||||
|
||||
<inherits name="org.eclipse.hawkbit.ui.AppWidgetSet" />
|
||||
|
||||
<inherits name="org.vaadin.peter.contextmenu.ContextmenuWidgetset" />
|
||||
|
||||
<inherits name="org.vaadin.tokenfield.TokenfieldWidgetset" />
|
||||
|
||||
<inherits name="org.eclipse.hawkbit.ui.customrenderers.CustomRendererWidgetSet" />
|
||||
|
||||
<inherits name="org.vaadin.hene.flexibleoptiongroup.widgetset.FlexibleOptionGroupWidgetset" />
|
||||
|
||||
<inherits name="org.vaadin.alump.distributionbar.gwt.DistributionBarWidgetset" />
|
||||
</module>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 = { "<EOF>", "\" \"", "\"\\t\"", "<ALPHA>", "<UNRESERVED_STR>",
|
||||
"<SINGLE_QUOTED_STR>", "<DOUBLE_QUOTED_STR>", "<AND>", "<OR>", "\"(\"", "\")\"", "<==>|<!=>", ">=|<=", };
|
||||
|
||||
public String[] getTokenImage() {
|
||||
return Arrays.copyOf(TOKEN_IMAGE, TOKEN_IMAGE.length);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<String> 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<String> getExpectedTokens() {
|
||||
return expectedTokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param expectedTokens
|
||||
* the expectedTokens to set
|
||||
*/
|
||||
public void setExpectedTokens(final List<String> 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<SuggestTokenDto> 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());
|
||||
}
|
||||
}
|
||||
@@ -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 + "]";
|
||||
}
|
||||
}
|
||||
@@ -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<SuggestTokenDto> 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<SuggestTokenDto> suggestions) {
|
||||
this.cursorPosition = cursorPosition;
|
||||
this.suggestions = suggestions;
|
||||
}
|
||||
|
||||
public List<SuggestTokenDto> getSuggestions() {
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
public int getCursorPosition() {
|
||||
return cursorPosition;
|
||||
}
|
||||
|
||||
public void setCursorPosition(final int cursorPosition) {
|
||||
this.cursorPosition = cursorPosition;
|
||||
}
|
||||
|
||||
public void setSuggestions(final List<SuggestTokenDto> suggestions) {
|
||||
this.suggestions = suggestions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SuggestionContextDto [cursorPosition=" + cursorPosition + ", suggestions=" + suggestions + "]";
|
||||
}
|
||||
}
|
||||
@@ -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<String, TokenStartEnd> 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<SuggestTokenDto> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
2
pom.xml
2
pom.xml
@@ -105,7 +105,7 @@
|
||||
<commons-lang3.version>3.4</commons-lang3.version>
|
||||
<commons-collections4.version>4.1</commons-collections4.version>
|
||||
<json.version>20141113</json.version>
|
||||
<rsql-parser.version>2.0.0</rsql-parser.version>
|
||||
<rsql-parser.version>2.1.0</rsql-parser.version>
|
||||
<!-- Misc libraries versions - END -->
|
||||
|
||||
<!-- Release - START -->
|
||||
|
||||
Reference in New Issue
Block a user