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.util;
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.rest.server.exceptions.InternalErrorException;
026import ca.uhn.fhir.util.UrlUtil;
027import com.google.common.base.Charsets;
028import com.google.common.hash.HashCode;
029import com.google.common.hash.HashFunction;
030import com.google.common.hash.Hasher;
031import com.google.common.hash.Hashing;
032import jakarta.annotation.Nonnull;
033import jakarta.annotation.Nullable;
034
035/**
036 * Utility class for calculating hashes of SearchParam entity fields.
037 */
038public class SearchParamHash {
039
040        /**
041         * Don't change this without careful consideration. You will break existing hashes!
042         */
043        private static final HashFunction HASH_FUNCTION = Hashing.murmur3_128(0);
044
045        /**
046         * Don't make this public 'cause nobody better be able to modify it!
047         */
048        private static final byte[] DELIMITER_BYTES = "|".getBytes(Charsets.UTF_8);
049
050        private SearchParamHash() {}
051
052        /**
053         * Applies a fast and consistent hashing algorithm to a set of strings
054         */
055        public static long hashSearchParam(
056                        @Nonnull PartitionSettings thePartitionSettings,
057                        @Nonnull RequestPartitionId theRequestPartitionId,
058                        @Nonnull String... theValues) {
059                return doHashSearchParam(thePartitionSettings, theRequestPartitionId, theValues);
060        }
061
062        /**
063         * Applies a fast and consistent hashing algorithm to a set of strings
064         */
065        public static long hashSearchParam(@Nonnull String... theValues) {
066                return doHashSearchParam(null, null, theValues);
067        }
068
069        private static long doHashSearchParam(
070                        @Nullable PartitionSettings thePartitionSettings,
071                        @Nullable RequestPartitionId theRequestPartitionId,
072                        @Nonnull String[] theValues) {
073                Hasher hasher = HASH_FUNCTION.newHasher();
074
075                if (thePartitionSettings != null
076                                && theRequestPartitionId != null
077                                && thePartitionSettings.isPartitioningEnabled()
078                                && thePartitionSettings.isIncludePartitionInSearchHashes()) {
079                        if (theRequestPartitionId.getPartitionIds().size() > 1) {
080                                throw new InternalErrorException(Msg.code(1527)
081                                                + "Can not search multiple partitions when partitions are included in search hashes");
082                        }
083                        Integer partitionId = theRequestPartitionId.getFirstPartitionIdOrNull();
084                        if (partitionId != null) {
085                                hasher.putInt(partitionId);
086                        }
087                }
088
089                for (String next : theValues) {
090                        if (next == null) {
091                                hasher.putByte((byte) 0);
092                        } else {
093                                next = UrlUtil.escapeUrlParam(next);
094                                byte[] bytes = next.getBytes(Charsets.UTF_8);
095                                hasher.putBytes(bytes);
096                        }
097                        hasher.putBytes(DELIMITER_BYTES);
098                }
099
100                HashCode hashCode = hasher.hash();
101                long retVal = hashCode.asLong();
102                return retVal;
103        }
104}