001/*
002 * Licensed under the Apache License, Version 2.0 (the "License");
003 * you may not use this file except in compliance with the License.
004 * You may obtain a copy of the License at
005 *
006 * http://www.apache.org/licenses/LICENSE-2.0
007 *
008 * Unless required by applicable law or agreed to in writing, software
009 * distributed under the License is distributed on an "AS IS" BASIS,
010 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
011 * See the License for the specific language governing permissions and
012 * limitations under the License.
013 */
014
015package de.softwareforge.testing.postgres.embedded;
016
017import static com.google.common.base.Preconditions.checkNotNull;
018import static com.google.common.base.Preconditions.checkState;
019import static java.lang.String.format;
020
021import de.softwareforge.testing.maven.MavenArtifactLoader;
022
023import jakarta.annotation.Nonnull;
024import java.io.File;
025import java.io.FilterInputStream;
026import java.io.IOException;
027import java.io.InputStream;
028import java.io.UncheckedIOException;
029import java.nio.charset.StandardCharsets;
030import java.util.Objects;
031import java.util.function.Supplier;
032import java.util.jar.JarEntry;
033import java.util.jar.JarFile;
034
035import com.google.common.base.Suppliers;
036import com.google.common.hash.HashCode;
037import com.google.common.hash.Hashing;
038import com.google.common.io.BaseEncoding;
039import org.slf4j.Logger;
040import org.slf4j.LoggerFactory;
041
042/**
043 * Resolves PostgreSQL archives from the Maven repository. Looks for the zonky.io artifacts located at
044 * <code>io.zonky.test.postgres:embedded-postgres-binaries-&lt;os&gt;-&lt;arch&gt;</code>.
045 * <p>
046 * See <a href="https://github.com/zonkyio/embedded-postgres-binaries">The Zonky IO github page</a> for more details.
047 *
048 * @since 3.0
049 */
050public final class ZonkyIOPostgresLocator implements NativeBinaryLocator {
051
052    private static final String ZONKY_GROUP_ID = "io.zonky.test.postgres";
053    private static final String ZONKY_ARTIFACT_ID_TEMPLATE = "embedded-postgres-binaries-%s-%s";
054
055    private static final Logger LOG = LoggerFactory.getLogger(ZonkyIOPostgresLocator.class);
056
057    private static final boolean PREFER_NATIVE = Boolean.getBoolean("pg-embedded.prefer-native");
058
059    private final String architecture;
060    private final String os;
061    private final String serverVersion;
062
063    private final MavenArtifactLoader artifactLoader = new MavenArtifactLoader();
064
065    private final Supplier<File> fileSupplier = Suppliers.memoize(this::loadArtifact);
066
067    ZonkyIOPostgresLocator(String serverVersion) {
068        this.serverVersion = checkNotNull(serverVersion, "serverVersion is null");
069
070        this.os = computeOS();
071        this.architecture = computeTarXzArchitectureName();
072        LOG.debug(format("Detected a %s %s system, using PostgreSQL version %s/%s", EmbeddedUtil.OS_ARCH, os, serverVersion, architecture));
073    }
074
075    @Override
076    public InputStream getInputStream() throws IOException {
077        try {
078            File artifactFile = fileSupplier.get();
079            return createJarStream(artifactFile);
080        } catch (UncheckedIOException e) {
081            throw e.getCause();
082        }
083    }
084
085    @Override
086    @Nonnull
087    public String getIdentifier() throws IOException {
088        // the optimized identifier computation saves ~ 1% CPU according to the profiler
089        try {
090            File artifactFile = fileSupplier.get();
091            HashCode hashCode = Hashing.murmur3_128().hashString(artifactFile.getAbsolutePath(), StandardCharsets.UTF_8);
092            return INSTALL_DIRECTORY_PREFIX + BaseEncoding.base16().encode(hashCode.asBytes());
093        } catch (UncheckedIOException e) {
094            throw e.getCause();
095        }
096    }
097
098    private File loadArtifact() {
099        try {
100            String artifactId = format(ZONKY_ARTIFACT_ID_TEMPLATE, this.os, computeJarArchitectureName());
101
102            // alpine hack
103            if (EmbeddedUtil.IS_ALPINE_LINUX) {
104                artifactId += "-alpine";
105            }
106
107            String version = artifactLoader.builder(ZONKY_GROUP_ID, artifactId)
108                    .partialMatch(serverVersion)
109                    .includeSnapshots(false)
110                    .findBestMatch()
111                    .orElseThrow(() -> new IllegalStateException(format("Could not download artifact for Zonky Postgres %s", serverVersion)));
112
113            File file = artifactLoader.getArtifactFile(ZONKY_GROUP_ID, artifactId, version);
114            checkState(file != null && file.exists(), "Could not locate artifact file for %s:%s", artifactId, version);
115            LOG.info(format("Using PostgreSQL version %s (%s)", version, architecture));
116            return file;
117        } catch (IOException e) {
118            throw new UncheckedIOException(e);
119        }
120    }
121
122    private InputStream createJarStream(File file) {
123        try {
124            JarFile jar = new JarFile(file);
125            String entryName = format("postgres-%s-%s", computeOS(), computeTarXzArchitectureName());
126
127            // alpine hack
128            if (EmbeddedUtil.IS_ALPINE_LINUX) {
129                entryName += "-alpine_linux";
130            }
131
132            JarEntry jarEntry = jar.getJarEntry(entryName + ".txz");
133            checkState(jarEntry != null, "Could not locate %s in the jar file (%s)", entryName, file.getAbsoluteFile());
134
135            // When the input stream gets closed, close the jar file as well.
136            return new FilterInputStream(jar.getInputStream(jarEntry)) {
137                @Override
138                public void close() throws IOException {
139                    try {
140                        super.close();
141                    } finally {
142                        jar.close();
143                    }
144                }
145            };
146        } catch (IOException e) {
147            throw new UncheckedIOException(e);
148        }
149    }
150
151    @Override
152    public String toString() {
153        return format("ZonkyIO Stream locator for PostgreSQL (machine: %s os: %s, arch: %s, version: %s)",
154                EmbeddedUtil.OS_ARCH, os, architecture, serverVersion);
155    }
156
157    @Override
158    public boolean equals(Object o) {
159        if (this == o) {
160            return true;
161        }
162        if (o == null || getClass() != o.getClass()) {
163            return false;
164        }
165        ZonkyIOPostgresLocator that = (ZonkyIOPostgresLocator) o;
166        return architecture.equals(that.architecture) && os.equals(that.os) && serverVersion.equals(that.serverVersion);
167    }
168
169    @Override
170    public int hashCode() {
171        return Objects.hash(architecture, os, serverVersion);
172    }
173
174    private static String computeTarXzArchitectureName() {
175        String architecture = EmbeddedUtil.OS_ARCH;
176        if (EmbeddedUtil.IS_ARCH_X86_64) {
177            architecture = "x86_64";  // Zonky uses x86_64
178        } else if (EmbeddedUtil.IS_ARCH_AARCH64) {
179            if (!PREFER_NATIVE && EmbeddedUtil.IS_OS_MAC) {
180                // Mac binaries are fat binaries stored as x86_64
181                architecture = "x86_64";
182            } else {
183                architecture = "arm_64";
184            }
185        } else if (EmbeddedUtil.IS_ARCH_AARCH32) {
186            architecture = "arm_32";
187        }
188        return architecture;
189    }
190
191    private static String computeJarArchitectureName() {
192        String architecture = EmbeddedUtil.OS_ARCH;
193        if (EmbeddedUtil.IS_ARCH_X86_64) {
194            architecture = "amd64";  // Zonky uses amd64 for the jar name
195        } else if (EmbeddedUtil.IS_ARCH_AARCH64) {
196            if (!PREFER_NATIVE && EmbeddedUtil.IS_OS_MAC) {
197                // Mac binaries are fat binaries stored as amd64
198                architecture = "amd64";
199            } else {
200                architecture = "arm64v8";
201            }
202        } else if (EmbeddedUtil.IS_ARCH_AARCH32) {
203            architecture = "arm32v7";
204        }
205        return architecture;
206    }
207
208    private static String computeOS() {
209        String os = EmbeddedUtil.OS_NAME;
210        if (EmbeddedUtil.IS_OS_LINUX) {
211            os = "linux";
212        } else if (EmbeddedUtil.IS_OS_MAC) {
213            os = "darwin";
214        } else if (EmbeddedUtil.IS_OS_WINDOWS) {
215            os = "windows";
216        }
217        return os;
218    }
219}