@AutoValue
public abstract class ChosenNotifiers {

    private static final String SEPARATOR = ",";

    public abstract String primary();

    public abstract Set<String> secondary();

    ChosenNotifiers() {
        // prevent external subclasses
    }

    public static ChosenNotifiers from(String notifier) {
        checkNotNull(notifier);

        if (!notifier.contains(SEPARATOR)) {
            return new AutoValue_ChosenNotifiers(notifier, Collections.emptySet());
        }

        String[] notifiers = notifier.split(SEPARATOR);
        Set<String> secondary = new LinkedHashSet<>(asList(notifiers).subList(1, notifiers.length));
        return new AutoValue_ChosenNotifiers(notifiers[0], secondary);
    }
} public class OperatingSystem {

    private final String currentOs;

    public OperatingSystem() {
        currentOs = System.getProperty("os.name").toLowerCase();
    }

    public OperatingSystem(String currentOs) {
        this.currentOs = currentOs.toLowerCase();
    }

    public boolean isMac() {
        return currentOs.contains("mac");
    }

    public boolean isWindows() {
        return currentOs.contains("win");
    }

    @Override
    public String toString() {
        return "OperatingSystem{" +
                "currentOs='" + currentOs + '\'' +
                '}';
    }
} public class DoNothingNotifier implements DiscoverableNotifier {

    private static final DoNothingNotifier INSTANCE = new DoNothingNotifier();

    public static DoNothingNotifier doNothing() {
        return INSTANCE;
    }

    private DoNothingNotifier() {
    }

    @Override
    public Notifier init() {
        return this;
    }

    @Override
    public void send(Notification notification) {
        // do nothing
    }

    @Override
    public void close() {
        // do nothing
    }

    @Override
    public boolean isPersistent() {
        return false;
    }

    @Override
    public boolean tryInit() {
        return true;
    }
} public class AdditionalNotifier implements DiscoverableNotifier {

    private static final Logger LOGGER = getLogger(AdditionalNotifier.class);

    private final Set<DiscoverableNotifier> secondary;
    private final DiscoverableNotifier primary;
    private final boolean notifyWithSecondaryNotifiers;

    public AdditionalNotifier(DiscoverableNotifier primary, Set<DiscoverableNotifier> secondary) {
        checkArgument(!secondary.isEmpty());
        this.primary = checkNotNull(primary);
        this.secondary = secondary;
        this.notifyWithSecondaryNotifiers = Boolean.valueOf(System.getProperty("notifyAll"));
    }

    @Override
    public Notifier init() {
        List<Exception> errors = new ArrayList<>();

        safeInit(primary, errors);

        if (notifyWithSecondaryNotifiers) {
            for (DiscoverableNotifier notifier : secondary) {
                safeInit(notifier, errors);
            }
        }

        failIfNotEmpty(errors);

        return this;
    }

    @Override
    public boolean tryInit() {
        return false;
    }

    @Override
    public void send(Notification notification) {
        List<Exception> errors = new ArrayList<>();

        safeSend(primary, notification, errors);

        if (notifyWithSecondaryNotifiers) {
            for (DiscoverableNotifier notifier : secondary) {
                safeSend(notifier, notification, errors);
            }
        }

        failIfNotEmpty(errors);
    }

    @Override
    public void close() {
        List<Exception> errors = new ArrayList<>();

        safeClose(primary, errors);

        if (notifyWithSecondaryNotifiers) {
            for (DiscoverableNotifier notifier : secondary) {
                safeClose(notifier, errors);
            }
        }

        failIfNotEmpty(errors);
    }

    @Override
    public boolean isPersistent() {
        if (notifyWithSecondaryNotifiers) {
            boolean result = primary.isPersistent();
            for (DiscoverableNotifier notifier : secondary) {
                result |= notifier.isPersistent();
            }
            return result;
        }
        return primary.isPersistent();
    }

    public static void safeClose(DiscoverableNotifier notifier, List<Exception> errors) {
        try {
            notifier.close();
        } catch (RuntimeException e) {
            LOGGER.debug("Error closing {}.", notifier, e);
            errors.add(e);
        }
    }

    private static void failIfNotEmpty(List<Exception> errors) {
        if (!errors.isEmpty()) {
            throw new MultipleSendNotificationException(errors);
        }
    }

    private static void safeSend(DiscoverableNotifier notifier, Notification notification, List<Exception> errors) {
        try {
            notifier.send(notification);
        } catch (RuntimeException e) {
            LOGGER.debug("Error sending notification {} with {}.", notification, notifier, e);
            errors.add(e);
        }
    }

    public static void safeInit(DiscoverableNotifier notifier, List<Exception> errors) {
        try {
            notifier.init();
        } catch (RuntimeException e) {
            LOGGER.debug("Error initializing {}.", notifier, e);
            errors.add(e);
        }
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(secondary, primary, notifyWithSecondaryNotifiers);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        final AdditionalNotifier other = (AdditionalNotifier) obj;
        return Objects.equal(this.secondary, other.secondary)
            && Objects.equal(this.primary, other.primary)
            && Objects.equal(this.notifyWithSecondaryNotifiers, other.notifyWithSecondaryNotifiers);
    }

    @Override
    public String toString() {
        return MoreObjects.toStringHelper(this)
            .add("secondary", secondary)
            .add("primary", primary)
            .add("notifyWithSecondaryNotifiers", notifyWithSecondaryNotifiers)
            .toString();
    }
} @AutoValue
public abstract class AnyBarConfiguration {

    private static final AnyBarConfiguration DEFAULT = new AutoValue_AnyBarConfiguration("localhost", 1738);

    public abstract String host();

    public abstract int port();

    AnyBarConfiguration() {
        // prevent external subclasses
    }

    public static AnyBarConfiguration byDefault() {
        return DEFAULT;
    }

    public static AnyBarConfiguration create(Properties properties) {
        if (properties == null) {
            return byDefault();
        }

        return new AutoValue_AnyBarConfiguration(
            properties.getProperty("notifier.anybar.host", DEFAULT.host()),
            Integer.valueOf(
                properties.getProperty("notifier.anybar.port", String.valueOf(DEFAULT.port()))
            )
        );
    }
} public class AnyBarNotifier implements DiscoverableNotifier {

    private static final Logger LOGGER = getLogger(AnyBarNotifier.class);

    private final Application application;
    private final AnyBarConfiguration configuration;
    private final IconFileWriter iconWriter;
    private final DatagramSocket socket;

    private boolean isInitialized;

    @VisibleForTesting
    AnyBarNotifier(Application application, AnyBarConfiguration configuration, DatagramSocket socket, IconFileWriter iconWriter) {
        LOGGER.debug("Configuring AnyBar for application {}: {}.", application, configuration);
        this.socket = socket;
        this.iconWriter = iconWriter;
        this.application = application;
        this.configuration = configuration;
    }

    public static AnyBarNotifier create(Application application, AnyBarConfiguration configuration) {
        try {
            return new AnyBarNotifier(application, configuration, new DatagramSocket(), new AnyBarIconWriter());
        } catch (SocketException e) {
            throw new AnyBarException("Error while initiating connection with AnyBar", e);
        }
    }

    @Override
    public Notifier init() {
        if (isInitialized) {
            return this;
        }

        try {
            if (application.timeout() > 0) {
                socket.setSoTimeout((int) application.timeout());
            }
        } catch (java.io.IOException e) {
            throw new AnyBarException("Error while setting socket timeout", e);
        }

        changeIcon(application.icon());

        isInitialized = true;
        return this;
    }

    @Override
    public void send(Notification notification) {
        changeIcon(notification.icon());
    }

    @Override
    public void close() {
        socket.close();
    }

    @Override
    public boolean isPersistent() {
        return true;
    }

    @Override
    public boolean tryInit() {
        return false;
    }

    private void changeIcon(Icon icon) {
        iconWriter.write(icon);

        byte[] data = icon.id().getBytes();
        try {
            socket.send(new DatagramPacket(data, data.length, InetAddress.getByName(configuration.host()), configuration.port()));
        } catch (IOException e) {
            throw new AnyBarException("Error while changing AnyBar icon", e);
        }
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(application, configuration);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        final AnyBarNotifier other = (AnyBarNotifier) obj;
        return Objects.equal(this.application, other.application)
            && Objects.equal(this.configuration, other.configuration);
    }

    @Override
    public String toString() {
        return MoreObjects.toStringHelper(this)
            .add("configuration", configuration)
            .add("application", application)
            .toString();
    }
} public class BurntToastNotifier implements DiscoverableNotifier {

    private static final Logger LOGGER = getLogger(BurntToastNotifier.class);

    private final Application application;
    private final BurntToastNotifierConfiguration configuration;

    public BurntToastNotifier(Application application, BurntToastNotifierConfiguration configuration) {
        this.application = application;
        this.configuration = configuration;
    }

    @Override
    public void send(Notification notification) {
        StringBuilder command = new StringBuilder()
            .append("New-BurntToastNotification -Text '")
            .append(notification.title())
            .append("', '")
            .append(notification.message())
            .append("' -AppLogo ")
            .append(notification.icon().asPath());

        if (configuration.sound() == null) {
            command.append(" -Silent");
        } else {
            command.append(" -Sound ")
                .append(configuration.sound());
        }

        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Will execute PowerShell: {}", command.toString());
        }

        PowerShellResponse response = PowerShell.executeSingleCommand(command.toString());
        LOGGER.debug("Response: {}", response.getCommandOutput());
        if (response.isError()) {
            throw new BurntToastException(response.getCommandOutput());
        }
    }

    @Override
    public void close() {
        // do nothing
    }

    @Override
    public boolean isPersistent() {
        return false;
    }

    @Override
    public Notifier init() {
        return this;
    }

    @Override
    public boolean tryInit() {
        PowerShellResponse response = PowerShell.executeSingleCommand("Get-Module -ListAvailable -Name BurntToast | Format-Table Name");
        return !response.isError() && response.getCommandOutput().toLowerCase().contains("burnttoast");
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(application, configuration);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        final BurntToastNotifier other = (BurntToastNotifier) obj;
        return Objects.equal(this.application, other.application)
            && Objects.equal(this.configuration, other.configuration);
    }

    @Override
    public String toString() {
        return MoreObjects.toStringHelper(this)
            .add("application", application)
            .add("configuration", configuration)
            .toString();
    }
} @AutoValue
public abstract class BurntToastNotifierConfiguration {

    private static final BurntToastNotifierConfiguration DEFAULT = new AutoValue_BurntToastNotifierConfiguration(null);

    BurntToastNotifierConfiguration() {
        // prevent external subclasses
    }

    public static BurntToastNotifierConfiguration byDefault() {
        return DEFAULT;
    }

    public static BurntToastNotifierConfiguration create(Properties properties) {
        if (properties == null) {
            return byDefault();
        }

        return new AutoValue_BurntToastNotifierConfiguration(properties.getProperty("notifier.burnttoast.sound"));
    }

    @Nullable
    public abstract String sound();
} public class RuntimeExecutor implements Executor {

    private static final Logger LOGGER = getLogger(RuntimeExecutor.class);
    private static final int DEFAULT_WAIT_PROCESS_TIMEOUT = 500;

    private final ExecutorService executor = Executors.newSingleThreadExecutor();
    private final long timeout;

    public RuntimeExecutor(long timeout) {
        this.timeout = timeout == -1 ? DEFAULT_WAIT_PROCESS_TIMEOUT : timeout;
    }

    @Override
    public void exec(final String[] command) {

        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Will execute command line: {} (waiting at most: {} milliseconds)", logCommand(command), timeout);
        }

        Future<Integer> task = executor.submit(() -> {
            try {
                Process execution = new ProcessBuilder(command)
                    .redirectErrorStream(true)
                    .start();

                int returnCode = execution.waitFor();
                if (returnCode != 0) {
                    String message = "Command <[" + logCommand(command) + "]> returns code: " + returnCode + ".\n" + asString(execution.getInputStream());
                    LOGGER.debug(message);
                    throw new SendNotificationException(message);
                }

                LOGGER.debug("Command <[{}]> ends successfully.", logCommand(command));

                return returnCode;

            } catch (IOException e) {
                throw Throwables.propagate(e);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw Throwables.propagate(e);
            }
        });

        try {
            task.get(timeout, MILLISECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw Throwables.propagate(e);
        } catch (ExecutionException e) {
            LOGGER.debug("An error occurs while running command: <[{}]>", logCommand(command), e);
            throw Throwables.propagate(e.getCause());
        } catch (TimeoutException e) {
            LOGGER.debug("Command <[{}]> takes too long to execute. Do not wait for the result...", logCommand(command), e);
        }
    }

    @Override
    public boolean tryExec(String[] command) {
        try {
            exec(command);
            return true;
        } catch (RuntimeException e) {
            return false;
        }
    }

    private static String logCommand(String[] command) {
        return Joiner.on(" ").join(command);
    }

    private static String asString(InputStream inputStream) throws IOException {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
            StringBuilder output = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                output.append(line).append("\n");
            }
            return output.toString();
        }
    }

    @Override
    public void close() {
        executor.shutdownNow();
    }
} @AutoValue
public abstract class GrowlConfiguration {

    private static final GrowlConfiguration DEFAULT = new AutoValue_GrowlConfiguration(
            "localhost", null, 23053
    );

    public abstract String host();

    @Nullable
    public abstract String password();

    public abstract int port();

    GrowlConfiguration() {
        // prevent external subclasses
    }

    public static GrowlConfiguration byDefault() {
        return DEFAULT;
    }

    public static GrowlConfiguration create(Properties properties) {
        if (properties == null) {
            return byDefault();
        }

        return new AutoValue_GrowlConfiguration(
                properties.getProperty("notifier.growl.host", DEFAULT.host()),
                properties.getProperty("notifier.growl.password"),
                Integer.valueOf(properties.getProperty("notifier.growl.port", String.valueOf(DEFAULT.port())))
        );
    }
} public class GrowlNotifier implements DiscoverableNotifier {

    private static final Logger LOGGER = getLogger(GrowlNotifier.class);

    private final Application application;
    private final GrowlConfiguration configuration;
    private final GntpSlf4jListener logListener;

    private GntpNotificationInfo gNotification;
    private GntpClient gClient;

    public GrowlNotifier(Application application, GrowlConfiguration configuration, GntpSlf4jListener logListener) {
        LOGGER.debug("Configuring Growl for application {}: {}.", application, configuration);
        this.application = application;
        this.configuration = configuration;
        this.logListener = logListener;
    }

    @Override
    public Notifier init() {
        if (isClientRegistered()) {
            return this;
        }

        GntpApplicationInfo gApplication = Gntp.appInfo(application.name()).build();
        gNotification = Gntp.notificationInfo(gApplication, application.id())
            .icon(application.icon().toImage())
            .build();
        Gntp clientBuilder = Gntp.client(gApplication)
            .onPort(configuration.port())
            .forHost(configuration.host())
            .withoutRetry()
            .listener(logListener);
        if (configuration.password() != null) {
            clientBuilder.withPassword(configuration.password());
        }
        gClient = clientBuilder.build();
        gClient.register();
        try {
            gClient.waitRegistration(1L, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        return this;
    }

    @Override
    public void send(Notification notification) {
        if (isClientRegistered()) {
            GntpNotification success = Gntp.notification(gNotification, notification.title())
                    .text(notification.message())
                    .icon(notification.icon().toImage())
                    .priority(toPriority(notification.level()))
                    .build();
            try {
                gClient.notify(success, 5, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }

    @Override
    public void close() {
        if (isClientRegistered()) {
            try {
                // Seems that the client can be shutdown without having processed all the notifications...
                TimeUnit.SECONDS.sleep(1);
                gClient.shutdown(5, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }

    @Override
    public boolean isPersistent() {
        return false;
    }

    @Override
    public boolean tryInit() {
        init();
        boolean hasSucceeded = isClientRegistered();
        if (!hasSucceeded) {
            try {
                gClient.shutdown(1, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
        return hasSucceeded;
    }

    private static GntpNotification.Priority toPriority(Notification.Level level) {
        switch (level) {
            case INFO:
                return NORMAL;
            case WARNING:
                return HIGH;
            case ERROR:
                return HIGHEST;
            default:
                return NORMAL;
        }
    }

    private boolean isClientRegistered() {
        return gClient != null && gClient.isRegistered();
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(application, configuration, logListener);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        final GrowlNotifier other = (GrowlNotifier) obj;
        return Objects.equal(this.application, other.application)
            && Objects.equal(this.configuration, other.configuration)
            && Objects.equal(this.logListener, other.logListener);
    }

    @Override
    public String toString() {
        return MoreObjects.toStringHelper(this)
            .add("application", application)
            .add("configuration", configuration)
            .add("logListener", logListener)
            .toString();
    }
} @AutoValue
public abstract class KdialogConfiguration {

    private static final KdialogConfiguration DEFAULT = new AutoValue_KdialogConfiguration("kdialog");

    public abstract String bin();

    KdialogConfiguration() {
        // prevent external subclasses
    }

    public static KdialogConfiguration byDefault() {
        return DEFAULT;
    }

    public static KdialogConfiguration create(Properties properties) {
        if (properties == null) {
            return byDefault();
        }

        return new AutoValue_KdialogConfiguration(
                properties.getProperty("notifier.kdialog.path", DEFAULT.bin())
        );
    }
} public class KdialogNotifier implements DiscoverableNotifier {

    private static final Logger LOGGER = getLogger(KdialogNotifier.class);

    private final Application application;
    private final KdialogConfiguration configuration;
    private final Executor executor;

    public KdialogNotifier(Application application, KdialogConfiguration configuration, Executor executor) {
        LOGGER.debug("Configuring Kdialog for application {}: {}.", application, configuration);
        this.application = application;
        this.configuration = configuration;
        this.executor = executor;
    }

    @Override
    public Notifier init() {
        return this;
    }

    @Override
    public void send(Notification notification) {
        List<String> commands = new ArrayList<>();
        commands.add(configuration.bin());
        commands.add("--passivepopup");
        commands.add(notification.message());
        if (application.timeout() != -1) {
            commands.add(String.valueOf(TimeUnit.MILLISECONDS.toSeconds(application.timeout())));
        }
        commands.add("--title");
        commands.add(notification.title());
        commands.add("--icon");
        commands.add(notification.icon().asPath());

        try {
            executor.exec(commands.toArray(new String[0]));
        } catch (RuntimeException e) {
            throw new KdialogException("Error while sending notification with Kdialog.", e);
        }
    }

    @Override
    public void close() {
        executor.close();
    }

    @Override
    public boolean isPersistent() {
        return false;
    }

    @Override
    public boolean tryInit() {
        List<String> commands = new ArrayList<>();
        commands.add(configuration.bin());
        commands.add("-v");

        return executor.tryExec(commands.toArray(new String[0]));
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(application, configuration);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        final KdialogNotifier other = (KdialogNotifier) obj;
        return Objects.equal(this.application, other.application)
            && Objects.equal(this.configuration, other.configuration);
    }

    @Override
    public String toString() {
        return MoreObjects.toStringHelper(this)
            .add("configuration", configuration)
            .add("application", application)
            .toString();
    }
} public class SimpleNotificationCenterNotifier implements DiscoverableNotifier {

    private static final Logger LOGGER = LoggerFactory.getLogger(SimpleNotificationCenterNotifier.class);
    private static final String DOUBLE_QUOTE = "\"";

    private final TerminalNotifierConfiguration configuration;
    private final Executor executor;

    public SimpleNotificationCenterNotifier(TerminalNotifierConfiguration configuration, Executor executor) {
        LOGGER.debug("Configuring notification-center : {}.", configuration);
        this.configuration = configuration;
        this.executor = executor;
    }

    @Override
    public Notifier init() {
        return this;
    }

    @Override
    public void send(Notification notification) {
        List<String> commands = new ArrayList<>();
        commands.add("osascript");
        commands.add("-e");
        commands.add(buildAppleScript(notification));

        try {
            executor.exec(commands.toArray(new String[0]));
        } catch (RuntimeException e) {
            throw new SimpleNotificationCenterException("Error while sending notification to terminal-notifier", e.getCause());
        }
    }

    private String buildAppleScript(Notification notification) {
        StringBuilder script = new StringBuilder("display notification ");
        script.append(DOUBLE_QUOTE)
            .append(notification.message())
            .append(DOUBLE_QUOTE);
        if (configuration.sound() != null) {
            script.append(" sound name ")
                .append(DOUBLE_QUOTE)
                .append(configuration.sound())
                .append(DOUBLE_QUOTE);
        }
        script.append(" with title ")
            .append(DOUBLE_QUOTE)
            .append(notification.title())
            .append(DOUBLE_QUOTE);
        if (notification.subtitle() != null) {
            script.append(" subtitle ")
                .append(DOUBLE_QUOTE)
                .append(notification.subtitle())
                .append(DOUBLE_QUOTE);
        }
        return script.toString();
    }

    @Override
    public void close() {
        executor.close();
    }

    @Override
    public boolean isPersistent() {
        return false;
    }

    @Override
    public boolean tryInit() {
        return false;
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(configuration);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        final SimpleNotificationCenterNotifier other = (SimpleNotificationCenterNotifier) obj;
        return Objects.equal(this.configuration, other.configuration);
    }

    @Override
    public String toString() {
        return MoreObjects.toStringHelper(this)
            .add("configuration", configuration)
            .toString();
    }
} public class TerminalNotifier implements DiscoverableNotifier {

    private static final Logger LOGGER = LoggerFactory.getLogger(TerminalNotifier.class);

    private final Application application;
    private final TerminalNotifierConfiguration configuration;
    private final Executor executor;

    public TerminalNotifier(Application application, TerminalNotifierConfiguration configuration, Executor executor) {
        LOGGER.debug("Configuring terminal-notifier for application {}: {}.", application, configuration);
        this.application = application;
        this.configuration = configuration;
        this.executor = executor;
    }

    @Override
    public Notifier init() {
        return this;
    }

    @Override
    public void send(Notification notification) {

        List<String> commands = new ArrayList<>();
        commands.add(configuration.bin());
        commands.add("-title");
        commands.add(application.name());
        if (notification.subtitle() != null) {
            commands.add("-subtitle");
            commands.add(notification.subtitle());
        }
        commands.add("-message");
        commands.add(notification.message());
        commands.add("-group");
        commands.add(application.id());
        if (configuration.activateApplication() != null) {
            commands.add("-activate");
            commands.add(configuration.activateApplication());
        }
        commands.add("-contentImage");
        commands.add(notification.icon().asPath());
        if (configuration.sound() != null) {
            commands.add("-sound");
            commands.add(configuration.sound());
        }
        commands.add("-appIcon");
        commands.add(application.icon().asPath());

        try {
            executor.exec(commands.toArray(new String[0]));
        } catch (RuntimeException e) {
            throw new TerminalNotifierNotificationException("Error while sending notification to terminal-notifier", e);
        }
    }

    @Override
    public void close() {
        executor.close();
    }

    @Override
    public boolean isPersistent() {
        return false;
    }

    @Override
    public boolean tryInit() {
        List<String> commands = new ArrayList<>();
        commands.add(configuration.bin());
        commands.add("-help");

        return executor.tryExec(commands.toArray(new String[0]));
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(application, configuration);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        final TerminalNotifier other = (TerminalNotifier) obj;
        return Objects.equal(this.application, other.application)
            && Objects.equal(this.configuration, other.configuration);
    }

    @Override
    public String toString() {
        return MoreObjects.toStringHelper(this)
            .add("application", application)
            .add("configuration", configuration)
            .toString();
    }
} @AutoValue
public abstract class TerminalNotifierConfiguration {

    private static final TerminalNotifierConfiguration DEFAULT = new AutoValue_TerminalNotifierConfiguration(
            "terminal-notifier", null, null
    );

    public abstract String bin();

    @Nullable
    public abstract String activateApplication();

    @Nullable
    public abstract String sound();

    TerminalNotifierConfiguration() {
        // prevent external subclasses
    }

    public static TerminalNotifierConfiguration byDefault() {
        return DEFAULT;
    }

    public static TerminalNotifierConfiguration create(Properties properties) {
        if (properties == null) {
            return byDefault();
        }

        return new AutoValue_TerminalNotifierConfiguration(
                properties.getProperty("notifier.notification-center.path", DEFAULT.bin()),
                properties.getProperty("notifier.notification-center.activate", DEFAULT.activateApplication()),
                properties.getProperty("notifier.notification-center.sound", DEFAULT.sound())
        );
    }
} @AutoValue
public abstract class NotifuConfiguration {

    private static final NotifuConfiguration DEFAULT = new AutoValue_NotifuConfiguration("notifu64");

    public abstract String bin();

    NotifuConfiguration() {
        // prevent external subclasses
    }

    public static NotifuConfiguration byDefault() {
        return DEFAULT;
    }

    public static NotifuConfiguration create(Properties properties) {
        if (properties == null) {
            return byDefault();
        }
        return new AutoValue_NotifuConfiguration(
                properties.getProperty("notifier.notifu.path", DEFAULT.bin())
        );
    }
} public class NotifuNotifier implements DiscoverableNotifier {

    private static final Logger LOGGER = getLogger(NotifuNotifier.class);

    private final Application application;
    private final NotifuConfiguration configuration;
    private final Executor executor;

    public NotifuNotifier(Application application, NotifuConfiguration configuration, Executor executor) {
        LOGGER.debug("Configuring notifu for application {}: {}.", application, configuration);
        this.application = application;
        this.configuration = configuration;
        this.executor = executor;
    }

    @Override
    public Notifier init() {
        return this;
    }

    @Override
    public void send(Notification notification) {
        List<String> commands = new ArrayList<>();
        commands.add(configuration.bin());
        commands.add("/p");
        commands.add(escape(notification.title()));
        commands.add("/m");
        commands.add(escape(notification.message()));
        commands.add("/d");
        if (application.timeout() == -1) {
            commands.add(String.valueOf(TimeUnit.SECONDS.toMillis(10)));
        } else {
            commands.add(String.valueOf(application.timeout()));
        }
        commands.add("/t");
        commands.add(toType(notification.level()));
        commands.add("/q");

        try {
            executor.exec(commands.toArray(new String[0]));
        } catch (RuntimeException e) {
            throw new NotifuException("Error while sending notification to notifu.", e.getCause());
        }
    }

    private static String escape(String title) {
        return title.replace("\"", "\\\"");
    }

    private static String toType(Notification.Level level) {
        switch (level) {
            case WARNING:
                return "warn";
            case ERROR:
                return "error";
            default:
                return "info";
        }
    }

    @Override
    public void close() {
        executor.close();
    }

    @Override
    public boolean isPersistent() {
        return false;
    }

    @Override
    public boolean tryInit() {
        List<String> commands = new ArrayList<>();
        commands.add(configuration.bin());
        commands.add("/v");

        return executor.tryExec(commands.toArray(new String[0]));
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(application, configuration);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        final NotifuNotifier other = (NotifuNotifier) obj;
        return Objects.equal(this.application, other.application)
            && Objects.equal(this.configuration, other.configuration);
    }

    @Override
    public String toString() {
        return MoreObjects.toStringHelper(this)
            .add("configuration", configuration)
            .add("application", application)
            .toString();
    }
} @AutoValue
public abstract class NotifyConfiguration {

    private static final NotifyConfiguration DEFAULT = new AutoValue_NotifyConfiguration(Pos.TOP_RIGHT, false);

    public abstract Pos position();

    public abstract boolean withDarkStyle();

    NotifyConfiguration() {
        // prevent external subclasses
    }

    public static NotifyConfiguration byDefault() {
        return DEFAULT;
    }

    public static NotifyConfiguration create(Properties properties) {
        if (properties == null) {
            return byDefault();
        }

        return new AutoValue_NotifyConfiguration(
            Pos.valueOf(properties.getProperty("notifier.notify.position", DEFAULT.position().name())),
            Boolean.valueOf(properties.getProperty("notifier.notify.darkstyle", String.valueOf(DEFAULT.withDarkStyle())))
        );
    }
} public class NotifyNotifier implements DiscoverableNotifier {

    private static final Logger LOGGER = getLogger(NotifyNotifier.class);

    private final Application application;
    private final NotifyConfiguration configuration;

    private boolean skipNotifications;

    public NotifyNotifier(Application application, NotifyConfiguration configuration) {
        LOGGER.debug("Configuring notify: {}.", configuration);
        this.configuration = configuration;
        this.application = application;
    }

    @Override
    public Notifier init() {
        if (isHeadless()) {
            skipNotifications = true;
        }
        return this;
    }

    @Override
    public boolean tryInit() {
        init();
        return !skipNotifications;
    }

    @Override
    public void send(Notification notification) {
        Notify notify = Notify.create()
            .title(notification.title())
            .text(notification.message())
            .image(notification.icon().toImage())
            .position(configuration.position())
            .hideAfter((int) (application.timeout() == -1 ? SECONDS.toMillis(3) : application.timeout()));

        if (configuration.withDarkStyle()) {
            notify.darkStyle();
        }

        notify.show();
    }

    @Override
    public void close() {
        if (!skipNotifications) {
            try {
                Thread.sleep(application.timeout() == -1 ? SECONDS.toMillis(3) : application.timeout());
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }

    private static boolean isHeadless() {
        return "true".equals(System.getProperty("java.awt.headless"));
    }

    @Override
    public boolean isPersistent() {
        return false;
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(application, configuration, skipNotifications);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        final NotifyNotifier other = (NotifyNotifier) obj;
        return Objects.equal(this.application, other.application)
            && Objects.equal(this.configuration, other.configuration)
            && Objects.equal(this.skipNotifications, other.skipNotifications);
    }

    @Override
    public String toString() {
        return MoreObjects.toStringHelper(this)
            .add("application", application)
            .add("configuration", configuration)
            .add("skipNotifications", skipNotifications)
            .toString();
    }
} @AutoValue
public abstract class NotifySendConfiguration {

    private static final NotifySendConfiguration DEFAULT = new AutoValue_NotifySendConfiguration("notify-send");

    public abstract String bin();

    NotifySendConfiguration() {
        // prevent external subclasses
    }

    public static NotifySendConfiguration byDefault() {
        return DEFAULT;
    }

    public static NotifySendConfiguration create(Properties properties) {
        if (properties == null) {
            return byDefault();
        }

        return new AutoValue_NotifySendConfiguration(properties.getProperty("notifier.notify-send.path", DEFAULT.bin()));
    }
} public class NotifySendNotifier implements DiscoverableNotifier {

    private static final Logger LOGGER = LoggerFactory.getLogger(NotifySendNotifier.class);

    private static final String CMD_TIMEOUT = "-t";
    private static final String CMD_ICON = "-i";
    private static final String CMD_URGENCY = "-u";

    private final Application application;
    private final NotifySendConfiguration configuration;
    private final Executor executor;

    public NotifySendNotifier(Application application, NotifySendConfiguration configuration, Executor executor) {
        LOGGER.debug("Configuring notify-send for application {}: {}.", application, configuration);
        this.application = application;
        this.configuration = configuration;
        this.executor = executor;
    }

    @Override
    public Notifier init() {
        return this;
    }

    @Override
    public void send(Notification notification) {
        List<String> commands = new ArrayList<>();
        commands.add(configuration.bin());
        commands.add(notification.title());
        commands.add(notification.message());
        if (application.timeout() != -1) {
            commands.add(CMD_TIMEOUT);
            commands.add(String.valueOf(application.timeout()));
        }
        commands.add(CMD_ICON);
        commands.add(notification.icon().asPath());
        commands.add(CMD_URGENCY);
        commands.add(toUrgency(notification.level()));

        try {
            executor.exec(commands.toArray(new String[0]));
        } catch (RuntimeException e) {
            throw new NotifySendNotificationException("Error while sending notification to notify-send.", e.getCause());
        }
    }

    @Override
    public void close() {
        executor.close();
    }

    @Override
    public boolean isPersistent() {
        return false;
    }

    @Override
    public boolean tryInit() {
        List<String> commands = new ArrayList<>();
        commands.add(configuration.bin());
        commands.add("-v");

        return executor.tryExec(commands.toArray(new String[0]));
    }

    private static String toUrgency(Notification.Level level) {
        switch (level) {
            case WARNING:
            case ERROR:
                return "critical";
            default:
                return "normal";
        }
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(application, configuration);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        final NotifySendNotifier other = (NotifySendNotifier) obj;
        return Objects.equal(this.application, other.application)
            && Objects.equal(this.configuration, other.configuration);
    }

    @Override
    public String toString() {
        return MoreObjects.toStringHelper(this)
            .add("application", application)
            .add("configuration", configuration)
            .toString();
    }
} @AutoValue
public abstract class PushbulletConfiguration {

    public abstract String key();

    @Nullable
    public abstract String device();

    @Nullable
    public abstract String email();

    PushbulletConfiguration() {
        // prevent external subclasses
    }

    public static PushbulletConfiguration create(Properties properties) {
        checkNotNull(properties, "Cannot create Pushbullet configuration without user configuration.");

        return new AutoValue_PushbulletConfiguration(
                properties.getProperty("notifier.pushbullet.apikey"),
                properties.getProperty("notifier.pushbullet.device"),
                properties.getProperty("notifier.pushbullet.email")
        );
    }
} public class PushbulletNotifier implements DiscoverableNotifier {

    private static final Logger LOGGER = LoggerFactory.getLogger(PushbulletNotifier.class);

    private final PushbulletConfiguration configuration;
    private final String url;

    private OkHttpClient client;

    public PushbulletNotifier(Application application, PushbulletConfiguration configuration) {
        this(application, configuration, "https://api.pushbullet.com/v2/pushes");
    }

    PushbulletNotifier(Application application, PushbulletConfiguration configuration, String url) {
        LOGGER.debug("Configuring Pushbullet for application {}: {}.", application, configuration);
        this.configuration = configuration;
        this.url = url;
    }

    @Override
    public Notifier init() {
        if (client != null) {
            return this;
        }

        client = new OkHttpClient();
        client.setAuthenticator(new Authenticator() {
            @Override
            public Request authenticate(Proxy proxy, Response response) {
                String credentials = Credentials.basic(configuration.key(), "");
                if (credentials.equals(response.request().header("Authorization"))) {
                    return null; // If we already failed with these credentials, don't retry.
                }
                return response.request().newBuilder().header("Authorization", credentials).build();
            }

            @Override
            public Request authenticateProxy(Proxy proxy, Response response) {
                return authenticate(proxy, response);
            }
        });

        return this;
    }

    @Override
    public void send(Notification notification) {
        Request request = new Request.Builder()
                .url(url)
                .post(buildRequestBody(notification))
                .build();

        try {
            Response response = client.newCall(request).execute();
            if (response.code() != 200) {
                String message = String.format("Pushbullet notification has failed, [%s] - [%s]%n%s",
                        response.code(),
                        response.message(),
                        response.body().string()
                );
                LOGGER.error(message);
                throw new PushbulletNotificationException(message);
            }
        } catch (IOException e) {
            String message = "Error while sending pushbullet notification.";
            LOGGER.error(message, e);
            throw new PushbulletNotificationException(message, e);
        }
    }

    private RequestBody buildRequestBody(Notification notification) {
        FormEncoding.Builder builder = new FormEncoding.Builder();
        if (configuration.device() != null) {
            builder.add("device_iden", configuration.device());
        }
        if (configuration.email() != null) {
            builder.add("email", configuration.email());
        }
        builder.add("type", "note")
                .add("title", notification.title())
                .add("body", notification.message());

        ByteArrayOutputStream data;
        try {
            data = new ByteArrayOutputStream();
            builder.build().writeBodyTo(data);
        } catch (IOException e) {
            String message = "Can't build request body.";
            LOGGER.error(message, e);
            throw new PushbulletNotificationException(message, e);
        }

        return RequestBody.create(MediaType.parse("application/x-www-form-urlencoded"), data.toByteArray());
    }

    @Override
    public void close() {
        // do nothing
    }

    @Override
    public boolean isPersistent() {
        return false;
    }

    @Override
    public boolean tryInit() {
        return false;
    }
} @AutoValue
public abstract class SlackConfiguration {

    /**
     * @return token to access Slack APIs
     * @see <a href="https://api.slack.com/custom-integrations/legacy-tokens">Legacy Token generator</a>
     */
    public abstract String token();

    /**
     * @return the channel name to send messages to (you should use your own @handle)
     */
    public abstract String channel();

    SlackConfiguration() {
        // prevent external subclasses
    }

    public static SlackConfiguration create(Properties properties) {
        checkNotNull(properties, "Cannot create Slack configuration without user configuration.");

        return new AutoValue_SlackConfiguration(
            properties.getProperty("notifier.slack.token"),
            properties.getProperty("notifier.slack.channel")
        );
    }
} public class SlackNotifier implements DiscoverableNotifier {

    private static final Logger LOGGER = getLogger(SlackNotifier.class);

    private final Application application;
    private final SlackConfiguration configuration;
    private final String url;

    private OkHttpClient client;

    public SlackNotifier(Application application, SlackConfiguration configuration, String url) {
        LOGGER.debug("Configuring Slack for application {}: {}.", application, configuration);
        this.application = application;
        this.configuration = configuration;
        this.url = url;
    }

    public SlackNotifier(Application application, SlackConfiguration configuration) {
        this(application, configuration, "https://slack.com/api/chat.postMessage");
    }

    @Override
    public Notifier init() {
        if (client != null) {
            return this;
        }

        client = new OkHttpClient();

        return this;
    }

    @Override
    public boolean tryInit() {
        return false;
    }

    @Override
    public void send(Notification notification) {
        StringBuilder builder = new StringBuilder(url)
            .append("?token=").append(configuration.token())
            .append("&channel=").append(encode(configuration.channel()))
            .append("&attachments=").append(encode(attachments(notification)));

        try {
            String requestUrl = builder.toString();
            LOGGER.debug("Calling Slack with parameters: {}", requestUrl);
            Response response = client.newCall(new Request.Builder().url(requestUrl).get().build()).execute();
            if (response.code() != 200) {
                String message = String.format("Slack notification has failed, [%s] - [%s]%n%s",
                    response.code(),
                    response.message(),
                    response.body().string()
                );
                LOGGER.error(message);
                throw new SlackNotificationException(message);
            }
        } catch (IOException e) {
            String message = "Error while sending Slack notification.";
            LOGGER.error(message, e);
            throw new SlackNotificationException(message, e);
        }
    }

    private String attachments(Notification notification) {
        return "[{" +
            "\"fallback\":\"" + notification.message() + "\"," +
            "\"color\":\"" + colorize(notification) + "\"," +
            "\"author_name\":\"" + application.name() + "\"," +
            "\"title\":\"" + notification.title() + "\"," +
            "\"text\":\"" + notification.message() + "\n" + notification.subtitle() + "\"," +
            "}]";
    }

    private static String colorize(Notification notification) {
        switch (notification.level()) {
            case INFO:
                return "good";
            case WARNING:
                return "warning";
            case ERROR:
                return "danger";
            default:
                return "warning";
        }
    }

    private static String encode(String string) {
        try {
            return URLEncoder.encode(string, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new SlackNotificationException("Cannot encode URL parameter: " + string, e);
        }
    }

    @Override
    public void close() {
        // do nothing
    }

    @Override
    public boolean isPersistent() {
        return false;
    }
} @AutoValue
public abstract class SnarlConfiguration {

    private static final SnarlConfiguration DEFAULT = new AutoValue_SnarlConfiguration("localhost", 9887, null);

    public abstract String host();

    public abstract int port();

    @Nullable
    public abstract String applicationPassword();

    SnarlConfiguration() {
        // prevent external subclasses
    }

    public static SnarlConfiguration byDefault() {
        return DEFAULT;
    }

    public static SnarlConfiguration create(Properties properties) {
        if (properties == null) {
            return byDefault();
        }

        return new AutoValue_SnarlConfiguration(
                properties.getProperty("notifier.snarl.host", DEFAULT.host()),
                Integer.valueOf(properties.getProperty("notifier.snarl.port", String.valueOf(DEFAULT.port()))),
                properties.getProperty("notifier.snarl.appPassword", DEFAULT.applicationPassword())
        );
    }
} public class SnarlNotifier implements DiscoverableNotifier {

    private static final Logger LOGGER = LoggerFactory.getLogger(SnarlNotifier.class);

    private final Application application;
    private final SnarlConfiguration configuration;
    private final fr.jcgay.snp4j.Application snarlApplication;

    private fr.jcgay.snp4j.Notifier snarl;

    public SnarlNotifier(Application application, SnarlConfiguration configuration) {
        LOGGER.debug("Configuring Snarl for application {}: {}.", application, configuration);
        this.application = application;
        this.configuration = configuration;
        this.snarlApplication = fr.jcgay.snp4j.Application.withPassword(application.id(), application.name(), configuration.applicationPassword());
    }

    @Override
    public Notifier init() {
        Server server = Server.builder()
            .withHost(configuration.host())
            .withPort(configuration.port())
            .build();

        try {
            snarl = SnpNotifier.of(snarlApplication, server);
        } catch (SnpException e) {
            throw new SnarlNotificationException("Cannot register application with Snarl.", e);
        }
        return this;
    }

    @Override
    public void send(Notification notification) {
        if (snarl == null) {
            LOGGER.warn("Snarl notifier is not initialized, cannot send notification.");
            return;
        }

        fr.jcgay.snp4j.request.Notification snarlNotification = new fr.jcgay.snp4j.request.Notification();
        snarlNotification.setIcon(Icon.base64(notification.icon().toByteArray()));
        snarlNotification.setText(notification.message());
        snarlNotification.setTitle(notification.title());
        snarlNotification.setPriority(toPriority(notification.level()));

        init();
        try {
            snarl.send(snarlNotification);
        } catch (SnpException e) {
            throw new SnarlNotificationException("Cannot send notification to Snarl.", e);
        }
    }

    @Override
    public void close() {
        if (snarl != null) {
            try {
                Closeables.close(snarl, true);
            } catch (IOException ignored) {}
        }
    }

    @Override
    public boolean isPersistent() {
        return false;
    }

    @Override
    public boolean tryInit() {
        try {
            init();
            return true;
        } catch (RuntimeException e) {
            close();
            return false;
        }
    }

    private static Priority toPriority(Notification.Level level) {
        switch (level) {
            case WARNING:
            case ERROR:
                return Priority.HIGH;
            default:
                return Priority.NORMAL;
        }
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(application, configuration);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        final SnarlNotifier other = (SnarlNotifier) obj;
        return Objects.equal(this.application, other.application)
            && Objects.equal(this.configuration, other.configuration);
    }

    @Override
    public String toString() {
        return MoreObjects.toStringHelper(this)
            .add("application", application)
            .add("configuration", configuration)
            .toString();
    }
} public class SystemTrayNotifier implements DiscoverableNotifier {

    private static final Logger LOGGER = LoggerFactory.getLogger(SystemTrayNotifier.class);

    private final Application application;

    private boolean skipNotifications;
    private TrayIcon icon;

    public SystemTrayNotifier(Application application) {
        LOGGER.debug("Configuring System Tray for application {}.", application);
        this.application = application;
    }

    @Override
    public Notifier init() {
        if (icon != null) {
            return this;
        }

        if (!SystemTray.isSupported()) {
            skipNotifications = true;
            LOGGER.warn("SystemTray is not supported, skipping notifications...");
            return this;
        }

        icon = new TrayIcon(createImage(application.icon().toByteArray()), application.name());
        icon.setImageAutoSize(true);

        try {
            SystemTray.getSystemTray().add(icon);
        } catch (AWTException e) {
            throw new SystemTrayNotificationException("Error initializing SystemTray Icon.", e);
        }

        return this;
    }

    @Override
    public void send(Notification notification) {
        if (!skipNotifications) {
            icon.setImage(createImage(notification.icon().toByteArray()));
            icon.displayMessage(notification.title(), notification.message(), toMessageType(notification.level()));
        }
    }

    @Override
    public void close() {
        if (!skipNotifications) {
            try {
                Thread.sleep(application.timeout() == -1 ? SECONDS.toMillis(2) : application.timeout());
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            SystemTray.getSystemTray().remove(icon);
        }
    }

    @Override
    public boolean isPersistent() {
        return true;
    }

    @Override
    public boolean tryInit() {
        init();
        return !skipNotifications;
    }

    private static MessageType toMessageType(Notification.Level level) {
        switch (level) {
            case INFO:
                return MessageType.INFO;
            case WARNING:
                return MessageType.WARNING;
            case ERROR:
                return MessageType.ERROR;
            default:
                return MessageType.NONE;
        }
    }

    private Image createImage(byte[] imageData) {
        return Toolkit.getDefaultToolkit().createImage(imageData);
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(application);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        final SystemTrayNotifier other = (SystemTrayNotifier) obj;
        return Objects.equal(this.application, other.application);
    }

    @Override
    public String toString() {
        return MoreObjects.toStringHelper(this)
            .add("application", application)
            .toString();
    }
} @AutoValue
public abstract class ToasterConfiguration {

    private static final ToasterConfiguration DEFAULT = new AutoValue_ToasterConfiguration("toast");

    abstract String bin();

    ToasterConfiguration() {
        // prevent external subclasses
    }

    public static ToasterConfiguration byDefault() {
        return DEFAULT;
    }

    public static ToasterConfiguration create(Properties properties) {
        if (properties == null) {
            return byDefault();
        }

        return new AutoValue_ToasterConfiguration(
            properties.getProperty("notifier.toaster.bin", DEFAULT.bin())
        );
    }
} public class ToasterNotifier implements DiscoverableNotifier {

    private static final Logger LOGGER = LoggerFactory.getLogger(ToasterNotifier.class);
    private static final String DOUBLE_QUOTE = "\"";

    private final ToasterConfiguration configuration;
    private final Executor executor;

    public ToasterNotifier(ToasterConfiguration configuration, Executor executor) {
        LOGGER.debug("Configuring toaster: {}.", configuration);
        this.configuration = configuration;
        this.executor = executor;
    }

    @Override
    public Notifier init() {
        return this;
    }

    @Override
    public void send(Notification notification) {
        List<String> commands = new ArrayList<>();
        commands.add(configuration.bin());
        commands.add("-t");
        commands.add(DOUBLE_QUOTE + notification.title() + DOUBLE_QUOTE);
        commands.add("-m");
        commands.add(DOUBLE_QUOTE + notification.message() + DOUBLE_QUOTE);
        commands.add("-p");
        commands.add(DOUBLE_QUOTE + notification.icon().asPath() + DOUBLE_QUOTE);

        try {
            executor.exec(commands.toArray(new String[0]));
        } catch (RuntimeException e) {
            throw new ToasterNotificationException("Error while sending notification with toaster", e);
        }
    }

    @Override
    public void close() {
        executor.close();
    }

    @Override
    public boolean isPersistent() {
        return false;
    }

    @Override
    public boolean tryInit() {
        List<String> commands = new ArrayList<>();
        commands.add(configuration.bin());

        return executor.tryExec(commands.toArray(new String[0]));
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(configuration);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
        final ToasterNotifier other = (ToasterNotifier) obj;
        return Objects.equal(this.configuration, other.configuration);
    }

    @Override
    public String toString() {
        return MoreObjects.toStringHelper(this)
            .add("configuration", configuration)
            .toString();
    }
}
import fr.jcgay.notification.configuration.ChosenNotifiers;
import fr.jcgay.notification.configuration.OperatingSystem;
import fr.jcgay.notification.notifier.DoNothingNotifier;
import fr.jcgay.notification.notifier.additional.AdditionalNotifier;
import fr.jcgay.notification.notifier.anybar.AnyBarConfiguration;
import fr.jcgay.notification.notifier.anybar.AnyBarNotifier;
import fr.jcgay.notification.notifier.burnttoast.BurntToastNotifier;
import fr.jcgay.notification.notifier.burnttoast.BurntToastNotifierConfiguration;
import fr.jcgay.notification.notifier.executor.RuntimeExecutor;
import fr.jcgay.notification.notifier.growl.GrowlConfiguration;
import fr.jcgay.notification.notifier.growl.GrowlNotifier;
import fr.jcgay.notification.notifier.kdialog.KdialogConfiguration;
import fr.jcgay.notification.notifier.kdialog.KdialogNotifier;
import fr.jcgay.notification.notifier.notificationcenter.SimpleNotificationCenterNotifier;
import fr.jcgay.notification.notifier.notificationcenter.TerminalNotifier;
import fr.jcgay.notification.notifier.notificationcenter.TerminalNotifierConfiguration;
import fr.jcgay.notification.notifier.notifu.NotifuConfiguration;
import fr.jcgay.notification.notifier.notifu.NotifuNotifier;
import fr.jcgay.notification.notifier.notify.NotifyConfiguration;
import fr.jcgay.notification.notifier.notify.NotifyNotifier;
import fr.jcgay.notification.notifier.notifysend.NotifySendConfiguration;
import fr.jcgay.notification.notifier.notifysend.NotifySendNotifier;
import fr.jcgay.notification.notifier.pushbullet.PushbulletConfiguration;
import fr.jcgay.notification.notifier.pushbullet.PushbulletNotifier;
import fr.jcgay.notification.notifier.slack.SlackConfiguration;
import fr.jcgay.notification.notifier.slack.SlackNotifier;
import fr.jcgay.notification.notifier.snarl.SnarlConfiguration;
import fr.jcgay.notification.notifier.snarl.SnarlNotifier;
import fr.jcgay.notification.notifier.systemtray.SystemTrayNotifier;
import fr.jcgay.notification.notifier.toaster.ToasterConfiguration;
import fr.jcgay.notification.notifier.toaster.ToasterNotifier;
import org.slf4j.Logger;
import java.util.LinkedHashSet;
import java.util.Properties;
import java.util.Set;
import static fr.jcgay.notification.notifier.growl.GntpSlf4jListener.DEBUG;
import static fr.jcgay.notification.notifier.growl.GntpSlf4jListener.ERROR;
import static java.util.Collections.unmodifiableSet;
import static org.slf4j.LoggerFactory.getLogger;
package fr.jcgay.notification;




class NotifierProvider {

    private static final Logger LOGGER = getLogger(NotifierProvider.class);

    private static final ChosenNotifiers GROWL = ChosenNotifiers.from("growl");
    private static final ChosenNotifiers NOTIFICATION_CENTER = ChosenNotifiers.from("notificationcenter");
    private static final ChosenNotifiers NOTIFY_SEND = ChosenNotifiers.from("notifysend");
    private static final ChosenNotifiers PUSHBULLET = ChosenNotifiers.from("pushbullet");
    private static final ChosenNotifiers SNARL = ChosenNotifiers.from("snarl");
    private static final ChosenNotifiers SYSTEM_TRAY = ChosenNotifiers.from("systemtray");
    private static final ChosenNotifiers NOTIFU = ChosenNotifiers.from("notifu");
    private static final ChosenNotifiers KDIALOG = ChosenNotifiers.from("kdialog");
    private static final ChosenNotifiers ANYBAR = ChosenNotifiers.from("anybar");
    private static final ChosenNotifiers SIMPLE_NOTIFICATION_CENTER = ChosenNotifiers.from("simplenc");
    private static final ChosenNotifiers TOASTER = ChosenNotifiers.from("toaster");
    private static final ChosenNotifiers NOTIFY = ChosenNotifiers.from("notify");
    private static final ChosenNotifiers BURNT_TOAST = ChosenNotifiers.from("burnttoast");
    private static final ChosenNotifiers SLACK = ChosenNotifiers.from("slack");

    private final OperatingSystem os;

    public NotifierProvider(OperatingSystem os) {
        this.os = os;
    }

    public DiscoverableNotifier byName(ChosenNotifiers notifier, Properties properties, Application application) {

        if (!notifier.secondary().isEmpty()) {
            LinkedHashSet<DiscoverableNotifier> secondary = new LinkedHashSet<>(notifier.secondary().size());
            for (String secondaryNotifier : notifier.secondary()) {
                secondary.add(byName(ChosenNotifiers.from(secondaryNotifier), properties, application));
            }

            return new AdditionalNotifier(
                byName(ChosenNotifiers.from(notifier.primary()), properties, application),
                unmodifiableSet(secondary)
            );
        }

        if (GROWL.equals(notifier)) {
            return new GrowlNotifier(application, GrowlConfiguration.create(properties), ERROR);
        }
        if (NOTIFICATION_CENTER.equals(notifier)) {
            return new TerminalNotifier(application, TerminalNotifierConfiguration.create(properties), new RuntimeExecutor(application.timeout()));