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.packages.loader; 021 022import ca.uhn.fhir.i18n.Msg; 023import ca.uhn.fhir.jpa.packages.PackageInstallationSpec; 024import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 025import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; 026import ca.uhn.fhir.util.ClasspathUtil; 027import jakarta.annotation.Nullable; 028import org.apache.commons.io.IOUtils; 029import org.apache.commons.lang3.Validate; 030import org.apache.http.client.methods.CloseableHttpResponse; 031import org.apache.http.client.methods.HttpGet; 032import org.apache.http.conn.HttpClientConnectionManager; 033import org.apache.http.impl.client.HttpClientBuilder; 034import org.apache.http.impl.conn.BasicHttpClientConnectionManager; 035import org.hl7.fhir.exceptions.FHIRException; 036import org.hl7.fhir.utilities.npm.BasePackageCacheManager; 037import org.hl7.fhir.utilities.npm.NpmPackage; 038import org.slf4j.Logger; 039import org.slf4j.LoggerFactory; 040 041import java.io.ByteArrayInputStream; 042import java.io.IOException; 043import java.io.InputStream; 044import java.net.URI; 045import java.net.URISyntaxException; 046import java.nio.file.Files; 047import java.nio.file.Paths; 048 049import static org.apache.commons.lang3.StringUtils.isNotBlank; 050 051public class PackageLoaderSvc extends BasePackageCacheManager { 052 053 private static final Logger ourLog = LoggerFactory.getLogger(PackageLoaderSvc.class); 054 055 public NpmPackageData fetchPackageFromPackageSpec(PackageInstallationSpec theSpec) throws IOException { 056 if (isNotBlank(theSpec.getPackageUrl())) { 057 byte[] contents = loadPackageUrlContents(theSpec.getPackageUrl()); 058 return createNpmPackageDataFromData( 059 theSpec.getName(), 060 theSpec.getVersion(), 061 theSpec.getPackageUrl(), 062 new ByteArrayInputStream(contents)); 063 } 064 065 return fetchPackageFromServerInternal(theSpec.getName(), theSpec.getVersion()); 066 } 067 068 /** 069 * Loads the package, but won't save it anywhere. 070 * Returns the data to the caller 071 * 072 * @return - a POJO containing information about the NpmPackage, as well as it's contents 073 * as fetched from the server 074 * @throws IOException 075 */ 076 public NpmPackageData fetchPackageFromPackageSpec(String thePackageId, String thePackageVersion) 077 throws FHIRException, IOException { 078 return fetchPackageFromServerInternal(thePackageId, thePackageVersion); 079 } 080 081 private NpmPackageData fetchPackageFromServerInternal(String thePackageId, String thePackageVersion) 082 throws IOException { 083 BasePackageCacheManager.InputStreamWithSrc pkg = this.loadFromPackageServer(thePackageId, thePackageVersion); 084 085 if (pkg == null) { 086 throw new ResourceNotFoundException( 087 Msg.code(1301) + "Unable to locate package " + thePackageId + "#" + thePackageVersion); 088 } 089 090 NpmPackageData npmPackage = createNpmPackageDataFromData( 091 thePackageId, thePackageVersion == null ? pkg.version : thePackageVersion, pkg.url, pkg.stream); 092 093 return npmPackage; 094 } 095 096 /** 097 * Creates an NpmPackage data object. 098 * 099 * @param thePackageId - the id of the npm package 100 * @param thePackageVersionId - the version id of the npm package 101 * @param theSourceDesc - the installation spec description or package url 102 * @param thePackageTgzInputStream - the package contents. 103 * Typically fetched from a server, but can be added directly to the package spec 104 * @return 105 * @throws IOException 106 */ 107 public NpmPackageData createNpmPackageDataFromData( 108 String thePackageId, String thePackageVersionId, String theSourceDesc, InputStream thePackageTgzInputStream) 109 throws IOException { 110 Validate.notBlank(thePackageId, "thePackageId must not be null"); 111 Validate.notBlank(thePackageVersionId, "thePackageVersionId must not be null"); 112 Validate.notNull(thePackageTgzInputStream, "thePackageTgzInputStream must not be null"); 113 114 byte[] bytes = IOUtils.toByteArray(thePackageTgzInputStream); 115 116 ourLog.info("Parsing package .tar.gz ({} bytes) from {}", bytes.length, theSourceDesc); 117 118 NpmPackage npmPackage = NpmPackage.fromPackage(new ByteArrayInputStream(bytes)); 119 120 return new NpmPackageData( 121 thePackageId, thePackageVersionId, theSourceDesc, bytes, npmPackage, thePackageTgzInputStream); 122 } 123 124 @Override 125 public NpmPackage loadPackageFromCacheOnly(String theS, @Nullable String theS1) { 126 throw new UnsupportedOperationException(Msg.code(2215) 127 + "Cannot load from cache. " 128 + "Caching not supported in PackageLoaderSvc. Use JpaPackageCache instead."); 129 } 130 131 @Override 132 public NpmPackage addPackageToCache(String theS, String theS1, InputStream theInputStream, String theS2) 133 throws IOException { 134 throw new UnsupportedOperationException(Msg.code(2216) 135 + "Cannot add to cache. " 136 + "Caching not supported in PackageLoaderSvc. Use JpaPackageCache instead."); 137 } 138 139 @Override 140 public NpmPackage loadPackage(String theS, String theS1) throws FHIRException { 141 /* 142 * We throw an exception because while we could pipe this call through 143 * to loadPackageOnly ourselves, returning NpmPackage details 144 * on their own provides no value if nothing is cached/loaded onto hard disk somewhere 145 * 146 */ 147 throw new UnsupportedOperationException(Msg.code(2217) 148 + "No packages are cached; " 149 + " this service only loads from the server directly. " 150 + "Call fetchPackageFromServer to fetch the npm package from the server. " 151 + "Or use JpaPackageCache for a cache implementation."); 152 } 153 154 public byte[] loadPackageUrlContents(String thePackageUrl) { 155 if (thePackageUrl.startsWith("classpath:")) { 156 return ClasspathUtil.loadResourceAsByteArray(thePackageUrl.substring("classpath:".length())); 157 } else if (thePackageUrl.startsWith("file:")) { 158 try { 159 byte[] bytes = Files.readAllBytes(Paths.get(new URI(thePackageUrl))); 160 return bytes; 161 } catch (IOException | URISyntaxException e) { 162 throw new InternalErrorException( 163 Msg.code(2031) + "Error loading \"" + thePackageUrl + "\": " + e.getMessage()); 164 } 165 } else { 166 HttpClientConnectionManager connManager = new BasicHttpClientConnectionManager(); 167 try (CloseableHttpResponse request = HttpClientBuilder.create() 168 .setConnectionManager(connManager) 169 .build() 170 .execute(new HttpGet(thePackageUrl))) { 171 if (request.getStatusLine().getStatusCode() != 200) { 172 throw new ResourceNotFoundException(Msg.code(1303) + "Received HTTP " 173 + request.getStatusLine().getStatusCode() + " from URL: " + thePackageUrl); 174 } 175 return IOUtils.toByteArray(request.getEntity().getContent()); 176 } catch (IOException e) { 177 throw new InternalErrorException( 178 Msg.code(1304) + "Error loading \"" + thePackageUrl + "\": " + e.getMessage()); 179 } 180 } 181 } 182}