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}