xref: /OpenGrok/opengrok-indexer/src/main/java/org/opengrok/indexer/web/messages/MessagesContainer.java (revision aa6abf429bacc2c0baa482bff3022e77ef23c183)
1 /*
2  * CDDL HEADER START
3  *
4  * The contents of this file are subject to the terms of the
5  * Common Development and Distribution License (the "License").
6  * You may not use this file except in compliance with the License.
7  *
8  * See LICENSE.txt included in this distribution for the specific
9  * language governing permissions and limitations under the License.
10  *
11  * When distributing Covered Code, include this CDDL HEADER in each
12  * file and include the License file at LICENSE.txt.
13  * If applicable, add the following below this CDDL HEADER, with the
14  * fields enclosed by brackets "[]" replaced with your own identifying
15  * information: Portions Copyright [yyyy] [name of copyright owner]
16  *
17  * CDDL HEADER END
18  */
19 
20 /*
21  * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
22  */
23 package org.opengrok.indexer.web.messages;
24 
25 import com.fasterxml.jackson.annotation.JsonIgnore;
26 import com.fasterxml.jackson.annotation.JsonProperty;
27 import com.fasterxml.jackson.core.JsonGenerator;
28 import com.fasterxml.jackson.databind.SerializerProvider;
29 import com.fasterxml.jackson.databind.annotation.JsonSerialize;
30 import com.fasterxml.jackson.databind.ser.std.StdSerializer;
31 import jakarta.validation.constraints.NotBlank;
32 import org.opengrok.indexer.logger.LoggerFactory;
33 
34 import java.io.IOException;
35 import java.text.DateFormat;
36 import java.time.Instant;
37 import java.util.Collection;
38 import java.util.Date;
39 import java.util.HashMap;
40 import java.util.Locale;
41 import java.util.Map;
42 import java.util.Objects;
43 import java.util.Set;
44 import java.util.SortedSet;
45 import java.util.Timer;
46 import java.util.TimerTask;
47 import java.util.TreeSet;
48 import java.util.function.Predicate;
49 import java.util.logging.Level;
50 import java.util.logging.Logger;
51 import java.util.stream.Collectors;
52 
53 public class MessagesContainer {
54 
55     private static final Logger LOGGER = LoggerFactory.getLogger(MessagesContainer.class);
56 
57     public static final String MESSAGES_MAIN_PAGE_TAG = "main";
58 
59     private static final int DEFAULT_MESSAGE_LIMIT = 100;
60 
61     private final Map<String, SortedSet<AcceptedMessage>> tagMessages = new HashMap<>();
62 
63     private int messagesInTheSystem = 0;
64 
65     private int messageLimit = DEFAULT_MESSAGE_LIMIT;
66 
67     private Timer expirationTimer;
68 
69     private final Object lock = new Object();
70 
71     /**
72      * @return all messages regardless their tag
73      */
getAllMessages()74     public Set<AcceptedMessage> getAllMessages() {
75         synchronized (lock) {
76             if (expirationTimer == null) {
77                 expireMessages();
78             }
79             return tagMessages.values().stream().flatMap(Collection::stream).collect(Collectors.toSet());
80         }
81     }
82 
83     /**
84      * Get the default set of messages for the main tag.
85      *
86      * @return set of messages
87      */
getMessages()88     public SortedSet<AcceptedMessage> getMessages() {
89         synchronized (lock) {
90             if (expirationTimer == null) {
91                 expireMessages();
92             }
93             return emptyMessageSet(tagMessages.get(MESSAGES_MAIN_PAGE_TAG));
94         }
95     }
96 
97     /**
98      * Get the set of messages for the arbitrary tag.
99      *
100      * @param tag the message tag
101      * @return set of messages
102      */
getMessages(final String tag)103     public SortedSet<AcceptedMessage> getMessages(final String tag) {
104         if (tag == null) {
105             throw new IllegalArgumentException("Cannot get messages for null tag");
106         }
107 
108         synchronized (lock) {
109             if (expirationTimer == null) {
110                 expireMessages();
111             }
112             return emptyMessageSet(tagMessages.get(tag));
113         }
114     }
115 
116     /**
117      * Add a message to the application.
118      * Also schedules a expiration timer to remove this message after its expiration.
119      *
120      * @param m the message
121      */
addMessage(final Message m)122     public void addMessage(final Message m) {
123         synchronized (lock) {
124             if (m == null) {
125                 throw new IllegalArgumentException("Cannot add null message");
126             }
127 
128             if (isMessageLimitExceeded()) {
129                 LOGGER.log(Level.WARNING, "cannot add message to the system, " +
130                                 "exceeded Configuration messageLimit of {0}", messageLimit);
131                 throw new IllegalStateException("Cannot add message - message limit exceeded");
132             }
133 
134             if (expirationTimer == null) {
135                 expireMessages();
136             }
137 
138             AcceptedMessage acceptedMessage = new AcceptedMessage(m);
139             addMessage(acceptedMessage);
140         }
141     }
142 
addMessage(final AcceptedMessage acceptedMessage)143     private void addMessage(final AcceptedMessage acceptedMessage) {
144         boolean added = false;
145         for (String tag : acceptedMessage.getMessage().getTags()) {
146             if (!tagMessages.containsKey(tag)) {
147                 tagMessages.put(tag, new TreeSet<>());
148             }
149             if (tagMessages.get(tag).add(acceptedMessage)) {
150                 messagesInTheSystem++;
151                 added = true;
152             }
153         }
154 
155         if (added && expirationTimer != null) {
156             expirationTimer.schedule(new TimerTask() {
157                 @Override
158                 public void run() {
159                     expireMessages();
160                 }
161             }, Date.from(acceptedMessage.getExpirationTime().plusMillis(10)));
162         }
163     }
164 
165     /**
166      * Remove all messages containing at least one of the tags.
167      *
168      * @param tags set of tags
169      */
removeAnyMessage(Set<String> tags)170     public void removeAnyMessage(Set<String> tags) {
171         if (tags == null) {
172             return;
173         }
174         removeAnyMessage(t -> t.getMessage().hasAny(tags));
175     }
176 
removeAnyMessage(Set<String> tags, String text)177     public void removeAnyMessage(Set<String> tags, String text) {
178         if (tags == null) {
179             return;
180         }
181         removeAnyMessage(t -> t.getMessage().hasTagsAndText(tags, text));
182     }
183 
184     /**
185      * Remove messages which have expired.
186      */
expireMessages()187     private void expireMessages() {
188         removeAnyMessage(AcceptedMessage::isExpired);
189     }
190 
191     /**
192      * Generic function to remove any message according to the result of the
193      * predicate.
194      *
195      * @param predicate the testing predicate
196      */
removeAnyMessage(Predicate<AcceptedMessage> predicate)197     private void removeAnyMessage(Predicate<AcceptedMessage> predicate) {
198         synchronized (lock) {
199             int size;
200             for (Map.Entry<String, SortedSet<AcceptedMessage>> set : tagMessages.entrySet()) {
201                 size = set.getValue().size();
202                 set.getValue().removeIf(predicate);
203                 messagesInTheSystem -= size - set.getValue().size();
204             }
205 
206             tagMessages.entrySet().removeIf(entry -> entry.getValue().isEmpty());
207         }
208     }
209 
isMessageLimitExceeded()210     private boolean isMessageLimitExceeded() {
211         return messagesInTheSystem >= messageLimit;
212     }
213 
214     /**
215      * Set the maximum number of messages in the application.
216      *
217      * @param limit the new limit
218      */
setMessageLimit(int limit)219     public void setMessageLimit(int limit) {
220         messageLimit = limit;
221     }
222 
startExpirationTimer()223     public void startExpirationTimer() {
224         if (expirationTimer != null) {
225             stopExpirationTimer();
226         }
227         expirationTimer = new Timer("expirationThread");
228         expireMessages();
229     }
230 
231     /**
232      * Stops the watch dog service.
233      */
stopExpirationTimer()234     public void stopExpirationTimer() {
235         if (expirationTimer != null) {
236             expirationTimer.cancel();
237             expirationTimer = null;
238         }
239     }
240 
emptyMessageSet(SortedSet<AcceptedMessage> toRet)241     private static SortedSet<AcceptedMessage> emptyMessageSet(SortedSet<AcceptedMessage> toRet) {
242         return toRet == null ? new TreeSet<>() : toRet;
243     }
244 
245     public static class AcceptedMessage implements Comparable<AcceptedMessage>, JSONable {
246 
247         private final Instant acceptedTime = Instant.now();
248 
249         // The message member is ignored so that it can be flattened using the getters specified below.
250         @JsonIgnore
251         private final Message message;
252 
253         @JsonProperty("text")
254         @NotBlank(message = "text cannot be empty")
255         @JsonSerialize(using = Message.HTMLSerializer.class)
getText()256         public String getText() {
257             return message.getText();
258         }
259 
260         @JsonProperty("messageLevel")
261         @JsonSerialize(using = Message.MessageLevelSerializer.class)
getMessageLevel()262         public Message.MessageLevel getMessageLevel() {
263             return message.getMessageLevel();
264         }
265 
AcceptedMessage(final Message message)266         private AcceptedMessage(final Message message) {
267             this.message = message;
268         }
269 
270         @JsonProperty("created")
271         @JsonSerialize(using = InstantSerializer.class)
getAcceptedTime()272         public Instant getAcceptedTime() {
273             return acceptedTime;
274         }
275 
276         @JsonProperty("tags")
getTags()277         public Set<String> getTags() {
278             return message.getTags();
279         }
280 
getMessage()281         public Message getMessage() {
282             return message;
283         }
284 
isExpired()285         public boolean isExpired() {
286             return getExpirationTime().isBefore(Instant.now());
287         }
288 
289         @JsonProperty("expiration")
290         @JsonSerialize(using = InstantSerializer.class)
getExpirationTime()291         public Instant getExpirationTime() {
292             return acceptedTime.plus(message.getDuration());
293         }
294 
295         @Override
compareTo(final AcceptedMessage o)296         public int compareTo(final AcceptedMessage o) {
297             int cmpRes = acceptedTime.compareTo(o.acceptedTime);
298             if (cmpRes == 0) {
299                 return message.compareTo(o.message);
300             }
301             return cmpRes;
302         }
303 
304         @Override
equals(final Object o)305         public boolean equals(final Object o) {
306             if (this == o) {
307                 return true;
308             }
309             if (o == null || getClass() != o.getClass()) {
310                 return false;
311             }
312             AcceptedMessage that = (AcceptedMessage) o;
313             return Objects.equals(acceptedTime, that.acceptedTime)
314                     && Objects.equals(message, that.message);
315         }
316 
317         @Override
hashCode()318         public int hashCode() {
319             return Objects.hash(acceptedTime, message);
320         }
321 
322         private static class InstantSerializer extends StdSerializer<Instant> {
323 
324             private static final long serialVersionUID = -369908820170764793L;
325 
InstantSerializer()326             InstantSerializer() {
327                 this(null);
328             }
329 
InstantSerializer(final Class<Instant> cl)330             InstantSerializer(final Class<Instant> cl) {
331                 super(cl);
332             }
333 
334             @Override
serialize( final Instant instant, final JsonGenerator jsonGenerator, final SerializerProvider serializerProvider )335             public void serialize(
336                     final Instant instant,
337                     final JsonGenerator jsonGenerator,
338                     final SerializerProvider serializerProvider
339             ) throws IOException {
340                 if (instant != null) {
341                     DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, Locale.ROOT);
342                     jsonGenerator.writeString(formatter.format(Date.from(instant)));
343                 } else {
344                     jsonGenerator.writeNull();
345                 }
346             }
347         }
348     }
349 
350 }
351