001/*-
002 * #%L
003 * HAPI FHIR JPA Server
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.search.elastic;
021
022import ca.uhn.fhir.context.ConfigurationException;
023import ca.uhn.fhir.i18n.Msg;
024import ca.uhn.fhir.jpa.search.HapiHSearchAnalysisConfigurers;
025import org.apache.commons.lang3.StringUtils;
026import org.hibernate.search.backend.elasticsearch.aws.cfg.ElasticsearchAwsBackendSettings;
027import org.hibernate.search.backend.elasticsearch.aws.cfg.ElasticsearchAwsCredentialsTypeNames;
028import org.hibernate.search.backend.elasticsearch.cfg.ElasticsearchBackendSettings;
029import org.hibernate.search.backend.elasticsearch.cfg.ElasticsearchIndexSettings;
030import org.hibernate.search.backend.elasticsearch.index.IndexStatus;
031import org.hibernate.search.engine.cfg.BackendSettings;
032import org.hibernate.search.mapper.orm.automaticindexing.session.AutomaticIndexingSynchronizationStrategyNames;
033import org.hibernate.search.mapper.orm.cfg.HibernateOrmMapperSettings;
034import org.hibernate.search.mapper.orm.schema.management.SchemaManagementStrategyName;
035import org.slf4j.Logger;
036
037import java.util.Properties;
038
039import static org.hibernate.search.backend.elasticsearch.cfg.ElasticsearchBackendSettings.Defaults.SCROLL_TIMEOUT;
040import static org.slf4j.LoggerFactory.getLogger;
041
042/**
043 * This class is used to inject appropriate properties into a hibernate
044 * Properties object being used to create an entitymanager for a HAPI
045 * FHIR JPA server. This class also injects a starter template into the ES cluster.
046 */
047public class ElasticsearchHibernatePropertiesBuilder {
048        private static final Logger ourLog = getLogger(ElasticsearchHibernatePropertiesBuilder.class);
049
050        private IndexStatus myRequiredIndexStatus = IndexStatus.YELLOW;
051        private SchemaManagementStrategyName myIndexSchemaManagementStrategy = SchemaManagementStrategyName.CREATE;
052
053        private String myHosts;
054        private String myUsername;
055        private String myPassword;
056
057        public String getAwsRegion() {
058                return myAwsRegion;
059        }
060
061        private String myAwsRegion;
062        private long myIndexManagementWaitTimeoutMillis = 10000L;
063        private long myScrollTimeoutSecs = SCROLL_TIMEOUT;
064        private String myDebugSyncStrategy = AutomaticIndexingSynchronizationStrategyNames.ASYNC;
065        private boolean myDebugPrettyPrintJsonLog = false;
066        private String myProtocol;
067
068        public ElasticsearchHibernatePropertiesBuilder setUsername(String theUsername) {
069                myUsername = theUsername;
070                return this;
071        }
072
073        public ElasticsearchHibernatePropertiesBuilder setPassword(String thePassword) {
074                myPassword = thePassword;
075                return this;
076        }
077
078        public void apply(Properties theProperties) {
079
080                // the below properties are used for ElasticSearch integration
081                theProperties.put(BackendSettings.backendKey(BackendSettings.TYPE), "elasticsearch");
082                theProperties.put(
083                                BackendSettings.backendKey(ElasticsearchIndexSettings.ANALYSIS_CONFIGURER),
084                                HapiHSearchAnalysisConfigurers.HapiElasticsearchAnalysisConfigurer.class.getName());
085                theProperties.put(BackendSettings.backendKey(ElasticsearchBackendSettings.HOSTS), myHosts);
086                theProperties.put(BackendSettings.backendKey(ElasticsearchBackendSettings.PROTOCOL), myProtocol);
087
088                if (StringUtils.isNotBlank(myUsername)) {
089                        theProperties.put(BackendSettings.backendKey(ElasticsearchBackendSettings.USERNAME), myUsername);
090                }
091                if (StringUtils.isNotBlank(myPassword)) {
092                        theProperties.put(BackendSettings.backendKey(ElasticsearchBackendSettings.PASSWORD), myPassword);
093                }
094                theProperties.put(
095                                HibernateOrmMapperSettings.SCHEMA_MANAGEMENT_STRATEGY,
096                                myIndexSchemaManagementStrategy.externalRepresentation());
097                theProperties.put(
098                                BackendSettings.backendKey(
099                                                ElasticsearchIndexSettings.SCHEMA_MANAGEMENT_MINIMAL_REQUIRED_STATUS_WAIT_TIMEOUT),
100                                Long.toString(myIndexManagementWaitTimeoutMillis));
101                theProperties.put(
102                                BackendSettings.backendKey(ElasticsearchIndexSettings.SCHEMA_MANAGEMENT_MINIMAL_REQUIRED_STATUS),
103                                myRequiredIndexStatus.externalRepresentation());
104                // Need the mapping to be dynamic because of terminology indexes.
105                theProperties.put(BackendSettings.backendKey(ElasticsearchIndexSettings.DYNAMIC_MAPPING), "true");
106                // Only for unit tests
107                theProperties.put(HibernateOrmMapperSettings.AUTOMATIC_INDEXING_SYNCHRONIZATION_STRATEGY, myDebugSyncStrategy);
108                theProperties.put(
109                                BackendSettings.backendKey(ElasticsearchBackendSettings.LOG_JSON_PRETTY_PRINTING),
110                                Boolean.toString(myDebugPrettyPrintJsonLog));
111                theProperties.put(
112                                BackendSettings.backendKey(ElasticsearchBackendSettings.SCROLL_TIMEOUT),
113                                Long.toString(myScrollTimeoutSecs));
114
115                // This tells elasticsearch to use our custom index naming strategy.
116                theProperties.put(
117                                BackendSettings.backendKey(ElasticsearchBackendSettings.LAYOUT_STRATEGY),
118                                IndexNamePrefixLayoutStrategy.class.getName());
119
120                // This tells hibernate search to use this custom file for creating index settings. We use this to add a custom
121                // max_ngram_diff
122                theProperties.put(
123                                BackendSettings.backendKey(ElasticsearchIndexSettings.SCHEMA_MANAGEMENT_SETTINGS_FILE),
124                                "ca/uhn/fhir/jpa/elastic/index-settings.json");
125
126                if (!StringUtils.isBlank(myAwsRegion)) {
127                        theProperties.put(BackendSettings.backendKey(ElasticsearchAwsBackendSettings.REGION), myAwsRegion);
128                        theProperties.put(BackendSettings.backendKey(ElasticsearchAwsBackendSettings.SIGNING_ENABLED), true);
129                        if (!StringUtils.isBlank(myUsername) && !StringUtils.isBlank(myPassword)) {
130                                theProperties.put(
131                                                BackendSettings.backendKey(ElasticsearchAwsBackendSettings.CREDENTIALS_TYPE),
132                                                ElasticsearchAwsCredentialsTypeNames.STATIC);
133                                theProperties.put(
134                                                BackendSettings.backendKey(ElasticsearchAwsBackendSettings.CREDENTIALS_ACCESS_KEY_ID),
135                                                myUsername);
136                                theProperties.put(
137                                                BackendSettings.backendKey(ElasticsearchAwsBackendSettings.CREDENTIALS_SECRET_ACCESS_KEY),
138                                                myPassword);
139                        } else {
140                                // Default to Standard IAM Auth provider if username or password is absent.
141                                theProperties.put(
142                                                BackendSettings.backendKey(ElasticsearchAwsBackendSettings.CREDENTIALS_TYPE),
143                                                ElasticsearchAwsCredentialsTypeNames.DEFAULT);
144                        }
145                }
146        }
147
148        public ElasticsearchHibernatePropertiesBuilder setRequiredIndexStatus(IndexStatus theRequiredIndexStatus) {
149                myRequiredIndexStatus = theRequiredIndexStatus;
150                return this;
151        }
152
153        public ElasticsearchHibernatePropertiesBuilder setHosts(String hosts) {
154                if (hosts.contains("://")) {
155                        throw new ConfigurationException(
156                                        Msg.code(2139)
157                                                        + "Elasticsearch URLs cannot include a protocol, that is a separate property. Remove http:// or https:// from this URL.");
158                }
159                myHosts = hosts;
160                return this;
161        }
162
163        public ElasticsearchHibernatePropertiesBuilder setProtocol(String theProtocol) {
164                myProtocol = theProtocol;
165                return this;
166        }
167
168        public ElasticsearchHibernatePropertiesBuilder setIndexSchemaManagementStrategy(
169                        SchemaManagementStrategyName theIndexSchemaManagementStrategy) {
170                myIndexSchemaManagementStrategy = theIndexSchemaManagementStrategy;
171                return this;
172        }
173
174        public ElasticsearchHibernatePropertiesBuilder setIndexManagementWaitTimeoutMillis(
175                        long theIndexManagementWaitTimeoutMillis) {
176                myIndexManagementWaitTimeoutMillis = theIndexManagementWaitTimeoutMillis;
177                return this;
178        }
179
180        public ElasticsearchHibernatePropertiesBuilder setScrollTimeoutSecs(long theScrollTimeoutSecs) {
181                myScrollTimeoutSecs = theScrollTimeoutSecs;
182                return this;
183        }
184
185        public ElasticsearchHibernatePropertiesBuilder setDebugIndexSyncStrategy(String theSyncStrategy) {
186                myDebugSyncStrategy = theSyncStrategy;
187                return this;
188        }
189
190        public ElasticsearchHibernatePropertiesBuilder setDebugPrettyPrintJsonLog(boolean theDebugPrettyPrintJsonLog) {
191                myDebugPrettyPrintJsonLog = theDebugPrettyPrintJsonLog;
192                return this;
193        }
194
195        /**
196         * If this is set to `true`, the AWS region will be used to configure the AWS client. Additionally, this will trigger
197         * HibernateSearch to attempt to use IAM Authentication. If the username and password are set in addition to the region,
198         * then the username and password will be used as the AWS_ACCESS_KEY_ID and AWS_SECRET_KEY_ID for a static credentials file for IAM.
199         *
200         * @param theAwsRegion The String version of the region, e.g. `us-east-2`.
201         * @return This builder.
202         */
203        public ElasticsearchHibernatePropertiesBuilder setAwsRegion(String theAwsRegion) {
204                myAwsRegion = theAwsRegion;
205                return this;
206        }
207}