diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5379d05 --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +**/.classpath +.DS_Store +**/nb-configuration.xml +**/.springBeans +**/testing.properties +**/*.eml +**/Scripts +.idea +*.iml +.project +.idea +.metadata +.DS_Store +Servers +.settings +.classpath +mvn-repo +bin +*.war +*.log +*/nbactions.xml +*.eml +nbactions.xml +testing.properties +*/test-output +JargonVersion.java +**/${test.option.mount.basedir} +**/*.*~ +*.*~ +.dbeaver* +test.*.properties +**/target +target \ No newline at end of file diff --git a/README.md b/README.md index d074d38..d286a9b 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,86 @@ -# notification-service +# Notification Service(WIP) A pluggable Spring Boot RESTful web service to enable Notification system in Metalnx. + +## Overview +This server was generated by the [swagger-codegen](https://github.com/swagger-api/swagger-codegen) project. +Refer: [OpenAPI-Spec](https://github.com/swagger-api/swagger-core) + +The underlying library integrating swagger to SpringBoot is [springfox](https://github.com/springfox/springfox) + +Start your server as an simple java application + +You can view the api documentation in swagger-ui by pointing to +http://localhost:8888/swagger-ui.html + +Change default port value in application.properties + + +## Database schema migration and setup +Database schema should be configured using [flywaydb](https://flywaydb.org/). This project includes code that supports database migrations across versions. + +Reference [docs](https://flywaydb.org/documentation/) + +We'll use the Maven plugin in our own processes and documentation, but there are other options as well. + +>**N.B.:** +User and password information needs to be kept secure. There are maven plugin configuration [options](https://flywaydb.org/documentation/maven/). + +1. Set up the database (we will use postgres in the spirit of the iRODS install) +```bash +$ (sudo) su - postgres +postgres$ psql +psql> CREATE USER irodsext WITH PASSWORD 'password'; +psql> CREATE DATABASE "notification"; +psql> GRANT ALL PRIVILEGES ON DATABASE "notifications" TO irodsext; +psql> ALTER USER irodsext WITH SUPERUSER; +psql>Alter user irodsext with SUPERUSER; +``` + +> need to add superuser role to use uuid-ossp + +1. Setting up Maven profile +```bash +vi ~/.m2/settings.xml +``` +```xml + + flyway-local + + jdbc:postgresql://
:/ + irodsext + password + public + + +``` +1. Clean database (**Warning** destructive! clears your database) +```bash +mvn flyway:clean +``` +1. Migration, this is the first command to run from 0 on a clean database +```bash +mvn flyway:migrate +``` + + For future migration use benchmark + ```bash + mvn flyway:benchmark + ``` + > Looks at existing database to set to initial version. This is not for new databases. + +## Initial database table: "notification" + +|attribute_name|datatype|comment| +|---|---|---| +| id(PK) | BIGINT | auto generated sequence | +| notification_id | uuid | using uuid-ossp module functions to generate universally unique identifiers (UUIDs) | +| sender_id | VARCHAR | notification sender's userId extracted from JWT | +| recipient_id | VARCHAR | notification recipient's userId | +| subject | TEXT | notification subject | +| message | TEXT | notification message | +| seen | BOOLEAN | boolean for user's seen/unseen notification status | +| deleted | BOOLEAN | boolean for deleted notification, notification will not be deleted from postgres db | +| date_created | TIMESTAMP | timestamp auto-generated by postgres db | +| severity_level | INTEGER | severity level of notification an integer value between 1 and 5 | +| notification_type | VARCHAR | type of notification options are workflow, permission, system | +| data_location_url | TEXT | notification associated logical location of data | \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..6c3d817 --- /dev/null +++ b/pom.xml @@ -0,0 +1,135 @@ + + 4.0.0 + gov.nih.niehs.notification + notification-service + jar + Notification Service + 1.0.0 + + 1.7 + ${java.version} + ${java.version} + 2.9.2 + + + org.springframework.boot + spring-boot-starter-parent + 1.5.22.RELEASE + + + src/main/java + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + + org.flywaydb + flyway-maven-plugin + 4.2.0 + + ${flyway.jdbc.url} + ${flyway.db.user} + ${flyway.db.password} + + ${flyway.db.schema} + + + classpath:migrations + + + + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-tomcat + + + + org.springframework.security + spring-security-web + + + org.slf4j + log4j-over-slf4j + + + + + org.springframework.security + spring-security-config + + + org.slf4j + log4j-over-slf4j + + + + + + io.jsonwebtoken + jjwt + 0.9.0 + + + + + org.postgresql + postgresql + + + + + io.springfox + springfox-swagger2 + ${springfox-version} + + + io.springfox + springfox-swagger-ui + ${springfox-version} + + + + com.github.joschi.jackson + jackson-datatype-threetenbp + 2.6.4 + + + + javax.validation + validation-api + + + + javax.xml.bind + jaxb-api + 2.3.1 + + + + org.springframework.boot + spring-boot-starter-test + test + + + diff --git a/src/main/java/gov/nih/niehs/notification/RFC3339DateFormat.java b/src/main/java/gov/nih/niehs/notification/RFC3339DateFormat.java new file mode 100644 index 0000000..0b0d706 --- /dev/null +++ b/src/main/java/gov/nih/niehs/notification/RFC3339DateFormat.java @@ -0,0 +1,22 @@ +package gov.nih.niehs.notification; + +import com.fasterxml.jackson.databind.util.ISO8601DateFormat; +import com.fasterxml.jackson.databind.util.ISO8601Utils; + +import java.text.FieldPosition; +import java.util.Date; + + +public class RFC3339DateFormat extends ISO8601DateFormat { + + private static final long serialVersionUID = 1L; + + // Same as ISO8601DateFormat but serializing milliseconds. + @Override + public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition fieldPosition) { + String value = ISO8601Utils.format(date, true); + toAppendTo.append(value); + return toAppendTo; + } + +} diff --git a/src/main/java/gov/nih/niehs/notification/Swagger2SpringBoot.java b/src/main/java/gov/nih/niehs/notification/Swagger2SpringBoot.java new file mode 100644 index 0000000..8c24224 --- /dev/null +++ b/src/main/java/gov/nih/niehs/notification/Swagger2SpringBoot.java @@ -0,0 +1,36 @@ +package gov.nih.niehs.notification; + +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.ExitCodeGenerator; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; + +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +@SpringBootApplication +@EnableSwagger2 +@ComponentScan(basePackages = { "gov.nih.niehs.notification", "gov.nih.niehs.notification.api" , "gov.nih.niehs.notification.configuration"}) +public class Swagger2SpringBoot implements CommandLineRunner { + + @Override + public void run(String... arg0) throws Exception { + if (arg0.length > 0 && arg0[0].equals("exitcode")) { + throw new ExitException(); + } + } + + public static void main(String[] args) throws Exception { + new SpringApplication(Swagger2SpringBoot.class).run(args); + } + + class ExitException extends RuntimeException implements ExitCodeGenerator { + private static final long serialVersionUID = 1L; + + @Override + public int getExitCode() { + return 10; + } + + } +} diff --git a/src/main/java/gov/nih/niehs/notification/api/ApiException.java b/src/main/java/gov/nih/niehs/notification/api/ApiException.java new file mode 100644 index 0000000..f568e8e --- /dev/null +++ b/src/main/java/gov/nih/niehs/notification/api/ApiException.java @@ -0,0 +1,11 @@ +package gov.nih.niehs.notification.api; + + +public class ApiException extends Exception { + private int code; + + public ApiException(int code, String msg) { + super(msg); + this.code = code; + } +} diff --git a/src/main/java/gov/nih/niehs/notification/api/ApiOriginFilter.java b/src/main/java/gov/nih/niehs/notification/api/ApiOriginFilter.java new file mode 100644 index 0000000..2af8109 --- /dev/null +++ b/src/main/java/gov/nih/niehs/notification/api/ApiOriginFilter.java @@ -0,0 +1,26 @@ +package gov.nih.niehs.notification.api; + +import java.io.IOException; + +import javax.servlet.*; +import javax.servlet.http.HttpServletResponse; + +public class ApiOriginFilter implements javax.servlet.Filter { + @Override + public void doFilter(ServletRequest request, ServletResponse response, + FilterChain chain) throws IOException, ServletException { + HttpServletResponse res = (HttpServletResponse) response; + res.addHeader("Access-Control-Allow-Origin", "*"); + res.addHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT"); + res.addHeader("Access-Control-Allow-Headers", "Content-Type"); + chain.doFilter(request, response); + } + + @Override + public void destroy() { + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + } +} diff --git a/src/main/java/gov/nih/niehs/notification/api/ApiResponseMessage.java b/src/main/java/gov/nih/niehs/notification/api/ApiResponseMessage.java new file mode 100644 index 0000000..8fed828 --- /dev/null +++ b/src/main/java/gov/nih/niehs/notification/api/ApiResponseMessage.java @@ -0,0 +1,69 @@ +package gov.nih.niehs.notification.api; + + +import javax.xml.bind.annotation.XmlTransient; + +@javax.xml.bind.annotation.XmlRootElement +public class ApiResponseMessage { + public static final int ERROR = 1; + public static final int WARNING = 2; + public static final int INFO = 3; + public static final int OK = 4; + public static final int TOO_BUSY = 5; + + int code; + String type; + String message; + + public ApiResponseMessage(){} + + public ApiResponseMessage(int code, String message){ + this.code = code; + switch(code){ + case ERROR: + setType("error"); + break; + case WARNING: + setType("warning"); + break; + case INFO: + setType("info"); + break; + case OK: + setType("ok"); + break; + case TOO_BUSY: + setType("too busy"); + break; + default: + setType("unknown"); + break; + } + this.message = message; + } + + @XmlTransient + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/src/main/java/gov/nih/niehs/notification/api/NotFoundException.java b/src/main/java/gov/nih/niehs/notification/api/NotFoundException.java new file mode 100644 index 0000000..90ccb81 --- /dev/null +++ b/src/main/java/gov/nih/niehs/notification/api/NotFoundException.java @@ -0,0 +1,9 @@ +package gov.nih.niehs.notification.api; + +public class NotFoundException extends ApiException { + private int code; + public NotFoundException (int code, String msg) { + super(code, msg); + this.code = code; + } +} diff --git a/src/main/java/gov/nih/niehs/notification/api/NotificationApi.java b/src/main/java/gov/nih/niehs/notification/api/NotificationApi.java new file mode 100644 index 0000000..986ecdc --- /dev/null +++ b/src/main/java/gov/nih/niehs/notification/api/NotificationApi.java @@ -0,0 +1,74 @@ +package gov.nih.niehs.notification.api; + +import javax.validation.Valid; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; + +import gov.nih.niehs.notification.model.AddNotification; +import gov.nih.niehs.notification.model.BulkNotificationOptStatus; +import gov.nih.niehs.notification.model.NotificationArray; +import gov.nih.niehs.notification.model.UuidList; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import io.swagger.annotations.Authorization; + +@Api(value = "notification", description = "the notification API") +public interface NotificationApi { + + @ApiOperation(value = "Add new notification", nickname = "addNotification", notes = "Query database to insert new notification", authorizations = { + @Authorization(value = "BearerAuth") }, tags = { "Notification", }) + @ApiResponses(value = { @ApiResponse(code = 201, message = "notification added successfully"), + @ApiResponse(code = 400, message = "bad request") }) + @RequestMapping(value = "/addNotification", consumes = { "application/json" }, method = RequestMethod.POST) + ResponseEntity addNotification( + @ApiParam(value = "Schema to add new notification") @Valid @RequestBody AddNotification body); + + @ApiOperation(value = "Reterieve notifications", nickname = "getNotifications", notes = "Query database to get all the notifications for session user", response = NotificationArray.class, authorizations = { + @Authorization(value = "BearerAuth") }, tags = { "Notification", }) + @ApiResponses(value = { + @ApiResponse(code = 200, message = "List of all notifications", response = NotificationArray.class) }) + @RequestMapping(value = "/getNotification", produces = { "application/json" }, method = RequestMethod.GET) + ResponseEntity getNotifications(); + + @ApiOperation(value = "Update notification as seen", nickname = "markSeen", notes = "Query database to mark of notifications as seen once user is validated of ownership", response = BulkNotificationOptStatus.class, authorizations = { + @Authorization(value = "BearerAuth") }, tags = { "Notification", }) + @ApiResponses(value = { + @ApiResponse(code = 200, message = "notifications marked seen successfully", response = BulkNotificationOptStatus.class), + @ApiResponse(code = 409, message = "conflict in update", response = BulkNotificationOptStatus.class) }) + @RequestMapping(value = "/markSeen", produces = { "application/json" }, consumes = { + "application/json" }, method = RequestMethod.PUT) + ResponseEntity markSeen( + @ApiParam(value = "List of uuids one for each notification") @Valid @RequestBody UuidList body); + + @ApiOperation(value = "Reterieve all unseen notifications", nickname = "getUnseenNotifications", notes = "Query database to get all unseen notifications for session user", response = NotificationArray.class, authorizations = { + @Authorization(value = "BearerAuth") }, tags = { "Notification", }) + @ApiResponses(value = { + @ApiResponse(code = 200, message = "List of all notifications", response = NotificationArray.class) }) + @RequestMapping(value = "/getUnseenNotifications", produces = { "application/json" }, method = RequestMethod.GET) + ResponseEntity getUnseenNotifications(); + + @ApiOperation(value = "Reterieve all unseen notifications", nickname = "getUnseenNotificationsCount", notes = "Query database to get all unseen notifications for session user", response = Integer.class, authorizations = { + @Authorization(value = "BearerAuth") }, tags = { "Notification", }) + @ApiResponses(value = { + @ApiResponse(code = 200, message = "count reterieved sucessfully", response = Integer.class) }) + @RequestMapping(value = "/getUnseenNotificationsCount", produces = { + "application/json" }, method = RequestMethod.GET) + ResponseEntity getUnseenNotificationsCount(); + + @ApiOperation(value = "Delete notifications", nickname = "deleteNotifications", notes = "Query database to delete notifications once user is validated of ownership", response = BulkNotificationOptStatus.class, authorizations = { + @Authorization(value = "BearerAuth") }, tags = { "Notification", }) + @ApiResponses(value = { + @ApiResponse(code = 200, message = "notifications marked seen successfully", response = BulkNotificationOptStatus.class), + @ApiResponse(code = 409, message = "conflict in update", response = BulkNotificationOptStatus.class) }) + @RequestMapping(value = "/deleteNotifications", produces = { "application/json" }, consumes = { + "application/json" }, method = RequestMethod.PUT) + ResponseEntity deleteNotifications( + @ApiParam(value = "List of uuids one for each notification") @Valid @RequestBody UuidList body); + +} \ No newline at end of file diff --git a/src/main/java/gov/nih/niehs/notification/api/NotificationApiController.java b/src/main/java/gov/nih/niehs/notification/api/NotificationApiController.java new file mode 100644 index 0000000..09dea3c --- /dev/null +++ b/src/main/java/gov/nih/niehs/notification/api/NotificationApiController.java @@ -0,0 +1,133 @@ +package gov.nih.niehs.notification.api; + +import javax.servlet.http.HttpServletRequest; +import javax.validation.Valid; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestBody; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import gov.nih.niehs.notification.model.AddNotification; +import gov.nih.niehs.notification.model.BulkNotificationOptStatus; +import gov.nih.niehs.notification.model.BulkNotificationOptStatusInner; +import gov.nih.niehs.notification.model.NotificationArray; +import gov.nih.niehs.notification.model.UuidList; +import gov.nih.niehs.notification.security.UserDetails; +import gov.nih.niehs.notification.service.NotificationServiceImpl; +import io.swagger.annotations.ApiParam; + +@Controller +public class NotificationApiController implements NotificationApi { + + private static final Logger logger = LoggerFactory.getLogger(NotificationApiController.class); + + private final ObjectMapper objectMapper; + + private final HttpServletRequest request; + + @Autowired + public NotificationApiController(ObjectMapper objectMapper, HttpServletRequest request) { + this.objectMapper = objectMapper; + this.request = request; + } + + @Autowired + private NotificationServiceImpl notificationService; + + @Autowired + private UserDetails userDetails; + + public ResponseEntity addNotification( + @ApiParam(value = "Schema to add new notification") @Valid @RequestBody AddNotification body) { + logger.info("controller addNotification()"); + String sessionUser = userDetails.getSessionUserId(); + logger.info("sessionUser: {}", sessionUser); + logger.info("add notification: {}", body); + Integer status = notificationService.addNotification(userDetails.getSessionUserId(), body.getRecipientId(), + body.getSubject(), body.getMessage(), body.getSeverityLevel(), body.getNotificationType(), + body.getDataLocationUrl()); + if (status > 0) { + logger.info("Notification added successfully"); + return new ResponseEntity<>(HttpStatus.CREATED); + } else { + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + } + + public ResponseEntity getNotifications() { + logger.info("controller getNotifications()"); + String sessionUser = userDetails.getSessionUserId(); + logger.info("Retrieve notifications for session user: {}", sessionUser); + NotificationArray notifications = notificationService.getAllNotifications(sessionUser); + return new ResponseEntity(notifications, HttpStatus.OK); + } + + public ResponseEntity markSeen( + @ApiParam(value = "List of uuids one for each notification") @Valid @RequestBody UuidList body) { + logger.info("contoller markseen()"); + String sessionUser = userDetails.getSessionUserId(); + logger.info("User to validate ownership. userId: {}", sessionUser); + logger.info("markSeen notification uuidList: {}", body); + BulkNotificationOptStatus status = notificationService.markSeen(sessionUser, body); + logger.info("request status: {}", status); + + // validating update status + Integer updateCount = 0; + for (BulkNotificationOptStatusInner entry : status) { + if (entry.isOperationStatus()) { + updateCount++; + } + } + if (updateCount == body.size()) { + return new ResponseEntity(status, HttpStatus.OK); + } else { + return new ResponseEntity(status, HttpStatus.CONFLICT); + } + + } + + public ResponseEntity getUnseenNotifications() { + logger.info("controller getUnseenNotifications()"); + String sessionUser = userDetails.getSessionUserId(); + logger.info("Reterieve unseen notifications for session user: {}", sessionUser); + NotificationArray notifications = notificationService.getUnseenNotifications(sessionUser); + return new ResponseEntity(notifications, HttpStatus.OK); + } + + public ResponseEntity getUnseenNotificationsCount() { + logger.info("controller getUnseenNotificationsCount()"); + String sessionUser = userDetails.getSessionUserId(); + logger.info("Reterieve unseen notifications count for session user: {}", sessionUser); + Integer count = notificationService.getUnseenNotificationsCount(sessionUser); + return new ResponseEntity(count, HttpStatus.OK); + } + + public ResponseEntity deleteNotifications( + @ApiParam(value = "List of uuids one for each notification") @Valid @RequestBody UuidList body) { + logger.info("controller deleteNotifications()"); + String sessionUser = userDetails.getSessionUserId(); + logger.info("User to validate ownership. userId: {}", sessionUser); + logger.info("markSeen notification uuidList: {}", body); + BulkNotificationOptStatus status = notificationService.deleteNotifications(sessionUser, body); + logger.info("request status: {}", status); + + // validating update status + Integer updateCount = 0; + for (BulkNotificationOptStatusInner entry : status) { + if (entry.isOperationStatus()) { + updateCount++; + } + } + if (updateCount == body.size()) { + return new ResponseEntity(status, HttpStatus.OK); + } else { + return new ResponseEntity(status, HttpStatus.CONFLICT); + } + } +} diff --git a/src/main/java/gov/nih/niehs/notification/configuration/CustomInstantDeserializer.java b/src/main/java/gov/nih/niehs/notification/configuration/CustomInstantDeserializer.java new file mode 100644 index 0000000..20323d5 --- /dev/null +++ b/src/main/java/gov/nih/niehs/notification/configuration/CustomInstantDeserializer.java @@ -0,0 +1,231 @@ +package gov.nih.niehs.notification.configuration; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonTokenId; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.datatype.threetenbp.DateTimeUtils; +import com.fasterxml.jackson.datatype.threetenbp.DecimalUtils; +import com.fasterxml.jackson.datatype.threetenbp.deser.ThreeTenDateTimeDeserializerBase; +import com.fasterxml.jackson.datatype.threetenbp.function.BiFunction; +import com.fasterxml.jackson.datatype.threetenbp.function.Function; +import org.threeten.bp.DateTimeException; +import org.threeten.bp.Instant; +import org.threeten.bp.OffsetDateTime; +import org.threeten.bp.ZoneId; +import org.threeten.bp.ZonedDateTime; +import org.threeten.bp.format.DateTimeFormatter; +import org.threeten.bp.temporal.Temporal; +import org.threeten.bp.temporal.TemporalAccessor; + +import java.io.IOException; +import java.math.BigDecimal; + +/** + * Deserializer for ThreeTen temporal {@link Instant}s, {@link OffsetDateTime}, and {@link ZonedDateTime}s. + * Adapted from the jackson threetenbp InstantDeserializer to add support for deserializing rfc822 format. + * + * @author Nick Williams + */ +public class CustomInstantDeserializer + extends ThreeTenDateTimeDeserializerBase { + private static final long serialVersionUID = 1L; + + public static final CustomInstantDeserializer INSTANT = new CustomInstantDeserializer( + Instant.class, DateTimeFormatter.ISO_INSTANT, + new Function() { + @Override + public Instant apply(TemporalAccessor temporalAccessor) { + return Instant.from(temporalAccessor); + } + }, + new Function() { + @Override + public Instant apply(FromIntegerArguments a) { + return Instant.ofEpochMilli(a.value); + } + }, + new Function() { + @Override + public Instant apply(FromDecimalArguments a) { + return Instant.ofEpochSecond(a.integer, a.fraction); + } + }, + null + ); + + public static final CustomInstantDeserializer OFFSET_DATE_TIME = new CustomInstantDeserializer( + OffsetDateTime.class, DateTimeFormatter.ISO_OFFSET_DATE_TIME, + new Function() { + @Override + public OffsetDateTime apply(TemporalAccessor temporalAccessor) { + return OffsetDateTime.from(temporalAccessor); + } + }, + new Function() { + @Override + public OffsetDateTime apply(FromIntegerArguments a) { + return OffsetDateTime.ofInstant(Instant.ofEpochMilli(a.value), a.zoneId); + } + }, + new Function() { + @Override + public OffsetDateTime apply(FromDecimalArguments a) { + return OffsetDateTime.ofInstant(Instant.ofEpochSecond(a.integer, a.fraction), a.zoneId); + } + }, + new BiFunction() { + @Override + public OffsetDateTime apply(OffsetDateTime d, ZoneId z) { + return d.withOffsetSameInstant(z.getRules().getOffset(d.toLocalDateTime())); + } + } + ); + + public static final CustomInstantDeserializer ZONED_DATE_TIME = new CustomInstantDeserializer( + ZonedDateTime.class, DateTimeFormatter.ISO_ZONED_DATE_TIME, + new Function() { + @Override + public ZonedDateTime apply(TemporalAccessor temporalAccessor) { + return ZonedDateTime.from(temporalAccessor); + } + }, + new Function() { + @Override + public ZonedDateTime apply(FromIntegerArguments a) { + return ZonedDateTime.ofInstant(Instant.ofEpochMilli(a.value), a.zoneId); + } + }, + new Function() { + @Override + public ZonedDateTime apply(FromDecimalArguments a) { + return ZonedDateTime.ofInstant(Instant.ofEpochSecond(a.integer, a.fraction), a.zoneId); + } + }, + new BiFunction() { + @Override + public ZonedDateTime apply(ZonedDateTime zonedDateTime, ZoneId zoneId) { + return zonedDateTime.withZoneSameInstant(zoneId); + } + } + ); + + protected final Function fromMilliseconds; + + protected final Function fromNanoseconds; + + protected final Function parsedToValue; + + protected final BiFunction adjust; + + protected CustomInstantDeserializer(Class supportedType, + DateTimeFormatter parser, + Function parsedToValue, + Function fromMilliseconds, + Function fromNanoseconds, + BiFunction adjust) { + super(supportedType, parser); + this.parsedToValue = parsedToValue; + this.fromMilliseconds = fromMilliseconds; + this.fromNanoseconds = fromNanoseconds; + this.adjust = adjust == null ? new BiFunction() { + @Override + public T apply(T t, ZoneId zoneId) { + return t; + } + } : adjust; + } + + @SuppressWarnings("unchecked") + protected CustomInstantDeserializer(CustomInstantDeserializer base, DateTimeFormatter f) { + super((Class) base.handledType(), f); + parsedToValue = base.parsedToValue; + fromMilliseconds = base.fromMilliseconds; + fromNanoseconds = base.fromNanoseconds; + adjust = base.adjust; + } + + @Override + protected JsonDeserializer withDateFormat(DateTimeFormatter dtf) { + if (dtf == _formatter) { + return this; + } + return new CustomInstantDeserializer(this, dtf); + } + + @Override + public T deserialize(JsonParser parser, DeserializationContext context) throws IOException { + //NOTE: Timestamps contain no timezone info, and are always in configured TZ. Only + //string values have to be adjusted to the configured TZ. + switch (parser.getCurrentTokenId()) { + case JsonTokenId.ID_NUMBER_FLOAT: { + BigDecimal value = parser.getDecimalValue(); + long seconds = value.longValue(); + int nanoseconds = DecimalUtils.extractNanosecondDecimal(value, seconds); + return fromNanoseconds.apply(new FromDecimalArguments( + seconds, nanoseconds, getZone(context))); + } + + case JsonTokenId.ID_NUMBER_INT: { + long timestamp = parser.getLongValue(); + if (context.isEnabled(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS)) { + return this.fromNanoseconds.apply(new FromDecimalArguments( + timestamp, 0, this.getZone(context) + )); + } + return this.fromMilliseconds.apply(new FromIntegerArguments( + timestamp, this.getZone(context) + )); + } + + case JsonTokenId.ID_STRING: { + String string = parser.getText().trim(); + if (string.length() == 0) { + return null; + } + if (string.endsWith("+0000")) { + string = string.substring(0, string.length() - 5) + "Z"; + } + T value; + try { + TemporalAccessor acc = _formatter.parse(string); + value = parsedToValue.apply(acc); + if (context.isEnabled(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE)) { + return adjust.apply(value, this.getZone(context)); + } + } catch (DateTimeException e) { + throw _peelDTE(e); + } + return value; + } + } + throw context.mappingException("Expected type float, integer, or string."); + } + + private ZoneId getZone(DeserializationContext context) { + // Instants are always in UTC, so don't waste compute cycles + return (_valueClass == Instant.class) ? null : DateTimeUtils.timeZoneToZoneId(context.getTimeZone()); + } + + private static class FromIntegerArguments { + public final long value; + public final ZoneId zoneId; + + private FromIntegerArguments(long value, ZoneId zoneId) { + this.value = value; + this.zoneId = zoneId; + } + } + + private static class FromDecimalArguments { + public final long integer; + public final int fraction; + public final ZoneId zoneId; + + private FromDecimalArguments(long integer, int fraction, ZoneId zoneId) { + this.integer = integer; + this.fraction = fraction; + this.zoneId = zoneId; + } + } +} diff --git a/src/main/java/gov/nih/niehs/notification/configuration/HomeController.java b/src/main/java/gov/nih/niehs/notification/configuration/HomeController.java new file mode 100644 index 0000000..91b2b34 --- /dev/null +++ b/src/main/java/gov/nih/niehs/notification/configuration/HomeController.java @@ -0,0 +1,16 @@ +package gov.nih.niehs.notification.configuration; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; + +/** + * Home redirection to swagger api documentation + */ +@Controller +public class HomeController { + @RequestMapping(value = "/") + public String index() { + System.out.println("swagger-ui.html"); + return "redirect:swagger-ui.html"; + } +} diff --git a/src/main/java/gov/nih/niehs/notification/configuration/JacksonConfiguration.java b/src/main/java/gov/nih/niehs/notification/configuration/JacksonConfiguration.java new file mode 100644 index 0000000..b5ddf93 --- /dev/null +++ b/src/main/java/gov/nih/niehs/notification/configuration/JacksonConfiguration.java @@ -0,0 +1,23 @@ +package gov.nih.niehs.notification.configuration; + +import com.fasterxml.jackson.datatype.threetenbp.ThreeTenModule; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.threeten.bp.Instant; +import org.threeten.bp.OffsetDateTime; +import org.threeten.bp.ZonedDateTime; + +@Configuration +public class JacksonConfiguration { + + @Bean + @ConditionalOnMissingBean(ThreeTenModule.class) + ThreeTenModule threeTenModule() { + ThreeTenModule module = new ThreeTenModule(); + module.addDeserializer(Instant.class, CustomInstantDeserializer.INSTANT); + module.addDeserializer(OffsetDateTime.class, CustomInstantDeserializer.OFFSET_DATE_TIME); + module.addDeserializer(ZonedDateTime.class, CustomInstantDeserializer.ZONED_DATE_TIME); + return module; + } +} diff --git a/src/main/java/gov/nih/niehs/notification/configuration/SwaggerDocumentationConfig.java b/src/main/java/gov/nih/niehs/notification/configuration/SwaggerDocumentationConfig.java new file mode 100644 index 0000000..8f3bc30 --- /dev/null +++ b/src/main/java/gov/nih/niehs/notification/configuration/SwaggerDocumentationConfig.java @@ -0,0 +1,39 @@ +package gov.nih.niehs.notification.configuration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.service.Contact; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; + +@Configuration +public class SwaggerDocumentationConfig { + + ApiInfo apiInfo() { + return new ApiInfoBuilder() + .title("Notification Service API") + .description("This is a notification microservice for Metalnx") + .license("Apache 2.0") + .licenseUrl("http://www.apache.org/licenses/LICENSE-2.0.html") + .termsOfServiceUrl("") + .version("1.0.0") + .contact(new Contact("","", "deep.patel@nih.gov")) + .build(); + } + + @Bean + public Docket customImplementation(){ + return new Docket(DocumentationType.SWAGGER_2) + .select() + .apis(RequestHandlerSelectors.basePackage("gov.nih.niehs.notification.api")) + .build() + .directModelSubstitute(org.threeten.bp.LocalDate.class, java.sql.Date.class) + .directModelSubstitute(org.threeten.bp.OffsetDateTime.class, java.util.Date.class) + .apiInfo(apiInfo()); + } + +} diff --git a/src/main/java/gov/nih/niehs/notification/interfaces/NotificationRepository.java b/src/main/java/gov/nih/niehs/notification/interfaces/NotificationRepository.java new file mode 100644 index 0000000..ab01190 --- /dev/null +++ b/src/main/java/gov/nih/niehs/notification/interfaces/NotificationRepository.java @@ -0,0 +1,45 @@ +package gov.nih.niehs.notification.interfaces; + +import java.util.UUID; + +import javax.transaction.Transactional; + +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; + +import gov.nih.niehs.notification.model.Notification; +import gov.nih.niehs.notification.model.NotificationArray; + +public interface NotificationRepository extends CrudRepository { + + @Transactional + @Modifying + @Query(value = "INSERT INTO public.notification (sender_id, recipient_id, subject, message, severity_level, notification_type, data_location_url) values(:sender_id, :recipient_id, :subject, :message, :severity_level, :notification_type, :data_location_url)", nativeQuery = true) + public Integer addNotification(@Param("sender_id") String sender_id, @Param("recipient_id") String recipient_id, + @Param("subject") String subject, @Param("message") String message, + @Param("severity_level") Integer severity_level, @Param("notification_type") String notification_type, + @Param("data_location_url") String data_location_url); + + @Query(value = "SELECT n.* FROM public.notification as n WHERE n.recipient_id= :recipient_id", nativeQuery = true) + public NotificationArray getAllNotification(@Param("recipient_id") String recipient_id); + + @Transactional + @Modifying + @Query(value = "UPDATE public.notification SET seen= :seenFlag WHERE notification_id= :notification_id and recipient_id= :recipient_id", nativeQuery = true) + public Integer markSeen(@Param("recipient_id") String recipient_id, @Param("notification_id") UUID notification_id, + @Param("seenFlag") Boolean seenFlag); + + @Query(value = "SELECT n.* FROM public.notification as n WHERE n.recipient_id= :recipient_id and n.seen= false", nativeQuery = true) + public NotificationArray getUnseenNotifications(@Param("recipient_id") String recipient_id); + + @Query(value = "SELECT COUNT(n) FROM public.notification as n WHERE n.recipient_id= :recipient_id and n.seen= false", nativeQuery = true) + public Integer getUnseenNotificationsCount(@Param("recipient_id") String recipient_id); + + @Transactional + @Modifying + @Query(value = "UPDATE public.notification SET deleted= :deletedFlag WHERE notification_id= :notification_id and recipient_id= :recipient_id", nativeQuery = true) + public Integer deleteNotifications(@Param("recipient_id") String recipient_id, + @Param("notification_id") UUID notification_id, @Param("deletedFlag") Boolean deletedFlag); +} diff --git a/src/main/java/gov/nih/niehs/notification/interfaces/NotificationService.java b/src/main/java/gov/nih/niehs/notification/interfaces/NotificationService.java new file mode 100644 index 0000000..f0ade82 --- /dev/null +++ b/src/main/java/gov/nih/niehs/notification/interfaces/NotificationService.java @@ -0,0 +1,20 @@ +package gov.nih.niehs.notification.interfaces; + +import gov.nih.niehs.notification.model.BulkNotificationOptStatus; +import gov.nih.niehs.notification.model.NotificationArray; +import gov.nih.niehs.notification.model.UuidList; + +public interface NotificationService { + public Integer addNotification(String senderId, String recipientId, String subject, String message, + Integer severityLevel, String notificationType, String dataLocationUrl); + + public NotificationArray getAllNotifications(String userId); + + public BulkNotificationOptStatus markSeen(String userId, UuidList uuid); + + public NotificationArray getUnseenNotifications(String userId); + + public Integer getUnseenNotificationsCount(String userId); + + public BulkNotificationOptStatus deleteNotifications(String userId, UuidList uuid); +} diff --git a/src/main/java/gov/nih/niehs/notification/model/AddNotification.java b/src/main/java/gov/nih/niehs/notification/model/AddNotification.java new file mode 100644 index 0000000..24668bf --- /dev/null +++ b/src/main/java/gov/nih/niehs/notification/model/AddNotification.java @@ -0,0 +1,209 @@ +package gov.nih.niehs.notification.model; + +import java.util.Objects; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; + +import org.springframework.validation.annotation.Validated; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.swagger.annotations.ApiModelProperty; + +/** + * AddNotification + */ +@Validated +public class AddNotification { + @JsonProperty("recipient_id") + private String recipientId = null; + + @JsonProperty("subject") + private String subject = null; + + @JsonProperty("message") + private String message = null; + + @JsonProperty("severity_level") + private Integer severityLevel = null; + + @JsonProperty("notification_type") + private String notificationType = null; + + @JsonProperty("data_location_url") + private String dataLocationUrl = null; + + public AddNotification recipientId(String recipientId) { + this.recipientId = recipientId; + return this; + } + + /** + * notification recipient's userID + * + * @return recipientId + **/ + @ApiModelProperty(required = true, value = "notification recipient's userId") + @NotNull + + public String getRecipientId() { + return recipientId; + } + + public void setRecipientId(String recipientId) { + this.recipientId = recipientId; + } + + public AddNotification subject(String subject) { + this.subject = subject; + return this; + } + + /** + * notification subject + * + * @return subject + **/ + @ApiModelProperty(required = true, value = "notification subject") + @NotNull + + public String getSubject() { + return subject; + } + + public void setSubject(String subject) { + this.subject = subject; + } + + public AddNotification message(String message) { + this.message = message; + return this; + } + + /** + * notification message content + * + * @return message + **/ + @ApiModelProperty(required = true, value = "notification message content") + @NotNull + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public AddNotification severityLevel(Integer severityLevel) { + this.severityLevel = severityLevel; + return this; + } + + /** + * severity level of notification an integer value between 1 and 5 minimum: 1 + * maximum: 5 + * + * @return severityLevel + **/ + @ApiModelProperty(required = true, value = "severity level of notification an integer value between 1 and 5") + @NotNull + + @Min(1) + @Max(5) + public Integer getSeverityLevel() { + return severityLevel; + } + + public void setSeverityLevel(Integer severityLevel) { + this.severityLevel = severityLevel; + } + + public AddNotification notificationType(String notificationType) { + this.notificationType = notificationType; + return this; + } + + /** + * type of notification options are workflow, permission, system + * + * @return notificationType + **/ + @ApiModelProperty(required = true, value = "type of notification options are workflow, permission, system") + @NotNull + + public String getNotificationType() { + return notificationType; + } + + public void setNotificationType(String notificationType) { + this.notificationType = notificationType; + } + + public AddNotification dataLocationUrl(String dataLocationUrl) { + this.dataLocationUrl = dataLocationUrl; + return this; + } + + /** + * notification associated logical location of data + * + * @return dataLocationUrl + **/ + @ApiModelProperty(value = "notification associated logical location of data") + + public String getDataLocationUrl() { + return dataLocationUrl; + } + + @Override + public boolean equals(java.lang.Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + AddNotification addNotification = (AddNotification) o; + return Objects.equals(this.recipientId, addNotification.recipientId) + && Objects.equals(this.subject, addNotification.subject) + && Objects.equals(this.message, addNotification.message) + && Objects.equals(this.severityLevel, addNotification.severityLevel) + && Objects.equals(this.notificationType, addNotification.notificationType) + && Objects.equals(this.dataLocationUrl, addNotification.dataLocationUrl); + } + + @Override + public int hashCode() { + return Objects.hash(recipientId, subject, message, severityLevel, notificationType, dataLocationUrl); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class AddNotification {\n"); + + sb.append(" recipientId: ").append(toIndentedString(recipientId)).append("\n"); + sb.append(" subject: ").append(toIndentedString(subject)).append("\n"); + sb.append(" message: ").append(toIndentedString(message)).append("\n"); + sb.append(" severityLevel: ").append(toIndentedString(severityLevel)).append("\n"); + sb.append(" notificationType: ").append(toIndentedString(notificationType)).append("\n"); + sb.append(" dataLocationUrl: ").append(toIndentedString(dataLocationUrl)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(java.lang.Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} diff --git a/src/main/java/gov/nih/niehs/notification/model/BulkNotificationOptStatus.java b/src/main/java/gov/nih/niehs/notification/model/BulkNotificationOptStatus.java new file mode 100644 index 0000000..cc59cd9 --- /dev/null +++ b/src/main/java/gov/nih/niehs/notification/model/BulkNotificationOptStatus.java @@ -0,0 +1,50 @@ +package gov.nih.niehs.notification.model; + +import java.util.ArrayList; +import java.util.Objects; + +import org.springframework.validation.annotation.Validated; + +/** + * BulkNotificationOptStatus + */ +@Validated +@javax.annotation.Generated(value = "io.swagger.codegen.v3.generators.java.SpringCodegen", date = "2019-12-06T14:02:52.798Z[GMT]") +public class BulkNotificationOptStatus extends ArrayList { + + @Override + public boolean equals(java.lang.Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + return true; + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode()); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class BulkNotificationOptStatus {\n"); + sb.append(" ").append(toIndentedString(super.toString())).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(java.lang.Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} diff --git a/src/main/java/gov/nih/niehs/notification/model/BulkNotificationOptStatusInner.java b/src/main/java/gov/nih/niehs/notification/model/BulkNotificationOptStatusInner.java new file mode 100644 index 0000000..4a59e4c --- /dev/null +++ b/src/main/java/gov/nih/niehs/notification/model/BulkNotificationOptStatusInner.java @@ -0,0 +1,115 @@ +package gov.nih.niehs.notification.model; + +import java.util.Objects; +import java.util.UUID; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; + +import org.springframework.validation.annotation.Validated; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.swagger.annotations.ApiModelProperty; + +/** + * BulkNotificationOptStatusInner + */ +@Validated +public class BulkNotificationOptStatusInner { + + public BulkNotificationOptStatusInner(UUID notificationId, Boolean operationStatus) { + super(); + this.notificationId = notificationId; + this.operationStatus = operationStatus; + } + + @JsonProperty("notification_id") + private UUID notificationId = null; + + @JsonProperty("operation_status") + private Boolean operationStatus = null; + + public BulkNotificationOptStatusInner notificationId(UUID notificationId) { + this.notificationId = notificationId; + return this; + } + + /** + * an unique uuid auto-generated by postgres db + * + * @return notificationId + **/ + @ApiModelProperty(required = true, value = "an unique uuid auto-generated by postgres db") + @NotNull + + @Valid + public UUID getNotificationId() { + return notificationId; + } + + public void setNotificationId(UUID notificationId) { + this.notificationId = notificationId; + } + + public BulkNotificationOptStatusInner operationStatus(Boolean operationStatus) { + this.operationStatus = operationStatus; + return this; + } + + /** + * Get operationStatus + * + * @return operationStatus + **/ + @ApiModelProperty(required = true, value = "") + @NotNull + + public Boolean isOperationStatus() { + return operationStatus; + } + + public void setOperationStatus(Boolean operationStatus) { + this.operationStatus = operationStatus; + } + + @Override + public boolean equals(java.lang.Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + BulkNotificationOptStatusInner bulkNotificationOptStatusInner = (BulkNotificationOptStatusInner) o; + return Objects.equals(this.notificationId, bulkNotificationOptStatusInner.notificationId) + && Objects.equals(this.operationStatus, bulkNotificationOptStatusInner.operationStatus); + } + + @Override + public int hashCode() { + return Objects.hash(notificationId, operationStatus); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class BulkNotificationOptStatusInner {\n"); + + sb.append(" notificationId: ").append(toIndentedString(notificationId)).append("\n"); + sb.append(" operationStatus: ").append(toIndentedString(operationStatus)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(java.lang.Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} diff --git a/src/main/java/gov/nih/niehs/notification/model/Count.java b/src/main/java/gov/nih/niehs/notification/model/Count.java new file mode 100644 index 0000000..6137f46 --- /dev/null +++ b/src/main/java/gov/nih/niehs/notification/model/Count.java @@ -0,0 +1,52 @@ +package gov.nih.niehs.notification.model; + +import java.util.Objects; + +import org.springframework.validation.annotation.Validated; + +import io.swagger.annotations.ApiModel; + +/** + * count of unseen notifications + */ +@ApiModel(description = "count of unseen notifications") +@Validated +@javax.annotation.Generated(value = "io.swagger.codegen.v3.generators.java.SpringCodegen", date = "2019-12-09T17:17:18.165Z[GMT]") +public class Count { + + @Override + public boolean equals(java.lang.Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + return true; + } + + @Override + public int hashCode() { + return Objects.hash(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class Count {\n"); + + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(java.lang.Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} diff --git a/src/main/java/gov/nih/niehs/notification/model/Notification.java b/src/main/java/gov/nih/niehs/notification/model/Notification.java new file mode 100644 index 0000000..125b442 --- /dev/null +++ b/src/main/java/gov/nih/niehs/notification/model/Notification.java @@ -0,0 +1,362 @@ +package gov.nih.niehs.notification.model; + +import java.sql.Timestamp; +import java.util.Objects; +import java.util.UUID; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.validation.Valid; + +import org.springframework.validation.annotation.Validated; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.swagger.annotations.ApiModelProperty; + +/** + * Notification + */ +@Validated +@Entity +public class Notification { + @JsonProperty("id") + @Id + private Integer id = null; + + @JsonProperty("notification_id") + private UUID notificationId = null; + + @JsonProperty("sender_id") + private String senderId = null; + + @JsonProperty("recipient_id") + private String recipientId = null; + + @JsonProperty("subject") + private String subject = null; + + @JsonProperty("message") + private String message = null; + + @JsonProperty("seen") + private Boolean seen = false; + + @JsonProperty("deleted") + private Boolean deleted = false; + + @JsonProperty("date_created") + private Timestamp dateCreated = null; + + @JsonProperty("severity_level") + private Integer severityLevel = null; + + @JsonProperty("notification_type") + private String notificationType = null; + + @JsonProperty("data_location_url") + private String dataLocationUrl = null; + + public Notification id(Integer id) { + this.id = id; + return this; + } + + /** + * an auto-generated sequence by postgres db + * + * @return id + **/ + @ApiModelProperty(value = "an auto-generated sequence by postgres db") + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Notification notificationId(UUID notificationId) { + this.notificationId = notificationId; + return this; + } + + /** + * an unique uuid auto-generated by postgres db + * + * @return notificationId + **/ + @ApiModelProperty(value = "an unique uuid auto-generated by postgres db") + + @Valid + public UUID getNotificationId() { + return notificationId; + } + + public void setNotificationId(UUID notificationId) { + this.notificationId = notificationId; + } + + public Notification senderId(String senderId) { + this.senderId = senderId; + return this; + } + + /** + * notification sender's userId + * + * @return senderId + **/ + @ApiModelProperty(value = "notification sender's userId") + + public String getSenderId() { + return senderId; + } + + public void setSenderId(String senderId) { + this.senderId = senderId; + } + + public Notification recipientId(String recipientId) { + this.recipientId = recipientId; + return this; + } + + /** + * notification recipient's userId + * + * @return recipientId + **/ + @ApiModelProperty(value = "notification recipient's userId") + + public String getRecipientId() { + return recipientId; + } + + public void setRecipientId(String recipientId) { + this.recipientId = recipientId; + } + + public Notification subject(String subject) { + this.subject = subject; + return this; + } + + /** + * notification subject + * + * @return subject + **/ + @ApiModelProperty(value = "notification subject") + + public String getSubject() { + return subject; + } + + public void setSubject(String subject) { + this.subject = subject; + } + + public Notification message(String message) { + this.message = message; + return this; + } + + /** + * notification message content + * + * @return message + **/ + @ApiModelProperty(value = "notification message content") + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public Notification seen(Boolean seen) { + this.seen = seen; + return this; + } + + /** + * boolean for user's seen/unseen notification status + * + * @return seen + **/ + @ApiModelProperty(value = "boolean for user's seen/unseen notification status") + + public Boolean isSeen() { + return seen; + } + + public void setSeen(Boolean seen) { + this.seen = seen; + } + + public Notification deleted(Boolean deleted) { + this.deleted = deleted; + return this; + } + + /** + * boolean for deleted notification, notification will not be deleted from + * postgres db + * + * @return deleted + **/ + @ApiModelProperty(value = "boolean for deleted notification, notification will not be deleted from postgres db") + + public Boolean isDeleted() { + return deleted; + } + + public void setDeleted(Boolean deleted) { + this.deleted = deleted; + } + + public Notification dateCreated(Timestamp dateCreated) { + this.dateCreated = dateCreated; + return this; + } + + /** + * timestamp auto-generated by postgres db + * + * @return dateCreated + **/ + @ApiModelProperty(value = "timestamp auto-generated by postgres db") + + @Valid + public Timestamp getDateCreated() { + return dateCreated; + } + + public void setDateCreated(Timestamp dateCreated) { + this.dateCreated = dateCreated; + } + + public Notification severityLevel(Integer severityLevel) { + this.severityLevel = severityLevel; + return this; + } + + /** + * severity level of notification an integer value between 1 and 5 + * + * @return severityLevel + **/ + @ApiModelProperty(value = "severity level of notification an integer value between 1 and 5") + + public Integer getSeverityLevel() { + return severityLevel; + } + + public void setSeverityLevel(Integer severityLevel) { + this.severityLevel = severityLevel; + } + + public Notification notificationType(String notificationType) { + this.notificationType = notificationType; + return this; + } + + /** + * type of notification options are workflow, permission, system + * + * @return notificationType + **/ + @ApiModelProperty(value = "type of notification options are workflow, permission, system") + + public String getNotificationType() { + return notificationType; + } + + public void setNotificationType(String notificationType) { + this.notificationType = notificationType; + } + + public Notification dataLocationUrl(String dataLocationUrl) { + this.dataLocationUrl = dataLocationUrl; + return this; + } + + /** + * notification associated logical location of data + * + * @return dataLocationUrl + **/ + @ApiModelProperty(value = "notification associated logical location of data") + + public String getDataLocationUrl() { + return dataLocationUrl; + } + + public void setDataLocationUrl(String dataLocationUrl) { + this.dataLocationUrl = dataLocationUrl; + } + + @Override + public boolean equals(java.lang.Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Notification notification = (Notification) o; + return Objects.equals(this.id, notification.id) + && Objects.equals(this.notificationId, notification.notificationId) + && Objects.equals(this.senderId, notification.senderId) + && Objects.equals(this.recipientId, notification.recipientId) + && Objects.equals(this.subject, notification.subject) + && Objects.equals(this.message, notification.message) && Objects.equals(this.seen, notification.seen) + && Objects.equals(this.deleted, notification.deleted) + && Objects.equals(this.dateCreated, notification.dateCreated) + && Objects.equals(this.severityLevel, notification.severityLevel) + && Objects.equals(this.notificationType, notification.notificationType) + && Objects.equals(this.dataLocationUrl, notification.dataLocationUrl); + } + + @Override + public int hashCode() { + return Objects.hash(id, notificationId, senderId, recipientId, subject, message, seen, deleted, dateCreated, + severityLevel, notificationType); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class Notification {\n"); + + sb.append(" id: ").append(toIndentedString(id)).append("\n"); + sb.append(" notificationId: ").append(toIndentedString(notificationId)).append("\n"); + sb.append(" senderId: ").append(toIndentedString(senderId)).append("\n"); + sb.append(" recipientId: ").append(toIndentedString(recipientId)).append("\n"); + sb.append(" subject: ").append(toIndentedString(subject)).append("\n"); + sb.append(" message: ").append(toIndentedString(message)).append("\n"); + sb.append(" seen: ").append(toIndentedString(seen)).append("\n"); + sb.append(" deleted: ").append(toIndentedString(deleted)).append("\n"); + sb.append(" dateCreated: ").append(toIndentedString(dateCreated)).append("\n"); + sb.append(" severityLevel: ").append(toIndentedString(severityLevel)).append("\n"); + sb.append(" notificationType: ").append(toIndentedString(notificationType)).append("\n"); + sb.append(" dataLocationUrl: ").append(toIndentedString(dataLocationUrl)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(java.lang.Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} diff --git a/src/main/java/gov/nih/niehs/notification/model/NotificationArray.java b/src/main/java/gov/nih/niehs/notification/model/NotificationArray.java new file mode 100644 index 0000000..fc059f1 --- /dev/null +++ b/src/main/java/gov/nih/niehs/notification/model/NotificationArray.java @@ -0,0 +1,49 @@ +package gov.nih.niehs.notification.model; + +import java.util.ArrayList; +import java.util.Objects; + +import org.springframework.validation.annotation.Validated; + +/** + * NotificationArray + */ +@Validated +public class NotificationArray extends ArrayList { + + @Override + public boolean equals(java.lang.Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + return true; + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode()); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class NotificationArray {\n"); + sb.append(" ").append(toIndentedString(super.toString())).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(java.lang.Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} diff --git a/src/main/java/gov/nih/niehs/notification/model/UuidList.java b/src/main/java/gov/nih/niehs/notification/model/UuidList.java new file mode 100644 index 0000000..70dd047 --- /dev/null +++ b/src/main/java/gov/nih/niehs/notification/model/UuidList.java @@ -0,0 +1,50 @@ +package gov.nih.niehs.notification.model; + +import java.util.ArrayList; +import java.util.Objects; +import java.util.UUID; + +import org.springframework.validation.annotation.Validated; + +/** + * UuidList + */ +@Validated +public class UuidList extends ArrayList { + + @Override + public boolean equals(java.lang.Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + return true; + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode()); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class UuidList {\n"); + sb.append(" ").append(toIndentedString(super.toString())).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(java.lang.Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} diff --git a/src/main/java/gov/nih/niehs/notification/security/JwtAuthEntryPoint.java b/src/main/java/gov/nih/niehs/notification/security/JwtAuthEntryPoint.java new file mode 100644 index 0000000..b4bfa74 --- /dev/null +++ b/src/main/java/gov/nih/niehs/notification/security/JwtAuthEntryPoint.java @@ -0,0 +1,27 @@ +package gov.nih.niehs.notification.security; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +@Component +public class JwtAuthEntryPoint implements AuthenticationEntryPoint { + + private static final Logger logger = LoggerFactory.getLogger(JwtAuthEntryPoint.class); + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) + throws IOException, ServletException { + + logger.error("Unauthorized error. Message - {}", e.getMessage()); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Error -> Unauthorized"); + } +} diff --git a/src/main/java/gov/nih/niehs/notification/security/JwtAuthFilter.java b/src/main/java/gov/nih/niehs/notification/security/JwtAuthFilter.java new file mode 100644 index 0000000..68af91b --- /dev/null +++ b/src/main/java/gov/nih/niehs/notification/security/JwtAuthFilter.java @@ -0,0 +1,64 @@ +package gov.nih.niehs.notification.security; + +import java.io.IOException; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.web.filter.OncePerRequestFilter; + +public class JwtAuthFilter extends OncePerRequestFilter { + + private static final Logger logger = LoggerFactory.getLogger(JwtAuthFilter.class); + + @Autowired + private JwtValidator jwtValidator; + + @Autowired + private UserDetails userDetails; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + logger.info("doFilterInternal(): filtering jwts on each request"); + try { + + String jwt = getJwt(request); + if (jwt != null && jwtValidator.validateJwtToken(jwt)) { + String username = jwtValidator.getUserNameFromJwtToken(jwt); + logger.info("username found in JWT: {}", username); + userDetails.setSessionUserId(username); + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, + null, null); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + + SecurityContextHolder.getContext().setAuthentication(authentication); + + } + } catch (Exception e) { + logger.error("Can NOT set user authentication -> Message: {}", e); + } + logger.info("request: " + request); + logger.info("response: " + response); + + filterChain.doFilter(request, response); + } + + private String getJwt(HttpServletRequest request) { + String authHeader = request.getHeader("Authorization"); + + if (authHeader != null && authHeader.startsWith("Bearer ")) { + return authHeader.replace("Bearer ", ""); + } + + return null; + } +} diff --git a/src/main/java/gov/nih/niehs/notification/security/JwtValidator.java b/src/main/java/gov/nih/niehs/notification/security/JwtValidator.java new file mode 100644 index 0000000..82d3aea --- /dev/null +++ b/src/main/java/gov/nih/niehs/notification/security/JwtValidator.java @@ -0,0 +1,44 @@ +package gov.nih.niehs.notification.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.UnsupportedJwtException; + +@Component +public class JwtValidator { + + private static final Logger logger = LoggerFactory.getLogger(JwtValidator.class); + + @Value("${app.jwtSecret}") + private String jwtSecret; + + public boolean validateJwtToken(String authToken) { + logger.info("validateJwtToken()"); + try { + Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken); + return true; + } catch (MalformedJwtException e) { + logger.error("Invalid JWT token -> Message: {}", e); + } catch (ExpiredJwtException e) { + logger.error("Expired JWT token -> Message: {}", e); + } catch (UnsupportedJwtException e) { + logger.error("Unsupported JWT token -> Message: {}", e); + } catch (IllegalArgumentException e) { + logger.error("JWT claims string is empty -> Message: {}", e); + } + logger.info("returning false"); + return false; + } + + public String getUserNameFromJwtToken(String token) { + logger.info("getUserNameFromJwtToken()"); + return Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody().getSubject(); + } + +} diff --git a/src/main/java/gov/nih/niehs/notification/security/SecurityConfiguration.java b/src/main/java/gov/nih/niehs/notification/security/SecurityConfiguration.java new file mode 100644 index 0000000..2382f8a --- /dev/null +++ b/src/main/java/gov/nih/niehs/notification/security/SecurityConfiguration.java @@ -0,0 +1,50 @@ +package gov.nih.niehs.notification.security; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +@EnableWebSecurity +@EnableGlobalMethodSecurity(securedEnabled = true) +public class SecurityConfiguration extends WebSecurityConfigurerAdapter { + + @Autowired + private JwtAuthEntryPoint unauthorizedHandler; + + @Bean + public JwtAuthFilter authenticationJwtTokenFilter() { + return new JwtAuthFilter(); + } + + private static final String[] AUTH_WHITELIST = { + // -- swagger ui + "/api-docs", "/swagger-resources", "/swagger-resources/**", "/configuration/ui", "/configuration/security", + "/swagger-ui.html", "/webjars/**" + // other public endpoints of your API may be appended to this array + }; + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.cors().and().csrf().disable().authorizeRequests().antMatchers(AUTH_WHITELIST).permitAll() + .antMatchers("/**").authenticated().and().exceptionHandling() + .authenticationEntryPoint(unauthorizedHandler).and().sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS); + + http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); + } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues()); + return source; + } +} diff --git a/src/main/java/gov/nih/niehs/notification/security/UserDetails.java b/src/main/java/gov/nih/niehs/notification/security/UserDetails.java new file mode 100644 index 0000000..d0bb6ec --- /dev/null +++ b/src/main/java/gov/nih/niehs/notification/security/UserDetails.java @@ -0,0 +1,31 @@ +package gov.nih.niehs.notification.security; + +import org.springframework.stereotype.Service; + +import io.swagger.annotations.ApiModelProperty; + +@Service +public class UserDetails { + + private String sessionUserId = null; + + public UserDetails sessionUserId(String sessionUserId) { + this.sessionUserId = sessionUserId; + return this; + } + + /** + * Get senderID + * + * @return senderID + **/ + @ApiModelProperty(value = "") + + public String getSessionUserId() { + return sessionUserId; + } + + public void setSessionUserId(String username) { + this.sessionUserId = username; + } +} diff --git a/src/main/java/gov/nih/niehs/notification/service/NotificationServiceImpl.java b/src/main/java/gov/nih/niehs/notification/service/NotificationServiceImpl.java new file mode 100644 index 0000000..31d5266 --- /dev/null +++ b/src/main/java/gov/nih/niehs/notification/service/NotificationServiceImpl.java @@ -0,0 +1,94 @@ +package gov.nih.niehs.notification.service; + +import java.util.UUID; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import gov.nih.niehs.notification.interfaces.NotificationRepository; +import gov.nih.niehs.notification.interfaces.NotificationService; +import gov.nih.niehs.notification.model.BulkNotificationOptStatus; +import gov.nih.niehs.notification.model.BulkNotificationOptStatusInner; +import gov.nih.niehs.notification.model.NotificationArray; +import gov.nih.niehs.notification.model.UuidList; + +@Service +public class NotificationServiceImpl implements NotificationService { + private static final Logger logger = LoggerFactory.getLogger(NotificationServiceImpl.class); + + @Autowired + private NotificationRepository notificationRepository; + + @Override + public Integer addNotification(String senderId, String recipientId, String subject, String message, + Integer severityLevel, String notificationType, String dataLocationUrl) { + logger.info("Initiating addNotification()"); + logger.info("Querying database"); + Integer affectedRows = notificationRepository.addNotification(senderId, recipientId, subject, message, + severityLevel, notificationType, dataLocationUrl); + logger.info("Number of rows affected: {}", affectedRows); + return affectedRows; + } + + @Override + public NotificationArray getAllNotifications(String userId) { + logger.info("Initiating getAllNotifications()"); + logger.info("Querying database"); + NotificationArray notifications = notificationRepository.getAllNotification(userId); + return notifications; + } + + @Override + public BulkNotificationOptStatus markSeen(String userId, UuidList uuids) { + logger.info("Initiating markSeen()"); + BulkNotificationOptStatus updateStatus = new BulkNotificationOptStatus(); + Boolean seenFlag = true; + logger.info("Querying database"); + for (UUID uuid : uuids) { + logger.info("Marking notifications: " + uuid + "as seen: " + seenFlag); + Integer status = notificationRepository.markSeen(userId, uuid, seenFlag); + if (status == 1) { + updateStatus.add(new BulkNotificationOptStatusInner(uuid, true)); + } else { + logger.error("Error updating notification with uuid: {}", uuid); + updateStatus.add(new BulkNotificationOptStatusInner(uuid, false)); + } + } + return updateStatus; + } + + @Override + public NotificationArray getUnseenNotifications(String userId) { + logger.info("Initiating getUnseenNotifications()"); + NotificationArray notifications = notificationRepository.getUnseenNotifications(userId); + return notifications; + } + + @Override + public Integer getUnseenNotificationsCount(String userId) { + logger.info("Initiating getUnseenNotificationsCount()"); + Integer result = notificationRepository.getUnseenNotificationsCount(userId); + return result; + } + + @Override + public BulkNotificationOptStatus deleteNotifications(String userId, UuidList uuids) { + logger.info("Initiating markSeen()"); + BulkNotificationOptStatus updateStatus = new BulkNotificationOptStatus(); + Boolean deletedFlag = true; + logger.info("Querying database"); + for (UUID uuid : uuids) { + logger.info("Marking notifications: " + uuid + "as deleted: " + deletedFlag); + Integer status = notificationRepository.deleteNotifications(userId, uuid, deletedFlag); + if (status == 1) { + updateStatus.add(new BulkNotificationOptStatusInner(uuid, true)); + } else { + logger.error("Error updating notification with uuid: {}", uuid); + updateStatus.add(new BulkNotificationOptStatusInner(uuid, false)); + } + } + return updateStatus; + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..feb4de3 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,12 @@ +springfox.documentation.swagger.v2.path=/api-docs +server.contextPath=/ +server.port=8888 +spring.jackson.date-format=gov.nih.niehs.notification.RFC3339DateFormat +spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS=false + +spring.datasource.platform=postgres +spring.datasource.url=jdbc:postgresql://localhost:5432/notification +spring.datasource.username=irodsext +spring.datasource.password=password + +app.jwtSecret=ThisIsAVeryLongSecret diff --git a/src/main/resources/migrations/V1__Base_version.sql b/src/main/resources/migrations/V1__Base_version.sql new file mode 100644 index 0000000..0829e4a --- /dev/null +++ b/src/main/resources/migrations/V1__Base_version.sql @@ -0,0 +1,33 @@ +-- +-- Extentsion for auto generating UUID. +-- +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- +-- ID sequence for the notifications table. +-- +CREATE SEQUENCE notifications_id_seq + START WITH 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; + +-- +-- Stores notification information. +-- +CREATE TABLE notification ( + id BIGINT DEFAULT nextval('notifications_id_seq'::regclass) NOT NULL, + notification_id uuid NOT NULL DEFAULT uuid_generate_v1(), + sender_id VARCHAR(512) NOT NULL, + recipient_id VARCHAR(512) NOT NULL, + subject TEXT NOT NULL, + message TEXT NOT NULL, + seen BOOLEAN DEFAULT FALSE NOT NULL, + deleted BOOLEAN DEFAULT FALSE NOT NULL, + date_created TIMESTAMP DEFAULT now() NOT NULL, + severity_level INTEGER NOT NULL CHECK (severity_level > 0 AND severity_level < 6), + notification_type VARCHAR(50) NOT NULL, + data_location_url TEXT, + PRIMARY KEY(id) +); \ No newline at end of file diff --git a/swagger/notification-service.yaml b/swagger/notification-service.yaml new file mode 100644 index 0000000..fb95b6d --- /dev/null +++ b/swagger/notification-service.yaml @@ -0,0 +1,267 @@ +openapi: 3.0.2 +info: + version: '1.0.0' + title: 'Notification Service API' + description: 'This is a notification microservice for Metalnx' + contact: + email: deep.patel@nih.gov + license: + name: Apache 2.0 + url: http://www.apache.org/licenses/LICENSE-2.0.html +servers: +- url: http://localhost:8888 + description: Local dev server + +# Apply the security globally to all operations +security: + - BearerAuth: + - read + +tags: +- name: "Notification" + description: "Notification Operations" + +paths: + /addNotification: + post: + tags: + - Notification + summary: Add new notification + description: Query database to insert new notification + operationId: addNotification + requestBody: + description: Schema to add new notification + content: + application/json: + schema: + $ref: '#/components/schemas/addNotification' + example: + recipient_id: user_1 + subject: notification subject + message: notification message + severity_level: 2 + notification_type: workflow + responses: + 201: + description: notification added successfully + 400: + description: bad request + + /getNotification: + get: + tags: + - Notification + summary: Reterieve notifications + description: Query database to get all the notifications for session user + operationId: getNotifications + responses: + 200: + description: List of all notifications + content: + application/json: + schema: + $ref: '#/components/schemas/notificationArray' + + /markSeen: + put: + tags: + - Notification + summary: Update notification as seen + description: Query database to mark of notifications as seen once user is validated of ownership + operationId: markSeen + requestBody: + description: List of uuids one for each notification + content: + application/json: + schema: + $ref: '#/components/schemas/uuidList' + responses: + 200: + description: notifications marked seen successfully + content: + application/json: + schema: + $ref: '#/components/schemas/bulkNotificationOptStatus' + 409: + description: conflict in update + content: + application/json: + schema: + $ref: '#/components/schemas/bulkNotificationOptStatus' + + /getUnseenNotifications: + get: + tags: + - Notification + summary: Reterieve all unseen notifications + description: Query database to get all unseen notifications for session user + operationId: getUnseenNotifications + responses: + 200: + description: List of all notifications + content: + application/json: + schema: + $ref: '#/components/schemas/notificationArray' + + /getUnseenNotificationsCount: + get: + tags: + - Notification + summary: Reterieve all unseen notifications + description: Query database to get all unseen notifications for session user + operationId: getUnseenNotificationsCount + responses: + 200: + description: count reterieved sucessfully + content: + application/json: + schema: + $ref: '#/components/schemas/count' + + /deleteNotifications: + put: + tags: + - Notification + summary: Delete notifications + description: Query database to delete notifications once user is validated of ownership + operationId: deleteNotifications + requestBody: + description: List of uuids one for each notification + content: + application/json: + schema: + $ref: '#/components/schemas/uuidList' + responses: + 200: + description: notifications marked seen successfully + content: + application/json: + schema: + $ref: '#/components/schemas/bulkNotificationOptStatus' + 409: + description: conflict in update + content: + application/json: + schema: + $ref: '#/components/schemas/bulkNotificationOptStatus' + + +components: + schemas: + addNotification: + type: object + required: + - recipient_id + - subject + - message + - severity_level + - notification_type + properties: + recipient_id: + type: string + description: notification recipient's userId + subject: + type: string + description: notification subject + message: + type: string + description: notification message content + severity_level: + type: integer + format: int32 + minimum: 1 + maximum: 5 + description: severity level of notification an integer value between 1 and 5 + notification_type: + type: string + description: type of notification options are workflow, permission, system + data_location_url: + type: string + format: uri + description: notification associated logical location of data + + notification: + type: object + properties: + id: + type: integer + format: int32 + description: an auto-generated sequence by postgres db + notification_id: + type: string + format: uuid + description: an unique uuid auto-generated by postgres db + sender_id: + type: string + description: notification sender's userId + recipient_id: + type: string + description: notification recipient's userId + subject: + type: string + description: notification subject + message: + type: string + description: notification message content + seen: + type: boolean + default: false + description: boolean for user's seen/unseen notification status + deleted: + type: boolean + default: false + description: boolean for deleted notification, notification will not be deleted from postgres db + date_created: + type: string + format: date-time + description: timestamp auto-generated by postgres db + severity_level: + type: integer + format: int32 + description: severity level of notification an integer value between 1 and 5 + notification_type: + type: string + description: type of notification options are workflow, permission, system + data_location_url: + type: string + format: uri + description: notification associated logical location of data + + notificationArray: + type: array + items: + $ref: '#/components/schemas/notification' + + uuidList: + type: array + items: + type: string + format: uuid + description: list of unique uuid one for each notification + + bulkNotificationOptStatus: + type: array + items: + type: object + required: + - notification_id + - operation_status + properties: + notification_id: + type: string + format: uuid + description: an unique uuid auto-generated by postgres db + operation_status: + type: boolean + + count: + type: integer + format: int32 + description: count of unseen notifications + + securitySchemes: + BearerAuth: + type: http + scheme: bearer + bearerFormat: JWT \ No newline at end of file