View Javadoc
1   /*
2    * Licensed under the Apache License, Version 2.0 (the "License");
3    * you may not use this file except in compliance with the License.
4    * You may obtain a copy of the License at
5    *
6    * http://www.apache.org/licenses/LICENSE-2.0
7    *
8    * Unless required by applicable law or agreed to in writing, software
9    * distributed under the License is distributed on an "AS IS" BASIS,
10   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11   * See the License for the specific language governing permissions and
12   * limitations under the License.
13   */
14  
15  package de.softwareforge.testing.postgres.embedded;
16  
17  import static com.google.common.base.Preconditions.checkNotNull;
18  import static com.google.common.base.Preconditions.checkState;
19  import static java.lang.String.format;
20  
21  import de.softwareforge.testing.maven.MavenArtifactLoader;
22  
23  import jakarta.annotation.Nonnull;
24  import java.io.File;
25  import java.io.FilterInputStream;
26  import java.io.IOException;
27  import java.io.InputStream;
28  import java.io.UncheckedIOException;
29  import java.nio.charset.StandardCharsets;
30  import java.util.Objects;
31  import java.util.function.Supplier;
32  import java.util.jar.JarEntry;
33  import java.util.jar.JarFile;
34  
35  import com.google.common.base.Suppliers;
36  import com.google.common.hash.HashCode;
37  import com.google.common.hash.Hashing;
38  import com.google.common.io.BaseEncoding;
39  import org.slf4j.Logger;
40  import org.slf4j.LoggerFactory;
41  
42  /**
43   * Resolves PostgreSQL archives from the Maven repository. Looks for the zonky.io artifacts located at
44   * <code>io.zonky.test.postgres:embedded-postgres-binaries-&lt;os&gt;-&lt;arch&gt;</code>.
45   * <p>
46   * See <a href="https://github.com/zonkyio/embedded-postgres-binaries">The Zonky IO github page</a> for more details.
47   *
48   * @since 3.0
49   */
50  public final class ZonkyIOPostgresLocator implements NativeBinaryLocator {
51  
52      private static final String ZONKY_GROUP_ID = "io.zonky.test.postgres";
53      private static final String ZONKY_ARTIFACT_ID_TEMPLATE = "embedded-postgres-binaries-%s-%s";
54  
55      private static final Logger LOG = LoggerFactory.getLogger(ZonkyIOPostgresLocator.class);
56  
57      private static final boolean PREFER_NATIVE = Boolean.getBoolean("pg-embedded.prefer-native");
58  
59      private final String architecture;
60      private final String os;
61      private final String serverVersion;
62  
63      private final MavenArtifactLoader artifactLoader = new MavenArtifactLoader();
64  
65      private final Supplier<File> fileSupplier = Suppliers.memoize(this::loadArtifact);
66  
67      ZonkyIOPostgresLocator(String serverVersion) {
68          this.serverVersion = checkNotNull(serverVersion, "serverVersion is null");
69  
70          this.os = computeOS();
71          this.architecture = computeTarXzArchitectureName();
72          LOG.debug(format("Detected a %s %s system, using PostgreSQL version %s/%s", EmbeddedUtil.OS_ARCH, os, serverVersion, architecture));
73      }
74  
75      @Override
76      public InputStream getInputStream() throws IOException {
77          try {
78              File artifactFile = fileSupplier.get();
79              return createJarStream(artifactFile);
80          } catch (UncheckedIOException e) {
81              throw e.getCause();
82          }
83      }
84  
85      @Override
86      @Nonnull
87      public String getIdentifier() throws IOException {
88          // the optimized identifier computation saves ~ 1% CPU according to the profiler
89          try {
90              File artifactFile = fileSupplier.get();
91              HashCode hashCode = Hashing.murmur3_128().hashString(artifactFile.getAbsolutePath(), StandardCharsets.UTF_8);
92              return INSTALL_DIRECTORY_PREFIX + BaseEncoding.base16().encode(hashCode.asBytes());
93          } catch (UncheckedIOException e) {
94              throw e.getCause();
95          }
96      }
97  
98      private File loadArtifact() {
99          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 }