Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
9f66fa9
_source search parameter needs to support modifiers - added test for …
volodymyr-korzh Jul 14, 2023
7996540
_source search parameter needs to support modifiers - added below mod…
volodymyr-korzh Jul 14, 2023
807f588
_source search parameter needs to support modifiers - formatting fix
volodymyr-korzh Jul 14, 2023
44c8e53
Merge remote-tracking branch 'origin/rel_6_8' into 5094-_source-searc…
volodymyr-korzh Jul 24, 2023
2a389c2
_source search parameter needs to support modifiers - added support f…
volodymyr-korzh Jul 24, 2023
697e6bd
_source search parameter needs to support modifiers - added support f…
volodymyr-korzh Jul 24, 2023
080c186
_source search parameter needs to support modifiers - added support f…
volodymyr-korzh Jul 28, 2023
d67d1b5
_source search parameter needs to support modifiers - added support f…
volodymyr-korzh Jul 28, 2023
173cc38
_source search parameter needs to support modifiers - added support f…
volodymyr-korzh Jul 28, 2023
efc3ccf
_source search parameter needs to support modifiers - added support f…
volodymyr-korzh Jul 28, 2023
c5352b4
_source search parameter needs to support modifiers - added support f…
volodymyr-korzh Jul 31, 2023
6b08ae2
_source search parameter needs to support modifiers - added support f…
volodymyr-korzh Jul 31, 2023
d8b6860
Merge branch 'rel_6_8' of github.com:hapifhir/hapi-fhir into 5094-_so…
volodymyr-korzh Jul 31, 2023
895339d
_source search parameter needs to support modifiers - added support f…
volodymyr-korzh Jul 31, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,16 @@ public enum UriParamQualifierEnum {
* Value <code>:below</code>
* </p>
*/
BELOW(":below");
BELOW(":below"),

/**
* The contains modifier allows clients to indicate that a supplied URI input should be matched
* as a case-insensitive and combining-character insensitive match anywhere in the target URI.
* <p>
* Value <code>:contains</code>
* </p>
*/
CONTAINS(":contains");

private static final Map<String, UriParamQualifierEnum> KEY_TO_VALUE;

Expand Down
38 changes: 38 additions & 0 deletions hapi-fhir-base/src/main/java/ca/uhn/fhir/util/UrlUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import java.net.URL;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
Expand Down Expand Up @@ -602,6 +603,43 @@ public static List<NameValuePair> translateMatchUrl(String theMatchUrl) {
return parameters;
}

/**
* Creates list of sub URIs candidates for search with :above modifier
* Example input: http://[host]/[pathPart1]/[pathPart2]
* Example output: http://[host], http://[host]/[pathPart1], http://[host]/[pathPart1]/[pathPart2]
*
* @param theUri String URI parameter
* @return List of URI candidates
*/
public static List<String> getAboveUriCandidates(String theUri) {
List<String> candidates = new ArrayList<>();
try {
URI uri = new URI(theUri);

if (uri.getScheme() == null || uri.getHost() == null) {
throwInvalidRequestExceptionForNotValidUri(theUri, null);
}
StringBuilder sb =
new StringBuilder().append(uri.getScheme()).append("://").append(uri.getHost());

candidates.add(sb.toString());

String[] pathParts = uri.getPath().split("/");
Arrays.stream(pathParts)
.filter(part -> !part.isEmpty())
.forEach(part -> candidates.add(sb.append("/").append(part).toString()));

} catch (URISyntaxException e) {
throwInvalidRequestExceptionForNotValidUri(theUri, e);
}
return candidates;
}

private static void throwInvalidRequestExceptionForNotValidUri(String theUri, Exception theCause) {
throw new InvalidRequestException(
Msg.code(2419) + String.format("Provided URI is not valid: %s", theUri), theCause);
}

public static class UrlParts {
private String myParams;
private String myResourceId;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
type: add
issue: 5095
title: "Added support for :above, :below, :contains and :missing _source search parameter modifiers."
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -1854,14 +1855,23 @@ private Condition createPredicateSource(
throw new InvalidRequestException(Msg.code(1216) + msg);
}

SourcePredicateBuilder join = createOrReusePredicateBuilder(
PredicateBuilderTypeEnum.SOURCE,
theSourceJoinColumn,
Constants.PARAM_SOURCE,
() -> mySqlBuilder.addSourcePredicateBuilder(theSourceJoinColumn))
.getResult();

List<Condition> orPredicates = new ArrayList<>();

// :missing=true modifier processing requires "LEFT JOIN" with HFJ_RESOURCE table to return correct results
// if both sourceUri and requestId are not populated for the resource
Optional<? extends IQueryParameterType> isMissingSourceOptional = theList.stream()
.filter(nextParameter -> nextParameter.getMissing() != null && nextParameter.getMissing())
.findFirst();

if (isMissingSourceOptional.isPresent()) {
SourcePredicateBuilder join =
getSourcePredicateBuilder(theSourceJoinColumn, SelectQuery.JoinType.LEFT_OUTER);
orPredicates.add(join.createPredicateMissingSourceUri());
return toOrPredicate(orPredicates);
}
// for all other cases we use "INNER JOIN" to match search parameters
SourcePredicateBuilder join = getSourcePredicateBuilder(theSourceJoinColumn, SelectQuery.JoinType.INNER);

for (IQueryParameterType nextParameter : theList) {
SourceParam sourceParameter = new SourceParam(nextParameter.getValueAsQueryToken(myFhirContext));
String sourceUri = sourceParameter.getSourceUri();
Expand All @@ -1870,7 +1880,8 @@ private Condition createPredicateSource(
orPredicates.add(toAndPredicate(
join.createPredicateSourceUri(sourceUri), join.createPredicateRequestId(requestId)));
} else if (isNotBlank(sourceUri)) {
orPredicates.add(join.createPredicateSourceUri(sourceUri));
orPredicates.add(
join.createPredicateSourceUriWithModifiers(nextParameter, myStorageSettings, sourceUri));
} else if (isNotBlank(requestId)) {
orPredicates.add(join.createPredicateRequestId(requestId));
}
Expand All @@ -1879,6 +1890,16 @@ private Condition createPredicateSource(
return toOrPredicate(orPredicates);
}

private SourcePredicateBuilder getSourcePredicateBuilder(
@Nullable DbColumn theSourceJoinColumn, SelectQuery.JoinType theJoinType) {
return createOrReusePredicateBuilder(
PredicateBuilderTypeEnum.SOURCE,
theSourceJoinColumn,
Constants.PARAM_SOURCE,
() -> mySqlBuilder.addSourcePredicateBuilder(theSourceJoinColumn, theJoinType))
.getResult();
}

public Condition createPredicateString(
@Nullable DbColumn theSourceJoinColumn,
String theResourceName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,29 @@
*/
package ca.uhn.fhir.jpa.search.builder.predicate;

import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder;
import ca.uhn.fhir.jpa.util.QueryParameterUtils;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.rest.param.UriParam;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
import ca.uhn.fhir.util.StringUtil;
import ca.uhn.fhir.util.UrlUtil;
import com.healthmarketscience.sqlbuilder.BinaryCondition;
import com.healthmarketscience.sqlbuilder.Condition;
import com.healthmarketscience.sqlbuilder.FunctionCall;
import com.healthmarketscience.sqlbuilder.UnaryCondition;
import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;

import static ca.uhn.fhir.jpa.search.builder.predicate.StringPredicateBuilder.createLeftAndRightMatchLikeExpression;
import static ca.uhn.fhir.jpa.search.builder.predicate.StringPredicateBuilder.createLeftMatchLikeExpression;

public class SourcePredicateBuilder extends BaseJoiningPredicateBuilder {

private static final Logger ourLog = LoggerFactory.getLogger(SourcePredicateBuilder.class);
Expand Down Expand Up @@ -53,6 +69,46 @@ public Condition createPredicateSourceUri(String theSourceUri) {
return BinaryCondition.equalTo(myColumnSourceUri, generatePlaceholder(theSourceUri));
}

public Condition createPredicateMissingSourceUri() {
return UnaryCondition.isNull(myColumnSourceUri);
}

public Condition createPredicateSourceUriWithModifiers(
IQueryParameterType theQueryParameter, JpaStorageSettings theStorageSetting, String theSourceUri) {
if (theQueryParameter.getMissing() != null && !theQueryParameter.getMissing()) {
return UnaryCondition.isNotNull(myColumnSourceUri);
} else if (theQueryParameter instanceof UriParam && theQueryParameter.getQueryParameterQualifier() != null) {
UriParam uriParam = (UriParam) theQueryParameter;
switch (uriParam.getQualifier()) {
case ABOVE:
List<String> aboveUriCandidates = UrlUtil.getAboveUriCandidates(theSourceUri);
List<String> aboveUriPlaceholders = generatePlaceholders(aboveUriCandidates);
return QueryParameterUtils.toEqualToOrInPredicate(myColumnSourceUri, aboveUriPlaceholders);
case BELOW:
String belowLikeExpression = createLeftMatchLikeExpression(theSourceUri);
return BinaryCondition.like(myColumnSourceUri, generatePlaceholder(belowLikeExpression));
case CONTAINS:
if (theStorageSetting.isAllowContainsSearches()) {
FunctionCall upperFunction = new FunctionCall("UPPER");
upperFunction.addCustomParams(myColumnSourceUri);
String normalizedString = StringUtil.normalizeStringForSearchIndexing(theSourceUri);
String containsLikeExpression = createLeftAndRightMatchLikeExpression(normalizedString);
return BinaryCondition.like(upperFunction, generatePlaceholder(containsLikeExpression));
} else {
throw new MethodNotAllowedException(
Msg.code(2417) + ":contains modifier is disabled on this server");
}
default:
throw new InvalidRequestException(Msg.code(2418)
+ String.format(
"Unsupported qualifier specified, qualifier=%s",
theQueryParameter.getQueryParameterQualifier()));
}
} else {
return createPredicateSourceUri(theSourceUri);
}
}

public Condition createPredicateRequestId(String theRequestId) {
return BinaryCondition.equalTo(myColumnRequestId, generatePlaceholder(theRequestId));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -288,9 +288,10 @@ public QuantityNormalizedPredicateBuilder addQuantityNormalizedPredicateBuilder(
/**
* Add and return a predicate builder (or a root query if no root query exists yet) for selecting on a <code>_source</code> search parameter
*/
public SourcePredicateBuilder addSourcePredicateBuilder(@Nullable DbColumn theSourceJoinColumn) {
public SourcePredicateBuilder addSourcePredicateBuilder(
@Nullable DbColumn theSourceJoinColumn, SelectQuery.JoinType theJoinType) {
SourcePredicateBuilder retVal = mySqlBuilderFactory.newSourcePredicateBuilder(this);
addTable(retVal, theSourceJoinColumn);
addTable(retVal, theSourceJoinColumn, theJoinType);
return retVal;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
import ca.uhn.fhir.jpa.search.CompositeSearchParameterTestCases;
import ca.uhn.fhir.jpa.search.QuantitySearchParameterTestCases;
import ca.uhn.fhir.jpa.search.SourceSearchParameterTestCases;
import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc;
Expand Down Expand Up @@ -88,7 +89,6 @@
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.aop.support.Pointcuts;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.test.annotation.DirtiesContext;
Expand All @@ -113,7 +113,6 @@
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import static ca.uhn.fhir.jpa.model.util.UcumServiceUtil.UCUM_CODESYSTEM_URL;
Expand All @@ -124,10 +123,6 @@
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.equalToCompressingWhiteSpace;
import static org.hamcrest.Matchers.equalToIgnoringWhiteSpace;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.hasSize;
Expand Down Expand Up @@ -2365,6 +2360,18 @@ protected boolean isCorrelatedSupported() {
}
}

@Nested
class SourceSearch extends SourceSearchParameterTestCases {
SourceSearch() {
super(myTestDataBuilder.getTestDataBuilderSupport(), myTestDaoSearch, myStorageSettings);
}

@Override
protected boolean isRequestIdSupported() {
return false;
}
}

/**
* Disallow context dirtying for nested classes
*/
Expand Down
Loading