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-<os>-<arch></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}