diff --git a/.idea/gradle.xml b/.idea/gradle.xml index f05bb75..e639131 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -9,8 +9,8 @@ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7acf3c0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM eclipse-temurin:11-jre-jammy + +WORKDIR /examples + +COPY ./build/install/examples/ . + +# Expose the port your application listens on (if applicable) +#EXPOSE 8080 + +# Define the entrypoint to run your application +ENTRYPOINT ["sh", "-c", "exec /examples/bin/examples"] \ No newline at end of file diff --git a/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/Command.java b/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/Command.java deleted file mode 100644 index ab4bcaa..0000000 --- a/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/Command.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.github.jonloucks.examples.concurrency.basic; - -@FunctionalInterface -public interface Command { - - String execute(); - - default boolean foreground() { - return false; - } - -} diff --git a/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/Constants.java b/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/Constants.java deleted file mode 100644 index f276258..0000000 --- a/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/Constants.java +++ /dev/null @@ -1,58 +0,0 @@ -package io.github.jonloucks.examples.concurrency.basic; - -import io.github.jonloucks.concurrency.api.Waitable; -import io.github.jonloucks.contracts.api.Contract; - -import java.util.function.Consumer; -import java.util.function.Supplier; - -/** - * Contracts can be defined anywhere, placing them here to demonstrate package private access - */ -final class Constants { - - /** - * Constant string, but open for uses cases like localization. - */ - static final Contract PROGRAM_NAME = Contract.create("Program name contract"); - - /** - * The main program implementation, not everything has to be in the class with main() - */ - static final Contract PROGRAM = Contract.create("Program contract"); - - /** - * Example of a configuration setting that is a singleton with lazy evaluation of value - */ - static final Contract RUNNER_THREAD_COUNT = Contract.create("Number of worker threads contract"); - - /** - * Example of a shared executor with auto resource management - */ - static final Contract RUNNER = Contract.create("Dispatcher contract"); - - /** - * Set to true when program is quitting - */ - static final Contract> IS_QUITTING = Contract.create("Quitting contract"); - - /** - * The health the program - */ - static final Contract> HEALTH = Contract.create("Health contract"); - - /** - * Command Output - */ - static final Contract> OUTPUT = Contract.create("Command output contract"); - - /** - * Command Input - */ - static final Contract> INPUT = Contract.create("Command input contract"); - - /** - * Command Output - */ - static final Contract> ERROR = Contract.create("Command error contract"); -} diff --git a/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/Dispatcher.java b/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/Dispatcher.java deleted file mode 100644 index 0ab85f9..0000000 --- a/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/Dispatcher.java +++ /dev/null @@ -1,6 +0,0 @@ -package io.github.jonloucks.examples.concurrency.basic; - -public interface Dispatcher { - - void dispatch(Command command); -} diff --git a/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/HelpCommand.java b/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/HelpCommand.java deleted file mode 100644 index 1fc5c48..0000000 --- a/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/HelpCommand.java +++ /dev/null @@ -1,17 +0,0 @@ -package io.github.jonloucks.examples.concurrency.basic; - -final class HelpCommand implements Command { - @Override - public String execute() { - return "This is an example..."; - } - - @Override - public boolean foreground() { - return true; - } - - HelpCommand() { - - } -} diff --git a/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/Main.java b/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/Main.java deleted file mode 100644 index 38177c3..0000000 --- a/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/Main.java +++ /dev/null @@ -1,95 +0,0 @@ -package io.github.jonloucks.examples.concurrency.basic; - -import io.github.jonloucks.contracts.api.AutoClose; -import io.github.jonloucks.contracts.api.BindStrategy; -import io.github.jonloucks.contracts.api.Repository; - -import java.util.Scanner; - -import static io.github.jonloucks.concurrency.api.GlobalConcurrency.createWaitable; -import static io.github.jonloucks.contracts.api.BindStrategy.IF_NOT_BOUND; -import static io.github.jonloucks.contracts.api.GlobalContracts.*; -import static io.github.jonloucks.examples.concurrency.basic.Constants.*; -import static java.lang.Boolean.TRUE; - -@SuppressWarnings("unused") -public final class Main { - - /** - * Main entry point. - * Note. Entry points are where final decisions on dependency inversions are made. - * Not all decisions must be made in an entry point, but in this example it is exposed - * for visibility - * @param args the command line arguments - */ - public static void main(String[] args) { - final Repository repository = createMyRepository(); - try (AutoClose closeRepository = repository.open(); - AutoClose closeHealthNotify = openHealthNotify()) { - - final Program program = claimContract(PROGRAM); - - waitForQuit(); - - waitForIdle(); - } // all repository resources will be released when this try block exits - } - - private static void waitForQuit() { - claimContract(IS_QUITTING).getWhen(TRUE::equals); - } - - private static void waitForIdle() { - claimContract(HEALTH).getWhen("idle"::equals); - } - - private static AutoClose openHealthNotify() { - return claimContract(HEALTH).notifyIf("idle"::equals, health -> { - System.out.println( "All commands have completed."); - }); - } - - /** - * For complex projects with many modules and entry points there a many - * ways to extend and manage dependency inversions without - * have them all defined here. - * For example: - * each module could have its own Repository. - * MyModule.createRepository(contracts) - * each module could have a static method to install contracts to a given repository - * MyModule.install(repository) - * each module could use the ServiceLoader mechanism - * I am sure there are more like SpringBoot - */ - private static Repository createMyRepository() { - final Repository repository = claimContract(Repository.FACTORY).get(); - - final BindStrategy strategy = IF_NOT_BOUND; - - // Constant string, but could be changed to a localized value without changing uses - repository.keep(PROGRAM_NAME, () -> "Concurrency Example"); - - // lifeCycle will create a singleton and detect AutoOpen implementations - repository.keep(PROGRAM, lifeCycle(ProgramImpl::new)); - - repository.keep(OUTPUT, singleton(() -> text -> System.out.println(text)), strategy); - - repository.keep(ERROR, singleton(() -> text -> System.err.println(text)), strategy); - - repository.keep(INPUT, singleton(() -> new Scanner(System.in)::nextLine), strategy); - - // lazy evaluated singleton - repository.keep(RUNNER_THREAD_COUNT, singleton(() -> Runtime.getRuntime().availableProcessors() * 8)); - - // lifeCycle will create a singleton and detect AutoOpen implementations - repository.keep(RUNNER, lifeCycle(DispatcherImpl::new)); - - // set to true when program is quiting - repository.keep(IS_QUITTING, singleton(() -> createWaitable(false))); - - // program health - repository.keep(HEALTH, singleton(() -> createWaitable("ready"))); - - return repository; - } -} diff --git a/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/Program.java b/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/Program.java deleted file mode 100644 index a99dbec..0000000 --- a/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/Program.java +++ /dev/null @@ -1,4 +0,0 @@ -package io.github.jonloucks.examples.concurrency.basic; - -interface Program { -} diff --git a/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/ProgramImpl.java b/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/ProgramImpl.java deleted file mode 100644 index 37d97ca..0000000 --- a/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/ProgramImpl.java +++ /dev/null @@ -1,77 +0,0 @@ -package io.github.jonloucks.examples.concurrency.basic; - -import io.github.jonloucks.contracts.api.AutoClose; -import io.github.jonloucks.contracts.api.AutoOpen; - -import java.time.Duration; -import java.time.Instant; -import java.util.Optional; -import java.util.function.Consumer; -import java.util.function.Supplier; - -import static io.github.jonloucks.contracts.api.GlobalContracts.claimContract; -import static io.github.jonloucks.examples.concurrency.basic.Constants.*; -import static java.util.Optional.ofNullable; - -final class ProgramImpl implements Program, AutoOpen { - - @Override - public AutoClose open() { - serviceStart = Instant.now(); - dispatcher = claimContract(Constants.RUNNER); - output.accept("Welcome to " + claimContract(PROGRAM_NAME)); - new Thread(this::commandLoop).start(); - return this::privateClose; - } - - ProgramImpl() { - } - - void commandLoop() { - while (!isQuitting()) { - promptForCommand().ifPresent(dispatcher::dispatch); - } - } - - private Duration getUptime() { - return Duration.between(serviceStart, ofNullable(serviceEnd).orElseGet(Instant::now)); - } - - private boolean isQuitting() { - return claimContract(IS_QUITTING).get(); - } - - private Optional promptForCommand() { - try { - output.accept("Enter command: "); - return Optional.of(parseCommand(input.get())); // Read the entire line until a newline character - } catch (Exception thrown) { - claimContract(IS_QUITTING).accept(true); - return Optional.empty(); - } - } - - private Command parseCommand(String commandLine) { - switch (ofNullable(commandLine).orElse("").toLowerCase()) { - case "": - case "help": - case "?": - return new HelpCommand(); - case "quit": - return new QuitCommand(); - default: - return () -> "Unrecognized command: " + commandLine; - } - } - - private void privateClose() { - serviceEnd = Instant.now(); - output.accept("Service Uptime: " + getUptime()); - } - - private final Supplier input = claimContract(INPUT); - private final Consumer output = claimContract(OUTPUT); - private Instant serviceStart; - private Instant serviceEnd; - private Dispatcher dispatcher; -} diff --git a/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/QuitCommand.java b/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/QuitCommand.java deleted file mode 100644 index c663f65..0000000 --- a/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/QuitCommand.java +++ /dev/null @@ -1,21 +0,0 @@ -package io.github.jonloucks.examples.concurrency.basic; - -import static io.github.jonloucks.contracts.api.GlobalContracts.claimContract; -import static io.github.jonloucks.examples.concurrency.basic.Constants.IS_QUITTING; - -final class QuitCommand implements Command { - @Override - public String execute() { - claimContract(IS_QUITTING).accept(true); - return "Quit initiated"; - } - - @Override - public boolean foreground() { - return true; - } - - QuitCommand() { - - } -} diff --git a/basic-concurrency/src/main/java/module-info.java b/basic-concurrency/src/main/java/module-info.java deleted file mode 100644 index 97bd0ba..0000000 --- a/basic-concurrency/src/main/java/module-info.java +++ /dev/null @@ -1,6 +0,0 @@ -module io.github.jonloucks.examples.concurrency.basic { - requires transitive io.github.jonloucks.contracts; - requires transitive io.github.jonloucks.concurrency; - - exports io.github.jonloucks.examples.concurrency.basic; -} \ No newline at end of file diff --git a/basic-contracts/build.gradle b/basic-contracts/build.gradle deleted file mode 100644 index 9181bd8..0000000 --- a/basic-contracts/build.gradle +++ /dev/null @@ -1,20 +0,0 @@ -plugins { - id 'java' -} - -getTasks().withType(JavaCompile.class).configureEach { - getOptions().getRelease().set(9) -} - -repositories { - mavenCentral() -} - -dependencies { - implementation libs.contracts - testImplementation libs.contracts.test -} - -test { - useJUnitPlatform() -} \ No newline at end of file diff --git a/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/Command.java b/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/Command.java deleted file mode 100644 index 56e89c5..0000000 --- a/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/Command.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.github.jonloucks.examples.contracts.basic; - -@FunctionalInterface -public interface Command { - - String execute(); - - default boolean foreground() { - return false; - } - -} diff --git a/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/Constants.java b/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/Constants.java deleted file mode 100644 index 9295785..0000000 --- a/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/Constants.java +++ /dev/null @@ -1,60 +0,0 @@ -package io.github.jonloucks.examples.contracts.basic; - -import io.github.jonloucks.contracts.api.Contract; - -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; -import java.util.function.Supplier; - -/** - * Contracts can be defined anywhere, placing them here to demonstrate package private access - */ -final class Constants { - - /** - * Constant string, but open for uses cases like localization. - */ - static final Contract PROGRAM_NAME = Contract.create("Program name contract"); - - /** - * The main program implementation, not everything has to be in the class with main() - */ - static final Contract PROGRAM = Contract.create("Program contract"); - - /** - * Example of a configuration setting that is a singleton with lazy evaluation of value - */ - static final Contract RUNNER_THREAD_COUNT = Contract.create("Number of worker threads contract"); - - /** - * Example of a shared executor with auto resource management - */ - static final Contract DISPATCHER = Contract.create("Dispatcher contract"); - - /** - * Set to true when program is quitting - */ - static final Contract IS_QUITTING = Contract.create("Quitting contract"); - - /** - * The health the program - */ - static final Contract> HEALTH = Contract.create("Health contract"); - - /** - * Command Output - */ - static final Contract> OUTPUT = Contract.create("Command output contract"); - - /** - * Command Input - */ - static final Contract> INPUT = Contract.create("Command input contract"); - - /** - * Command Output - */ - static final Contract> ERROR = Contract.create("Command error contract"); - -} diff --git a/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/Dispatcher.java b/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/Dispatcher.java deleted file mode 100644 index 042f966..0000000 --- a/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/Dispatcher.java +++ /dev/null @@ -1,6 +0,0 @@ -package io.github.jonloucks.examples.contracts.basic; - -public interface Dispatcher { - - void dispatch(Command command); -} diff --git a/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/DispatcherImpl.java b/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/DispatcherImpl.java deleted file mode 100644 index 26627eb..0000000 --- a/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/DispatcherImpl.java +++ /dev/null @@ -1,76 +0,0 @@ -package io.github.jonloucks.examples.contracts.basic; - -import io.github.jonloucks.contracts.api.AutoClose; -import io.github.jonloucks.contracts.api.AutoOpen; - -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; - -import static io.github.jonloucks.contracts.api.GlobalContracts.claimContract; -import static io.github.jonloucks.examples.contracts.basic.Constants.*; -import static java.util.Optional.ofNullable; - -final class DispatcherImpl implements Dispatcher, AutoOpen { - - @Override - public void dispatch(Command command) { - incrementRunning(); - - final Runnable job = () -> { - try { - output.accept(command.execute()); - } catch (Throwable thrown) { - error.accept(thrown.getMessage()); - } finally { - decrementRunning(); - } - }; - - if (!command.foreground() && ofNullable(delegate).isPresent()) { - delegate.execute(job); - } else { - job.run(); - } - } - - @Override - public AutoClose open() { - delegate = Executors.newFixedThreadPool(claimContract(RUNNER_THREAD_COUNT)); - return this::privateClose; // only open caller can close - } - - private void privateClose() { - ofNullable(delegate).ifPresent(executor -> { - delegate = null; - executor.shutdown(); - try { - if (!executor.awaitTermination(1, TimeUnit.MINUTES)) { - executor.shutdownNow(); - } - } catch (InterruptedException ignored) { - Thread.currentThread().interrupt(); - } - }); - } - - private void incrementRunning() { - runningCommandCount.incrementAndGet(); - health.set("busy"); - } - - private void decrementRunning() { - if (runningCommandCount.decrementAndGet() == 0) { - health.set("idle"); - } - } - - private ExecutorService delegate; - private final AtomicInteger runningCommandCount = new AtomicInteger(); - private final Consumer error = claimContract(ERROR); - private final Consumer output = claimContract(OUTPUT); - private final AtomicReference health = claimContract(HEALTH); -} diff --git a/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/HelpCommand.java b/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/HelpCommand.java deleted file mode 100644 index 224ab08..0000000 --- a/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/HelpCommand.java +++ /dev/null @@ -1,17 +0,0 @@ -package io.github.jonloucks.examples.contracts.basic; - -final class HelpCommand implements Command { - @Override - public String execute() { - return "This is an example..."; - } - - @Override - public boolean foreground() { - return true; - } - - HelpCommand() { - - } -} diff --git a/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/Main.java b/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/Main.java deleted file mode 100644 index 3f20fbf..0000000 --- a/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/Main.java +++ /dev/null @@ -1,86 +0,0 @@ -package io.github.jonloucks.examples.contracts.basic; - -import io.github.jonloucks.contracts.api.AutoClose; -import io.github.jonloucks.contracts.api.BindStrategy; -import io.github.jonloucks.contracts.api.Repository; - -import java.util.Scanner; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.atomic.AtomicReference; - -import static io.github.jonloucks.contracts.api.BindStrategy.IF_NOT_BOUND; -import static io.github.jonloucks.contracts.api.GlobalContracts.*; -import static io.github.jonloucks.examples.contracts.basic.Constants.*; - -@SuppressWarnings("unused") -public final class Main { - - /** - * Main entry point. - * Note. Entry points are where final decisions on dependency inversions are made. - * Not all decisions must be made in an entry point, but in this example it is exposed - * for visibility - * @param args the command line arguments - */ - public static void main(String[] args) { - final Repository repository = createMyRepository(); - try (AutoClose closeRepository = repository.open()) { - - final Program program = claimContract(PROGRAM); - - waitForQuit(); - } // all repository resources will be released when this try block exits - } - - private static void waitForQuit() { - try { - claimContract(IS_QUITTING).await(); - } catch (InterruptedException ignored) { - throw new RuntimeException("Waiting for quit"); - } - } - - /** - * For complex projects with many modules and entry points there a many - * ways to extend and manage dependency inversions without - * have them all defined here. - * For example: - * each module could have its own Repository. - * MyModule.createRepository(contracts) - * each module could have a static method to install contracts to a given repository - * MyModule.install(repository) - * each module could use the ServiceLoader mechanism - * I am sure there are more like SpringBoot - */ - private static Repository createMyRepository() { - final Repository repository = claimContract(Repository.FACTORY).get(); - - final BindStrategy strategy = IF_NOT_BOUND; - - // Constant string, but could be changed to a localized value without changing uses - repository.keep(PROGRAM_NAME, () -> "Concurrency Example", strategy); - - // lifeCycle will create a singleton and detect AutoOpen implementations - repository.keep(PROGRAM, lifeCycle(ProgramImpl::new), strategy); - - repository.keep(OUTPUT, singleton(() -> text -> System.out.println(text)), strategy); - - repository.keep(ERROR, singleton(() -> text -> System.err.println(text)), strategy); - - repository.keep(INPUT, singleton(() -> new Scanner(System.in)::nextLine), strategy); - - // lazy evaluated singleton - repository.keep(RUNNER_THREAD_COUNT, singleton(() -> Runtime.getRuntime().availableProcessors() * 8), strategy); - - // lifeCycle will create a singleton and detect AutoOpen implementations - repository.keep(DISPATCHER, lifeCycle(DispatcherImpl::new), strategy); - - // set to true when program is quiting - repository.keep(IS_QUITTING, singleton(() -> new CountDownLatch(1)), strategy); - - // program health - repository.keep(HEALTH, singleton(() -> new AtomicReference<>("ready")), strategy); - - return repository; - } -} diff --git a/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/Program.java b/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/Program.java deleted file mode 100644 index 2be3712..0000000 --- a/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/Program.java +++ /dev/null @@ -1,5 +0,0 @@ -package io.github.jonloucks.examples.contracts.basic; - -interface Program { - -} diff --git a/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/ProgramImpl.java b/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/ProgramImpl.java deleted file mode 100644 index 85eeb9c..0000000 --- a/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/ProgramImpl.java +++ /dev/null @@ -1,83 +0,0 @@ -package io.github.jonloucks.examples.contracts.basic; - -import io.github.jonloucks.contracts.api.AutoClose; -import io.github.jonloucks.contracts.api.AutoOpen; - - -import java.time.Duration; -import java.time.Instant; -import java.util.Optional; -import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; -import java.util.function.Supplier; - -import static io.github.jonloucks.contracts.api.GlobalContracts.claimContract; -import static io.github.jonloucks.examples.contracts.basic.Constants.*; -import static java.util.Optional.ofNullable; - -final class ProgramImpl implements Program, AutoOpen { - - @Override - public AutoClose open() { - serviceStart = Instant.now(); - dispatcher = claimContract(DISPATCHER); - output.accept("Welcome to " + claimContract(PROGRAM_NAME)); - new Thread(this::commandLoop).start(); - return this::privateClose; - } - - ProgramImpl() { - } - - private void commandLoop() { - while (!isQuitting()) { - promptForCommand().ifPresent(dispatcher::dispatch); - } - } - - private Duration getUptime() { - return Duration.between(serviceStart, ofNullable(serviceEnd).orElseGet(Instant::now)); - } - - private boolean isQuitting() { - try { - return claimContract(IS_QUITTING).await(0, TimeUnit.MINUTES); - } catch (InterruptedException e) { - return true; - } - } - - private Optional promptForCommand() { - try { - output.accept("Enter command: "); - return Optional.of(parseCommand(input.get())); // Read the entire line until a newline character - } catch (Exception thrown) { - claimContract(IS_QUITTING).countDown(); - return Optional.empty(); - } - } - - private Command parseCommand(String commandLine) { - switch (ofNullable(commandLine).orElse("").toLowerCase()) { - case "": - case "help": - case "?": - return new HelpCommand(); - case "quit": - return new QuitCommand(); - default: - return () -> "Unrecognized command: " + commandLine; - } - } - - private void privateClose() { - serviceEnd = Instant.now(); - output.accept("Service Uptime: " + getUptime()); - } - - private final Supplier input = claimContract(INPUT); - private final Consumer output = claimContract(OUTPUT); - private Instant serviceStart; - private Instant serviceEnd; - private Dispatcher dispatcher; -} diff --git a/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/QuitCommand.java b/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/QuitCommand.java deleted file mode 100644 index 1361343..0000000 --- a/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/QuitCommand.java +++ /dev/null @@ -1,21 +0,0 @@ -package io.github.jonloucks.examples.contracts.basic; - -import static io.github.jonloucks.contracts.api.GlobalContracts.claimContract; -import static io.github.jonloucks.examples.contracts.basic.Constants.IS_QUITTING; - -final class QuitCommand implements Command { - @Override - public String execute() { - claimContract(IS_QUITTING).countDown(); - return "Quit initiated"; - } - - @Override - public boolean foreground() { - return true; - } - - QuitCommand() { - - } -} diff --git a/basic-contracts/src/main/java/module-info.java b/basic-contracts/src/main/java/module-info.java deleted file mode 100644 index d88a09a..0000000 --- a/basic-contracts/src/main/java/module-info.java +++ /dev/null @@ -1,5 +0,0 @@ -module io.github.jonloucks.examples.contracts.basic { - requires transitive io.github.jonloucks.contracts; - - exports io.github.jonloucks.examples.contracts.basic; -} \ No newline at end of file diff --git a/build.gradle b/build.gradle index e1175ce..644295f 100644 --- a/build.gradle +++ b/build.gradle @@ -1,15 +1,62 @@ +buildscript { + repositories { + mavenCentral() + gradlePluginPortal() + mavenLocal() + } +} + plugins { id 'java' + id 'application' +} + +defaultTasks 'build', 'check', 'installDist' + +getTasks().withType(JavaCompile.class).configureEach { + getOptions().getRelease().set(9) +} + +jar { + manifest { + attributes 'Main-Class': 'io.github.jonloucks.examples.Main' + } +} + +application { + applicationName = 'examples' + mainModule = 'io.github.jonloucks.examples' + mainClass = 'io.github.jonloucks.examples.Main' +} + +tasks.register('buildDocker', Exec) { + group = 'build' + dependsOn 'installDist' + commandLine 'docker', 'build', '-t', 'examples:latest', '.' +} + +tasks.register('buildAndRunDocker', Exec) { + group = 'build' + dependsOn 'dockerBuild' + commandLine 'docker', 'run', '-i', '-p', '8080:8080', 'examples:latest' } repositories { mavenCentral() + mavenLocal() } dependencies { - testImplementation platform('org.junit:junit-bom:5.10.0') - testImplementation 'org.junit.jupiter:junit-jupiter' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + implementation project(":common") + implementation project(":server") + + implementation libs.contracts + implementation libs.concurrency + implementation libs.metalog + + testImplementation libs.contracts.test + testImplementation libs.concurrency.test + testImplementation libs.metalog.test } test { diff --git a/basic-concurrency/build.gradle b/common/build.gradle similarity index 64% rename from basic-concurrency/build.gradle rename to common/build.gradle index e35abb8..d4d57b7 100644 --- a/basic-concurrency/build.gradle +++ b/common/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'java' + id 'java-library' } getTasks().withType(JavaCompile.class).configureEach { @@ -8,14 +8,21 @@ getTasks().withType(JavaCompile.class).configureEach { repositories { mavenCentral() + mavenLocal() } dependencies { + api libs.contracts.api + api libs.concurrency.api + api libs.metalog.api + implementation libs.contracts implementation libs.concurrency + implementation libs.metalog testImplementation libs.contracts.test testImplementation libs.concurrency.test + testImplementation libs.metalog.test } test { diff --git a/common/src/main/java/io/github/jonloucks/examples/common/Command.java b/common/src/main/java/io/github/jonloucks/examples/common/Command.java new file mode 100644 index 0000000..2cda515 --- /dev/null +++ b/common/src/main/java/io/github/jonloucks/examples/common/Command.java @@ -0,0 +1,9 @@ +package io.github.jonloucks.examples.common; + +import java.util.List; + +public interface Command { + String execute(List arguments); + + String getName(); +} diff --git a/common/src/main/java/io/github/jonloucks/examples/common/Common.java b/common/src/main/java/io/github/jonloucks/examples/common/Common.java new file mode 100644 index 0000000..d61c7d5 --- /dev/null +++ b/common/src/main/java/io/github/jonloucks/examples/common/Common.java @@ -0,0 +1,67 @@ +package io.github.jonloucks.examples.common; + +import io.github.jonloucks.contracts.api.BindStrategy; +import io.github.jonloucks.contracts.api.Repository; +import io.github.jonloucks.metalog.api.Console; +import io.github.jonloucks.metalog.api.GlobalMetalog; + +import static io.github.jonloucks.concurrency.api.GlobalConcurrency.createWaitable; +import static io.github.jonloucks.contracts.api.BindStrategy.IF_ALLOWED; +import static io.github.jonloucks.contracts.api.GlobalContracts.*; +import static io.github.jonloucks.examples.common.Constants.*; +import static java.lang.Boolean.TRUE; + +public final class Common { + + /** + * For complex projects with many modules and entry points there a many + * ways to extend and manage dependency inversions without + * have them all defined here. + * For example: + * each module could have its own Repository. + * MyModule.createRepository(contracts) + * each module could have a static method to install contracts to a given repository + * MyModule.install(repository) + * each module could use the ServiceLoader mechanism + * I am sure there are more like SpringBoot + */ + public static void install(Repository repository) { + + //noinspection ResultOfMethodCallIgnored + GlobalMetalog.getInstance(); + + final BindStrategy strategy = IF_ALLOWED; + + // Constant string, but could be changed to a localized value without changing uses + repository.keep(PROGRAM_NAME, () -> "Unnamed", strategy); + + // lifeCycle will create a singleton and detect AutoOpen implementations + repository.keep(PROGRAM, lifeCycle(ProgramImpl::new), strategy); + + repository.keep(OUTPUT, singleton(() -> claimContract(Console.CONTRACT)), strategy); + + // lazy evaluated singleton + repository.keep(RUNNER_THREAD_COUNT, singleton(() -> Runtime.getRuntime().availableProcessors() * 8), strategy); + + // lifeCycle will create a singleton and detect AutoOpen implementations + repository.keep(DISPATCHER, lifeCycle(DispatcherImpl::new), strategy); + + // set to true when program is quiting + repository.keep(IS_QUITTING, singleton(() -> createWaitable(false)), strategy); + + // program health + repository.keep(HEALTH, singleton(() -> createWaitable("idle")), strategy); + } + + public static void setQuitting() { + claimContract(IS_QUITTING).accept(true); + } + + public static void waitForQuitting() { + claimContract(IS_QUITTING).getWhen(TRUE::equals); + } + + public static void waitForIdle() { + claimContract(HEALTH).getWhen("idle"::equals); + } +} diff --git a/common/src/main/java/io/github/jonloucks/examples/common/Constants.java b/common/src/main/java/io/github/jonloucks/examples/common/Constants.java new file mode 100644 index 0000000..2cfa672 --- /dev/null +++ b/common/src/main/java/io/github/jonloucks/examples/common/Constants.java @@ -0,0 +1,54 @@ +package io.github.jonloucks.examples.common; + +import io.github.jonloucks.concurrency.api.Waitable; +import io.github.jonloucks.contracts.api.Contract; +import io.github.jonloucks.metalog.api.Publisher; + +import java.util.List; + +/** + * Contracts can be defined anywhere, placing them here to demonstrate package private access + */ +public final class Constants { + + /** + * Constant string, but open for uses cases like localization. + */ + public static final Contract PROGRAM_NAME = Contract.create("Program name contract"); + + /** + * The main program implementation, not everything has to be in the class with main() + */ + public static final Contract PROGRAM = Contract.create("Program contract"); + + + /** + * Main arguments + */ + public static final Contract> PROGRAM_ARGUMENTS = Contract.create("Program arguments"); + + /** + * Example of a configuration setting that is a singleton with lazy evaluation of value + */ + public static final Contract RUNNER_THREAD_COUNT = Contract.create("Number of worker threads contract"); + + /** + * Example of a shared executor with auto resource management + */ + public static final Contract DISPATCHER = Contract.create("Dispatcher contract"); + + /** + * Set to true when program is quitting + */ + public static final Contract> IS_QUITTING = Contract.create("Quitting contract"); + + /** + * The health the program + */ + public static final Contract> HEALTH = Contract.create("Health contract"); + + /** + * Command Output + */ + public static final Contract OUTPUT = Contract.create("Command output contract"); +} diff --git a/common/src/main/java/io/github/jonloucks/examples/common/Dispatcher.java b/common/src/main/java/io/github/jonloucks/examples/common/Dispatcher.java new file mode 100644 index 0000000..29f3c32 --- /dev/null +++ b/common/src/main/java/io/github/jonloucks/examples/common/Dispatcher.java @@ -0,0 +1,10 @@ +package io.github.jonloucks.examples.common; + +import io.github.jonloucks.concurrency.api.OnCompletion; + +import java.util.function.Supplier; + +public interface Dispatcher { + + void dispatch(Supplier supplier, OnCompletion onCompletion); +} diff --git a/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/DispatcherImpl.java b/common/src/main/java/io/github/jonloucks/examples/common/DispatcherImpl.java similarity index 56% rename from basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/DispatcherImpl.java rename to common/src/main/java/io/github/jonloucks/examples/common/DispatcherImpl.java index 38787ec..a071d43 100644 --- a/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/DispatcherImpl.java +++ b/common/src/main/java/io/github/jonloucks/examples/common/DispatcherImpl.java @@ -1,32 +1,31 @@ -package io.github.jonloucks.examples.concurrency.basic; +package io.github.jonloucks.examples.common; import io.github.jonloucks.concurrency.api.OnCompletion; import io.github.jonloucks.concurrency.api.Waitable; import io.github.jonloucks.contracts.api.AutoClose; import io.github.jonloucks.contracts.api.AutoOpen; -import java.util.concurrent.*; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Consumer; +import java.util.function.Supplier; -import static io.github.jonloucks.concurrency.api.GlobalConcurrency.*; +import static io.github.jonloucks.concurrency.api.GlobalConcurrency.completeLater; +import static io.github.jonloucks.concurrency.api.GlobalConcurrency.completeNow; +import static io.github.jonloucks.contracts.api.Checks.nullCheck; import static io.github.jonloucks.contracts.api.GlobalContracts.claimContract; -import static io.github.jonloucks.examples.concurrency.basic.Constants.*; +import static io.github.jonloucks.examples.common.Constants.*; import static java.util.Optional.ofNullable; final class DispatcherImpl implements Dispatcher, AutoOpen { @Override - public void dispatch(Command command) { - incrementRunning(); - final OnCompletion onCompletion = c -> { - c.getValue().ifPresent(output); - c.getThrown().ifPresent(e ->error.accept(e.getMessage())); - decrementRunning(); - }; - if (!command.foreground() && ofNullable(delegate).isPresent()) { - completeLater(onCompletion, on -> delegate.execute(() -> completeNow(on, command::execute))); + public void dispatch(Supplier valueSupplier, OnCompletion onCompletion) { + final OnCompletion trackingOnCompletion = trackingOnCompletion(onCompletion); + if (ofNullable(delegate).isPresent()) { + completeLater(trackingOnCompletion, on -> delegate.execute(() -> completeNow(on, valueSupplier))); } else { - completeNow(onCompletion, command::execute); + completeNow(trackingOnCompletion, valueSupplier); } } @@ -50,6 +49,19 @@ private void privateClose() { }); } + // this is not required if keeping track of how many in-flight activities there are + private OnCompletion trackingOnCompletion(OnCompletion onCompletion) { + final OnCompletion validOnCompletion = nullCheck(onCompletion, "OnCompletion must be present."); + incrementRunning(); + return completion -> { + try { + validOnCompletion.onCompletion(completion); + } finally { + decrementRunning(); + } + }; + } + private void incrementRunning() { runningCommandCount.incrementAndGet(); health.accept("busy"); @@ -63,7 +75,5 @@ private void decrementRunning() { private ExecutorService delegate; private final AtomicInteger runningCommandCount = new AtomicInteger(); - private final Consumer output = claimContract(OUTPUT); - private final Consumer error = claimContract(ERROR); private final Waitable health = claimContract(HEALTH); } diff --git a/common/src/main/java/io/github/jonloucks/examples/common/HelpCommand.java b/common/src/main/java/io/github/jonloucks/examples/common/HelpCommand.java new file mode 100644 index 0000000..5567634 --- /dev/null +++ b/common/src/main/java/io/github/jonloucks/examples/common/HelpCommand.java @@ -0,0 +1,33 @@ +package io.github.jonloucks.examples.common; + +import java.util.List; + +import static io.github.jonloucks.contracts.api.GlobalContracts.claimContract; +import static io.github.jonloucks.examples.common.Common.setQuitting; +import static io.github.jonloucks.examples.common.Constants.PROGRAM; +import static io.github.jonloucks.examples.common.Constants.PROGRAM_NAME; + +final class HelpCommand implements Command { + @Override + public String execute(List arguments) { + final StringBuilder builder = new StringBuilder(); + final Program program = claimContract(PROGRAM); + final String programName = claimContract(PROGRAM_NAME); + builder.append(programName).append("\n"); + builder.append("Available commands:\n"); + for (Command command : program.getCommands()) { + builder.append(" ").append(command.getName()).append("\n"); + } + setQuitting(); + return builder.toString(); + } + + @Override + public String getName() { + return "help"; + } + + HelpCommand() { + + } +} diff --git a/common/src/main/java/io/github/jonloucks/examples/common/Program.java b/common/src/main/java/io/github/jonloucks/examples/common/Program.java new file mode 100644 index 0000000..a71d6c8 --- /dev/null +++ b/common/src/main/java/io/github/jonloucks/examples/common/Program.java @@ -0,0 +1,21 @@ +package io.github.jonloucks.examples.common; + +import io.github.jonloucks.concurrency.api.OnCompletion; +import io.github.jonloucks.contracts.api.AutoClose; + +import java.util.List; + +public interface Program { + AutoClose addCommand(Command command); + + default void keepCommand(Command command) { + //noinspection resource + addCommand(command); + } + + List getCommands(); + + void runCommandLine(OnCompletion onCompletion); + + void runCommand(String name, List options, OnCompletion onCompletion); +} diff --git a/common/src/main/java/io/github/jonloucks/examples/common/ProgramImpl.java b/common/src/main/java/io/github/jonloucks/examples/common/ProgramImpl.java new file mode 100644 index 0000000..f0929ad --- /dev/null +++ b/common/src/main/java/io/github/jonloucks/examples/common/ProgramImpl.java @@ -0,0 +1,105 @@ +package io.github.jonloucks.examples.common; + +import io.github.jonloucks.concurrency.api.OnCompletion; +import io.github.jonloucks.contracts.api.AutoClose; +import io.github.jonloucks.contracts.api.AutoOpen; +import io.github.jonloucks.metalog.api.Console; + +import java.util.*; + +import static io.github.jonloucks.concurrency.api.GlobalConcurrency.completeNow; +import static io.github.jonloucks.contracts.api.Checks.nullCheck; +import static io.github.jonloucks.contracts.api.GlobalContracts.claimContract; +import static io.github.jonloucks.examples.common.Constants.*; +import static java.util.Collections.emptyList; +import static java.util.Comparator.comparing; +import static java.util.Optional.ofNullable; +import static java.util.stream.Collectors.toList; + +final class ProgramImpl implements Program, AutoOpen { + + @Override + public AutoClose open() { + dispatcher = claimContract(Constants.DISPATCHER); + console.output(() -> "Welcome to " + claimContract(PROGRAM_NAME) ); + return this::privateClose; + } + + ProgramImpl() { + keepCommand(new HelpCommand()); + } + + @Override + public void runCommandLine(OnCompletion onCompletion) { + final List arguments = claimContract(PROGRAM_ARGUMENTS); + if (arguments.isEmpty()) { + runCommand("help", emptyList(), onCompletion); + } else { + runCommand(arguments.get(0), arguments.subList(1, arguments.size()), onCompletion); + } + } + + @Override + public void runCommand(String name, List options, OnCompletion onCompletion) { + final OnCompletion capture = captureResults(onCompletion); + final Optional optionalCommand = findCommand(name); + if (optionalCommand.isPresent()) { + dispatcher.dispatch(() -> optionalCommand.get().execute(options), capture); + } else { + completeNow(capture, () -> { + throw new IllegalArgumentException("Unknown command: " + name); + }); + } + } + + @Override + public AutoClose addCommand(Command command) { + final Command validCommand = nullCheck(command, "Command must be present."); + final String key = toCommandKey(validCommand.getName()); + commands.put(key, validCommand); + return () -> commands.remove(key, validCommand); + } + + @Override + public List getCommands() { + return commands.values().stream().sorted(comparing(Command::getName)).collect(toList()); + } + + private Optional findCommand(String commandName) { + return ofNullable(commands.get(toCommandKey(commandName))); + } + + private void privateClose() { + console.output(() -> "Good bye to " + claimContract(PROGRAM_NAME) ); + } + + private OnCompletion captureResults(OnCompletion onCompletion) { + final OnCompletion validonOnCompletion = nullCheck(onCompletion, "OnCompletion must be present."); + return completion -> { + try { + if (completion.getThrown().isPresent()) { + final Throwable thrown = completion.getThrown().get(); + console.publish(() -> completion.getValue().orElse("Error: " + thrown.getMessage()), + b -> b.thrown(thrown).channel("Console.error")); + } else if (completion.getValue().isPresent()) { + console.publish(completion.getValue()::get); + } + } finally { + validonOnCompletion.onCompletion(completion); + } + }; + } + + private static String toCommandKey(String commandName) { + final String validName = nullCheck(commandName, "Command name must be present."); + final String key = validName.trim().toLowerCase(); + if (key.isEmpty()) { + throw new IllegalArgumentException("Command name must not be empty."); + } + return key; + } + + private final Map commands = new HashMap<>(); + private final Console console = claimContract(Console.CONTRACT); + private Dispatcher dispatcher; +} diff --git a/common/src/main/java/module-info.java b/common/src/main/java/module-info.java new file mode 100644 index 0000000..ea2d1f2 --- /dev/null +++ b/common/src/main/java/module-info.java @@ -0,0 +1,8 @@ +module io.github.jonloucks.examples.common { + requires transitive io.github.jonloucks.contracts; + requires transitive io.github.jonloucks.concurrency; + requires transitive io.github.jonloucks.metalog; + requires io.github.jonloucks.concurrency.api; + + exports io.github.jonloucks.examples.common; +} \ No newline at end of file diff --git a/docker/build-and-deploy/Dockerfile b/docker/build-and-deploy/Dockerfile new file mode 100644 index 0000000..e33595f --- /dev/null +++ b/docker/build-and-deploy/Dockerfile @@ -0,0 +1,19 @@ +FROM eclipse-temurin:17-jdk-jammy AS build + +WORKDIR /examples + +COPY . . + +RUN ./gradlew build check installDist --no-daemon + +FROM eclipse-temurin:11-jre-jammy + +WORKDIR /examples + +COPY --from=build /examples/build/install/examples/ . + +# Expose the port your application listens on (if applicable) +#EXPOSE 8080 + +# Define the entrypoint to run your application +ENTRYPOINT ["sh", "-c", "exec ./bin/examples"] \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 24193e1..7df5a26 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] -contracts-version = "[2.5.0,3.0.0)" +contracts-version = "[2.5.1,3.0.0)" concurrency-version = "[1.3.0,2.0.0)" -metalog-version = "[1.0.0,2.0.0)" +metalog-version = "[1.2.2,2.0.0)" [libraries] contracts-api = { module = "io.github.jonloucks.contracts:contracts-api", version.ref = "contracts-version" } diff --git a/server/build.gradle b/server/build.gradle new file mode 100644 index 0000000..d4d57b7 --- /dev/null +++ b/server/build.gradle @@ -0,0 +1,30 @@ +plugins { + id 'java-library' +} + +getTasks().withType(JavaCompile.class).configureEach { + getOptions().getRelease().set(9) +} + +repositories { + mavenCentral() + mavenLocal() +} + +dependencies { + api libs.contracts.api + api libs.concurrency.api + api libs.metalog.api + + implementation libs.contracts + implementation libs.concurrency + implementation libs.metalog + + testImplementation libs.contracts.test + testImplementation libs.concurrency.test + testImplementation libs.metalog.test +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/server/src/main/java/io/github/jonloucks/example/server/ConfigBuilderImpl.java b/server/src/main/java/io/github/jonloucks/example/server/ConfigBuilderImpl.java new file mode 100644 index 0000000..06deb1d --- /dev/null +++ b/server/src/main/java/io/github/jonloucks/example/server/ConfigBuilderImpl.java @@ -0,0 +1,25 @@ +package io.github.jonloucks.example.server; + +import io.github.jonloucks.contracts.api.Contracts; + +import static io.github.jonloucks.contracts.api.Checks.contractsCheck; + +final class ConfigBuilderImpl implements Server.Config.Builder { + + @Override + public Contracts contracts() { + return contracts; + } + + @Override + public Builder contracts(Contracts contracts) { + this.contracts = contractsCheck(contracts); + return this; + } + + ConfigBuilderImpl() { + + } + + private Contracts contracts; +} diff --git a/server/src/main/java/io/github/jonloucks/example/server/Server.java b/server/src/main/java/io/github/jonloucks/example/server/Server.java new file mode 100644 index 0000000..8211b4c --- /dev/null +++ b/server/src/main/java/io/github/jonloucks/example/server/Server.java @@ -0,0 +1,64 @@ +package io.github.jonloucks.example.server; + +import io.github.jonloucks.concurrency.api.Idempotent; +import io.github.jonloucks.concurrency.api.WaitableNotify; +import io.github.jonloucks.contracts.api.AutoOpen; +import io.github.jonloucks.contracts.api.Contract; +import io.github.jonloucks.contracts.api.Contracts; +import io.github.jonloucks.contracts.api.GlobalContracts; + +import java.util.function.Supplier; + +public interface Server extends AutoOpen { + Contract CONTRACT = Contract.create(Server.class); + + WaitableNotify lifeCycleNotify(); + + interface Config { + /** + * The default configuration used when creating a new Server instance + */ + Config DEFAULT = new Config() {}; + + /** + * @return the contracts, some use case have their own Contracts instance. + */ + default Contracts contracts() { + return GlobalContracts.getInstance(); + } + + /** + * @return if true, reflection might be used to locate the ServerFactory + */ + default boolean useReflection() { + return true; + } + + /** + * @return the class name to use if reflection is used to find the ServerFactory + */ + default String reflectionClassName() { + return "io.github.jonloucks.example.server.ServerFactoryImpl"; + } + + /** + * @return if true, the ServiceLoader might be used to locate the ServerFactory + */ + default boolean useServiceLoader() { + return true; + } + + /** + * @return the class name to load from the ServiceLoader to find the ServerFactory + */ + default Class serviceLoaderClass() { + return ServerFactory.class; + } + + interface Builder extends Config { + Contract> FACTORY = Contract.create("Server Config Builder Factory"); + + Builder contracts(Contracts contracts); + } + } +} diff --git a/server/src/main/java/io/github/jonloucks/example/server/ServerException.java b/server/src/main/java/io/github/jonloucks/example/server/ServerException.java new file mode 100644 index 0000000..53da4b9 --- /dev/null +++ b/server/src/main/java/io/github/jonloucks/example/server/ServerException.java @@ -0,0 +1,31 @@ +package io.github.jonloucks.example.server; + +import static io.github.jonloucks.contracts.api.Checks.messageCheck; + +/** + * Runtime exception thrown for Server related problems. + * For example, when Server fails to initialize. + */ +public class ServerException extends RuntimeException { + + private static final long serialVersionUID = 0L; + + /** + * Passthrough for {@link RuntimeException#RuntimeException(String)} + * + * @param message the message for this exception + */ + public ServerException(String message) { + this(message, null); + } + + /** + * Passthrough for {@link RuntimeException#RuntimeException(String, Throwable)} + * + * @param message the message for this exception + * @param thrown the cause of this exception, null is allowed + */ + public ServerException(String message, Throwable thrown) { + super(messageCheck(message), thrown); + } +} diff --git a/server/src/main/java/io/github/jonloucks/example/server/ServerFactory.java b/server/src/main/java/io/github/jonloucks/example/server/ServerFactory.java new file mode 100644 index 0000000..526ae1d --- /dev/null +++ b/server/src/main/java/io/github/jonloucks/example/server/ServerFactory.java @@ -0,0 +1,48 @@ +package io.github.jonloucks.example.server; + +import io.github.jonloucks.contracts.api.AutoOpen; +import io.github.jonloucks.contracts.api.Contract; +import io.github.jonloucks.contracts.api.Repository; + +import java.util.function.Consumer; + +/** + * Responsible for creating new instances of Server + */ +public interface ServerFactory { + + /** + * Used to promise and claim the ServerFactory implementation + */ + Contract CONTRACT = Contract.create(ServerFactory.class); + + /** + * Create a new instance of Server + *

+ * Note: caller is responsible for calling {@link AutoOpen#open()} and calling + * the {@link io.github.jonloucks.contracts.api.AutoClose#close() when done} + *

+ * @param config the Server configuration for the new instance + * @return the new Server instance + */ + Server create(Server.Config config); + + /** + * Create a new instance of Server + * + * @param builderConsumer the config builder consumer callback + * @return the new Server instance + * @throws IllegalArgumentException if builderConsumer is null or when configuration is invalid + */ + Server create(Consumer builderConsumer); + + /** + * Install all the requirements and promises to the given Server Repository. + * Include Server#CONTRACT which will private a unique + * + * @param config the Server config + * @param repository the repository to add requirements and promises to + * @throws IllegalArgumentException if config is null, config is invalid, or repository is null + */ + void install(Server.Config config, Repository repository); +} diff --git a/server/src/main/java/io/github/jonloucks/example/server/ServerFactoryFinder.java b/server/src/main/java/io/github/jonloucks/example/server/ServerFactoryFinder.java new file mode 100644 index 0000000..b7483f5 --- /dev/null +++ b/server/src/main/java/io/github/jonloucks/example/server/ServerFactoryFinder.java @@ -0,0 +1,71 @@ +package io.github.jonloucks.example.server; + +import java.util.Optional; +import java.util.ServiceLoader; + +import static io.github.jonloucks.contracts.api.Checks.configCheck; +import static io.github.jonloucks.contracts.api.Checks.nullCheck; +import static java.util.Optional.ofNullable; + +/** + * Responsible for locating and creating the ServerFactory for a deployment. + */ +public final class ServerFactoryFinder { + public ServerFactoryFinder(Server.Config config) { + this.config = configCheck(config); + } + + public ServerFactory get() { + return find().orElseThrow(this::newNotFoundException); + } + + public Optional find() { + final Optional byReflection = createByReflection(); + if (byReflection.isPresent()) { + return byReflection; + } + return createByServiceLoader(); + } + + private Optional createByServiceLoader() { + if (config.useServiceLoader()) { + try { + for (ServerFactory factory : ServiceLoader.load(getServiceFactoryClass())) { + return Optional.of(factory); + } + } catch (Throwable ignored) { + return Optional.empty(); + } + } + return Optional.empty(); + } + + private Class getServiceFactoryClass() { + return nullCheck(config.serviceLoaderClass(), "Server Service Loader class must be present."); + } + + private Optional createByReflection() { + if (config.useReflection()) { + return getReflectionClassName().map(this::createNewInstance); + } + return Optional.empty(); + } + + private ServerFactory createNewInstance(String className) { + try { + return (ServerFactory)Class.forName(className).getConstructor().newInstance(); + } catch (Throwable thrown) { + return null; + } + } + + private Optional getReflectionClassName() { + return ofNullable(config.reflectionClassName()).filter(x -> !x.isEmpty()); + } + + private ServerException newNotFoundException() { + return new ServerException("Unable to find Server factory."); + } + + private final Server.Config config; +} diff --git a/server/src/main/java/io/github/jonloucks/example/server/ServerFactoryImpl.java b/server/src/main/java/io/github/jonloucks/example/server/ServerFactoryImpl.java new file mode 100644 index 0000000..f90440c --- /dev/null +++ b/server/src/main/java/io/github/jonloucks/example/server/ServerFactoryImpl.java @@ -0,0 +1,103 @@ +package io.github.jonloucks.example.server; + +import io.github.jonloucks.concurrency.api.Concurrency; +import io.github.jonloucks.concurrency.api.ConcurrencyFactory; +import io.github.jonloucks.concurrency.api.StateMachineFactory; +import io.github.jonloucks.concurrency.api.WaitableFactory; +import io.github.jonloucks.contracts.api.Contracts; +import io.github.jonloucks.contracts.api.Promisor; +import io.github.jonloucks.contracts.api.Repository; +import io.github.jonloucks.example.server.Server.Config; +import io.github.jonloucks.metalog.api.*; + +import java.util.Optional; +import java.util.function.Consumer; + +import static io.github.jonloucks.concurrency.api.GlobalConcurrency.findConcurrencyFactory; +import static io.github.jonloucks.contracts.api.BindStrategy.IF_NOT_BOUND; +import static io.github.jonloucks.contracts.api.Checks.*; +import static io.github.jonloucks.contracts.api.GlobalContracts.lifeCycle; +import static io.github.jonloucks.metalog.api.GlobalMetalog.findMetalogFactory; + +public class ServerFactoryImpl implements ServerFactory { + @Override + public Server create(Config config) { + final Server.Config validConfig = enhancedConfigCheck(config); + final Repository repository = validConfig.contracts().claim(Repository.FACTORY).get(); + + installConcurrency(validConfig, repository); + installMetalog(validConfig, repository); + installCore(validConfig, repository); + + final Server server = new ServerImpl(validConfig, repository, true); + repository.keep(Server.CONTRACT, () -> server); + return server; + } + + @Override + public Server create(Consumer builderConsumer) { + final Consumer validBuilderConsumer = builderConsumerCheck(builderConsumer); + final ConfigBuilderImpl configBuilder = new ConfigBuilderImpl(); + validBuilderConsumer.accept(configBuilder); + return create(configBuilder); + } + + @Override + public void install(Config config, Repository repository) { + final Server.Config validConfig = enhancedConfigCheck(config); + final Repository validRepository = nullCheck(repository, "Repository must be present."); + + installConcurrency(validConfig, validRepository); + installMetalog(validConfig, validRepository); + installCore(validConfig, validRepository); + + final Promisor serverPromisor = lifeCycle(() -> new ServerImpl(validConfig, validRepository, false)); + + validRepository.keep(Server.CONTRACT, serverPromisor, IF_NOT_BOUND); + } + + private void installConcurrency(Server.Config config, Repository repository) { + final Concurrency.Config concurrencyConfig = new Concurrency.Config() { + @Override + public Contracts contracts() { + return config.contracts(); + } + }; + //noinspection ResultOfMethodCallIgnored + contractsCheck(concurrencyConfig.contracts()); + final Optional optionalFactory = findConcurrencyFactory(concurrencyConfig); + + optionalFactory.ifPresent(f -> f.install(concurrencyConfig, repository)); + } + + private void installMetalog(Server.Config config, Repository repository) { + final Metalog.Config metalogConfig = new Metalog.Config() { + @Override + public Contracts contracts() { + return config.contracts(); + } + }; + //noinspection ResultOfMethodCallIgnored + contractsCheck(metalogConfig.contracts()); + final Optional optionalFactory = findMetalogFactory(metalogConfig); + + optionalFactory.ifPresent(f -> f.install(metalogConfig, repository)); + } + + private Server.Config enhancedConfigCheck(Server.Config config) { + final Server.Config candidateConfig = configCheck(config); + final Contracts contracts = contractsCheck(candidateConfig.contracts()); + + if (contracts.isBound(Server.CONTRACT)) { + throw new ServerException("Server is already bound."); + } + + return candidateConfig; + } + + private void installCore(Server.Config config, Repository repository) { + repository.require(Repository.FACTORY); + repository.require(WaitableFactory.CONTRACT); + repository.require(StateMachineFactory.CONTRACT); + } +} diff --git a/server/src/main/java/io/github/jonloucks/example/server/ServerImpl.java b/server/src/main/java/io/github/jonloucks/example/server/ServerImpl.java new file mode 100644 index 0000000..47b497a --- /dev/null +++ b/server/src/main/java/io/github/jonloucks/example/server/ServerImpl.java @@ -0,0 +1,48 @@ +package io.github.jonloucks.example.server; + +import io.github.jonloucks.concurrency.api.Idempotent; +import io.github.jonloucks.concurrency.api.StateMachine; +import io.github.jonloucks.concurrency.api.WaitableNotify; +import io.github.jonloucks.contracts.api.AutoClose; +import io.github.jonloucks.contracts.api.Repository; + +import static io.github.jonloucks.concurrency.api.Idempotent.withClose; +import static io.github.jonloucks.concurrency.api.Idempotent.withOpen; +import static io.github.jonloucks.contracts.api.Checks.configCheck; +import static io.github.jonloucks.contracts.api.Checks.nullCheck; + +final class ServerImpl implements Server { + + @Override + public AutoClose open() { + return withOpen(stateMachine, this::realOpen); + } + + @Override + public WaitableNotify lifeCycleNotify() { + return stateMachine; + } + + ServerImpl(Server.Config config, Repository repository, boolean openRepository) { + this.config = configCheck(config); + final Repository validRepository = nullCheck(repository, "Repository must be present."); + this.closeRepository = openRepository ? validRepository.open() : AutoClose.NONE; + this.stateMachine = Idempotent.createStateMachine(config.contracts()); + } + + private AutoClose realOpen() { + return this::exposedClose; + } + + private void exposedClose() { + withClose(stateMachine, this::realClose); + } + + private void realClose() { + closeRepository.close(); + } + + private final Server.Config config; + private final AutoClose closeRepository; + private final StateMachine stateMachine; +} diff --git a/server/src/main/java/module-info.java b/server/src/main/java/module-info.java new file mode 100644 index 0000000..4c2f498 --- /dev/null +++ b/server/src/main/java/module-info.java @@ -0,0 +1,13 @@ +module io.github.jonloucks.example.server { + requires transitive io.github.jonloucks.contracts; + requires transitive io.github.jonloucks.concurrency; + requires transitive io.github.jonloucks.metalog; + + uses io.github.jonloucks.contracts.api.ContractsFactory; + uses io.github.jonloucks.concurrency.api.ConcurrencyFactory; + uses io.github.jonloucks.metalog.api.MetalogFactory; + + provides io.github.jonloucks.example.server.ServerFactory with io.github.jonloucks.example.server.ServerFactoryImpl; + + exports io.github.jonloucks.example.server; +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index e5a05d5..ff37d8e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,4 @@ rootProject.name = 'examples' -include 'basic-contracts' -include 'basic-concurrency' \ No newline at end of file +include 'common' + +include 'server' \ No newline at end of file diff --git a/src/main/java/io/github/jonloucks/examples/Main.java b/src/main/java/io/github/jonloucks/examples/Main.java new file mode 100644 index 0000000..e705ac3 --- /dev/null +++ b/src/main/java/io/github/jonloucks/examples/Main.java @@ -0,0 +1,82 @@ +package io.github.jonloucks.examples; + +import io.github.jonloucks.contracts.api.AutoClose; +import io.github.jonloucks.contracts.api.Repository; +import io.github.jonloucks.example.server.Server; +import io.github.jonloucks.example.server.ServerFactory; +import io.github.jonloucks.example.server.ServerFactoryFinder; +import io.github.jonloucks.examples.common.Common; +import io.github.jonloucks.examples.common.Program; + +import java.util.Arrays; + +import static io.github.jonloucks.contracts.api.BindStrategy.ALWAYS; +import static io.github.jonloucks.contracts.api.GlobalContracts.claimContract; +import static io.github.jonloucks.examples.common.Common.waitForQuitting; +import static io.github.jonloucks.examples.common.Constants.*; + +public final class Main { + + /** + * Main entry point. + * Note. Entry points are where final decisions on dependency inversions are made. + * Not all decisions must be made in an entry point, but in this example it is exposed + * for visibility + * @param args the command line arguments + */ + public static void main(String[] args) { + final Repository repository = createMyRepository(args); + try (AutoClose closeRepository = repository.open()) { + final Program program = claimContract(PROGRAM); + installCommand(program); + program.runCommandLine( c -> {}); + waitForQuitting(); + System.exit(0); + } catch (Exception thrown) { + System.err.println(thrown.getMessage()); + System.exit(1); + } + } + + private static void installCommand(Program program) { + program.keepCommand(new ServerCommand()); + } + + /** + * For complex projects with many modules and entry points there a many + * ways to extend and manage dependency inversions without + * have them all defined here. + * For example: + * each module could have its own Repository. + * MyModule.createRepository(contracts) + * each module could have a static method to install contracts to a given repository + * MyModule.install(repository) + * each module could use the ServiceLoader mechanism + * I am sure there are more like SpringBoot + */ + private static Repository createMyRepository(String[] args) { + final Repository repository = claimContract(Repository.FACTORY).get(); + + Common.install(repository); + + installServer(repository); + + // Save the command line for later use + repository.keep(PROGRAM_ARGUMENTS, () -> Arrays.asList(args), ALWAYS); + + // Constant string, but could be changed to a localized value without changing uses + repository.keep(PROGRAM_NAME, () -> "Examples", ALWAYS); + + return repository; + } + + private static void installServer(Repository repository) { + final ServerFactoryFinder finder = new ServerFactoryFinder(Server.Config.DEFAULT); + final ServerFactory serverFactory = finder.get(); + serverFactory.install(Server.Config.DEFAULT, repository); + } + + private Main() { + + } +} diff --git a/src/main/java/io/github/jonloucks/examples/ServerCommand.java b/src/main/java/io/github/jonloucks/examples/ServerCommand.java new file mode 100644 index 0000000..aa1a6c0 --- /dev/null +++ b/src/main/java/io/github/jonloucks/examples/ServerCommand.java @@ -0,0 +1,25 @@ +package io.github.jonloucks.examples; + +import io.github.jonloucks.example.server.Server; +import io.github.jonloucks.examples.common.Command; +import io.github.jonloucks.examples.common.Common; + +import java.util.List; + +import static io.github.jonloucks.concurrency.api.Idempotent.CLOSED; +import static io.github.jonloucks.contracts.api.GlobalContracts.claimContract; + +final class ServerCommand implements Command { + @Override + public String execute(List arguments) { + final Server server = claimContract(Server.CONTRACT); + //noinspection resource, scope is full life of server + server.lifeCycleNotify().notifyIf(s -> s == CLOSED, s -> Common.setQuitting()); + return "Server started"; + } + + @Override + public String getName() { + return "server"; + } +} diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java new file mode 100644 index 0000000..1f5de32 --- /dev/null +++ b/src/main/java/module-info.java @@ -0,0 +1,14 @@ +module io.github.jonloucks.examples { + requires transitive io.github.jonloucks.contracts; + requires transitive io.github.jonloucks.concurrency; + requires transitive io.github.jonloucks.metalog; + requires transitive io.github.jonloucks.examples.common; + requires transitive io.github.jonloucks.example.server; + + uses io.github.jonloucks.contracts.api.ContractsFactory; + uses io.github.jonloucks.concurrency.api.ConcurrencyFactory; + uses io.github.jonloucks.metalog.api.MetalogFactory; + uses io.github.jonloucks.example.server.ServerFactory; + + exports io.github.jonloucks.examples; +} \ No newline at end of file