UncompressBundleDirectoryResolver.java

/*
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package de.softwareforge.testing.postgres.embedded;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static java.lang.String.format;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.channels.FileLock;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Supplier;

import com.google.common.base.Suppliers;
import com.google.common.io.BaseEncoding;
import com.google.common.io.ByteStreams;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class UncompressBundleDirectoryResolver implements PgDirectoryResolver {

    private static final Logger LOG = LoggerFactory.getLogger(UncompressBundleDirectoryResolver.class);

    private static final String INSTALL_DIRECTORY_PREFIX = "PG-";

    private static final Supplier<UncompressBundleDirectoryResolver> DEFAULT_INSTANCE_HOLDER =
            Suppliers.memoize(UncompressBundleDirectoryResolver::new);

    private static final Map<PgArchiveResolver, File> KNOWN_INSTALLATIONS = new ConcurrentHashMap<>();

    private final Lock prepareBinariesLock = new ReentrantLock();

    private final PgArchiveResolver pgArchiveResolver;

    public static synchronized UncompressBundleDirectoryResolver getDefault() {
        return DEFAULT_INSTANCE_HOLDER.get();
    }

    private UncompressBundleDirectoryResolver() {
        this(ZonkyIOPostgresArchiveResolver.INSTANCE);
    }

    public UncompressBundleDirectoryResolver(PgArchiveResolver pgArchiveResolver) {
        this.pgArchiveResolver = checkNotNull(pgArchiveResolver, "pgArchiveResolver is null");
    }

    @Override
    public File getDirectory(final File installationDirectory) {
        prepareBinariesLock.lock();
        try {
            if (KNOWN_INSTALLATIONS.containsKey(pgArchiveResolver)) {
                File pgDir = KNOWN_INSTALLATIONS.get(pgArchiveResolver);
                if (pgDir.exists()) {
                    return pgDir;
                }
            }

            final String system = EmbeddedUtil.getOS();
            final String machineHardware = EmbeddedUtil.getArchitecture();

            LOG.debug(format("Detected a %s %s system", system, machineHardware));

            String pgDigest;
            try (InputStream pgArchive = pgArchiveResolver.locatePgArchive(system, machineHardware)) {
                checkState(pgArchive != null, "No Postgres archive found for " + system + " / " + machineHardware);
                try (DigestInputStream pgArchiveData = new DigestInputStream(pgArchive, MessageDigest.getInstance("MD5"))) {
                    ByteStreams.exhaust(pgArchiveData);
                    pgDigest = BaseEncoding.base16().encode(pgArchiveData.getMessageDigest().digest());
                }
            }

            File pgDir;
            if (!installationDirectory.setWritable(true, false)) {
                LOG.warn(format("Could not make install directory %s writable!", installationDirectory));
            }

            pgDir = new File(installationDirectory, INSTALL_DIRECTORY_PREFIX + pgDigest);

            EmbeddedUtil.mkdirs(pgDir);

            final File unpackLockFile = new File(pgDir, EmbeddedPostgres.LOCK_FILE_NAME);

            if (pgDir.getName().startsWith(INSTALL_DIRECTORY_PREFIX)) {
                final File pgDirExists = new File(pgDir, ".exists");
                if (!pgDirExists.exists()) {
                    try (FileOutputStream lockStream = new FileOutputStream(unpackLockFile);
                            FileLock unpackLock = lockStream.getChannel().tryLock()) {
                        if (unpackLock != null) {
                            try {
                                checkState(!pgDirExists.exists(), "unpack lock acquired but .exists file is present " + pgDirExists);
                                LOG.info("extracting archive...");
                                InputStream x = pgArchiveResolver.locatePgArchive(system, machineHardware);
                                EmbeddedUtil.extractTxz(x, pgDir.getPath());
                                checkState(pgDirExists.createNewFile(), "couldn't make .exists file " + pgDirExists);
                            } catch (Exception e) {
                                LOG.error("while unpacking archive:", e);
                            }
                        } else {
                            // the other guy is unpacking for us.
                            int maxAttempts = 60;
                            while (!pgDirExists.exists() && --maxAttempts > 0) { // NOPMD
                                Thread.sleep(1000L);
                            }
                            checkState(pgDirExists.exists(), "Waited 60 seconds for postgres to be unpacked but it never finished!");
                        }
                    } finally {
                        if (unpackLockFile.exists() && !unpackLockFile.delete()) {
                            LOG.error(format("could not remove lock file %s", unpackLockFile.getAbsolutePath()));
                        }
                    }
                }
            }

            KNOWN_INSTALLATIONS.putIfAbsent(pgArchiveResolver, pgDir);
            LOG.debug(format("Unpacked archive at %s", pgDir));
            return pgDir;
        } catch (final IOException | NoSuchAlgorithmException e) {
            throw new ExceptionInInitializerError(e);
        } catch (final InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new ExceptionInInitializerError(e);
        } finally {
            prepareBinariesLock.unlock();
        }
    }
}