001/*-
002 * #%L
003 * HAPI FHIR JPA Model
004 * %%
005 * Copyright (C) 2014 - 2024 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.jpa.model.search;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.jpa.model.entity.StorageSettings;
024import ca.uhn.fhir.model.dstu2.composite.CodingDt;
025import com.google.common.collect.HashMultimap;
026import com.google.common.collect.Multimaps;
027import com.google.common.collect.SetMultimap;
028import org.hibernate.search.engine.backend.document.DocumentElement;
029import org.hl7.fhir.instance.model.api.IBaseCoding;
030import org.hl7.fhir.instance.model.api.IBaseResource;
031import org.slf4j.Logger;
032import org.slf4j.LoggerFactory;
033
034import java.math.BigDecimal;
035import java.util.Date;
036import java.util.Objects;
037import java.util.function.BiConsumer;
038
039/**
040 * Collects our HSearch extended indexing data.
041 *
042 */
043public class ExtendedHSearchIndexData {
044        private static final Logger ourLog = LoggerFactory.getLogger(ExtendedHSearchIndexData.class);
045
046        final FhirContext myFhirContext;
047        final StorageSettings myStorageSettings;
048
049        final SetMultimap<String, String> mySearchParamStrings = HashMultimap.create();
050        final SetMultimap<String, IBaseCoding> mySearchParamTokens = HashMultimap.create();
051        final SetMultimap<String, BigDecimal> mySearchParamNumbers = HashMultimap.create();
052        final SetMultimap<String, String> mySearchParamLinks = HashMultimap.create();
053        final SetMultimap<String, String> mySearchParamUri = HashMultimap.create();
054        final SetMultimap<String, DateSearchIndexData> mySearchParamDates = HashMultimap.create();
055        final SetMultimap<String, QuantitySearchIndexData> mySearchParamQuantities = HashMultimap.create();
056        final SetMultimap<String, CompositeSearchIndexData> mySearchParamComposites = HashMultimap.create();
057        private String myForcedId;
058        private String myResourceJSON;
059        private IBaseResource myResource;
060
061        public ExtendedHSearchIndexData(
062                        FhirContext theFhirContext, StorageSettings theStorageSettings, IBaseResource theResource) {
063                this.myFhirContext = theFhirContext;
064                this.myStorageSettings = theStorageSettings;
065                myResource = theResource;
066        }
067
068        private <V> BiConsumer<String, V> ifNotContained(BiConsumer<String, V> theIndexWriter) {
069                return (s, v) -> {
070                        // Ignore contained resources for now.
071                        if (!s.contains(".")) {
072                                theIndexWriter.accept(s, v);
073                        }
074                };
075        }
076
077        /**
078         * Write the index document.
079         *
080         * Called by Hibernate Search after the ResourceTable entity has been flushed/committed.
081         * Keep this in sync with the schema defined in {@link SearchParamTextPropertyBinder}
082         *
083         * @param theDocument the Hibernate Search document for ResourceTable
084         */
085        public void writeIndexElements(DocumentElement theDocument) {
086                HSearchIndexWriter indexWriter = HSearchIndexWriter.forRoot(myStorageSettings, theDocument);
087
088                ourLog.debug("Writing JPA index to Hibernate Search");
089
090                // todo can this be moved back to ResourceTable as a computed field to merge with myId?
091                theDocument.addValue("myForcedId", myForcedId);
092
093                if (myResourceJSON != null) {
094                        theDocument.addValue("myRawResource", myResourceJSON);
095                }
096
097                mySearchParamStrings.forEach(ifNotContained(indexWriter::writeStringIndex));
098                mySearchParamTokens.forEach(ifNotContained(indexWriter::writeTokenIndex));
099                mySearchParamLinks.forEach(ifNotContained(indexWriter::writeReferenceIndex));
100                // we want to receive the whole entry collection for each invocation
101                mySearchParamQuantities.forEach(ifNotContained(indexWriter::writeQuantityIndex));
102                Multimaps.asMap(mySearchParamNumbers).forEach(ifNotContained(indexWriter::writeNumberIndex));
103                mySearchParamDates.forEach(ifNotContained(indexWriter::writeDateIndex));
104                Multimaps.asMap(mySearchParamUri).forEach(ifNotContained(indexWriter::writeUriIndex));
105                Multimaps.asMap(mySearchParamComposites).forEach(indexWriter::writeCompositeIndex);
106        }
107
108        public void addStringIndexData(String theSpName, String theText) {
109                mySearchParamStrings.put(theSpName, theText);
110        }
111
112        /**
113         * Add if not already present.
114         */
115        public void addTokenIndexDataIfNotPresent(String theSpName, String theSystem, String theValue) {
116                boolean isPresent = mySearchParamTokens.get(theSpName).stream()
117                                .anyMatch(c -> Objects.equals(c.getSystem(), theSystem) && Objects.equals(c.getCode(), theValue));
118                if (!isPresent) {
119                        addTokenIndexData(theSpName, new CodingDt(theSystem, theValue));
120                }
121        }
122
123        public void addTokenIndexData(String theSpName, IBaseCoding theNextValue) {
124                mySearchParamTokens.put(theSpName, theNextValue);
125        }
126
127        public void addUriIndexData(String theSpName, String theValue) {
128                mySearchParamUri.put(theSpName, theValue);
129        }
130
131        public void addResourceLinkIndexData(String theSpName, String theTargetResourceId) {
132                mySearchParamLinks.put(theSpName, theTargetResourceId);
133        }
134
135        public void addDateIndexData(
136                        String theSpName,
137                        Date theLowerBound,
138                        int theLowerBoundOrdinal,
139                        Date theUpperBound,
140                        int theUpperBoundOrdinal) {
141                addDateIndexData(
142                                theSpName,
143                                new DateSearchIndexData(theLowerBound, theLowerBoundOrdinal, theUpperBound, theUpperBoundOrdinal));
144        }
145
146        public void addDateIndexData(String theSpName, DateSearchIndexData value) {
147                mySearchParamDates.put(theSpName, value);
148        }
149
150        public SetMultimap<String, DateSearchIndexData> getDateIndexData() {
151                return mySearchParamDates;
152        }
153
154        public void addNumberIndexDataIfNotPresent(String theParamName, BigDecimal theValue) {
155                mySearchParamNumbers.put(theParamName, theValue);
156        }
157
158        public void addQuantityIndexData(String theSpName, QuantitySearchIndexData value) {
159                mySearchParamQuantities.put(theSpName, value);
160        }
161
162        public SetMultimap<String, QuantitySearchIndexData> getQuantityIndexData() {
163                return mySearchParamQuantities;
164        }
165
166        public void setForcedId(String theForcedId) {
167                myForcedId = theForcedId;
168        }
169
170        public String getForcedId() {
171                return myForcedId;
172        }
173
174        public void setRawResourceData(String theResourceJSON) {
175                myResourceJSON = theResourceJSON;
176        }
177
178        public SetMultimap<String, CompositeSearchIndexData> getSearchParamComposites() {
179                return mySearchParamComposites;
180        }
181
182        public void addCompositeIndexData(String theSearchParamName, CompositeSearchIndexData theBuildCompositeIndexData) {
183                mySearchParamComposites.put(theSearchParamName, theBuildCompositeIndexData);
184        }
185}