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