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. &#12;)
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