Skip to content

Commit

Permalink
Merge pull request #522 from pranavrd/feat/string-based-search
Browse files Browse the repository at this point in the history
feat: string based entities search
  • Loading branch information
pranavrd authored Jul 1, 2024
2 parents 0c6e08d + 485b27e commit 90c7824
Show file tree
Hide file tree
Showing 9 changed files with 704 additions and 152 deletions.
10 changes: 8 additions & 2 deletions docs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ paths:

- name: q
in: query
description: 'The attribute query is used for querying <b> allowed quantitative</b> properties. <br/> Used to query on a value of a resource attribute using `<`,`>`,`<=`,`>=`,`!=`, `==` operators. <br/> For e.g, attribute > value, attribute < value, attribute >= value, attribute <= value, attribute != value and attribute == value. <br/> Allowed values for all operators is double. <br/> For the operator `==` if the query is on `id` then the only value allowed is an `data exchange ID` of a resource.'
description: 'The attribute query is used for querying <b> allowed quantitative</b> properties. <br/> Used to query on a value of a resource attribute using `<`,`>`,`<=`,`>=`,`!=`, `==` operators. <br/> Additionally, a user can search a resource attribute with a alpha-numeric value using `==` operator. For e.g, attribute > value, attribute < value, attribute >= value, attribute <= value, attribute != value and attribute == value. <br/> Allowed values for all operators is double, and as a special case alpha-nmeric values are allowed for `==`. <br/> For the operator `==` if the query is on `id` then the only value allowed is an `data exchange ID` of a resource.'
schema:
type: string
maxLength: 512
Expand Down Expand Up @@ -405,6 +405,12 @@ paths:
curl --location -g --request GET 'https://example.com/ngsi-ld/v1/entities?id=UUID&geoproperty=location&georel=near;maxDistance=10&geometry=Point&coordinates=[21.178,72.834]&options=count' \
--header 'token: <tokenValue>'
- lang: 'cURL'
label: 'search by string attribute'
source: |
curl --location --request GET 'https://example.com/ngsi-ld/v1/entities?id=UUID&offset=0&limit=10&q=license_plate==GJ05BU3663' \
--header 'token: <tokenValue>'
deprecated: false
description: |
Expand Down Expand Up @@ -452,7 +458,7 @@ paths:
The attribute query is used for querying <b> allowed quantitative</b> properties.
- Used to query on a value of a resource attribute using `<`,`>`,`<=`,`>=`,`!=`,`==` operators.
- For e.g, attribute > value, attribute < value, attribute >= value, attribute <= value and attribute == value.
- Allowed values for all operators is double.
- Allowed values for all operators is double and alphanumeric for `==` operator.
- For the operator `==` if the query is on `id` then the only value allowed is an `data exchange ID` of a resource.
- e.g, `q=attribute-name>attribute-value`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public void handle(RoutingContext context) {
new RestResponse.Builder()
.withType(exception.getUrn().getUrn())
.withTitle(code.getDescription())
.withMessage(code.getDescription())
.withMessage(exception.getMessage())
.build()
.toJson();

Expand Down
154 changes: 79 additions & 75 deletions src/main/java/iudx/resource/server/apiserver/query/QueryMapper.java
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
package iudx.resource.server.apiserver.query;

import static iudx.resource.server.apiserver.util.Constants.*;
import static iudx.resource.server.common.HttpStatusCode.BAD_REQUEST;
import static iudx.resource.server.common.ResponseUrn.INVALID_ATTR_PARAM_URN;
import static iudx.resource.server.common.ResponseUrn.INVALID_GEO_PARAM_URN;
import static iudx.resource.server.common.ResponseUrn.*;

import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
import iudx.resource.server.apiserver.exceptions.DxRuntimeException;
import iudx.resource.server.apiserver.util.Constants;
import iudx.resource.server.common.HttpStatusCode;
import iudx.resource.server.common.ResponseUrn;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

Expand Down Expand Up @@ -55,14 +56,14 @@ public JsonObject toJson(NgsildQueryParams params, boolean isTemporal, boolean i
if (params.getId() != null) {
JsonArray jsonArray = new JsonArray();
params.getId().forEach(s -> jsonArray.add(s.toString()));
json.put(Constants.JSON_ID, jsonArray);
json.put(JSON_ID, jsonArray);
LOGGER.debug("Info : json " + json);
}
if (params.getAttrs() != null) {
isResponseFilter = true;
JsonArray jsonArray = new JsonArray();
params.getAttrs().forEach(attribute -> jsonArray.add(attribute));
json.put(Constants.JSON_ATTRIBUTE_FILTER, jsonArray);
json.put(JSON_ATTRIBUTE_FILTER, jsonArray);
LOGGER.debug("Info : json " + json);
}
if (isGeoQuery(params)) {
Expand All @@ -71,23 +72,23 @@ public JsonObject toJson(NgsildQueryParams params, boolean isTemporal, boolean i
&& params.getGeometry() != null
&& params.getGeoProperty() != null) {
isGeoSearch = true;
if (params.getGeometry().equalsIgnoreCase(Constants.GEOM_POINT)
&& params.getGeoRel().getRelation().equals(Constants.JSON_NEAR)
if (params.getGeometry().equalsIgnoreCase(GEOM_POINT)
&& params.getGeoRel().getRelation().equals(JSON_NEAR)
&& params.getGeoRel().getMaxDistance() != null) {
String[] coords = params.getCoordinates().replaceAll("\\[|\\]", "").split(",");
json.put(Constants.JSON_LAT, Double.parseDouble(coords[0]));
json.put(Constants.JSON_LON, Double.parseDouble(coords[1]));
json.put(Constants.JSON_RADIUS, params.getGeoRel().getMaxDistance());
json.put(JSON_LAT, Double.parseDouble(coords[0]));
json.put(JSON_LON, Double.parseDouble(coords[1]));
json.put(JSON_RADIUS, params.getGeoRel().getMaxDistance());
} else {
json.put(Constants.JSON_GEOMETRY, params.getGeometry());
json.put(Constants.JSON_COORDINATES, params.getCoordinates());
json.put(JSON_GEOMETRY, params.getGeometry());
json.put(JSON_COORDINATES, params.getCoordinates());
json.put(
Constants.JSON_GEOREL,
getOrDefault(params.getGeoRel().getRelation(), Constants.JSON_WITHIN));
JSON_GEOREL,
getOrDefault(params.getGeoRel().getRelation(), JSON_WITHIN));
if (params.getGeoRel().getMaxDistance() != null) {
json.put(Constants.JSON_MAXDISTANCE, params.getGeoRel().getMaxDistance());
json.put(JSON_MAXDISTANCE, params.getGeoRel().getMaxDistance());
} else if (params.getGeoRel().getMinDistance() != null) {
json.put(Constants.JSON_MINDISTANCE, params.getGeoRel().getMinDistance());
json.put(JSON_MINDISTANCE, params.getGeoRel().getMinDistance());
}
}
LOGGER.debug("Info : json " + json);
Expand All @@ -106,22 +107,22 @@ public JsonObject toJson(NgsildQueryParams params, boolean isTemporal, boolean i
&& params.getTemporalRelation().getTemprel() != null
&& params.getTemporalRelation().getTime() != null) {
isTemporal = true;
if (params.getTemporalRelation().getTemprel().equalsIgnoreCase(Constants.JSON_DURING)
|| params.getTemporalRelation().getTemprel().equalsIgnoreCase(Constants.JSON_BETWEEN)) {
if (params.getTemporalRelation().getTemprel().equalsIgnoreCase(JSON_DURING)
|| params.getTemporalRelation().getTemprel().equalsIgnoreCase(JSON_BETWEEN)) {
LOGGER.debug("Info : inside during ");

json.put(Constants.JSON_TIME, params.getTemporalRelation().getTime());
json.put(Constants.JSON_ENDTIME, params.getTemporalRelation().getEndTime());
json.put(Constants.JSON_TIMEREL, params.getTemporalRelation().getTemprel());
json.put(JSON_TIME, params.getTemporalRelation().getTime());
json.put(JSON_ENDTIME, params.getTemporalRelation().getEndTime());
json.put(JSON_TIMEREL, params.getTemporalRelation().getTemprel());

isValidTimeInterval(
Constants.JSON_DURING,
json.getString(Constants.JSON_TIME),
json.getString(Constants.JSON_ENDTIME),
JSON_DURING,
json.getString(JSON_TIME),
json.getString(JSON_ENDTIME),
isAsyncQuery);
} else {
json.put(Constants.JSON_TIME, params.getTemporalRelation().getTime().toString());
json.put(Constants.JSON_TIMEREL, params.getTemporalRelation().getTemprel());
json.put(JSON_TIME, params.getTemporalRelation().getTime().toString());
json.put(JSON_TIMEREL, params.getTemporalRelation().getTemprel());
}
LOGGER.debug("Info : json " + json);
}
Expand All @@ -132,25 +133,25 @@ public JsonObject toJson(NgsildQueryParams params, boolean isTemporal, boolean i
for (String term : qterms) {
query.add(getQueryTerms(term));
}
json.put(Constants.JSON_ATTR_QUERY, query);
json.put(JSON_ATTR_QUERY, query);
LOGGER.debug("Info : json " + json);
}
if (params.getGeoProperty() != null) {
json.put(Constants.JSON_GEOPROPERTY, params.getGeoProperty());
json.put(JSON_GEOPROPERTY, params.getGeoProperty());
LOGGER.debug("Info : json " + json);
}
if (params.getOptions() != null) {
json.put(Constants.IUDXQUERY_OPTIONS, params.getOptions());
json.put(IUDXQUERY_OPTIONS, params.getOptions());
LOGGER.debug("Info : json " + json);
}
if (params.getPageFrom() != null) {
json.put(Constants.NGSILDQUERY_FROM, params.getPageFrom());
json.put(NGSILDQUERY_FROM, params.getPageFrom());
}
if (params.getPageSize() != null) {
json.put(Constants.NGSILDQUERY_SIZE, params.getPageSize());
json.put(NGSILDQUERY_SIZE, params.getPageSize());
}

json.put(Constants.JSON_SEARCH_TYPE, getSearchType(isAsyncQuery));
json.put(JSON_SEARCH_TYPE, getSearchType(isAsyncQuery));
LOGGER.debug("Info : json " + json);
return json;
}
Expand All @@ -162,12 +163,12 @@ public JsonObject toJson(NgsildQueryParams params, boolean isTemporal, boolean i
private void isValidTimeInterval(
String timeRel, String time, String endTime, boolean isAsyncQuery) {
long totalDaysAllowed = 0;
if (timeRel.equalsIgnoreCase(Constants.JSON_DURING)) {
if (timeRel.equalsIgnoreCase(JSON_DURING)) {
if (isNullorEmpty(time) || isNullorEmpty(endTime)) {
DxRuntimeException ex =
new DxRuntimeException(
BAD_REQUEST.getValue(),
ResponseUrn.INVALID_TEMPORAL_PARAM_URN,
INVALID_TEMPORAL_PARAM_URN,
"time and endTime both are mandatory for during Query.");
this.context.fail(400, ex);
}
Expand All @@ -181,25 +182,25 @@ private void isValidTimeInterval(
DxRuntimeException exc =
new DxRuntimeException(
BAD_REQUEST.getValue(),
ResponseUrn.INVALID_TEMPORAL_PARAM_URN,
INVALID_TEMPORAL_PARAM_URN,
"time and endTime both are mandatory for during Query.");
this.context.fail(400, exc);
}
}
if (isAsyncQuery
&& totalDaysAllowed > Constants.VALIDATION_MAX_DAYS_INTERVAL_ALLOWED_FOR_ASYNC) {
&& totalDaysAllowed > VALIDATION_MAX_DAYS_INTERVAL_ALLOWED_FOR_ASYNC) {
DxRuntimeException ex =
new DxRuntimeException(
BAD_REQUEST.getValue(),
ResponseUrn.INVALID_TEMPORAL_PARAM_URN,
INVALID_TEMPORAL_PARAM_URN,
"time interval greater than 1 year is not allowed");
this.context.fail(400, ex);
}
if (!isAsyncQuery && totalDaysAllowed > Constants.VALIDATION_MAX_DAYS_INTERVAL_ALLOWED) {
if (!isAsyncQuery && totalDaysAllowed > VALIDATION_MAX_DAYS_INTERVAL_ALLOWED) {
DxRuntimeException ex =
new DxRuntimeException(
BAD_REQUEST.getValue(),
ResponseUrn.INVALID_TEMPORAL_PARAM_URN,
INVALID_TEMPORAL_PARAM_URN,
"time interval greater than 10 days is not allowed");
this.context.fail(400, ex);
}
Expand Down Expand Up @@ -228,59 +229,62 @@ private <T> T getOrDefault(T value, T def) {
private String getSearchType(boolean isAsyncQuery) {
StringBuilder searchType = new StringBuilder();
if (isTemporal) {
searchType.append(Constants.JSON_TEMPORAL_SEARCH);
searchType.append(JSON_TEMPORAL_SEARCH);
} else if (!isTemporal && !isAsyncQuery) {
searchType.append(Constants.JSON_LATEST_SEARCH);
searchType.append(JSON_LATEST_SEARCH);
}
if (isGeoSearch) {
searchType.append(Constants.JSON_GEO_SEARCH);
searchType.append(JSON_GEO_SEARCH);
}
if (isResponseFilter) {
searchType.append(Constants.JSON_RESPONSE_FILTER_SEARCH);
searchType.append(JSON_RESPONSE_FILTER_SEARCH);
}
if (isAttributeSearch) {
searchType.append(Constants.JSON_ATTRIBUTE_SEARCH);
searchType.append(JSON_ATTRIBUTE_SEARCH);
}
return searchType.toString().isEmpty()
? ""
: searchType.substring(0, searchType.length() - 1).toString();
}

JsonObject getQueryTerms(String queryTerms) {
JsonObject getQueryTerms(final String queryTerms) {
JsonObject json = new JsonObject();
int length = queryTerms.length();
List<Character> allowedSpecialCharacter = Arrays.asList('>', '=', '<', '!');
List<String> allowedOperators = Arrays.asList(">", "=", "<", ">=", "<=", "==", "!=");
int startIndex = 0;
boolean specialCharFound = false;
for (int i = 0; i < length; i++) {
Character c = queryTerms.charAt(i);
if (!(Character.isLetter(c) || Character.isDigit(c)) && !specialCharFound) {
if (allowedSpecialCharacter.contains(c)) {
json.put(Constants.JSON_ATTRIBUTE, queryTerms.substring(startIndex, i));
startIndex = i;
specialCharFound = true;
} else {
LOGGER.debug("Ignore " + c.toString());
DxRuntimeException ex =
new DxRuntimeException(
BAD_REQUEST.getValue(), INVALID_ATTR_PARAM_URN, "Operator not allowed.");
this.context.fail(400, ex);
}
String jsonOperator = "";
String jsonValue = "";
String jsonAttribute = "";

String[] attributes = queryTerms.split(";");
LOGGER.info("Attributes : {} ", attributes);

for (String attr : attributes) {

String[] attributeQueryTerms =
attr.split("((?=>)|(?<=>)|(?=<)|(?<=<)|(?<==)|(?=!)|(?<=!)|(?==)|(?===))");
LOGGER.info(Arrays.stream(attributeQueryTerms).collect(Collectors.toList()));
LOGGER.info(attributeQueryTerms.length);
if (attributeQueryTerms.length == 3) {
jsonOperator = attributeQueryTerms[1];
jsonValue = attributeQueryTerms[2];
json.put(JSON_OPERATOR, jsonOperator).put(JSON_VALUE, jsonValue);
} else if (attributeQueryTerms.length == 4) {
jsonOperator = attributeQueryTerms[1].concat(attributeQueryTerms[2]);
jsonValue = attributeQueryTerms[3];
json.put(JSON_OPERATOR, jsonOperator).put(JSON_VALUE, jsonValue);
} else {
if (specialCharFound && (Character.isLetter(c) || Character.isDigit(c))) {
json.put(Constants.JSON_OPERATOR, queryTerms.substring(startIndex, i));
json.put(Constants.JSON_VALUE, queryTerms.substring(i));
break;
}
throw new DxRuntimeException(failureCode(), INVALID_PARAM_VALUE_URN, failureMessage());
}
jsonAttribute = attributeQueryTerms[0];
json.put(JSON_ATTRIBUTE, jsonAttribute);
}
if (!allowedOperators.contains(json.getString(Constants.JSON_OPERATOR))) {
DxRuntimeException ex =
new DxRuntimeException(
BAD_REQUEST.getValue(), INVALID_ATTR_PARAM_URN, "Operator not allowed.");
this.context.fail(400, ex);
}

return json;
}

public int failureCode() {
return HttpStatusCode.BAD_REQUEST.getValue();
}

public String failureMessage() {
return INVALID_PARAM_VALUE_URN.getMessage();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ public class Constants {
List.of("during", "between");

public static final Pattern VALIDATION_Q_ATTR_PATTERN = Pattern.compile("^[a-zA-Z0-9_]{1,100}$");
public static final Pattern VALIDATION_Q_VALUE_PATTERN = Pattern.compile("^[a-zA-Z0-9_.]{1,100}$");

// subscriptions queries
public static final String CREATE_SUB_SQL =
Expand Down
Loading

0 comments on commit 90c7824

Please sign in to comment.