More updates

This commit is contained in:
James Agnew 2018-07-29 17:10:26 -04:00
parent f03d6b7c22
commit 518092cbd4
23 changed files with 717 additions and 181 deletions

View File

@ -124,7 +124,7 @@ public class Constants {
/**
* Used in paging links
*/
public static final Object PARAM_BUNDLETYPE = "_bundletype";
public static final String PARAM_BUNDLETYPE = "_bundletype";
public static final String PARAM_CONTENT = "_content";
public static final String PARAM_COUNT = "_count";
public static final String PARAM_DELETE = "_delete";
@ -134,7 +134,7 @@ public class Constants {
public static final String PARAM_HISTORY = "_history";
public static final String PARAM_INCLUDE = "_include";
public static final String PARAM_INCLUDE_QUALIFIER_RECURSE = ":recurse";
public static final String PARAM_INCLUDE_RECURSE = "_include"+PARAM_INCLUDE_QUALIFIER_RECURSE;
public static final String PARAM_INCLUDE_RECURSE = "_include" + PARAM_INCLUDE_QUALIFIER_RECURSE;
public static final String PARAM_LASTUPDATED = "_lastUpdated";
public static final String PARAM_NARRATIVE = "_narrative";
public static final String PARAM_PAGINGACTION = "_getpages";
@ -146,7 +146,7 @@ public class Constants {
public static final String PARAM_QUERY = "_query";
public static final String PARAM_RESPONSE_URL = "response-url"; //Used in messaging
public static final String PARAM_REVINCLUDE = "_revinclude";
public static final String PARAM_REVINCLUDE_RECURSE = PARAM_REVINCLUDE+PARAM_INCLUDE_QUALIFIER_RECURSE;
public static final String PARAM_REVINCLUDE_RECURSE = PARAM_REVINCLUDE + PARAM_INCLUDE_QUALIFIER_RECURSE;
public static final String PARAM_SEARCH = "_search";
public static final String PARAM_SECURITY = "_security";
public static final String PARAM_SINCE = "_since";
@ -154,9 +154,9 @@ public class Constants {
public static final String PARAM_SORT_ASC = "_sort:asc";
public static final String PARAM_SORT_DESC = "_sort:desc";
public static final String PARAM_SUMMARY = "_summary";
public static final String PARAM_TAG = "_tag";
public static final String PARAM_TAGS = "_tags";
public static final String PARAM_TEXT = "_text";
public static final String PARAM_TAG = "_tag";
public static final String PARAM_TAGS = "_tags";
public static final String PARAM_TEXT = "_text";
public static final String PARAM_VALIDATE = "_validate";
public static final String PARAMQUALIFIER_MISSING = ":missing";
public static final String PARAMQUALIFIER_MISSING_FALSE = "false";
@ -171,7 +171,7 @@ public class Constants {
public static final int STATUS_HTTP_400_BAD_REQUEST = 400;
public static final int STATUS_HTTP_401_CLIENT_UNAUTHORIZED = 401;
public static final int STATUS_HTTP_403_FORBIDDEN = 403;
public static final int STATUS_HTTP_404_NOT_FOUND = 404;
public static final int STATUS_HTTP_405_METHOD_NOT_ALLOWED = 405;
public static final int STATUS_HTTP_409_CONFLICT = 409;
@ -190,6 +190,7 @@ public class Constants {
public static final String HEADER_X_SECURITY_CONTEXT = "X-Security-Context";
public static final String POWERED_BY_HEADER = "X-Powered-By";
public static final Charset CHARSET_US_ASCII;
public static final String PARAM_PAGEID = "_pageId";
static {
CHARSET_UTF8 = Charset.forName(CHARSET_NAME_UTF8);
@ -259,7 +260,7 @@ public class Constants {
statusNames.put(510, "Not Extended");
statusNames.put(511, "Network Authentication Required");
HTTP_STATUS_NAMES = Collections.unmodifiableMap(statusNames);
Set<String> formatsHtml = new HashSet<>();
formatsHtml.add(CT_HTML);
formatsHtml.add(FORMAT_HTML);

View File

@ -14,9 +14,9 @@ import java.util.Map;
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.gclient;
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

View File

@ -19,9 +19,9 @@ import java.util.Map;
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.gclient;
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

View File

@ -25,9 +25,9 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.server.exceptions;
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.util;
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

View File

@ -25,9 +25,9 @@ import static org.apache.commons.lang3.StringUtils.isBlank;
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

View File

@ -51,6 +51,7 @@ public class Search implements Serializable {
private static final long serialVersionUID = 1L;
public static final int MAX_SEARCH_QUERY_STRING = 10000;
public static final int UUID_COLUMN_LENGTH = 36;
@Temporal(TemporalType.TIMESTAMP)
@Column(name="CREATED", nullable=false, updatable=false)
@ -118,7 +119,7 @@ public class Search implements Serializable {
@Column(name="TOTAL_COUNT", nullable=true)
private Integer myTotalCount;
@Column(name="SEARCH_UUID", length=40, nullable=false, updatable=false)
@Column(name="SEARCH_UUID", length= UUID_COLUMN_LENGTH, nullable=false, updatable=false)
private String myUuid;
/**

View File

@ -1,6 +1,10 @@
package ca.uhn.fhir.rest.api.server;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import java.util.Date;
import java.util.List;
/*
* #%L
@ -22,51 +26,89 @@ import java.util.Date;
* #L%
*/
import java.util.List;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
public interface IBundleProvider {
/**
* If this method is implemented, provides an ID for the current
* page of results. This ID should be unique (at least within
* the current search as identified by {@link #getUuid()})
* so that it can be used to look up a specific page of results.
* <p>
* This can be used in order to allow the
* server paging mechanism to work using completely
* opaque links (links that do not encode any index/offset
* information), which can be useful on some servers.
* </p>
*
* @since 3.5.0
*/
default String getCurrentPageId() {
return null;
}
/**
* If this method is implemented, provides an ID for the next
* page of results. This ID should be unique (at least within
* the current search as identified by {@link #getUuid()})
* so that it can be used to look up a specific page of results.
* <p>
* This can be used in order to allow the
* server paging mechanism to work using completely
* opaque links (links that do not encode any index/offset
* information), which can be useful on some servers.
* </p>
*
* @since 3.5.0
*/
default String getNextPageId() {
return null;
}
/**
* If this method is implemented, provides an ID for the previous
* page of results. This ID should be unique (at least within
* the current search as identified by {@link #getUuid()})
* so that it can be used to look up a specific page of results.
* <p>
* This can be used in order to allow the
* server paging mechanism to work using completely
* opaque links (links that do not encode any index/offset
* information), which can be useful on some servers.
* </p>
*
* @since 3.5.0
*/
default String getPreviousPageId() {
return null;
}
/**
* Returns the instant as of which this result was created. The
* result of this value is used to populate the <code>lastUpdated</code>
* value on search result/history result bundles.
*/
IPrimitiveType<Date> getPublished();
/**
* Load the given collection of resources by index, plus any additional resources per the
* server's processing rules (e.g. _include'd resources, OperationOutcome, etc.). For example,
* if the method is invoked with index 0,10 the method might return 10 search results, plus an
* additional 20 resources which matched a client's _include specification.
*
* @param theFromIndex
* The low index (inclusive) to return
* @param theToIndex
* The high index (exclusive) to return
* <p>
* Note that if this bundle provider was loaded using a
* page ID (i.e. via {@link ca.uhn.fhir.rest.server.IPagingProvider#retrieveResultList(String, String)}
* because {@link #getNextPageId()} provided a value on the
* previous page, then the indexes should be ignored and the
* whole page returned.
* </p>
*
* @param theFromIndex The low index (inclusive) to return
* @param theToIndex The high index (exclusive) to return
* @return A list of resources. The size of this list must be at least <code>theToIndex - theFromIndex</code>.
*/
List<IBaseResource> getResources(int theFromIndex, int theToIndex);
/**
* Optionally may be used to signal a preferred page size to the server, e.g. because
* the implementing code recognizes that the resources which will be returned by this
* implementation are expensive to load so a smaller page size should be used. The value
* returned by this method will only be used if the client has not explicitly requested
* a page size.
*
* @return Returns the preferred page size or <code>null</code>
*/
Integer preferredPageSize();
/**
* Returns the total number of results which match the given query (exclusive of any
* _include's or OperationOutcome). May return {@literal null} if the total size is not
* known or would be too expensive to calculate.
*/
Integer size();
/**
* Returns the instant as of which this result was valid
*/
IPrimitiveType<Date> getPublished();
/**
* Returns the UUID associated with this search. Note that this
* does not need to return a non-null value unless it a
@ -79,7 +121,29 @@ public interface IBundleProvider {
* IPagingProvider implementation you might use this method to communicate
* the search ID back to the provider.
* </p>
* <p>
* Note that the UUID returned by this method corresponds to
* the search, and not to the individual page.
* </p>
*/
public String getUuid();
String getUuid();
/**
* Optionally may be used to signal a preferred page size to the server, e.g. because
* the implementing code recognizes that the resources which will be returned by this
* implementation are expensive to load so a smaller page size should be used. The value
* returned by this method will only be used if the client has not explicitly requested
* a page size.
*
* @return Returns the preferred page size or <code>null</code>
*/
Integer preferredPageSize();
/**
* Returns the total number of results which match the given query (exclusive of any
* _include's or OperationOutcome). May return {@literal null} if the total size is not
* known or would be too expensive to calculate.
*/
Integer size();
}

View File

@ -0,0 +1,98 @@
package ca.uhn.fhir.rest.server;
/*-
* #%L
* HAPI FHIR - Server Framework
* %%
* Copyright (C) 2014 - 2018 University Health Network
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBaseResource;
import java.util.List;
/**
* Bundle provider that uses named pages instead of counts
*/
public class BundleProviderWithNamedPages extends SimpleBundleProvider {
private String myNextPageId;
private String myCurrentPageId;
private String myPreviousPageId;
/**
* Constructor
*
* @param theResultsInThisPage The complete list of results in the current page. Must not be null.
* @param theSearchId The ID for the search. Note that you should also populate {@link #setNextPageId(String)} and {@link #setPreviousPageId(String)} if these are known. Must not be <code>null</code> or blank.
* @param thePageId The ID for the current page. Note that you should also populate {@link #setNextPageId(String)} and {@link #setPreviousPageId(String)} if these are known. Must not be <code>null</code> or blank.
* @param theTotalResults The total number of result (if this is known), or <code>null</code>
* @see #setNextPageId(String)
* @see #setPreviousPageId(String)
*/
public BundleProviderWithNamedPages(List<IBaseResource> theResultsInThisPage, String theSearchId, String thePageId, Integer theTotalResults) {
super(theResultsInThisPage, theSearchId);
Validate.notNull(theResultsInThisPage, "theResultsInThisPage must not be null");
Validate.notBlank(thePageId, "thePageId must not be null or blank");
setCurrentPageId(thePageId);
setSize(theTotalResults);
}
@Override
public String getCurrentPageId() {
return myCurrentPageId;
}
public BundleProviderWithNamedPages setCurrentPageId(String theCurrentPageId) {
myCurrentPageId = theCurrentPageId;
return this;
}
@Override
public String getNextPageId() {
return myNextPageId;
}
public BundleProviderWithNamedPages setNextPageId(String theNextPageId) {
myNextPageId = theNextPageId;
return this;
}
@Override
public String getPreviousPageId() {
return myPreviousPageId;
}
public BundleProviderWithNamedPages setPreviousPageId(String thePreviousPageId) {
myPreviousPageId = thePreviousPageId;
return this;
}
@Override
public List<IBaseResource> getResources(int theFromIndex, int theToIndex) {
return getList(); // indexes are ignored for this provider type
}
@Override
public BundleProviderWithNamedPages setSize(Integer theSize) {
super.setSize(theSize);
return this;
}
}

View File

@ -25,17 +25,24 @@ import ca.uhn.fhir.rest.api.server.IBundleProvider;
public interface IPagingProvider {
int getDefaultPageSize();
int getMaximumPageSize();
/**
* Stores a result list and returns an ID with which that list can be returned
*/
public String storeResultList(IBundleProvider theList);
/**
* Retrieve a result list by ID
*/
public IBundleProvider retrieveResultList(String theId);
IBundleProvider retrieveResultList(String theSearchId);
/**
* Retrieve a result list by ID
*/
default IBundleProvider retrieveResultList(String theSearchId, String thePageId) {
return null;
}
/**
* Stores a result list and returns an ID with which that list can be returned
*/
String storeResultList(IBundleProvider theList);
}

View File

@ -130,6 +130,18 @@ public class RestfulServerUtils {
public static String createPagingLink(Set<Include> theIncludes, String theServerBase, String theSearchId, int theOffset, int theCount, Map<String, String[]> theRequestParameters, boolean thePrettyPrint,
BundleTypeEnum theBundleType) {
return createPagingLink(theIncludes, theServerBase, theSearchId, theOffset, theCount, theRequestParameters, thePrettyPrint,
theBundleType, null);
}
public static String createPagingLink(Set<Include> theIncludes, String theServerBase, String theSearchId, String thePageId, Map<String, String[]> theRequestParameters, boolean thePrettyPrint,
BundleTypeEnum theBundleType) {
return createPagingLink(theIncludes, theServerBase, theSearchId, null, null, theRequestParameters, thePrettyPrint,
theBundleType, thePageId);
}
private static String createPagingLink(Set<Include> theIncludes, String theServerBase, String theSearchId, Integer theOffset, Integer theCount, Map<String, String[]> theRequestParameters, boolean thePrettyPrint,
BundleTypeEnum theBundleType, String thePageId) {
StringBuilder b = new StringBuilder();
b.append(theServerBase);
b.append('?');
@ -137,14 +149,24 @@ public class RestfulServerUtils {
b.append('=');
b.append(UrlUtil.escapeUrlParam(theSearchId));
b.append('&');
b.append(Constants.PARAM_PAGINGOFFSET);
b.append('=');
b.append(theOffset);
b.append('&');
b.append(Constants.PARAM_COUNT);
b.append('=');
b.append(theCount);
if (theOffset != null) {
b.append('&');
b.append(Constants.PARAM_PAGINGOFFSET);
b.append('=');
b.append(theOffset);
}
if (theCount != null) {
b.append('&');
b.append(Constants.PARAM_COUNT);
b.append('=');
b.append(theCount);
}
if (isNotBlank(thePageId)) {
b.append('&');
b.append(Constants.PARAM_PAGEID);
b.append('=');
b.append(UrlUtil.escapeUrlParam(thePageId));
}
String[] strings = theRequestParameters.get(Constants.PARAM_FORMAT);
if (strings != null && strings.length > 0) {
b.append('&');
@ -442,6 +464,18 @@ public class RestfulServerUtils {
return retVal;
}
private static FhirContext getContextForVersion(FhirContext theContext, FhirVersionEnum theForVersion) {
FhirContext context = theContext;
if (context.getVersion().getVersion() != theForVersion) {
context = myFhirContextMap.get(theForVersion);
if (context == null) {
context = theForVersion.newContext();
myFhirContextMap.put(theForVersion, context);
}
}
return context;
}
private static ResponseEncoding getEncodingForContentType(FhirContext theFhirContext, boolean theStrict, String theContentType) {
EncodingEnum encoding;
if (theStrict) {
@ -476,18 +510,6 @@ public class RestfulServerUtils {
return parser;
}
private static FhirContext getContextForVersion(FhirContext theContext, FhirVersionEnum theForVersion) {
FhirContext context = theContext;
if (context.getVersion().getVersion() != theForVersion) {
context = myFhirContextMap.get(theForVersion);
if (context == null) {
context = theForVersion.newContext();
myFhirContextMap.put(theForVersion, context);
}
}
return context;
}
public static Set<String> parseAcceptHeaderAndReturnHighestRankedOptions(HttpServletRequest theRequest) {
Set<String> retVal = new HashSet<String>();
@ -725,7 +747,7 @@ public class RestfulServerUtils {
try {
return Integer.parseInt(retVal[0]);
} catch (NumberFormatException e) {
ourLog.debug("Failed to parse {} value '{}': {}", new Object[]{theParamName, retVal[0], e});
ourLog.debug("Failed to parse {} value '{}': {}", new Object[] {theParamName, retVal[0], e});
return null;
}
}

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.server;
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -23,8 +23,10 @@ package ca.uhn.fhir.rest.server;
import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import java.util.Collections;
import java.util.Date;
import java.util.List;
public class SimpleBundleProvider implements IBundleProvider {
@ -32,32 +34,48 @@ public class SimpleBundleProvider implements IBundleProvider {
private final List<IBaseResource> myList;
private final String myUuid;
private Integer myPreferredPageSize;
private Integer mySize;
private IPrimitiveType<Date> myPublished = InstantDt.withCurrentTime();
public SimpleBundleProvider(List<IBaseResource> theList) {
this(theList, null);
}
public SimpleBundleProvider(IBaseResource theResource) {
myList = Collections.singletonList(theResource);
myUuid = null;
this(Collections.singletonList(theResource));
}
/**
* Create an empty bundle
*/
public SimpleBundleProvider() {
myList = Collections.emptyList();
myUuid = null;
this(Collections.emptyList());
}
public SimpleBundleProvider(List<IBaseResource> theList, String theUuid) {
myList = theList;
myUuid = theUuid;
setSize(theList.size());
}
/**
* Returns the results stored in this provider
*/
protected List<IBaseResource> getList() {
return myList;
}
@Override
public InstantDt getPublished() {
return InstantDt.withCurrentTime();
public IPrimitiveType<Date> getPublished() {
return myPublished;
}
/**
* By default this class uses the object creation date/time (for this object)
* to determine {@link #getPublished() the published date} but this
* method may be used to specify an alternate date/time
*/
public void setPublished(IPrimitiveType<Date> thePublished) {
myPublished = thePublished;
}
@Override
@ -86,9 +104,18 @@ public class SimpleBundleProvider implements IBundleProvider {
myPreferredPageSize = thePreferredPageSize;
}
/**
* Sets the total number of results, if this provider
* corresponds to a single page within a larger search result
*/
public SimpleBundleProvider setSize(Integer theSize) {
mySize = theSize;
return this;
}
@Override
public Integer size() {
return myList.size();
return mySize;
}
}

View File

@ -123,8 +123,8 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi
}
protected IBaseResource createBundleFromBundleProvider(IRestfulServer<?> theServer, RequestDetails theRequest, Integer theLimit, String theLinkSelf, Set<Include> theIncludes,
IBundleProvider theResult, int theOffset, BundleTypeEnum theBundleType, EncodingEnum theLinkEncoding, String theSearchId) {
IBaseResource createBundleFromBundleProvider(IRestfulServer<?> theServer, RequestDetails theRequest, Integer theLimit, String theLinkSelf, Set<Include> theIncludes,
IBundleProvider theResult, int theOffset, BundleTypeEnum theBundleType, EncodingEnum theLinkEncoding, String theSearchId) {
IVersionSpecificBundleFactory bundleFactory = theServer.getFhirContext().newBundleFactory();
int numToReturn;
@ -152,7 +152,7 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi
numToReturn = Math.min(numToReturn, numTotalResults - theOffset);
}
if (numToReturn > 0) {
if (numToReturn > 0 || theResult.getCurrentPageId() != null) {
resourceList = theResult.getResources(theOffset, numToReturn + theOffset);
} else {
resourceList = Collections.emptyList();
@ -166,6 +166,7 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi
searchId = pagingProvider.storeResultList(theResult);
if (isBlank(searchId)) {
ourLog.info("Found {} results but paging provider did not provide an ID to use for paging", numTotalResults);
searchId = null;
}
}
}
@ -183,11 +184,7 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi
}
}
if (hasNull) {
for (Iterator<IBaseResource> iter = resourceList.iterator(); iter.hasNext(); ) {
if (iter.next() == null) {
iter.remove();
}
}
resourceList.removeIf(Objects::isNull);
}
/*
@ -207,7 +204,18 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi
String linkPrev = null;
String linkNext = null;
if (searchId != null) {
if (isNotBlank(theResult.getCurrentPageId())) {
// We're doing named pages
searchId = theResult.getUuid();
if (isNotBlank(theResult.getNextPageId())) {
linkNext = RestfulServerUtils.createPagingLink(theIncludes, serverBase, searchId, theResult.getNextPageId(), theRequest.getParameters(), prettyPrint, theBundleType);
}
if (isNotBlank(theResult.getPreviousPageId())) {
linkPrev = RestfulServerUtils.createPagingLink(theIncludes, serverBase, searchId, theResult.getPreviousPageId(), theRequest.getParameters(), prettyPrint, theBundleType);
}
} else if (searchId != null) {
// We're doing offset pages
if (numTotalResults == null || theOffset + numToReturn < numTotalResults) {
linkNext = (RestfulServerUtils.createPagingLink(theIncludes, serverBase, searchId, theOffset + numToReturn, numToReturn, theRequest.getParameters(), prettyPrint, theBundleType));
}

View File

@ -19,16 +19,6 @@ package ca.uhn.fhir.rest.server.method;
* limitations under the License.
* #L%
*/
import static org.apache.commons.lang3.StringUtils.isBlank;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Date;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.IResource;
@ -45,12 +35,22 @@ import ca.uhn.fhir.rest.param.ParameterUtil;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IPrimitiveType;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Date;
import java.util.List;
import static org.apache.commons.lang3.StringUtils.isBlank;
public class HistoryMethodBinding extends BaseResourceReturningMethodBinding {
private final Integer myIdParamIndex;
private String myResourceName;
private final RestOperationTypeEnum myResourceOperationType;
private String myResourceName;
public HistoryMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) {
super(toReturnType(theMethod, theProvider), theMethod, theContext, theProvider);
@ -87,13 +87,13 @@ public class HistoryMethodBinding extends BaseResourceReturningMethodBinding {
}
@Override
public RestOperationTypeEnum getRestOperationType() {
return myResourceOperationType;
protected BundleTypeEnum getResponseBundleType() {
return BundleTypeEnum.HISTORY;
}
@Override
protected BundleTypeEnum getResponseBundleType() {
return BundleTypeEnum.HISTORY;
public RestOperationTypeEnum getRestOperationType() {
return myResourceOperationType;
}
@Override
@ -128,7 +128,7 @@ public class HistoryMethodBinding extends BaseResourceReturningMethodBinding {
return true;
}
@Override
public IBundleProvider invokeServer(IRestfulServer<?> theServer, RequestDetails theRequest, Object[] theMethodParams) throws InvalidRequestException, InternalErrorException {
@ -139,18 +139,33 @@ public class HistoryMethodBinding extends BaseResourceReturningMethodBinding {
Object response = invokeServerMethod(theServer, theRequest, theMethodParams);
final IBundleProvider resources = toResourceList(response);
/*
* We wrap the response so we can verify that it has the ID and version set,
* as is the contract for history
*/
return new IBundleProvider() {
@Override
public String getCurrentPageId() {
return resources.getCurrentPageId();
}
@Override
public String getNextPageId() {
return resources.getNextPageId();
}
@Override
public String getPreviousPageId() {
return resources.getPreviousPageId();
}
@Override
public IPrimitiveType<Date> getPublished() {
return resources.getPublished();
}
@Override
public List<IBaseResource> getResources(int theFromIndex, int theToIndex) {
List<IBaseResource> retVal = resources.getResources(theFromIndex, theToIndex);
@ -170,10 +185,10 @@ public class HistoryMethodBinding extends BaseResourceReturningMethodBinding {
}
return retVal;
}
@Override
public Integer size() {
return resources.size();
public String getUuid() {
return resources.getUuid();
}
@Override
@ -182,8 +197,8 @@ public class HistoryMethodBinding extends BaseResourceReturningMethodBinding {
}
@Override
public String getUuid() {
return resources.getUuid();
public Integer size() {
return resources.size();
}
};
}

View File

@ -20,23 +20,30 @@ package ca.uhn.fhir.rest.server.method;
* #L%
*/
import static org.apache.commons.lang3.StringUtils.isBlank;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.model.valueset.BundleTypeEnum;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.RequestTypeEnum;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.IRestfulServer;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.IPagingProvider;
import ca.uhn.fhir.rest.server.RestfulServerUtils;
import ca.uhn.fhir.rest.server.RestfulServerUtils.ResponseEncoding;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
import org.hl7.fhir.instance.model.api.IBaseResource;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Set;
import org.hl7.fhir.instance.model.api.IBaseResource;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.model.valueset.BundleTypeEnum;
import ca.uhn.fhir.rest.api.*;
import ca.uhn.fhir.rest.api.server.*;
import ca.uhn.fhir.rest.server.IPagingProvider;
import ca.uhn.fhir.rest.server.RestfulServerUtils;
import ca.uhn.fhir.rest.server.RestfulServerUtils.ResponseEncoding;
import ca.uhn.fhir.rest.server.exceptions.*;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class PageMethodBinding extends BaseResourceReturningMethodBinding {
@ -75,34 +82,51 @@ public class PageMethodBinding extends BaseResourceReturningMethodBinding {
if (pagingProvider == null) {
throw new InvalidRequestException("This server does not support paging");
}
IBundleProvider resultList = pagingProvider.retrieveResultList(thePagingAction);
Integer offsetI;
int start = 0;
IBundleProvider resultList;
String pageId = null;
String[] pageIdParams = theRequest.getParameters().get(Constants.PARAM_PAGEID);
if (pageIdParams != null) {
if (pageIdParams.length > 0) {
if (isNotBlank(pageIdParams[0])) {
pageId = pageIdParams[0];
}
}
}
if (pageId != null) {
resultList = pagingProvider.retrieveResultList(thePagingAction, pageId);
} else {
resultList = pagingProvider.retrieveResultList(thePagingAction);
offsetI = RestfulServerUtils.tryToExtractNamedParameter(theRequest, Constants.PARAM_PAGINGOFFSET);
if (offsetI == null || offsetI < 0) {
offsetI = 0;
}
Integer totalNum = resultList.size();
start = offsetI;
if (totalNum != null) {
start = Math.min(start, totalNum - 1);
}
}
// Return an HTTP 409 if the search is not known
if (resultList == null) {
ourLog.info("Client requested unknown paging ID[{}]", thePagingAction);
String msg = getContext().getLocalizer().getMessage(PageMethodBinding.class, "unknownSearchId", thePagingAction);
throw new ResourceGoneException(msg);
}
Integer count = RestfulServerUtils.extractCountParameter(theRequest);
if (count == null) {
count = pagingProvider.getDefaultPageSize();
} else if (count > pagingProvider.getMaximumPageSize()) {
count = pagingProvider.getMaximumPageSize();
}
Integer offsetI = RestfulServerUtils.tryToExtractNamedParameter(theRequest, Constants.PARAM_PAGINGOFFSET);
if (offsetI == null || offsetI < 0) {
offsetI = 0;
}
Integer totalNum = resultList.size();
int start = offsetI;
if (totalNum != null) {
start = Math.min(start, totalNum - 1);
}
ResponseEncoding responseEncoding = RestfulServerUtils.determineResponseEncodingNoDefault(theRequest, theServer.getDefaultResponseEncoding());
Set<Include> includes = new HashSet<Include>();
Set<Include> includes = new HashSet<>();
String[] reqIncludes = theRequest.getParameters().get(Constants.PARAM_INCLUDE);
if (reqIncludes != null) {
for (String nextInclude : reqIncludes) {
@ -125,7 +149,14 @@ public class PageMethodBinding extends BaseResourceReturningMethodBinding {
if (responseEncoding != null) {
encodingEnum = responseEncoding.getEncoding();
}
Integer count = RestfulServerUtils.extractCountParameter(theRequest);
if (count == null) {
count = pagingProvider.getDefaultPageSize();
} else if (count > pagingProvider.getMaximumPageSize()) {
count = pagingProvider.getMaximumPageSize();
}
return createBundleFromBundleProvider(theServer, theRequest, count, linkSelf, includes, resultList, start, bundleType, encodingEnum, thePagingAction);
}
@ -140,10 +171,7 @@ public class PageMethodBinding extends BaseResourceReturningMethodBinding {
if (pageId == null || pageId.length == 0 || isBlank(pageId[0])) {
return false;
}
if (theRequest.getRequestType() != RequestTypeEnum.GET) {
return false;
}
return true;
return theRequest.getRequestType() == RequestTypeEnum.GET;
}

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.server.provider;
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

View File

@ -9,9 +9,9 @@ package org.hl7.fhir.r4.hapi.rest.server;
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -123,9 +123,9 @@ public class R4BundleFactory implements IVersionSpecificBundleFactory {
}
}
/*
* Actually add the resources to the bundle
*/
/*
* Actually add the resources to the bundle
*/
for (IBaseResource next : includedResources) {
BundleEntryComponent entry = myBundle.addEntry();
entry.setResource((Resource) next).getSearch().setMode(SearchEntryMode.INCLUDE);
@ -195,7 +195,7 @@ public class R4BundleFactory implements IVersionSpecificBundleFactory {
includedResources.addAll(addedResourcesThisPass);
// Linked resources may themselves have linked resources
references = new ArrayList<ResourceReferenceInfo>();
references = new ArrayList<>();
for (IAnyResource iResource : addedResourcesThisPass) {
List<ResourceReferenceInfo> newReferences = myContext.newTerser().getAllResourceReferences(iResource);
references.addAll(newReferences);
@ -219,9 +219,9 @@ public class R4BundleFactory implements IVersionSpecificBundleFactory {
}
}
/*
* Actually add the resources to the bundle
*/
/*
* Actually add the resources to the bundle
*/
for (IAnyResource next : includedResources) {
BundleEntryComponent entry = myBundle.addEntry();
entry.setResource((Resource) next).getSearch().setMode(SearchEntryMode.INCLUDE);

View File

@ -0,0 +1,226 @@
package ca.uhn.fhir.rest.server;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.util.PortUtil;
import ca.uhn.fhir.util.TestUtil;
import ca.uhn.fhir.util.UrlUtil;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.Validate;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.config.SocketConfig;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Patient;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.contains;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class PagingUsingNamedPagesR4Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(PagingUsingNamedPagesR4Test.class);
private static CloseableHttpClient ourClient;
private static FhirContext ourCtx = FhirContext.forR4();
private static int ourPort;
private static Server ourServer;
private static RestfulServer servlet;
private IPagingProvider myPagingProvider;
@Before
public void before() {
myPagingProvider = mock(IPagingProvider.class);
servlet.setPagingProvider(myPagingProvider);
ourNextBundleProvider = null;
}
private Bundle executeAndReturnBundle(HttpGet httpGet, EncodingEnum theExpectEncoding) throws IOException {
Bundle bundle;
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
EncodingEnum ct = EncodingEnum.forContentType(status.getEntity().getContentType().getValue().replaceAll(";.*", "").trim());
assertEquals(theExpectEncoding, ct);
assert ct != null;
bundle = ct.newParser(ourCtx).parseResource(Bundle.class, responseContent);
assertEquals(10, bundle.getEntry().size());
}
return bundle;
}
@Test
public void testPagingLinksSanitizeBundleType() throws Exception {
List<IBaseResource> patients0 = createPatients(0, 9);
BundleProviderWithNamedPages provider0 = new BundleProviderWithNamedPages(patients0, "SEARCHID0", "PAGEID0", 1000);
provider0.setNextPageId("PAGEID1");
when(myPagingProvider.retrieveResultList(eq("SEARCHID0"), eq("PAGEID0"))).thenReturn(provider0);
// Initial search
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "?_getpages=SEARCHID0&pageId=PAGEID0&_format=xml&_bundletype=FOO" + UrlUtil.escapeUrlParam("\""));
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(responseContent);
assertThat(responseContent, not(containsString("FOO\"")));
assertEquals(200, status.getStatusLine().getStatusCode());
EncodingEnum ct = EncodingEnum.forContentType(status.getEntity().getContentType().getValue().replaceAll(";.*", "").trim());
assert ct != null;
Bundle bundle = EncodingEnum.XML.newParser(ourCtx).parseResource(Bundle.class, responseContent);
assertEquals(10, bundle.getEntry().size());
}
}
@Test
public void testPaging() throws Exception {
List<IBaseResource> patients0 = createPatients(0, 9);
BundleProviderWithNamedPages provider0 = new BundleProviderWithNamedPages(patients0, "SEARCHID0", "PAGEID0", 1000);
provider0.setNextPageId("PAGEID1");
when(myPagingProvider.retrieveResultList(eq("SEARCHID0"), eq("PAGEID0"))).thenReturn(provider0);
List<IBaseResource> patients1 = createPatients(10, 19);
BundleProviderWithNamedPages provider1 = new BundleProviderWithNamedPages(patients1, "SEARCHID0", "PAGEID1", 1000);
provider1.setPreviousPageId("PAGEID0");
provider1.setNextPageId("PAGEID2");
when(myPagingProvider.retrieveResultList(eq("SEARCHID0"), eq("PAGEID1"))).thenReturn(provider1);
List<IBaseResource> patients2 = createPatients(20, 29);
BundleProviderWithNamedPages provider2 = new BundleProviderWithNamedPages(patients2, "SEARCHID0", "PAGEID2", 1000);
provider2.setPreviousPageId("PAGEID1");
when(myPagingProvider.retrieveResultList(eq("SEARCHID0"), eq("PAGEID2"))).thenReturn(provider2);
ourNextBundleProvider = provider0;
HttpGet httpGet;
String linkSelf;
String linkNext;
String linkPrev;
Bundle bundle;
// Initial search
httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_format=xml");
bundle = executeAndReturnBundle(httpGet, EncodingEnum.XML);
linkSelf = bundle.getLink(Constants.LINK_SELF).getUrl();
assertEquals("http://localhost:"+ourPort+"/Patient?_format=xml", linkSelf);
linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl();
assertEquals("http://localhost:"+ourPort+"?_getpages=SEARCHID0&pageId=PAGEID1&_format=xml&_bundletype=searchset", linkNext);
assertNull(bundle.getLink(Constants.LINK_PREVIOUS));
// Fetch the next page
httpGet = new HttpGet(linkNext);
bundle = executeAndReturnBundle(httpGet, EncodingEnum.XML);
linkSelf = bundle.getLink(Constants.LINK_SELF).getUrl();
assertEquals("http://localhost:"+ourPort+"?_getpages=SEARCHID0&pageId=PAGEID1&_format=xml&_bundletype=searchset", linkSelf);
linkNext = bundle.getLink(Constants.LINK_NEXT).getUrl();
assertEquals("http://localhost:"+ourPort+"?_getpages=SEARCHID0&pageId=PAGEID2&_format=xml&_bundletype=searchset", linkNext);
linkPrev = bundle.getLink(Constants.LINK_PREVIOUS).getUrl();
assertEquals("http://localhost:"+ourPort+"?_getpages=SEARCHID0&pageId=PAGEID0&_format=xml&_bundletype=searchset", linkPrev);
// Fetch the next page
httpGet = new HttpGet(linkNext);
bundle = executeAndReturnBundle(httpGet, EncodingEnum.XML);
linkSelf = bundle.getLink(Constants.LINK_SELF).getUrl();
assertEquals("http://localhost:"+ourPort+"?_getpages=SEARCHID0&pageId=PAGEID2&_format=xml&_bundletype=searchset", linkSelf);
assertNull(bundle.getLink(Constants.LINK_NEXT));
linkPrev = bundle.getLink(Constants.LINK_PREVIOUS).getUrl();
assertEquals("http://localhost:"+ourPort+"?_getpages=SEARCHID0&pageId=PAGEID1&_format=xml&_bundletype=searchset", linkPrev);
}
private List<IBaseResource> createPatients(int theLow, int theHigh) {
List<IBaseResource> patients = new ArrayList<>();
for (int id = theLow; id <= theHigh; id++) {
Patient pt = new Patient();
pt.setId("Patient/" + id);
pt.addName().setFamily("FAM" + id);
patients.add(pt);
}
return patients;
}
@AfterClass
public static void afterClassClearContext() throws Exception {
ourServer.stop();
TestUtil.clearAllStaticFieldsForUnitTest();
}
@BeforeClass
public static void beforeClass() throws Exception {
ourPort = PortUtil.findFreePort();
ourServer = new Server(ourPort);
DummyPatientResourceProvider patientProvider = new DummyPatientResourceProvider();
ServletHandler proxyHandler = new ServletHandler();
servlet = new RestfulServer(ourCtx);
servlet.setDefaultResponseEncoding(EncodingEnum.JSON);
servlet.setPagingProvider(new FifoMemoryPagingProvider(10));
servlet.setResourceProviders(patientProvider);
ServletHolder servletHolder = new ServletHolder(servlet);
proxyHandler.addServletWithMapping(servletHolder, "/*");
ourServer.setHandler(proxyHandler);
ourServer.start();
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS);
HttpClientBuilder builder = HttpClientBuilder.create();
builder.setConnectionManager(connectionManager);
builder.setDefaultSocketConfig(SocketConfig.custom().setSoTimeout(600000).build());
ourClient = builder.build();
}
private static IBundleProvider ourNextBundleProvider;
public static class DummyPatientResourceProvider implements IResourceProvider {
@Override
public Class<? extends IBaseResource> getResourceType() {
return Patient.class;
}
@SuppressWarnings("rawtypes")
@Search()
public IBundleProvider search() {
IBundleProvider retVal = ourNextBundleProvider;
Validate.notNull(retVal);
ourNextBundleProvider = null;
return retVal;
}
}
}

View File

@ -214,6 +214,18 @@
code to specify a search UUID, and a field to allow the preferred
page size to be configured.
</action>
<action type="add">
The JPA server search UUID column has been reduced in length from
40 chars to 36, in order to align with the actual length of the
generated UUIDs.
</action>
<action type="add">
Plain servers using paging may now specify an ID/name for
individual pages being returned, avoiding the need to
respond to arbitrary offset/index requests from the server.
In this mode, page links in search result bundles simply
include the ID to the next page.
</action>
</release>
<release version="3.4.0" date="2018-05-28">
<action type="add">

View File

@ -336,7 +336,34 @@
</macro>
</subsection>
<subsection name="Using Named Pages">
<p>
By default, the paging system uses parameters that are embedded into the
page links for the start index and the page size. This is useful for servers that
can retrieve arbitrary offsets within a search result. For example,
if a given search can easily retrieve "items 5-10 from the given search", then
the mechanism above works well.
</p>
<p>
Another option is to use "named pages", meaning that each
page is simply assigned an ID by the server, and the next/previous
page is requested using this ID.
</p>
<p>
In order to support named pages, the IPagingProvider must
implement the
<code>retrieveResultList(String theSearchId, String thePageId)</code>
method.
</p>
<p>
Then, individual search/history methods may return a
<code>BundleProviderWithNamedPages</code> instead of a simple
<code>IBundleProvider</code>.
</p>
</subsection>
</section>