001/*-
002 * #%L
003 * HAPI FHIR - Core Library
004 * %%
005 * Copyright (C) 2014 - 2025 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.repository.impl;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.i18n.Msg;
024import ca.uhn.fhir.repository.IRepository;
025import ca.uhn.fhir.repository.IRepositoryLoader;
026import ca.uhn.fhir.repository.IRepositoryLoader.IRepositoryRequest;
027import ca.uhn.fhir.util.Logs;
028import com.google.common.annotations.Beta;
029import jakarta.annotation.Nonnull;
030import jakarta.annotation.Nullable;
031import org.slf4j.Logger;
032
033import java.util.Objects;
034import java.util.Optional;
035import java.util.ServiceLoader;
036import java.util.regex.Matcher;
037import java.util.regex.Pattern;
038
039/**
040 * Use ServiceLoader to load {@link IRepositoryLoader} implementations
041 * and provide chain-of-responsibility style matching by url to build IRepository instances.
042 */
043@Beta()
044public class UrlRepositoryFactory {
045        private static final Logger ourLog = Logs.getRepositoryTroubleshootingLog();
046
047        public static final String FHIR_REPOSITORY_URL_SCHEME = "fhir-repository:";
048        static final Pattern ourUrlPattern = Pattern.compile("^fhir-repository:([A-Za-z-]+):(.*)");
049
050        public static boolean isRepositoryUrl(String theBaseUrl) {
051                return theBaseUrl != null
052                                && theBaseUrl.startsWith(FHIR_REPOSITORY_URL_SCHEME)
053                                && ourUrlPattern.matcher(theBaseUrl).matches();
054        }
055
056        /**
057         * Find a factory for {@link IRepository} based on the given URL.
058         * This URL is expected to be in the form of fhir-repository:subscheme:details.
059         * The subscheme is used to find a matching {@link IRepositoryLoader} implementation.
060         *
061         * @param theFhirContext   the FHIR context to use for the repository, if required.
062         * @param theRepositoryUrl a url of the form fhir-repository:subscheme:details
063         * @return a repository instance
064         * @throws IllegalArgumentException if the URL is not a valid repository URL, or no loader can be found for the URL.
065         */
066        @Nonnull
067        public static IRepository buildRepository(@Nullable FhirContext theFhirContext, @Nonnull String theRepositoryUrl) {
068                ourLog.debug("Loading repository for url: {}", theRepositoryUrl);
069                Objects.requireNonNull(theRepositoryUrl);
070
071                if (!isRepositoryUrl(theRepositoryUrl)) {
072                        throw new IllegalArgumentException(
073                                        Msg.code(2737) + "Base URL is not a valid repository URL: " + theRepositoryUrl);
074                }
075
076                ServiceLoader<IRepositoryLoader> load = ServiceLoader.load(IRepositoryLoader.class);
077                IRepositoryRequest request = buildRequest(theRepositoryUrl, theFhirContext);
078                for (IRepositoryLoader nextLoader : load) {
079                        logLoaderDetails(nextLoader);
080                        if (nextLoader.canLoad(request)) {
081                                ourLog.debug(
082                                                "Loader {} can handle URL: {}.  Instantiating repository.",
083                                                nextLoader.getClass().getName(),
084                                                theRepositoryUrl);
085                                return nextLoader.loadRepository(request);
086                        }
087                }
088                throw new IllegalArgumentException(
089                                Msg.code(2738) + "Unable to find a repository loader for URL: " + theRepositoryUrl);
090        }
091
092        private static void logLoaderDetails(IRepositoryLoader nextLoader) {
093                Class<? extends IRepositoryLoader> clazz = nextLoader.getClass();
094                ourLog.debug(
095                                "Checking repository loader {} from {}",
096                                clazz.getName(),
097                                clazz.getProtectionDomain().getCodeSource().getLocation());
098        }
099
100        /**
101         * Builder for our abstract {@link IRepositoryRequest} interface.
102         * @param theBaseUrl the fhir-repository URL to parse, e.g. fhir-repository:memory:my-repo
103         * @param theFhirContext the FHIR context to use for the repository, if required.
104         */
105        @Nonnull
106        public static IRepositoryRequest buildRequest(@Nonnull String theBaseUrl, @Nullable FhirContext theFhirContext) {
107                Matcher matcher = ourUrlPattern.matcher(theBaseUrl);
108                String subScheme = null;
109                String details = null;
110                boolean found = matcher.matches();
111                if (found) {
112                        subScheme = matcher.group(1);
113                        details = matcher.group(2);
114                }
115
116                return new RepositoryRequest(theBaseUrl, subScheme, details, theFhirContext);
117        }
118
119        /**
120         * Internal implementation of {@link IRepositoryRequest}.
121         */
122        record RepositoryRequest(String url, String subScheme, String details, FhirContext fhirContext)
123                        implements IRepositoryRequest {
124                @Override
125                public String getUrl() {
126                        return url;
127                }
128
129                @Override
130                public String getSubScheme() {
131                        return subScheme;
132                }
133
134                @Override
135                public String getDetails() {
136                        return details;
137                }
138
139                @SuppressWarnings("java:S6211")
140                @Override
141                public Optional<FhirContext> getFhirContext() {
142                        return Optional.ofNullable(fhirContext);
143                }
144        }
145}