introduce autocomplete textfield with RSQL suggestion

Signed-off-by: Michael Hirsch <michael.hirsch@bosch-si.com>
This commit is contained in:
Michael Hirsch
2016-08-26 13:26:37 +02:00
parent 2f415c4839
commit 79b1ae8728
28 changed files with 1735 additions and 462 deletions

View File

@@ -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);
}

View File

@@ -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 + "]";
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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,

View File

@@ -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() + "]";
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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) {

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}

View File

@@ -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 + "]";
}
}

View File

@@ -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 + "]";
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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 {

View File

@@ -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 -->