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.entity;
021
022import ca.uhn.fhir.i18n.Msg;
023import ca.uhn.fhir.interceptor.model.RequestPartitionId;
024import ca.uhn.fhir.jpa.model.config.PartitionSettings;
025import ca.uhn.fhir.model.api.IQueryParameterType;
026import ca.uhn.fhir.rest.api.Constants;
027import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
028import ca.uhn.fhir.util.UrlUtil;
029import com.google.common.base.Charsets;
030import com.google.common.hash.HashCode;
031import com.google.common.hash.HashFunction;
032import com.google.common.hash.Hasher;
033import com.google.common.hash.Hashing;
034import jakarta.persistence.Column;
035import jakarta.persistence.MappedSuperclass;
036import jakarta.persistence.Temporal;
037import jakarta.persistence.TemporalType;
038import jakarta.persistence.Transient;
039import org.apache.commons.lang3.StringUtils;
040import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField;
041import org.hibernate.search.mapper.pojo.mapping.definition.annotation.GenericField;
042
043import java.util.Date;
044import java.util.List;
045
046@MappedSuperclass
047public abstract class BaseResourceIndexedSearchParam extends BaseResourceIndex {
048        static final int MAX_SP_NAME = 100;
049        /**
050         * Don't change this without careful consideration. You will break existing hashes!
051         */
052        private static final HashFunction HASH_FUNCTION = Hashing.murmur3_128(0);
053
054        /**
055         * Don't make this public 'cause nobody better be able to modify it!
056         */
057        private static final byte[] DELIMITER_BYTES = "|".getBytes(Charsets.UTF_8);
058
059        private static final long serialVersionUID = 1L;
060
061        @GenericField
062        @Column(name = "SP_MISSING", nullable = false)
063        private boolean myMissing = false;
064
065        @FullTextField
066        @Column(name = "SP_NAME", length = MAX_SP_NAME, nullable = false)
067        private String myParamName;
068
069        @Column(name = "RES_ID", insertable = false, updatable = false, nullable = false)
070        private Long myResourcePid;
071
072        @FullTextField
073        @Column(name = "RES_TYPE", updatable = false, nullable = false, length = Constants.MAX_RESOURCE_NAME_LENGTH)
074        private String myResourceType;
075
076        @GenericField
077        @Column(name = "SP_UPDATED", nullable = true) // TODO: make this false after HAPI 2.3
078        @Temporal(TemporalType.TIMESTAMP)
079        private Date myUpdated;
080
081        @Transient
082        private transient PartitionSettings myPartitionSettings;
083
084        @Transient
085        private transient StorageSettings myStorageSettings;
086
087        @Override
088        public abstract Long getId();
089
090        public String getParamName() {
091                return myParamName;
092        }
093
094        public void setParamName(String theName) {
095                if (!StringUtils.equals(myParamName, theName)) {
096                        myParamName = theName;
097                        clearHashes();
098                }
099        }
100
101        // MB pushed these down to the individual SP classes so we could name the FK in the join annotation
102        /**
103         * Get the Resource this SP indexes
104         */
105        public abstract ResourceTable getResource();
106
107        public abstract BaseResourceIndexedSearchParam setResource(ResourceTable theResource);
108
109        @Override
110        public <T extends BaseResourceIndex> void copyMutableValuesFrom(T theSource) {
111                BaseResourceIndexedSearchParam source = (BaseResourceIndexedSearchParam) theSource;
112                myMissing = source.myMissing;
113                myParamName = source.myParamName;
114                myUpdated = source.myUpdated;
115                myStorageSettings = source.myStorageSettings;
116                myPartitionSettings = source.myPartitionSettings;
117                setPartitionId(source.getPartitionId());
118        }
119
120        public Long getResourcePid() {
121                return myResourcePid;
122        }
123
124        public String getResourceType() {
125                return myResourceType;
126        }
127
128        public void setResourceType(String theResourceType) {
129                myResourceType = theResourceType;
130        }
131
132        public Date getUpdated() {
133                return myUpdated;
134        }
135
136        public void setUpdated(Date theUpdated) {
137                myUpdated = theUpdated;
138        }
139
140        public boolean isMissing() {
141                return myMissing;
142        }
143
144        public BaseResourceIndexedSearchParam setMissing(boolean theMissing) {
145                myMissing = theMissing;
146                return this;
147        }
148
149        public abstract IQueryParameterType toQueryParameterType();
150
151        public boolean matches(IQueryParameterType theParam) {
152                throw new UnsupportedOperationException(Msg.code(1526) + "No parameter matcher for " + theParam);
153        }
154
155        public PartitionSettings getPartitionSettings() {
156                return myPartitionSettings;
157        }
158
159        public BaseResourceIndexedSearchParam setPartitionSettings(PartitionSettings thePartitionSettings) {
160                myPartitionSettings = thePartitionSettings;
161                return this;
162        }
163
164        public StorageSettings getStorageSettings() {
165                return myStorageSettings;
166        }
167
168        public BaseResourceIndexedSearchParam setStorageSettings(StorageSettings theStorageSettings) {
169                myStorageSettings = theStorageSettings;
170                return this;
171        }
172
173        public static long calculateHashIdentity(
174                        PartitionSettings thePartitionSettings,
175                        PartitionablePartitionId theRequestPartitionId,
176                        String theResourceType,
177                        String theParamName) {
178                RequestPartitionId requestPartitionId = PartitionablePartitionId.toRequestPartitionId(theRequestPartitionId);
179                return calculateHashIdentity(thePartitionSettings, requestPartitionId, theResourceType, theParamName);
180        }
181
182        public static long calculateHashIdentity(
183                        PartitionSettings thePartitionSettings,
184                        RequestPartitionId theRequestPartitionId,
185                        String theResourceType,
186                        String theParamName) {
187                return hash(thePartitionSettings, theRequestPartitionId, theResourceType, theParamName);
188        }
189
190        public static long calculateHashIdentity(
191                        PartitionSettings thePartitionSettings,
192                        RequestPartitionId theRequestPartitionId,
193                        String theResourceType,
194                        String theParamName,
195                        List<String> theAdditionalValues) {
196                String[] values = new String[theAdditionalValues.size() + 2];
197                values[0] = theResourceType;
198                values[1] = theParamName;
199                for (int i = 0; i < theAdditionalValues.size(); i++) {
200                        values[i + 2] = theAdditionalValues.get(i);
201                }
202
203                return hash(thePartitionSettings, theRequestPartitionId, values);
204        }
205
206        /**
207         * Applies a fast and consistent hashing algorithm to a set of strings
208         */
209        static long hash(
210                        PartitionSettings thePartitionSettings, RequestPartitionId theRequestPartitionId, String... theValues) {
211                Hasher hasher = HASH_FUNCTION.newHasher();
212
213                if (thePartitionSettings.isPartitioningEnabled()
214                                && thePartitionSettings.isIncludePartitionInSearchHashes()
215                                && theRequestPartitionId != null) {
216                        if (theRequestPartitionId.getPartitionIds().size() > 1) {
217                                throw new InternalErrorException(Msg.code(1527)
218                                                + "Can not search multiple partitions when partitions are included in search hashes");
219                        }
220                        Integer partitionId = theRequestPartitionId.getFirstPartitionIdOrNull();
221                        if (partitionId != null) {
222                                hasher.putInt(partitionId);
223                        }
224                }
225
226                for (String next : theValues) {
227                        if (next == null) {
228                                hasher.putByte((byte) 0);
229                        } else {
230                                next = UrlUtil.escapeUrlParam(next);
231                                byte[] bytes = next.getBytes(Charsets.UTF_8);
232                                hasher.putBytes(bytes);
233                        }
234                        hasher.putBytes(DELIMITER_BYTES);
235                }
236
237                HashCode hashCode = hasher.hash();
238                long retVal = hashCode.asLong();
239                return retVal;
240        }
241}