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 java.io.File;
024import java.io.FilterInputStream;
025import java.io.IOException;
026import java.io.InputStream;
027import java.io.UncheckedIOException;
028import java.nio.charset.StandardCharsets;
029import java.util.Objects;
030import java.util.function.Supplier;
031import java.util.jar.JarEntry;
032import java.util.jar.JarFile;
033
034import com.google.common.base.Suppliers;
035import com.google.common.hash.HashCode;
036import com.google.common.hash.Hashing;
037import com.google.common.io.BaseEncoding;
038import org.slf4j.Logger;
039import org.slf4j.LoggerFactory;
040
041/**
042 * Resolves PostgreSQL archives from the Maven repository. Looks for the zonky.io artifacts located at
043 * <code>io.zonky.test.postgres:embedded-postgres-binaries-&lt;os&gt;-&lt;arch&gt;</code>.
044 * <p>
045 * See <a href="https://github.com/zonkyio/embedded-postgres-binaries">The Zonky IO github page</a> for more details.
046 */
047public final class ZonkyIOPostgresLocator implements NativeBinaryLocator {
048
049    private static final String ZONKY_GROUP_ID = "io.zonky.test.postgres";
050    private static final String ZONKY_ARTIFACT_ID_TEMPLATE = "embedded-postgres-binaries-%s-%s";
051
052    public static final Logger LOG = LoggerFactory.getLogger(ZonkyIOPostgresLocator.class);
053
054    private static final boolean PREFER_NATIVE = Boolean.getBoolean("pg-embedded.prefer-native");
055
056    private final String architecture;
057    private final String os;
058    private final String serverVersion;
059
060    private final MavenArtifactLoader artifactLoader = new MavenArtifactLoader();
061
062    private final Supplier<File> fileSupplier = Suppliers.memoize(this::loadArtifact);
063
064    ZonkyIOPostgresLocator(String serverVersion) {
065        this.serverVersion = checkNotNull(serverVersion, "serverVersion is null");
066
067        this.os = computeOS();
068        this.architecture = computeTarXzArchitectureName();
069        LOG.debug(format("Detected a %s %s system, using PostgreSQL version %s", architecture, os, serverVersion));
070    }
071
072    @Override
073    public InputStream getInputStream() throws IOException {
074        try {
075            File artifactFile = fileSupplier.get();
076            return createJarStream(artifactFile);
077        } catch (UncheckedIOException e) {
078            throw e.getCause();
079        }
080    }
081
082    @Override
083    public String getIdentifier() throws IOException {
084        // the optimized identifier computation saves ~ 1% CPU according to the profiler
085        try {
086            File artifactFile = fileSupplier.get();
087            HashCode hashCode = Hashing.murmur3_128().hashString(artifactFile.getAbsolutePath(), StandardCharsets.UTF_8);
088            return INSTALL_DIRECTORY_PREFIX + BaseEncoding.base16().encode(hashCode.asBytes());
089        } catch (UncheckedIOException e) {
090            throw e.getCause();
091        }
092    }
093
094    private File loadArtifact() {
095        try {
096            String artifactId = format(ZONKY_ARTIFACT_ID_TEMPLATE, this.os, computeJarArchitectureName());
097
098            // alpine hack
099            if (EmbeddedUtil.IS_ALPINE_LINUX) {
100                artifactId += "-alpine";
101            }
102
103            String version = artifactLoader.findLatestVersion(ZONKY_GROUP_ID, artifactId, serverVersion);
104            File file = artifactLoader.getArtifactFile(ZONKY_GROUP_ID, artifactId, version);
105            checkState(file != null && file.exists(), "Could not locate artifact file for %s:%s", artifactId, version);
106            LOG.info(format("Using PostgreSQL version %s", version));
107            return file;
108        } catch (IOException e) {
109            throw new UncheckedIOException(e);
110        }
111    }
112
113    private InputStream createJarStream(File file) {
114        try {
115            JarFile jar = new JarFile(file);
116            String entryName = format("postgres-%s-%s", computeOS(), computeTarXzArchitectureName());
117
118            // alpine hack
119            if (EmbeddedUtil.IS_ALPINE_LINUX) {
120                entryName += "-alpine_linux";
121            }
122
123            JarEntry jarEntry = jar.getJarEntry(entryName + ".txz");
124            checkState(jarEntry != null, "Could not locate %s in the jar file (%s)", entryName, file.getAbsoluteFile());
125
126            // When the input stream gets closed, close the jar file as well.
127            return new FilterInputStream(jar.getInputStream(jarEntry)) {
128                @Override
129                public void close() throws IOException {
130                    try {
131                        super.close();
132                    } finally {
133                        jar.close();
134                    }
135                }
136            };
137        } catch (IOException e) {
138            throw new UncheckedIOException(e);
139        }
140    }
141
142    @Override
143    public String toString() {
144        return format("ZonkyIO Stream locator for PostgreSQL (arch: %s os: %s, version: %s)", architecture, os, serverVersion);
145    }
146
147    @Override
148    public boolean equals(Object o) {
149        if (this == o) {
150            return true;
151        }
152        if (o == null || getClass() != o.getClass()) {
153            return false;
154        }
155        ZonkyIOPostgresLocator that = (ZonkyIOPostgresLocator) o;
156        return architecture.equals(that.architecture) && os.equals(that.os) && serverVersion.equals(that.serverVersion);
157    }
158
159    @Override
160    public int hashCode() {
161        return Objects.hash(architecture, os, serverVersion);
162    }
163
164    private static String computeTarXzArchitectureName() {
165        String architecture = EmbeddedUtil.OS_ARCH;
166        if (EmbeddedUtil.IS_ARCH_X86_64) {
167            architecture = "x86_64";  // Zonky uses x86_64
168        } else if (EmbeddedUtil.IS_ARCH_AARCH64) {
169            if (!PREFER_NATIVE && EmbeddedUtil.IS_OS_MAC) {
170                // Mac binaries are fat binaries stored as x86_64
171                architecture = "x86_64";
172            } else {
173                architecture = "arm_64";
174            }
175        } else if (EmbeddedUtil.IS_ARCH_AARCH32) {
176            architecture = "arm_32";
177        }
178        return architecture;
179    }
180
181    private static String computeJarArchitectureName() {
182        String architecture = EmbeddedUtil.OS_ARCH;
183        if (EmbeddedUtil.IS_ARCH_X86_64) {
184            architecture = "amd64";  // Zonky uses amd64 for the jar name
185        } else if (EmbeddedUtil.IS_ARCH_AARCH64) {
186            if (!PREFER_NATIVE && EmbeddedUtil.IS_OS_MAC) {
187                // Mac binaries are fat binaries stored as amd64
188                architecture = "amd64";
189            } else {
190                architecture = "arm64v8";
191            }
192        } else if (EmbeddedUtil.IS_ARCH_AARCH32) {
193            architecture = "arm32v7";
194        }
195        return architecture;
196    }
197
198    private static String computeOS() {
199        String os = EmbeddedUtil.OS_NAME;
200        if (EmbeddedUtil.IS_OS_LINUX) {
201            os = "linux";
202        } else if (EmbeddedUtil.IS_OS_MAC) {
203            os = "darwin";
204        } else if (EmbeddedUtil.IS_OS_WINDOWS) {
205            os = "windows";
206        }
207        return os;
208    }
209}