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 edu.umd.cs.findbugs.annotations.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    public 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.findLatestVersion(ZONKY_GROUP_ID, artifactId, serverVersion);
108            File file = artifactLoader.getArtifactFile(ZONKY_GROUP_ID, artifactId, version);
109            checkState(file != null && file.exists(), "Could not locate artifact file for %s:%s", artifactId, version);
110            LOG.info(format("Using PostgreSQL version %s (%s)", version, architecture));
111            return file;
112        } catch (IOException e) {
113            throw new UncheckedIOException(e);
114        }
115    }
116
117    private InputStream createJarStream(File file) {
118        try {
119            JarFile jar = new JarFile(file);
120            String entryName = format("postgres-%s-%s", computeOS(), computeTarXzArchitectureName());
121
122            // alpine hack
123            if (EmbeddedUtil.IS_ALPINE_LINUX) {
124                entryName += "-alpine_linux";
125            }
126
127            JarEntry jarEntry = jar.getJarEntry(entryName + ".txz");
128            checkState(jarEntry != null, "Could not locate %s in the jar file (%s)", entryName, file.getAbsoluteFile());
129
130            // When the input stream gets closed, close the jar file as well.
131            return new FilterInputStream(jar.getInputStream(jarEntry)) {
132                @Override
133                public void close() throws IOException {
134                    try {
135                        super.close();
136                    } finally {
137                        jar.close();
138                    }
139                }
140            };
141        } catch (IOException e) {
142            throw new UncheckedIOException(e);
143        }
144    }
145
146    @Override
147    public String toString() {
148        return format("ZonkyIO Stream locator for PostgreSQL (machine: %s os: %s, arch: %s, version: %s)", EmbeddedUtil.OS_ARCH, os, architecture, serverVersion);
149    }
150
151    @Override
152    public boolean equals(Object o) {
153        if (this == o) {
154            return true;
155        }
156        if (o == null || getClass() != o.getClass()) {
157            return false;
158        }
159        ZonkyIOPostgresLocator that = (ZonkyIOPostgresLocator) o;
160        return architecture.equals(that.architecture) && os.equals(that.os) && serverVersion.equals(that.serverVersion);
161    }
162
163    @Override
164    public int hashCode() {
165        return Objects.hash(architecture, os, serverVersion);
166    }
167
168    private static String computeTarXzArchitectureName() {
169        String architecture = EmbeddedUtil.OS_ARCH;
170        if (EmbeddedUtil.IS_ARCH_X86_64) {
171            architecture = "x86_64";  // Zonky uses x86_64
172        } else if (EmbeddedUtil.IS_ARCH_AARCH64) {
173            if (!PREFER_NATIVE && EmbeddedUtil.IS_OS_MAC) {
174                // Mac binaries are fat binaries stored as x86_64
175                architecture = "x86_64";
176            } else {
177                architecture = "arm_64";
178            }
179        } else if (EmbeddedUtil.IS_ARCH_AARCH32) {
180            architecture = "arm_32";
181        }
182        return architecture;
183    }
184
185    private static String computeJarArchitectureName() {
186        String architecture = EmbeddedUtil.OS_ARCH;
187        if (EmbeddedUtil.IS_ARCH_X86_64) {
188            architecture = "amd64";  // Zonky uses amd64 for the jar name
189        } else if (EmbeddedUtil.IS_ARCH_AARCH64) {
190            if (!PREFER_NATIVE && EmbeddedUtil.IS_OS_MAC) {
191                // Mac binaries are fat binaries stored as amd64
192                architecture = "amd64";
193            } else {
194                architecture = "arm64v8";
195            }
196        } else if (EmbeddedUtil.IS_ARCH_AARCH32) {
197            architecture = "arm32v7";
198        }
199        return architecture;
200    }
201
202    private static String computeOS() {
203        String os = EmbeddedUtil.OS_NAME;
204        if (EmbeddedUtil.IS_OS_LINUX) {
205            os = "linux";
206        } else if (EmbeddedUtil.IS_OS_MAC) {
207            os = "darwin";
208        } else if (EmbeddedUtil.IS_OS_WINDOWS) {
209            os = "windows";
210        }
211        return os;
212    }
213}