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}