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 *
047 * @since 3.0
048 */
049public final class ZonkyIOPostgresLocator implements NativeBinaryLocator {
050
051    private static final String ZONKY_GROUP_ID = "io.zonky.test.postgres";
052    private static final String ZONKY_ARTIFACT_ID_TEMPLATE = "embedded-postgres-binaries-%s-%s";
053
054    public static final Logger LOG = LoggerFactory.getLogger(ZonkyIOPostgresLocator.class);
055
056    private static final boolean PREFER_NATIVE = Boolean.getBoolean("pg-embedded.prefer-native");
057
058    private final String architecture;
059    private final String os;
060    private final String serverVersion;
061
062    private final MavenArtifactLoader artifactLoader = new MavenArtifactLoader();
063
064    private final Supplier<File> fileSupplier = Suppliers.memoize(this::loadArtifact);
065
066    ZonkyIOPostgresLocator(String serverVersion) {
067        this.serverVersion = checkNotNull(serverVersion, "serverVersion is null");
068
069        this.os = computeOS();
070        this.architecture = computeTarXzArchitectureName();
071        LOG.debug(format("Detected a %s %s system, using PostgreSQL version %s/%s", EmbeddedUtil.OS_ARCH, os, serverVersion, architecture));
072    }
073
074    @Override
075    public InputStream getInputStream() throws IOException {
076        try {
077            File artifactFile = fileSupplier.get();
078            return createJarStream(artifactFile);
079        } catch (UncheckedIOException e) {
080            throw e.getCause();
081        }
082    }
083
084    @Override
085    public String getIdentifier() throws IOException {
086        // the optimized identifier computation saves ~ 1% CPU according to the profiler
087        try {
088            File artifactFile = fileSupplier.get();
089            HashCode hashCode = Hashing.murmur3_128().hashString(artifactFile.getAbsolutePath(), StandardCharsets.UTF_8);
090            return INSTALL_DIRECTORY_PREFIX + BaseEncoding.base16().encode(hashCode.asBytes());
091        } catch (UncheckedIOException e) {
092            throw e.getCause();
093        }
094    }
095
096    private File loadArtifact() {
097        try {
098            String artifactId = format(ZONKY_ARTIFACT_ID_TEMPLATE, this.os, computeJarArchitectureName());
099
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}