├── src ├── test │ ├── resources │ │ ├── version-test-empty │ │ ├── version-test │ │ ├── implementation.properties │ │ ├── implementation-user.properties │ │ ├── anybar.properties │ │ └── logback-test.xml │ └── groovy │ │ └── fr │ │ └── jcgay │ │ └── maven │ │ └── notifier │ │ ├── DummyNotifier.groovy │ │ ├── FakeContext.groovy │ │ ├── VersionTest.groovy │ │ ├── KnownElapsedTimeTicker.groovy │ │ ├── sound │ │ └── SoundNotifierTest.groovy │ │ ├── AbstractNotifierTest.groovy │ │ ├── AbstractNotifierThresholdTest.groovy │ │ ├── Fixtures.groovy │ │ ├── ConfigurationParserTest.groovy │ │ ├── NotificationEventSpyChooserTest.groovy │ │ └── sendnotification │ │ └── SendNotificationNotifierTest.groovy ├── main │ ├── properties │ │ └── version │ ├── resources │ │ ├── maven.png │ │ ├── dialog-clean.png │ │ ├── dialog-error-5.png │ │ ├── Action-build-icon.png │ │ ├── 109662__grunz__success.wav │ │ └── Sad_Trombone-Joe_Lamb-665429450.wav │ ├── java │ │ └── fr │ │ │ └── jcgay │ │ │ └── maven │ │ │ └── notifier │ │ │ ├── sound │ │ │ ├── EndListener.java │ │ │ └── SoundNotifier.java │ │ │ ├── UselessNotifier.java │ │ │ ├── Status.java │ │ │ ├── Notifier.java │ │ │ ├── Version.java │ │ │ ├── Configuration.java │ │ │ ├── AbstractNotifier.java │ │ │ ├── NotificationEventSpyChooser.java │ │ │ ├── sendnotification │ │ │ └── SendNotificationNotifier.java │ │ │ └── ConfigurationParser.java │ └── assembly │ │ └── jar-with-logging.xml └── docker │ ├── Dockerfile │ └── README.md ├── .gitignore ├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ └── maven.yml ├── etc └── deploy-settings.xml ├── .travis.yml ├── jreleaser.yml ├── LICENSE ├── README.md ├── pom.xml └── CHANGELOG.md /src/test/resources/version-test-empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/test/resources/version-test: -------------------------------------------------------------------------------- 1 | 1.0.0 2 | -------------------------------------------------------------------------------- /src/main/properties/version: -------------------------------------------------------------------------------- 1 | ${project.version} 2 | -------------------------------------------------------------------------------- /src/test/resources/implementation.properties: -------------------------------------------------------------------------------- 1 | notifier.implementation = growl 2 | -------------------------------------------------------------------------------- /src/test/resources/implementation-user.properties: -------------------------------------------------------------------------------- 1 | notifier.implementation = snarl 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | *.iml 3 | .idea 4 | .java-version 5 | dependency-reduced-pom.xml 6 | -------------------------------------------------------------------------------- /src/test/resources/anybar.properties: -------------------------------------------------------------------------------- 1 | notifier.anybar.port=1738 2 | notifier.anybar.host=192.168.1.1 3 | -------------------------------------------------------------------------------- /src/main/resources/maven.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcgay/maven-notifier/HEAD/src/main/resources/maven.png -------------------------------------------------------------------------------- /src/main/resources/dialog-clean.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcgay/maven-notifier/HEAD/src/main/resources/dialog-clean.png -------------------------------------------------------------------------------- /src/main/resources/dialog-error-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcgay/maven-notifier/HEAD/src/main/resources/dialog-error-5.png -------------------------------------------------------------------------------- /src/main/resources/Action-build-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcgay/maven-notifier/HEAD/src/main/resources/Action-build-icon.png -------------------------------------------------------------------------------- /src/main/resources/109662__grunz__success.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcgay/maven-notifier/HEAD/src/main/resources/109662__grunz__success.wav -------------------------------------------------------------------------------- /src/test/groovy/fr/jcgay/maven/notifier/DummyNotifier.groovy: -------------------------------------------------------------------------------- 1 | package fr.jcgay.maven.notifier 2 | 3 | 4 | interface DummyNotifier { 5 | void send() 6 | } 7 | -------------------------------------------------------------------------------- /src/main/resources/Sad_Trombone-Joe_Lamb-665429450.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcgay/maven-notifier/HEAD/src/main/resources/Sad_Trombone-Joe_Lamb-665429450.wav -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = space 7 | indent_size = 4 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | 11 | [.travis.yml] 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /src/test/groovy/fr/jcgay/maven/notifier/FakeContext.groovy: -------------------------------------------------------------------------------- 1 | package fr.jcgay.maven.notifier 2 | 3 | import org.apache.maven.eventspy.EventSpy 4 | 5 | 6 | class FakeContext implements EventSpy.Context { 7 | 8 | private Map internal = [:] 9 | 10 | @Override 11 | Map getData() { 12 | return internal 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} [%thread] %L %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: maven 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "04:00" 8 | open-pull-requests-limit: 10 9 | ignore: 10 | - dependency-name: org.mockito:mockito-core 11 | versions: 12 | - 3.7.7 13 | - 3.8.0 14 | - dependency-name: org.testng:testng 15 | versions: 16 | - 7.3.0 17 | - dependency-name: org.apache.maven:maven-core 18 | versions: 19 | - 3.6.3 20 | -------------------------------------------------------------------------------- /src/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian 2 | 3 | RUN apt-get update && apt-get install -y maven xterm libnotify-bin 4 | RUN apt-get install -y dbus-x11 x11-utils x11-xserver-utils xfce4 --no-install-recommends 5 | RUN apt-get install -y xfce4-terminal mousepad xfce4-notifyd --no-install-recommends 6 | RUN apt-get install -y libxv1 mesa-utils mesa-utils-extra libgl1-mesa-glx libglew2.0 \ 7 | libglu1-mesa libgl1-mesa-dri libdrm2 libgles2-mesa libegl1-mesa --no-install-recommends 8 | 9 | ENTRYPOINT [ "startxfce4" ] 10 | -------------------------------------------------------------------------------- /etc/deploy-settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | ossrh 8 | ${env.OSSRH_USER} 9 | ${env.OSSRH_PASS} 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/java/fr/jcgay/maven/notifier/sound/EndListener.java: -------------------------------------------------------------------------------- 1 | package fr.jcgay.maven.notifier.sound; 2 | 3 | import javax.sound.sampled.LineEvent; 4 | import javax.sound.sampled.LineListener; 5 | 6 | class EndListener implements LineListener { 7 | 8 | private boolean playing = true; 9 | 10 | @Override 11 | public synchronized void update(LineEvent event) { 12 | LineEvent.Type type = event.getType(); 13 | if (type == LineEvent.Type.STOP || type == LineEvent.Type.CLOSE) { 14 | playing = false; 15 | notifyAll(); 16 | } 17 | } 18 | 19 | public synchronized void waitEnd() throws InterruptedException { 20 | while (playing) { 21 | wait(); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/docker/README.md: -------------------------------------------------------------------------------- 1 | # Linux graphical docker image 2 | 3 | This image is a debian build with [Xfce](https://xfce.org). 4 | 5 | ## Run with macOS 6 | 7 | ### Prerequesites 8 | 9 | Install [XQuartz](https://www.xquartz.org): 10 | 11 | $> brew cask install xquartz 12 | 13 | In the `Preferences > Security` tab, check that `Allow connections from network clients` is activated. 14 | In the `Preferences > Output` tab, activate `Full screen mode` to not mess your macOS and X11 display. 15 | 16 | ### Docker 17 | 18 | $> docker build -t maven-notifier . 19 | $> ip=$(ifconfig en0 | grep inet | awk '$1=="inet" {print $2}') 20 | $> xhost + $ip 21 | $> docker run -d -e DISPLAY=$ip:0 -v /tmp/.X11-unix:/tmp/.X11-unix -v /Users/jcgay/.m2:/root/.m2 -v /Users/jcgay/dev/maven-notifier/target:/usr/share/maven/lib/ext maven-notifier -------------------------------------------------------------------------------- /src/main/java/fr/jcgay/maven/notifier/UselessNotifier.java: -------------------------------------------------------------------------------- 1 | package fr.jcgay.maven.notifier; 2 | 3 | import org.apache.maven.eventspy.EventSpy; 4 | import org.apache.maven.execution.MavenExecutionResult; 5 | 6 | import java.util.List; 7 | 8 | public enum UselessNotifier implements Notifier { 9 | EMPTY; 10 | 11 | @Override 12 | public boolean isCandidateFor(String desiredImplementation) { 13 | return false; 14 | } 15 | 16 | @Override 17 | public void init(EventSpy.Context context) { 18 | // do nothing 19 | } 20 | 21 | @Override 22 | public void onEvent(MavenExecutionResult event) { 23 | // do nothing 24 | } 25 | 26 | @Override 27 | public void close() { 28 | // do nothing 29 | } 30 | 31 | @Override 32 | public void onFailWithoutProject(List exceptions) { 33 | // do nothing 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | 4 | name: Java CI with Maven 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up JDK 8 20 | uses: actions/setup-java@v2 21 | with: 22 | java-version: '8' 23 | distribution: 'adopt' 24 | - name: Cache 25 | uses: actions/cache@v2.1.6 26 | with: 27 | path: ~/.m2/repository 28 | key: local-maven-repository 29 | - name: Build with Maven 30 | run: mvn -B verify coveralls:report -Prun-coverage -DrepoToken=${{ secrets.COVERALLS_TOKEN }} 31 | -------------------------------------------------------------------------------- /src/test/groovy/fr/jcgay/maven/notifier/VersionTest.groovy: -------------------------------------------------------------------------------- 1 | package fr.jcgay.maven.notifier 2 | 3 | import org.testng.annotations.Test 4 | 5 | import static org.assertj.core.api.Assertions.assertThat 6 | 7 | class VersionTest { 8 | 9 | @Test 10 | void 'should read version from existing resource'() { 11 | def version = new Version('/version-test') 12 | 13 | assertThat(version.get()).isEqualTo('1.0.0') 14 | } 15 | 16 | @Test 17 | void 'should return unknown version when resource does not exist'() { 18 | def version = new Version('/does-not-exist') 19 | 20 | assertThat(version.get()).isEqualTo('unknown-version') 21 | } 22 | 23 | @Test 24 | void 'should return unknown version when resource is empty'() { 25 | def version = new Version('/version-test-empty') 26 | 27 | assertThat(version.get()).isEqualTo('unknown-version') 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/assembly/jar-with-logging.xml: -------------------------------------------------------------------------------- 1 | 5 | packaging-with-logging 6 | 7 | zip 8 | 9 | false 10 | 11 | 12 | ${project.build.directory}/${project.build.finalName}.jar 13 | 14 | 15 | ${project.build.directory}/slf4j-nop-${slf4j-version}.jar 16 | 17 | 18 | ${project.build.directory}/slf4j-api-${slf4j-version}.jar 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/test/groovy/fr/jcgay/maven/notifier/KnownElapsedTimeTicker.groovy: -------------------------------------------------------------------------------- 1 | package fr.jcgay.maven.notifier; 2 | 3 | import com.google.common.base.Stopwatch; 4 | import com.google.common.base.Ticker 5 | import groovy.transform.CompileStatic; 6 | 7 | @CompileStatic 8 | class KnownElapsedTimeTicker extends Ticker { 9 | 10 | private long expectedElapsedTime 11 | private boolean firstRead 12 | 13 | KnownElapsedTimeTicker(long expectedElapsedTime) { 14 | this.expectedElapsedTime = expectedElapsedTime 15 | } 16 | 17 | static Stopwatch aStopWatchWithElapsedTime(long elapsedTime) { 18 | aStartedStopwatchWithElapsedTime(elapsedTime).stop() 19 | } 20 | 21 | static Stopwatch aStartedStopwatchWithElapsedTime(long elapsedTimeNano) { 22 | Stopwatch.createStarted(new KnownElapsedTimeTicker(elapsedTimeNano)) 23 | } 24 | 25 | @Override 26 | long read() { 27 | firstRead = !firstRead; 28 | firstRead ? 0 : expectedElapsedTime 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/test/groovy/fr/jcgay/maven/notifier/sound/SoundNotifierTest.groovy: -------------------------------------------------------------------------------- 1 | package fr.jcgay.maven.notifier.sound 2 | 3 | 4 | import groovy.transform.CompileStatic 5 | import org.testng.annotations.BeforeMethod 6 | import org.testng.annotations.Test 7 | 8 | import static fr.jcgay.maven.notifier.Fixtures.skipSendNotificationInit 9 | import static org.assertj.core.api.Assertions.assertThat 10 | 11 | @CompileStatic 12 | class SoundNotifierTest { 13 | 14 | private SoundNotifier notifier 15 | 16 | @BeforeMethod 17 | void init() throws Exception { 18 | notifier = new SoundNotifier() 19 | notifier.init(skipSendNotificationInit()) 20 | } 21 | 22 | @Test 23 | void 'should return true when sound is the choosen notifier'() throws Exception { 24 | assertThat notifier.isCandidateFor('sound') isTrue() 25 | } 26 | 27 | @Test 28 | void 'should return false when sound is not the choosen notifier'() throws Exception { 29 | assertThat notifier.isCandidateFor('growl') isFalse() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: java 3 | addons: 4 | sonarcloud: 5 | organization: "jcgay-github" 6 | token: 7 | secure: "MLW7muRsJiF3Cwrqa/UFeKwNJzSd8g0WVZ30d6Yyy2egkbTiZ2M9U7/WfXQwV1Fd4lMjEOXFyrRcH3cBla4dMRG9JQLGEsBr1FGRJZZ3JiA01hTEWu9bD9vZM0pIj0X8psLZ+NoepzPWrhgMR+AUwrOSaKK95qx2ncEgWq3lAO4=" 8 | jdk: 9 | - openjdk8 10 | install: true 11 | script: 12 | - mvn verify sonar:sonar -Prun-coverage -B 13 | after_success: 14 | - mvn coveralls:report 15 | - "[[ ${TRAVIS_PULL_REQUEST} == 'false' ]] && [[ ${TRAVIS_TAG} == '' ]] && mvn deploy -DskipTests --settings etc/deploy-settings.xml" 16 | cache: 17 | directories: 18 | - '$HOME/.m2/repository' 19 | - '$HOME/.sonar/cache' 20 | env: 21 | global: 22 | - secure: cttGV5bt0VuZZXHK0yo9rfszAjX67+pcRKMBjGuFEdr3v8uD55ho2qrXAtKEbG5x/g1atBXKD3HhBzK5Mb00XDkF93TQdX9veO6619xcVqQ7LdzPDIbbpX7lrS7vIaUaM77+hCtHFEKZs6+DVyeL93z0Vy7GiIAnRAnGomlmAwc= 23 | - secure: bjSYB4bs3FXRt9kPkPE3Qex00rckmnrKc1SoNiDXJ9ZHVjGRl3SXjcAUDQiTvmfliuUBMSRryCpjR4xqUywICEwD8tg9ubW7EbI8X12yxtGszJCoGQYTSlVc/bVgM+qaT/KxLvoCI3PTsPgEI/DsSIjmM4uVqzStgcgb02qZWE8= 24 | -------------------------------------------------------------------------------- /src/main/java/fr/jcgay/maven/notifier/Status.java: -------------------------------------------------------------------------------- 1 | package fr.jcgay.maven.notifier; 2 | 3 | import org.apache.maven.execution.BuildFailure; 4 | import org.apache.maven.execution.BuildSuccess; 5 | import org.apache.maven.execution.BuildSummary; 6 | 7 | import java.net.URL; 8 | 9 | import static com.google.common.base.Preconditions.checkNotNull; 10 | 11 | public enum Status { 12 | 13 | SUCCESS("/dialog-clean.png", "Success"), 14 | FAILURE("/dialog-error-5.png", "Failure"), 15 | SKIPPED(null, "Skipped"); 16 | 17 | private final String icon; 18 | private final String message; 19 | 20 | private Status(String icon, String message) { 21 | this.icon = icon; 22 | this.message = checkNotNull(message); 23 | } 24 | 25 | public String message() { 26 | return message; 27 | } 28 | 29 | public static Status of(BuildSummary summary) { 30 | if (summary == null) { 31 | return SKIPPED; 32 | } else if (summary instanceof BuildSuccess) { 33 | return SUCCESS; 34 | } else if (summary instanceof BuildFailure) { 35 | return FAILURE; 36 | } 37 | throw new IllegalArgumentException(String.format("Summary status type [%s] is not handle.", summary.getClass().getName())); 38 | } 39 | 40 | public URL url() { 41 | return getClass().getResource(icon); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/fr/jcgay/maven/notifier/Notifier.java: -------------------------------------------------------------------------------- 1 | package fr.jcgay.maven.notifier; 2 | 3 | import org.apache.maven.eventspy.EventSpy; 4 | import org.apache.maven.execution.MavenExecutionResult; 5 | 6 | import java.util.List; 7 | 8 | public interface Notifier { 9 | 10 | /** 11 | * Indicate if the notifier is an implementation for the provided parameter. 12 | * 13 | * @return {@code true} if the notifier should be used to notify the build status. 14 | * @param desiredImplementation 15 | */ 16 | boolean isCandidateFor(String desiredImplementation); 17 | 18 | /** 19 | * Initializes the spy. 20 | * 21 | * @param context The event spy context, never {@code null}. 22 | */ 23 | void init(EventSpy.Context context); 24 | 25 | 26 | /** 27 | * Notifies the notifier of build result. 28 | */ 29 | void onEvent(MavenExecutionResult event); 30 | 31 | /** 32 | * Notifies the notifier of Maven's termination, allowing it to free any resources allocated by it. 33 | */ 34 | void close(); 35 | 36 | /** 37 | * Notifies the notifier of a build ends with error without information about the build.
38 | * This is for example, a malformed {@code pom.xml} which breaks everything. 39 | * @param exceptions errors that happened 40 | */ 41 | void onFailWithoutProject(List exceptions); 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/fr/jcgay/maven/notifier/Version.java: -------------------------------------------------------------------------------- 1 | package fr.jcgay.maven.notifier; 2 | 3 | import org.slf4j.Logger; 4 | 5 | import java.io.BufferedReader; 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.io.InputStreamReader; 9 | 10 | import static com.google.common.base.MoreObjects.firstNonNull; 11 | import static java.nio.charset.StandardCharsets.UTF_8; 12 | import static org.slf4j.LoggerFactory.getLogger; 13 | 14 | public class Version { 15 | 16 | private static final String UNKNOWN_VERSION = "unknown-version"; 17 | private static final Logger LOGGER = getLogger(Version.class); 18 | 19 | private final String source; 20 | 21 | Version(String classpathResource) { 22 | this.source = classpathResource; 23 | } 24 | 25 | public static Version current() { 26 | return new Version("/version"); 27 | } 28 | 29 | public String get() { 30 | InputStream resource = Version.class.getResourceAsStream(source); 31 | if (resource == null) { 32 | return UNKNOWN_VERSION; 33 | } 34 | try (BufferedReader reader = new BufferedReader(new InputStreamReader(resource, UTF_8))) { 35 | return firstNonNull(reader.readLine(), UNKNOWN_VERSION); 36 | } catch (IOException e) { 37 | LOGGER.warn("Error while trying to read current maven-notifier version.", e); 38 | return UNKNOWN_VERSION; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /jreleaser.yml: -------------------------------------------------------------------------------- 1 | release: 2 | github: 3 | owner: jcgay 4 | username: jcgay 5 | overwrite: true 6 | skipTag: true 7 | branch: master 8 | files: false 9 | artifacts: false 10 | checksums: false 11 | signatures: false 12 | changelog: 13 | links: true 14 | formatted: ALWAYS 15 | format: '- {{commitShortHash}} {{commitTitle}}' 16 | contributors: 17 | enabled: true 18 | labelers: 19 | - label: 'merge_pull' 20 | title: 'Merge pull' 21 | - label: 'merge_branch' 22 | title: 'Merge branch' 23 | - label: 'dependencies' 24 | title: 'Bump' 25 | - label: 'release' 26 | title: '[maven-release-plugin]' 27 | - label: 'issue' 28 | title: '^Fix' 29 | body: 'Fixes #' 30 | categories: 31 | - title: 'Noise' 32 | labels: 33 | - 'merge_pull' 34 | - 'merge_branch' 35 | - 'release' 36 | - title: '✅ Issues' 37 | labels: 38 | - 'issue' 39 | - title: '⚙️ Dependencies' 40 | labels: 41 | - 'dependencies' 42 | hide: 43 | categories: 44 | - 'Noise' 45 | contributors: 46 | - 'dependabot' 47 | -------------------------------------------------------------------------------- /src/main/java/fr/jcgay/maven/notifier/Configuration.java: -------------------------------------------------------------------------------- 1 | package fr.jcgay.maven.notifier; 2 | 3 | import com.google.common.base.MoreObjects; 4 | 5 | import java.util.Properties; 6 | 7 | public class Configuration { 8 | 9 | private String implementation; 10 | private boolean shortDescription; 11 | private int threshold; 12 | private Properties notifierProperties; 13 | private int timeout; 14 | 15 | public void setImplementation(String implementation) { 16 | this.implementation = implementation; 17 | } 18 | 19 | public String getImplementation() { 20 | return implementation; 21 | } 22 | 23 | public boolean isShortDescription() { 24 | return shortDescription; 25 | } 26 | 27 | public void setShortDescription(boolean shortDescription) { 28 | this.shortDescription = shortDescription; 29 | } 30 | 31 | public int getThreshold() { 32 | return threshold; 33 | } 34 | 35 | public void setThreshold(int threshold) { 36 | this.threshold = threshold; 37 | } 38 | 39 | public int getTimeout() { 40 | return timeout; 41 | } 42 | 43 | public void setTimeout(int timeout) { 44 | this.timeout = timeout; 45 | } 46 | 47 | public void setNotifierProperties(Properties notifierProperties) { 48 | this.notifierProperties = notifierProperties; 49 | } 50 | 51 | public Properties getNotifierProperties() { 52 | return notifierProperties; 53 | } 54 | 55 | @Override 56 | public String toString() { 57 | return MoreObjects.toStringHelper(this) 58 | .add("implementation", implementation) 59 | .add("shortDescription", shortDescription) 60 | .add("threshold", threshold) 61 | .add("notifier-properties", notifierProperties) 62 | .toString(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/test/groovy/fr/jcgay/maven/notifier/AbstractNotifierTest.groovy: -------------------------------------------------------------------------------- 1 | package fr.jcgay.maven.notifier 2 | 3 | import com.google.common.base.Stopwatch 4 | import groovy.transform.CompileStatic 5 | import org.apache.maven.execution.DefaultMavenExecutionResult 6 | import org.apache.maven.execution.MavenExecutionResult 7 | import org.testng.annotations.BeforeMethod 8 | import org.testng.annotations.Test 9 | 10 | import static fr.jcgay.maven.notifier.Fixtures.skipSendNotificationInit 11 | import static java.util.concurrent.TimeUnit.SECONDS 12 | import static org.assertj.core.api.Assertions.assertThat 13 | 14 | @CompileStatic 15 | class AbstractNotifierTest { 16 | 17 | private AbstractNotifier eventSpy 18 | private Stopwatch stopwatch 19 | 20 | @BeforeMethod 21 | void setUp() throws Exception { 22 | eventSpy = new AbstractNotifier() { 23 | @Override 24 | protected void fireNotification(MavenExecutionResult event) { 25 | 26 | } 27 | } 28 | stopwatch = Stopwatch.createUnstarted(new KnownElapsedTimeTicker(SECONDS.toNanos(2L))) 29 | eventSpy.stopwatch = stopwatch 30 | } 31 | 32 | @Test 33 | void 'should start timer when initiating event spy'() throws Exception { 34 | 35 | eventSpy.init(skipSendNotificationInit()) 36 | 37 | assertThat stopwatch.isRunning() isTrue() 38 | } 39 | 40 | @Test 41 | void 'should stop timer when listening to an event'() throws Exception { 42 | 43 | eventSpy.init(skipSendNotificationInit()) 44 | eventSpy.onEvent(new DefaultMavenExecutionResult()) 45 | 46 | assertThat stopwatch.isRunning() isFalse() 47 | assertThat stopwatch.elapsed(SECONDS) isEqualTo 2L 48 | } 49 | 50 | @Test 51 | void 'should reset time when closing event spy'() { 52 | 53 | eventSpy.init({ Collections.emptyMap() }) 54 | eventSpy.close() 55 | 56 | assertThat stopwatch.isRunning() isFalse() 57 | assertThat stopwatch.elapsed(SECONDS) isEqualTo 0L 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2012 Jean-Christophe Gay 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 10 | the Software, and to permit persons to whom the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | 24 | src/main/resources/dialog-clean.png comes from crystal. 25 | src/main/resources/Action-build-icon.png comes from crystal. 26 | Crystal Project (crystal) 27 | link: http://everaldo.com/crystal/, CrystalXp.net 28 | license: LGPL-2.1 29 | license link: http://creativecommons.org/licenses/LGPL/2.1/ 30 | 31 | src/main/resources/dialog-error-5.png comes from oxygen. 32 | Oxygen Icons 4.3.1 (KDE) (oxygen) 33 | link: http://www.oxygen-icons.org/ 34 | license: Dual: CC-BY-SA 3.0 or LGPL 35 | License link: http://creativecommons.org/licenses/by-sa/3.0/ 36 | http://creativecommons.org/licenses/LGPL/2.1/ 37 | 38 | src/main/resources/109662__grunz__success.wav comes from 39 | http://www.freesound.org/people/grunz/sounds/109662/ 40 | license: http://creativecommons.org/licenses/by/3.0/ 41 | 42 | src/main/resources/Sad_Trombone-Joe_Lamb-665429450.wav comes from 43 | http://soundbible.com/1830-Sad-Trombone.html 44 | license: http://creativecommons.org/licenses/by/3.0/ -------------------------------------------------------------------------------- /src/main/java/fr/jcgay/maven/notifier/AbstractNotifier.java: -------------------------------------------------------------------------------- 1 | package fr.jcgay.maven.notifier; 2 | 3 | import com.google.common.base.Stopwatch; 4 | import org.apache.maven.eventspy.EventSpy; 5 | import org.apache.maven.execution.MavenExecutionResult; 6 | import org.codehaus.plexus.component.annotations.Requirement; 7 | import org.codehaus.plexus.logging.Logger; 8 | 9 | import java.util.List; 10 | 11 | import static java.util.concurrent.TimeUnit.SECONDS; 12 | 13 | public abstract class AbstractNotifier implements Notifier { 14 | 15 | protected Logger logger; 16 | protected Configuration configuration; 17 | 18 | private Stopwatch stopwatch = Stopwatch.createUnstarted(); 19 | 20 | protected abstract void fireNotification(MavenExecutionResult event); 21 | 22 | @Override 23 | public final void init(EventSpy.Context context) { 24 | stopwatch.start(); 25 | configuration = (Configuration) context.getData().get("notifier.configuration"); 26 | if (!context.getData().containsKey("notifier.skip.init")) { 27 | initNotifier(); 28 | } 29 | } 30 | 31 | @Override 32 | public final void onEvent(MavenExecutionResult event) { 33 | stopwatch.stop(); 34 | if (stopwatch.elapsed(SECONDS) > configuration.getThreshold() || isPersistent()) { 35 | fireNotification(event); 36 | } else { 37 | logger.debug("No notification sent because build ends before threshold: " + configuration.getThreshold() + "s."); 38 | } 39 | } 40 | 41 | @Override 42 | public final void close() { 43 | stopwatch.reset(); 44 | closeNotifier(); 45 | } 46 | 47 | @Override 48 | public void onFailWithoutProject(List exceptions) { 49 | // do nothing 50 | } 51 | 52 | @Override 53 | public boolean isCandidateFor(String desiredImplementation) { 54 | return getClass().getName().contains(desiredImplementation); 55 | } 56 | 57 | @Requirement 58 | public void setLogger(Logger logger) { 59 | this.logger = logger; 60 | } 61 | 62 | public void setStopwatch(Stopwatch stopwatch) { 63 | this.stopwatch = stopwatch; 64 | } 65 | 66 | protected void initNotifier() { 67 | // do nothing 68 | } 69 | 70 | protected void closeNotifier() { 71 | // do nothing 72 | } 73 | 74 | protected Status getBuildStatus(MavenExecutionResult result) { 75 | return result.hasExceptions() ? Status.FAILURE : Status.SUCCESS; 76 | } 77 | 78 | protected long elapsedTime() { 79 | return stopwatch.elapsed(SECONDS); 80 | } 81 | 82 | protected boolean isPersistent() { 83 | return false; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/test/groovy/fr/jcgay/maven/notifier/AbstractNotifierThresholdTest.groovy: -------------------------------------------------------------------------------- 1 | package fr.jcgay.maven.notifier 2 | 3 | import com.google.common.base.Stopwatch 4 | import groovy.transform.CompileStatic 5 | import org.apache.maven.execution.DefaultMavenExecutionResult 6 | import org.apache.maven.execution.MavenExecutionResult 7 | import org.codehaus.plexus.logging.Logger 8 | import org.testng.annotations.BeforeMethod 9 | import org.testng.annotations.Test 10 | 11 | import static fr.jcgay.maven.notifier.ConfigurationParser.ConfigurationProperties.Property.THRESHOLD 12 | import static fr.jcgay.maven.notifier.Fixtures.skipSendNotificationInit 13 | import static java.util.concurrent.TimeUnit.SECONDS 14 | import static org.mockito.Mockito.* 15 | 16 | @CompileStatic 17 | class AbstractNotifierThresholdTest { 18 | 19 | private TestNotifier spy 20 | private DummyNotifier notifier 21 | private Configuration configuration 22 | 23 | @BeforeMethod 24 | void init() throws Exception { 25 | notifier = mock(DummyNotifier) 26 | spy = new TestNotifier(notifier: notifier) 27 | configuration = new Configuration() 28 | spy.logger = mock(Logger) 29 | } 30 | 31 | @Test 32 | void 'should not send notification when build ends before threshold'() throws Exception { 33 | configuration.threshold = 10 34 | build_will_last(SECONDS.toNanos(2L)) 35 | 36 | spy.init(skipSendNotificationInit(configuration)) 37 | spy.onEvent(new DefaultMavenExecutionResult()) 38 | 39 | verifyZeroInteractions(notifier) 40 | } 41 | 42 | @Test 43 | void 'should send notification when build ends after threshold'() throws Exception { 44 | configuration.threshold = 1 45 | build_will_last(SECONDS.toNanos(2L)) 46 | 47 | spy.init(skipSendNotificationInit(configuration)) 48 | spy.onEvent(new DefaultMavenExecutionResult()) 49 | 50 | verify(notifier).send() 51 | } 52 | 53 | @Test 54 | void 'should send notification when threshold is set to its default value'() throws Exception { 55 | configuration.threshold = THRESHOLD.defaultValue() as int 56 | build_will_last(SECONDS.toNanos(2L)) 57 | 58 | spy.init(skipSendNotificationInit(configuration)) 59 | spy.onEvent(new DefaultMavenExecutionResult()) 60 | 61 | verify(notifier).send() 62 | } 63 | 64 | private void build_will_last(Long time) { 65 | def stopwatch = Stopwatch.createUnstarted(new KnownElapsedTimeTicker(time)) 66 | spy.stopwatch = stopwatch 67 | } 68 | 69 | static class TestNotifier extends AbstractNotifier { 70 | private DummyNotifier notifier 71 | 72 | @Override 73 | protected void fireNotification(MavenExecutionResult event) { 74 | notifier.send() 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/fr/jcgay/maven/notifier/sound/SoundNotifier.java: -------------------------------------------------------------------------------- 1 | package fr.jcgay.maven.notifier.sound; 2 | 3 | import fr.jcgay.maven.notifier.AbstractNotifier; 4 | import fr.jcgay.maven.notifier.Notifier; 5 | import fr.jcgay.maven.notifier.Status; 6 | import org.apache.maven.execution.MavenExecutionResult; 7 | import org.codehaus.plexus.component.annotations.Component; 8 | 9 | import javax.sound.sampled.AudioInputStream; 10 | import javax.sound.sampled.AudioSystem; 11 | import javax.sound.sampled.Clip; 12 | import javax.sound.sampled.LineUnavailableException; 13 | import javax.sound.sampled.UnsupportedAudioFileException; 14 | import java.io.BufferedInputStream; 15 | import java.io.IOException; 16 | import java.io.InputStream; 17 | import java.util.List; 18 | 19 | @Component(role = Notifier.class, hint = "sound") 20 | public class SoundNotifier extends AbstractNotifier { 21 | 22 | @Override 23 | public void onFailWithoutProject(List exceptions) { 24 | playSound(Status.FAILURE); 25 | } 26 | 27 | @Override 28 | protected void fireNotification(MavenExecutionResult event) { 29 | playSound(getBuildStatus(event)); 30 | } 31 | 32 | private void playSound(Status status) { 33 | try (AudioInputStream ais = getAudioStream(status)) { 34 | if (ais == null) { 35 | logger.warn("Cannot get a sound to play. Skipping notification..."); 36 | return; 37 | } 38 | play(ais); 39 | } catch (IOException | LineUnavailableException e) { 40 | fail(e); 41 | } 42 | } 43 | 44 | private void play(AudioInputStream ais) throws LineUnavailableException, IOException { 45 | try (Clip clip = AudioSystem.getClip()) { 46 | EndListener listener = new EndListener(); 47 | clip.addLineListener(listener); 48 | clip.open(ais); 49 | clip.start(); 50 | wait(listener); 51 | } 52 | } 53 | 54 | private void wait(EndListener listener) { 55 | try { 56 | listener.waitEnd(); 57 | } catch (InterruptedException e) { 58 | Thread.currentThread().interrupt(); 59 | } 60 | } 61 | 62 | private void fail(Exception e) { 63 | logger.debug("Error playing sound.", e); 64 | } 65 | 66 | private AudioInputStream getAudioStream(Status success) { 67 | try { 68 | return AudioSystem.getAudioInputStream(getUrl(success)); 69 | } catch (UnsupportedAudioFileException | IOException e) { 70 | return noAudioStream(e); 71 | } 72 | } 73 | 74 | private AudioInputStream noAudioStream(Exception e) { 75 | logger.warn("Error reading audio stream.", e); 76 | return null; 77 | } 78 | 79 | private InputStream getUrl(Status status) { 80 | String sound = status == Status.SUCCESS ? "/109662__grunz__success.wav" : "/Sad_Trombone-Joe_Lamb-665429450.wav"; 81 | return new BufferedInputStream(getClass().getResourceAsStream(sound)); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/test/groovy/fr/jcgay/maven/notifier/Fixtures.groovy: -------------------------------------------------------------------------------- 1 | package fr.jcgay.maven.notifier 2 | 3 | import org.apache.maven.eventspy.EventSpy 4 | import org.apache.maven.execution.BuildSuccess 5 | import org.apache.maven.execution.MavenExecutionResult 6 | import org.apache.maven.project.MavenProject 7 | 8 | import static org.mockito.AdditionalAnswers.returnsElementsOf 9 | import static org.mockito.ArgumentMatchers.isA 10 | import static org.mockito.Mockito.* 11 | 12 | class Fixtures { 13 | 14 | static MavenExecutionResult aSuccessfulProject() { 15 | def project = aProjectWithOneModule('project') 16 | when project.hasExceptions() thenReturn false 17 | project 18 | } 19 | 20 | static MavenExecutionResult aFailingProject() { 21 | def project = aProjectWithOneModule('project') 22 | when project.hasExceptions() thenReturn true 23 | project 24 | } 25 | 26 | static MavenExecutionResult aProjectWithMultipleModule(String projectName) { 27 | def result = anEvent(projectName) 28 | when result.getTopologicallySortedProjects().size() thenReturn 4 29 | result 30 | } 31 | 32 | static MavenExecutionResult aProjectWithMultipleModule(String projectName, Project... projects) { 33 | def result = anEvent(projectName) 34 | when result.getTopologicallySortedProjects() thenReturn projects.collect { Project project -> mavenProject(project.name) } 35 | when result.getBuildSummary(isA(MavenProject)) thenAnswer returnsElementsOf(projects.collect { Project project -> new BuildSuccess(mavenProject(project.name), project.time) }) 36 | result 37 | } 38 | 39 | static MavenExecutionResult aProjectWithOneModule(String projectName) { 40 | def result = anEvent(projectName) 41 | when result.getTopologicallySortedProjects().size() thenReturn 1 42 | result 43 | } 44 | 45 | static MavenExecutionResult anEvent(String projectName) { 46 | def result = mock(MavenExecutionResult, RETURNS_DEEP_STUBS) 47 | when result.project.name thenReturn projectName 48 | result 49 | } 50 | 51 | static URL resource(String resource) { 52 | Thread.currentThread().getContextClassLoader().getResource(resource) 53 | } 54 | 55 | static Project aModule(String name, long time) { 56 | def project = new Project() 57 | project.name = name 58 | project.time = time 59 | project 60 | } 61 | 62 | static MavenProject mavenProject(String name) { 63 | def project = new MavenProject() 64 | project.name = name 65 | project 66 | } 67 | 68 | static EventSpy.Context skipSendNotificationInit() { 69 | skipSendNotificationInit(new Configuration()) 70 | } 71 | 72 | static EventSpy.Context skipSendNotificationInit(Configuration configuration) { 73 | new EventSpy.Context() { 74 | @Override 75 | Map getData() { 76 | return ["notifier.skip.init": true as Object, "notifier.configuration": configuration] 77 | } 78 | } 79 | } 80 | 81 | static class Project { 82 | String name 83 | long time 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/fr/jcgay/maven/notifier/NotificationEventSpyChooser.java: -------------------------------------------------------------------------------- 1 | package fr.jcgay.maven.notifier; 2 | 3 | import com.google.common.annotations.VisibleForTesting; 4 | import org.apache.maven.eventspy.AbstractEventSpy; 5 | import org.apache.maven.eventspy.EventSpy; 6 | import org.apache.maven.execution.MavenExecutionResult; 7 | import org.codehaus.plexus.component.annotations.Component; 8 | import org.codehaus.plexus.component.annotations.Requirement; 9 | import org.codehaus.plexus.logging.Logger; 10 | 11 | import java.util.List; 12 | 13 | @Component(role = EventSpy.class, hint = "notification", description = "Send notification to indicate build status.") 14 | public class NotificationEventSpyChooser extends AbstractEventSpy { 15 | 16 | public static final String SKIP_NOTIFICATION = "skipNotification"; 17 | 18 | @Requirement 19 | private Logger logger; 20 | 21 | @Requirement 22 | private List availableNotifiers; 23 | 24 | @Requirement 25 | private ConfigurationParser configurationParser; 26 | 27 | private Notifier activeNotifier; 28 | 29 | @Override 30 | public void init(Context context) throws Exception { 31 | if (logger.isDebugEnabled()) { 32 | logger.debug("Using maven-notifier " + Version.current().get()); 33 | } 34 | Configuration configuration = configurationParser.get(); 35 | context.getData().put("notifier.configuration", configuration); 36 | 37 | chooseNotifier(configuration); 38 | activeNotifier.init(context); 39 | } 40 | 41 | @Override 42 | public void onEvent(Object event) throws Exception { 43 | if (shouldSendNotification()) { 44 | if (isExecutionResult(event) && hasFailedWithoutProject((MavenExecutionResult) event)) { 45 | activeNotifier.onFailWithoutProject(((MavenExecutionResult) event).getExceptions()); 46 | } else if (isExecutionResult(event)) { 47 | activeNotifier.onEvent((MavenExecutionResult) event); 48 | } 49 | } 50 | } 51 | 52 | private boolean hasFailedWithoutProject(MavenExecutionResult event) { 53 | return event.getProject() == null && event.hasExceptions(); 54 | } 55 | 56 | private boolean shouldSendNotification() { 57 | return !"true".equalsIgnoreCase(System.getProperty(SKIP_NOTIFICATION)); 58 | } 59 | 60 | @Override 61 | public void close() throws Exception { 62 | activeNotifier.close(); 63 | } 64 | 65 | private boolean isExecutionResult(Object event) { 66 | return event instanceof MavenExecutionResult; 67 | } 68 | 69 | private void chooseNotifier(Configuration configuration) { 70 | for (Notifier notifier : availableNotifiers) { 71 | if (notifier.isCandidateFor(configuration.getImplementation())) { 72 | activeNotifier = notifier; 73 | logger.debug("Will notify build success/failure with: " + activeNotifier); 74 | return; 75 | } 76 | } 77 | 78 | if (activeNotifier == null) { 79 | activeNotifier = UselessNotifier.EMPTY; 80 | } 81 | } 82 | 83 | @VisibleForTesting 84 | void setAvailableNotifiers(List availableNotifiers) { 85 | this.availableNotifiers = availableNotifiers; 86 | } 87 | 88 | @VisibleForTesting 89 | void setConfigurationParser(ConfigurationParser configurationParser) { 90 | this.configurationParser = configurationParser; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/test/groovy/fr/jcgay/maven/notifier/ConfigurationParserTest.groovy: -------------------------------------------------------------------------------- 1 | package fr.jcgay.maven.notifier 2 | import org.codehaus.plexus.logging.Logger 3 | import org.mockito.InjectMocks 4 | import org.mockito.Mock 5 | import org.mockito.MockitoAnnotations 6 | import org.testng.annotations.BeforeMethod 7 | import org.testng.annotations.Test 8 | 9 | import static fr.jcgay.maven.notifier.ConfigurationParser.ConfigurationProperties.Property 10 | import static fr.jcgay.maven.notifier.ConfigurationParser.ConfigurationProperties.Property.IMPLEMENTATION 11 | import static org.assertj.core.api.Assertions.assertThat 12 | 13 | class ConfigurationParserTest { 14 | 15 | @InjectMocks 16 | private ConfigurationParser parser 17 | 18 | @Mock 19 | Logger logger 20 | 21 | @BeforeMethod 22 | void setUp() throws Exception { 23 | MockitoAnnotations.initMocks(this) 24 | System.clearProperty(Property.NOTIFY_WITH.key()) 25 | } 26 | 27 | @Test 28 | void 'should return default implementation configuration'() throws Exception { 29 | 30 | // Given 31 | Properties properties = new Properties() 32 | 33 | // When 34 | Configuration result = parser.get(properties) 35 | 36 | // Then 37 | assertThat result.getImplementation() isEqualTo 'send-notification' 38 | } 39 | 40 | @Test 41 | void 'should return default configuration'() throws Exception { 42 | 43 | Configuration result = parser.get(new Properties()); 44 | 45 | assertThat result.isShortDescription() isTrue() 46 | assertThat result.threshold isEqualTo(-1) 47 | assertThat result.timeout isEqualTo(-1) 48 | } 49 | 50 | @Test 51 | void 'should return configuration'() { 52 | 53 | Properties properties = new Properties(); 54 | properties << [(IMPLEMENTATION.key()):('test')] 55 | properties << [(Property.SHORT_DESCRIPTION.key()):('false')] 56 | properties << [(Property.THRESHOLD.key()):('10')] 57 | properties << [(Property.TIMEOUT.key()):('20')] 58 | 59 | Configuration result = parser.get(properties) 60 | 61 | assertThat result.getImplementation() isEqualTo 'test' 62 | assertThat result.isShortDescription() isFalse() 63 | assertThat result.threshold isEqualTo(10) 64 | assertThat result.timeout isEqualTo(20) 65 | } 66 | 67 | @Test 68 | void 'should not override implementation with property when no property is set'() throws Exception { 69 | 70 | def result = parser.get(this.getClass().getResource('/implementation.properties')).notifierProperties 71 | 72 | assertThat result[IMPLEMENTATION.key()] isEqualTo 'growl' 73 | } 74 | 75 | @Test 76 | void 'should override implementation with system property'() throws Exception { 77 | 78 | System.setProperty(Property.NOTIFY_WITH.key(), 'override-implementation') 79 | 80 | def result = parser.get(getClass().getResource('/implementation.properties')).notifierProperties 81 | 82 | assertThat result[IMPLEMENTATION.key()] isEqualTo 'override-implementation' 83 | } 84 | 85 | @Test 86 | void 'should override implementation with system property when configuration file is not found'() throws Exception { 87 | 88 | System.setProperty(Property.NOTIFY_WITH.key(), 'override-implementation') 89 | 90 | def result = parser.get(new URL('file:///non-existing.properties')).notifierProperties 91 | 92 | assertThat result[IMPLEMENTATION.key()] isEqualTo 'override-implementation' 93 | } 94 | 95 | @Test 96 | void 'should overwrite global configuration with user one'() { 97 | 98 | def result = parser.get( 99 | getClass().getResource('/implementation.properties'), 100 | getClass().getResource('/implementation-user.properties') 101 | ).notifierProperties 102 | 103 | assertThat(result[IMPLEMENTATION.key()]).isEqualTo('snarl') 104 | } 105 | 106 | @Test 107 | void 'should use configuration passed by system property'() { 108 | 109 | System.setProperty('notifier.anybar.port', '1111') 110 | System.setProperty('notifier.anybar.host', 'localhost') 111 | 112 | def result = parser.get(getClass().getResource('/anybar.properties')).notifierProperties 113 | 114 | assertThat result['notifier.anybar.port'] isEqualTo '1111' 115 | assertThat result['notifier.anybar.host'] isEqualTo 'localhost' 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Maven Notifier 2 | 3 | Notifiers that can be used with Maven 3.x. 4 | A status notification will be send at the end of a Maven build. 5 | 6 | ## Installation 7 | 8 | `$M2_HOME` refers to maven installation folder. 9 | 10 | ``` 11 | . 12 | ├── bin 13 | ├── boot 14 | ├── conf 15 | └── lib 16 | ``` 17 | 18 | ### OS X ? 19 | 20 | You can install a pre-packaged maven named [maven-deluxe](https://github.com/jcgay/homebrew-jcgay#maven-deluxe) using `brew`. 21 | It comes with [maven-color](https://github.com/jcgay/maven-color), [maven-notifier](https://github.com/jcgay/maven-notifier) and [maven-profiler](https://github.com/jcgay/maven-profiler). 22 | It is based on latest maven release. 23 | 24 | brew tap jcgay/jcgay 25 | brew install maven-deluxe 26 | 27 | ### Maven >= 3.3.x 28 | 29 | Get [maven-notifier](https://repo1.maven.org/maven2/fr/jcgay/maven/maven-notifier/2.1.2/maven-notifier-2.1.2.jar) and copy it in `%M2_HOME%/lib/ext` folder. 30 | 31 | *or* 32 | 33 | Use the [core extensions configuration mechanism](http://takari.io/2015/03/19/core-extensions.html) by creating a `${maven.multiModuleProjectDirectory}/.mvn/extensions.xml` file with: 34 | 35 | ```xml 36 | 37 | 38 | 39 | fr.jcgay.maven 40 | maven-notifier 41 | 2.1.2 42 | 43 | 44 | ``` 45 | 46 | ### Maven >= 3.1 47 | 48 | Get [maven-notifier](https://repo1.maven.org/maven2/fr/jcgay/maven/maven-notifier/2.1.2/maven-notifier-2.1.2.jar) and copy it in your `$M2_HOME/lib/ext` folder. 49 | 50 | ### Maven < 3.1 51 | 52 | Get [maven-notifier](https://repo1.maven.org/maven2/fr/jcgay/maven/maven-notifier/2.1.2/maven-notifier-2.1.2.zip) and extract it in your `$M2_HOME/lib/ext` folder. 53 | 54 | ## What's new ? 55 | 56 | See [CHANGELOG](https://github.com/jcgay/maven-notifier/blob/master/CHANGELOG.md) to get latest changes. 57 | 58 | ## Notifiers 59 | 60 | | Notifier | Screenshot | 61 | |:--------:|-----------------| 62 | | **Growl**, for [Windows](http://www.growlforwindows.com/gfw/) and [OS X](http://growl.info/). | ![Growl](http://jeanchristophegay.com/images/notifier.growl_.success.png) | 63 | | **[Snarl](http://snarl.fullphat.net/)**, for Windows | ![Snarl](http://jeanchristophegay.com/images/notifier.snarl.success.png) | 64 | | **[terminal-notifier](https://github.com/alloy/terminal-notifier)**, OS X | ![terminal-notifier](http://jeanchristophegay.com/images/notifier.notification-center.success.png) | 65 | | **notification center** OS X (since Mavericks) | ![notification-center](http://jeanchristophegay.com/images/notifier.simplenc.thumbnail.png) | 66 | | **notify-send** for Linux | ![notify-send](http://jeanchristophegay.com/images/notifier.notify-send.success.png) | 67 | | **SystemTray** since Java 6 | ![System Tray](http://jeanchristophegay.com/images/notifier.system.tray_.success.png) | 68 | | **[Pushbullet](https://www.pushbullet.com/)** | ![pushbullet](http://jeanchristophegay.com/images/notifier.pushbullet.success.png) | 69 | | **Kdialog** for KDE | ![Kdialog](http://jeanchristophegay.com/images/notifier.kdialog.fail.png) | 70 | | **[notifu](http://www.paralint.com/projects/notifu/index.html)** for Windows | ![notifu](http://jeanchristophegay.com/images/notifier.notifu.success.png) | 71 | | **AnyBar** for [OS X](https://github.com/tonsky/AnyBar) and [Linux](https://github.com/limpbrains/somebar) | ![anybar](http://jeanchristophegay.com/images/notifier.anybar_maven.png) | 72 | | **[Toaster](https://github.com/nels-o/toaster)** for Windows 8 | ![Toaster](http://jeanchristophegay.com/images/notifier.toaster.success.png) | 73 | | **[Notify](https://github.com/dorkbox/Notify)** since Java 6 | ![Notify](http://jeanchristophegay.com/images/notifier.notify.png) | 74 | | **[BurntToast](https://github.com/Windos/BurntToast)** for Windows 10 | ![BurntToast](http://jeanchristophegay.com/images/notifier.burnttoast.png) | 75 | | **[Slack](https://slack.com)** | ![Slack](http://jeanchristophegay.com/images/slack-success.png) | 76 | 77 | ### Sound 78 | 79 | Play a success or failure sound when build ends. 80 | 81 | ## Configuration 82 | 83 | Go to [Wiki](https://github.com/jcgay/maven-notifier/wiki) to read full configuration guide for each notifier. 84 | 85 | # Build status 86 | [![Build Status](https://github.com/jcgay/maven-notifier/actions/workflows/maven.yml/badge.svg)](https://github.com/jcgay/maven-notifier/actions/workflows/maven.yml) 87 | [![Coverage Status](https://coveralls.io/repos/jcgay/maven-notifier/badge.svg?branch=master)](https://coveralls.io/r/jcgay/maven-notifier?branch=master) 88 | [![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=fr.jcgay.maven%3Amaven-notifier&metric=alert_status)](https://sonarcloud.io/dashboard?id=fr.jcgay.maven%3Amaven-notifier) 89 | [![Technical debt ratio](https://sonarcloud.io/api/project_badges/measure?project=fr.jcgay.maven%3Amaven-notifier&metric=sqale_index)](https://sonarcloud.io/dashboard?id=fr.jcgay.maven%3Amaven-notifier) 90 | 91 | # Release 92 | 93 | mvn -B release:prepare release:perform 94 | -------------------------------------------------------------------------------- /src/test/groovy/fr/jcgay/maven/notifier/NotificationEventSpyChooserTest.groovy: -------------------------------------------------------------------------------- 1 | package fr.jcgay.maven.notifier 2 | 3 | import org.apache.maven.execution.DefaultMavenExecutionResult 4 | import org.apache.maven.execution.MavenExecutionResult 5 | import org.codehaus.plexus.logging.Logger 6 | import org.mockito.ArgumentCaptor 7 | import org.mockito.InjectMocks 8 | import org.mockito.Mock 9 | import org.mockito.MockitoAnnotations 10 | import org.testng.annotations.BeforeMethod 11 | import org.testng.annotations.Test 12 | 13 | import static fr.jcgay.maven.notifier.NotificationEventSpyChooser.SKIP_NOTIFICATION 14 | import static org.assertj.core.api.Assertions.assertThat 15 | import static org.assertj.core.api.Assertions.entry 16 | import static org.mockito.Matchers.any 17 | import static org.mockito.Mockito.mock 18 | import static org.mockito.Mockito.never 19 | import static org.mockito.Mockito.verify 20 | import static org.mockito.Mockito.when 21 | 22 | class NotificationEventSpyChooserTest { 23 | 24 | @InjectMocks 25 | private NotificationEventSpyChooser chooser 26 | 27 | @Mock 28 | private Notifier notifier 29 | @Mock 30 | private Notifier unexpectedNotifier 31 | @Mock 32 | private MavenExecutionResult anEvent 33 | @Mock 34 | private Logger logger 35 | 36 | private Configuration configuration 37 | 38 | @BeforeMethod 39 | void setUp() throws Exception { 40 | def configurationParser = mock(ConfigurationParser.class) 41 | configuration = new Configuration() 42 | configuration.setImplementation("anything") 43 | when configurationParser.get() thenReturn this.configuration 44 | 45 | chooser = new NotificationEventSpyChooser() 46 | chooser.setConfigurationParser(configurationParser) 47 | MockitoAnnotations.initMocks(this) 48 | 49 | System.setProperty(SKIP_NOTIFICATION, String.valueOf(false)) 50 | 51 | when unexpectedNotifier.isCandidateFor("anything") thenReturn false 52 | 53 | when notifier.isCandidateFor("anything") thenReturn true 54 | chooser.availableNotifiers = [notifier] 55 | } 56 | 57 | @Test 58 | void 'should not notify if event is not a build result'() throws Exception { 59 | chooser.init({ [:] }) 60 | chooser.onEvent('this is not a build result') 61 | chooser.close() 62 | 63 | verify(notifier, never()).onEvent(any(MavenExecutionResult)) 64 | } 65 | 66 | @Test 67 | void 'should not notify when property skipNotification is true'() throws Exception { 68 | System.setProperty(SKIP_NOTIFICATION, String.valueOf(true)) 69 | 70 | chooser.init({ [:] }) 71 | chooser.onEvent(anEvent) 72 | chooser.close() 73 | 74 | verify(notifier, never()).onEvent(any(MavenExecutionResult)) 75 | } 76 | 77 | @Test 78 | void 'should notify when property skipNotification is false'() throws Exception { 79 | System.setProperty(SKIP_NOTIFICATION, String.valueOf(false)) 80 | 81 | chooser.init({ [:] }) 82 | chooser.onEvent(anEvent) 83 | chooser.close() 84 | 85 | verify(notifier).onEvent(anEvent) 86 | } 87 | 88 | @Test 89 | void 'should notify failure when build fails without project'() throws Exception { 90 | DefaultMavenExecutionResult event = new DefaultMavenExecutionResult() 91 | event.project = null 92 | event.addException(new NullPointerException()) 93 | 94 | chooser.init({ [:] }) 95 | chooser.onEvent(event) 96 | chooser.close() 97 | 98 | verify(notifier).onFailWithoutProject(event.getExceptions()) 99 | verify(notifier, never()).onEvent(event) 100 | } 101 | 102 | @Test 103 | void 'should send notification with configured notifier'() throws Exception { 104 | chooser.availableNotifiers = [unexpectedNotifier, notifier] 105 | 106 | chooser.init({ [:] }) 107 | chooser.onEvent(anEvent) 108 | chooser.close() 109 | 110 | verify(notifier).onEvent(anEvent) 111 | } 112 | 113 | @Test 114 | void 'should not fail when no notifier is configured'() throws Exception { 115 | chooser.availableNotifiers = [unexpectedNotifier] 116 | 117 | chooser.init({ [:] }) 118 | chooser.onEvent(anEvent) 119 | chooser.close() 120 | 121 | verify(unexpectedNotifier, never()).onEvent(any(MavenExecutionResult)) 122 | } 123 | 124 | @Test 125 | void 'should close notifier'() throws Exception { 126 | chooser.init({ [:] }) 127 | chooser.onEvent(anEvent) 128 | chooser.close() 129 | 130 | verify(notifier).close() 131 | } 132 | 133 | @Test 134 | void 'should forward configuration to notifier'() { 135 | def contextCaptor = ArgumentCaptor.forClass(FakeContext) 136 | 137 | chooser.init(new FakeContext()) 138 | 139 | verify(notifier).init(contextCaptor.capture()) 140 | assertThat(contextCaptor.value.data).contains(entry('notifier.configuration', configuration)) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/main/java/fr/jcgay/maven/notifier/sendnotification/SendNotificationNotifier.java: -------------------------------------------------------------------------------- 1 | package fr.jcgay.maven.notifier.sendnotification; 2 | 3 | import com.google.common.annotations.VisibleForTesting; 4 | import fr.jcgay.maven.notifier.AbstractNotifier; 5 | import fr.jcgay.maven.notifier.Configuration; 6 | import fr.jcgay.maven.notifier.Notifier; 7 | import fr.jcgay.maven.notifier.Status; 8 | import fr.jcgay.notification.Application; 9 | import fr.jcgay.notification.Icon; 10 | import fr.jcgay.notification.Notification; 11 | import fr.jcgay.notification.SendNotification; 12 | import org.apache.maven.execution.BuildSummary; 13 | import org.apache.maven.execution.MavenExecutionResult; 14 | import org.apache.maven.project.MavenProject; 15 | import org.codehaus.plexus.component.annotations.Component; 16 | 17 | import java.net.URL; 18 | import java.util.List; 19 | import java.util.concurrent.TimeUnit; 20 | 21 | import static fr.jcgay.notification.Notification.Level.ERROR; 22 | import static fr.jcgay.notification.Notification.Level.INFO; 23 | import static fr.jcgay.notification.Notification.Level.WARNING; 24 | 25 | @Component(role = Notifier.class, hint = "send-notification") 26 | public class SendNotificationNotifier extends AbstractNotifier { 27 | 28 | private static final Icon ICON = Icon.create(resource("maven.png"), "maven"); 29 | private static final String LINE_BREAK = System.getProperty("line.separator"); 30 | 31 | private fr.jcgay.notification.Notifier notifier; 32 | 33 | public SendNotificationNotifier() { 34 | } 35 | 36 | @VisibleForTesting 37 | SendNotificationNotifier(fr.jcgay.notification.Notifier notifier) { 38 | this.notifier = notifier; 39 | } 40 | 41 | @Override 42 | protected void initNotifier() { 43 | this.notifier = new SendNotification() 44 | .setApplication(Application.builder("application/x-vnd-apache.maven", "Maven", ICON).timeout(configuration.getTimeout()).build()) 45 | .addConfigurationProperties(configuration.getNotifierProperties()) 46 | .initNotifier(); 47 | } 48 | 49 | @Override 50 | public void closeNotifier() { 51 | notifier.close(); 52 | } 53 | 54 | @Override 55 | public boolean isCandidateFor(String desiredImplementation) { 56 | return !"sound".equals(desiredImplementation); 57 | } 58 | 59 | @Override 60 | protected void fireNotification(MavenExecutionResult event) { 61 | Status status = getBuildStatus(event); 62 | notifier.send( 63 | Notification.builder() 64 | .title(buildTitle(event)) 65 | .message(buildNotificationMessage(event)) 66 | .icon(Icon.create(status.url(), status.name())) 67 | .level(toLevel(status)) 68 | .subtitle(status.message()) 69 | .build() 70 | ); 71 | } 72 | 73 | @Override 74 | protected boolean isPersistent() { 75 | return notifier.isPersistent(); 76 | } 77 | 78 | @Override 79 | public void onFailWithoutProject(List exceptions) { 80 | super.onFailWithoutProject(exceptions); 81 | Status status = Status.FAILURE; 82 | notifier.send( 83 | Notification.builder() 84 | .title("Build Error") 85 | .message(buildErrorDescription(exceptions)) 86 | .icon(Icon.create(status.url(), status.name())) 87 | .subtitle(status.message()) 88 | .level(ERROR) 89 | .build() 90 | ); 91 | } 92 | 93 | private static Notification.Level toLevel(Status status) { 94 | switch (status) { 95 | case SKIPPED: 96 | return WARNING; 97 | case FAILURE: 98 | return ERROR; 99 | default: 100 | return INFO; 101 | } 102 | } 103 | 104 | private static URL resource(String resource) { 105 | return SendNotificationNotifier.class.getClassLoader().getResource(resource); 106 | } 107 | 108 | protected String buildNotificationMessage(MavenExecutionResult result) { 109 | if (shouldBuildShortDescription(result)) { 110 | return buildShortDescription(result); 111 | } 112 | return buildFullDescription(result); 113 | } 114 | 115 | private String buildFullDescription(MavenExecutionResult result) { 116 | StringBuilder builder = new StringBuilder(); 117 | for (MavenProject project : result.getTopologicallySortedProjects()) { 118 | BuildSummary buildSummary = result.getBuildSummary(project); 119 | Status status = Status.of(buildSummary); 120 | builder.append(project.getName()); 121 | builder.append(": "); 122 | builder.append(status.message()); 123 | if (status != Status.SKIPPED) { 124 | builder.append(" ["); 125 | builder.append(TimeUnit.MILLISECONDS.toSeconds(buildSummary.getTime())); 126 | builder.append("s] "); 127 | } 128 | builder.append(LINE_BREAK); 129 | } 130 | return builder.toString(); 131 | } 132 | 133 | private String buildShortDescription(MavenExecutionResult result) { 134 | switch (getBuildStatus(result)) { 135 | case SUCCESS: 136 | return "Built in: " + elapsedTime() + " second(s)."; 137 | case FAILURE: 138 | return "Build Failed."; 139 | default: 140 | return "..."; 141 | } 142 | } 143 | 144 | private boolean shouldBuildShortDescription(MavenExecutionResult result) { 145 | return configuration.isShortDescription() || hasOnlyOneModule(result); 146 | } 147 | 148 | private boolean hasOnlyOneModule(MavenExecutionResult result) { 149 | return result.getTopologicallySortedProjects().size() == 1; 150 | } 151 | 152 | protected String buildTitle(MavenExecutionResult result) { 153 | if (shouldBuildShortDescription(result)) { 154 | return buildShortTitle(result); 155 | } 156 | return buildFullTitle(result); 157 | } 158 | 159 | private String buildFullTitle(MavenExecutionResult result) { 160 | return result.getProject().getName() + " [" + elapsedTime() + "s]"; 161 | } 162 | 163 | private String buildShortTitle(MavenExecutionResult result) { 164 | return result.getProject().getName(); 165 | } 166 | 167 | protected String buildErrorDescription(List exceptions) { 168 | StringBuilder builder = new StringBuilder(); 169 | for (Throwable exception : exceptions) { 170 | builder.append(exception.getMessage()); 171 | builder.append(LINE_BREAK); 172 | } 173 | return builder.toString(); 174 | } 175 | 176 | @VisibleForTesting 177 | Configuration getConfiguration() { 178 | return configuration; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/test/groovy/fr/jcgay/maven/notifier/sendnotification/SendNotificationNotifierTest.groovy: -------------------------------------------------------------------------------- 1 | package fr.jcgay.maven.notifier.sendnotification 2 | 3 | import fr.jcgay.maven.notifier.Configuration 4 | import fr.jcgay.notification.Notification 5 | import fr.jcgay.notification.Notifier 6 | import groovy.transform.CompileStatic 7 | import org.codehaus.plexus.logging.Logger 8 | import org.mockito.ArgumentCaptor 9 | import org.mockito.Captor 10 | import org.mockito.Mock 11 | import org.testng.annotations.BeforeMethod 12 | import org.testng.annotations.Test 13 | 14 | import static fr.jcgay.maven.notifier.Fixtures.* 15 | import static fr.jcgay.maven.notifier.KnownElapsedTimeTicker.aStartedStopwatchWithElapsedTime 16 | import static fr.jcgay.notification.Notification.Level.ERROR 17 | import static fr.jcgay.notification.Notification.Level.INFO 18 | import static java.util.concurrent.TimeUnit.SECONDS 19 | import static org.assertj.core.api.Assertions.assertThat 20 | import static org.mockito.ArgumentMatchers.any 21 | import static org.mockito.Mockito.* 22 | import static org.mockito.MockitoAnnotations.initMocks 23 | 24 | @CompileStatic 25 | class SendNotificationNotifierTest { 26 | 27 | @Mock 28 | private Notifier notifier 29 | 30 | @Captor 31 | private ArgumentCaptor notification 32 | 33 | private Configuration configuration 34 | 35 | private SendNotificationNotifier underTest 36 | 37 | @BeforeMethod 38 | void setUp() throws Exception { 39 | initMocks this 40 | 41 | configuration = new Configuration() 42 | 43 | underTest = new SendNotificationNotifier(notifier) 44 | underTest.logger = mock(Logger) 45 | underTest.init(skipSendNotificationInit(configuration)) 46 | } 47 | 48 | @Test 49 | void 'should call close when exiting notifier'() { 50 | underTest.close() 51 | 52 | verify(notifier).close() 53 | } 54 | 55 | @Test 56 | void 'should send notification when an event is triggered'() { 57 | underTest.stopwatch = aStartedStopwatchWithElapsedTime(SECONDS.toNanos(2L)) 58 | 59 | underTest.onEvent(anEvent('title')) 60 | 61 | verify(notifier).send(notification.capture()) 62 | assertThat notification.value.title() isEqualTo 'title [2s]' 63 | } 64 | 65 | @Test 66 | void 'should send notification for a multi module project'() { 67 | underTest.stopwatch = aStartedStopwatchWithElapsedTime(SECONDS.toNanos(3L)) 68 | 69 | underTest.onEvent(aProjectWithMultipleModule('project-multimodule', 70 | aModule('module-1', SECONDS.toMillis(1L)), aModule('module-2', SECONDS.toMillis(2L)))) 71 | 72 | verify(notifier).send(notification.capture()) 73 | assertThat notification.value.title() isEqualTo 'project-multimodule [3s]' 74 | assertThat notification.value.message() isEqualTo String.format('module-1: Success [1s] %nmodule-2: Success [2s] %n') 75 | } 76 | 77 | @Test 78 | void 'should send notification with short message when project has only one module'() { 79 | underTest.stopwatch = aStartedStopwatchWithElapsedTime(SECONDS.toNanos(1L)) 80 | 81 | underTest.onEvent(aProjectWithOneModule('title')) 82 | 83 | verify(notifier).send(notification.capture()) 84 | assertThat notification.value.title() isEqualTo 'title' 85 | assertThat notification.value.message() isEqualTo 'Built in: 1 second(s).' 86 | } 87 | 88 | @Test 89 | void 'should send notification with short message when project has only one module and fails'() { 90 | underTest.stopwatch = aStartedStopwatchWithElapsedTime(SECONDS.toNanos(1L)) 91 | 92 | underTest.onEvent(aFailingProject()) 93 | 94 | verify(notifier).send(notification.capture()) 95 | assertThat notification.value.title() isEqualTo 'project' 96 | assertThat notification.value.message() isEqualTo 'Build Failed.' 97 | } 98 | 99 | @Test 100 | void 'should send notification with short message when configuration is set to be short'() { 101 | underTest.stopwatch = aStartedStopwatchWithElapsedTime(SECONDS.toNanos(1L)) 102 | underTest.configuration.shortDescription = true 103 | 104 | underTest.onEvent(aProjectWithMultipleModule('title')) 105 | 106 | verify(notifier).send(notification.capture()) 107 | assertThat notification.value.title() isEqualTo 'title' 108 | assertThat notification.value.message() isEqualTo 'Built in: 1 second(s).' 109 | } 110 | 111 | @Test 112 | void 'should use notifier when implementation is not sound'() { 113 | assertThat underTest.isCandidateFor('growl') isTrue() 114 | } 115 | 116 | @Test 117 | void 'should not use notifier when implementation is sound'() { 118 | assertThat underTest.isCandidateFor('sound') isFalse() 119 | } 120 | 121 | @Test 122 | void 'should send a notification with level ERROR when build is failing'() throws Exception { 123 | underTest.stopwatch = aStartedStopwatchWithElapsedTime(SECONDS.toNanos(1L)) 124 | 125 | underTest.onEvent(aFailingProject()) 126 | 127 | verify(notifier).send(notification.capture()) 128 | assertThat notification.value.level() isEqualTo ERROR 129 | } 130 | 131 | @Test 132 | void 'should send a notification with level INFO when build is successful'() throws Exception { 133 | underTest.stopwatch = aStartedStopwatchWithElapsedTime(SECONDS.toNanos(1L)) 134 | 135 | underTest.onEvent(aSuccessfulProject()) 136 | 137 | verify(notifier).send(notification.capture()) 138 | assertThat notification.value.level() isEqualTo INFO 139 | } 140 | 141 | @Test 142 | void 'should send a notification with level ERROR when build is misconfigured'() throws Exception { 143 | underTest.onFailWithoutProject([new Throwable("error")]) 144 | 145 | verify(notifier).send(notification.capture()) 146 | 147 | def result = notification.value 148 | assertThat result.level() isEqualTo ERROR 149 | assertThat result.message() contains 'error' 150 | assertThat result.title() isEqualTo 'Build Error' 151 | } 152 | 153 | @Test 154 | void 'should always send notification when notifier is persistent even if threshold is passed'() { 155 | underTest.stopwatch = aStartedStopwatchWithElapsedTime(SECONDS.toNanos(1L)) 156 | configuration.setThreshold(10) 157 | when(notifier.isPersistent()).thenReturn(true) 158 | 159 | underTest.onEvent(aSuccessfulProject()) 160 | 161 | verify(notifier).send(any(Notification.class)) 162 | } 163 | 164 | @Test 165 | void 'should not send notification when notifier is not persistent and threshold is passed'() { 166 | underTest.stopwatch = aStartedStopwatchWithElapsedTime(SECONDS.toNanos(1L)) 167 | configuration.setThreshold(10) 168 | when(notifier.isPersistent()).thenReturn(false) 169 | 170 | underTest.onEvent(aSuccessfulProject()) 171 | 172 | verify(notifier, never()).send(any(Notification.class)) 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/main/java/fr/jcgay/maven/notifier/ConfigurationParser.java: -------------------------------------------------------------------------------- 1 | package fr.jcgay.maven.notifier; 2 | 3 | import com.google.common.annotations.VisibleForTesting; 4 | import org.codehaus.plexus.component.annotations.Component; 5 | import org.codehaus.plexus.component.annotations.Requirement; 6 | import org.codehaus.plexus.logging.Logger; 7 | 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.net.MalformedURLException; 11 | import java.net.URL; 12 | import java.util.Map; 13 | import java.util.Properties; 14 | 15 | import static fr.jcgay.maven.notifier.ConfigurationParser.ConfigurationProperties.Property.IMPLEMENTATION; 16 | import static fr.jcgay.maven.notifier.ConfigurationParser.ConfigurationProperties.Property.NOTIFY_WITH; 17 | import static fr.jcgay.maven.notifier.ConfigurationParser.ConfigurationProperties.Property.SHORT_DESCRIPTION; 18 | import static fr.jcgay.maven.notifier.ConfigurationParser.ConfigurationProperties.Property.THRESHOLD; 19 | import static fr.jcgay.maven.notifier.ConfigurationParser.ConfigurationProperties.Property.TIMEOUT; 20 | import static java.lang.Boolean.parseBoolean; 21 | 22 | @Component(role = ConfigurationParser.class, hint = "maven-notifier-configuration") 23 | public class ConfigurationParser { 24 | 25 | @Requirement 26 | private Logger logger; 27 | 28 | public ConfigurationParser() { 29 | } 30 | 31 | public ConfigurationParser(Logger logger) { 32 | this.logger = logger; 33 | } 34 | 35 | public Configuration get() { 36 | return get(globalConfiguration(), userConfiguration()); 37 | } 38 | 39 | public Configuration get(URL... urls) { 40 | return get(readProperties(urls)); 41 | } 42 | 43 | @VisibleForTesting Configuration get(Properties properties) { 44 | Configuration configuration = parse(new ConfigurationProperties(properties)); 45 | logger.debug("maven-notifier user configuration: " + configuration); 46 | return configuration; 47 | } 48 | 49 | private Properties readProperties(URL... urls) { 50 | return new ConfiguredProperties(logger) 51 | .load(urls) 52 | .properties(); 53 | } 54 | 55 | private URL globalConfiguration() { 56 | try { 57 | URL url = new URL(ConfigurationParser.class.getProtectionDomain().getCodeSource().getLocation(), "maven-notifier.properties"); 58 | logger.debug("Global configuration is located at: " + url); 59 | return url; 60 | } catch (MalformedURLException e) { 61 | return null; 62 | } 63 | } 64 | 65 | private URL userConfiguration() { 66 | try { 67 | URL url = new URL("file:///" + System.getProperty("user.home") + "/.m2/maven-notifier.properties"); 68 | logger.debug("User specific configuration is located at: " + url); 69 | return url; 70 | } catch (MalformedURLException e) { 71 | return null; 72 | } 73 | } 74 | 75 | private Configuration parse(ConfigurationProperties properties) { 76 | Configuration configuration = new Configuration(); 77 | configuration.setImplementation(properties.get(IMPLEMENTATION)); 78 | configuration.setShortDescription(parseBoolean(properties.get(SHORT_DESCRIPTION))); 79 | configuration.setThreshold(Integer.valueOf(properties.get(THRESHOLD))); 80 | configuration.setTimeout(Integer.valueOf(properties.get(TIMEOUT))); 81 | configuration.setNotifierProperties(properties.all()); 82 | return configuration; 83 | } 84 | 85 | public static class ConfigurationProperties { 86 | 87 | private final Properties properties; 88 | 89 | private ConfigurationProperties(Properties properties) { 90 | this.properties = properties; 91 | } 92 | 93 | public String get(Property property) { 94 | switch (property) { 95 | case IMPLEMENTATION: 96 | return properties.getProperty(property.key(), "send-notification"); 97 | default: 98 | return properties.getProperty(property.key(), property.defaultValue()); 99 | } 100 | } 101 | 102 | public Properties all() { 103 | return properties; 104 | } 105 | 106 | public enum Property { 107 | IMPLEMENTATION("notifier.implementation"), 108 | SHORT_DESCRIPTION("notifier.message.short", "true"), 109 | NOTIFY_WITH("notifyWith"), 110 | THRESHOLD("notifier.threshold", "-1"), 111 | TIMEOUT("notifier.timeout", "-1"); 112 | 113 | private final String key; 114 | private String defaultValue; 115 | 116 | Property(String key) { 117 | this.key = key; 118 | } 119 | 120 | Property(String key, String defaultValue) { 121 | this.key = key; 122 | this.defaultValue = defaultValue; 123 | } 124 | 125 | public String key() { 126 | return key; 127 | } 128 | 129 | public String defaultValue() { 130 | return defaultValue; 131 | } 132 | } 133 | } 134 | 135 | private static class ConfiguredProperties { 136 | 137 | private final Logger logger; 138 | private final Properties properties = new Properties(); 139 | 140 | public ConfiguredProperties(Logger logger) { 141 | this.logger = logger; 142 | } 143 | 144 | public Properties properties() { 145 | Properties result = new Properties(); 146 | result.putAll(properties); 147 | 148 | for (Map.Entry property : System.getProperties().entrySet()) { 149 | if (property.getKey().toString().startsWith("notifier.")) { 150 | result.put(property.getKey(), property.getValue()); 151 | } 152 | } 153 | 154 | String overrideImplementation = System.getProperty(NOTIFY_WITH.key()); 155 | if (overrideImplementation != null) { 156 | logger.debug("Overriding configured notifier with: " + overrideImplementation); 157 | result.put(IMPLEMENTATION.key(), overrideImplementation); 158 | } 159 | return result; 160 | } 161 | 162 | public ConfiguredProperties load(URL... urls) { 163 | for (URL url : urls) { 164 | if (url != null) { 165 | try (InputStream in = url.openStream()) { 166 | properties.load(in); 167 | logger.debug("Properties after loading [" + url + "]: " + properties); 168 | } catch (IOException e) { 169 | // cannot read configuration file (which is not mandatory) 170 | logger.debug("Can't read file at [" + url + "]. Skipping it...", e); 171 | } 172 | } 173 | } 174 | return this; 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | fr.jcgay.maven 7 | jcgay-build-configuration 8 | 1.18 9 | 10 | 11 | maven-notifier 12 | 2.1.3-SNAPSHOT 13 | 14 | 15 | https://github.com/jcgay/maven-notifier.git 16 | scm:git:git://github.com/jcgay/maven-notifier.git 17 | scm:git:git@github.com:jcgay/maven-notifier.git 18 | HEAD 19 | 20 | 21 | 22 | 1.8 23 | 1.8 24 | 1.7.32 25 | 26 | 27 | 28 | 29 | org.apache.maven 30 | maven-core 31 | 3.0.5 32 | 33 | 34 | org.sonatype.sisu 35 | sisu-guava 36 | 37 | 38 | 39 | 40 | org.slf4j 41 | slf4j-api 42 | ${slf4j-version} 43 | 44 | 45 | fr.jcgay.send-notification 46 | send-notification 47 | 0.16.0 48 | 49 | 50 | org.testng 51 | testng 52 | 7.4.0 53 | test 54 | 55 | 56 | org.mockito 57 | mockito-core 58 | 3.12.0 59 | test 60 | 61 | 62 | ch.qos.logback 63 | logback-classic 64 | 1.2.5 65 | test 66 | 67 | 68 | org.codehaus.groovy 69 | groovy 70 | 3.0.8 71 | test 72 | 73 | 74 | org.assertj 75 | assertj-core 76 | 3.20.2 77 | test 78 | 79 | 80 | 81 | 82 | 83 | 84 | src/main/resources 85 | 86 | 87 | src/main/properties 88 | true 89 | 90 | 91 | 92 | 93 | maven-shade-plugin 94 | 95 | 96 | package 97 | 98 | shade 99 | 100 | 101 | 102 | 103 | org.apache.maven 104 | org.sonatype.* 105 | org.codehaus.plexus 106 | org.slf4j 107 | 108 | 109 | 110 | 111 | com.google.common 112 | com.shaded.notifier.google.common 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | maven-dependency-plugin 121 | 122 | 123 | get-logging-binding 124 | prepare-package 125 | 126 | copy 127 | 128 | 129 | 130 | 131 | org.slf4j 132 | slf4j-nop 133 | ${slf4j-version} 134 | jar 135 | true 136 | ${project.build.directory} 137 | slf4j-nop-${slf4j-version}.jar 138 | 139 | 140 | org.slf4j 141 | slf4j-api 142 | ${slf4j-version} 143 | jar 144 | true 145 | ${project.build.directory} 146 | slf4j-api-${slf4j-version}.jar 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | maven-assembly-plugin 155 | 156 | false 157 | 158 | src/main/assembly/jar-with-logging.xml 159 | 160 | 161 | 162 | 163 | make-assembly 164 | package 165 | 166 | single 167 | 168 | 169 | 170 | 171 | 172 | maven-source-plugin 173 | 174 | 175 | org.codehaus.gmavenplus 176 | gmavenplus-plugin 177 | 178 | 179 | 180 | compileTests 181 | 182 | 183 | 184 | 185 | 186 | org.jreleaser 187 | jreleaser-maven-plugin 188 | 0.6.0 189 | 190 | jreleaser.yml 191 | 192 | 193 | 194 | org.codehaus.plexus 195 | plexus-component-metadata 196 | 2.1.0 197 | 198 | 199 | 200 | generate-metadata 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | build-with-toolchains 211 | 212 | !1.8 213 | 214 | 215 | 216 | 217 | maven-toolchains-plugin 218 | 219 | 220 | 221 | toolchain 222 | 223 | 224 | 225 | 226 | 227 | 228 | ${maven.compiler.target} 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2.1.2 2 | *** 3 | 4 | - Fix sound not playing with SoundNotifier ([6b6b861](http://github.com/jcgay/maven-notifier/commit/6b6b8615376c94464a55360b8a9609e504b21c1d)) 5 | - Reset all state in EventSpy#init to works with mvnd ([db9c057](http://github.com/jcgay/maven-notifier/commit/db9c057417db976efb1aed30d55a526db83dbf5c)) 6 | 7 | # 2.1.1 8 | *** 9 | 10 | - Reset build timer when closing the EventSpy ([ac1433a](http://github.com/jcgay/maven-notifier/commit/ac1433a978a29ad4a56b36ef227b0d8e58b6a018)) 11 | - Do not close notifier with mvnd ([1de061c](http://github.com/jcgay/maven-notifier/commit/1de061cb1fa8d42896eb40f61e4fd5419aece2bd)) 12 | - Build with GitHub actions ([eea0d96](http://github.com/jcgay/maven-notifier/commit/eea0d960b35e740a56e3f3f890b72254536981d4)) 13 | 14 | # 2.1.0 15 | *** 16 | 17 | - Use send-notification 0.16.0 ([f1424e6](http://github.com/jcgay/maven-notifier/commit/f1424e6dee4ed299051f54400687b12e773cc362)) 18 | * Disable auto-detect for BurntToast ([03952d5](http://github.com/jcgay/send-notification/commit/03952d5d8fbbd776c10b4e5ecd2d4ed94d53456d)) 19 | 20 | # 2.0.0 21 | *** 22 | 23 | - Use send-notification 0.15.1 ([f1c6968](http://github.com/jcgay/maven-notifier/commit/f1c69685e95b927c5eaa16295d73445717a5b427)) 24 | * Log a warning when a configured notifier is not valid ([4e227b4](http://github.com/jcgay/send-notification/commit/4e227b440cd1c87518b35a41400892580e3afcb2)) 25 | * Do not set a default app activation for terminal-notifier ([61f142f](http://github.com/jcgay/send-notification/commit/61f142f0940542117a3494e034bb723797337faf)) 26 | * Use jPowerShell 3.0.4 [Fixes [#10](https://github.com/jcgay/send-notification/issues/10)] ([b67f9bd](http://github.com/jcgay/send-notification/commit/b67f9bda4e8290468328ab10b97fc3c5a1b1c37b)) 27 | * Removed no longer existing parameter for BurntNotificationToast '-AppId' [Fixes [#11](https://github.com/jcgay/send-notification/issues/11)] ([89c84e3](http://github.com/jcgay/send-notification/commit/89c84e3f0350c68163d8293c63134901df203d10)) 28 | - Set timeout when building send-notifier Application ([19f8521](http://github.com/jcgay/maven-notifier/commit/19f852161154627a82f42293e0d0df329cc641e1)) 29 | - Migrate to Java 8 ([6ca062f](http://github.com/jcgay/maven-notifier/commit/6ca062fc6018e0d67368a2fcd44900ae63f3289f)) 30 | - Log current maven-notifier version in DEBUG ([961ca93](http://github.com/jcgay/maven-notifier/commit/961ca9354c0b2c775d682370da327952c62ce907)) 31 | - Add a docker image with notify-send ([cebffa0](http://github.com/jcgay/maven-notifier/commit/cebffa03b90786e583edc6dc4a954ed83408355e)) 32 | 33 | # 1.10.1 34 | *** 35 | 36 | - Get compatible with Guava 18 ([ac3de41](http://github.com/jcgay/maven-notifier/commit/ac3de41c0934c2398f519752caae842213a7df20)) 37 | 38 | # 1.10.0 39 | *** 40 | 41 | - Use send-notification 0.11.0 ([b3217c4](http://github.com/jcgay/maven-notifier/commit/b3217c4542190be2303baafb0edfc067f5f36958)) 42 | * Add Slack notifier ([613e0af](http://github.com/jcgay/send-notification/commit/613e0af8ad444b89f231a26e36e800efef8f26e2)) 43 | * Add BurntToast notifier ([00af537](http://github.com/jcgay/send-notification/commit/00af5378207297374f8b9c42feb7ebd149a6498d)) 44 | * Add Notify notifier ([f6c190d](http://github.com/jcgay/send-notification/commit/f6c190dddb8160996ae84372b11bd20cb1fc8e5a)) 45 | 46 | # 1.9.1 47 | *** 48 | 49 | - Use send-notification 0.10.1 ([3e432dd](http://github.com/jcgay/maven-notifier/commit/3e432dd612db9c0dac2cdbf30afb8c2f1db8b0a4)) 50 | * Escape argument when executing notifu ([41358dd](http://github.com/jcgay/send-notification/commit/41358ddc20125d35996ebba5545c00e2b66ff31f)) 51 | 52 | # 1.9 53 | *** 54 | 55 | - Use send-notification 0.10-SNAPSHOT ([f387c10](http://github.com/jcgay/maven-notifier/commit/f387c10e23cec61849ea060230ee09b8e066be6b)) 56 | * Prevent dock icon creation on OS X ([f7ba636](http://github.com/jcgay/send-notification/commit/f7ba63631fe6e1c9f2bbad126164eeca1cf2d7b5)) 57 | 58 | # 1.8 59 | *** 60 | 61 | - Fix Windows user configuration loading ([9e4f933](http://github.com/jcgay/maven-notifier/commit/9e4f933e05c9478ca5efac2e60ddb89a5552aaa5)) 62 | - Better debug log messages ([9b10622](http://github.com/jcgay/maven-notifier/commit/9b106223427d7cd1237fe644b461a4abd5ce7e37)) 63 | - Default notifier implementation is chosen by send-notification ([a113649](http://github.com/jcgay/maven-notifier/commit/a113649dcf96e693b9fb14bc2143165572ef1097)) 64 | - Can launch multiple build with Snarl ([da0b38e](http://github.com/jcgay/maven-notifier/commit/da0b38eb7db12654003de7d0efc011a7f965ba78)) 65 | 66 | # 1.7 67 | *** 68 | 69 | - Use notifier 'none' to not send notifications ([23eb400](http://github.com/jcgay/maven-notifier/commit/23eb40006ad8637f65693e89c31e9ed35ba91366)) 70 | - Log Growl errors in debug when used in auto discovery mode ([23eb400](http://github.com/jcgay/maven-notifier/commit/23eb40006ad8637f65693e89c31e9ed35ba91366)) 71 | 72 | # 1.6 73 | *** 74 | 75 | - Always send notification when notifier is persistent ([d3a3d08](http://github.com/jcgay/maven-notifier/commit/d3a3d081401c862491c23aeb6108e1637bbb45de)) 76 | - Replace '...' by 'Build Failed' for short notification messages ([30310ae](http://github.com/jcgay/maven-notifier/commit/30310ae092f0d5313129ac57f8e6e41a18237e73)) 77 | - Can use multiple notifiers at once ([f8e0f90](http://github.com/jcgay/maven-notifier/commit/f8e0f90ae2d3a694a5cdeb4d6ef2fb541f3cb8e8)) 78 | 79 | # 1.5 80 | *** 81 | 82 | - Smarter default notifier ([cab405b](http://github.com/jcgay/maven-notifier/commit/cab405b6071dee5df41168d4e0f2388f32ec9970)) 83 | 84 | # 1.4 85 | *** 86 | 87 | - Add Toaster notifier ([97b395b](http://github.com/jcgay/maven-notifier/commit/97b395b1dd94c894e26c79ce5f8e0f3593436aac)) 88 | - Add notification center (with AppleScript) notifier ([97b395b](http://github.com/jcgay/maven-notifier/commit/97b395b1dd94c894e26c79ce5f8e0f3593436aac)) 89 | - Notification center (with terminal-notifier) now uses application icon ([97b395b](http://github.com/jcgay/maven-notifier/commit/97b395b1dd94c894e26c79ce5f8e0f3593436aac)) 90 | 91 | # 1.3 92 | *** 93 | 94 | - Can pass configuration using system properties [view](http://github.com/jcgay/maven-notifier/commit/351c771187947ad995757e3211ffabfe25330156) 95 | - Add AnyBar (SomeBar) notifier [view](http://github.com/jcgay/maven-notifier/commit/f1bbee89aa878739d0374341ece3600470bec521) 96 | 97 | # 1.2 98 | *** 99 | 100 | - Configuration location is now ~/.m2/maven-notifier.properties [view](http://github.com/jcgay/maven-notifier/commit/86ba7057adf1f148107b25f7fc2e6a6567a97e57) 101 | - Compatibility with Maven 3.3.1 extension mechanism [view](http://github.com/jcgay/maven-notifier/commit/258d769c42e3531c0f45281c3ae6f8457595d922) 102 | 103 | # 1.1 104 | *** 105 | 106 | - No notification if build ends before configured threshold [Fixes #11] [view](http://github.com/jcgay/maven-notifier/commit/9b513b5) 107 | 108 | # 1.0 109 | *** 110 | 111 | - Use short description by default [view](http://github.com/jcgay/maven-notifier/commit/6be9df9bec4bfa043f60b5bd0df4154f22eda70c) 112 | - Replace short description default message with '...' [view](http://github.com/jcgay/maven-notifier/commit/eb89d89359bbc867739bd2255c58b9b6db462e83) 113 | - New notifiers: kdialog, notifu [view](http://github.com/jcgay/maven-notifier/commit/9eb59c48a29821f4f2e83b77e680dc263297cbad) 114 | - Use send-notification v0.3 [Fixes #10] [view](http://github.com/jcgay/maven-notifier/commit/9eb59c48a29821f4f2e83b77e680dc263297cbad) 115 | - Set notification level based on build status [view](http://github.com/jcgay/maven-notifier/commit/8690f63b7f16bd39fa7aa17689ef245563ccd22f) 116 | - Can use -DnotifyWith when no configuration file is present [view](http://github.com/jcgay/maven-notifier/commit/eb9b1f0dbc81cd1ca2951b59db699b3b020c785f) 117 | 118 | # 0.11 119 | *** 120 | 121 | - Maven 3.2.5 compatibility [Fixes #9] [view](http://github.com/jcgay/maven-notifier/commit/d2c17389117167b64154859ab7bf2b80895f9b24) 122 | 123 | # 0.10 124 | *** 125 | 126 | - Use [send-notification](https://github.com/jcgay/send-notification) 127 | 128 | # 0.9 129 | *** 130 | 131 | - Can override notifier implementation with system property [view](http://github.com/jcgay/maven-notifier/commit/9096f4472d6ea939e4fab28e5fc2f8f874cbd3ea) 132 | - Exclude SLF4J api from the Uber jar [Fixes #7] [view](http://github.com/jcgay/maven-notifier/commit/7df6bc9006eb6c1bfef62fcbd45bb1e3dab76150) 133 | - Add Pushbullet notifier [view](http://github.com/jcgay/maven-notifier/commit/a19ea22af668af6c4feed2fd18ce804a0f942914) 134 | - Add wiki link when Snarl notification fails [view](http://github.com/jcgay/maven-notifier/commit/b12b344482513f6a58da90fd3941595ec77e6ed4) 135 | - Add wiki links when Growl notification fails [Fixes #6] [view](http://github.com/jcgay/maven-notifier/commit/77ba7213077d2dc7270c2ead7ad9924476e09474) 136 | - Remove password from Configuration#toString [view](http://github.com/jcgay/maven-notifier/commit/2df057c68ea2ecbfeb82c3c6cffdcbfe713d905a) 137 | - Remove SLF4J Gntp listener [view](http://github.com/jcgay/maven-notifier/commit/916932911a2973c5bd64d62f913738567d06301d) 138 | - Add a Plexus Gntp listener to log Growl events [#6] [view](http://github.com/jcgay/maven-notifier/commit/fadc1eb4a793665e3b1d64052deb2d31a3123d1f) 139 | - Send notification when build fails with error [Fixes #5] [view](http://github.com/jcgay/maven-notifier/commit/25dc1055d905ffe409a7401cdfe0a9ae6f5e2cc3) 140 | 141 | # 0.8.1 142 | *** 143 | 144 | - Do not fail when notification-center does not use sound [Fix #4] [view](http://github.com/jcgay/maven-notifier/commit/2e7c08adfe1edaabb1d5ef6c8f79d3779dc816d9) 145 | 146 | # 0.8 147 | *** 148 | 149 | - Add icon for notification center. [view](http://github.com/jcgay/maven-notifier/commit/91dcab8678b3cab6d19635a3b564a8b432b2282c) 150 | - Short message notification when project contains only one module. [view](http://github.com/jcgay/maven-notifier/commit/d8f267df1b8ee2ba7f6d3337631c57fa8c034507) 151 | - Can configure notification message type (short/full). [view](http://github.com/jcgay/maven-notifier/commit/f7467c5ca840b40ed9ef7c19196036de04a9117d) 152 | - Can configure growl password. [view](http://github.com/jcgay/maven-notifier/commit/7d06186e3254b31370e986fcd83be0155998b8a6) 153 | - Can configure growl host. [Fix #3] [view](http://github.com/jcgay/maven-notifier/commit/d168f2c080456574c72a269c8633d1e1cc3883a9) 154 | - Can set a sound when using Apple Notification Center. [Fix #1] [view](http://github.com/jcgay/maven-notifier/commit/0a8e12a3c3d41c9d4963053a562ee0188f2210cd) 155 | - Display total time spent building the project. [view](http://github.com/jcgay/maven-notifier/commit/639a63203d2bd07f1178348200a4bd69351ebf3f) 156 | - Add Snarl notifier. [Fix #2] [view](http://github.com/jcgay/maven-notifier/commit/139cc7b345e11f9085b4c8637d55baf7d58442b6) 157 | --------------------------------------------------------------------------------