001/*-
002 * #%L
003 * Smile CDR - CDR
004 * %%
005 * Copyright (C) 2016 - 2025 Smile CDR, Inc.
006 * %%
007 * All rights reserved.
008 * #L%
009 */
010package ca.cdr.test.app.clients;
011
012import ca.cdr.test.app.clients.common.RequestFactoryUtil;
013import ca.uhn.fhir.jpa.packages.PackageInstallOutcomeJson;
014import ca.uhn.fhir.jpa.packages.PackageInstallationSpec;
015import jakarta.annotation.Nonnull;
016import org.apache.commons.lang3.Validate;
017import org.slf4j.Logger;
018import org.slf4j.LoggerFactory;
019import org.springframework.http.MediaType;
020import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
021import org.springframework.http.client.support.BasicAuthenticationInterceptor;
022import org.springframework.web.client.RestClient;
023
024import java.util.Objects;
025
026/**
027 * NpmPackageClient is a client for the NPM Package Registry API of the CDR.
028 * It currently supports anonymous and http-basic authentication.
029 * 
030 * This client provides access to package installation operations through
031 * Smile CDR's package registry endpoint, typically running on port 8002.
032 */
033public class NpmPackageClient {
034        private static final Logger ourLog = LoggerFactory.getLogger(NpmPackageClient.class);
035
036        private final RestClient myRestClient;
037
038        NpmPackageClient(RestClient theRestClient) {
039                myRestClient = theRestClient;
040        }
041
042        /**
043         * Build an NpmPackageClient with authentication
044         * @param theBaseUrl The base URL of the package registry
045         * @param theUsername Username for authentication
046         * @param thePassword Password for authentication
047         * @return Configured NpmPackageClient
048         */
049        public static NpmPackageClient build(String theBaseUrl, String theUsername, String thePassword) {
050                RestClient restClient = builderForUrlWithJsonDefault(theBaseUrl)
051                        .requestInterceptor(new BasicAuthenticationInterceptor(theUsername, thePassword))
052                        .build();
053
054                return new NpmPackageClient(restClient);
055        }
056
057        /**
058         * Build an anonymous NpmPackageClient
059         * @param theBaseUrl The base URL of the package registry
060         * @return Configured NpmPackageClient
061         */
062        public static NpmPackageClient buildAnonymous(String theBaseUrl) {
063                RestClient restClient = builderForUrlWithJsonDefault(theBaseUrl).build();
064
065                return new NpmPackageClient(restClient);
066        }
067
068        /**
069         * Install a package using the provided specification
070         * @param theSpec The package installation specification
071         * @return The installation outcome
072         */
073        @Nonnull
074        public PackageInstallOutcomeJson installPackageBySpec(PackageInstallationSpec theSpec) {
075                Validate.notNull(theSpec, "Package specification is required");
076                Validate.notEmpty(theSpec.getName(), "Package name is required");
077                Validate.notEmpty(theSpec.getVersion(), "Package version is required");
078                
079                ourLog.info("Installing package {}#{}", theSpec.getName(), theSpec.getVersion());
080                
081                PackageInstallOutcomeJson result = myRestClient
082                        .put()
083                        .uri("/write/install/by-spec")
084                        .body(theSpec)
085                        .retrieve()
086                        .body(PackageInstallOutcomeJson.class);
087                return Objects.requireNonNull(result);
088        }
089
090        /**
091         * Install a package using the provided specification as a JSON string
092         * @param theSpecJson The package installation specification as JSON string
093         * @return The installation outcome
094         */
095        @Nonnull
096        public PackageInstallOutcomeJson installPackageBySpec(String theSpecJson) {
097                Validate.notEmpty(theSpecJson, "Package specification JSON is required");
098                
099                ourLog.info("Installing package from JSON specification: {}", theSpecJson);
100                
101                PackageInstallOutcomeJson result = myRestClient
102                        .put()
103                        .uri("/write/install/by-spec")
104                        .contentType(MediaType.APPLICATION_JSON)
105                        .body(theSpecJson)
106                        .retrieve()
107                        .body(PackageInstallOutcomeJson.class);
108                return Objects.requireNonNull(result);
109        }
110
111        /**
112         * Create a RestClient builder configured for JSON communication
113         * @param theBaseUrl The base URL
114         * @return Configured RestClient builder
115         */
116        private static @Nonnull RestClient.Builder builderForUrlWithJsonDefault(String theBaseUrl) {
117                HttpComponentsClientHttpRequestFactory requestFactory = RequestFactoryUtil.buildSmileRequestFactory();
118
119                return RestClient.builder()
120                        .baseUrl(theBaseUrl)
121                        .requestFactory(requestFactory)
122                        // This sets the default content type to JSON, but allows the actual request to override it.
123                        .defaultRequest(r -> r.accept(MediaType.APPLICATION_JSON));
124        }
125}