001/* 002 * Copyright 2015-2022 Transmogrify LLC, 2022-2026 Revetware LLC. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package com.pyranid; 018 019import org.jspecify.annotations.NonNull; 020import org.jspecify.annotations.Nullable; 021 022import javax.annotation.concurrent.ThreadSafe; 023import java.util.Objects; 024 025import static java.lang.String.format; 026import static java.util.Objects.requireNonNull; 027 028/** 029 * A named database notification received by a {@link NotificationSession}. 030 * <p> 031 * Notifications are transient, lossy hints rather than durable events. An implementation may coalesce notifications, 032 * and the number of returned {@code Notification} instances does not represent an underlying event count. 033 * <p> 034 * This value deliberately omits any backend-specific sender identifier. Payload nullability and null/empty-string 035 * handling are database-specific; Pyranid performs no generic normalization. 036 * 037 * @author <a href="https://www.revetkn.com">Mark Allen</a> 038 * @since 4.6.0 039 */ 040@ThreadSafe 041public final class Notification { 042 @NonNull 043 private final String channel; 044 @Nullable 045 private final String payload; 046 047 private Notification(@NonNull String channel, 048 @Nullable String payload) { 049 this.channel = requireNonNull(channel); 050 this.payload = payload; 051 } 052 053 /** 054 * Creates a notification value. 055 * <p> 056 * This factory enforces the common channel contract but does not apply backend-specific byte limits. 057 * 058 * @param channel nonblank notification channel, which must not contain a NUL character 059 * @param payload notification payload, which may be null or empty 060 * @return a notification value 061 * @throws NullPointerException if {@code channel} is null 062 * @throws IllegalArgumentException if {@code channel} is blank or contains a NUL character 063 * @since 4.6.0 064 */ 065 @NonNull 066 public static Notification of(@NonNull String channel, 067 @Nullable String payload) { 068 validateChannel(channel); 069 070 return new Notification(channel, payload); 071 } 072 073 @NonNull 074 static String validateChannel(@NonNull String channel) { 075 requireNonNull(channel); 076 077 if (channel.isBlank()) 078 throw new IllegalArgumentException("channel must not be blank"); 079 080 if (channel.indexOf('\0') >= 0) 081 throw new IllegalArgumentException("channel must not contain a NUL character"); 082 083 return channel; 084 } 085 086 /** 087 * Gets the notification channel. 088 * 089 * @return notification channel 090 * @since 4.6.0 091 */ 092 @NonNull 093 public String getChannel() { 094 return this.channel; 095 } 096 097 /** 098 * Gets the notification payload. 099 * 100 * @return notification payload, which may be null or empty according to backend behavior 101 * @since 4.6.0 102 */ 103 @Nullable 104 public String getPayload() { 105 return this.payload; 106 } 107 108 @Override 109 public boolean equals(@Nullable Object object) { 110 if (this == object) 111 return true; 112 113 if (!(object instanceof Notification notification)) 114 return false; 115 116 return Objects.equals(getChannel(), notification.getChannel()) 117 && Objects.equals(getPayload(), notification.getPayload()); 118 } 119 120 @Override 121 public int hashCode() { 122 return Objects.hash(getChannel(), getPayload()); 123 } 124 125 /** 126 * Returns a diagnostic representation containing the channel and payload length, but never the payload contents. 127 * A non-null payload's length is its Java {@link String#length()} in UTF-16 code units, not a backend-specific 128 * encoded byte count. 129 * 130 * @return diagnostic representation of this notification 131 * @since 4.6.0 132 */ 133 @Override 134 @NonNull 135 public String toString() { 136 String payload = getPayload(); 137 String payloadLength = payload == null ? "null" : String.valueOf(payload.length()); 138 139 return format("%s{channel=%s, payloadLength=%s}", 140 getClass().getSimpleName(), getChannel(), payloadLength); 141 } 142}