diff --git a/src/prerna/auth/utils/SecurityEngineUtils.java b/src/prerna/auth/utils/SecurityEngineUtils.java index 27893e22708..74966e2c365 100644 --- a/src/prerna/auth/utils/SecurityEngineUtils.java +++ b/src/prerna/auth/utils/SecurityEngineUtils.java @@ -665,9 +665,10 @@ public static void approveEngineUserAccessRequests(User user, String engineId, L if (Utility.isNotificationDatabaseEnabled()) { String engineType = String.valueOf(getEngineType(engineId)).toLowerCase(); for (int i = 0; i < requests.size(); i++) { - NotificationDbUtils.createNotification(user, requests.get(i).get("userid"), - requests.get(i).get("type"), engineId, NotificationConstants.Type.REQUEST_APPROVAL, - engineType, NotificationConstants.Priority.MEDIUM, null, requests.get(i).get("permission")); + NotificationDbUtils.createNotification(user, requests.get(i).get("userid"), + requests.get(i).get("type"), engineId, NotificationConstants.Type.REQUEST_APPROVAL, + engineType, NotificationConstants.Priority.MEDIUM, null, requests.get(i).get("permission"), + NotificationConstants.DisplaySurface.BELL); // Adding email notification EmailUtility.sendAccessRequestApprovalEmailNotification(user, requests.get(i).get("userid"), engineId, requests.get(i).get("permission"), EmailUtility.RESOURCE_TYPE.ENGINE); @@ -738,10 +739,11 @@ public static void denyEngineUserAccessRequests(User user, String engineId, List List> deniedUserDetails = getUserDetailsFromEngineAccessRequest(requestId); String permission = AccessPermissionEnum .getPermissionValueById((Integer) deniedUserDetails.get(i).get("permission")); - NotificationDbUtils.createNotification(user, (String) deniedUserDetails.get(i).get("userId"), - (String) deniedUserDetails.get(i).get("type"), engineId, - NotificationConstants.Type.REQUEST_DENIAL, engineType, - NotificationConstants.Priority.MEDIUM, null, permission); + NotificationDbUtils.createNotification(user, (String) deniedUserDetails.get(i).get("userId"), + (String) deniedUserDetails.get(i).get("type"), engineId, + NotificationConstants.Type.REQUEST_DENIAL, engineType, + NotificationConstants.Priority.MEDIUM, null, permission, + NotificationConstants.DisplaySurface.BELL); } } @@ -1154,10 +1156,10 @@ public static void addEngineUserPermissions(User user, String engineId, List> deniedUserDetails = getUserDetailsFromProjectAccessRequest(requestId); String permission = AccessPermissionEnum .getPermissionValueById((Integer) deniedUserDetails.get(i).get("permission")); - NotificationDbUtils.createNotification(user, (String) deniedUserDetails.get(i).get("userId"), - (String) deniedUserDetails.get(i).get("type"), projectId, - NotificationConstants.Type.REQUEST_DENIAL, NotificationConstants.APP_CATALOG, - NotificationConstants.Priority.MEDIUM, null, permission); + NotificationDbUtils.createNotification(user, (String) deniedUserDetails.get(i).get("userId"), + (String) deniedUserDetails.get(i).get("type"), projectId, + NotificationConstants.Type.REQUEST_DENIAL, NotificationConstants.APP_CATALOG, + NotificationConstants.Priority.MEDIUM, null, permission, + NotificationConstants.DisplaySurface.BELL); } } @@ -4825,10 +4827,10 @@ public static void addProjectUserPermissions(User user, String projectId, List>>> db boolean allowIfExistsTable = queryUtil.allowsIfExistsTableSyntax(); boolean allowIfExistsIndexs = queryUtil.allowIfExistsIndexSyntax(); - // create the tables and columns from the OWL creator schema for (Pair>> tableSchema : dbSchema) { String tableName = tableSchema.getValue0(); String[] colNames = tableSchema.getValue1().stream().map(Pair::getValue0).toArray(String[]::new); @@ -101,12 +100,10 @@ private static void initialize(List>>> db String sql = queryUtil.createTableIfNotExists(tableName, colNames, types); classLogger.info("Running sql {}", sql); notificationDb.insertData(sql); - } else { - if (!queryUtil.tableExists(conn, tableName, database, schema)) { - String sql = queryUtil.createTable(tableName, colNames, types); - classLogger.info("Running sql {}", sql); - notificationDb.insertData(sql); - } + } else if (!queryUtil.tableExists(conn, tableName, database, schema)) { + String sql = queryUtil.createTable(tableName, colNames, types); + classLogger.info("Running sql {}", sql); + notificationDb.insertData(sql); } List allCols = queryUtil.getTableColumns(conn, tableName, database, schema); @@ -122,36 +119,55 @@ private static void initialize(List>>> db } } - // NOTIFICATION index (kept) - if (allowIfExistsIndexs) { - String sql = queryUtil.createIndexIfNotExists("NOTIFICATION_NOTIFICATIONID_INDEX", "NOTIFICATION", - "NOTIFICATIONID"); - classLogger.info("Running sql {}", sql); - notificationDb.insertData(sql); - } else { - // see if index exists - if (!queryUtil.indexExists(notificationDb, "NOTIFICATION_NOTIFICATIONID_INDEX", "NOTIFICATION", - database, schema)) { - String sql = queryUtil.createIndex("NOTIFICATION_NOTIFICATIONID_INDEX", "NOTIFICATION", - "NOTIFICATIONID"); - classLogger.info("Running sql {}", sql); - notificationDb.insertData(sql); - } - } + createIndexIfMissing(notificationDb, queryUtil, allowIfExistsIndexs, database, schema, + "NOTIFICATION_EVENT_NOTIFICATION_ID_INDEX", "NOTIFICATION_EVENT", "NOTIFICATION_ID"); + createIndexIfMissing(notificationDb, queryUtil, allowIfExistsIndexs, database, schema, + "NOTIFICATION_EVENT_SCOPE_INDEX", "NOTIFICATION_EVENT", Arrays.asList("SCOPE_TYPE", "SCOPE_ID")); + createIndexIfMissing(notificationDb, queryUtil, allowIfExistsIndexs, database, schema, + "NOTIFICATION_EVENT_AUDIENCE_INDEX", "NOTIFICATION_EVENT", + Arrays.asList("AUDIENCE_TYPE", "AUDIENCE_ID")); + createIndexIfMissing(notificationDb, queryUtil, allowIfExistsIndexs, database, schema, + "NOTIFICATION_EVENT_TARGET_INDEX", "NOTIFICATION_EVENT", Arrays.asList("TARGET_TYPE", "TARGET_ID")); + createIndexIfMissing(notificationDb, queryUtil, allowIfExistsIndexs, database, schema, + "NOTIFICATION_USER_STATE_NOTIFICATION_USER_INDEX", "NOTIFICATION_USER_STATE", + Arrays.asList("NOTIFICATION_ID", "USER_ID", "USER_TYPE")); + createIndexIfMissing(notificationDb, queryUtil, allowIfExistsIndexs, database, schema, + "NOTIFICATION_USER_STATE_USER_INDEX", "NOTIFICATION_USER_STATE", + Arrays.asList("USER_ID", "USER_TYPE")); if (!conn.getAutoCommit()) { conn.commit(); } } finally { - // clean up the connection used for this method if (conn != null && notificationDb.isConnectionPooling()) { conn.close(); } } } + private static void createIndexIfMissing(IRDBMSEngine notificationDb, AbstractSqlQueryUtil queryUtil, + boolean allowIfExistsIndexs, String database, String schema, String indexName, String tableName, + String columnName) throws Exception { + createIndexIfMissing(notificationDb, queryUtil, allowIfExistsIndexs, database, schema, indexName, tableName, + Arrays.asList(columnName)); + } + + private static void createIndexIfMissing(IRDBMSEngine notificationDb, AbstractSqlQueryUtil queryUtil, + boolean allowIfExistsIndexs, String database, String schema, String indexName, String tableName, + Collection columns) throws Exception { + if (allowIfExistsIndexs) { + String sql = queryUtil.createIndexIfNotExists(indexName, tableName, columns); + classLogger.info("Running sql {}", sql); + notificationDb.insertData(sql); + } else if (!queryUtil.indexExists(notificationDb, indexName, tableName, database, schema)) { + String sql = queryUtil.createIndex(indexName, tableName, columns); + classLogger.info("Running sql {}", sql); + notificationDb.insertData(sql); + } + } + /** - * Determine if the theme db is present to be able to set custom themes + * Determine if the notification db is present. * * @return */ @@ -160,349 +176,772 @@ public static boolean isInitalized() { } /** - * Add notification into database - * - * @param loggedInUser - The logged-in user performing the action - * @param affectedUserId - The user whose role or permission changed - * @param catalogId - * @param notificationType - e.g. USER_REQUEST, REQUEST_APPROVAL - * @param notificationSource - * @param priority - e.g. HIGH, MEDIUM, LOW - * @param affectedUserPreviousRole - * @param affectedUserNewRole + * Adapter for existing access-request notification producers. */ public static void createNotification(User loggedInUser, String affectedUserId, String affectedUserType, String catalogId, String notificationType, String notificationSource, String priority, - String affectedUserPreviousRole, String affectedUserNewRole) { + String affectedUserPreviousRole, String affectedUserNewRole, String displaySurface) { IRDBMSEngine notificationDb = SystemEngineRegistry.getNotificationDb(); - // Fetch all authors based on source - List> authors = NotificationConstants.APP_CATALOG.equalsIgnoreCase(notificationSource) - ? SecurityProjectUtils.getProjectAuthors(catalogId) - : SecurityEngineUtils.getEngineAuthors(catalogId); - - // Check if initiator already present - boolean initiatorFound = false; - - if (affectedUserId != null && authors != null && !authors.isEmpty()) { - for (int i = 0; i < authors.size(); i++) { - Map user = authors.get(i); - if (user == null) { + List> authorRecipients = NotificationConstants.APP_CATALOG + .equalsIgnoreCase(notificationSource) ? SecurityProjectUtils.getProjectAuthors(catalogId) + : SecurityEngineUtils.getEngineAuthors(catalogId); + List> recipients = authorRecipients == null ? new ArrayList<>() + : new ArrayList<>(authorRecipients); + + boolean affectedUserFound = false; + if (affectedUserId != null) { + for (Map recipient : recipients) { + if (recipient == null) { continue; } - String userId = (String) user.get("userId"); - if (userId == null) { - continue; - } - - if (affectedUserId.equals(userId)) { - initiatorFound = true; + if (affectedUserId.equals(recipient.get("userId"))) { + affectedUserFound = true; break; } } } - if (!initiatorFound && affectedUserId != null) { - Map initiatorMap = new HashMap<>(); - initiatorMap.put("userId", affectedUserId); - initiatorMap.put("userType", affectedUserType); - authors.add(initiatorMap); + if (!affectedUserFound && affectedUserId != null) { + Map affectedUser = new HashMap<>(); + affectedUser.put("userId", affectedUserId); + affectedUser.put("userType", affectedUserType); + recipients.add(affectedUser); } String createdBy = loggedInUser.getAccessToken(loggedInUser.getLogins().get(0)).getId(); - Timestamp createdDate = Utility.getCurrentSqlTimestampUTC(); - - for (Map author : authors) { - String recipientId = (String) author.get("userId"); - String recipientType = (String) author.get("userType"); - - String query = "INSERT INTO NOTIFICATION (NOTIFICATIONID,RECIPIENTID,RECIPIENTTYPE,NOTIFICATIONTITLE,MESSAGE,ACTIONTYPE,ACTIONTARGET,ISREAD,PRIORITY,NOTIFICATIONTYPE,CATALOGID,CREATEDBY,CREATEDDATE,READDATE,NOTIFICATIONSOURCE,USERID,USERTYPE,USEREXISTINGROLE,USERNEWROLE) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"; + Timestamp createdAt = Utility.getCurrentSqlTimestampUTC(); + String type = deriveType(notificationType); + String scopeType = deriveScopeType(notificationSource); + String scopeId = NotificationConstants.Scope.APP.equals(scopeType) ? catalogId : null; + String sourceType = deriveSourceType(notificationSource); + String targetType = NotificationConstants.Scope.APP.equals(scopeType) ? NotificationConstants.Target.APP + : NotificationConstants.Target.NONE; + String metadataJson = buildLegacyMetadata(affectedUserId, affectedUserType, affectedUserPreviousRole, + affectedUserNewRole, notificationType, notificationSource); + + String query = "INSERT INTO NOTIFICATION_EVENT (NOTIFICATION_ID,TYPE,SCOPE_TYPE,SCOPE_ID,AUDIENCE_TYPE,AUDIENCE_ID,AUDIENCE_USER_TYPE,TITLE,MESSAGE,PRIORITY,DISPLAY_SURFACE,SOURCE_TYPE,SOURCE_ID,TARGET_TYPE,TARGET_ID,METADATA_JSON,CREATED_BY,CREATED_AT) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"; + for (Map recipient : recipients) { + if (recipient == null || recipient.get("userId") == null) { + continue; + } PreparedStatement ps = null; try { ps = notificationDb.getPreparedStatement(query); int parameterIndex = 1; - ps.setString(parameterIndex++, GUID.v7().toUUID().toString()); // notificationId - ps.setString(parameterIndex++, recipientId); - ps.setString(parameterIndex++, recipientType); - ps.setString(parameterIndex++, "NOTIFICATION"); // notificationTitle - ps.setString(parameterIndex++, null); // message - ps.setString(parameterIndex++, "NEW"); // actionType - ps.setString(parameterIndex++, "IN-APP"); - ps.setBoolean(parameterIndex++, false); // isRead - ps.setString(parameterIndex++, priority); - ps.setString(parameterIndex++, notificationType); + ps.setString(parameterIndex++, GUID.v7().toUUID().toString()); + ps.setString(parameterIndex++, type); + ps.setString(parameterIndex++, scopeType); + ps.setString(parameterIndex++, scopeId); + ps.setString(parameterIndex++, NotificationConstants.Audience.USER); + ps.setString(parameterIndex++, String.valueOf(recipient.get("userId"))); + ps.setString(parameterIndex++, + recipient.get("userType") == null ? null : String.valueOf(recipient.get("userType"))); + ps.setString(parameterIndex++, "NOTIFICATION"); + ps.setString(parameterIndex++, null); + ps.setString(parameterIndex++, normalizePriority(priority)); + ps.setString(parameterIndex++, normalizeDisplaySurface(displaySurface)); + ps.setString(parameterIndex++, sourceType); + ps.setString(parameterIndex++, catalogId); + ps.setString(parameterIndex++, targetType); ps.setString(parameterIndex++, catalogId); + ps.setString(parameterIndex++, metadataJson); ps.setString(parameterIndex++, createdBy); - ps.setTimestamp(parameterIndex++, createdDate); - ps.setTimestamp(parameterIndex++, null); // readDate - ps.setString(parameterIndex++, notificationSource); - ps.setString(parameterIndex++, affectedUserId); - ps.setString(parameterIndex++, affectedUserType);// userType - ps.setString(parameterIndex++, affectedUserPreviousRole); - ps.setString(parameterIndex++, affectedUserNewRole); + ps.setTimestamp(parameterIndex++, createdAt); ps.execute(); if (!ps.getConnection().getAutoCommit()) { ps.getConnection().commit(); } } catch (SQLException e) { - classLogger.error("Failed to insert notification for recipient {} (type {}) on catalog {}", recipientId, - recipientType, catalogId, e); + classLogger.error("Failed to insert notification for recipient {} (type {}) on catalog {}", + recipient.get("userId"), recipient.get("userType"), catalogId, e); } finally { ConnectionUtils.closeAllConnectionsIfPooling(notificationDb, ps); } } } - /** - * Get all notifications for user - * - * @param memberId - * @param limit - * @param offset - * @return list of notifications - */ - public static List> fetchAllNotifications(User user, String limit, String offset) { + static String insertNotificationEvent(String type, String scopeType, String scopeId, + String audienceType, String audienceId, String audienceUserType, String title, String message, + String priority, String displaySurface, String sourceType, String sourceId, String targetType, + String targetId, String metadataJson, String createdBy) { IRDBMSEngine notificationDb = SystemEngineRegistry.getNotificationDb(); + String notificationId = GUID.v7().toUUID().toString(); + String query = "INSERT INTO NOTIFICATION_EVENT (NOTIFICATION_ID,TYPE,SCOPE_TYPE,SCOPE_ID,AUDIENCE_TYPE,AUDIENCE_ID,AUDIENCE_USER_TYPE,TITLE,MESSAGE,PRIORITY,DISPLAY_SURFACE,SOURCE_TYPE,SOURCE_ID,TARGET_TYPE,TARGET_ID,METADATA_JSON,CREATED_BY,CREATED_AT) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"; + PreparedStatement ps = null; + try { + ps = notificationDb.getPreparedStatement(query); + int parameterIndex = 1; + ps.setString(parameterIndex++, notificationId); + ps.setString(parameterIndex++, type); + ps.setString(parameterIndex++, scopeType); + ps.setString(parameterIndex++, scopeId); + ps.setString(parameterIndex++, audienceType); + ps.setString(parameterIndex++, audienceId); + ps.setString(parameterIndex++, audienceUserType); + ps.setString(parameterIndex++, title); + ps.setString(parameterIndex++, message); + ps.setString(parameterIndex++, priority); + ps.setString(parameterIndex++, normalizeDisplaySurface(displaySurface)); + ps.setString(parameterIndex++, sourceType); + ps.setString(parameterIndex++, sourceId); + ps.setString(parameterIndex++, targetType); + ps.setString(parameterIndex++, targetId); + ps.setString(parameterIndex++, metadataJson); + ps.setString(parameterIndex++, createdBy); + ps.setTimestamp(parameterIndex++, Utility.getCurrentSqlTimestampUTC()); + ps.execute(); + if (!ps.getConnection().getAutoCommit()) { + ps.getConnection().commit(); + } + return notificationId; + } catch (SQLException e) { + classLogger.error("Failed to insert notification event [type={}, scopeType={}, scopeId={}]", type, + scopeType, scopeId, e); + throw new IllegalStateException("Unable to create notification", e); + } finally { + ConnectionUtils.closeAllConnectionsIfPooling(notificationDb, ps); + } + } + + public static List> fetchAllNotifications(User user, String limit, String offset) { + return fetchNotifications(user, NotificationConstants.FetchScope.ALL, null, limit, offset); + } + + public static List> fetchNotifications(User user, String scopeType, String scopeId, String limit, + String offset) { List> userIdAndTypeList = User.getUserIdAndType(user); if (userIdAndTypeList.isEmpty()) { return new ArrayList<>(); } - SelectQueryStruct qs = new SelectQueryStruct(); - qs.addSelector(new QueryColumnSelector("NOTIFICATION__NOTIFICATIONID", "notification_id")); - qs.addSelector(new QueryColumnSelector("NOTIFICATION__RECIPIENTID", "recipient_id")); - qs.addSelector(new QueryColumnSelector("NOTIFICATION__RECIPIENTTYPE", "recipient_type")); - qs.addSelector(new QueryColumnSelector("NOTIFICATION__NOTIFICATIONTITLE", "notification_title")); - qs.addSelector(new QueryColumnSelector("NOTIFICATION__MESSAGE", "notification_message")); - qs.addSelector(new QueryColumnSelector("NOTIFICATION__ACTIONTYPE", "notification_actiontype")); - qs.addSelector(new QueryColumnSelector("NOTIFICATION__ACTIONTARGET", "notification_actiontarget")); - qs.addSelector(new QueryColumnSelector("NOTIFICATION__ISREAD", "notification_isread")); - qs.addSelector(new QueryColumnSelector("NOTIFICATION__PRIORITY", "notification_priority")); - qs.addSelector(new QueryColumnSelector("NOTIFICATION__NOTIFICATIONTYPE", "notification_type")); - qs.addSelector(new QueryColumnSelector("NOTIFICATION__CATALOGID", "catalog_id")); - qs.addSelector(new QueryColumnSelector("NOTIFICATION__CREATEDDATE", "notification_createddate")); - qs.addSelector(new QueryColumnSelector("NOTIFICATION__READDATE", "notification_readdate")); - qs.addSelector(new QueryColumnSelector("NOTIFICATION__NOTIFICATIONSOURCE", "notification_source")); - qs.addSelector(new QueryColumnSelector("NOTIFICATION__USERID", "recipient_user_id")); - qs.addSelector(new QueryColumnSelector("NOTIFICATION__USEREXISTINGROLE", "user_existingrole")); - qs.addSelector(new QueryColumnSelector("NOTIFICATION__USERNEWROLE", "user_newrole")); - qs.addSelector(new QueryColumnSelector("NOTIFICATION__CREATEDBY", "notification_createdby")); - qs.addSelector(new QueryColumnSelector("NOTIFICATION__USERTYPE", "notification_usertype")); - - Pair userPair = userIdAndTypeList.get(0); - String userId = userPair.getValue0(); - String userType = userPair.getValue1(); - - List andFilters = new ArrayList<>(); - andFilters.add(SimpleQueryFilter.makeColToValFilter("NOTIFICATION__RECIPIENTID", "==", userId)); - andFilters.add(SimpleQueryFilter.makeColToValFilter("NOTIFICATION__RECIPIENTTYPE", "==", userType)); - - // (RECIPIENTID == userId AND RECIPIENTTYPE == userType) - AndQueryFilter andCombined = new AndQueryFilter(andFilters); - qs.addExplicitFilter(andCombined); - qs.addOrderBy("NOTIFICATION__CREATEDDATE", "desc"); - - Long long_limit = -1L; - Long long_offset = -1L; - if (limit != null && !limit.trim().isEmpty()) { - long_limit = ((Number) Double.parseDouble(limit)).longValue(); - } - if (offset != null && !offset.trim().isEmpty()) { - long_offset = ((Number) Double.parseDouble(offset)).longValue(); - } - qs.setLimit(long_limit); - qs.setOffSet(long_offset); - - List> notificationList = QueryExecutionUtility.flushRsToMap(notificationDb, qs); - if (notificationList == null || notificationList.isEmpty()) { - return new ArrayList<>(); + List accessibleProjectIds = getAccessibleProjectIds(user); + List audienceParameters = new ArrayList<>(); + List scopeParameters = new ArrayList<>(); + List dismissedParameters = new ArrayList<>(); + List readParameters = new ArrayList<>(); + String audienceCondition = buildVisibleAudienceSqlCondition(userIdAndTypeList, accessibleProjectIds, + audienceParameters); + String scopeCondition = buildVisibleScopeSqlCondition(scopeType, scopeId, accessibleProjectIds, scopeParameters); + String dismissedCondition = buildStateExistsSqlCondition("us", userIdAndTypeList, dismissedParameters, + "us.IS_DISMISSED = TRUE"); + String readCondition = buildStateExistsSqlCondition("urs", userIdAndTypeList, readParameters, + "urs.IS_READ = TRUE"); + + StringBuilder query = new StringBuilder(); + query.append("SELECT n.NOTIFICATION_ID, n.TYPE, n.SCOPE_TYPE, n.SCOPE_ID, n.AUDIENCE_TYPE, ") + .append("n.AUDIENCE_ID, n.AUDIENCE_USER_TYPE, n.TITLE, n.MESSAGE, n.PRIORITY, n.DISPLAY_SURFACE, n.SOURCE_TYPE, ") + .append("n.SOURCE_ID, n.TARGET_TYPE, n.TARGET_ID, n.METADATA_JSON, n.CREATED_BY, n.CREATED_AT, ") + .append("CASE WHEN EXISTS (SELECT 1 FROM NOTIFICATION_USER_STATE urs WHERE urs.NOTIFICATION_ID = n.NOTIFICATION_ID AND ") + .append(readCondition).append(") THEN TRUE ELSE FALSE END AS IS_READ ") + .append("FROM NOTIFICATION_EVENT n WHERE (").append(audienceCondition).append(") ") + .append("AND (").append(scopeCondition).append(") ") + .append("AND NOT EXISTS (SELECT 1 FROM NOTIFICATION_USER_STATE us WHERE us.NOTIFICATION_ID = n.NOTIFICATION_ID AND ") + .append(dismissedCondition).append(") ").append("ORDER BY n.CREATED_AT DESC"); + List parameters = new ArrayList<>(); + parameters.addAll(readParameters); + parameters.addAll(audienceParameters); + parameters.addAll(scopeParameters); + parameters.addAll(dismissedParameters); + + Long longLimit = parseLong(limit); + Long longOffset = parseLong(offset); + if (longLimit != null && longLimit >= 0) { + query.append(" LIMIT ?"); + parameters.add(longLimit); + } + if (longOffset != null && longOffset >= 0) { + query.append(" OFFSET ?"); + parameters.add(longOffset); } - // Collect all unique IDs - Set userIds = new HashSet<>(); - Set catalogIds = new HashSet<>(); + List> notificationList = executeNotificationFetch(query.toString(), parameters); + hydrateLegacyDisplayFields(notificationList); + return notificationList; + } - for (Map row : notificationList) { - if (row.get("recipient_user_id") != null) { - userIds.add(String.valueOf(row.get("recipient_user_id"))); - } - if (row.get("catalog_id") != null) { - catalogIds.add(String.valueOf(row.get("catalog_id"))); - } - if (row.get("notification_createdby") != null) { - userIds.add(String.valueOf(row.get("notification_createdby"))); + public static int deleteNotification(String recipientId, String recipientType, String notificationId) { + List> recipientPairs = new ArrayList<>(); + if (recipientId != null && recipientType != null) { + recipientPairs.add(Pair.with(recipientId, recipientType)); + } + return deleteNotification(recipientPairs, notificationId); + } + + public static int deleteNotification(User user, String notificationId) { + return deleteNotification(User.getUserIdAndType(user), getAccessibleProjectIds(user), notificationId); + } + + public static int deleteNotification(List> recipientPairs, String notificationId) { + return deleteNotification(recipientPairs, new ArrayList<>(), notificationId); + } + + private static int deleteNotification(List> recipientPairs, List accessibleProjectIds, + String notificationId) { + if (recipientPairs == null || recipientPairs.isEmpty()) { + return 0; + } + List notificationIds = new ArrayList<>(); + if (notificationId != null) { + if (isNotificationVisibleToUser(notificationId, recipientPairs, accessibleProjectIds)) { + notificationIds.add(notificationId); } + } else { + notificationIds.addAll(fetchVisibleNotificationIds(recipientPairs, accessibleProjectIds, + NotificationConstants.FetchScope.ALL, null)); } - // bulk fetch - Map userIdToNameMap = SecurityUserUtils.getUserNamesByIds(userIds); - Map projectIdToNameMap = SecurityProjectUtils.getProjectNamesByIds(catalogIds); - Map engineIdToNameMap = SecurityEngineUtils.getEngineNamesByIds(catalogIds); + int count = 0; + Timestamp dismissedAt = Utility.getCurrentSqlTimestampUTC(); + Pair statePair = firstValidPair(recipientPairs); + if (statePair == null) { + return 0; + } + for (String id : notificationIds) { + count += upsertNotificationState(id, statePair, true, dismissedAt, true, dismissedAt); + } + return count; + } - for (Map row : notificationList) { - String catalogId = String.valueOf(row.get("catalog_id")); - String notificationSource = String.valueOf(row.get("notification_source")).trim(); + public static void resetNotificationActionType(User user) { + resetNotificationActionType(user, NotificationConstants.FetchScope.ALL, null); + } - // user name from cached map - row.put("recipient_user_name", userIdToNameMap.getOrDefault(row.get("recipient_user_id"), "Unknown User")); + public static void resetNotificationActionType(User user, String scopeType, String scopeId) { + List> userIdAndTypeList = User.getUserIdAndType(user); + if (userIdAndTypeList.isEmpty()) { + return; + } + Pair statePair = firstValidPair(userIdAndTypeList); + if (statePair == null) { + return; + } + Timestamp readAt = Utility.getCurrentSqlTimestampUTC(); + for (String notificationId : fetchVisibleNotificationIds(userIdAndTypeList, getAccessibleProjectIds(user), + scopeType, scopeId)) { + upsertNotificationState(notificationId, statePair, true, readAt, null, null); + } + } - // project and engine names from cached maps - String projectName = projectIdToNameMap.get(catalogId); - String engineName = engineIdToNameMap.get(catalogId); + public static void markNotificationRead(String notificationId, Timestamp readDate) { + markNotificationRead(notificationId, readDate, null); + } - // final catalog name - String finalCatalogName; - if (NotificationConstants.APP_CATALOG.equalsIgnoreCase(notificationSource)) { - finalCatalogName = (projectName != null && !projectName.isEmpty()) ? projectName - : ((engineName != null && !engineName.isEmpty()) ? engineName : null); - } else { - finalCatalogName = (engineName != null && !engineName.isEmpty()) ? engineName - : ((projectName != null && !projectName.isEmpty()) ? projectName : null); - } - row.put("catalog_name", finalCatalogName); + public static int markNotificationRead(String notificationId, Timestamp readDate, + List> recipientPairs) { + return markNotificationRead(notificationId, readDate, recipientPairs, new ArrayList<>()); + } + + public static int markNotificationRead(User user, String notificationId, Timestamp readDate) { + return markNotificationRead(notificationId, readDate, User.getUserIdAndType(user), getAccessibleProjectIds(user)); + } + + private static int markNotificationRead(String notificationId, Timestamp readDate, + List> recipientPairs, List accessibleProjectIds) { + if (notificationId == null || recipientPairs == null || recipientPairs.isEmpty() + || !isNotificationVisibleToUser(notificationId, recipientPairs, accessibleProjectIds)) { + return 0; } + Pair statePair = firstValidPair(recipientPairs); + if (statePair == null) { + return 0; + } + return upsertNotificationState(notificationId, statePair, true, readDate, null, null); + } - return notificationList; + public static int fetchNewNotificationCount(String recipientId, String recipientType) { + List> recipientPairs = new ArrayList<>(); + if (recipientId != null && recipientType != null) { + recipientPairs.add(Pair.with(recipientId, recipientType)); + } + return fetchNewNotificationCount(recipientPairs); } - /** - * - * @param recipientId -the ID of the recipient - * @param recipientType -the type of the recipient (e.g., NATIVE, MS) - * @param notificationId -the ID of the notification - * @return - */ - public static int deleteNotification(String recipientId, String recipientType, String notificationId) { - IRDBMSEngine notificationDb = SystemEngineRegistry.getNotificationDb(); - StringBuilder deleteQuery = new StringBuilder("DELETE FROM NOTIFICATION WHERE "); - List conditions = new ArrayList<>(); - List parameters = new ArrayList<>(); + public static int fetchNewNotificationCount(User user, String scopeType, String scopeId) { + return fetchNewNotificationCount(User.getUserIdAndType(user), getAccessibleProjectIds(user), scopeType, scopeId); + } - if (notificationId != null) { - conditions.add("NOTIFICATIONID = ?"); - parameters.add(notificationId); - } else if (recipientId != null && recipientType != null) { - conditions.add("RECIPIENTID = ?"); - parameters.add(recipientId); - conditions.add("RECIPIENTTYPE = ?"); - parameters.add(recipientType); - } else { - return 0; // nothing to delete + public static int fetchNewNotificationCount(List> recipientPairs) { + return fetchNewNotificationCount(recipientPairs, new ArrayList<>(), NotificationConstants.FetchScope.ALL, null); + } + + private static int fetchNewNotificationCount(List> recipientPairs, + List accessibleProjectIds, String scopeType, String scopeId) { + if (recipientPairs == null || recipientPairs.isEmpty()) { + return 0; } + IRDBMSEngine notificationDb = SystemEngineRegistry.getNotificationDb(); + List audienceParameters = new ArrayList<>(); + List scopeParameters = new ArrayList<>(); + List dismissedParameters = new ArrayList<>(); + List readParameters = new ArrayList<>(); + String audienceCondition = buildVisibleAudienceSqlCondition(recipientPairs, accessibleProjectIds, + audienceParameters); + String scopeCondition = buildVisibleScopeSqlCondition(scopeType, scopeId, accessibleProjectIds, scopeParameters); + String dismissedCondition = buildStateExistsSqlCondition("us", recipientPairs, dismissedParameters, + "us.IS_DISMISSED = TRUE"); + String readCondition = buildStateExistsSqlCondition("urs", recipientPairs, readParameters, + "urs.IS_READ = TRUE"); + List parameters = new ArrayList<>(); + parameters.addAll(audienceParameters); + parameters.addAll(scopeParameters); + parameters.addAll(dismissedParameters); + parameters.addAll(readParameters); + String query = "SELECT COUNT(n.NOTIFICATION_ID) FROM NOTIFICATION_EVENT n WHERE (" + audienceCondition + ") " + + "AND (" + scopeCondition + ") " + + "AND NOT EXISTS (SELECT 1 FROM NOTIFICATION_USER_STATE us WHERE us.NOTIFICATION_ID = n.NOTIFICATION_ID AND " + + dismissedCondition + ") " + + "AND NOT EXISTS (SELECT 1 FROM NOTIFICATION_USER_STATE urs WHERE urs.NOTIFICATION_ID = n.NOTIFICATION_ID AND " + + readCondition + ")"; - deleteQuery.append(String.join(" AND ", conditions)); PreparedStatement ps = null; - int deletedCount = 0; try { - ps = notificationDb.getPreparedStatement(deleteQuery.toString()); - int index = 1; - for (Object param : parameters) { - ps.setObject(index++, param); - } - - deletedCount = ps.executeUpdate(); - - if (!ps.getConnection().getAutoCommit()) { - ps.getConnection().commit(); + ps = notificationDb.getPreparedStatement(query); + setParameters(ps, parameters); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + return rs.getInt(1); + } } } catch (SQLException e) { - classLogger.error("Failed to delete notification(s) [notificationId={}, recipientId={}, recipientType={}]", - notificationId, recipientId, recipientType, e); + classLogger.error("Failed to fetch new notification count for recipient pairs {}", recipientPairs, e); } finally { ConnectionUtils.closeAllConnectionsIfPooling(notificationDb, ps); } - return deletedCount; + return 0; } - /** - * Updates notification action type for a given user. - * - * @param user the user whose notifications need to be updated - */ - public static void resetNotificationActionType(User user) { + private static List> executeNotificationFetch(String query, List parameters) { IRDBMSEngine notificationDb = SystemEngineRegistry.getNotificationDb(); - List> userIdAndTypeList = User.getUserIdAndType(user); - if (userIdAndTypeList.isEmpty()) { - return; - } - String updateQuery = "UPDATE NOTIFICATION SET ACTIONTYPE = 'NONE' WHERE ACTIONTYPE = 'NEW' AND RECIPIENTID=? AND RECIPIENTTYPE = ?"; PreparedStatement ps = null; + List> rows = new ArrayList<>(); try { - ps = notificationDb.getPreparedStatement(updateQuery); - for (Pair pair : userIdAndTypeList) { - String recipientId = pair.getValue0(); - String recipientType = pair.getValue1(); - - ps.setString(1, recipientId); - ps.setString(2, recipientType); - ps.executeUpdate(); - } - - Connection conn = (ps != null) ? ps.getConnection() : null; - if (conn != null && !conn.getAutoCommit()) { - conn.commit(); + ps = notificationDb.getPreparedStatement(query); + setParameters(ps, parameters); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + rows.add(mapNotificationRow(rs)); + } } } catch (SQLException e) { - classLogger.error("Failed to reset notification action type from NEW to NONE for user id/type pairs {}", - userIdAndTypeList, e); + classLogger.error("Failed to fetch notifications", e); } finally { ConnectionUtils.closeAllConnectionsIfPooling(notificationDb, ps); } + return rows; } - /** - * Marks a notification as read and updates the read date. - * - * @param notificationId -the ID of the notification - * @param readDate -the timestamp when the notification was read - */ - public static void markNotificationRead(String notificationId, Timestamp readDate) { + private static Map mapNotificationRow(ResultSet rs) throws SQLException { + Map row = new HashMap<>(); + JsonObject metadata = parseMetadata(getString(rs, "METADATA_JSON")); + String legacyType = getMetadataString(metadata, "legacyNotificationType"); + String sourceType = getString(rs, "SOURCE_TYPE"); + String sourceId = getString(rs, "SOURCE_ID"); + String targetId = getString(rs, "TARGET_ID"); + String catalogId = targetId != null ? targetId : sourceId; + + row.put("notification_id", getString(rs, "NOTIFICATION_ID")); + row.put("recipient_id", getString(rs, "AUDIENCE_ID")); + row.put("recipient_type", getString(rs, "AUDIENCE_USER_TYPE")); + row.put("notification_title", getString(rs, "TITLE")); + row.put("notification_message", getString(rs, "MESSAGE")); + row.put("notification_actiontype", Boolean.TRUE.equals(rs.getObject("IS_READ")) ? "NONE" : "NEW"); + row.put("notification_actiontarget", null); + row.put("notification_isread", rs.getBoolean("IS_READ")); + row.put("notification_priority", getString(rs, "PRIORITY")); + row.put("display_surface", normalizeDisplaySurface(getString(rs, "DISPLAY_SURFACE"))); + row.put("notification_type", legacyType == null ? getString(rs, "TYPE") : legacyType); + row.put("catalog_id", catalogId); + row.put("notification_createddate", rs.getTimestamp("CREATED_AT")); + row.put("notification_readdate", null); + row.put("notification_source", deriveLegacyNotificationSource(sourceType, getString(rs, "SCOPE_TYPE"))); + row.put("recipient_user_id", getMetadataString(metadata, "affectedUserId")); + row.put("notification_usertype", getMetadataString(metadata, "affectedUserType")); + row.put("user_existingrole", getMetadataString(metadata, "affectedUserPreviousRole")); + row.put("user_newrole", getMetadataString(metadata, "affectedUserNewRole")); + row.put("notification_createdby", getString(rs, "CREATED_BY")); + row.put("type", getString(rs, "TYPE")); + row.put("scope_type", getString(rs, "SCOPE_TYPE")); + row.put("scope_id", getString(rs, "SCOPE_ID")); + row.put("target_type", getString(rs, "TARGET_TYPE")); + row.put("target_id", targetId); + return row; + } + + private static void hydrateLegacyDisplayFields(List> notificationList) { + if (notificationList == null || notificationList.isEmpty()) { + return; + } + Set userIds = new HashSet<>(); + Set catalogIds = new HashSet<>(); + for (Map row : notificationList) { + if (row.get("recipient_user_id") != null) { + userIds.add(String.valueOf(row.get("recipient_user_id"))); + } + if (row.get("notification_createdby") != null) { + userIds.add(String.valueOf(row.get("notification_createdby"))); + } + if (row.get("catalog_id") != null) { + catalogIds.add(String.valueOf(row.get("catalog_id"))); + } + } + Map userIdToNameMap = SecurityUserUtils.getUserNamesByIds(userIds); + Map projectIdToNameMap = SecurityProjectUtils.getProjectNamesByIds(catalogIds); + Map engineIdToNameMap = SecurityEngineUtils.getEngineNamesByIds(catalogIds); + + for (Map row : notificationList) { + String catalogId = row.get("catalog_id") == null ? null : String.valueOf(row.get("catalog_id")); + String notificationSource = row.get("notification_source") == null ? null + : String.valueOf(row.get("notification_source")); + Object affectedUserId = row.get("recipient_user_id"); + row.put("recipient_user_name", affectedUserId == null ? "Unknown User" + : userIdToNameMap.getOrDefault(affectedUserId, "Unknown User")); + + String projectName = catalogId == null ? null : projectIdToNameMap.get(catalogId); + String engineName = catalogId == null ? null : engineIdToNameMap.get(catalogId); + if (NotificationConstants.APP_CATALOG.equalsIgnoreCase(notificationSource)) { + row.put("catalog_name", projectName != null && !projectName.isEmpty() ? projectName : engineName); + } else { + row.put("catalog_name", engineName != null && !engineName.isEmpty() ? engineName : projectName); + } + } + } + + private static List fetchVisibleNotificationIds(List> recipientPairs, + List accessibleProjectIds, String scopeType, String scopeId) { + List audienceParameters = new ArrayList<>(); + List scopeParameters = new ArrayList<>(); + List dismissedParameters = new ArrayList<>(); + String audienceCondition = buildVisibleAudienceSqlCondition(recipientPairs, accessibleProjectIds, + audienceParameters); + String scopeCondition = buildVisibleScopeSqlCondition(scopeType, scopeId, accessibleProjectIds, scopeParameters); + String dismissedCondition = buildStateExistsSqlCondition("us", recipientPairs, dismissedParameters, + "us.IS_DISMISSED = TRUE"); + List parameters = new ArrayList<>(); + parameters.addAll(audienceParameters); + parameters.addAll(scopeParameters); + parameters.addAll(dismissedParameters); + String query = "SELECT n.NOTIFICATION_ID FROM NOTIFICATION_EVENT n WHERE (" + audienceCondition + ") " + + "AND (" + scopeCondition + ") " + + "AND NOT EXISTS (SELECT 1 FROM NOTIFICATION_USER_STATE us WHERE us.NOTIFICATION_ID = n.NOTIFICATION_ID AND " + + dismissedCondition + ")"; + List ids = new ArrayList<>(); IRDBMSEngine notificationDb = SystemEngineRegistry.getNotificationDb(); - String query = "UPDATE NOTIFICATION SET ISREAD = TRUE, READDATE=? WHERE NOTIFICATIONID=?"; PreparedStatement ps = null; try { ps = notificationDb.getPreparedStatement(query); - int parameterIndex = 1; - ps.setTimestamp(parameterIndex++, readDate); - ps.setString(parameterIndex++, notificationId); - ps.executeUpdate(); - if (!ps.getConnection().getAutoCommit()) { - ps.getConnection().commit(); + setParameters(ps, parameters); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + ids.add(rs.getString(1)); + } } } catch (SQLException e) { - classLogger.error("Failed to mark notification {} as read (readDate={})", notificationId, readDate, e); + classLogger.error("Failed to fetch visible notification ids for recipient pairs {}", recipientPairs, e); } finally { ConnectionUtils.closeAllConnectionsIfPooling(notificationDb, ps); } + return ids; } - /** - * Retrieves the count of new notifications for a given user and type. - * - * @param recipientId -the ID of the recipient - * @param recipientType -the type of the recipient (e.g., NATIVE, MS) - * @return the count of new notifications - */ - public static int fetchNewNotificationCount(String recipientId, String recipientType) { + private static boolean isNotificationVisibleToUser(String notificationId, + List> recipientPairs, List accessibleProjectIds) { + List audienceParameters = new ArrayList<>(); + List scopeParameters = new ArrayList<>(); + List dismissedParameters = new ArrayList<>(); + String audienceCondition = buildVisibleAudienceSqlCondition(recipientPairs, accessibleProjectIds, + audienceParameters); + String scopeCondition = buildVisibleScopeSqlCondition(NotificationConstants.FetchScope.ALL, null, + accessibleProjectIds, scopeParameters); + String dismissedCondition = buildStateExistsSqlCondition("us", recipientPairs, dismissedParameters, + "us.IS_DISMISSED = TRUE"); + List parameters = new ArrayList<>(); + parameters.addAll(audienceParameters); + parameters.addAll(scopeParameters); + parameters.add(notificationId); + parameters.addAll(dismissedParameters); + String query = "SELECT 1 FROM NOTIFICATION_EVENT n WHERE (" + audienceCondition + ") " + + "AND (" + scopeCondition + ") " + "AND n.NOTIFICATION_ID = ? " + + "AND NOT EXISTS (SELECT 1 FROM NOTIFICATION_USER_STATE us WHERE us.NOTIFICATION_ID = n.NOTIFICATION_ID AND " + + dismissedCondition + ")"; IRDBMSEngine notificationDb = SystemEngineRegistry.getNotificationDb(); PreparedStatement ps = null; - String query = "SELECT COUNT(NOTIFICATIONID) FROM NOTIFICATION " - + "WHERE RECIPIENTID = ? AND RECIPIENTTYPE = ? AND ACTIONTYPE = 'NEW'"; try { ps = notificationDb.getPreparedStatement(query); - int parameterIndex = 1; - ps.setString(parameterIndex++, recipientId); - ps.setString(parameterIndex++, recipientType); + setParameters(ps, parameters); try (ResultSet rs = ps.executeQuery()) { - if (rs.next()) { - return rs.getInt(1); - } + return rs.next(); } } catch (SQLException e) { - classLogger.error("Failed to fetch new notification count for recipient {} (type {})", recipientId, - recipientType, e); + classLogger.error("Failed to check notification visibility [notificationId={}, recipientPairs={}]", + notificationId, recipientPairs, e); } finally { ConnectionUtils.closeAllConnectionsIfPooling(notificationDb, ps); } + return false; + } + + private static int upsertNotificationState(String notificationId, Pair userPair, Boolean isRead, + Timestamp readAt, Boolean isDismissed, Timestamp dismissedAt) { + IRDBMSEngine notificationDb = SystemEngineRegistry.getNotificationDb(); + PreparedStatement updatePs = null; + PreparedStatement insertPs = null; + try { + List sets = new ArrayList<>(); + List parameters = new ArrayList<>(); + if (isRead != null) { + sets.add("IS_READ = ?"); + parameters.add(isRead); + sets.add("READ_AT = ?"); + parameters.add(readAt); + } + if (isDismissed != null) { + sets.add("IS_DISMISSED = ?"); + parameters.add(isDismissed); + sets.add("DISMISSED_AT = ?"); + parameters.add(dismissedAt); + } + if (sets.isEmpty()) { + return 0; + } + parameters.add(notificationId); + parameters.add(userPair.getValue0()); + parameters.add(userPair.getValue1()); + String update = "UPDATE NOTIFICATION_USER_STATE SET " + String.join(", ", sets) + + " WHERE NOTIFICATION_ID = ? AND USER_ID = ? AND USER_TYPE = ?"; + updatePs = notificationDb.getPreparedStatement(update); + setParameters(updatePs, parameters); + int updated = updatePs.executeUpdate(); + if (updated == 0) { + insertPs = notificationDb.getPreparedStatement( + "INSERT INTO NOTIFICATION_USER_STATE (NOTIFICATION_ID,USER_ID,USER_TYPE,IS_READ,READ_AT,IS_DISMISSED,DISMISSED_AT) VALUES (?,?,?,?,?,?,?)"); + insertPs.setString(1, notificationId); + insertPs.setString(2, userPair.getValue0()); + insertPs.setString(3, userPair.getValue1()); + insertPs.setBoolean(4, isRead != null && isRead.booleanValue()); + insertPs.setTimestamp(5, readAt); + insertPs.setBoolean(6, isDismissed != null && isDismissed.booleanValue()); + insertPs.setTimestamp(7, dismissedAt); + insertPs.executeUpdate(); + } + Connection conn = updatePs.getConnection(); + if (conn != null && !conn.getAutoCommit()) { + conn.commit(); + } + return 1; + } catch (SQLException e) { + classLogger.error("Failed to upsert notification state [notificationId={}, userPair={}]", notificationId, + userPair, e); + } finally { + ConnectionUtils.closeAllConnectionsIfPooling(notificationDb, insertPs); + ConnectionUtils.closeAllConnectionsIfPooling(notificationDb, updatePs); + } return 0; } + private static String buildVisibleAudienceSqlCondition(List> recipientPairs, + List accessibleProjectIds, List parameters) { + List conditions = new ArrayList<>(); + for (Pair pair : recipientPairs) { + if (pair == null || pair.getValue0() == null) { + continue; + } + conditions.add( + "(n.AUDIENCE_TYPE = ? AND n.AUDIENCE_ID = ? AND (n.AUDIENCE_USER_TYPE = ? OR n.AUDIENCE_USER_TYPE IS NULL))"); + parameters.add(NotificationConstants.Audience.USER); + parameters.add(pair.getValue0()); + parameters.add(pair.getValue1()); + } + if (accessibleProjectIds != null && !accessibleProjectIds.isEmpty()) { + conditions.add("(n.AUDIENCE_TYPE = ? AND n.SCOPE_TYPE = ? AND n.SCOPE_ID IN (" + + String.join(",", java.util.Collections.nCopies(accessibleProjectIds.size(), "?")) + "))"); + parameters.add(NotificationConstants.Audience.APP_MEMBERS); + parameters.add(NotificationConstants.Scope.APP); + parameters.addAll(accessibleProjectIds); + } + conditions.add("(n.AUDIENCE_TYPE = ?)"); + parameters.add(NotificationConstants.Audience.GLOBAL); + return String.join(" OR ", conditions); + } + + private static String buildVisibleScopeSqlCondition(String scopeType, String scopeId, + List accessibleProjectIds, List parameters) { + String normalizedScopeType = scopeType == null ? NotificationConstants.FetchScope.ALL : scopeType.trim().toUpperCase(); + if (!NotificationConstants.FetchScope.isValid(normalizedScopeType)) { + throw new IllegalArgumentException("Notification scopeType must be ALL, SYSTEM, or APP"); + } + if (NotificationConstants.FetchScope.SYSTEM.equals(normalizedScopeType)) { + parameters.add(NotificationConstants.Scope.SYSTEM); + return "n.SCOPE_TYPE = ?"; + } + if (NotificationConstants.FetchScope.APP.equals(normalizedScopeType)) { + if (scopeId == null || scopeId.trim().isEmpty()) { + throw new IllegalArgumentException("Notification scopeId is required when scopeType is APP"); + } + parameters.add(NotificationConstants.Scope.APP); + parameters.add(scopeId.trim()); + return "n.SCOPE_TYPE = ? AND n.SCOPE_ID = ?"; + } + if (accessibleProjectIds == null || accessibleProjectIds.isEmpty()) { + parameters.add(NotificationConstants.Scope.SYSTEM); + return "n.SCOPE_TYPE = ?"; + } + parameters.add(NotificationConstants.Scope.SYSTEM); + parameters.add(NotificationConstants.Scope.APP); + parameters.addAll(accessibleProjectIds); + return "n.SCOPE_TYPE = ? OR (n.SCOPE_TYPE = ? AND n.SCOPE_ID IN (" + + String.join(",", java.util.Collections.nCopies(accessibleProjectIds.size(), "?")) + "))"; + } + + private static List getAccessibleProjectIds(User user) { + if (user == null) { + return new ArrayList<>(); + } + return SecurityProjectUtils.getUserProjectIdList(user, true, false, true).stream().distinct() + .collect(java.util.stream.Collectors.toList()); + } + + private static String buildStateExistsSqlCondition(String alias, List> recipientPairs, + List parameters, String statePredicate) { + List pairConditions = new ArrayList<>(); + for (Pair pair : recipientPairs) { + if (pair == null || pair.getValue0() == null || pair.getValue1() == null) { + continue; + } + pairConditions.add("(" + alias + ".USER_ID = ? AND " + alias + ".USER_TYPE = ?)"); + parameters.add(pair.getValue0()); + parameters.add(pair.getValue1()); + } + if (pairConditions.isEmpty()) { + pairConditions.add("1 = 0"); + } + return "(" + String.join(" OR ", pairConditions) + ") AND " + statePredicate; + } + + private static Pair firstValidPair(List> recipientPairs) { + for (Pair pair : recipientPairs) { + if (pair != null && pair.getValue0() != null && pair.getValue1() != null) { + return pair; + } + } + return null; + } + + private static void setParameters(PreparedStatement ps, List parameters) throws SQLException { + for (int i = 0; i < parameters.size(); i++) { + ps.setObject(i + 1, parameters.get(i)); + } + } + + private static Long parseLong(String value) { + if (value == null || value.trim().isEmpty()) { + return null; + } + return ((Number) Double.parseDouble(value)).longValue(); + } + + private static String deriveType(String notificationType) { + if (NotificationConstants.Type.USER_REQUEST.equalsIgnoreCase(notificationType)) { + return NotificationConstants.Type.ACCESS_REQUEST; + } + return notificationType; + } + + private static String deriveScopeType(String notificationSource) { + if (NotificationConstants.APP_CATALOG.equalsIgnoreCase(notificationSource)) { + return NotificationConstants.Scope.APP; + } + return NotificationConstants.Scope.SYSTEM; + } + + private static String deriveSourceType(String notificationSource) { + if (NotificationConstants.APP_CATALOG.equalsIgnoreCase(notificationSource)) { + return NotificationConstants.Source.PROJECT; + } + if (notificationSource == null || notificationSource.trim().isEmpty()) { + return NotificationConstants.Source.SYSTEM; + } + return NotificationConstants.Source.ENGINE; + } + + private static String deriveLegacyNotificationSource(String sourceType, String scopeType) { + if (NotificationConstants.Scope.APP.equalsIgnoreCase(scopeType) + || NotificationConstants.Source.PROJECT.equalsIgnoreCase(sourceType)) { + return NotificationConstants.APP_CATALOG; + } + return sourceType; + } + + private static String normalizePriority(String priority) { + if (priority == null || priority.trim().isEmpty()) { + return NotificationConstants.Priority.NORMAL; + } + if (NotificationConstants.Priority.MEDIUM.equalsIgnoreCase(priority)) { + return NotificationConstants.Priority.NORMAL; + } + return priority.toUpperCase(); + } + + private static String normalizeDisplaySurface(String displaySurface) { + if (displaySurface == null || displaySurface.trim().isEmpty()) { + return NotificationConstants.DisplaySurface.BELL; + } + String normalized = displaySurface.trim().toUpperCase(); + if (NotificationConstants.DisplaySurface.isValid(normalized)) { + return normalized; + } + return NotificationConstants.DisplaySurface.BELL; + } + + private static String buildLegacyMetadata(String affectedUserId, String affectedUserType, + String affectedUserPreviousRole, String affectedUserNewRole, String legacyNotificationType, + String legacyNotificationSource) { + JsonObject metadata = new JsonObject(); + addJsonProperty(metadata, "affectedUserId", affectedUserId); + addJsonProperty(metadata, "affectedUserType", affectedUserType); + addJsonProperty(metadata, "affectedUserPreviousRole", affectedUserPreviousRole); + addJsonProperty(metadata, "affectedUserNewRole", affectedUserNewRole); + addJsonProperty(metadata, "legacyNotificationType", legacyNotificationType); + addJsonProperty(metadata, "legacyNotificationSource", legacyNotificationSource); + return metadata.toString(); + } + + private static void addJsonProperty(JsonObject metadata, String key, String value) { + if (value == null) { + return; + } + metadata.addProperty(key, value); + } + + private static JsonObject parseMetadata(String metadataJson) { + if (metadataJson == null || metadataJson.trim().isEmpty()) { + return new JsonObject(); + } + try { + JsonElement element = JsonParser.parseString(metadataJson); + return element != null && element.isJsonObject() ? element.getAsJsonObject() : new JsonObject(); + } catch (Exception e) { + classLogger.warn("Failed to parse notification metadata json", e); + return new JsonObject(); + } + } + + private static String getMetadataString(JsonObject metadata, String key) { + JsonElement element = metadata.get(key); + if (element == null || element.isJsonNull()) { + return null; + } + return element.getAsString(); + } + + private static String getString(ResultSet rs, String column) throws SQLException { + Object value = rs.getObject(column); + if (value == null) { + return null; + } + if (value instanceof Clob) { + Clob clob = (Clob) value; + return clob.getSubString(1L, (int) clob.length()); + } + return String.valueOf(value); + } + } diff --git a/src/prerna/notifications/NotificationOwlCreator.java b/src/prerna/notifications/NotificationOwlCreator.java index aab8139d714..be9b7091c03 100644 --- a/src/prerna/notifications/NotificationOwlCreator.java +++ b/src/prerna/notifications/NotificationOwlCreator.java @@ -50,26 +50,33 @@ public void createColumnsAndTypes(AbstractSqlQueryUtil queryUtil) { this.allSchemas = new ArrayList<>(); // @formatter:off - addTable("NOTIFICATION", Arrays.asList( - Pair.with("NOTIFICATIONID", VARCHAR_255), - Pair.with("RECIPIENTID", VARCHAR_255), - Pair.with("RECIPIENTTYPE", VARCHAR_255), - Pair.with("NOTIFICATIONTITLE", VARCHAR_255), + addTable("NOTIFICATION_EVENT", Arrays.asList( + Pair.with("NOTIFICATION_ID", "VARCHAR(50)"), + Pair.with("TYPE", "VARCHAR(50)"), + Pair.with("SCOPE_TYPE", "VARCHAR(20)"), + Pair.with("SCOPE_ID", "VARCHAR(50)"), + Pair.with("AUDIENCE_TYPE", "VARCHAR(20)"), + Pair.with("AUDIENCE_ID", VARCHAR_255), + Pair.with("AUDIENCE_USER_TYPE", "VARCHAR(50)"), + Pair.with("TITLE", VARCHAR_255), Pair.with("MESSAGE", CLOB_DATATYPE_NAME), - Pair.with("ACTIONTYPE", "VARCHAR(50)"), - Pair.with("ACTIONTARGET", VARCHAR_255), - Pair.with("ISREAD", BOOLEAN_DATATYPE_NAME), Pair.with("PRIORITY", "VARCHAR(20)"), - Pair.with("NOTIFICATIONTYPE", VARCHAR_255), - Pair.with("CATALOGID", VARCHAR_255), - Pair.with("CREATEDBY", VARCHAR_255), - Pair.with("CREATEDDATE", TIMESTAMP_DATATYPE_NAME), - Pair.with("READDATE", TIMESTAMP_DATATYPE_NAME), - Pair.with("NOTIFICATIONSOURCE", VARCHAR_255), - Pair.with("USERID", VARCHAR_255), - Pair.with("USERTYPE", VARCHAR_255), - Pair.with("USEREXISTINGROLE", VARCHAR_255), - Pair.with("USERNEWROLE", VARCHAR_255))); + Pair.with("DISPLAY_SURFACE", "VARCHAR(20)"), + Pair.with("SOURCE_TYPE", "VARCHAR(20)"), + Pair.with("SOURCE_ID", "VARCHAR(50)"), + Pair.with("TARGET_TYPE", "VARCHAR(30)"), + Pair.with("TARGET_ID", "VARCHAR(50)"), + Pair.with("METADATA_JSON", CLOB_DATATYPE_NAME), + Pair.with("CREATED_BY", VARCHAR_255), + Pair.with("CREATED_AT", TIMESTAMP_DATATYPE_NAME))); + addTable("NOTIFICATION_USER_STATE", Arrays.asList( + Pair.with("NOTIFICATION_ID", "VARCHAR(50)"), + Pair.with("USER_ID", VARCHAR_255), + Pair.with("USER_TYPE", "VARCHAR(50)"), + Pair.with("IS_READ", BOOLEAN_DATATYPE_NAME), + Pair.with("READ_AT", TIMESTAMP_DATATYPE_NAME), + Pair.with("IS_DISMISSED", BOOLEAN_DATATYPE_NAME), + Pair.with("DISMISSED_AT", TIMESTAMP_DATATYPE_NAME))); // @formatter:on } } diff --git a/src/prerna/notifications/NotificationService.java b/src/prerna/notifications/NotificationService.java new file mode 100644 index 00000000000..59a1df2bd93 --- /dev/null +++ b/src/prerna/notifications/NotificationService.java @@ -0,0 +1,118 @@ +/******************************************************************************* + * Copyright 2015 Defense Health Agency (DHA) + * + * If your use of this software does not include any GPLv2 components: + * 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 + * + * 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. + * ---------------------------------------------------------------------------- + * If your use of this software includes any GPLv2 components: + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *******************************************************************************/ +package prerna.notifications; + +import org.apache.commons.lang3.StringUtils; + +import prerna.auth.AccessToken; +import prerna.auth.AuthProvider; +import prerna.auth.User; +import prerna.auth.utils.SecurityProjectUtils; +import prerna.om.Insight; +import prerna.util.NotificationConstants; + +/** Public notification creation boundary for application-facing flows. */ +public final class NotificationService { + + private NotificationService() { + } + + public static String createAppAnnouncement(User creator, String projectId, String title, String message, + String priority) { + if (!SecurityProjectUtils.userCanEditProject(creator, projectId)) { + throw new IllegalArgumentException("Project does not exist or user is not an editor of the project"); + } + return createAppNotification(projectId, NotificationConstants.Audience.APP_MEMBERS, null, null, + creator == null ? null : User.getSingleLogginName(creator), title, message, priority); + } + + /** + * Notifies the authenticated insight user within the insight's active app context. + * This has no public reactor because callers must not be able to target other users. + */ + public static String createAppUserNotification(Insight insight, String title, String message, String priority) { + if (insight == null) { + throw new IllegalArgumentException("Insight is required to create an app user notification"); + } + String projectId = requireValue(insight.getContextProjectId(), "app context project id"); + User user = insight.getUser(); + if (user == null || user.isAnonymous()) { + throw new IllegalArgumentException("A signed-in insight user is required to create an app user notification"); + } + if (!SecurityProjectUtils.userCanViewProject(user, projectId)) { + throw new IllegalArgumentException("Insight user does not have access to the active app project"); + } + + AuthProvider provider = user.getPrimaryLogin(); + if (provider == null) { + throw new IllegalArgumentException("Insight user does not have a primary login provider"); + } + AccessToken token = user.getAccessToken(provider); + if (token == null) { + throw new IllegalArgumentException("Insight user does not have an access token for the primary login provider"); + } + return createAppUserNotification(projectId, requireValue(token.getId(), "user id"), provider.toString(), title, + message, priority); + } + + private static String createAppUserNotification(String projectId, String userId, String userType, String title, + String message, String priority) { + return createAppNotification(projectId, NotificationConstants.Audience.USER, requireValue(userId, "user id"), + requireValue(userType, "user type"), userId, title, message, priority); + } + + private static String createAppNotification(String projectId, String audienceType, String audienceId, + String audienceUserType, String createdBy, String title, String message, String priority) { + String normalizedProjectId = requireValue(projectId, "project id"); + String normalizedTitle = requireValue(title, "title"); + if (normalizedTitle.length() > 255) { + throw new IllegalArgumentException("Notification title cannot exceed 255 characters"); + } + String normalizedMessage = requireValue(message, "message"); + String normalizedPriority = normalizePriority(priority); + return NotificationDbUtils.insertNotificationEvent(NotificationConstants.Type.ANNOUNCEMENT, + NotificationConstants.Scope.APP, normalizedProjectId, + audienceType, audienceId, audienceUserType, normalizedTitle, normalizedMessage, normalizedPriority, + NotificationConstants.DisplaySurface.BELL, NotificationConstants.Source.PROJECT, normalizedProjectId, + NotificationConstants.Target.APP, normalizedProjectId, null, createdBy); + } + + private static String normalizePriority(String priority) { + String normalized = StringUtils.defaultIfBlank(priority, NotificationConstants.Priority.NORMAL).trim() + .toUpperCase(); + if (!NotificationConstants.Priority.isValid(normalized)) { + throw new IllegalArgumentException("Notification priority must be LOW, NORMAL, HIGH, or URGENT"); + } + return normalized; + } + + private static String requireValue(String value, String label) { + if (StringUtils.isBlank(value)) { + throw new IllegalArgumentException("Notification " + label + " is required"); + } + return value.trim(); + } +} diff --git a/src/prerna/reactor/engine/RequestEngineReactor.java b/src/prerna/reactor/engine/RequestEngineReactor.java index 2cd79178159..5cd829cfb0a 100644 --- a/src/prerna/reactor/engine/RequestEngineReactor.java +++ b/src/prerna/reactor/engine/RequestEngineReactor.java @@ -106,8 +106,9 @@ public NounMetadata execute() { if (Utility.isNotificationDatabaseEnabled()) { String priority = AccessPermissionEnum.isOwner(requestPermission) ? NotificationConstants.Priority.HIGH : NotificationConstants.Priority.MEDIUM; - NotificationDbUtils.createNotification(user, userId, userType, engineId, - NotificationConstants.Type.USER_REQUEST, engineType, priority, null, permission); + NotificationDbUtils.createNotification(user, userId, userType, engineId, + NotificationConstants.Type.USER_REQUEST, engineType, priority, null, permission, + NotificationConstants.DisplaySurface.BELL); EmailUtility.sendAccessRequestEmailNotification(user, engineId, permission, requestComment, EmailUtility.RESOURCE_TYPE.ENGINE); diff --git a/src/prerna/reactor/notification/CreateAppNotificationReactor.java b/src/prerna/reactor/notification/CreateAppNotificationReactor.java new file mode 100644 index 00000000000..2342a4ef486 --- /dev/null +++ b/src/prerna/reactor/notification/CreateAppNotificationReactor.java @@ -0,0 +1,77 @@ +/******************************************************************************* + * Copyright 2015 Defense Health Agency (DHA) + * + * If your use of this software does not include any GPLv2 components: + * 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 + * + * 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. + * ---------------------------------------------------------------------------- + * If your use of this software includes any GPLv2 components: + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *******************************************************************************/ +package prerna.reactor.notification; + +import java.util.HashMap; +import java.util.Map; + +import prerna.auth.User; +import prerna.auth.utils.AbstractSecurityUtils; +import prerna.notifications.NotificationService; +import prerna.reactor.AbstractReactor; +import prerna.sablecc2.om.PixelDataType; +import prerna.sablecc2.om.ReactorKeysEnum; +import prerna.sablecc2.om.nounmeta.NounMetadata; +import prerna.util.Utility; + +/** Creates an app-wide announcement for the authenticated app editor/owner. */ +public class CreateAppNotificationReactor extends AbstractReactor { + + private static final String TITLE = "title"; + private static final String PRIORITY = "priority"; + + public CreateAppNotificationReactor() { + this.keysToGet = new String[] { ReactorKeysEnum.PROJECT.getKey(), TITLE, ReactorKeysEnum.MESSAGE.getKey(), + PRIORITY }; + this.keyRequired = new int[] { 1, 1, 1, 0 }; + } + + @Override + public NounMetadata execute() { + if (!Utility.isNotificationDatabaseEnabled()) { + throw new IllegalArgumentException("Notifications are not enabled on this instance"); + } + organizeKeys(); + User user = this.insight.getUser(); + if (user == null || (AbstractSecurityUtils.anonymousUsersEnabled() && user.isAnonymous())) { + throwAnonymousUserError(); + } + + String notificationId = NotificationService.createAppAnnouncement(user, + this.keyValue.get(ReactorKeysEnum.PROJECT.getKey()), this.keyValue.get(TITLE), + this.keyValue.get(ReactorKeysEnum.MESSAGE.getKey()), this.keyValue.get(PRIORITY)); + Map response = new HashMap<>(); + response.put("notificationId", notificationId); + response.put("scopeType", "APP"); + response.put("audienceType", "APP_MEMBERS"); + return new NounMetadata(response, PixelDataType.MAP); + } + + @Override + public String getReactorDescription() { + return "Create an announcement for all users of an app"; + } +} diff --git a/src/prerna/reactor/notification/DeleteNotificationReactor.java b/src/prerna/reactor/notification/DeleteNotificationReactor.java index 397b22d2f9d..928efae6274 100644 --- a/src/prerna/reactor/notification/DeleteNotificationReactor.java +++ b/src/prerna/reactor/notification/DeleteNotificationReactor.java @@ -27,18 +27,12 @@ *******************************************************************************/ package prerna.reactor.notification; -import java.util.List; - -import org.javatuples.Pair; - import prerna.auth.User; import prerna.auth.utils.AbstractSecurityUtils; import prerna.notifications.NotificationDbUtils; import prerna.reactor.AbstractReactor; import prerna.sablecc2.om.PixelDataType; -import prerna.sablecc2.om.PixelOperationType; import prerna.sablecc2.om.ReactorKeysEnum; -import prerna.sablecc2.om.execptions.SemossPixelException; import prerna.sablecc2.om.nounmeta.NounMetadata; import prerna.util.Utility; @@ -62,20 +56,7 @@ public NounMetadata execute() { organizeKeys(); String notificationId = this.keyValue.get(this.keysToGet[0]); - List> userIdAndTypeList = User.getUserIdAndType(user); - if (userIdAndTypeList == null || userIdAndTypeList.isEmpty()) { - throw new SemossPixelException(new NounMetadata("Unable to determine user type for deletion", - PixelDataType.CONST_STRING, PixelOperationType.ERROR, PixelOperationType.LOGGIN_REQUIRED_ERROR)); - } - - String recipientId = userIdAndTypeList.get(0).getValue0(); - String recipientType = userIdAndTypeList.get(0).getValue1(); - int deleteCount; - if (notificationId != null) { - deleteCount = NotificationDbUtils.deleteNotification(null, null, notificationId); - } else { - deleteCount = NotificationDbUtils.deleteNotification(recipientId, recipientType, null); - } + int deleteCount = NotificationDbUtils.deleteNotification(user, notificationId); return new NounMetadata(deleteCount, PixelDataType.CONST_INT); } diff --git a/src/prerna/reactor/notification/FetchAppNotificationsReactor.java b/src/prerna/reactor/notification/FetchAppNotificationsReactor.java new file mode 100644 index 00000000000..96f5db4f27c --- /dev/null +++ b/src/prerna/reactor/notification/FetchAppNotificationsReactor.java @@ -0,0 +1,89 @@ +/******************************************************************************* + * Copyright 2015 Defense Health Agency (DHA) + * + * If your use of this software does not include any GPLv2 components: + * 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 + * + * 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. + * ---------------------------------------------------------------------------- + * If your use of this software includes any GPLv2 components: + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *******************************************************************************/ +package prerna.reactor.notification; + +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang3.StringUtils; + +import prerna.auth.User; +import prerna.auth.utils.AbstractSecurityUtils; +import prerna.auth.utils.SecurityProjectUtils; +import prerna.notifications.NotificationDbUtils; +import prerna.reactor.AbstractReactor; +import prerna.sablecc2.om.PixelDataType; +import prerna.sablecc2.om.ReactorKeysEnum; +import prerna.sablecc2.om.nounmeta.NounMetadata; +import prerna.util.NotificationConstants; +import prerna.util.Utility; + +/** Fetches notifications scoped to the project bound to the current insight. */ +public class FetchAppNotificationsReactor extends AbstractReactor { + + public FetchAppNotificationsReactor() { + this.keysToGet = new String[] { ReactorKeysEnum.LIMIT.getKey(), ReactorKeysEnum.OFFSET.getKey() }; + this.keyRequired = new int[] { 0, 0 }; + } + + @Override + public NounMetadata execute() { + if (!Utility.isNotificationDatabaseEnabled()) { + throw new IllegalArgumentException("Notifications are not enabled on this instance"); + } + organizeKeys(); + User user = this.insight.getUser(); + if (user == null || (AbstractSecurityUtils.anonymousUsersEnabled() && user.isAnonymous())) { + throwAnonymousUserError(); + } + + String projectId = getInsightProjectId(); + if (!SecurityProjectUtils.userCanViewProject(user, projectId)) { + throw new IllegalArgumentException("Project does not exist or user does not have access to the project"); + } + + List> notifications = NotificationDbUtils.fetchNotifications(user, + NotificationConstants.FetchScope.APP, projectId, this.keyValue.get(ReactorKeysEnum.LIMIT.getKey()), + this.keyValue.get(ReactorKeysEnum.OFFSET.getKey())); + if (!notifications.isEmpty()) { + NotificationDbUtils.resetNotificationActionType(user, NotificationConstants.FetchScope.APP, projectId); + } + return new NounMetadata(notifications, PixelDataType.MAP); + } + + @Override + public String getReactorDescription() { + return "Fetch notifications for the project bound to the current insight"; + } + + private String getInsightProjectId() { + String projectId = this.insight.getContextProjectId(); + if (StringUtils.isBlank(projectId)) { + throw new IllegalStateException("Current insight is not associated with an app project"); + } + return projectId; + } +} diff --git a/src/prerna/reactor/notification/FetchNotificationsReactor.java b/src/prerna/reactor/notification/FetchNotificationsReactor.java index 37cd7e72b1a..a02fa24803b 100644 --- a/src/prerna/reactor/notification/FetchNotificationsReactor.java +++ b/src/prerna/reactor/notification/FetchNotificationsReactor.java @@ -32,6 +32,7 @@ import prerna.auth.User; import prerna.auth.utils.AbstractSecurityUtils; +import prerna.auth.utils.SecurityProjectUtils; import prerna.notifications.NotificationDbUtils; import prerna.reactor.AbstractReactor; import prerna.sablecc2.om.PixelDataType; @@ -39,13 +40,17 @@ import prerna.sablecc2.om.ReactorKeysEnum; import prerna.sablecc2.om.execptions.SemossPixelException; import prerna.sablecc2.om.nounmeta.NounMetadata; +import prerna.util.NotificationConstants; import prerna.util.Utility; public class FetchNotificationsReactor extends AbstractReactor { + private static final String SCOPE_TYPE = "scopeType"; + private static final String SCOPE_ID = "scopeId"; public FetchNotificationsReactor() { - this.keysToGet = new String[] { ReactorKeysEnum.LIMIT.getKey(), ReactorKeysEnum.OFFSET.getKey() }; - this.keyRequired = new int[] { 0, 0 }; + this.keysToGet = new String[] { ReactorKeysEnum.LIMIT.getKey(), ReactorKeysEnum.OFFSET.getKey(), SCOPE_TYPE, + SCOPE_ID }; + this.keyRequired = new int[] { 0, 0, 0, 0 }; } @Override @@ -57,6 +62,8 @@ public NounMetadata execute() { User user = this.insight.getUser(); String limit = this.keyValue.get(ReactorKeysEnum.LIMIT.getKey()); String offset = this.keyValue.get(ReactorKeysEnum.OFFSET.getKey()); + String scopeType = normalizeScopeType(this.keyValue.get(SCOPE_TYPE)); + String scopeId = this.keyValue.get(SCOPE_ID); if (user == null) { NounMetadata noun = new NounMetadata( "User must be signed into an account to retrieve the function engine files", @@ -68,11 +75,15 @@ public NounMetadata execute() { if (user == null || (AbstractSecurityUtils.anonymousUsersEnabled() && user.isAnonymous())) { throwAnonymousUserError(); } + if (NotificationConstants.FetchScope.APP.equals(scopeType) + && !SecurityProjectUtils.userCanViewProject(user, scopeId)) { + throw new IllegalArgumentException("Project does not exist or user does not have access to the project"); + } - List> allNotifications = null; - allNotifications = NotificationDbUtils.fetchAllNotifications(user, limit, offset); + List> allNotifications = NotificationDbUtils.fetchNotifications(user, scopeType, scopeId, limit, + offset); if (!allNotifications.isEmpty()) { - NotificationDbUtils.resetNotificationActionType(user); + NotificationDbUtils.resetNotificationActionType(user, scopeType, scopeId); } return new NounMetadata(allNotifications, PixelDataType.MAP); @@ -82,4 +93,17 @@ public NounMetadata execute() { public String getReactorDescription() { return "Fetch all user notifications"; } + + private String normalizeScopeType(String scopeType) { + String normalized = scopeType == null || scopeType.trim().isEmpty() ? NotificationConstants.FetchScope.ALL + : scopeType.trim().toUpperCase(); + if (!NotificationConstants.FetchScope.isValid(normalized)) { + throw new IllegalArgumentException("Notification scopeType must be ALL, SYSTEM, or APP"); + } + if (NotificationConstants.FetchScope.APP.equals(normalized) + && (this.keyValue.get(SCOPE_ID) == null || this.keyValue.get(SCOPE_ID).trim().isEmpty())) { + throw new IllegalArgumentException("Notification scopeId is required when scopeType is APP"); + } + return normalized; + } } diff --git a/src/prerna/reactor/notification/MarkNotificationReadReactor.java b/src/prerna/reactor/notification/MarkNotificationReadReactor.java index 6ad7376d844..c6138ef2e56 100644 --- a/src/prerna/reactor/notification/MarkNotificationReadReactor.java +++ b/src/prerna/reactor/notification/MarkNotificationReadReactor.java @@ -56,7 +56,7 @@ public NounMetadata execute() { organizeKeys(); String notificationId = this.keyValue.get(this.keysToGet[0]); Timestamp readAt = Utility.getCurrentSqlTimestampUTC(); - NotificationDbUtils.markNotificationRead(notificationId, readAt); + NotificationDbUtils.markNotificationRead(this.insight.getUser(), notificationId, readAt); NounMetadata retNoun = NounMetadata.getSuccessNounMessage("Success!"); return retNoun; } diff --git a/src/prerna/reactor/notification/PollAppNotificationsReactor.java b/src/prerna/reactor/notification/PollAppNotificationsReactor.java new file mode 100644 index 00000000000..e1068d0ce48 --- /dev/null +++ b/src/prerna/reactor/notification/PollAppNotificationsReactor.java @@ -0,0 +1,70 @@ +/******************************************************************************* + * Copyright 2015 Defense Health Agency (DHA) + * + * If your use of this software does not include any GPLv2 components: + * 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 + * + * 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. + * ---------------------------------------------------------------------------- + * If your use of this software includes any GPLv2 components: + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *******************************************************************************/ +package prerna.reactor.notification; + +import org.apache.commons.lang3.StringUtils; + +import prerna.auth.User; +import prerna.auth.utils.AbstractSecurityUtils; +import prerna.auth.utils.SecurityProjectUtils; +import prerna.notifications.NotificationDbUtils; +import prerna.reactor.AbstractReactor; +import prerna.sablecc2.om.PixelDataType; +import prerna.sablecc2.om.nounmeta.NounMetadata; +import prerna.util.NotificationConstants; +import prerna.util.Utility; + +/** Counts unread notifications for the project bound to the current insight. */ +public class PollAppNotificationsReactor extends AbstractReactor { + + @Override + public NounMetadata execute() { + if (!Utility.isNotificationDatabaseEnabled()) { + throw new IllegalArgumentException("Notifications are not enabled on this instance"); + } + User user = this.insight.getUser(); + if (user == null || (AbstractSecurityUtils.anonymousUsersEnabled() && user.isAnonymous())) { + throwAnonymousUserError(); + } + + String projectId = this.insight.getContextProjectId(); + if (StringUtils.isBlank(projectId)) { + throw new IllegalStateException("Current insight is not associated with an app project"); + } + if (!SecurityProjectUtils.userCanViewProject(user, projectId)) { + throw new IllegalArgumentException("Project does not exist or user does not have access to the project"); + } + + int count = NotificationDbUtils.fetchNewNotificationCount(user, NotificationConstants.FetchScope.APP, + projectId); + return new NounMetadata(count, PixelDataType.CONST_INT); + } + + @Override + public String getReactorDescription() { + return "Get unread notifications for the project bound to the current insight"; + } +} diff --git a/src/prerna/reactor/notification/PollNotificationsReactor.java b/src/prerna/reactor/notification/PollNotificationsReactor.java index e48f1a2ea05..4bd93803b9f 100644 --- a/src/prerna/reactor/notification/PollNotificationsReactor.java +++ b/src/prerna/reactor/notification/PollNotificationsReactor.java @@ -27,41 +27,44 @@ *******************************************************************************/ package prerna.reactor.notification; -import java.util.List; - -import org.javatuples.Pair; - import prerna.auth.User; import prerna.auth.utils.AbstractSecurityUtils; +import prerna.auth.utils.SecurityProjectUtils; import prerna.notifications.NotificationDbUtils; import prerna.reactor.AbstractReactor; import prerna.sablecc2.om.PixelDataType; -import prerna.sablecc2.om.PixelOperationType; -import prerna.sablecc2.om.execptions.SemossPixelException; import prerna.sablecc2.om.nounmeta.NounMetadata; +import prerna.util.NotificationConstants; import prerna.util.Utility; public class PollNotificationsReactor extends AbstractReactor { + private static final String SCOPE_TYPE = "scopeType"; + private static final String SCOPE_ID = "scopeId"; + + public PollNotificationsReactor() { + this.keysToGet = new String[] { SCOPE_TYPE, SCOPE_ID }; + this.keyRequired = new int[] { 0, 0 }; + } @Override public NounMetadata execute() { if (!Utility.isNotificationDatabaseEnabled()) { throw new IllegalArgumentException("Notifications are not enabled on this instance"); } + organizeKeys(); User user = this.insight.getUser(); if (user == null || (AbstractSecurityUtils.anonymousUsersEnabled() && user.isAnonymous())) { throwAnonymousUserError(); } - List> userIdAndTypeList = User.getUserIdAndType(user); - if (userIdAndTypeList == null || userIdAndTypeList.isEmpty()) { - throw new SemossPixelException(new NounMetadata("Unable to determine user type for deletion", - PixelDataType.CONST_STRING, PixelOperationType.ERROR, PixelOperationType.LOGGIN_REQUIRED_ERROR)); + String scopeType = normalizeScopeType(this.keyValue.get(SCOPE_TYPE)); + String scopeId = this.keyValue.get(SCOPE_ID); + if (NotificationConstants.FetchScope.APP.equals(scopeType) + && !SecurityProjectUtils.userCanViewProject(user, scopeId)) { + throw new IllegalArgumentException("Project does not exist or user does not have access to the project"); } - String recipientId = userIdAndTypeList.get(0).getValue0(); - String recipientType = userIdAndTypeList.get(0).getValue1(); - int newNotificationCount = NotificationDbUtils.fetchNewNotificationCount(recipientId, recipientType); + int newNotificationCount = NotificationDbUtils.fetchNewNotificationCount(user, scopeType, scopeId); return new NounMetadata(newNotificationCount, PixelDataType.CONST_INT); } @@ -69,4 +72,17 @@ public NounMetadata execute() { public String getReactorDescription() { return "Get the number of new notifications for the user"; } + + private String normalizeScopeType(String scopeType) { + String normalized = scopeType == null || scopeType.trim().isEmpty() ? NotificationConstants.FetchScope.ALL + : scopeType.trim().toUpperCase(); + if (!NotificationConstants.FetchScope.isValid(normalized)) { + throw new IllegalArgumentException("Notification scopeType must be ALL, SYSTEM, or APP"); + } + if (NotificationConstants.FetchScope.APP.equals(normalized) + && (this.keyValue.get(SCOPE_ID) == null || this.keyValue.get(SCOPE_ID).trim().isEmpty())) { + throw new IllegalArgumentException("Notification scopeId is required when scopeType is APP"); + } + return normalized; + } } diff --git a/src/prerna/reactor/project/RequestProjectReactor.java b/src/prerna/reactor/project/RequestProjectReactor.java index c8b33b36f33..d20158c1efa 100644 --- a/src/prerna/reactor/project/RequestProjectReactor.java +++ b/src/prerna/reactor/project/RequestProjectReactor.java @@ -103,9 +103,9 @@ public NounMetadata execute() { if (Utility.isNotificationDatabaseEnabled()) { String priority = AccessPermissionEnum.isOwner(requestPermission) ? NotificationConstants.Priority.HIGH : NotificationConstants.Priority.MEDIUM; - NotificationDbUtils.createNotification(user, userId, userType, projectId, - NotificationConstants.Type.USER_REQUEST, NotificationConstants.APP_CATALOG, priority, null, - permission); + NotificationDbUtils.createNotification(user, userId, userType, projectId, + NotificationConstants.Type.USER_REQUEST, NotificationConstants.APP_CATALOG, priority, null, + permission, NotificationConstants.DisplaySurface.BELL); EmailUtility.sendAccessRequestEmailNotification(user, projectId, permission, requestComment, EmailUtility.RESOURCE_TYPE.PROJECT); diff --git a/src/prerna/util/NotificationConstants.java b/src/prerna/util/NotificationConstants.java index 0a013b67c1d..232f43d8db2 100644 --- a/src/prerna/util/NotificationConstants.java +++ b/src/prerna/util/NotificationConstants.java @@ -27,6 +27,8 @@ *******************************************************************************/ package prerna.util; +import java.util.Set; + public final class NotificationConstants { private NotificationConstants() { @@ -36,9 +38,31 @@ private NotificationConstants() { public static final String APP_CATALOG = "APP"; public static final class Priority { + public static final String URGENT = "URGENT"; public static final String HIGH = "HIGH"; + public static final String NORMAL = "NORMAL"; public static final String MEDIUM = "MEDIUM"; public static final String LOW = "LOW"; + + private static final Set VALUES = Set.of(URGENT, HIGH, NORMAL, LOW); + + public static boolean isValid(String priority) { + return VALUES.contains(priority); + } + } + + // In-app render surface; external channels are modeled separately in Phase 5. + public static final class DisplaySurface { + public static final String BELL = "BELL"; + public static final String MODAL = "MODAL"; + public static final String TOAST = "TOAST"; + public static final String BANNER = "BANNER"; + + private static final Set VALUES = Set.of(BELL, MODAL, TOAST, BANNER); + + public static boolean isValid(String displaySurface) { + return VALUES.contains(displaySurface); + } } public static final class Type { @@ -48,6 +72,48 @@ public static final class Type { public static final String PERMISSION_CHANGE = "PERMISSION_CHANGE"; public static final String REQUEST_DENIAL = "REQUEST_DENIAL"; public static final String SMSS_UPDATE = "SMSS_UPDATE"; + public static final String ACCESS_REQUEST = "ACCESS_REQUEST"; + public static final String ANNOUNCEMENT = "ANNOUNCEMENT"; + public static final String APP_TASK_COMPLETE = "APP_TASK_COMPLETE"; + } + + public static final class Scope { + public static final String SYSTEM = "SYSTEM"; + public static final String APP = "APP"; + } + + /** Scope selectors accepted by notification read APIs. ALL is never persisted. */ + public static final class FetchScope { + public static final String ALL = "ALL"; + public static final String SYSTEM = Scope.SYSTEM; + public static final String APP = Scope.APP; + + private static final Set VALUES = Set.of(ALL, SYSTEM, APP); + + public static boolean isValid(String scope) { + return VALUES.contains(scope); + } + } + + public static final class Audience { + public static final String USER = "USER"; + public static final String APP_MEMBERS = "APP_MEMBERS"; + public static final String APP_OWNERS = "APP_OWNERS"; + public static final String APP_EDITORS = "APP_EDITORS"; + public static final String GLOBAL = "GLOBAL"; + } + + public static final class Source { + public static final String SYSTEM = "SYSTEM"; + public static final String USER = "USER"; + public static final String ENGINE = "ENGINE"; + public static final String PROJECT = "PROJECT"; + } + + public static final class Target { + public static final String NONE = "NONE"; + public static final String ROUTE = "ROUTE"; + public static final String APP = "APP"; } }