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.checkArgument;
18  import static com.google.common.base.Preconditions.checkNotNull;
19  import static com.google.common.base.Preconditions.checkState;
20  import static de.softwareforge.testing.postgres.embedded.DatabaseInfo.PG_DEFAULT_DB;
21  import static de.softwareforge.testing.postgres.embedded.DatabaseInfo.PG_DEFAULT_USER;
22  import static de.softwareforge.testing.postgres.embedded.EmbeddedUtil.formatDuration;
23  import static java.lang.String.format;
24  
25  import java.io.File;
26  import java.io.FileOutputStream;
27  import java.io.IOException;
28  import java.io.InputStreamReader;
29  import java.net.ConnectException;
30  import java.net.InetAddress;
31  import java.net.InetSocketAddress;
32  import java.net.Socket;
33  import java.nio.channels.FileLock;
34  import java.nio.channels.OverlappingFileLockException;
35  import java.nio.charset.StandardCharsets;
36  import java.nio.file.Path;
37  import java.sql.Connection;
38  import java.sql.ResultSet;
39  import java.sql.SQLException;
40  import java.sql.Statement;
41  import java.time.Duration;
42  import java.util.HashMap;
43  import java.util.List;
44  import java.util.Map;
45  import java.util.Map.Entry;
46  import java.util.Objects;
47  import java.util.concurrent.TimeUnit;
48  import java.util.concurrent.atomic.AtomicBoolean;
49  import javax.sql.DataSource;
50  
51  import com.google.common.annotations.VisibleForTesting;
52  import com.google.common.base.Stopwatch;
53  import com.google.common.collect.ImmutableList;
54  import com.google.common.collect.ImmutableMap;
55  import com.google.common.io.CharStreams;
56  import com.google.common.io.Closeables;
57  import edu.umd.cs.findbugs.annotations.NonNull;
58  import edu.umd.cs.findbugs.annotations.Nullable;
59  import org.postgresql.ds.PGSimpleDataSource;
60  import org.slf4j.Logger;
61  import org.slf4j.LoggerFactory;
62  
63  /**
64   * Manages an embedded PostgreSQL server instance.
65   */
66  public final class EmbeddedPostgres implements AutoCloseable {
67  
68      /**
69       * The version of postgres used if no specific version has been given.
70       */
71      public static final String DEFAULT_POSTGRES_VERSION = "13";
72  
73      static final String[] LOCALHOST_SERVER_NAMES = new String[]{"localhost"};
74  
75      private static final String PG_TEMPLATE_DB = "template1";
76  
77      @VisibleForTesting
78      static final Duration DEFAULT_PG_STARTUP_WAIT = Duration.ofSeconds(10);
79  
80      // folders need to be at least 10 minutes old to be considered for deletion.
81      private static final long MINIMUM_AGE_IN_MS = Duration.ofMinutes(10).toMillis();
82  
83      // prefix for data folders in the parent that might be deleted
84      private static final String DATA_DIRECTORY_PREFIX = "data-";
85  
86      private static final String PG_STOP_MODE = "fast";
87      private static final String PG_STOP_WAIT_SECONDS = "5";
88      static final String LOCK_FILE_NAME = "epg-lock";
89  
90      private final Logger logger;
91  
92      private final String instanceId;
93      private final File postgresInstallDirectory;
94      private final File dataDirectory;
95  
96      private final Duration serverStartupWait;
97      private final int port;
98      private final AtomicBoolean started = new AtomicBoolean();
99      private final AtomicBoolean closed = new AtomicBoolean();
100 
101     private final ImmutableMap<String, String> serverConfiguration;
102     private final ImmutableMap<String, String> localeConfiguration;
103     private final ImmutableMap<String, String> connectionProperties;
104 
105     private final File lockFile;
106     private volatile FileOutputStream lockStream;
107     private volatile FileLock lock;
108 
109     private final boolean removeDataOnShutdown;
110 
111     private final ProcessBuilder.Redirect errorRedirector;
112     private final ProcessBuilder.Redirect outputRedirector;
113 
114 
115     /**
116      * Returns an instance that has been started and configured. The {@link Builder#withDefaults()} configuration has been applied.
117      */
118     @NonNull
119     public static EmbeddedPostgres defaultInstance() throws IOException {
120         return builderWithDefaults().build();
121     }
122 
123     /**
124      * Returns a builder with default {@link Builder#withDefaults()} configuration already applied.
125      */
126     @NonNull
127     public static EmbeddedPostgres.Builder builderWithDefaults() {
128         return new Builder().withDefaults();
129     }
130 
131     /**
132      * Returns a new {@link Builder}.
133      */
134     @NonNull
135     public static EmbeddedPostgres.Builder builder() {
136         return new Builder();
137     }
138 
139     private EmbeddedPostgres(
140             final String instanceId,
141             final File postgresInstallDirectory,
142             final File dataDirectory,
143             final boolean removeDataOnShutdown,
144             final Map<String, String> serverConfiguration,
145             final Map<String, String> localeConfiguration,
146             final Map<String, String> connectionProperties,
147             final int port,
148             final ProcessBuilder.Redirect errorRedirector,
149             final ProcessBuilder.Redirect outputRedirector,
150             final Duration serverStartupWait) {
151 
152         this.instanceId = checkNotNull(instanceId, "instanceId is null");
153 
154         this.logger = LoggerFactory.getLogger(toString());
155 
156         this.postgresInstallDirectory = checkNotNull(postgresInstallDirectory, "postgresInstallDirectory is null");
157         this.dataDirectory = checkNotNull(dataDirectory, "dataDirectory is null");
158 
159         this.removeDataOnShutdown = removeDataOnShutdown;
160 
161         this.serverConfiguration = ImmutableMap.copyOf(checkNotNull(serverConfiguration, "serverConfiguration is null"));
162         this.localeConfiguration = ImmutableMap.copyOf(checkNotNull(localeConfiguration, "localeConfiguration is null"));
163         this.connectionProperties = ImmutableMap.copyOf(checkNotNull(connectionProperties, "connectionProperties is null"));
164 
165         this.port = port;
166 
167         this.errorRedirector = checkNotNull(errorRedirector, "errorRedirector is null");
168         this.outputRedirector = checkNotNull(outputRedirector, "outputRedirector is null");
169 
170         this.serverStartupWait = checkNotNull(serverStartupWait, "serverStartupWait is null");
171         this.lockFile = new File(this.dataDirectory, LOCK_FILE_NAME);
172 
173         logger.debug(format("data dir is %s, install dir is %s", this.dataDirectory, this.postgresInstallDirectory));
174     }
175 
176     /**
177      * Creates a {@link DataSource} object that connects to the standard system database.
178      * <p>
179      * The standard system database is the <code>template1</code> database.
180      * <p>
181      * Any modification to this database will be propagated to any new database that is created with <code>CREATE DATABASE...</code> unless another database is
182      * explicitly named as the template..
183      */
184     @NonNull
185     public DataSource createTemplateDataSource() throws SQLException {
186         return createDataSource(PG_DEFAULT_USER, PG_TEMPLATE_DB, getPort(), getConnectionProperties());
187     }
188 
189     /**
190      * Creates a {@link DataSource} object that connects to the default database.
191      * <p>
192      * The default database is the <code>postgres</code> database.
193      */
194     @NonNull
195     public DataSource createDefaultDataSource() throws SQLException {
196         return createDataSource(PG_DEFAULT_USER, PG_DEFAULT_DB, getPort(), getConnectionProperties());
197     }
198 
199     /**
200      * Creates a {@link DataSource} with a specific user and database name.
201      * <p>
202      * Creating the DataSource does <b>not</b> create the database or the user itself. This must be done by the calling code (e.g. with a {@link
203      * EmbeddedPostgresPreparer}).
204      */
205     @NonNull
206     public DataSource createDataSource(@NonNull String user, @NonNull String databaseName) throws SQLException {
207         return createDataSource(user, databaseName, getPort(), getConnectionProperties());
208     }
209 
210     static DataSource createDataSource(String user, String databaseName, int port, Map<String, String> connectionProperties) throws SQLException {
211         checkNotNull(user, "user is null");
212         checkNotNull(databaseName, "databaseName is null");
213         checkNotNull(connectionProperties, "connectionProperties is null");
214 
215         final PGSimpleDataSource ds = new PGSimpleDataSource();
216 
217         ds.setServerNames(LOCALHOST_SERVER_NAMES);
218         ds.setPortNumbers(new int[]{port});
219         ds.setDatabaseName(databaseName);
220         ds.setUser(user);
221 
222         for (final Entry<String, String> entry : connectionProperties.entrySet()) {
223             ds.setProperty(entry.getKey(), entry.getValue());
224         }
225 
226         return ds;
227     }
228 
229     /**
230      * Returns the network (TCP) port for the PostgreSQL server instance.
231      */
232     public int getPort() {
233         return port;
234     }
235 
236     /**
237      * Returns the connection properties for the PostgreSQL server instance.
238      */
239     @NonNull
240     ImmutableMap<String, String> getConnectionProperties() {
241         return connectionProperties;
242     }
243 
244     /**
245      * Returns the instance id for the PostgreSQL server instance. This id is an alphanumeric string that can be used to differentiate between multiple embedded
246      * PostgreSQL server instances.
247      */
248     @NonNull
249     public String instanceId() {
250         return instanceId;
251     }
252 
253     @Override
254     public String toString() {
255         return this.getClass().getName() + "$" + this.instanceId;
256     }
257 
258     @Override
259     public boolean equals(Object o) {
260         if (this == o) {
261             return true;
262         }
263         if (o == null || getClass() != o.getClass()) {
264             return false;
265         }
266         EmbeddedPostgres/../../de/softwareforge/testing/postgres/embedded/EmbeddedPostgres.html#EmbeddedPostgres">EmbeddedPostgres that = (EmbeddedPostgres) o;
267         return instanceId.equals(that.instanceId);
268     }
269 
270     @Override
271     public int hashCode() {
272         return Objects.hash(instanceId);
273     }
274 
275     // internal methods
276     DatabaseInfo createDefaultDatabaseInfo() {
277         return DatabaseInfo.builder().port(getPort()).connectionProperties(getConnectionProperties()).build();
278     }
279 
280 
281     private void boot() throws IOException {
282         EmbeddedUtil.mkdirs(this.dataDirectory);
283 
284         if (this.removeDataOnShutdown || !new File(this.dataDirectory, "postgresql.conf").exists()) {
285             initDatabase();
286         }
287 
288         lock();
289 
290         startDatabase();
291     }
292 
293 
294     private synchronized void lock() throws IOException {
295         this.lockStream = new FileOutputStream(this.lockFile);
296         this.lock = lockStream.getChannel().tryLock();
297         checkState(lock != null, "could not lock %s", lockFile);
298     }
299 
300     private synchronized void unlock() throws IOException {
301         if (lock != null) {
302             lock.release();
303         }
304         Closeables.close(lockStream, true);
305     }
306 
307     private void initDatabase() throws IOException {
308         ImmutableList.Builder<String> commandBuilder = ImmutableList.builder();
309         commandBuilder.add(pgBin("initdb"))
310                 .addAll(createInitDbOptions())
311                 .add("-A", "trust",
312                         "-U", PG_DEFAULT_USER,
313                         "-D", this.dataDirectory.getPath(),
314                         "-E", "UTF-8");
315         final Stopwatch watch = system(commandBuilder.build());
316         logger.debug(format("initdb completed in %s", formatDuration(watch.elapsed())));
317     }
318 
319     private void startDatabase() throws IOException {
320         checkState(!started.getAndSet(true), "database already started!");
321 
322         final ImmutableList.Builder<String> commandBuilder = ImmutableList.builder();
323         commandBuilder.add(pgBin("pg_ctl"),
324                 "-D", this.dataDirectory.getPath(),
325                 "-o", String.join(" ", createInitOptions()),
326                 "start"
327         );
328 
329         final Stopwatch watch = Stopwatch.createStarted();
330         final Process postmaster = spawn("pg", commandBuilder.build());
331 
332         logger.info(format("started as pid %d on port %d", postmaster.pid(), port));
333         logger.debug(format("Waiting up to %s for server startup to finish", formatDuration(serverStartupWait)));
334 
335         Runtime.getRuntime().addShutdownHook(newCloserThread());
336 
337         checkState(waitForServerStartup(), "Could not start pg, interrupted?");
338         logger.debug(format("startup complete in %s", formatDuration(watch.elapsed())));
339     }
340 
341     private void stopDatabase(File dataDirectory) throws IOException {
342         final ImmutableList.Builder<String> commandBuilder = ImmutableList.builder();
343         commandBuilder.add(pgBin("pg_ctl"),
344                 "-D", dataDirectory.getPath(),
345                 "stop",
346                 "-m", PG_STOP_MODE,
347                 "-t", PG_STOP_WAIT_SECONDS, "-w");
348 
349         final Stopwatch watch = system(commandBuilder.build());
350         logger.debug(format("shutdown complete in %s", formatDuration(watch.elapsed())));
351     }
352 
353     private List<String> createInitOptions() {
354         final ImmutableList.Builder<String> initOptions = ImmutableList.builder();
355         initOptions.add(
356                 "-p", Integer.toString(port),
357                 "-F");
358 
359         serverConfiguration.forEach((key, value) -> {
360             initOptions.add("-c");
361             if (value.length() > 0) {
362                 initOptions.add(key + "=" + value);
363             } else {
364                 initOptions.add(key + "=true");
365             }
366         });
367 
368         return initOptions.build();
369     }
370 
371     @VisibleForTesting
372     List<String> createInitDbOptions() {
373         final ImmutableList.Builder<String> localeOptions = ImmutableList.builder();
374 
375         localeConfiguration.forEach((key, value) -> {
376             if (value.length() > 0) {
377                 localeOptions.add("--" + key + "=" + value);
378             } else {
379                 localeOptions.add("--" + key);
380             }
381         });
382         return localeOptions.build();
383     }
384 
385     private boolean waitForServerStartup() throws IOException {
386         Throwable lastCause = null;
387         final long start = System.nanoTime();
388         final long maxWaitNs = TimeUnit.NANOSECONDS.convert(serverStartupWait.toMillis(), TimeUnit.MILLISECONDS);
389         while (System.nanoTime() - start < maxWaitNs) {
390             try {
391                 if (verifyReady()) {
392                     return true;
393                 }
394             } catch (final SQLException e) {
395                 lastCause = e;
396                 logger.trace("while waiting for server startup:", e);
397             }
398 
399             try {
400                 Thread.sleep(100);
401             } catch (final InterruptedException e) {
402                 Thread.currentThread().interrupt();
403                 return false;
404             }
405         }
406         throw new IOException("Gave up waiting for server to start after " + serverStartupWait.toMillis() + "ms", lastCause);
407     }
408 
409     private boolean verifyReady() throws IOException, SQLException {
410         // check TCP connection
411         final InetAddress localhost = InetAddress.getLoopbackAddress();
412         try (Socket sock = new Socket()) {
413             sock.setSoTimeout((int) Duration.ofMillis(500).toMillis());
414             sock.connect(new InetSocketAddress(localhost, port), (int) Duration.ofMillis(500).toMillis());
415         } catch (ConnectException e) {
416             return false;
417         }
418 
419         // check JDBC connection
420         try (Connection c = createDefaultDataSource().getConnection();
421                 Statement s = c.createStatement();
422                 ResultSet rs = s.executeQuery("SELECT 1")) {
423             checkState(rs.next(), "expecting single row");
424             checkState(rs.getInt(1) == 1, "expecting 1 as result");
425             checkState(!rs.next(), "expecting single row");
426             return true;
427         }
428     }
429 
430     private Thread newCloserThread() {
431         final Thread closeThread = new Thread(() -> {
432             try {
433                 EmbeddedPostgres.this.close();
434             } catch (IOException e) {
435                 logger.trace("while closing instance:", e);
436             }
437         });
438 
439         closeThread.setName("pg-closer");
440         return closeThread;
441     }
442 
443     /**
444      * Closing an {@link EmbeddedPostgres} instance shuts down the connected database instance.
445      */
446     @Override
447     public void close() throws IOException {
448         if (closed.getAndSet(true)) {
449             return;
450         }
451 
452         try {
453             stopDatabase(this.dataDirectory);
454         } catch (final Exception e) {
455             logger.error("could not stop pg:", e);
456         }
457 
458         unlock();
459 
460         if (removeDataOnShutdown) {
461             try {
462                 EmbeddedUtil.rmdirs(dataDirectory);
463             } catch (Exception e) {
464                 logger.error(format("Could not clean up directory %s:", dataDirectory.getAbsolutePath()), e);
465             }
466         } else {
467             logger.debug(format("preserved data directory %s", dataDirectory.getAbsolutePath()));
468         }
469     }
470 
471     @VisibleForTesting
472     File getDataDirectory() {
473         return dataDirectory;
474     }
475 
476     @VisibleForTesting
477     Map<String, String> getLocaleConfiguration() {
478         return localeConfiguration;
479     }
480 
481 
482     private void cleanOldDataDirectories(File parentDirectory) {
483         final File[] children = parentDirectory.listFiles();
484         if (children == null) {
485             return;
486         }
487         for (final File dir : children) {
488             if (!dir.isDirectory()) {
489                 continue;
490             }
491 
492             // only ever touch known data directories.
493             if (!dir.getName().startsWith(DATA_DIRECTORY_PREFIX)) {
494                 continue;
495             }
496 
497             // only touch data directories that hold a lock file.
498             final File lockFile = new File(dir, LOCK_FILE_NAME);
499             if (!lockFile.exists()) {
500                 continue;
501             }
502 
503             // file must have a minimum age. This can not be the same check as
504             // the exists b/c non-existent files return 0 (epoch) as lastModified so
505             // they are considered "ancient".
506             if (System.currentTimeMillis() - lockFile.lastModified() < MINIMUM_AGE_IN_MS) {
507                 continue;
508             }
509 
510             try (FileOutputStream fos = new FileOutputStream(lockFile);
511                     FileLock lock = fos.getChannel().tryLock()) {
512                 if (lock != null) {
513                     logger.debug(format("found stale data directory %s", dir));
514                     if (new File(dir, "postmaster.pid").exists()) {
515                         try {
516                             stopDatabase(dir);
517                             logger.debug("shutting down orphaned database!");
518                         } catch (Exception e) {
519                             logger.warn(format("failed to orphaned database in %s:", dir), e);
520                         }
521                     }
522                     EmbeddedUtil.rmdirs(dir);
523                 }
524             } catch (final OverlappingFileLockException e) {
525                 // The directory belongs to another instance in this VM.
526                 logger.trace("while cleaning old data directories:", e);
527             } catch (final Exception e) {
528                 logger.warn("while cleaning old data directories:", e);
529             }
530         }
531     }
532 
533     private String pgBin(String binaryName) {
534         final String extension = EmbeddedUtil.IS_OS_WINDOWS ? ".exe" : "";
535         return new File(this.postgresInstallDirectory, "bin/" + binaryName + extension).getPath();
536     }
537 
538     private Process spawn(@Nullable String processName, List<String> commandAndArgs) throws IOException {
539         final ProcessBuilder builder = new ProcessBuilder(commandAndArgs);
540         builder.redirectErrorStream(true);
541         builder.redirectError(errorRedirector);
542         builder.redirectOutput(outputRedirector);
543         final Process process = builder.start();
544 
545         processName = processName != null ? processName : process.info().command().map(EmbeddedUtil::getFileBaseName).orElse("<unknown>");
546         String name = format("%s (%d)", processName, process.pid());
547 
548         ProcessOutputLogger.logOutput(logger, name, process);
549         return process;
550     }
551 
552 
553     private Stopwatch system(List<String> commandAndArgs) throws IOException {
554         checkArgument(commandAndArgs.size() > 0, "No commandAndArgs given!");
555         String prefix = EmbeddedUtil.getFileBaseName(commandAndArgs.get(0));
556 
557         Stopwatch watch = Stopwatch.createStarted();
558         Process process = spawn(prefix, commandAndArgs);
559         try {
560             if (process.waitFor() != 0) {
561                 try (InputStreamReader reader = new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8)) {
562                     throw new IllegalStateException(format("Process %s failed%n%s",
563                             commandAndArgs, CharStreams.toString(reader)));
564                 }
565             }
566         } catch (InterruptedException e) {
567             Thread.currentThread().interrupt();
568         }
569 
570         return watch;
571     }
572 
573 
574     /**
575      * Callback interface to customize a builder during creation.
576      *
577      * @deprecated Use {@link EmbeddedPostgresPreparer} with {@link Builder} as type parameter.
578      */
579     @Deprecated
580     @FunctionalInterface
581     public interface BuilderCustomizer {
582 
583         /**
584          * Callback to customize a given {@link Builder}.
585          *
586          * @param builder The builder instance. Any method on the builder can be called.
587          * @throws SQLException For any SQL related problems.
588          * @throws IOException  For any IO related problem.
589          */
590         void customize(@NonNull Builder builder) throws IOException, SQLException;
591     }
592 
593     /**
594      * Creates a new {@link EmbeddedPostgres} instance and starts it.
595      */
596     public static class Builder {
597 
598         private File installationBaseDirectory = null;
599         private File dataDirectory = null;
600 
601         private final Map<String, String> serverConfiguration = new HashMap<>();
602         private final Map<String, String> localeConfiguration = new HashMap<>();
603         private boolean removeDataOnShutdown = true;
604         private int port = 0;
605         private String serverVersion = DEFAULT_POSTGRES_VERSION;
606         private final Map<String, String> connectionProperties = new HashMap<>();
607         private NativeBinaryManager nativeBinaryManager = null;
608         private Duration serverStartupWait = DEFAULT_PG_STARTUP_WAIT;
609 
610         private ProcessBuilder.Redirect errRedirector = ProcessBuilder.Redirect.PIPE;
611         private ProcessBuilder.Redirect outRedirector = ProcessBuilder.Redirect.PIPE;
612 
613         Builder() {
614         }
615 
616         /**
617          * Apply a set of defaults to the database server:
618          * <ul>
619          *     <li>timezone: UTC</li>
620          *     <li>synchronous_commit: off</li>
621          *     <li>max_connections: 300</li>
622          * </ul>
623          *
624          * @return The builder itself.
625          */
626         @NonNull
627         public Builder withDefaults() {
628             serverConfiguration.put("timezone", "UTC");
629             serverConfiguration.put("synchronous_commit", "off");
630             serverConfiguration.put("max_connections", "300");
631             return this;
632         }
633 
634         /**
635          * Sets the time that the builder will wait for the PostgreSQL server instance to start. Default is 10 seconds.
636          *
637          * @param serverStartupWait Startup wait time. Must not be null or negative.
638          * @return The builder itself.
639          */
640         @NonNull
641         public Builder setServerStartupWait(@NonNull Duration serverStartupWait) {
642             checkNotNull(serverStartupWait, "serverStartupWait is null");
643             checkArgument(!serverStartupWait.isNegative(), "Negative durations are not permitted.");
644 
645             this.serverStartupWait = serverStartupWait;
646             return this;
647         }
648 
649         /**
650          * Whether to remove the data directory on server shutdown. If true, the contents of the data directory are deleted when the {@link EmbeddedPostgres}
651          * instance is closed. Default is true.
652          *
653          * @param removeDataOnShutdown True removes the contents of the data directory on shutdown.
654          * @return The builder itself.
655          */
656         @NonNull
657         public Builder setRemoveDataOnShutdown(boolean removeDataOnShutdown) {
658             this.removeDataOnShutdown = removeDataOnShutdown;
659             return this;
660         }
661 
662         /**
663          * Explicitly set the location of the data directory. Default is using a managed directory.
664          *
665          * @param dataDirectory The directory to use. Must not be null. If it exists, the current user must be able to access the directory for reading and
666          *                      writing. If the directory does not exist then the current user must be able to create it for reading and writing.
667          * @return The builder itself.
668          */
669         @NonNull
670         public Builder setDataDirectory(@NonNull Path dataDirectory) {
671             checkNotNull(dataDirectory, "dataDirectory is null");
672             return setDataDirectory(dataDirectory.toFile());
673         }
674 
675         /**
676          * Explicitly set the location of the data directory. Default is using a managed directory.
677          *
678          * @param dataDirectory The directory to use. Must not be null. If it exists, the current user must be able to access the directory for reading and
679          *                      writing. If the directory does not exist then the current user must be able to create it for reading and writing.
680          * @return The builder itself.
681          */
682         @NonNull
683         public Builder setDataDirectory(@NonNull String dataDirectory) {
684             checkNotNull(dataDirectory, "dataDirectory is null");
685             return setDataDirectory(new File(dataDirectory));
686         }
687 
688         /**
689          * Explicitly set the location of the data directory. Default is using a managed directory.
690          *
691          * @param dataDirectory The directory to use. Must not be null. If it exists, the current user must be able to access the directory for reading and
692          *                      writing. If the directory does not exist then the current user must be able to create it for reading and writing.
693          * @return The builder itself.
694          */
695         @NonNull
696         public Builder setDataDirectory(@NonNull File dataDirectory) {
697             this.dataDirectory = checkNotNull(dataDirectory, "dataDirectory is null");
698             return this;
699         }
700 
701         /**
702          * Adds a server configuration parameter. All parameters are passed to the PostgreSQL server a startup using the <code>postgres</code> command.
703          * <p>
704          * Values and their function are specific to the PostgreSQL version selected.
705          * <p>
706          * See <a href="https://www.postgresql.org/docs/13/runtime-config.html">the PostgreSQL runtime configuration</a> for more information.
707          *
708          * @param key   Configuration parameter name. Must not be null.
709          * @param value Configuration parameter value. Must not be null.
710          * @return The builder itself.
711          */
712         @NonNull
713         public Builder addServerConfiguration(@NonNull String key, @NonNull String value) {
714             checkNotNull(key, "key is null");
715             checkNotNull(value, "value is null");
716             this.serverConfiguration.put(key, value);
717             return this;
718         }
719 
720         /**
721          * @deprecated Use {@link #addInitDbConfiguration(String, String)}.
722          */
723         @Deprecated
724         @NonNull
725         public Builder addLocaleConfiguration(@NonNull String key, @NonNull String value) {
726             checkNotNull(key, "key is null");
727             checkNotNull(value, "value is null");
728             this.localeConfiguration.put(key, value);
729             return this;
730         }
731 
732         /**
733          * Adds a configuration parameters for the <code>initdb</code> command. The <code>initdb</code> command is used to create the PostgreSQL server.
734          * <p>
735          * Each value is added as a command line parameter to the command.
736          * <p>
737          * See the <a href="https://www.postgresql.org/docs/13/app-initdb.html">PostgreSQL initdb documentation</a> for an overview of possible values.
738          *
739          * @param key   initdb parameter name. Must not be null.
740          * @param value initdb parameter value. Must not be null. When the empty string is used as the value, the resulting command line parameter will not have
741          *              a equal sign and a value assigned.
742          * @return The builder itself.
743          */
744         @NonNull
745         public Builder addInitDbConfiguration(@NonNull String key, @NonNull String value) {
746             checkNotNull(key, "key is null");
747             checkNotNull(value, "value is null");
748             this.localeConfiguration.put(key, value);
749             return this;
750         }
751 
752         /**
753          * Adds a connection property. These properties are set on every connection handed out by the data source. See
754          * <a href="https://jdbc.postgresql.org/documentation/head/connect.html#connection-parameters">the
755          * PostgreSQL JDBC driver documentation</a> for possible values.
756          *
757          * @param key   connection property name. Must not be null.
758          * @param value connection property value. Must not be null.
759          * @return The builder itself.
760          */
761         @NonNull
762         public Builder addConnectionProperty(@NonNull String key, @NonNull String value) {
763             checkNotNull(key, "key is null");
764             checkNotNull(value, "value is null");
765             this.connectionProperties.put(key, value);
766             return this;
767         }
768 
769         /**
770          * Sets the directory where the PostgreSQL distribution is unpacked. Setting the installation base directory resets the {@link NativeBinaryManager} used
771          * to locate the postgres installation back to the default (which is to download the zonky.io Postgres archive and unpack it in the installation
772          * directory. The default is using a managed directory.
773          *
774          * @param installationBaseDirectory The directory to unpack the postgres distribution. The current user must be able to create and write this directory.
775          *                                  Must not be null.
776          * @return The builder itself.
777          */
778         @NonNull
779         public Builder setInstallationBaseDirectory(@NonNull File installationBaseDirectory) {
780             checkNotNull(installationBaseDirectory, "installationBaseDirectory is null");
781             this.installationBaseDirectory = installationBaseDirectory;
782             this.nativeBinaryManager = null;
783             return this;
784         }
785 
786         /**
787          * Explicitly set the TCP port for the PostgreSQL server. If the port is not available, starting the server will fail. Default is to find and use an
788          * available TCP port.
789          *
790          * @param port The port to use. Must be &gt; 1023 and &lt; 65536.
791          * @return The builder itself.
792          */
793         @NonNull
794         public Builder setPort(int port) {
795             checkState(port > 1023 && port < 65535, "Port %s is not within 1024..65535", port);
796             this.port = port;
797             return this;
798         }
799 
800         /**
801          * Set the version of the PostgreSQL server. This value is passed to the default binary manager which will try to resolve this version from existing
802          * Maven artifacts. The value is ignored if {@link #setNativeBinaryManager(NativeBinaryManager)} is called.
803          * <p>
804          * Not every PostgreSQL version is supported by pg-embedded. Some older versions lack the necessary options for the command line parameters and will
805          * fail at startup. Currently, every version 10 or newer should be working.
806          *
807          * @param serverVersion A partial or full version. Valid values are e.g. "12" or "11.3".
808          */
809         @NonNull
810         public Builder setServerVersion(@NonNull String serverVersion) {
811             this.serverVersion = checkNotNull(serverVersion, "serverVersion is null");
812 
813             return this;
814         }
815 
816         /**
817          * Set a {@link ProcessBuilder.Redirect} instance to receive stderr output from the spawned processes.
818          *
819          * @param errRedirector a {@link ProcessBuilder.Redirect} instance. Must not be null.
820          * @return The builder itself.
821          */
822         @NonNull
823         public Builder setErrorRedirector(@NonNull ProcessBuilder.Redirect errRedirector) {
824             this.errRedirector = checkNotNull(errRedirector, "errRedirector is null");
825             return this;
826         }
827 
828         /**
829          * Set a {@link ProcessBuilder.Redirect} instance to receive stdout output from the spawned processes.
830          *
831          * @param outRedirector a {@link ProcessBuilder.Redirect} instance. Must not be null.
832          * @return The builder itself.
833          */
834         @NonNull
835         public Builder setOutputRedirector(@NonNull ProcessBuilder.Redirect outRedirector) {
836             this.outRedirector = checkNotNull(outRedirector, "outRedirector is null");
837             return this;
838         }
839 
840         /**
841          * Sets the {@link NativeBinaryManager} that provides the location of the postgres installation. Explicitly setting a binary manager overrides the
842          * installation base directory location set with {@link #setInstallationBaseDirectory(File)} as this is only used by the default binary manager. Calling
843          * {@link #setInstallationBaseDirectory(File)} after this method undoes setting the binary manager.
844          *
845          * @param nativeBinaryManager A {@link NativeBinaryManager} implementation. Must not be null.
846          * @return The builder itself.
847          */
848         @NonNull
849         public Builder setNativeBinaryManager(@NonNull NativeBinaryManager nativeBinaryManager) {
850             this.nativeBinaryManager = checkNotNull(nativeBinaryManager, "nativeBinaryManager is null");
851             return this;
852         }
853 
854         /**
855          * Use a locally installed PostgreSQL server for tests. The tests will still spin up a new instance and locate the data in the data directory but it
856          * will use the locally installed binaries for starting and stopping. Calling this method sets a binary manager, so it overrides {@link
857          * #setNativeBinaryManager(NativeBinaryManager)}. Calling this method makes the builder ignore the {@link #setInstallationBaseDirectory(File)} setting.
858          *
859          * @param directory A local directory that contains a standard PostgreSQL installation. The directory must exist and read and executable.
860          * @return The builder itself.
861          */
862         @NonNull
863         public Builder useLocalPostgresInstallation(@NonNull File directory) {
864             checkNotNull(directory, "directory is null");
865             checkState(directory.exists() && directory.isDirectory(), "'%s' either does not exist or is not a directory!", directory);
866             return setNativeBinaryManager(() -> directory);
867         }
868 
869         /**
870          * Creates and boots a new {@link EmbeddedPostgres} instance.
871          *
872          * @return A {@link EmbeddedPostgres} instance representing a started PostgreSQL server.
873          * @throws IOException If the server could not be installed or started.
874          */
875         @NonNull
876         public EmbeddedPostgres build() throws IOException {
877             // Builder Id
878             final String instanceId = EmbeddedUtil.randomAlphaNumeric(16);
879 
880             int port = this.port != 0 ? this.port : EmbeddedUtil.allocatePort();
881 
882             // installation root if nothing has been set by the user.
883             final File parentDirectory = EmbeddedUtil.getWorkingDirectory();
884             EmbeddedUtil.mkdirs(parentDirectory);
885 
886             NativeBinaryManager nativeBinaryManager = this.nativeBinaryManager;
887             if (nativeBinaryManager == null) {
888                 final String serverVersion = System.getProperty("pg-embedded.postgres-version", this.serverVersion);
889                 nativeBinaryManager = new TarXzCompressedBinaryManager(new ZonkyIOPostgresLocator(serverVersion));
890             }
891 
892             // Use the parent directory if no installation directory set.
893             File installationBaseDirectory = Objects.requireNonNullElse(this.installationBaseDirectory, parentDirectory);
894             nativeBinaryManager.setInstallationBaseDirectory(installationBaseDirectory);
895 
896             // this is where the binary manager actually places the unpackaged postgres installation.
897             final File postgresInstallDirectory = nativeBinaryManager.getLocation();
898 
899             File dataDirectory = this.dataDirectory;
900             if (dataDirectory == null) {
901                 dataDirectory = new File(parentDirectory, DATA_DIRECTORY_PREFIX + instanceId);
902             }
903 
904             EmbeddedPostgresbeddedPostgres.html#EmbeddedPostgres">EmbeddedPostgres embeddedPostgres = new EmbeddedPostgres(instanceId, postgresInstallDirectory, dataDirectory,
905                     removeDataOnShutdown, serverConfiguration, localeConfiguration, connectionProperties,
906                     port, errRedirector, outRedirector,
907                     serverStartupWait);
908 
909             embeddedPostgres.cleanOldDataDirectories(parentDirectory);
910 
911             embeddedPostgres.boot();
912 
913             return embeddedPostgres;
914         }
915     }
916 }