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}