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.ResourceTable;
024import ca.uhn.fhir.jpa.model.entity.StorageSettings;
025import ca.uhn.fhir.model.dstu2.composite.CodingDt;
026import com.google.common.collect.HashMultimap;
027import com.google.common.collect.Multimaps;
028import com.google.common.collect.SetMultimap;
029import org.hibernate.search.engine.backend.document.DocumentElement;
030import org.hl7.fhir.instance.model.api.IBaseCoding;
031import org.hl7.fhir.instance.model.api.IBaseResource;
032import org.slf4j.Logger;
033import org.slf4j.LoggerFactory;
034
035import java.math.BigDecimal;
036import java.util.Date;
037import java.util.Objects;
038import java.util.function.BiConsumer;
039
040/**
041 * Collects our HSearch extended indexing data.
042 *
043 */
044public class ExtendedHSearchIndexData {
045        private static final Logger ourLog = LoggerFactory.getLogger(ExtendedHSearchIndexData.class);
046
047        final FhirContext myFhirContext;
048        final StorageSettings myStorageSettings;
049
050        final SetMultimap<String, String> mySearchParamStrings = HashMultimap.create();
051        final SetMultimap<String, IBaseCoding> mySearchParamTokens = HashMultimap.create();
052        final SetMultimap<String, BigDecimal> mySearchParamNumbers = HashMultimap.create();
053        final SetMultimap<String, String> mySearchParamLinks = HashMultimap.create();
054        final SetMultimap<String, String> mySearchParamUri = HashMultimap.create();
055        final SetMultimap<String, DateSearchIndexData> mySearchParamDates = HashMultimap.create();
056        final SetMultimap<String, QuantitySearchIndexData> mySearchParamQuantities = HashMultimap.create();
057        final SetMultimap<String, CompositeSearchIndexData> mySearchParamComposites = HashMultimap.create();
058        private String myForcedId;
059        private String myResourceJSON;
060        private IBaseResource myResource;
061        private ResourceTable myEntity;
062
063        public ExtendedHSearchIndexData(
064                        FhirContext theFhirContext,
065                        StorageSettings theStorageSettings,
066                        IBaseResource theResource,
067                        ResourceTable theEntity) {
068                this.myFhirContext = theFhirContext;
069                this.myStorageSettings = theStorageSettings;
070                myResource = theResource;
071                myEntity = theEntity;
072        }
073
074        private <V> BiConsumer<String, V> ifNotContained(BiConsumer<String, V> theIndexWriter) {
075                return (s, v) -> {
076                        // Ignore contained resources for now.
077                        if (!s.contains(".")) {
078                                theIndexWriter.accept(s, v);
079                        }
080                };
081        }
082
083        /**
084         * Write the index document.
085         *
086         * Called by Hibernate Search after the ResourceTable entity has been flushed/committed.
087         * Keep this in sync with the schema defined in {@link SearchParamTextPropertyBinder}
088         *
089         * @param theDocument the Hibernate Search document for ResourceTable
090         */
091        public void writeIndexElements(DocumentElement theDocument) {
092                HSearchIndexWriter indexWriter = HSearchIndexWriter.forRoot(myStorageSettings, theDocument);
093
094                ourLog.debug("Writing JPA index to Hibernate Search");
095
096                // todo can this be moved back to ResourceTable as a computed field to merge with myId?
097                theDocument.addValue("myForcedId", myForcedId);
098
099                if (myResourceJSON != null) {
100                        theDocument.addValue("myRawResource", myResourceJSON);
101                }
102
103                mySearchParamStrings.forEach(ifNotContained(indexWriter::writeStringIndex));
104                mySearchParamTokens.forEach(ifNotContained(indexWriter::writeTokenIndex));
105                mySearchParamLinks.forEach(ifNotContained(indexWriter::writeReferenceIndex));
106                // we want to receive the whole entry collection for each invocation
107                mySearchParamQuantities.forEach(ifNotContained(indexWriter::writeQuantityIndex));
108                Multimaps.asMap(mySearchParamNumbers).forEach(ifNotContained(indexWriter::writeNumberIndex));
109                mySearchParamDates.forEach(ifNotContained(indexWriter::writeDateIndex));
110                Multimaps.asMap(mySearchParamUri).forEach(ifNotContained(indexWriter::writeUriIndex));
111                Multimaps.asMap(mySearchParamComposites).forEach(indexWriter::writeCompositeIndex);
112        }
113
114        public void addStringIndexData(String theSpName, String theText) {
115                mySearchParamStrings.put(theSpName, theText);
116        }
117
118        /**
119         * Add if not already present.
120         */
121        public void addTokenIndexDataIfNotPresent(String theSpName, String theSystem, String theValue) {
122                boolean isPresent = mySearchParamTokens.get(theSpName).stream()
123                                .anyMatch(c -> Objects.equals(c.getSystem(), theSystem) && Objects.equals(c.getCode(), theValue));
124                if (!isPresent) {
125                        addTokenIndexData(theSpName, new CodingDt(theSystem, theValue));
126                }
127        }
128
129        public void addTokenIndexData(String theSpName, IBaseCoding theNextValue) {
130                mySearchParamTokens.put(theSpName, theNextValue);
131        }
132
133        public void addUriIndexData(String theSpName, String theValue) {
134                mySearchParamUri.put(theSpName, theValue);
135        }
136
137        public void addResourceLinkIndexData(String theSpName, String theTargetResourceId) {
138                mySearchParamLinks.put(theSpName, theTargetResourceId);
139        }
140
141        public void addDateIndexData(
142                        String theSpName,
143                        Date theLowerBound,
144                        int theLowerBoundOrdinal,
145                        Date theUpperBound,
146                        int theUpperBoundOrdinal) {
147                addDateIndexData(
148                                theSpName,
149                                new DateSearchIndexData(theLowerBound, theLowerBoundOrdinal, theUpperBound, theUpperBoundOrdinal));
150        }
151
152        public void addDateIndexData(String theSpName, DateSearchIndexData value) {
153                mySearchParamDates.put(theSpName, value);
154        }
155
156        public SetMultimap<String, DateSearchIndexData> getDateIndexData() {
157                return mySearchParamDates;
158        }
159
160        public void addNumberIndexDataIfNotPresent(String theParamName, BigDecimal theValue) {
161                mySearchParamNumbers.put(theParamName, theValue);
162        }
163
164        public void addQuantityIndexData(String theSpName, QuantitySearchIndexData value) {
165                mySearchParamQuantities.put(theSpName, value);
166        }
167
168        public SetMultimap<String, QuantitySearchIndexData> getQuantityIndexData() {
169                return mySearchParamQuantities;
170        }
171
172        public void setForcedId(String theForcedId) {
173                myForcedId = theForcedId;
174        }
175
176        public String getForcedId() {
177                return myForcedId;
178        }
179
180        public void setRawResourceData(String theResourceJSON) {
181                myResourceJSON = theResourceJSON;
182        }
183
184        public SetMultimap<String, CompositeSearchIndexData> getSearchParamComposites() {
185                return mySearchParamComposites;
186        }
187
188        public void addCompositeIndexData(String theSearchParamName, CompositeSearchIndexData theBuildCompositeIndexData) {
189                mySearchParamComposites.put(theSearchParamName, theBuildCompositeIndexData);
190        }
191}