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