1
2
3
4
5
6
7
8
9
10
11
12
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 import static java.nio.file.StandardOpenOption.CREATE;
25 import static java.nio.file.StandardOpenOption.DELETE_ON_CLOSE;
26 import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
27 import static java.nio.file.StandardOpenOption.WRITE;
28
29 import de.softwareforge.testing.postgres.embedded.ProcessOutputLogger.StreamCapture;
30
31 import jakarta.annotation.Nonnull;
32 import jakarta.annotation.Nullable;
33 import java.io.File;
34 import java.io.IOException;
35 import java.io.InputStreamReader;
36 import java.lang.ProcessBuilder.Redirect;
37 import java.net.ConnectException;
38 import java.net.InetAddress;
39 import java.net.InetSocketAddress;
40 import java.net.Socket;
41 import java.nio.channels.FileChannel;
42 import java.nio.channels.FileLock;
43 import java.nio.channels.OverlappingFileLockException;
44 import java.nio.charset.StandardCharsets;
45 import java.nio.file.Files;
46 import java.nio.file.Path;
47 import java.sql.Connection;
48 import java.sql.ResultSet;
49 import java.sql.SQLException;
50 import java.sql.Statement;
51 import java.time.Duration;
52 import java.util.HashMap;
53 import java.util.List;
54 import java.util.Map;
55 import java.util.Map.Entry;
56 import java.util.Objects;
57 import java.util.concurrent.ExecutionException;
58 import java.util.concurrent.TimeUnit;
59 import java.util.concurrent.atomic.AtomicBoolean;
60 import javax.sql.DataSource;
61
62 import com.google.common.annotations.VisibleForTesting;
63 import com.google.common.base.Joiner;
64 import com.google.common.base.Stopwatch;
65 import com.google.common.collect.ImmutableList;
66 import com.google.common.collect.ImmutableMap;
67 import com.google.common.io.CharStreams;
68 import com.google.common.io.Closeables;
69 import org.postgresql.ds.PGSimpleDataSource;
70 import org.slf4j.Logger;
71 import org.slf4j.LoggerFactory;
72
73
74
75
76 public final class EmbeddedPostgres implements AutoCloseable {
77
78
79
80
81 public static final String DEFAULT_POSTGRES_VERSION = "15";
82
83 static final String[] LOCALHOST_SERVER_NAMES = {"localhost"};
84
85 private static final String PG_TEMPLATE_DB = "template1";
86
87 @VisibleForTesting
88 static final Duration DEFAULT_PG_STARTUP_WAIT = Duration.ofSeconds(10);
89
90
91 private static final long MINIMUM_AGE_IN_MS = Duration.ofMinutes(10).toMillis();
92
93
94 private static final String DATA_DIRECTORY_PREFIX = "data-";
95
96 private static final String PG_STOP_MODE = "fast";
97 private static final String PG_STOP_WAIT_SECONDS = "5";
98 static final String LOCK_FILE_NAME = "epg-lock";
99
100 private final Logger logger;
101
102 private final String instanceId;
103 private final File postgresInstallDirectory;
104 private final File dataDirectory;
105
106 private final Duration serverStartupWait;
107 private final int port;
108 private final AtomicBoolean started = new AtomicBoolean();
109 private final AtomicBoolean closed = new AtomicBoolean();
110
111 private final ImmutableMap<String, String> serverConfiguration;
112 private final ImmutableMap<String, String> localeConfiguration;
113 private final ImmutableMap<String, String> connectionProperties;
114
115 private final File lockFile;
116 private volatile FileChannel lockChannel;
117 private volatile FileLock lock;
118
119 private final boolean removeDataOnShutdown;
120
121 private final ProcessBuilder.Redirect errorRedirector;
122 private final ProcessBuilder.Redirect outputRedirector;
123 private final ProcessOutputLogger pgServerLogger;
124
125
126
127
128
129 @Nonnull
130 public static EmbeddedPostgres defaultInstance() throws IOException {
131 return builderWithDefaults().build();
132 }
133
134
135
136
137 @Nonnull
138 public static EmbeddedPostgres.Builder builderWithDefaults() {
139 return new Builder().withDefaults();
140 }
141
142
143
144
145
146
147
148
149
150
151 public static EmbeddedPostgres forVersionCheck() throws IOException {
152 return new Builder(false).build();
153 }
154
155
156
157
158 @Nonnull
159 public static EmbeddedPostgres.Builder builder() {
160 return new Builder();
161 }
162
163 private EmbeddedPostgres(
164 final String instanceId,
165 final File postgresInstallDirectory,
166 final File dataDirectory,
167 final boolean removeDataOnShutdown,
168 final Map<String, String> serverConfiguration,
169 final Map<String, String> localeConfiguration,
170 final Map<String, String> connectionProperties,
171 final int port,
172 final ProcessBuilder.Redirect errorRedirector,
173 final ProcessBuilder.Redirect outputRedirector,
174 final Duration serverStartupWait) {
175
176 this.instanceId = checkNotNull(instanceId, "instanceId is null");
177
178 this.logger = LoggerFactory.getLogger(toString());
179 this.pgServerLogger = new ProcessOutputLogger(logger);
180
181 this.postgresInstallDirectory = checkNotNull(postgresInstallDirectory, "postgresInstallDirectory is null");
182 this.dataDirectory = checkNotNull(dataDirectory, "dataDirectory is null");
183
184 this.removeDataOnShutdown = removeDataOnShutdown;
185
186 this.serverConfiguration = ImmutableMap.copyOf(checkNotNull(serverConfiguration, "serverConfiguration is null"));
187 this.localeConfiguration = ImmutableMap.copyOf(checkNotNull(localeConfiguration, "localeConfiguration is null"));
188 this.connectionProperties = ImmutableMap.copyOf(checkNotNull(connectionProperties, "connectionProperties is null"));
189
190 this.port = port;
191
192 this.errorRedirector = checkNotNull(errorRedirector, "errorRedirector is null");
193 this.outputRedirector = checkNotNull(outputRedirector, "outputRedirector is null");
194
195 this.serverStartupWait = checkNotNull(serverStartupWait, "serverStartupWait is null");
196 this.lockFile = new File(this.dataDirectory, LOCK_FILE_NAME);
197
198 logger.debug(format("data dir is %s, install dir is %s", this.dataDirectory, this.postgresInstallDirectory));
199 }
200
201
202
203
204
205
206
207
208
209 @Nonnull
210 public DataSource createTemplateDataSource() throws SQLException {
211 checkState(started.get(), "instance has not been started!");
212
213 return createDataSource(PG_DEFAULT_USER, PG_TEMPLATE_DB, getPort(), getConnectionProperties());
214 }
215
216
217
218
219
220
221 @Nonnull
222 public DataSource createDefaultDataSource() throws SQLException {
223 checkState(started.get(), "instance has not been started!");
224
225 return createDataSource(PG_DEFAULT_USER, PG_DEFAULT_DB, getPort(), getConnectionProperties());
226 }
227
228
229
230
231
232
233
234 @Nonnull
235 public DataSource createDataSource(@Nonnull String user, @Nonnull String databaseName) throws SQLException {
236 checkState(started.get(), "instance has not been started!");
237
238 return createDataSource(user, databaseName, getPort(), getConnectionProperties());
239 }
240
241 static DataSource createDataSource(String user, String databaseName, int port, Map<String, String> connectionProperties) throws SQLException {
242 checkNotNull(user, "user is null");
243 checkNotNull(databaseName, "databaseName is null");
244 checkNotNull(connectionProperties, "connectionProperties is null");
245
246 final PGSimpleDataSource ds = new PGSimpleDataSource();
247
248 ds.setServerNames(LOCALHOST_SERVER_NAMES);
249 ds.setPortNumbers(new int[]{port});
250 ds.setDatabaseName(databaseName);
251 ds.setUser(user);
252
253 for (final Entry<String, String> entry : connectionProperties.entrySet()) {
254 ds.setProperty(entry.getKey(), entry.getValue());
255 }
256
257 return ds;
258 }
259
260
261
262
263 public int getPort() {
264 checkState(started.get(), "instance has not been started!");
265
266 return port;
267 }
268
269
270
271
272 @Nonnull
273 ImmutableMap<String, String> getConnectionProperties() {
274 checkState(started.get(), "instance has not been started!");
275
276 return connectionProperties;
277 }
278
279
280
281
282
283 @Nonnull
284 public String instanceId() {
285 checkState(started.get(), "instance has not been started!");
286
287 return instanceId;
288 }
289
290
291
292
293
294
295
296
297 public String getPostgresVersion() throws IOException {
298
299 StringBuilder sb = new StringBuilder();
300 StreamCapture logCapture = pgServerLogger.captureStreamAsConsumer(sb::append);
301
302 List<String> commandAndArgs = ImmutableList.of(pgBin("pg_ctl"), "--version");
303 final Stopwatch watch = system(commandAndArgs, logCapture);
304
305 String version = "unknown";
306
307 try {
308 logCapture.getCompletion().get();
309 final String s = sb.toString();
310 checkState(s.startsWith("pg_ctl "), "Response %s does not match 'pg_ctl'", sb);
311 version = s.substring(s.lastIndexOf(' ')).trim();
312
313 } catch (ExecutionException e) {
314 throw new IOException(format("Process '%s' failed%n", Joiner.on(" ").join(commandAndArgs)), e);
315 } catch (InterruptedException e) {
316 Thread.currentThread().interrupt();
317 }
318
319 logger.debug(format("postgres version check completed in %s", formatDuration(watch.elapsed())));
320 return version;
321 }
322
323 @Override
324 public String toString() {
325 return this.getClass().getName() + "$" + this.instanceId;
326 }
327
328 @Override
329 public boolean equals(Object o) {
330 if (this == o) {
331 return true;
332 }
333 if (o == null || getClass() != o.getClass()) {
334 return false;
335 }
336 EmbeddedPostgres that = (EmbeddedPostgres) o;
337 return instanceId.equals(that.instanceId);
338 }
339
340 @Override
341 public int hashCode() {
342 return Objects.hash(instanceId);
343 }
344
345
346 DatabaseInfo createDefaultDatabaseInfo() {
347 return DatabaseInfo.builder().port(getPort()).connectionProperties(getConnectionProperties()).build();
348 }
349
350
351 private void boot() throws IOException {
352 EmbeddedUtil.ensureDirectory(this.dataDirectory);
353
354 if (this.removeDataOnShutdown || !new File(this.dataDirectory, "postgresql.conf").exists()) {
355 initDatabase();
356 }
357
358 lock();
359
360 startDatabase();
361 }
362
363
364 private synchronized void lock() throws IOException {
365 this.lockChannel = FileChannel.open(this.lockFile.toPath(), CREATE, WRITE, TRUNCATE_EXISTING);
366 this.lock = this.lockChannel.tryLock();
367 checkState(lock != null, "could not lock %s", lockFile);
368 }
369
370 private synchronized void unlock() throws IOException {
371 if (lock != null) {
372 lock.release();
373 }
374 Closeables.close(this.lockChannel, true);
375 Files.deleteIfExists(this.lockFile.toPath());
376 }
377
378 private void initDatabase() throws IOException {
379 ImmutableList.Builder<String> commandBuilder = ImmutableList.builder();
380 commandBuilder.add(pgBin("initdb"))
381 .addAll(createInitDbOptions())
382 .add("-A", "trust",
383 "-U", PG_DEFAULT_USER,
384 "-D", this.dataDirectory.getPath(),
385 "-E", "UTF-8");
386 final Stopwatch watch = system(commandBuilder.build(), pgServerLogger.captureStreamAsLog());
387 logger.debug(format("initdb completed in %s", formatDuration(watch.elapsed())));
388 }
389
390 private void startDatabase() throws IOException {
391 checkState(!started.getAndSet(true), "database already started!");
392
393 final ImmutableList.Builder<String> commandBuilder = ImmutableList.builder();
394 commandBuilder.add(pgBin("pg_ctl"),
395 "-D", this.dataDirectory.getPath(),
396 "-o", String.join(" ", createInitOptions()),
397 "start"
398 );
399
400 final Stopwatch watch = Stopwatch.createStarted();
401 final Process postmaster = spawn("pg", commandBuilder.build(), pgServerLogger.captureStreamAsLog());
402
403 logger.info(format("started as pid %d on port %d", postmaster.pid(), port));
404 logger.debug(format("Waiting up to %s for server startup to finish", formatDuration(serverStartupWait)));
405
406 Runtime.getRuntime().addShutdownHook(newCloserThread());
407
408 checkState(waitForServerStartup(), "Could not start PostgreSQL server, interrupted?");
409 logger.debug(format("startup complete in %s", formatDuration(watch.elapsed())));
410 }
411
412 private void stopDatabase(File dataDirectory) throws IOException {
413 if (started.get()) {
414 final ImmutableList.Builder<String> commandBuilder = ImmutableList.builder();
415 commandBuilder.add(pgBin("pg_ctl"),
416 "-D", dataDirectory.getPath(),
417 "stop",
418 "-m", PG_STOP_MODE,
419 "-t", PG_STOP_WAIT_SECONDS, "-w");
420
421 final Stopwatch watch = system(commandBuilder.build(), pgServerLogger.captureStreamAsLog());
422 logger.debug(format("shutdown complete in %s", formatDuration(watch.elapsed())));
423 }
424 pgServerLogger.close();
425 }
426
427 private List<String> createInitOptions() {
428 final ImmutableList.Builder<String> initOptions = ImmutableList.builder();
429 initOptions.add(
430 "-p", Integer.toString(port),
431 "-F");
432
433 serverConfiguration.forEach((key, value) -> {
434 initOptions.add("-c");
435 if (!value.isEmpty()) {
436 initOptions.add(key + "=" + value);
437 } else {
438 initOptions.add(key + "=true");
439 }
440 });
441
442 return initOptions.build();
443 }
444
445 @VisibleForTesting
446 List<String> createInitDbOptions() {
447 final ImmutableList.Builder<String> localeOptions = ImmutableList.builder();
448
449 localeConfiguration.forEach((key, value) -> {
450 if (!value.isEmpty()) {
451 localeOptions.add("--" + key + "=" + value);
452 } else {
453 localeOptions.add("--" + key);
454 }
455 });
456 return localeOptions.build();
457 }
458
459 private boolean waitForServerStartup() throws IOException {
460 Throwable lastCause = null;
461 final long start = System.nanoTime();
462 final long maxWaitNs = TimeUnit.NANOSECONDS.convert(serverStartupWait.toMillis(), TimeUnit.MILLISECONDS);
463 while (System.nanoTime() - start < maxWaitNs) {
464 try {
465 if (verifyReady()) {
466 return true;
467 }
468 } catch (final SQLException e) {
469 lastCause = e;
470 logger.trace("while waiting for server startup:", e);
471 }
472
473 try {
474 Thread.sleep(100);
475 } catch (final InterruptedException e) {
476 Thread.currentThread().interrupt();
477 return false;
478 }
479 }
480 throw new IOException("Gave up waiting for server to start after " + serverStartupWait.toMillis() + "ms", lastCause);
481 }
482
483 @SuppressWarnings("PMD.CheckResultSet")
484 private boolean verifyReady() throws IOException, SQLException {
485
486 final InetAddress localhost = InetAddress.getLoopbackAddress();
487 try (Socket sock = new Socket()) {
488 sock.setSoTimeout((int) Duration.ofMillis(500).toMillis());
489 sock.connect(new InetSocketAddress(localhost, port), (int) Duration.ofMillis(500).toMillis());
490 } catch (ConnectException e) {
491 return false;
492 }
493
494
495 try (Connection c = createDefaultDataSource().getConnection();
496 Statement s = c.createStatement();
497 ResultSet rs = s.executeQuery("SELECT 1")) {
498 checkState(rs.next(), "expecting single row");
499 checkState(rs.getInt(1) == 1, "expecting 1 as result");
500 checkState(!rs.next(), "expecting single row");
501 return true;
502 }
503 }
504
505 private Thread newCloserThread() {
506 final Thread closeThread = new Thread(() -> {
507 try {
508 this.close();
509 } catch (IOException e) {
510 logger.trace("while closing instance:", e);
511 }
512 });
513
514 closeThread.setName("pg-closer");
515 return closeThread;
516 }
517
518
519
520
521 @Override
522 public void close() throws IOException {
523 if (closed.getAndSet(true)) {
524 return;
525 }
526
527 try {
528 stopDatabase(this.dataDirectory);
529 } catch (final Exception e) {
530 logger.error("could not stop pg:", e);
531 }
532
533 unlock();
534
535 if (removeDataOnShutdown) {
536 try {
537 EmbeddedUtil.rmdirs(dataDirectory);
538 } catch (Exception e) {
539 logger.error(format("Could not clean up directory %s:", dataDirectory.getAbsolutePath()), e);
540 }
541 } else {
542 logger.debug(format("preserved data directory %s", dataDirectory.getAbsolutePath()));
543 }
544 }
545
546 @VisibleForTesting
547 File getDataDirectory() {
548 return dataDirectory;
549 }
550
551 @VisibleForTesting
552 Map<String, String> getLocaleConfiguration() {
553 return localeConfiguration;
554 }
555
556
557 private void cleanOldDataDirectories(File parentDirectory) {
558 final File[] children = parentDirectory.listFiles();
559 if (children == null) {
560 return;
561 }
562 for (final File dir : children) {
563 if (!dir.isDirectory()) {
564 continue;
565 }
566
567
568 if (!dir.getName().startsWith(DATA_DIRECTORY_PREFIX)) {
569 continue;
570 }
571
572
573 final File lockFile = new File(dir, LOCK_FILE_NAME);
574 if (!lockFile.exists()) {
575 continue;
576 }
577
578
579
580
581 if (System.currentTimeMillis() - lockFile.lastModified() < MINIMUM_AGE_IN_MS) {
582 continue;
583 }
584
585 try (FileChannel fileChannel = FileChannel.open(lockFile.toPath(), CREATE, WRITE, TRUNCATE_EXISTING, DELETE_ON_CLOSE);
586 FileLock lock = fileChannel.tryLock()) {
587 if (lock != null) {
588 logger.debug(format("found stale data directory %s", dir));
589 if (new File(dir, "postmaster.pid").exists()) {
590 try {
591 stopDatabase(dir);
592 logger.debug("shutting down orphaned database!");
593 } catch (Exception e) {
594 logger.warn(format("failed to orphaned database in %s:", dir), e);
595 }
596 }
597 EmbeddedUtil.rmdirs(dir);
598 }
599 } catch (final OverlappingFileLockException e) {
600
601 logger.trace("while cleaning old data directories:", e);
602 } catch (final IOException e) {
603 logger.warn("while cleaning old data directories:", e);
604 }
605 }
606 }
607
608 private String pgBin(String binaryName) {
609 final String extension = EmbeddedUtil.IS_OS_WINDOWS ? ".exe" : "";
610 return new File(this.postgresInstallDirectory, "bin/" + binaryName + extension).getPath();
611 }
612
613 private Process spawn(@Nullable String processName, List<String> commandAndArgs,
614 StreamCapture logCapture)
615 throws IOException {
616 final ProcessBuilder builder = new ProcessBuilder(commandAndArgs);
617 builder.redirectErrorStream(true);
618 builder.redirectError(errorRedirector);
619 builder.redirectOutput(outputRedirector);
620 final Process process = builder.start();
621
622 if (outputRedirector == Redirect.PIPE) {
623 processName = processName != null ? processName : process.info().command().map(EmbeddedUtil::getFileBaseName).orElse("<unknown>");
624 String name = format("%s (%d)", processName, process.pid());
625 logCapture.accept(name, process.getInputStream());
626 }
627 return process;
628 }
629
630 private Stopwatch system(List<String> commandAndArgs, StreamCapture logCapture) throws IOException {
631 checkArgument(!commandAndArgs.isEmpty(), "No commandAndArgs given!");
632 String prefix = EmbeddedUtil.getFileBaseName(commandAndArgs.get(0));
633
634 Stopwatch watch = Stopwatch.createStarted();
635 try {
636 Process process = spawn(prefix, commandAndArgs, logCapture);
637 if (process.waitFor() != 0) {
638 if (errorRedirector == Redirect.PIPE) {
639 try (InputStreamReader errorReader = new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8)) {
640 throw new IOException(format("Process '%s' failed%n%s", Joiner.on(" ").join(commandAndArgs), CharStreams.toString(errorReader)));
641 }
642 } else {
643 throw new IOException(format("Process '%s' failed",
644 Joiner.on(" ").join(commandAndArgs)));
645 }
646 }
647 } catch (InterruptedException e) {
648 Thread.currentThread().interrupt();
649 }
650
651 return watch;
652 }
653
654
655
656
657 public static class Builder {
658
659 private File installationBaseDirectory = null;
660 private File dataDirectory = null;
661
662 private final Map<String, String> serverConfiguration = new HashMap<>();
663 private final Map<String, String> localeConfiguration = new HashMap<>();
664 private boolean removeDataOnShutdown = true;
665 private int port = 0;
666 private String serverVersion = DEFAULT_POSTGRES_VERSION;
667 private final Map<String, String> connectionProperties = new HashMap<>();
668 private NativeBinaryManager nativeBinaryManager = null;
669 private Duration serverStartupWait = DEFAULT_PG_STARTUP_WAIT;
670
671 private ProcessBuilder.Redirect errorRedirector = ProcessBuilder.Redirect.PIPE;
672 private ProcessBuilder.Redirect outputRedirector = ProcessBuilder.Redirect.PIPE;
673
674 private final boolean bootInstance;
675
676 private Builder(boolean bootInstance) {
677 this.bootInstance = bootInstance;
678 }
679
680 Builder() {
681 this(true);
682 }
683
684
685
686
687
688
689
690
691
692
693
694 @Nonnull
695 public Builder withDefaults() {
696 serverConfiguration.put("timezone", "UTC");
697 serverConfiguration.put("synchronous_commit", "off");
698 serverConfiguration.put("max_connections", "300");
699 return this;
700 }
701
702
703
704
705
706
707
708 @Nonnull
709 public Builder setServerStartupWait(@Nonnull Duration serverStartupWait) {
710 checkNotNull(serverStartupWait, "serverStartupWait is null");
711 checkArgument(!serverStartupWait.isNegative(), "Negative durations are not permitted.");
712
713 this.serverStartupWait = serverStartupWait;
714 return this;
715 }
716
717
718
719
720
721
722
723
724 @Nonnull
725 public Builder setRemoveDataOnShutdown(boolean removeDataOnShutdown) {
726 this.removeDataOnShutdown = removeDataOnShutdown;
727 return this;
728 }
729
730
731
732
733
734
735
736
737 @Nonnull
738 public Builder setDataDirectory(@Nonnull Path dataDirectory) {
739 checkNotNull(dataDirectory, "dataDirectory is null");
740 return setDataDirectory(dataDirectory.toFile());
741 }
742
743
744
745
746
747
748
749
750 @Nonnull
751 public Builder setDataDirectory(@Nonnull String dataDirectory) {
752 checkNotNull(dataDirectory, "dataDirectory is null");
753 return setDataDirectory(new File(dataDirectory));
754 }
755
756
757
758
759
760
761
762
763 @Nonnull
764 public Builder setDataDirectory(@Nonnull File dataDirectory) {
765 this.dataDirectory = checkNotNull(dataDirectory, "dataDirectory is null");
766 return this;
767 }
768
769
770
771
772
773
774
775
776
777
778
779
780 @Nonnull
781 public Builder addServerConfiguration(@Nonnull String key, @Nonnull String value) {
782 checkNotNull(key, "key is null");
783 checkNotNull(value, "value is null");
784 this.serverConfiguration.put(key, value);
785 return this;
786 }
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801 @Nonnull
802 public Builder addInitDbConfiguration(@Nonnull String key, @Nonnull String value) {
803 checkNotNull(key, "key is null");
804 checkNotNull(value, "value is null");
805 this.localeConfiguration.put(key, value);
806 return this;
807 }
808
809
810
811
812
813
814
815
816
817
818 @Nonnull
819 public Builder addConnectionProperty(@Nonnull String key, @Nonnull String value) {
820 checkNotNull(key, "key is null");
821 checkNotNull(value, "value is null");
822 this.connectionProperties.put(key, value);
823 return this;
824 }
825
826
827
828
829
830
831
832
833
834
835
836 @Nonnull
837 public Builder setInstallationBaseDirectory(@Nonnull File installationBaseDirectory) {
838 checkNotNull(installationBaseDirectory, "installationBaseDirectory is null");
839 this.installationBaseDirectory = installationBaseDirectory;
840 this.nativeBinaryManager = null;
841 return this;
842 }
843
844
845
846
847
848
849
850
851 @Nonnull
852 public Builder setPort(int port) {
853 checkState(port > 1_023 && port < 65_535, "Port %s is not within 1024..65535", port);
854 this.port = port;
855 return this;
856 }
857
858
859
860
861
862
863
864
865
866
867
868
869 @Nonnull
870 public Builder setServerVersion(@Nonnull String serverVersion) {
871 this.serverVersion = checkNotNull(serverVersion, "serverVersion is null");
872
873 return this;
874 }
875
876
877
878
879
880
881
882 @Nonnull
883 public Builder setErrorRedirector(@Nonnull ProcessBuilder.Redirect errorRedirector) {
884 this.errorRedirector = checkNotNull(errorRedirector, "errorRedirector is null");
885 return this;
886 }
887
888
889
890
891
892
893
894 @Nonnull
895 public Builder setOutputRedirector(@Nonnull ProcessBuilder.Redirect outputRedirector) {
896 this.outputRedirector = checkNotNull(outputRedirector, "outputRedirector is null");
897 return this;
898 }
899
900
901
902
903
904
905
906
907
908
909 @Nonnull
910 public Builder setNativeBinaryManager(@Nonnull NativeBinaryManager nativeBinaryManager) {
911 this.nativeBinaryManager = checkNotNull(nativeBinaryManager, "nativeBinaryManager is null");
912 return this;
913 }
914
915
916
917
918
919
920
921
922
923
924
925 @Nonnull
926 public Builder useLocalPostgresInstallation(@Nonnull File directory) {
927 checkNotNull(directory, "directory is null");
928 checkState(directory.exists() && directory.isDirectory(), "'%s' either does not exist or is not a directory!", directory);
929 return setNativeBinaryManager(() -> directory);
930 }
931
932
933
934
935
936
937
938 @Nonnull
939 public EmbeddedPostgres build() throws IOException {
940
941 final String instanceId = EmbeddedUtil.randomAlphaNumeric(16);
942
943 int port = this.port != 0 ? this.port : EmbeddedUtil.allocatePort();
944
945
946 final File parentDirectory = EmbeddedUtil.getWorkingDirectory();
947
948 NativeBinaryManager nativeBinaryManager = this.nativeBinaryManager;
949 if (nativeBinaryManager == null) {
950 final String serverVersion = System.getProperty("pg-embedded.postgres-version", this.serverVersion);
951 nativeBinaryManager = new TarXzCompressedBinaryManager(new ZonkyIOPostgresLocator(serverVersion));
952 }
953
954
955 File installationBaseDirectory = Objects.requireNonNullElse(this.installationBaseDirectory, parentDirectory);
956 nativeBinaryManager.setInstallationBaseDirectory(installationBaseDirectory);
957
958
959 final File postgresInstallDirectory = nativeBinaryManager.getLocation();
960
961 File dataDirectory = this.dataDirectory;
962 if (dataDirectory == null) {
963 dataDirectory = new File(parentDirectory, DATA_DIRECTORY_PREFIX + instanceId);
964 }
965
966 EmbeddedPostgres embeddedPostgres = new EmbeddedPostgres(instanceId, postgresInstallDirectory, dataDirectory,
967 removeDataOnShutdown, serverConfiguration, localeConfiguration, connectionProperties,
968 port, errorRedirector, outputRedirector,
969 serverStartupWait);
970
971 embeddedPostgres.cleanOldDataDirectories(parentDirectory);
972
973
974
975 if (bootInstance) {
976 embeddedPostgres.boot();
977 }
978
979 return embeddedPostgres;
980 }
981 }
982 }