
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}