Skip to content

Commit

Permalink
Merge branch 'master' into BFD-3738
Browse files Browse the repository at this point in the history
  • Loading branch information
aschey-forpeople authored Jan 30, 2025
2 parents e82a73e + 36bb743 commit 7170301
Show file tree
Hide file tree
Showing 25 changed files with 633 additions and 158 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,20 @@
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import gov.cms.bfd.server.war.stu3.providers.ExplanationOfBenefitResourceProvider;
import gov.cms.bfd.sharedutils.exceptions.BadCodeMonkeyException;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import org.apache.http.client.utils.URIBuilder;
import org.hl7.fhir.dstu3.model.Bundle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* PagingArguments encapsulates the arguments related to paging for the ExplanationOfBenefit,
* Patient, and Coverage requests.
*/
public final class OffsetLinkBuilder implements LinkBuilder {

private static final Logger LOGGER =
LoggerFactory.getLogger(ExplanationOfBenefitResourceProvider.class);

/** The page size for paging. */
private final Optional<Integer> pageSize;

Expand All @@ -45,15 +38,22 @@ public final class OffsetLinkBuilder implements LinkBuilder {
*/
private int numTotalResults = -1;

/** Start index request parameter. */
private static final String PARAM_START_INDEX = "startIndex";

/**
* Instantiates a new Offset link builder.
*
* @param requestDetails the request details
* @param resource the resource
*/
public OffsetLinkBuilder(RequestDetails requestDetails, String resource) {
this.pageSize = parseIntegerParameters(requestDetails, Constants.PARAM_COUNT);
this.startIndex = parseIntegerParameters(requestDetails, "startIndex");
this.pageSize =
StringUtils.parseIntegersFromRequest(requestDetails, Constants.PARAM_COUNT).stream()
.findFirst();
this.startIndex =
StringUtils.parseIntegersFromRequest(requestDetails, PARAM_START_INDEX).stream()
.findFirst();
this.serverBase = requestDetails.getServerBaseForRequest();
this.resource = resource;
this.requestDetails = requestDetails;
Expand All @@ -76,27 +76,6 @@ private void validate() {
}
}

/**
* Returns the parsed parameter as an Integer.
*
* @param requestDetails the {@link RequestDetails} containing additional parameters for the URL
* in need of parsing out
* @param parameterToParse the parameter to parse from requestDetails
* @return the parsed parameter as an Integer, empty {@link Optional} if the parameter is not
* found
*/
private Optional<Integer> parseIntegerParameters(
RequestDetails requestDetails, String parameterToParse) {

if (requestDetails.getParameters().containsKey(parameterToParse)) {

return Optional.of(
StringUtils.parseIntOrBadRequest(
requestDetails.getParameters().get(parameterToParse)[0], parameterToParse));
}
return Optional.empty();
}

/** {@inheritDoc} */
@Override
public boolean isPagingRequested() {
Expand Down Expand Up @@ -238,16 +217,15 @@ private String createPageLink(int startIndex) {
Map<String, String[]> params = new HashMap<>(requestDetails.getParameters());

// Add in paging related changes.
params.put("startIndex", new String[] {String.valueOf(startIndex)});
params.put("_count", new String[] {String.valueOf(getPageSize())});
params.put(PARAM_START_INDEX, new String[] {String.valueOf(startIndex)});
params.put(Constants.PARAM_COUNT, new String[] {String.valueOf(getPageSize())});

try {
// Setup URL base and resource.
URIBuilder uri = new URIBuilder(serverBase + resource);

// Create query parameters by iterating thru all params entry sets. Handle multi values for
// Create query parameters by iterating through all params entry sets. Handle multi values for
// the same parameter key.
ArrayList<String> queryParams = new ArrayList<String>();
for (Map.Entry<String, String[]> paramSet : params.entrySet()) {
for (String param : paramSet.getValue()) {
uri.addParameter(paramSet.getKey(), param);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,12 +128,28 @@ public final class OpenAPIContentProvider {
*/
public static final String PATIENT_PARTD_CURSOR_VALUE =
"""
Provide a pagination cursor or numeric _offset_ for processing Patient's Part D events information.
Provides a pagination cursor or numeric _offset_ for processing Patient's Part D events information.
Examples:
- `cursor=200` the first record is the 201st record
- `cursor=1000` the first record is the 1001st record""";

/***
* Open API content for the _count parameter.
*/
public static final String COUNT_SHORT = "The number of records to return";

/***
* Open API content for the _count parameter.
*/
public static final String COUNT_VALUE =
"""
Provides the number of records to be used for pagination.
Examples:
- `_count=10`: return 10 values.
""";

/** Open API content short description for /Patient's identifier parameter. */
public static final String BENEFICIARY_SP_RES_ID_SHORT =
"Identifier resource for the covered party";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package gov.cms.bfd.server.war.commons;

import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import java.util.List;
import java.util.Optional;
import org.hl7.fhir.dstu3.model.Bundle;
import org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent;
import org.hl7.fhir.dstu3.model.Patient;
Expand All @@ -20,14 +22,14 @@ public final class PatientLinkBuilder implements LinkBuilder {
*/
public static final int MAX_PAGE_SIZE = Integer.MAX_VALUE - 1;

/** The uri components. */
private final UriComponents components;
/** The request details. */
private final RequestDetails requestDetails;

/** The count. */
private final Integer count;
private final Optional<Integer> count;

/** The cursor value. */
private final Long cursor;
private final Optional<Long> cursor;

/** If there is another page for this link. */
private final boolean hasAnotherPage;
Expand All @@ -38,12 +40,14 @@ public final class PatientLinkBuilder implements LinkBuilder {
/**
* Instantiates a new Patient link builder.
*
* @param requestString the request string
* @param requestDetails the request details
*/
public PatientLinkBuilder(String requestString) {
components = UriComponentsBuilder.fromUriString(requestString).build();
count = extractCountParam(components);
cursor = extractCursorParam(components);
public PatientLinkBuilder(RequestDetails requestDetails) {
this.requestDetails = requestDetails;
count =
StringUtils.parseIntegersFromRequest(requestDetails, Constants.PARAM_COUNT).stream()
.findFirst();
cursor = StringUtils.parseLongsFromRequest(requestDetails, PARAM_CURSOR).stream().findFirst();
hasAnotherPage = false; // Don't really know, so default to false
validate();
}
Expand All @@ -55,7 +59,7 @@ public PatientLinkBuilder(String requestString) {
* @param hasAnotherPage if there is another page
*/
public PatientLinkBuilder(PatientLinkBuilder prev, boolean hasAnotherPage) {
components = prev.components;
requestDetails = prev.requestDetails;
count = prev.count;
cursor = prev.cursor;
this.hasAnotherPage = hasAnotherPage;
Expand All @@ -71,27 +75,27 @@ private void validate() {
if (getPageSize() <= 0) {
throw new InvalidRequestException("Value for pageSize cannot be zero or negative: %s");
}
if (!(getPageSize() <= MAX_PAGE_SIZE)) {
if (getPageSize() > MAX_PAGE_SIZE) {
throw new InvalidRequestException("Page size must be less than " + MAX_PAGE_SIZE);
}
}

/** {@inheritDoc} */
@Override
public boolean isPagingRequested() {
return count != null;
return count.isPresent();
}

/** {@inheritDoc} */
@Override
public int getPageSize() {
return isPagingRequested() ? count : MAX_PAGE_SIZE;
return isPagingRequested() ? count.get() : MAX_PAGE_SIZE;
}

/** {@inheritDoc} */
@Override
public boolean isFirstPage() {
return cursor == null || !isPagingRequested();
return cursor.isEmpty() || !isPagingRequested();
}

/** {@inheritDoc} */
Expand All @@ -104,12 +108,12 @@ public void addLinks(Bundle to) {
to.addLink(
new Bundle.BundleLinkComponent()
.setRelation(Constants.LINK_SELF)
.setUrl(components.toUriString()));
.setUrl(requestDetails.getCompleteUrl()));
to.addLink(
new Bundle.BundleLinkComponent().setRelation(Constants.LINK_FIRST).setUrl(buildUrl(null)));

if (hasAnotherPage) {
Patient lastPatient = (Patient) entries.get(entries.size() - 1).getResource();
Patient lastPatient = (Patient) entries.getLast().getResource();
Long lastPatientId = StringUtils.parseLongOrBadRequest(lastPatient.getId(), PARAM_CURSOR);
to.addLink(
new Bundle.BundleLinkComponent()
Expand All @@ -128,15 +132,15 @@ public void addLinks(org.hl7.fhir.r4.model.Bundle to) {
to.addLink(
new org.hl7.fhir.r4.model.Bundle.BundleLinkComponent()
.setRelation(Constants.LINK_SELF)
.setUrl(components.toUriString()));
.setUrl(requestDetails.getCompleteUrl()));
to.addLink(
new org.hl7.fhir.r4.model.Bundle.BundleLinkComponent()
.setRelation(Constants.LINK_FIRST)
.setUrl(buildUrl(null)));

if (entries.size() == getPageSize() && entries.size() > 0) {
if (entries.size() == getPageSize() && !entries.isEmpty()) {
org.hl7.fhir.r4.model.Patient lastPatient =
(org.hl7.fhir.r4.model.Patient) entries.get(entries.size() - 1).getResource();
(org.hl7.fhir.r4.model.Patient) entries.getLast().getResource();
Long lastPatientId = Long.parseLong(lastPatient.getId());
to.addLink(
new org.hl7.fhir.r4.model.Bundle.BundleLinkComponent()
Expand All @@ -151,36 +155,7 @@ public void addLinks(org.hl7.fhir.r4.model.Bundle to) {
* @return the cursor
*/
public Long getCursor() {
return cursor;
}

/**
* Extracts the count from the component object.
*
* @param components the components
* @return the count, or {@code null} if the count text was {@code null} in the compntent object
* @throws InvalidRequestException (http 400 error) if the count could not be parsed into an
* integer
*/
private Integer extractCountParam(UriComponents components) {
String countText = components.getQueryParams().getFirst(Constants.PARAM_COUNT);
if (countText != null) {
return StringUtils.parseIntOrBadRequest(countText, Constants.PARAM_COUNT);
}
return null;
}

/**
* Extracts the cursor from the component object.
*
* @param components the components
* @return the cursor, or {@code null} if the cursor text was {@code null} in the component object
*/
private Long extractCursorParam(UriComponents components) {
String cursorText = components.getQueryParams().getFirst(PARAM_CURSOR);
return cursorText != null && cursorText.length() > 0
? StringUtils.parseLongOrBadRequest(cursorText, PARAM_CURSOR)
: null;
return cursor.orElse(null);
}

/**
Expand All @@ -190,6 +165,8 @@ private Long extractCursorParam(UriComponents components) {
* @return the url string
*/
private String buildUrl(Long cursor) {
UriComponents components =
UriComponentsBuilder.fromUriString(requestDetails.getCompleteUrl()).build();
MultiValueMap<String, String> params = components.getQueryParams();
if (cursor != null) {
params = new LinkedMultiValueMap<>(params);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
package gov.cms.bfd.server.war.commons;

import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import com.google.common.collect.ImmutableList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;

/** Helper Utils class for Functions shared across. multiple classes. */
public class StringUtils {
Expand Down Expand Up @@ -68,4 +74,52 @@ public static int parseIntOrBadRequest(String input, String fieldName) {
String.format("Failed to parse value for %s as a number.", fieldName));
}
}

/**
* Returns the parsed parameter as an Integer.
*
* @param requestDetails the {@link RequestDetails} containing additional parameters for the
* request
* @param parameterToParse the parameter to parse from requestDetails
* @return the parsed parameter as an Integer, empty {@link Optional} if the parameter is not
* found
*/
public static List<Integer> parseIntegersFromRequest(
RequestDetails requestDetails, String parameterToParse) {
return getParametersFromRequest(requestDetails, parameterToParse)
.map(p -> parseIntOrBadRequest(p, parameterToParse))
.toList();
}

/**
* Returns the parsed parameter as a Long.
*
* @param requestDetails the {@link RequestDetails} containing additional parameters for the
* request
* @param parameterToParse the parameter to parse from requestDetails
* @return the parsed parameter as a Long, empty {@link Optional} if the parameter is not found
*/
public static List<Long> parseLongsFromRequest(
RequestDetails requestDetails, String parameterToParse) {
return getParametersFromRequest(requestDetails, parameterToParse)
.map(p -> parseLongOrBadRequest(p, parameterToParse))
.toList();
}

/**
* Extracts the first parameter from the request if it's present and non-empty.
*
* @param requestDetails request details
* @param parameterToParse name of parameter to retrieve
* @return extracted value
*/
private static Stream<String> getParametersFromRequest(
RequestDetails requestDetails, String parameterToParse) {
Map<String, String[]> parameters = requestDetails.getParameters();
if (parameters.containsKey(parameterToParse)) {
String[] paramValues = parameters.get(parameterToParse);
return Arrays.stream(paramValues).filter(p -> p != null && !p.isBlank());
}
return Stream.of();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import ca.uhn.fhir.rest.annotation.Read;
import ca.uhn.fhir.rest.annotation.RequiredParam;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.param.DateRangeParam;
import ca.uhn.fhir.rest.param.ReferenceParam;
Expand Down Expand Up @@ -236,6 +237,7 @@ public Coverage read(@IdParam IdType coverageId) {
* and find matches for
* @param startIndex an {@link OptionalParam} for the startIndex (or offset) used to determine
* pagination
* @param count an {@link OptionalParam} for the count used in pagination
* @param lastUpdated an {@link OptionalParam} to filter the results based on the passed date
* range
* @param requestDetails a {@link RequestDetails} containing the details of the request URL, used
Expand All @@ -258,6 +260,11 @@ public Bundle searchByBeneficiary(
shortDefinition = OpenAPIContentProvider.PATIENT_START_INDEX_SHORT,
value = OpenAPIContentProvider.PATIENT_START_INDEX_VALUE)
String startIndex,
@OptionalParam(name = Constants.PARAM_COUNT)
@Description(
shortDefinition = OpenAPIContentProvider.COUNT_SHORT,
value = OpenAPIContentProvider.COUNT_VALUE)
String count,
@OptionalParam(name = "_lastUpdated")
@Description(
shortDefinition = OpenAPIContentProvider.PATIENT_LAST_UPDATED_SHORT,
Expand Down
Loading

0 comments on commit 7170301

Please sign in to comment.