1 /* 2 * Copyright (C) 2004-2008 Jive Software. All rights reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package org.jivesoftware.openfire; 18 19 import org.dom4j.Element; 20 import org.dom4j.Namespace; 21 import org.dom4j.QName; 22 import org.jivesoftware.database.DbConnectionManager; 23 import org.jivesoftware.database.SequenceManager; 24 import org.jivesoftware.openfire.cluster.ClusterManager; 25 import org.jivesoftware.openfire.container.BasicModule; 26 import org.jivesoftware.openfire.event.UserEventDispatcher; 27 import org.jivesoftware.openfire.event.UserEventListener; 28 import org.jivesoftware.openfire.user.User; 29 import org.jivesoftware.openfire.user.UserManager; 30 import org.jivesoftware.util.*; 31 import org.jivesoftware.util.cache.Cache; 32 import org.jivesoftware.util.cache.CacheFactory; 33 import org.slf4j.Logger; 34 import org.slf4j.LoggerFactory; 35 import org.xmpp.packet.JID; 36 import org.xmpp.packet.Message; 37 38 import java.sql.Connection; 39 import java.sql.PreparedStatement; 40 import java.sql.ResultSet; 41 import java.sql.SQLException; 42 import java.time.Duration; 43 import java.time.Instant; 44 import java.time.temporal.ChronoUnit; 45 import java.util.*; 46 import java.util.concurrent.ExecutionException; 47 import java.util.regex.Matcher; 48 import java.util.regex.Pattern; 49 50 /** 51 * Represents the user's offline message storage. A message store holds messages that were 52 * sent to the user while they were unavailable. The user can retrieve their messages by 53 * setting their presence to "available". The messages will then be delivered normally. 54 * Offline message storage is optional, in which case a null implementation is returned that 55 * always throws UnauthorizedException when adding messages to the store. 56 * 57 * @author Iain Shigeoka 58 */ 59 public class OfflineMessageStore extends BasicModule implements UserEventListener { 60 61 private static final Logger Log = LoggerFactory.getLogger(OfflineMessageStore.class); 62 63 private static final String INSERT_OFFLINE = 64 "INSERT INTO ofOffline (username, messageID, creationDate, messageSize, stanza) " + 65 "VALUES (?, ?, ?, ?, ?)"; 66 private static final String LOAD_OFFLINE = 67 "SELECT stanza, creationDate FROM ofOffline WHERE username=? ORDER BY creationDate ASC"; 68 private static final String LOAD_OFFLINE_MESSAGE = 69 "SELECT stanza FROM ofOffline WHERE username=? AND creationDate=?"; 70 private static final String SELECT_COUNT_OFFLINE = 71 "SELECT COUNT(*) FROM ofOffline WHERE username=?"; 72 private static final String SELECT_SIZE_OFFLINE = 73 "SELECT SUM(messageSize) FROM ofOffline WHERE username=?"; 74 private static final String SELECT_SIZE_ALL_OFFLINE = 75 "SELECT SUM(messageSize) FROM ofOffline"; 76 private static final String DELETE_OFFLINE = 77 "DELETE FROM ofOffline WHERE username=?"; 78 private static final String DELETE_OFFLINE_MESSAGE = 79 "DELETE FROM ofOffline WHERE username=? AND creationDate=?"; 80 private static final String DELETE_OFFLINE_MESSAGE_BEFORE = 81 "DELETE FROM ofOffline WHERE creationDate < ?"; 82 private static final String SELECT_SIZE_OFFLINE_ALL_USERS = 83 "SELECT SUM(messageSize),username FROM ofOffline GROUP BY username"; 84 85 private final Cache<String, Integer> sizeCache; 86 87 /** 88 * Members for automatic offline message cleaning 89 * */ 90 91 public static final SystemProperty<Duration> OFFLINE_AUTOCLEAN_DAYSTOLIVE = SystemProperty.Builder.ofType(Duration.class) 92 .setKey("xmpp.offline.autoclean.daystolive") 93 .setDefaultValue(Duration.ofDays(365)) 94 .setChronoUnit(ChronoUnit.DAYS) 95 .setDynamic(false) 96 .build(); 97 98 public static final SystemProperty<Duration> OFFLINE_AUTOCLEAN_CHECKINTERVAL = SystemProperty.Builder.ofType(Duration.class) 99 .setKey("xmpp.offline.autoclean.checkinterval") 100 .setDefaultValue(Duration.ofMinutes(30)) 101 .setChronoUnit(ChronoUnit.MINUTES) 102 .setDynamic(false) 103 .build(); 104 105 public static final SystemProperty<Boolean> OFFLINE_AUTOCLEAN_ENABLE = SystemProperty.Builder.ofType(Boolean.class) 106 .setKey("xmpp.offline.autoclean.enabled") 107 .setDefaultValue(false) 108 .setDynamic(false) 109 .build(); 110 111 private Timer timer = null; 112 113 /** 114 * Pattern to use for detecting invalid XML characters. Invalid XML characters will 115 * be removed from the stored offline messages. 116 */ 117 private Pattern pattern = Pattern.compile("&#[\\d]+;"); 118 119 /** 120 * Returns the instance of {@code OfflineMessageStore} being used by the XMPPServer. 121 * 122 * @return the instance of {@code OfflineMessageStore} being used by the XMPPServer. 123 */ getInstance()124 public static OfflineMessageStore getInstance() { 125 return XMPPServer.getInstance().getOfflineMessageStore(); 126 } 127 128 /** 129 * Constructs a new offline message store. 130 */ OfflineMessageStore()131 public OfflineMessageStore() { 132 super("Offline Message Store"); 133 sizeCache = CacheFactory.createCache("Offline Message Size"); 134 OFFLINE_AUTOCLEAN_ENABLE.addListener( enabled -> { 135 if (enabled) { 136 setTimer(); 137 } else { 138 cancelTimer(); 139 } } ); 140 OFFLINE_AUTOCLEAN_CHECKINTERVAL.addListener( duration -> { 141 // Restart timer if it's running to apply new configuration. 142 if ( OFFLINE_AUTOCLEAN_ENABLE.getValue()) { 143 cancelTimer(); 144 setTimer(); 145 } } ); 146 } 147 148 /** 149 * Adds a message to this message store. Messages will be stored and made 150 * available for later delivery. 151 * 152 * Note that certain messages are ignored by this implementation, for example, messages that are carbon copies, 153 * have 'no-store' hints, or for which the intended recipient is not a local user. When a message is discarded for 154 * reasons like these, this method will return 'null'. 155 * 156 * @param message the message to store. 157 * @return OfflineMessage when data was stored, otherwise null. 158 */ addMessage(Message message)159 public OfflineMessage addMessage(Message message) { 160 if (message == null) { 161 Log.trace( "Not storing null message." ); 162 return null; 163 } 164 if(!shouldStoreMessage(message)) { 165 Log.trace( "Not storing message, as 'should store' returned false." ); 166 return null; 167 } 168 JID recipient = message.getTo(); 169 String username = recipient.getNode(); 170 // If the username is null (such as when an anonymous user), don't store. 171 if (username == null || !UserManager.getInstance().isRegisteredUser(recipient, false)) { 172 Log.trace( "Not storing message for which the recipient ({}) is not a registered local user.", recipient ); 173 return null; 174 } 175 176 long messageID = SequenceManager.nextID(JiveConstants.OFFLINE); 177 178 // Get the message in XML format. 179 String msgXML = message.getElement().asXML(); 180 181 Connection con = null; 182 PreparedStatement pstmt = null; 183 OfflineMessage offlineMessage = null; 184 try { 185 con = DbConnectionManager.getConnection(); 186 pstmt = con.prepareStatement(INSERT_OFFLINE); 187 188 Date creationDate = new java.util.Date(); 189 pstmt.setString(1, username); 190 pstmt.setLong(2, messageID); 191 pstmt.setString(3, StringUtils.dateToMillis(creationDate)); 192 pstmt.setInt(4, msgXML.length()); 193 pstmt.setString(5, msgXML); 194 pstmt.executeUpdate(); 195 196 offlineMessage = new OfflineMessage(creationDate, message.getElement()); 197 } 198 catch (Exception e) { 199 Log.error(LocaleUtils.getLocalizedString("admin.error"), e); 200 return null; 201 } 202 finally { 203 DbConnectionManager.closeConnection(pstmt, con); 204 } 205 206 // Update the cached size if it exists. 207 if (sizeCache.containsKey(username)) { 208 int size = sizeCache.get(username); 209 size += msgXML.length(); 210 sizeCache.put(username, size); 211 } 212 return offlineMessage; 213 } 214 215 /** 216 * Returns a Collection of all messages in the store for a user. 217 * Messages may be deleted after being selected from the database depending on 218 * the delete param. 219 * 220 * @param username the username of the user who's messages you'd like to receive. 221 * @param delete true if the offline messages should be deleted. 222 * @return An iterator of packets containing all offline messages. 223 */ getMessages(String username, boolean delete)224 public Collection<OfflineMessage> getMessages(String username, boolean delete) { 225 List<OfflineMessage> messages = new ArrayList<>(); 226 Connection con = null; 227 PreparedStatement pstmt = null; 228 ResultSet rs = null; 229 try { 230 con = DbConnectionManager.getConnection(); 231 pstmt = con.prepareStatement(LOAD_OFFLINE); 232 pstmt.setString(1, username); 233 rs = pstmt.executeQuery(); 234 while (rs.next()) { 235 String msgXML = rs.getString(1); 236 Date creationDate = new Date(Long.parseLong(rs.getString(2).trim())); 237 OfflineMessage message; 238 try { 239 message = new OfflineMessage(creationDate, SAXReaderUtil.readRootElement(msgXML)); 240 } catch (ExecutionException e) { 241 // Try again after removing invalid XML chars (e.g. ) 242 Matcher matcher = pattern.matcher(msgXML); 243 if (matcher.find()) { 244 msgXML = matcher.replaceAll(""); 245 } 246 try { 247 message = new OfflineMessage(creationDate, SAXReaderUtil.readRootElement(msgXML)); 248 } catch (ExecutionException de) { 249 Log.error("Failed to route packet (offline message): " + msgXML, de); 250 continue; // skip and process remaining offline messages 251 } catch (InterruptedException de) { 252 Thread.currentThread().interrupt(); 253 Log.error("Offline Message retrieval interrupted", de); 254 break; // Skip all further offline messages 255 } 256 } 257 catch (InterruptedException e) { 258 Thread.currentThread().interrupt(); 259 Log.error("Offline Message retrieval interrupted", e); 260 break; // Skip all further offline messages 261 } 262 263 // if there is already a delay stamp, we shouldn't add another. 264 Element delaytest = message.getChildElement("delay", "urn:xmpp:delay"); 265 if (delaytest == null) { 266 // Add a delayed delivery (XEP-0203) element to the message. 267 Element delay = message.addChildElement("delay", "urn:xmpp:delay"); 268 delay.addAttribute("from", XMPPServer.getInstance().getServerInfo().getXMPPDomain()); 269 delay.addAttribute("stamp", XMPPDateTimeFormat.format(creationDate)); 270 } 271 messages.add(message); 272 } 273 // Check if the offline messages loaded should be deleted, and that there are 274 // messages to delete. 275 if (delete && !messages.isEmpty()) { 276 PreparedStatement pstmt2 = null; 277 try { 278 pstmt2 = con.prepareStatement(DELETE_OFFLINE); 279 pstmt2.setString(1, username); 280 pstmt2.executeUpdate(); 281 removeUsernameFromSizeCache(username); 282 } 283 catch (Exception e) { 284 Log.error("Error deleting offline messages of username: " + username, e); 285 } 286 finally { 287 DbConnectionManager.closeStatement(pstmt2); 288 } 289 } 290 } 291 catch (Exception e) { 292 Log.error("Error retrieving offline messages of username: " + username, e); 293 } 294 finally { 295 DbConnectionManager.closeConnection(rs, pstmt, con); 296 } 297 return messages; 298 } 299 300 /** 301 * Returns the offline message of the specified user with the given creation date. The 302 * returned message will NOT be deleted from the database. 303 * 304 * @param username the username of the user who's message you'd like to receive. 305 * @param creationDate the date when the offline message was stored in the database. 306 * @return the offline message of the specified user with the given creation stamp. 307 */ getMessage(String username, Date creationDate)308 public OfflineMessage getMessage(String username, Date creationDate) { 309 OfflineMessage message = null; 310 Connection con = null; 311 PreparedStatement pstmt = null; 312 ResultSet rs = null; 313 try { 314 con = DbConnectionManager.getConnection(); 315 pstmt = con.prepareStatement(LOAD_OFFLINE_MESSAGE); 316 pstmt.setString(1, username); 317 pstmt.setString(2, StringUtils.dateToMillis(creationDate)); 318 rs = pstmt.executeQuery(); 319 while (rs.next()) { 320 String msgXML = rs.getString(1); 321 message = new OfflineMessage(creationDate, SAXReaderUtil.readRootElement(msgXML)); 322 // Add a delayed delivery (XEP-0203) element to the message. 323 Element delay = message.addChildElement("delay", "urn:xmpp:delay"); 324 delay.addAttribute("from", XMPPServer.getInstance().getServerInfo().getXMPPDomain()); 325 delay.addAttribute("stamp", XMPPDateTimeFormat.format(creationDate)); 326 } 327 } 328 catch (Exception e) { 329 Log.error("Error retrieving offline messages of username: " + username + 330 " creationDate: " + creationDate, e); 331 if (e instanceof InterruptedException) { 332 Thread.currentThread().interrupt(); 333 } 334 } 335 finally { 336 DbConnectionManager.closeConnection(rs, pstmt, con); 337 } 338 return message; 339 } 340 341 /** 342 * Deletes all offline messages in the store for a user. 343 * 344 * @param username the username of the user who's messages are going to be deleted. 345 */ deleteMessages(String username)346 public void deleteMessages(String username) { 347 Connection con = null; 348 PreparedStatement pstmt = null; 349 try { 350 con = DbConnectionManager.getConnection(); 351 pstmt = con.prepareStatement(DELETE_OFFLINE); 352 pstmt.setString(1, username); 353 pstmt.executeUpdate(); 354 355 removeUsernameFromSizeCache(username); 356 } 357 catch (Exception e) { 358 Log.error("Error deleting offline messages of username: " + username, e); 359 } 360 finally { 361 DbConnectionManager.closeConnection(pstmt, con); 362 } 363 } 364 removeUsernameFromSizeCache(String username)365 private void removeUsernameFromSizeCache(String username) { 366 // Update the cached size if it exists. 367 sizeCache.remove(username); 368 } 369 370 /** 371 * Deletes the specified offline message in the store for a user. The way to identify the 372 * message to delete is based on the creationDate and username. 373 * 374 * @param username the username of the user who's message is going to be deleted. 375 * @param creationDate the date when the offline message was stored in the database. 376 */ deleteMessage(String username, Date creationDate)377 public void deleteMessage(String username, Date creationDate) { 378 Connection con = null; 379 PreparedStatement pstmt = null; 380 try { 381 con = DbConnectionManager.getConnection(); 382 pstmt = con.prepareStatement(DELETE_OFFLINE_MESSAGE); 383 pstmt.setString(1, username); 384 pstmt.setString(2, StringUtils.dateToMillis(creationDate)); 385 pstmt.executeUpdate(); 386 387 // Force a refresh for next call to getSize(username), 388 // it's easier than loading the message to be deleted just 389 // to update the cache. 390 removeUsernameFromSizeCache(username); 391 } 392 catch (Exception e) { 393 Log.error("Error deleting offline messages of username: " + username + 394 " creationDate: " + creationDate, e); 395 } 396 finally { 397 DbConnectionManager.closeConnection(pstmt, con); 398 } 399 } 400 401 /** 402 * Returns the number of XML messages stored for a particular user. 403 * 404 * @param username the username of the user. 405 * @return the amount of stored messages. 406 */ getCount(String username)407 public int getCount(String username) { 408 // No cache: this needs to be more accurate than the 'size' method (that does have a cache). 409 // Maintaining a cache would likely add more overhead than that the cache would save. 410 int count = 0; 411 Connection con = null; 412 PreparedStatement pstmt = null; 413 ResultSet rs = null; 414 try { 415 con = DbConnectionManager.getConnection(); 416 pstmt = con.prepareStatement(SELECT_COUNT_OFFLINE); 417 pstmt.setString(1, username); 418 rs = pstmt.executeQuery(); 419 if (rs.next()) { 420 count = rs.getInt(1); 421 } 422 } 423 catch (Exception e) { 424 Log.error(LocaleUtils.getLocalizedString("admin.error"), e); 425 } 426 finally { 427 DbConnectionManager.closeConnection(rs, pstmt, con); 428 } 429 return count; 430 } 431 432 /** 433 * Returns the approximate size (in bytes) of the XML messages stored for 434 * a particular user. 435 * 436 * @param username the username of the user. 437 * @return the approximate size of stored messages (in bytes). 438 */ getSize(String username)439 public int getSize(String username) { 440 // See if the size is cached. 441 if (sizeCache.containsKey(username)) { 442 return sizeCache.get(username); 443 } 444 int size = 0; 445 Connection con = null; 446 PreparedStatement pstmt = null; 447 ResultSet rs = null; 448 try { 449 con = DbConnectionManager.getConnection(); 450 pstmt = con.prepareStatement(SELECT_SIZE_OFFLINE); 451 pstmt.setString(1, username); 452 rs = pstmt.executeQuery(); 453 if (rs.next()) { 454 size = rs.getInt(1); 455 } 456 // Add the value to cache. 457 sizeCache.put(username, size); 458 } 459 catch (Exception e) { 460 Log.error(LocaleUtils.getLocalizedString("admin.error"), e); 461 } 462 finally { 463 DbConnectionManager.closeConnection(rs, pstmt, con); 464 } 465 return size; 466 } 467 468 /** 469 * Returns the approximate size (in bytes) of the XML messages stored for all 470 * users. 471 * 472 * @return the approximate size of all stored messages (in bytes). 473 */ getSize()474 public int getSize() { 475 int size = 0; 476 Connection con = null; 477 PreparedStatement pstmt = null; 478 ResultSet rs = null; 479 try { 480 con = DbConnectionManager.getConnection(); 481 pstmt = con.prepareStatement(SELECT_SIZE_ALL_OFFLINE); 482 rs = pstmt.executeQuery(); 483 if (rs.next()) { 484 size = rs.getInt(1); 485 } 486 } 487 catch (Exception e) { 488 Log.error(LocaleUtils.getLocalizedString("admin.error"), e); 489 } 490 finally { 491 DbConnectionManager.closeConnection(rs, pstmt, con); 492 } 493 return size; 494 } 495 496 @Override userCreated(User user, Map<String, Object> params)497 public void userCreated(User user, Map<String, Object> params) { 498 //Do nothing 499 } 500 501 @Override userDeleting(User user, Map<String, Object> params)502 public void userDeleting(User user, Map<String, Object> params) { 503 // Delete all offline messages of the user 504 deleteMessages(user.getUsername()); 505 } 506 507 @Override userModified(User user, Map<String, Object> params)508 public void userModified(User user, Map<String, Object> params) { 509 //Do nothing 510 } 511 512 @Override start()513 public void start() throws IllegalStateException { 514 super.start(); 515 516 // Add this module as a user event listener so we can delete 517 // all offline messages when a user is deleted 518 UserEventDispatcher.addListener(this); 519 //start timer if enabled 520 if (OFFLINE_AUTOCLEAN_ENABLE.getValue()) 521 { 522 setTimer(); 523 } 524 } 525 526 @Override stop()527 public void stop() { 528 super.stop(); 529 // Remove this module as a user event listener 530 UserEventDispatcher.removeListener(this); 531 //stop timer if started 532 cancelTimer(); 533 } 534 535 /** 536 * Decide whether a message should be stored offline according to XEP-0160 and XEP-0334. 537 * 538 * @param message The message to evaluate. Cannot be null. 539 * @return <code>true</code> if the message should be stored offline, <code>false</code> otherwise. 540 */ shouldStoreMessage(final Message message)541 static boolean shouldStoreMessage(final Message message) { 542 // XEP-0334: Implement the <no-store/> hint to override offline storage 543 if (message.getChildElement("no-store", "urn:xmpp:hints") != null) { 544 return false; 545 } 546 547 // OF-2083: Prevent storing offline message that is already stored 548 if (message.getChildElement("offline", "http://jabber.org/protocol/offline") != null) { 549 return false; 550 } 551 552 switch (message.getType()) { 553 case chat: 554 // XEP-0160: Messages with a 'type' attribute whose value is "chat" SHOULD be stored offline, with the exception of messages that contain only Chat State Notifications (XEP-0085) [7] content 555 556 // Iterate through the child elements to see if we can find anything that's not a chat state notification or 557 // real time text notification 558 Iterator<?> it = message.getElement().elementIterator(); 559 560 while (it.hasNext()) { 561 Object item = it.next(); 562 563 if (item instanceof Element) { 564 Element el = (Element) item; 565 if (Namespace.NO_NAMESPACE.equals(el.getNamespace())) { 566 continue; 567 } 568 if (!el.getNamespaceURI().equals("http://jabber.org/protocol/chatstates") 569 && !(el.getQName().equals(QName.get("rtt", "urn:xmpp:rtt:0"))) 570 ) { 571 return true; 572 } 573 } 574 } 575 576 return message.getBody() != null && !message.getBody().isEmpty(); 577 578 case groupchat: 579 case headline: 580 // XEP-0160: "groupchat" message types SHOULD NOT be stored offline 581 // XEP-0160: "headline" message types SHOULD NOT be stored offline 582 return false; 583 584 case error: 585 // XEP-0160: "error" message types SHOULD NOT be stored offline, 586 // although a server MAY store advanced message processing errors offline 587 if (message.getChildElement("amp", "http://jabber.org/protocol/amp") == null) { 588 return false; 589 } 590 break; 591 592 default: 593 // XEP-0160: Messages with a 'type' attribute whose value is "normal" (or messages with no 'type' attribute) SHOULD be stored offline. 594 break; 595 } 596 return true; 597 } 598 setTimer()599 private void setTimer() { 600 cancelTimer(); 601 602 Log.info("Offline message cleaning - Start timer"); 603 timer = new Timer(true); 604 timer.schedule(new TimerTask() { 605 @Override 606 public void run() { 607 try { 608 if (ClusterManager.isClusteringStarted()) 609 { 610 if (ClusterManager.isSeniorClusterMember()) 611 { 612 if (deleteOldOfflineMessagesFromDB()) 613 { 614 readSizeForAllUsers(); 615 } 616 } 617 } 618 else 619 { 620 if (deleteOldOfflineMessagesFromDB()) 621 { 622 readSizeForAllUsers(); 623 } 624 } 625 } catch (Exception e) { 626 Log.error("Offline message cleaning - Could not set timer for check interval!", e); 627 } 628 } 629 }, Duration.ofSeconds(10).toMillis(), OFFLINE_AUTOCLEAN_CHECKINTERVAL.getValue().toMillis()); // starts after 10 seconds and repeat... 630 } 631 cancelTimer()632 private void cancelTimer() { 633 Log.info("Offline message cleaning - Stop old timer if started"); 634 if (timer != null) { 635 try { 636 timer.cancel(); 637 timer.purge(); 638 } catch (Exception e) { 639 Log.warn("Offline message cleaning - Could not stop the timer!", e); 640 } 641 } 642 timer = null; 643 } 644 readSizeForAllUsers()645 public void readSizeForAllUsers() { 646 // See if the size is cached. 647 sizeCache.clear(); 648 649 int size = 0; 650 String username = null; 651 Connection con = null; 652 PreparedStatement pstmt = null; 653 ResultSet rs = null; 654 try { 655 con = DbConnectionManager.getConnection(); 656 pstmt = con.prepareStatement(SELECT_SIZE_OFFLINE_ALL_USERS); 657 658 rs = pstmt.executeQuery(); 659 while (rs.next()) { 660 size = rs.getInt(1); 661 username = rs.getString(2); 662 // Add the value to cache. 663 sizeCache.put(username, size); 664 } 665 } 666 catch (Exception e) { 667 Log.error(LocaleUtils.getLocalizedString("admin.error"), e); 668 } 669 finally { 670 DbConnectionManager.closeConnection(rs, pstmt, con); 671 } 672 } 673 deleteOldOfflineMessagesFromDB()674 private boolean deleteOldOfflineMessagesFromDB() { 675 676 Log.info("Offline message cleaning - Deleting offline messages older than {} days.", OFFLINE_AUTOCLEAN_DAYSTOLIVE.getValue().toDays()); 677 678 Connection con = null; 679 680 PreparedStatement pstmt = null; 681 try { 682 con = DbConnectionManager.getConnection(); 683 684 pstmt = con.prepareStatement(DELETE_OFFLINE_MESSAGE_BEFORE); 685 686 final Instant pastTime = Instant.now().minus(OFFLINE_AUTOCLEAN_DAYSTOLIVE.getValue()); 687 final String creationDatePast = StringUtils.zeroPadString(String.valueOf(pastTime.toEpochMilli()), 15); 688 689 pstmt.setString(1,creationDatePast); 690 final int updateCount = pstmt.executeUpdate(); 691 Log.info("Offline message cleaning - Cleaning successful. Removed {} message(s)", updateCount); 692 return true; 693 } catch (SQLException sqle) { 694 Log.warn("Offline message cleaning - ", sqle); 695 return false; 696 } finally { 697 DbConnectionManager.closeConnection(pstmt, con); 698 } 699 } 700 } 701