View Javadoc
1   /*
2    * Copyright 2016 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *       http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.openehealth.ipf.commons.ihe.fhir;
18  
19  import com.google.common.collect.DiscreteDomain;
20  import com.google.common.collect.Range;
21  import com.google.common.collect.RangeSet;
22  import com.google.common.collect.TreeRangeSet;
23  import org.hl7.fhir.instance.model.api.IBaseResource;
24  import org.slf4j.Logger;
25  import org.slf4j.LoggerFactory;
26  
27  import java.util.ArrayList;
28  import java.util.List;
29  import java.util.Map;
30  
31  import static org.openehealth.ipf.commons.ihe.fhir.Constants.FHIR_FROM_INDEX;
32  import static org.openehealth.ipf.commons.ihe.fhir.Constants.FHIR_REQUEST_SIZE_ONLY;
33  import static org.openehealth.ipf.commons.ihe.fhir.Constants.FHIR_TO_INDEX;
34  
35  /**
36   * Bundle provider that requests information from the {@link RequestConsumer} on request:
37   * <ul>
38   * <li>If only the size of the result set is requested, the request will contain an additional empty message header named
39   * {@link Constants#FHIR_REQUEST_SIZE_ONLY}, and the response is expected to populate this
40   * header with the result size as integer value. The size is cached for further attempts to request the size</li>
41   * <li>
42   * If a subset of the result is requested, the request will contain in addition the lower and upper index in the message
43   * headers {@link Constants#FHIR_FROM_INDEX} and {@link Constants#FHIR_TO_INDEX}, respectively. The response is
44   * expected to contain this result subset.
45   * </li>
46   * </ul>
47   * <p>
48   * Note: instances of this class is neither thread-safe nor can they be reused across requests
49   * </p>
50   */
51  public class LazyBundleProvider extends AbstractBundleProvider {
52  
53      private static final Logger LOG = LoggerFactory.getLogger(LazyBundleProvider.class);
54  
55      private final boolean cacheResults;
56      private int size = -1;
57      private transient List<IBaseResource> cachedResults = new ArrayList<>();
58      private transient ResultRanges resultRanges = new ResultRanges();
59  
60      /**
61       * Initializes a lazy bundle provider
62       *
63       * @param consumer     FHIR consumer that uses ths provider
64       * @param cacheResults cache results. So far, only the result set size is cached
65       * @param payload      incoming payload
66       * @param headers      incoming headers
67       */
68      public LazyBundleProvider(RequestConsumer consumer, boolean cacheResults, Object payload, Map<String, Object> headers) {
69          super(consumer, payload, headers);
70          this.cacheResults = cacheResults;
71      }
72  
73      @Override
74      public List<IBaseResource> getResources(int fromIndex, int toIndex) {
75          if (!cacheResults) {
76              return getPartialResult(fromIndex, toIndex);
77          }
78          LOG.debug("Cached results contain the following ranges: {}. Requesting resources from index {} to {}", resultRanges, fromIndex, toIndex);
79          Range<Integer> wanted = Range.closedOpen(fromIndex, toIndex);
80          RangeSet<Integer> needed = resultRanges.required(wanted);
81          LOG.debug("Requiring the following ranges {}", needed);
82          for (Range<Integer> requiredRange : needed.asDescendingSetOfRanges()) {
83              LOG.debug("Now requesting the following range {}", requiredRange);
84              List<IBaseResource> results = getPartialResult(requiredRange.lowerEndpoint(), requiredRange.upperEndpoint());
85              LOG.debug("Got back a list of size {}", results.size());
86              if (!results.isEmpty()) {
87                  cacheAll(requiredRange.lowerEndpoint(), results);
88                  // Take care, potentially less elements than requested have been retrieved
89                  resultRanges.add(Range.closedOpen(requiredRange.lowerEndpoint(), requiredRange.lowerEndpoint() + results.size()));
90              }
91          }
92          LOG.debug("Cached results now contain the following ranges: {}", resultRanges);
93  
94          // Everything went OK, return whatever we got
95          return cachedResults.subList(fromIndex, Math.min(cachedResults.size(), Math.min(cachedResults.size(), toIndex)));
96      }
97  
98      private List<IBaseResource> getPartialResult(int fromIndex, int toIndex) {
99          Map<String, Object> headers = getHeaders();
100         headers.put(FHIR_FROM_INDEX, fromIndex);
101         headers.put(FHIR_TO_INDEX, toIndex);
102         return obtainResources(getPayload(), headers);
103     }
104 
105     @Override
106     public Integer size() {
107         if (!cacheResults || size < 0) {
108             Map<String, Object> headers = getHeaders();
109             headers.put(FHIR_REQUEST_SIZE_ONLY, null);
110             size = getConsumer().handleSizeRequest(getPayload(), headers);
111         }
112         return size;
113     }
114 
115     private void cacheAll(int fromIndex, List<IBaseResource> resources) {
116         if (cachedResults.size() <= fromIndex) {
117             for (int i = cachedResults.size(); i < fromIndex; i++) {
118                 LOG.debug("Adding null for index {}", i);
119                 cachedResults.add(null);
120             }
121             cachedResults.addAll(resources);
122         } else {
123             for (int i = 0; i < resources.size(); i++) {
124                 cachedResults.set(fromIndex + i, resources.get(i));
125             }
126         }
127     }
128 
129     private static class ResultRanges {
130 
131         private RangeSet<Integer> rangeSet = TreeRangeSet.create();
132 
133         /**
134          * @param wantedRange the range of elements the caller wants to retrieve
135          * @return the ranges that actually need to be retrieved
136          */
137         public RangeSet<Integer> required(Range<Integer> wantedRange) {
138             RangeSet<Integer> intersection = rangeSet.subRangeSet(wantedRange);
139             return TreeRangeSet.create(intersection.complement().subRangeSet(wantedRange));
140         }
141 
142         public void add(Range<Integer> wantedRange) {
143             rangeSet.add(wantedRange.canonical(DiscreteDomain.integers()));
144         }
145 
146         @Override
147         public String toString() {
148             return rangeSet.toString();
149         }
150     }
151 
152 
153 
154 
155 }