diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bcc5487c..6fd5b607 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,10 +14,10 @@ jobs: java: [8, 11] steps: - uses: actions/checkout@v2 - + - name: Fetch git tags run: ./.github/fetch_to_tag.sh - + # Our build uses JDK7's rt.jar to make sure the artifact is fully # compatible with Java 7, so we let this action set Java 7 up for us # and we store its JAVA_HOME @@ -29,6 +29,14 @@ jobs: - name: Capture JDK7_HOME run: echo "export JDK7_HOME=\"$JAVA_HOME\"" > ~/.jdk7_home + - name: Set up Java 17 (needed for Spring Boot 3) + uses: actions/setup-java@v1 + with: + java-version: 17 + + - name: Capture JDK17_HOME + run: echo "export JDK17_HOME=\"$JAVA_HOME\"" > ~/.jdk17_home + # This is the JDK that'll run the build - name: Set up Java ${{ matrix.java }} uses: actions/setup-java@v1 diff --git a/.gitignore b/.gitignore index 19764399..ec2f0041 100644 --- a/.gitignore +++ b/.gitignore @@ -73,3 +73,5 @@ atlassian-ide-plugin.xml # Local gradle properties local.properties + +bin/ diff --git a/build.gradle b/build.gradle index e8165e74..850d1cc2 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { dependencies { classpath "de.marcphilipp.gradle:nexus-publish-plugin:0.4.0" classpath "io.codearte.gradle.nexus:gradle-nexus-staging-plugin:0.21.2" - classpath "gradle.plugin.com.github.spotbugs.snom:spotbugs-gradle-plugin:4.2.4" + classpath "com.github.spotbugs.snom:spotbugs-gradle-plugin:4.8.0" classpath "com.palantir.gradle.revapi:gradle-revapi:1.4.4" } } @@ -40,7 +40,7 @@ subprojects { project -> apply from: "$rootDir/gradle/release.gradle" apply from: "$rootDir/gradle/quality.gradle" - + apply from: "$rootDir/gradle/compatibility.gradle" repositories { @@ -87,9 +87,8 @@ subprojects { project -> } } } - } wrapper { - gradleVersion = '6.6' + gradleVersion = '6.9.4' } diff --git a/examples/rollbar-spring-boot3-webmvc/build.gradle b/examples/rollbar-spring-boot3-webmvc/build.gradle new file mode 100644 index 00000000..2e3128e6 --- /dev/null +++ b/examples/rollbar-spring-boot3-webmvc/build.gradle @@ -0,0 +1,27 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.0.3' + id 'io.spring.dependency-management' version '1.1.0' +} + +group = 'com.rollbar.example' +version = VERSION_NAME +sourceCompatibility = '17' + +repositories { + mavenLocal() + maven { + url 'https://oss.sonatype.org/content/repositories/snapshots/' + } + mavenCentral() +} + +dependencies { + implementation group: 'com.rollbar', name: 'rollbar-spring-boot3-webmvc', version: VERSION_NAME, changing: true + implementation 'org.springframework.boot:spring-boot-starter-web' + testImplementation 'org.springframework.boot:spring-boot-starter-test' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/examples/rollbar-spring-boot3-webmvc/gradle.properties b/examples/rollbar-spring-boot3-webmvc/gradle.properties new file mode 120000 index 00000000..03ca90c5 --- /dev/null +++ b/examples/rollbar-spring-boot3-webmvc/gradle.properties @@ -0,0 +1 @@ +../../gradle.properties \ No newline at end of file diff --git a/examples/rollbar-spring-boot3-webmvc/gradle/wrapper/gradle-wrapper.properties b/examples/rollbar-spring-boot3-webmvc/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..070cb702 --- /dev/null +++ b/examples/rollbar-spring-boot3-webmvc/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/examples/rollbar-spring-boot3-webmvc/gradlew b/examples/rollbar-spring-boot3-webmvc/gradlew new file mode 100755 index 00000000..a69d9cb6 --- /dev/null +++ b/examples/rollbar-spring-boot3-webmvc/gradlew @@ -0,0 +1,240 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/examples/rollbar-spring-boot3-webmvc/settings.gradle b/examples/rollbar-spring-boot3-webmvc/settings.gradle new file mode 100644 index 00000000..dccdb826 --- /dev/null +++ b/examples/rollbar-spring-boot3-webmvc/settings.gradle @@ -0,0 +1 @@ +rootProject.name = "rollbar-rollbar-spring-boot3-webmvc-example" diff --git a/examples/rollbar-spring-boot3-webmvc/src/main/java/com/example/springbootwebmvc/ExampleApplication.java b/examples/rollbar-spring-boot3-webmvc/src/main/java/com/example/springbootwebmvc/ExampleApplication.java new file mode 100644 index 00000000..055ac207 --- /dev/null +++ b/examples/rollbar-spring-boot3-webmvc/src/main/java/com/example/springbootwebmvc/ExampleApplication.java @@ -0,0 +1,14 @@ +package com.example.springbootwebmvc; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ExampleApplication { + /** + * Starts the Spring boot application. + */ + public static void main(String[] args) { + SpringApplication.run(ExampleApplication.class, args); + } +} diff --git a/examples/rollbar-spring-boot3-webmvc/src/main/java/com/example/springbootwebmvc/ExampleViewController.java b/examples/rollbar-spring-boot3-webmvc/src/main/java/com/example/springbootwebmvc/ExampleViewController.java new file mode 100644 index 00000000..7eb09a8a --- /dev/null +++ b/examples/rollbar-spring-boot3-webmvc/src/main/java/com/example/springbootwebmvc/ExampleViewController.java @@ -0,0 +1,44 @@ +package com.example.springbootwebmvc; + +import com.rollbar.notifier.Rollbar; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class ExampleViewController { + + @Autowired + private Rollbar rollbar; + + /** + * Testing an uncaught exception - The register Rollbar bean will pick this up. + */ + @RequestMapping("/") + public void exceptionTest() { + // This exception will be passed now via the exception resolver + int x = 1 / 0; + } + + /** + * Testing a handled exception. Rollbar will pick up uncaught automatically offering you + * the option to send a custom log. + */ + @RequestMapping("/handledExceptionTest") + public void handledExceptionTest() { + try { + int x = 1 / 0; + } catch (Exception e) { + rollbar.log("log some error to Rollbar"); + throw e; // continue to raise it and Rollbar will send the full payload + } + } + + /** + * This is an example of how to access the Rollbar object and send an error. + */ + @RequestMapping("/rollbarTest") + public void rollbarTest() { + rollbar.error("Error"); + } +} \ No newline at end of file diff --git a/examples/rollbar-spring-boot3-webmvc/src/main/java/com/example/springbootwebmvc/RollbarConfig.java b/examples/rollbar-spring-boot3-webmvc/src/main/java/com/example/springbootwebmvc/RollbarConfig.java new file mode 100644 index 00000000..2a5e2820 --- /dev/null +++ b/examples/rollbar-spring-boot3-webmvc/src/main/java/com/example/springbootwebmvc/RollbarConfig.java @@ -0,0 +1,41 @@ +package com.example.springbootwebmvc; + +import com.rollbar.notifier.Rollbar; +import com.rollbar.notifier.config.Config; +import com.rollbar.spring.webmvc.RollbarSpringConfigBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +@Configuration() +@EnableWebMvc +@ComponentScan({ + "com.example.springbootwebmvc", + "com.rollbar.spring" +}) +public class RollbarConfig { + + @Value("${rollbar.access_token}") + private String accessToken; + + @Value("${rollbar.environment}") + private String environment; + + /** + * Register a Rollbar bean to configure App with Rollbar. + */ + @Bean + public Rollbar rollbar() { + return new Rollbar(getRollbarConfigs()); + } + + private Config getRollbarConfigs() { + // Reference ConfigBuilder.java for all the properties you can set for Rollbar + return RollbarSpringConfigBuilder.withAccessToken(this.accessToken) + .environment(this.environment) + .build(); + } + +} \ No newline at end of file diff --git a/examples/rollbar-spring-boot3-webmvc/src/main/resources/application.properties b/examples/rollbar-spring-boot3-webmvc/src/main/resources/application.properties new file mode 100644 index 00000000..1b8dc868 --- /dev/null +++ b/examples/rollbar-spring-boot3-webmvc/src/main/resources/application.properties @@ -0,0 +1,2 @@ +rollbar.access_token = +rollbar.environment = development diff --git a/gradle/quality.gradle b/gradle/quality.gradle index f1ac0d4e..a91b5a9c 100644 --- a/gradle/quality.gradle +++ b/gradle/quality.gradle @@ -10,7 +10,7 @@ checkstyle { } spotbugs { - toolVersion = '3.1.10' + toolVersion = '4.7.3' includeFilter = file("$rootDir/tools/findbugs/findbugs.xml") } @@ -42,6 +42,10 @@ afterEvaluate { } } +jacoco { + toolVersion = "0.8.8" +} + test { jacoco { destinationFile = file("$buildDir/jacoco/jacocoTest.exec") @@ -49,4 +53,4 @@ test { } finalizedBy jacocoTestReport // report is always generated after tests run -} \ No newline at end of file +} diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 6c9a2247..53b9e380 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.9.4-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 4f906e0c..1b6c7873 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,67 +17,101 @@ # ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +APP_BASE_NAME=${0##*/} # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -106,80 +140,95 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/rollbar-jakarta-web/build.gradle b/rollbar-jakarta-web/build.gradle new file mode 100644 index 00000000..a8525d46 --- /dev/null +++ b/rollbar-jakarta-web/build.gradle @@ -0,0 +1,25 @@ +ext { + jakartaServletVersion = '6.0.0' +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(11) + } +} + +compileJava { + options.release = 11 +} + +compileTestJava { + options.release = 11 +} + +dependencies { + api project(':rollbar-java') + + compileOnly group: 'jakarta.servlet', name: 'jakarta.servlet-api', version: jakartaServletVersion + + testImplementation group: 'jakarta.servlet', name: 'jakarta.servlet-api', version: jakartaServletVersion +} diff --git a/rollbar-jakarta-web/src/main/java/com/rollbar/web/filter/RollbarFilter.java b/rollbar-jakarta-web/src/main/java/com/rollbar/web/filter/RollbarFilter.java new file mode 100755 index 00000000..70985daa --- /dev/null +++ b/rollbar-jakarta-web/src/main/java/com/rollbar/web/filter/RollbarFilter.java @@ -0,0 +1,96 @@ +package com.rollbar.web.filter; + +import static com.rollbar.notifier.config.ConfigBuilder.withAccessToken; + +import com.rollbar.notifier.Rollbar; +import com.rollbar.notifier.config.Config; +import com.rollbar.notifier.config.ConfigBuilder; +import com.rollbar.notifier.config.ConfigProvider; +import com.rollbar.notifier.config.ConfigProviderHelper; +import com.rollbar.web.provider.PersonProvider; +import com.rollbar.web.provider.RequestProvider; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import java.io.IOException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class RollbarFilter implements Filter { + + private static final Logger LOGGER = LoggerFactory.getLogger(RollbarFilter.class); + + static final String ACCESS_TOKEN_PARAM_NAME = "access_token"; + + static final String USER_IP_HEADER_PARAM_NAME = "user_ip_header"; + + static final String CONFIG_PROVIDER_CLASS_PARAM_NAME = "config_provider"; + + static final String CONFIG_IP_CAPTURE_PARAM_NAME = "capture_ip"; + + private Rollbar rollbar; + + public RollbarFilter() { + // Empty constructor. + } + + public RollbarFilter(Rollbar rollbar) { + this.rollbar = rollbar; + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + String accessToken = filterConfig.getInitParameter(ACCESS_TOKEN_PARAM_NAME); + String userIpHeaderName = filterConfig.getInitParameter(USER_IP_HEADER_PARAM_NAME); + String configProviderClassName = + filterConfig.getInitParameter(CONFIG_PROVIDER_CLASS_PARAM_NAME); + String captureIp = filterConfig.getInitParameter(CONFIG_IP_CAPTURE_PARAM_NAME); + + ConfigProvider configProvider = ConfigProviderHelper.getConfigProvider(configProviderClassName); + Config config; + + RequestProvider requestProvider = new RequestProvider.Builder() + .userIpHeaderName(userIpHeaderName) + .captureIp(captureIp) + .build(); + + ConfigBuilder configBuilder = withAccessToken(accessToken) + .request(requestProvider) + .person(new PersonProvider()); + + if (configProvider != null) { + config = configProvider.provide(configBuilder); + } else { + config = configBuilder.build(); + } + + rollbar = Rollbar.init(config); + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + try { + chain.doFilter(request, response); + } catch (Exception e) { + sendToRollbar(e); + throw e; + } + } + + private void sendToRollbar(Exception error) { + try { + rollbar.error(error); + } catch (Exception e) { + LOGGER.error("Error sending to rollbar the error: ", error, e); + } + } + + @Override + public void destroy() { + rollbar = null; + } +} diff --git a/rollbar-jakarta-web/src/main/java/com/rollbar/web/listener/RollbarRequestListener.java b/rollbar-jakarta-web/src/main/java/com/rollbar/web/listener/RollbarRequestListener.java new file mode 100755 index 00000000..76897d21 --- /dev/null +++ b/rollbar-jakarta-web/src/main/java/com/rollbar/web/listener/RollbarRequestListener.java @@ -0,0 +1,26 @@ +package com.rollbar.web.listener; + +import jakarta.servlet.ServletRequestEvent; +import jakarta.servlet.ServletRequestListener; +import jakarta.servlet.http.HttpServletRequest; + +public class RollbarRequestListener implements ServletRequestListener { + + private static final ThreadLocal CURRENT_REQUEST = new ThreadLocal<>(); + + public static HttpServletRequest getServletRequest() { + return CURRENT_REQUEST.get(); + } + + @Override + public void requestInitialized(ServletRequestEvent sre) { + if (sre.getServletRequest() instanceof HttpServletRequest) { + CURRENT_REQUEST.set((HttpServletRequest) sre.getServletRequest()); + } + } + + @Override + public void requestDestroyed(ServletRequestEvent sre) { + CURRENT_REQUEST.remove(); + } +} diff --git a/rollbar-jakarta-web/src/main/java/com/rollbar/web/provider/PersonProvider.java b/rollbar-jakarta-web/src/main/java/com/rollbar/web/provider/PersonProvider.java new file mode 100644 index 00000000..90cbd078 --- /dev/null +++ b/rollbar-jakarta-web/src/main/java/com/rollbar/web/provider/PersonProvider.java @@ -0,0 +1,21 @@ +package com.rollbar.web.provider; + +import com.rollbar.api.payload.data.Person; +import com.rollbar.notifier.provider.Provider; +import com.rollbar.web.listener.RollbarRequestListener; +import jakarta.servlet.http.HttpServletRequest; + +public class PersonProvider implements Provider { + + @Override + public Person provide() { + HttpServletRequest request = RollbarRequestListener.getServletRequest(); + + if (request != null && request.getUserPrincipal() != null) { + return new Person.Builder() + .id(request.getUserPrincipal().getName()) + .build(); + } + return null; + } +} diff --git a/rollbar-jakarta-web/src/main/java/com/rollbar/web/provider/RequestProvider.java b/rollbar-jakarta-web/src/main/java/com/rollbar/web/provider/RequestProvider.java new file mode 100755 index 00000000..2838c2ed --- /dev/null +++ b/rollbar-jakarta-web/src/main/java/com/rollbar/web/provider/RequestProvider.java @@ -0,0 +1,221 @@ +package com.rollbar.web.provider; + +import static java.util.Arrays.asList; + +import com.rollbar.api.payload.data.Request; +import com.rollbar.notifier.provider.Provider; +import com.rollbar.web.listener.RollbarRequestListener; +import jakarta.servlet.http.HttpServletRequest; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +/** + * {@link Request} provider. + */ +public class RequestProvider implements Provider { + + private final String userIpHeaderName; + private final int captureIp; + + // CAPTURE_IP_ANONYMIZE is the string value used to signify anonymizing captured IP addresses + public static final String CAPTURE_IP_ANONYMIZE = "anonymize"; + // CAPTURE_IP_NONE is the string value used to signify not capturing IP addresses + public static final String CAPTURE_IP_NONE = "none"; + + private static final int CAPTURE_IP_TYPE_FULL = 0; + private static final int CAPTURE_IP_TYPE_ANONYMIZE = 1; + private static final int CAPTURE_IP_TYPE_NONE = 2; + + /** + * Constructor. + */ + RequestProvider(Builder builder) { + this.userIpHeaderName = builder.userIpHeaderName; + if (builder.captureIp != null) { + if (builder.captureIp.equals(CAPTURE_IP_ANONYMIZE)) { + this.captureIp = CAPTURE_IP_TYPE_ANONYMIZE; + } else if (builder.captureIp.equals(CAPTURE_IP_NONE)) { + this.captureIp = CAPTURE_IP_TYPE_NONE; + } else { + this.captureIp = CAPTURE_IP_TYPE_FULL; + } + } else { + this.captureIp = CAPTURE_IP_TYPE_FULL; + } + } + + @Override + public Request provide() { + HttpServletRequest req = RollbarRequestListener.getServletRequest(); + + if (req != null) { + Request request = new Request.Builder() + .url(url(req)) + .method(method(req)) + .headers(headers(req)) + .get(getParams(req)) + .post(postParams(req)) + .queryString(queryString(req)) + .userIp(userIp(req)) + .build(); + + return request; + } + + return null; + } + + private String userIp(HttpServletRequest request) { + String rawIp; + if (userIpHeaderName == null || "".equals(userIpHeaderName)) { + rawIp = request.getRemoteAddr(); + } else { + rawIp = request.getHeader(userIpHeaderName); + } + if (rawIp == null) { + return rawIp; + } + + if (captureIp == CAPTURE_IP_TYPE_FULL) { + return rawIp; + } else if (captureIp == CAPTURE_IP_TYPE_ANONYMIZE) { + if (rawIp.contains(".")) { + // IPV4 + String[] parts = rawIp.split("\\."); + if (parts.length < 3) { + return rawIp; + } + // Java 7 does not have String.join + StringBuffer ip = new StringBuffer(parts[0]); + ip.append("."); + ip.append(parts[1]); + ip.append("."); + ip.append(parts[2]); + ip.append(".0"); + return ip.toString(); + } else if (rawIp.contains(":")) { + // IPV6 + String[] parts = rawIp.split(":"); + if (parts.length < 3) { + return rawIp; + } + StringBuffer ip = new StringBuffer(parts[0]); + ip.append(":"); + ip.append(parts[1]); + ip.append(":"); + ip.append(parts[2]); + ip.append(":0000:0000:0000:0000:0000"); + return ip.toString(); + } else { + return rawIp; + } + } else if (captureIp == CAPTURE_IP_TYPE_NONE) { + return null; + } + return null; + } + + private static String url(HttpServletRequest request) { + return request.getRequestURL().toString(); + } + + private static String method(HttpServletRequest request) { + return request.getMethod(); + } + + private static Map headers(HttpServletRequest request) { + Map headers = new HashMap<>(); + + Enumeration headerNames = request.getHeaderNames(); + while (headerNames.hasMoreElements()) { + String headerName = headerNames.nextElement(); + headers.put(headerName, request.getHeader(headerName)); + } + + return headers; + } + + private static Map> getParams(HttpServletRequest request) { + if ("GET".equalsIgnoreCase(request.getMethod())) { + return params(request); + } + + return null; + } + + private static Map postParams(HttpServletRequest request) { + if ("POST".equalsIgnoreCase(request.getMethod())) { + Map> params = params(request); + Map postParams = new HashMap<>(); + for (Entry> entry : params.entrySet()) { + if (entry.getValue() != null && entry.getValue().size() == 1) { + postParams.put(entry.getKey(), entry.getValue().get(0)); + } else { + postParams.put(entry.getKey(), entry.getValue()); + } + } + return postParams; + } + + return null; + } + + private static Map> params(HttpServletRequest request) { + Map> params = new HashMap<>(); + + Map paramNames = request.getParameterMap(); + for (Entry param : paramNames.entrySet()) { + if (param.getValue() != null && param.getValue().length > 0) { + params.put(param.getKey(), asList(param.getValue())); + } + } + + return params; + } + + private static String queryString(HttpServletRequest request) { + return request.getQueryString(); + } + + /** + * Builder class for {@link RequestProvider}. + */ + public static final class Builder { + + private String userIpHeaderName; + private String captureIp; + + /** + * The request header name to retrieve the user ip. + * @param userIpHeaderName the header name. + * @return the builder instance. + */ + public Builder userIpHeaderName(String userIpHeaderName) { + this.userIpHeaderName = userIpHeaderName; + return this; + } + + /** + * The policy to use for capturing the user ip. + * @param captureIp One of: full, anonymize, none + * If this value is empty, null, or otherwise invalid the default policy is full. + * @return the builder instance. + */ + public Builder captureIp(String captureIp) { + this.captureIp = captureIp; + return this; + } + + /** + * Builds the {@link RequestProvider request provider}. + * + * @return the payload. + */ + public RequestProvider build() { + return new RequestProvider(this); + } + } +} diff --git a/rollbar-jakarta-web/src/test/java/com/rollbar/web/config/FakeConfigProvider.java b/rollbar-jakarta-web/src/test/java/com/rollbar/web/config/FakeConfigProvider.java new file mode 100644 index 00000000..30eadca5 --- /dev/null +++ b/rollbar-jakarta-web/src/test/java/com/rollbar/web/config/FakeConfigProvider.java @@ -0,0 +1,19 @@ +package com.rollbar.web.config; + +import com.rollbar.notifier.config.Config; +import com.rollbar.notifier.config.ConfigBuilder; +import com.rollbar.notifier.config.ConfigProvider; + +public class FakeConfigProvider implements ConfigProvider { + + public static boolean CALLED = false; + + public FakeConfigProvider() { + CALLED = true; + } + + @Override + public Config provide(ConfigBuilder builder) { + return builder.build(); + } +} diff --git a/rollbar-jakarta-web/src/test/java/com/rollbar/web/filter/RollbarFilterTest.java b/rollbar-jakarta-web/src/test/java/com/rollbar/web/filter/RollbarFilterTest.java new file mode 100644 index 00000000..7077fcb1 --- /dev/null +++ b/rollbar-jakarta-web/src/test/java/com/rollbar/web/filter/RollbarFilterTest.java @@ -0,0 +1,110 @@ +package com.rollbar.web.filter; + +import static com.rollbar.web.filter.RollbarFilter.ACCESS_TOKEN_PARAM_NAME; +import static com.rollbar.web.filter.RollbarFilter.CONFIG_PROVIDER_CLASS_PARAM_NAME; +import static com.rollbar.web.filter.RollbarFilter.USER_IP_HEADER_PARAM_NAME; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.rollbar.web.config.FakeConfigProvider; +import com.rollbar.notifier.Rollbar; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +public class RollbarFilterTest { + + static final Throwable ERROR = new RuntimeException("Something went wrong"); + + @Rule + public MockitoRule rule = MockitoJUnit.rule(); + + @Mock + Rollbar rollbar; + + @Mock + ServletRequest request; + + @Mock + ServletResponse response; + + @Mock + FilterChain chain; + + @Mock + FilterConfig filterConfig; + + RollbarFilter sut; + + @Before + public void setUp() throws Exception { + doThrow(ERROR).when(chain).doFilter(request, response); + + sut = new RollbarFilter(rollbar); + } + + @Test + public void shouldInit() throws Exception { + sut.init(filterConfig); + + verify(filterConfig).getInitParameter(ACCESS_TOKEN_PARAM_NAME); + verify(filterConfig).getInitParameter(USER_IP_HEADER_PARAM_NAME); + verify(filterConfig).getInitParameter(CONFIG_PROVIDER_CLASS_PARAM_NAME); + } + + @Test + public void shouldUseConfigProviderIfAvailable() throws Exception { + when(filterConfig.getInitParameter(CONFIG_PROVIDER_CLASS_PARAM_NAME)).thenReturn( + FakeConfigProvider.class.getCanonicalName()); + sut.init(filterConfig); + + assertTrue(FakeConfigProvider.CALLED); + } + + @Test + public void shouldNotUseConfigProviderIfError() throws Exception { + when(filterConfig.getInitParameter(CONFIG_PROVIDER_CLASS_PARAM_NAME)).thenReturn( + "com.rollbar.not.exists"); + sut.init(filterConfig); + + assertFalse(FakeConfigProvider.CALLED); + } + + @Test + public void shouldLogError() throws Exception { + try { + sut.doFilter(request, response, chain); + } catch (Exception e) { + if(!e.equals(ERROR)) { + fail(); + } + } + verify(rollbar).error(ERROR); + } + + @Test + public void shouldSwallowException() throws Exception { + doThrow(new RuntimeException("Error sending to Rollbar")).when(rollbar). + error(any(Throwable.class)); + + try { + sut.doFilter(request, response, chain); + } catch (Exception e) { + if(!e.equals(ERROR)) { + fail(); + } + } + } +} diff --git a/rollbar-jakarta-web/src/test/java/com/rollbar/web/listener/RollbarRequestListenerTest.java b/rollbar-jakarta-web/src/test/java/com/rollbar/web/listener/RollbarRequestListenerTest.java new file mode 100644 index 00000000..f82440c2 --- /dev/null +++ b/rollbar-jakarta-web/src/test/java/com/rollbar/web/listener/RollbarRequestListenerTest.java @@ -0,0 +1,51 @@ +package com.rollbar.web.listener; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.when; + +import jakarta.servlet.ServletRequestEvent; +import jakarta.servlet.http.HttpServletRequest; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +public class RollbarRequestListenerTest { + + @Rule + public MockitoRule rule = MockitoJUnit.rule(); + + @Mock + ServletRequestEvent requestEvent; + + @Mock + HttpServletRequest request; + + RollbarRequestListener sut; + + @Before + public void setUp() { + when(requestEvent.getServletRequest()).thenReturn(request); + + sut = new RollbarRequestListener(); + } + + @Test + public void shouldSetTheRequest() { + sut.requestInitialized(requestEvent); + + assertThat(RollbarRequestListener.getServletRequest(), is(request)); + } + + @Test + public void shouldRemoveTheRequest() { + sut.requestInitialized(requestEvent); + sut.requestDestroyed(requestEvent); + + assertNull(RollbarRequestListener.getServletRequest()); + } +} diff --git a/rollbar-jakarta-web/src/test/java/com/rollbar/web/provider/PersonProviderTest.java b/rollbar-jakarta-web/src/test/java/com/rollbar/web/provider/PersonProviderTest.java new file mode 100644 index 00000000..8e4eab34 --- /dev/null +++ b/rollbar-jakarta-web/src/test/java/com/rollbar/web/provider/PersonProviderTest.java @@ -0,0 +1,69 @@ +package com.rollbar.web.provider; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.rollbar.api.payload.data.Person; +import com.rollbar.web.listener.RollbarRequestListener; +import java.security.Principal; +import jakarta.servlet.ServletRequestEvent; +import jakarta.servlet.http.HttpServletRequest; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +public class PersonProviderTest { + + static final String PRINCIPAL_NAME = "user"; + + @Rule + public MockitoRule rule = MockitoJUnit.rule(); + + @Mock + ServletRequestEvent requestEvent; + + @Mock + HttpServletRequest request; + + RollbarRequestListener listener; + + PersonProvider sut; + + @Before + public void setUp() { + when(requestEvent.getServletRequest()).thenReturn(request); + + listener = new RollbarRequestListener(); + listener.requestInitialized(requestEvent); + + sut = new PersonProvider(); + } + + @Test + public void shouldRetrieveThePersonIfPrincipalPresent() { + Principal principal = mock(Principal.class); + when(principal.getName()).thenReturn(PRINCIPAL_NAME); + + when(request.getUserPrincipal()).thenReturn(principal); + + Person result = sut.provide(); + Person expected = new Person.Builder().id(PRINCIPAL_NAME).build(); + + assertThat(result, is(expected)); + } + + @Test + public void shouldRetrieveNullIfPrincipalNotPresent() { + when(request.getUserPrincipal()).thenReturn(null); + + Person result = sut.provide(); + + assertNull(result); + } +} diff --git a/rollbar-jakarta-web/src/test/java/com/rollbar/web/provider/RequestProviderTest.java b/rollbar-jakarta-web/src/test/java/com/rollbar/web/provider/RequestProviderTest.java new file mode 100644 index 00000000..6f9fd82c --- /dev/null +++ b/rollbar-jakarta-web/src/test/java/com/rollbar/web/provider/RequestProviderTest.java @@ -0,0 +1,182 @@ +package com.rollbar.web.provider; + +import static java.util.Arrays.asList; +import static java.util.Collections.enumeration; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertNull; +import static org.hamcrest.core.Is.is; +import static org.mockito.Mockito.when; + +import com.rollbar.api.payload.data.Request; +import com.rollbar.web.listener.RollbarRequestListener; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import jakarta.servlet.ServletRequestEvent; +import jakarta.servlet.http.HttpServletRequest; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +public class RequestProviderTest { + + static final String QUERYSTRING = "param1=value1¶m1=value2¶m2=value3"; + + static final StringBuffer REQUEST_URL = new StringBuffer("https://rollbar.com"); + + static final String METHOD = "GET"; + + static final Map HEADERS = new HashMap<>(); + static { + HEADERS.put("accept", "text/html,application/xhtml+xml," + + "application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8"); + } + + static final Map REQUEST_PARAMS = new HashMap<>(); + static { + REQUEST_PARAMS.put("param1", new String[]{"value1", "value2"}); + REQUEST_PARAMS.put("param2", new String[]{"value3"}); + } + + static final String REMOTE_ADDRESS = "127.0.0.1"; + + @Rule + public MockitoRule rule = MockitoJUnit.rule(); + + @Mock + ServletRequestEvent requestEvent; + + @Mock + HttpServletRequest request; + + RollbarRequestListener listener; + + RequestProvider sut; + + @Before + public void setUp() { + when(request.getRequestURL()).thenReturn(REQUEST_URL); + when(request.getHeaderNames()).thenReturn(enumeration(HEADERS.keySet())); + for(String headerName : HEADERS.keySet()) { + when(request.getHeader(headerName)).thenReturn(HEADERS.get(headerName)); + } + when(request.getQueryString()).thenReturn(QUERYSTRING); + when(request.getRemoteAddr()).thenReturn(REMOTE_ADDRESS); + + when(requestEvent.getServletRequest()).thenReturn(request); + + listener = new RollbarRequestListener(); + listener.requestInitialized(requestEvent); + + sut = new RequestProvider.Builder().build(); + } + + @Test + public void shouldRetrieveTheRequest() { + when(request.getMethod()).thenReturn(METHOD); + when(request.getParameterMap()).thenReturn(REQUEST_PARAMS); + + Map> expectedGetParams = new HashMap<>(); + for(String paramName : REQUEST_PARAMS.keySet()) { + expectedGetParams.put(paramName, asList(REQUEST_PARAMS.get(paramName))); + } + + Request result = sut.provide(); + + assertThat(result.getUrl(), is(REQUEST_URL.toString())); + assertThat(result.getMethod(), is(METHOD)); + assertThat(result.getHeaders(), is(HEADERS)); + assertThat(result.getGet(), is(expectedGetParams)); + assertThat(result.getQueryString(), is(QUERYSTRING)); + assertThat(result.getUserIp(), is(REMOTE_ADDRESS)); + } + + @Test + public void shouldTrackPostParams() { + when(request.getMethod()).thenReturn("POST"); + when(request.getParameterMap()).thenReturn(REQUEST_PARAMS); + + Map expectedPostParams = new HashMap<>(); + for(String paramName : REQUEST_PARAMS.keySet()) { + String[] values = REQUEST_PARAMS.get(paramName); + if (values.length == 1) { + expectedPostParams.put(paramName, values[0]); + } else { + expectedPostParams.put(paramName, asList(values)); + } + } + + Request result = sut.provide(); + assertThat(result.getPost(), is(expectedPostParams)); + } + + @Test + public void shouldRetrieveTheRemoteAddressUsingHeader() { + String userIpHeaderName = "X-FORWARDED-FOR"; + + RequestProvider sut = new RequestProvider.Builder() + .userIpHeaderName(userIpHeaderName) + .build(); + + String remoteAddr = "192.168.1.1"; + when(request.getHeader(userIpHeaderName)).thenReturn(remoteAddr); + + Request result = sut.provide(); + + assertThat(result.getUserIp(), is(remoteAddr)); + } + + @Test + public void shouldRetrieveTheRemoteAddressUsingRequestRemoteAddress() { + String userIpHeaderName = ""; + + RequestProvider sut = new RequestProvider.Builder() + .userIpHeaderName(userIpHeaderName) + .build(); + + String remoteAddr = "192.168.1.1"; + when(request.getRemoteAddr()).thenReturn(remoteAddr); + + Request result = sut.provide(); + + assertThat(result.getUserIp(), is(remoteAddr)); + } + + @Test + public void shouldRetrieveTheRemoteAddressUsingRequestRemoteAddressAndAnonymize() { + String userIpHeaderName = ""; + + RequestProvider sut = new RequestProvider.Builder() + .userIpHeaderName(userIpHeaderName) + .captureIp("anonymize") + .build(); + + String remoteAddr = "192.168.1.1"; + String remoteAddrAnon = "192.168.1.0"; + when(request.getRemoteAddr()).thenReturn(remoteAddr); + + Request result = sut.provide(); + + assertThat(result.getUserIp(), is(remoteAddrAnon)); + } + + @Test + public void shouldRetrieveTheRemoteAddressUsingRequestRemoteAddressAndNotCaptureIfCaptureIpIsNone() { + String userIpHeaderName = ""; + + RequestProvider sut = new RequestProvider.Builder() + .userIpHeaderName(userIpHeaderName) + .captureIp("none") + .build(); + + String remoteAddr = "192.168.1.1"; + when(request.getRemoteAddr()).thenReturn(remoteAddr); + + Request result = sut.provide(); + + assertNull(result.getUserIp()); + } +} diff --git a/rollbar-spring-boot3-webmvc/build.gradle b/rollbar-spring-boot3-webmvc/build.gradle new file mode 100644 index 00000000..3c199706 --- /dev/null +++ b/rollbar-spring-boot3-webmvc/build.gradle @@ -0,0 +1,25 @@ +ext { + jakartaServletVersion = '6.0.0' + springBootVersion = '3.0.0' +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +compileJava { + options.release = 17 +} + +compileTestJava { + options.release = 17 +} + +dependencies { + api project(':rollbar-spring6-webmvc') + + implementation 'org.springframework.boot:spring-boot:' + springBootVersion + implementation group: 'jakarta.servlet', name: 'jakarta.servlet-api', version: jakartaServletVersion +} diff --git a/rollbar-spring-boot3-webmvc/src/main/java/com/rollbar/spring/boot/webmvc/RollbarServletContextInitializer.java b/rollbar-spring-boot3-webmvc/src/main/java/com/rollbar/spring/boot/webmvc/RollbarServletContextInitializer.java new file mode 100644 index 00000000..150cd6ff --- /dev/null +++ b/rollbar-spring-boot3-webmvc/src/main/java/com/rollbar/spring/boot/webmvc/RollbarServletContextInitializer.java @@ -0,0 +1,25 @@ +package com.rollbar.spring.boot.webmvc; + +import com.rollbar.web.listener.RollbarRequestListener; + +import jakarta.servlet.ServletContext; + +import org.springframework.boot.web.servlet.ServletContextInitializer; +import org.springframework.stereotype.Component; + +@Component +public class RollbarServletContextInitializer implements ServletContextInitializer { + + /** + * Adds RollbarListener when app starts up. This enriches HTTP request data + * with Rollbar exceptions. You can view this data from the Rollbar app. + */ + @Override + public void onStartup(ServletContext container) { + + // Attach the RollbarRequestListener to attribute HTTP Request data into the Exception object + // This will be visible in Rollbar + container.addListener(RollbarRequestListener.class); + } + +} diff --git a/rollbar-spring6-webmvc/build.gradle b/rollbar-spring6-webmvc/build.gradle new file mode 100644 index 00000000..9cae4a20 --- /dev/null +++ b/rollbar-spring6-webmvc/build.gradle @@ -0,0 +1,24 @@ +ext { + springWebmvcVersion = '6.0.0' + jakartaServletVersion = '6.0.0' +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +compileJava { + options.release = 17 +} + +compileTestJava { + options.release = 17 +} + +dependencies { + api project(":rollbar-jakarta-web") + implementation 'org.springframework:spring-webmvc:' + springWebmvcVersion + implementation group: 'jakarta.servlet', name: 'jakarta.servlet-api', version: jakartaServletVersion +} diff --git a/rollbar-spring6-webmvc/src/main/java/com/rollbar/spring/webmvc/RollbarHandlerExceptionResolver.java b/rollbar-spring6-webmvc/src/main/java/com/rollbar/spring/webmvc/RollbarHandlerExceptionResolver.java new file mode 100644 index 00000000..6c621f7a --- /dev/null +++ b/rollbar-spring6-webmvc/src/main/java/com/rollbar/spring/webmvc/RollbarHandlerExceptionResolver.java @@ -0,0 +1,34 @@ +package com.rollbar.spring.webmvc; + +import com.rollbar.notifier.Rollbar; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.servlet.HandlerExceptionResolver; +import org.springframework.web.servlet.ModelAndView; + +@Order(Ordered.HIGHEST_PRECEDENCE) +@ControllerAdvice +public class RollbarHandlerExceptionResolver implements HandlerExceptionResolver { + + private Rollbar rollbar; + + @Autowired + public RollbarHandlerExceptionResolver(Rollbar rollbar) { + this.rollbar = rollbar; + } + + @Override + public ModelAndView resolveException(HttpServletRequest request, + HttpServletResponse response, + Object handler, + Exception ex) { + rollbar.error(ex); + + return null; // returning null forces other resolvers to handle the exception + } + +} diff --git a/rollbar-spring6-webmvc/src/main/java/com/rollbar/spring/webmvc/RollbarSpringConfigBuilder.java b/rollbar-spring6-webmvc/src/main/java/com/rollbar/spring/webmvc/RollbarSpringConfigBuilder.java new file mode 100644 index 00000000..41fb61e2 --- /dev/null +++ b/rollbar-spring6-webmvc/src/main/java/com/rollbar/spring/webmvc/RollbarSpringConfigBuilder.java @@ -0,0 +1,23 @@ +package com.rollbar.spring.webmvc; + +import com.rollbar.notifier.config.ConfigBuilder; +import com.rollbar.notifier.provider.server.ServerProvider; +import com.rollbar.web.provider.RequestProvider; + +public class RollbarSpringConfigBuilder extends ConfigBuilder { + + protected RollbarSpringConfigBuilder(String accessToken) { + super(accessToken); + this.request = new RequestProvider.Builder().build(); + this.server = new ServerProvider(); + this.framework = "spring"; + } + + /** + * Helper to provide a Config Builder for Java Spring with access token. + */ + public static ConfigBuilder withAccessToken(String accessToken) { + return new RollbarSpringConfigBuilder(accessToken); + } + +} diff --git a/rollbar-spring6-webmvc/src/main/java/com/rollbar/spring/webmvc/RollbarWebApplicationInitializer.java b/rollbar-spring6-webmvc/src/main/java/com/rollbar/spring/webmvc/RollbarWebApplicationInitializer.java new file mode 100644 index 00000000..951d2c51 --- /dev/null +++ b/rollbar-spring6-webmvc/src/main/java/com/rollbar/spring/webmvc/RollbarWebApplicationInitializer.java @@ -0,0 +1,17 @@ +package com.rollbar.spring.webmvc; + +import com.rollbar.web.listener.RollbarRequestListener; +import jakarta.servlet.ServletContext; +import org.springframework.web.WebApplicationInitializer; + +public class RollbarWebApplicationInitializer implements WebApplicationInitializer { + + @Override + public void onStartup(ServletContext container) { + + // Attach the RollbarRequestListner to attribute HTTP Request data into the Exception object + // This will be visible in Rollbar + container.addListener(RollbarRequestListener.class); + } + +} diff --git a/rollbar-spring6-webmvc/src/test/java/com/rollbar/spring/webmvc/RollbarHandlerExceptionResolverTest.java b/rollbar-spring6-webmvc/src/test/java/com/rollbar/spring/webmvc/RollbarHandlerExceptionResolverTest.java new file mode 100644 index 00000000..a02a1ae5 --- /dev/null +++ b/rollbar-spring6-webmvc/src/test/java/com/rollbar/spring/webmvc/RollbarHandlerExceptionResolverTest.java @@ -0,0 +1,39 @@ +package com.rollbar.spring.webmvc; + +import com.rollbar.notifier.Rollbar; +import org.junit.Test; +import org.mockito.Mock; +import org.springframework.web.servlet.HandlerExceptionResolver; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import static org.mockito.Mockito.*; + +public class RollbarHandlerExceptionResolverTest { + + @Mock + HttpServletRequest request; + + @Mock + HttpServletResponse response; + + @Test + public void testRollbarExceptionResolver() { + Exception testException = new Exception("test exception"); + + // build the Rollbar mock object + Rollbar rollbar = mock(Rollbar.class); + doNothing().when(rollbar).error(testException); + + // construct exception resolver from the Rollbar resolver for Spring webmvc + HandlerExceptionResolver handlerExceptionResolver = new RollbarHandlerExceptionResolver(rollbar); + + // build a full mocked out request for the exception resolver + handlerExceptionResolver.resolveException(request, response, null, testException); + + // verify that the rollbar mocked object got the exception inside the resolver + verify(rollbar, times(1)).error(testException); + } + +} diff --git a/settings.gradle b/settings.gradle index cc25be2e..0728dbb9 100644 --- a/settings.gradle +++ b/settings.gradle @@ -4,10 +4,13 @@ include ":rollbar-api", ":rollbar-java", ":rollbar-android", ":rollbar-web", + ":rollbar-jakarta-web", ":rollbar-log4j2", ":rollbar-logback", ":rollbar-spring-webmvc", + ":rollbar-spring6-webmvc", ":rollbar-spring-boot-webmvc", + ":rollbar-spring-boot3-webmvc", ":rollbar-struts2", ":rollbar-reactive-streams", ":rollbar-reactive-streams-reactor", diff --git a/tools/findbugs/findbugs.xml b/tools/findbugs/findbugs.xml index 46a00048..73f0220a 100644 --- a/tools/findbugs/findbugs.xml +++ b/tools/findbugs/findbugs.xml @@ -97,9 +97,6 @@ - - - @@ -634,9 +631,6 @@ - - -