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.jpa.model.entity.StorageSettings;
023import ca.uhn.fhir.jpa.model.util.UcumServiceUtil;
024import org.apache.commons.lang3.StringUtils;
025import org.fhir.ucum.Pair;
026import org.hibernate.search.engine.backend.document.DocumentElement;
027import org.hl7.fhir.instance.model.api.IBaseCoding;
028import org.slf4j.Logger;
029import org.slf4j.LoggerFactory;
030
031import java.math.BigDecimal;
032import java.util.Collection;
033import java.util.Set;
034
035public class HSearchIndexWriter {
036        private static final Logger ourLog = LoggerFactory.getLogger(HSearchIndexWriter.class);
037
038        public static final String NESTED_SEARCH_PARAM_ROOT = "nsp";
039        public static final String SEARCH_PARAM_ROOT = "sp";
040        public static final String INDEX_TYPE_STRING = "string";
041        public static final String IDX_STRING_NORMALIZED = "norm";
042        public static final String IDX_STRING_EXACT = "exact";
043        public static final String IDX_STRING_TEXT = "text";
044        public static final String IDX_STRING_LOWER = "lower";
045
046        public static final String INDEX_TYPE_TOKEN = "token";
047        public static final String TOKEN_CODE = "code";
048        public static final String TOKEN_SYSTEM = "system";
049        public static final String TOKEN_SYSTEM_CODE = "code-system";
050        public static final String INDEX_TYPE_QUANTITY = "quantity";
051
052        // numeric
053        public static final String VALUE_FIELD = "value";
054        public static final String QTY_CODE = TOKEN_CODE;
055        public static final String QTY_SYSTEM = TOKEN_SYSTEM;
056        public static final String QTY_VALUE = VALUE_FIELD;
057        public static final String QTY_CODE_NORM = "code-norm";
058        public static final String QTY_VALUE_NORM = "value-norm";
059
060        public static final String URI_VALUE = "uri-value";
061
062        public static final String NUMBER_VALUE = "number-value";
063
064        public static final String DATE_LOWER_ORD = "lower-ord";
065        public static final String DATE_LOWER = "lower";
066        public static final String DATE_UPPER_ORD = "upper-ord";
067        public static final String DATE_UPPER = "upper";
068
069        final HSearchElementCache myNodeCache;
070        final StorageSettings myStorageSettings;
071
072        HSearchIndexWriter(StorageSettings theStorageSettings, DocumentElement theRoot) {
073                myStorageSettings = theStorageSettings;
074                myNodeCache = new HSearchElementCache(theRoot);
075        }
076
077        public DocumentElement getSearchParamIndexNode(String theSearchParamName, String theIndexType) {
078                return myNodeCache.getObjectElement(SEARCH_PARAM_ROOT, theSearchParamName, theIndexType);
079        }
080
081        public static HSearchIndexWriter forRoot(StorageSettings theStorageSettings, DocumentElement theDocument) {
082                return new HSearchIndexWriter(theStorageSettings, theDocument);
083        }
084
085        public void writeStringIndex(String theSearchParam, String theValue) {
086                DocumentElement stringIndexNode = getSearchParamIndexNode(theSearchParam, INDEX_TYPE_STRING);
087
088                // we are assuming that our analyzer matches
089                // StringUtil.normalizeStringForSearchIndexing(theValue).toLowerCase(Locale.ROOT))
090                writeBasicStringFields(stringIndexNode, theValue);
091                addDocumentValue(stringIndexNode, IDX_STRING_EXACT, theValue);
092                addDocumentValue(stringIndexNode, IDX_STRING_TEXT, theValue);
093                addDocumentValue(stringIndexNode, IDX_STRING_LOWER, theValue);
094
095                ourLog.debug("Adding Search Param Text: {} -- {}", theSearchParam, theValue);
096        }
097
098        public void writeBasicStringFields(DocumentElement theIndexNode, String theValue) {
099                addDocumentValue(theIndexNode, IDX_STRING_NORMALIZED, theValue);
100        }
101
102        public void writeTokenIndex(String theSearchParam, IBaseCoding theValue) {
103                DocumentElement nestedRoot = myNodeCache.getObjectElement(NESTED_SEARCH_PARAM_ROOT);
104                DocumentElement nestedSpNode = nestedRoot.addObject(theSearchParam);
105                DocumentElement nestedTokenNode = nestedSpNode.addObject(INDEX_TYPE_TOKEN);
106
107                writeTokenFields(nestedTokenNode, theValue);
108
109                if (StringUtils.isNotEmpty(theValue.getDisplay())) {
110                        DocumentElement nestedStringNode = nestedSpNode.addObject(INDEX_TYPE_STRING);
111                        addDocumentValue(nestedStringNode, IDX_STRING_TEXT, theValue.getDisplay());
112                }
113
114                DocumentElement tokenIndexNode = getSearchParamIndexNode(theSearchParam, INDEX_TYPE_TOKEN);
115                writeTokenFields(tokenIndexNode, theValue);
116                ourLog.debug("Adding Search Param Token: {} -- {}", theSearchParam, theValue);
117        }
118
119        public void writeTokenFields(DocumentElement theDocumentElement, IBaseCoding theValue) {
120                addDocumentValue(theDocumentElement, TOKEN_CODE, theValue.getCode());
121                addDocumentValue(theDocumentElement, TOKEN_SYSTEM, theValue.getSystem());
122                addDocumentValue(theDocumentElement, TOKEN_SYSTEM_CODE, theValue.getSystem() + "|" + theValue.getCode());
123        }
124
125        private void addDocumentValue(DocumentElement theDocumentElement, String theKey, Object theValue) {
126                if (theValue != null) {
127                        theDocumentElement.addValue(theKey, theValue);
128                }
129        }
130
131        public void writeReferenceIndex(String theSearchParam, String theValue) {
132                DocumentElement referenceIndexNode = getSearchParamIndexNode(theSearchParam, "reference");
133                addDocumentValue(referenceIndexNode, VALUE_FIELD, theValue);
134                ourLog.trace("Adding Search Param Reference: {} -- {}", theSearchParam, theValue);
135        }
136
137        public void writeDateIndex(String theSearchParam, DateSearchIndexData theValue) {
138                DocumentElement dateIndexNode = getSearchParamIndexNode(theSearchParam, "dt");
139                writeDateFields(dateIndexNode, theValue);
140
141                ourLog.trace("Adding Search Param Date. param: {} -- {}", theSearchParam, theValue);
142        }
143
144        public void writeDateFields(DocumentElement dateIndexNode, DateSearchIndexData theValue) {
145                // Lower bound
146                addDocumentValue(dateIndexNode, DATE_LOWER_ORD, theValue.getLowerBoundOrdinal());
147                addDocumentValue(dateIndexNode, DATE_LOWER, theValue.getLowerBoundDate().toInstant());
148                // Upper bound
149                addDocumentValue(dateIndexNode, DATE_UPPER_ORD, theValue.getUpperBoundOrdinal());
150                addDocumentValue(dateIndexNode, DATE_UPPER, theValue.getUpperBoundDate().toInstant());
151        }
152
153        public void writeQuantityIndex(String theSearchParam, QuantitySearchIndexData theValue) {
154                DocumentElement nestedRoot = myNodeCache.getObjectElement(NESTED_SEARCH_PARAM_ROOT);
155
156                DocumentElement nestedSpNode = nestedRoot.addObject(theSearchParam);
157                DocumentElement nestedQtyNode = nestedSpNode.addObject(INDEX_TYPE_QUANTITY);
158
159                ourLog.trace("Adding Search Param Quantity: {} -- {}", theSearchParam, theValue);
160                writeQuantityFields(nestedQtyNode, theValue);
161        }
162
163        public void writeQuantityFields(DocumentElement nestedQtyNode, QuantitySearchIndexData theValue) {
164                addDocumentValue(nestedQtyNode, QTY_CODE, theValue.getCode());
165                addDocumentValue(nestedQtyNode, QTY_SYSTEM, theValue.getSystem());
166                addDocumentValue(nestedQtyNode, QTY_VALUE, theValue.getValue());
167
168                if (!myStorageSettings.getNormalizedQuantitySearchLevel().storageOrSearchSupported()) {
169                        return;
170                }
171
172                // -- convert the value/unit to the canonical form if any
173                Pair canonicalForm = UcumServiceUtil.getCanonicalForm(
174                                theValue.getSystem(), BigDecimal.valueOf(theValue.getValue()), theValue.getCode());
175                if (canonicalForm == null) {
176                        return;
177                }
178
179                double canonicalValue = Double.parseDouble(canonicalForm.getValue().asDecimal());
180                String canonicalUnits = canonicalForm.getCode();
181
182                addDocumentValue(nestedQtyNode, QTY_CODE_NORM, canonicalUnits);
183                addDocumentValue(nestedQtyNode, QTY_VALUE_NORM, canonicalValue);
184        }
185
186        public void writeUriIndex(String theParamName, Collection<String> theUriValueCollection) {
187                DocumentElement uriNode =
188                                myNodeCache.getObjectElement(SEARCH_PARAM_ROOT).addObject(theParamName);
189                for (String uriSearchIndexValue : theUriValueCollection) {
190                        ourLog.trace("Adding Search Param Uri: {} -- {}", theParamName, uriSearchIndexValue);
191                        writeUriFields(uriNode, uriSearchIndexValue);
192                }
193        }
194
195        public void writeUriFields(DocumentElement uriNode, String uriSearchIndexValue) {
196                addDocumentValue(uriNode, URI_VALUE, uriSearchIndexValue);
197        }
198
199        public void writeNumberIndex(String theParamName, Collection<BigDecimal> theNumberValueCollection) {
200                DocumentElement numberNode =
201                                myNodeCache.getObjectElement(SEARCH_PARAM_ROOT).addObject(theParamName);
202                for (BigDecimal numberSearchIndexValue : theNumberValueCollection) {
203                        ourLog.trace("Adding Search Param Number: {} -- {}", theParamName, numberSearchIndexValue);
204                        writeNumberFields(numberNode, numberSearchIndexValue);
205                }
206        }
207
208        public void writeNumberFields(DocumentElement numberNode, BigDecimal numberSearchIndexValue) {
209                addDocumentValue(numberNode, NUMBER_VALUE, numberSearchIndexValue.doubleValue());
210        }
211
212        /**
213         * @param ignoredParamName unused - for consistent api
214         * @param theCompositeSearchIndexData extracted index data for this sp
215         */
216        public void writeCompositeIndex(
217                        String ignoredParamName, Set<CompositeSearchIndexData> theCompositeSearchIndexData) {
218                // must be nested.
219                for (CompositeSearchIndexData compositeSearchIndexDatum : theCompositeSearchIndexData) {
220                        compositeSearchIndexDatum.writeIndexEntry(this, myNodeCache);
221                }
222        }
223}