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 java.io.File;
24  import java.io.FilterInputStream;
25  import java.io.IOException;
26  import java.io.InputStream;
27  import java.io.UncheckedIOException;
28  import java.nio.charset.StandardCharsets;
29  import java.util.Objects;
30  import java.util.function.Supplier;
31  import java.util.jar.JarEntry;
32  import java.util.jar.JarFile;
33  
34  import com.google.common.base.Suppliers;
35  import com.google.common.hash.HashCode;
36  import com.google.common.hash.Hashing;
37  import com.google.common.io.BaseEncoding;
38  import org.slf4j.Logger;
39  import org.slf4j.LoggerFactory;
40  
41  /**
42   * Resolves PostgreSQL archives from the Maven repository. Looks for the zonky.io artifacts located at
43   * <code>io.zonky.test.postgres:embedded-postgres-binaries-&lt;os&gt;-&lt;arch&gt;</code>.
44   * <p>
45   * See <a href="https://github.com/zonkyio/embedded-postgres-binaries">The Zonky IO github page</a> for more details.
46   *
47   * @since 3.0
48   */
49  public final class ZonkyIOPostgresLocator implements NativeBinaryLocator {
50  
51      private static final String ZONKY_GROUP_ID = "io.zonky.test.postgres";
52      private static final String ZONKY_ARTIFACT_ID_TEMPLATE = "embedded-postgres-binaries-%s-%s";
53  
54      public static final Logger LOG = LoggerFactory.getLogger(ZonkyIOPostgresLocator.class);
55  
56      private static final boolean PREFER_NATIVE = Boolean.getBoolean("pg-embedded.prefer-native");
57  
58      private final String architecture;
59      private final String os;
60      private final String serverVersion;
61  
62      private final MavenArtifactLoader artifactLoader = new MavenArtifactLoader();
63  
64      private final Supplier<File> fileSupplier = Suppliers.memoize(this::loadArtifact);
65  
66      ZonkyIOPostgresLocator(String serverVersion) {
67          this.serverVersion = checkNotNull(serverVersion, "serverVersion is null");
68  
69          this.os = computeOS();
70          this.architecture = computeTarXzArchitectureName();
71          LOG.debug(format("Detected a %s %s system, using PostgreSQL version %s/%s", EmbeddedUtil.OS_ARCH, os, serverVersion, architecture));
72      }
73  
74      @Override
75      public InputStream getInputStream() throws IOException {
76          try {
77              File artifactFile = fileSupplier.get();
78              return createJarStream(artifactFile);
79          } catch (UncheckedIOException e) {
80              throw e.getCause();
81          }
82      }
83  
84      @Override
85      public String getIdentifier() throws IOException {
86          // the optimized identifier computation saves ~ 1% CPU according to the profiler
87          try {
88              File artifactFile = fileSupplier.get();
89              HashCode hashCode = Hashing.murmur3_128().hashString(artifactFile.getAbsolutePath(), StandardCharsets.UTF_8);
90              return INSTALL_DIRECTORY_PREFIX + BaseEncoding.base16().encode(hashCode.asBytes());
91          } catch (UncheckedIOException e) {
92              throw e.getCause();
93          }
94      }
95  
96      private File loadArtifact() {
97          try {
98              String artifactId = format(ZONKY_ARTIFACT_ID_TEMPLATE, this.os, computeJarArchitectureName());
99  
100             // alpine hack
101             if (EmbeddedUtil.IS_ALPINE_LINUX) {
102                 artifactId += "-alpine";
103             }
104 
105             String version = artifactLoader.findLatestVersion(ZONKY_GROUP_ID, artifactId, serverVersion);
106             File file = artifactLoader.getArtifactFile(ZONKY_GROUP_ID, artifactId, version);
107             checkState(file != null && file.exists(), "Could not locate artifact file for %s:%s", artifactId, version);
108             LOG.info(format("Using PostgreSQL version %s (%s)", version, architecture));
109             return file;
110         } catch (IOException e) {
111             throw new UncheckedIOException(e);
112         }
113     }
114 
115     private InputStream createJarStream(File file) {
116         try {
117             JarFile jar = new JarFile(file);
118             String entryName = format("postgres-%s-%s", computeOS(), computeTarXzArchitectureName());
119 
120             // alpine hack
121             if (EmbeddedUtil.IS_ALPINE_LINUX) {
122                 entryName += "-alpine_linux";
123             }
124 
125             JarEntry jarEntry = jar.getJarEntry(entryName + ".txz");
126             checkState(jarEntry != null, "Could not locate %s in the jar file (%s)", entryName, file.getAbsoluteFile());
127 
128             // When the input stream gets closed, close the jar file as well.
129             return new FilterInputStream(jar.getInputStream(jarEntry)) {
130                 @Override
131                 public void close() throws IOException {
132                     try {
133                         super.close();
134                     } finally {
135                         jar.close();
136                     }
137                 }
138             };
139         } catch (IOException e) {
140             throw new UncheckedIOException(e);
141         }
142     }
143 
144     @Override
145     public String toString() {
146         return format("ZonkyIO Stream locator for PostgreSQL (machine: %s os: %s, arch: %s, version: %s)", EmbeddedUtil.OS_ARCH, os, architecture, serverVersion);
147     }
148 
149     @Override
150     public boolean equals(Object o) {
151         if (this == o) {
152             return true;
153         }
154         if (o == null || getClass() != o.getClass()) {
155             return false;
156         }
157         ZonkyIOPostgresLocator that = (ZonkyIOPostgresLocator) o;
158         return architecture.equals(that.architecture) && os.equals(that.os) && serverVersion.equals(that.serverVersion);
159     }
160 
161     @Override
162     public int hashCode() {
163         return Objects.hash(architecture, os, serverVersion);
164     }
165 
166     private static String computeTarXzArchitectureName() {
167         String architecture = EmbeddedUtil.OS_ARCH;
168         if (EmbeddedUtil.IS_ARCH_X86_64) {
169             architecture = "x86_64";  // Zonky uses x86_64
170         } else if (EmbeddedUtil.IS_ARCH_AARCH64) {
171             if (!PREFER_NATIVE && EmbeddedUtil.IS_OS_MAC) {
172                 // Mac binaries are fat binaries stored as x86_64
173                 architecture = "x86_64";
174             } else {
175                 architecture = "arm_64";
176             }
177         } else if (EmbeddedUtil.IS_ARCH_AARCH32) {
178             architecture = "arm_32";
179         }
180         return architecture;
181     }
182 
183     private static String computeJarArchitectureName() {
184         String architecture = EmbeddedUtil.OS_ARCH;
185         if (EmbeddedUtil.IS_ARCH_X86_64) {
186             architecture = "amd64";  // Zonky uses amd64 for the jar name
187         } else if (EmbeddedUtil.IS_ARCH_AARCH64) {
188             if (!PREFER_NATIVE && EmbeddedUtil.IS_OS_MAC) {
189                 // Mac binaries are fat binaries stored as amd64
190                 architecture = "amd64";
191             } else {
192                 architecture = "arm64v8";
193             }
194         } else if (EmbeddedUtil.IS_ARCH_AARCH32) {
195             architecture = "arm32v7";
196         }
197         return architecture;
198     }
199 
200     private static String computeOS() {
201         String os = EmbeddedUtil.OS_NAME;
202         if (EmbeddedUtil.IS_OS_LINUX) {
203             os = "linux";
204         } else if (EmbeddedUtil.IS_OS_MAC) {
205             os = "darwin";
206         } else if (EmbeddedUtil.IS_OS_WINDOWS) {
207             os = "windows";
208         }
209         return os;
210     }
211 }