UpdateManager.java 34.4 KB
Newer Older
1 2 3 4 5
/**
 * $RCSfile$
 * $Revision: $
 * $Date: $
 *
6
 * Copyright (C) 2005-2008 Jive Software. All rights reserved.
7
 *
8 9 10 11 12 13 14 15 16 17 18
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
19 20
 */

21
package org.jivesoftware.openfire.update;
Gaston Dombiak's avatar
Gaston Dombiak committed
22

23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.StringReader;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

39
import org.apache.commons.httpclient.HostConfiguration;
Gaston Dombiak's avatar
Gaston Dombiak committed
40 41 42 43 44 45 46 47 48 49
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentFactory;
import org.dom4j.Element;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.SAXReader;
50 51 52 53
import org.jivesoftware.openfire.MessageRouter;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.container.BasicModule;
import org.jivesoftware.openfire.container.Plugin;
54 55 56 57 58 59
import org.jivesoftware.util.JiveConstants;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.XMLWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Gaston Dombiak's avatar
Gaston Dombiak committed
60 61 62 63 64 65 66
import org.xmpp.packet.JID;
import org.xmpp.packet.Message;

/**
 * Service that frequently checks for new server or plugins releases. By default the service
 * will check every 48 hours for updates. Use the system property <tt>update.frequency</tt>
 * to set new values.<p>
67
 * <p/>
Gaston Dombiak's avatar
Gaston Dombiak committed
68 69 70 71 72 73 74
 * New versions of plugins can be downloaded and installed. However, new server releases
 * should be manually installed.
 *
 * @author Gaston Dombiak
 */
public class UpdateManager extends BasicModule {

75 76
	private static final Logger Log = LoggerFactory.getLogger(UpdateManager.class);

Gaston Dombiak's avatar
Gaston Dombiak committed
77 78 79 80 81
    protected static DocumentFactory docFactory = DocumentFactory.getInstance();

    /**
     * URL of the servlet (JSP) that provides the "check for update" service.
     */
82
    private static String updateServiceURL = "http://www.igniterealtime.org/projects/openfire/versions.jsp";
Gaston Dombiak's avatar
Gaston Dombiak committed
83 84

    /**
85 86 87 88 89 90 91 92 93 94
     * Information about the available server update.
     */
    private Update serverUpdate;

    /**
     * List of plugins that need to be updated.
     */
    private Collection<Update> pluginUpdates = new ArrayList<Update>();

    /**
95
     * List of plugins available at igniterealtime.org.
Gaston Dombiak's avatar
Gaston Dombiak committed
96
     */
97
    private Map<String, AvailablePlugin> availablePlugins = new HashMap<String, AvailablePlugin>();
Gaston Dombiak's avatar
Gaston Dombiak committed
98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114

    /**
     * Thread that performs the periodic checks for updates.
     */
    private Thread thread;

    /**
     * Router to use for sending notitication messages to admins.
     */
    private MessageRouter router;
    private String serverName;


    public UpdateManager() {
        super("Update manager");
    }

115 116
    @Override
	public void start() throws IllegalStateException {
Gaston Dombiak's avatar
Gaston Dombiak committed
117
        super.start();
118
        startService();
Gaston Dombiak's avatar
Gaston Dombiak committed
119 120 121 122 123 124 125 126
    }

    /**
     * Starts sevice that checks for new updates.
     */
    private void startService() {
        // Thread that performs the periodic checks for updates
        thread = new Thread("Update Manager") {
127 128
            @Override
			public void run() {
Gaston Dombiak's avatar
Gaston Dombiak committed
129
                try {
Matt Tucker's avatar
Matt Tucker committed
130 131 132 133
                    // Sleep for 5 seconds before starting to work. This is required because
                    // this module has a dependency on the PluginManager, which is loaded
                    // after all other modules.
                    Thread.sleep(5000);
134 135
                    // Load last saved information (if any)
                    loadSavedInfo();
136
                    while (isServiceEnabled()) {
137
                        waitForNextCheck();
138
                        // Check if the service is still enabled
Gaston Dombiak's avatar
Gaston Dombiak committed
139
                        if (isServiceEnabled()) {
140 141 142 143 144 145 146 147
                            try {
                                // Check for server updates
                                checkForServerUpdate(true);
                                // Refresh list of available plugins and check for plugin updates
                                checkForPluginsUpdates(true);
                            }
                            catch (Exception e) {
                                Log.error("Error checking for updates", e);
Gaston Dombiak's avatar
Gaston Dombiak committed
148
                            }
149 150 151 152 153
                            // Keep track of the last time we checked for updates. 
                            long now = System.currentTimeMillis();
                            JiveGlobals.setProperty("update.lastCheck", String.valueOf(now));
                            // As an extra precaution, make sure that that the value
                            // we just set is saved. If not, return to make sure that
154
                            // no additional update checks are performed until Openfire
155 156 157 158 159 160
                            // is restarted.
                            if (now != JiveGlobals.getLongProperty("update.lastCheck", 0)) {
                                Log.error("Error: update service check did not save correctly. " +
                                        "Stopping update service.");
                                return;
                            }
161
                        }
Gaston Dombiak's avatar
Gaston Dombiak committed
162 163 164
                    }
                }
                catch (InterruptedException e) {
165
                    Log.error(e.getMessage(), e);
Gaston Dombiak's avatar
Gaston Dombiak committed
166 167 168 169 170 171 172
                }
                finally {
                    // Clean up reference to this thread
                    thread = null;
                }
            }

173 174
            private void waitForNextCheck() throws InterruptedException {
                long lastCheck = JiveGlobals.getLongProperty("update.lastCheck", 0);
Gaston Dombiak's avatar
Gaston Dombiak committed
175 176
                if (lastCheck == 0) {
                    // This is the first time the server is used (since we added this feature)
Matt Tucker's avatar
Matt Tucker committed
177
                    Thread.sleep(30000);
178 179
                }
                else {
Gaston Dombiak's avatar
Gaston Dombiak committed
180
                    long elapsed = System.currentTimeMillis() - lastCheck;
181 182 183
                    long frequency = getCheckFrequency() * JiveConstants.HOUR;
                    // Sleep until we've waited the appropriate amount of time.
                    while (elapsed < frequency) {
Gaston Dombiak's avatar
Gaston Dombiak committed
184
                        Thread.sleep(frequency - elapsed);
185 186 187
                        // Update the elapsed time. This check is necessary just in case the
                        // thread woke up early.
                        elapsed = System.currentTimeMillis() - lastCheck;
Gaston Dombiak's avatar
Gaston Dombiak committed
188 189 190 191 192 193 194 195
                    }
                }
            }
        };
        thread.setDaemon(true);
        thread.start();
    }

196 197
    @Override
	public void initialize(XMPPServer server) {
Gaston Dombiak's avatar
Gaston Dombiak committed
198 199
        super.initialize(server);
        router = server.getMessageRouter();
200
        serverName = server.getServerInfo().getXMPPDomain();
Gaston Dombiak's avatar
Gaston Dombiak committed
201 202 203
    }

    /**
204
     * Queries the igniterealtime.org server for new server and plugin updates.
Gaston Dombiak's avatar
Gaston Dombiak committed
205 206 207 208
     *
     * @param notificationsEnabled true if admins will be notified when new updates are found.
     * @throws Exception if some error happens during the query.
     */
209
    public synchronized void checkForServerUpdate(boolean notificationsEnabled) throws Exception {
Gaston Dombiak's avatar
Gaston Dombiak committed
210
        // Get the XML request to include in the HTTP request
211
        String requestXML = getServerUpdateRequest();
Gaston Dombiak's avatar
Gaston Dombiak committed
212 213
        // Send the request to the server
        HttpClient httpClient = new HttpClient();
214 215 216 217 218 219
        // Check if a proxy should be used
        if (isUsingProxy()) {
            HostConfiguration hc = new HostConfiguration();
            hc.setProxy(getProxyHost(), getProxyPort());
            httpClient.setHostConfiguration(hc);
        }
Gaston Dombiak's avatar
Gaston Dombiak committed
220 221 222 223 224 225
        PostMethod postMethod = new PostMethod(updateServiceURL);
        NameValuePair[] data = {
                new NameValuePair("type", "update"),
                new NameValuePair("query", requestXML)
        };
        postMethod.setRequestBody(data);
226 227 228 229 230 231 232 233 234
        if (httpClient.executeMethod(postMethod) == 200) {
            // Process answer from the server
            String responseBody = postMethod.getResponseBodyAsString();
            processServerUpdateResponse(responseBody, notificationsEnabled);
        }
    }

    public synchronized void checkForPluginsUpdates(boolean notificationsEnabled) throws Exception {
        // Get the XML request to include in the HTTP request
235
        String requestXML = getAvailablePluginsUpdateRequest();
236 237
        // Send the request to the server
        HttpClient httpClient = new HttpClient();
238 239 240 241 242 243
        // Check if a proxy should be used
        if (isUsingProxy()) {
            HostConfiguration hc = new HostConfiguration();
            hc.setProxy(getProxyHost(), getProxyPort());
            httpClient.setHostConfiguration(hc);
        }
244 245 246 247 248 249 250 251 252 253 254
        PostMethod postMethod = new PostMethod(updateServiceURL);
        NameValuePair[] data = {
                new NameValuePair("type", "available"),
                new NameValuePair("query", requestXML)
        };
        postMethod.setRequestBody(data);
        if (httpClient.executeMethod(postMethod) == 200) {
            // Process answer from the server
            String responseBody = postMethod.getResponseBodyAsString();
            processAvailablePluginsResponse(responseBody, notificationsEnabled);
        }
Gaston Dombiak's avatar
Gaston Dombiak committed
255 256 257
    }

    /**
258
     * Download and install latest version of plugin.
Gaston Dombiak's avatar
Gaston Dombiak committed
259
     *
260 261
     * @param url the URL of the latest version of the plugin.
     * @return true if the plugin was successfully downloaded and installed.
Gaston Dombiak's avatar
Gaston Dombiak committed
262
     */
263 264 265 266
    public boolean downloadPlugin(String url) {
        boolean installed = false;
        // Download and install new version of plugin
        HttpClient httpClient = new HttpClient();
267 268 269 270 271 272
        // Check if a proxy should be used
        if (isUsingProxy()) {
            HostConfiguration hc = new HostConfiguration();
            hc.setProxy(getProxyHost(), getProxyPort());
            httpClient.setHostConfiguration(hc);
        }
273 274
        GetMethod getMethod = new GetMethod(url);
        //execute the method
Gaston Dombiak's avatar
Gaston Dombiak committed
275
        try {
276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294
            int statusCode = httpClient.executeMethod(getMethod);
            if (statusCode == 200) {
                //get the resonse as an InputStream
                InputStream in = getMethod.getResponseBodyAsStream();
                String pluginFilename = url.substring(url.lastIndexOf("/") + 1);
                installed = XMPPServer.getInstance().getPluginManager()
                        .installPlugin(in, pluginFilename);
                in.close();
                if (installed) {
                    // Remove the plugin from the list of plugins to update
                    for (Update update : pluginUpdates) {
                        if (update.getURL().equals(url)) {
                            update.setDownloaded(true);
                        }
                    }
                    // Save response in a file for later retrieval
                    saveLatestServerInfo();
                }
            }
Gaston Dombiak's avatar
Gaston Dombiak committed
295
        }
296 297
        catch (IOException e) {
            Log.warn("Error downloading new plugin version", e);
Gaston Dombiak's avatar
Gaston Dombiak committed
298
        }
299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315
        return installed;
    }

    /**
     * Returns true if the plugin downloaded from the specified URL has been downloaded. Plugins
     * may be downloaded but not installed. The install process may take like 30 seconds to
     * detect new plugins to install.
     *
     * @param url the URL of the latest version of the plugin.
     * @return true if the plugin downloaded from the specified URL has been downloaded.
     */
    public boolean isPluginDownloaded(String url) {
        String pluginFilename = url.substring(url.lastIndexOf("/") + 1);
        return XMPPServer.getInstance().getPluginManager().isPluginDownloaded(pluginFilename);
    }

    /**
316
     * Returns the list of available plugins to install as reported by igniterealtime.org.
317 318
     * Currently installed plugins will not be included or plugins that require a newer
     * server version.
319
     *
320
     * @return the list of available plugins to install as reported by igniterealtime.org.
321 322
     */
    public List<AvailablePlugin> getNotInstalledPlugins() {
323
        List<AvailablePlugin> plugins = new ArrayList<AvailablePlugin>(availablePlugins.values());
324 325 326 327
        XMPPServer server = XMPPServer.getInstance();
        // Remove installed plugins from the list of available plugins
        for (Plugin plugin : server.getPluginManager().getPlugins()) {
            String pluginName = server.getPluginManager().getName(plugin);
328
            for (Iterator<AvailablePlugin> it = plugins.iterator(); it.hasNext();) {
329 330 331 332 333 334 335
                AvailablePlugin availablePlugin = it.next();
                if (availablePlugin.getName().equals(pluginName)) {
                    it.remove();
                    break;
                }
            }
        }
336 337 338 339 340 341 342 343
        // Remove plugins that require a newer server version
        String serverVersion = XMPPServer.getInstance().getServerInfo().getVersion().getVersionString();
        for (Iterator<AvailablePlugin> it=plugins.iterator(); it.hasNext();) {
            AvailablePlugin plugin = it.next();
            if (serverVersion.compareTo(plugin.getMinServerVersion()) < 0) {
                it.remove();
            }
        }
344
        return plugins;
Gaston Dombiak's avatar
Gaston Dombiak committed
345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372
    }

    /**
     * Returns the message to send to admins when new updates are available. When sending
     * this message information about the new updates avaiable will be appended.
     *
     * @return the message to send to admins when new updates are available.
     */
    public String getNotificationMessage() {
        return LocaleUtils.getLocalizedString("update.notification-message");
    }

    /**
     * Returns true if the check for updates service is enabled.
     *
     * @return true if the check for updates service is enabled.
     */
    public boolean isServiceEnabled() {
        return JiveGlobals.getBooleanProperty("update.service-enabled", true);
    }

    /**
     * Sets if the check for updates service is enabled.
     *
     * @param enabled true if the check for updates service is enabled.
     */
    public void setServiceEnabled(boolean enabled) {
        JiveGlobals.setProperty("update.service-enabled", enabled ? "true" : "false");
373 374 375
        if (enabled && thread == null) {
            startService();
        }
Gaston Dombiak's avatar
Gaston Dombiak committed
376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397
    }

    /**
     * Returns true if admins should be notified by IM when new updates are available.
     *
     * @return true if admins should be notified by IM when new updates are available.
     */
    public boolean isNotificationEnabled() {
        return JiveGlobals.getBooleanProperty("update.notify-admins", true);
    }

    /**
     * Sets if admins should be notified by IM when new updates are available.
     *
     * @param enabled true if admins should be notified by IM when new updates are available.
     */
    public void setNotificationEnabled(boolean enabled) {
        JiveGlobals.setProperty("update.notify-admins", enabled ? "true" : "false");
    }

    /**
     * Returns the frequency to check for updates. By default, this will happen every 48 hours.
398
     * The frequency returned will never be less than 12 hours.
Gaston Dombiak's avatar
Gaston Dombiak committed
399 400 401 402
     *
     * @return the frequency to check for updates in hours.
     */
    public int getCheckFrequency() {
403 404 405 406 407 408 409
        int frequency = JiveGlobals.getIntProperty("update.frequency", 48);
        if (frequency < 12) {
            return 12;
        }
        else {
            return frequency;
        }
Gaston Dombiak's avatar
Gaston Dombiak committed
410 411 412 413 414 415 416 417 418 419 420
    }

    /**
     * Sets the frequency to check for updates. By default, this will happen every 48 hours.
     *
     * @param checkFrequency the frequency to check for updates.
     */
    public void setCheckFrequency(int checkFrequency) {
        JiveGlobals.setProperty("update.frequency", Integer.toString(checkFrequency));
    }

421
    /**
422
     * Returns true if a proxy is being used to connect to igniterealtime.org or false if
423 424
     * a direct connection should be attempted.
     *
425
     * @return true if a proxy is being used to connect to igniterealtime.org.
426 427 428 429 430 431
     */
    public boolean isUsingProxy() {
        return getProxyHost() != null;
    }

    /**
432
     * Returns the host of the proxy to use to connect to igniterealtime.org or <tt>null</tt>
433 434 435 436 437 438 439 440 441
     * if no proxy is used.
     *
     * @return the host of the proxy or null if no proxy is used.
     */
    public String getProxyHost() {
        return JiveGlobals.getProperty("update.proxy.host");
    }

    /**
442
     * Sets the host of the proxy to use to connect to igniterealtime.org or <tt>null</tt>
443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458
     * if no proxy is used.
     *
     * @param host the host of the proxy or null if no proxy is used.
     */
    public void setProxyHost(String host) {
        if (host == null) {
            // Remove the property
            JiveGlobals.deleteProperty("update.proxy.host");
        }
        else {
            // Create or update the property
            JiveGlobals.setProperty("update.proxy.host", host);
        }
    }

    /**
459
     * Returns the port of the proxy to use to connect to igniterealtime.org or -1 if no
460 461
     * proxy is being used.
     *
462
     * @return the port of the proxy to use to connect to igniterealtime.org or -1 if no
463 464 465 466 467 468 469
     *         proxy is being used.
     */
    public int getProxyPort() {
        return JiveGlobals.getIntProperty("update.proxy.port", -1);
    }

    /**
470
     * Sets the port of the proxy to use to connect to igniterealtime.org or -1 if no
471 472
     * proxy is being used.
     *
473
     * @param port the port of the proxy to use to connect to igniterealtime.org or -1 if no
474 475 476 477 478 479
     *        proxy is being used.
     */
    public void setProxyPort(int port) {
        JiveGlobals.setProperty("update.proxy.port", Integer.toString(port));
    }

Gaston Dombiak's avatar
Gaston Dombiak committed
480 481 482 483 484 485
    /**
     * Returns the server update or <tt>null</tt> if the server is up to date.
     *
     * @return the server update or null if the server is up to date.
     */
    public Update getServerUpdate() {
486
        return serverUpdate;
Gaston Dombiak's avatar
Gaston Dombiak committed
487 488 489 490 491
    }

    /**
     * Returns the plugin update or <tt>null</tt> if the plugin is up to date.
     *
492
     * @param pluginName     the name of the plugin (as described in the meta-data).
Gaston Dombiak's avatar
Gaston Dombiak committed
493 494 495 496
     * @param currentVersion current version of the plugin that is installed.
     * @return the plugin update or null if the plugin is up to date.
     */
    public Update getPluginUpdate(String pluginName, String currentVersion) {
497
        for (Update update : pluginUpdates) {
Gaston Dombiak's avatar
Gaston Dombiak committed
498 499 500 501 502 503 504 505 506 507 508
            // Check if this is the requested plugin
            if (update.getComponentName().equals(pluginName)) {
                // Check if the plugin version is right
                if (update.getLatestVersion().compareTo(currentVersion) > 0) {
                    return update;
                }
            }
        }
        return null;
    }

509
    private String getServerUpdateRequest() {
Gaston Dombiak's avatar
Gaston Dombiak committed
510 511
        XMPPServer server = XMPPServer.getInstance();
        Element xmlRequest = docFactory.createDocument().addElement("version");
512
        // Add current openfire version
Gaston Dombiak's avatar
Gaston Dombiak committed
513
        Element openfire = xmlRequest.addElement("openfire");
514
        openfire.addAttribute("current", server.getServerInfo().getVersion().getVersionString());
Gaston Dombiak's avatar
Gaston Dombiak committed
515 516 517
        return xmlRequest.asXML();
    }

518 519 520 521 522 523 524 525
    private String getAvailablePluginsUpdateRequest() {
        Element xmlRequest = docFactory.createDocument().addElement("available");
        // Add locale so we can get current name and description of plugins
        Element locale = xmlRequest.addElement("locale");
        locale.addText(JiveGlobals.getLocale().toString());
        return xmlRequest.asXML();
    }

526
    private void processServerUpdateResponse(String response, boolean notificationsEnabled)
Gaston Dombiak's avatar
Gaston Dombiak committed
527
            throws DocumentException {
528 529
        // Reset last known update information
        serverUpdate = null;
530 531 532
        SAXReader xmlReader = new SAXReader();
        xmlReader.setEncoding("UTF-8");
        Element xmlResponse = xmlReader.read(new StringReader(response)).getRootElement();
Gaston Dombiak's avatar
Gaston Dombiak committed
533
        // Parse response and keep info as Update objects
Gaston Dombiak's avatar
Gaston Dombiak committed
534
        Element openfire = xmlResponse.element("openfire");
535 536 537 538 539
        if (openfire != null) {
            // A new version of openfire was found
            String latestVersion = openfire.attributeValue("latest");
            String changelog = openfire.attributeValue("changelog");
            String url = openfire.attributeValue("url");
540
            // Keep information about the available server update
541
            serverUpdate = new Update("Openfire", latestVersion, changelog, url);
Gaston Dombiak's avatar
Gaston Dombiak committed
542
        }
543 544 545 546 547 548 549 550 551 552 553
        // Check if we need to send notifications to admins
        if (notificationsEnabled && isNotificationEnabled() && serverUpdate != null) {
            Collection<JID> admins = XMPPServer.getInstance().getAdmins();
            Message notification = new Message();
            notification.setFrom(serverName);
            notification.setBody(getNotificationMessage() + " " + serverUpdate.getComponentName() +
                    " " + serverUpdate.getLatestVersion());
            for (JID jid : admins) {
                notification.setTo(jid);
                router.route(notification);
            }
Gaston Dombiak's avatar
Gaston Dombiak committed
554
        }
555 556
        // Save response in a file for later retrieval
        saveLatestServerInfo();
Gaston Dombiak's avatar
Gaston Dombiak committed
557 558
    }

559 560 561 562 563
    private void processAvailablePluginsResponse(String response, boolean notificationsEnabled)
            throws DocumentException {
        // Reset last known list of available plugins
        availablePlugins = new HashMap<String, AvailablePlugin>();

Gaston Dombiak's avatar
Gaston Dombiak committed
564
        // Parse response and keep info as AvailablePlugin objects
565 566 567
        SAXReader xmlReader = new SAXReader();
        xmlReader.setEncoding("UTF-8");
        Element xmlResponse = xmlReader.read(new StringReader(response)).getRootElement();
Gaston Dombiak's avatar
Gaston Dombiak committed
568 569
        Iterator plugins = xmlResponse.elementIterator("plugin");
        while (plugins.hasNext()) {
570
            Element plugin = (Element) plugins.next();
Gaston Dombiak's avatar
Gaston Dombiak committed
571 572 573 574 575 576
            String pluginName = plugin.attributeValue("name");
            String latestVersion = plugin.attributeValue("latest");
            String icon = plugin.attributeValue("icon");
            String readme = plugin.attributeValue("readme");
            String changelog = plugin.attributeValue("changelog");
            String url = plugin.attributeValue("url");
577
            String licenseType = plugin.attributeValue("licenseType");
Gaston Dombiak's avatar
Gaston Dombiak committed
578 579 580
            String description = plugin.attributeValue("description");
            String author = plugin.attributeValue("author");
            String minServerVersion = plugin.attributeValue("minServerVersion");
581
            String fileSize = plugin.attributeValue("fileSize");
Gaston Dombiak's avatar
Gaston Dombiak committed
582
            AvailablePlugin available = new AvailablePlugin(pluginName, description, latestVersion,
583
                    author, icon, changelog, readme, licenseType, minServerVersion, url, fileSize);
584 585
            // Add plugin to the list of available plugins at js.org
            availablePlugins.put(pluginName, available);
Gaston Dombiak's avatar
Gaston Dombiak committed
586
        }
587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602

        // Figure out local plugins that need to be updated
        buildPluginsUpdateList();

        // Check if we need to send notifications to admins
        if (notificationsEnabled && isNotificationEnabled() && !pluginUpdates.isEmpty()) {
            Collection<JID> admins = XMPPServer.getInstance().getAdmins();
            for (Update update : pluginUpdates) {
                Message notification = new Message();
                notification.setFrom(serverName);
                notification.setBody(getNotificationMessage() + " " + update.getComponentName() +
                        " " + update.getLatestVersion());
                for (JID jid : admins) {
                    notification.setTo(jid);
                    router.route(notification);
                }
Gaston Dombiak's avatar
Gaston Dombiak committed
603
            }
604 605 606 607
        }

        // Save information of available plugins
        saveAvailablePluginsInfo();
Gaston Dombiak's avatar
Gaston Dombiak committed
608 609 610
    }

    /**
611
     * Recreate the list of plugins that need to be updated based on the list of
612
     * available plugins at igniterealtime.org.
Gaston Dombiak's avatar
Gaston Dombiak committed
613
     */
614 615 616 617 618 619 620 621 622 623 624
    private void buildPluginsUpdateList() {
        // Reset list of plugins that need to be updated
        pluginUpdates = new ArrayList<Update>();
        XMPPServer server = XMPPServer.getInstance();
        // Compare local plugins versions with latest ones
        for (Plugin plugin : server.getPluginManager().getPlugins()) {
            String pluginName = server.getPluginManager().getName(plugin);
            AvailablePlugin latestPlugin = availablePlugins.get(pluginName);
            String currentVersion = server.getPluginManager().getVersion(plugin);
            if (latestPlugin != null &&
                    latestPlugin.getLatestVersion().compareTo(currentVersion) > 0) {
625 626 627 628 629 630 631 632
                // Check if the update can run in the current version of the server
                String serverVersion =
                        XMPPServer.getInstance().getServerInfo().getVersion().getVersionString();
                if (serverVersion.compareTo(latestPlugin.getMinServerVersion()) >= 0) {
                    Update update = new Update(pluginName, latestPlugin.getLatestVersion(),
                            latestPlugin.getChangelog(), latestPlugin.getURL());
                    pluginUpdates.add(update);
                }
633 634 635 636 637
            }
        }
    }

    /**
638
     * Saves to conf/server-update.xml information about the latest Openfire release that is
639 640 641
     * available for download.
     */
    private void saveLatestServerInfo() {
Gaston Dombiak's avatar
Gaston Dombiak committed
642
        Element xmlResponse = docFactory.createDocument().addElement("version");
643
        if (serverUpdate != null) {
Gaston Dombiak's avatar
Gaston Dombiak committed
644
            Element component = xmlResponse.addElement("openfire");
645 646 647 648 649 650 651 652 653 654 655
            component.addAttribute("latest", serverUpdate.getLatestVersion());
            component.addAttribute("changelog", serverUpdate.getChangelog());
            component.addAttribute("url", serverUpdate.getURL());
        }
        // Write data out to conf/server-update.xml file.
        Writer writer = null;
        try {
            // Create the conf folder if required
            File file = new File(JiveGlobals.getHomeDirectory(), "conf");
            if (!file.exists()) {
                file.mkdir();
Gaston Dombiak's avatar
Gaston Dombiak committed
656
            }
657 658 659 660 661
            file = new File(JiveGlobals.getHomeDirectory() + File.separator + "conf",
                    "server-update.xml");
            // Delete the old server-update.xml file if it exists
            if (file.exists()) {
                file.delete();
Gaston Dombiak's avatar
Gaston Dombiak committed
662
            }
663
            // Create new version.xml with returned data
664
            writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), "UTF-8"));
665 666 667 668 669
            OutputFormat prettyPrinter = OutputFormat.createPrettyPrint();
            XMLWriter xmlWriter = new XMLWriter(writer, prettyPrinter);
            xmlWriter.write(xmlResponse);
        }
        catch (Exception e) {
670
            Log.error(e.getMessage(), e);
671 672 673 674 675 676 677
        }
        finally {
            if (writer != null) {
                try {
                    writer.close();
                }
                catch (IOException e1) {
678
                    Log.error(e1.getMessage(), e1);
679
                }
Gaston Dombiak's avatar
Gaston Dombiak committed
680 681
            }
        }
682 683 684 685
    }

    /**
     * Saves to conf/available-plugins.xml the list of plugins that are available
686
     * at igniterealtime.org.
687 688 689 690 691 692 693 694 695 696 697 698 699 700 701
     */
    private void saveAvailablePluginsInfo() {
        //  XML to store in the file
        Element xml = docFactory.createDocument().addElement("available");
        for (AvailablePlugin plugin : availablePlugins.values()) {
            Element component = xml.addElement("plugin");
            component.addAttribute("name", plugin.getName());
            component.addAttribute("latest", plugin.getLatestVersion());
            component.addAttribute("changelog", plugin.getChangelog());
            component.addAttribute("url", plugin.getURL());
            component.addAttribute("author", plugin.getAuthor());
            component.addAttribute("description", plugin.getDescription());
            component.addAttribute("icon", plugin.getIcon());
            component.addAttribute("minServerVersion", plugin.getMinServerVersion());
            component.addAttribute("readme", plugin.getReadme());
702 703
            component.addAttribute("licenseType", plugin.getLicenseType());
            component.addAttribute("fileSize", Long.toString(plugin.getFileSize()));
704 705
        }
        // Write data out to conf/available-plugins.xml file.
Gaston Dombiak's avatar
Gaston Dombiak committed
706 707 708 709 710 711 712 713
        Writer writer = null;
        try {
            // Create the conf folder if required
            File file = new File(JiveGlobals.getHomeDirectory(), "conf");
            if (!file.exists()) {
                file.mkdir();
            }
            file = new File(JiveGlobals.getHomeDirectory() + File.separator + "conf",
714
                    "available-plugins.xml");
Gaston Dombiak's avatar
Gaston Dombiak committed
715 716 717 718 719
            // Delete the old version.xml file if it exists
            if (file.exists()) {
                file.delete();
            }
            // Create new version.xml with returned data
720
            writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), "UTF-8"));
Gaston Dombiak's avatar
Gaston Dombiak committed
721 722
            OutputFormat prettyPrinter = OutputFormat.createPrettyPrint();
            XMLWriter xmlWriter = new XMLWriter(writer, prettyPrinter);
723
            xmlWriter.write(xml);
Gaston Dombiak's avatar
Gaston Dombiak committed
724 725
        }
        catch (Exception e) {
726
            Log.error(e.getMessage(), e);
Gaston Dombiak's avatar
Gaston Dombiak committed
727 728 729 730 731 732 733
        }
        finally {
            if (writer != null) {
                try {
                    writer.close();
                }
                catch (IOException e1) {
734
                    Log.error(e1.getMessage(), e1);
Gaston Dombiak's avatar
Gaston Dombiak committed
735 736 737 738 739 740
                }
            }
        }
    }

    /**
741 742
     * Loads list of available plugins and latest available server version from
     * conf/available-plugins.xml and conf/server-update.xml respectively.
Gaston Dombiak's avatar
Gaston Dombiak committed
743
     */
744 745 746 747 748 749 750 751 752 753
    private void loadSavedInfo() {
        // Load server update information
        loadLatestServerInfo();
        // Load available plugins information
        loadAvailablePluginsInfo();
        // Recreate list of plugins to update
        buildPluginsUpdateList();
    }

    private void loadLatestServerInfo() {
Gaston Dombiak's avatar
Gaston Dombiak committed
754
        Document xmlResponse;
755 756
        File file = new File(JiveGlobals.getHomeDirectory() + File.separator + "conf",
                "server-update.xml");
Gaston Dombiak's avatar
Gaston Dombiak committed
757
        if (!file.exists()) {
758
            return;
Gaston Dombiak's avatar
Gaston Dombiak committed
759 760 761
        }
        // Check read privs.
        if (!file.canRead()) {
762 763
            Log.warn("Cannot retrieve server updates. File must be readable: " + file.getName());
            return;
Gaston Dombiak's avatar
Gaston Dombiak committed
764 765 766 767 768
        }
        FileReader reader = null;
        try {
            reader = new FileReader(file);
            SAXReader xmlReader = new SAXReader();
769
            xmlReader.setEncoding("UTF-8");
Gaston Dombiak's avatar
Gaston Dombiak committed
770 771 772
            xmlResponse = xmlReader.read(reader);
        }
        catch (Exception e) {
773 774
            Log.error("Error reading server-update.xml", e);
            return;
Gaston Dombiak's avatar
Gaston Dombiak committed
775 776 777 778 779 780 781 782 783 784 785
        }
        finally {
            if (reader != null) {
                try {
                    reader.close();
                }
                catch (Exception e) {
                    // Do nothing
                }
            }
        }
786
        // Parse info and recreate update information (if still required)
Gaston Dombiak's avatar
Gaston Dombiak committed
787
        Element openfire = xmlResponse.getRootElement().element("openfire");
788 789 790 791
        if (openfire != null) {
            String latestVersion = openfire.attributeValue("latest");
            String changelog = openfire.attributeValue("changelog");
            String url = openfire.attributeValue("url");
792 793 794 795
            // Check if current server version is correct
            String serverVersion =
                    XMPPServer.getInstance().getServerInfo().getVersion().getVersionString();
            if (serverVersion.compareTo(latestVersion) < 0) {
796
                serverUpdate = new Update("Openfire", latestVersion, changelog, url);
797 798
            }
        }
Gaston Dombiak's avatar
Gaston Dombiak committed
799 800
    }

801 802 803 804 805 806 807 808 809 810 811 812 813
    private void loadAvailablePluginsInfo() {
        Document xmlResponse;
        File file = new File(JiveGlobals.getHomeDirectory() + File.separator + "conf",
                "available-plugins.xml");
        if (!file.exists()) {
            return;
        }
        // Check read privs.
        if (!file.canRead()) {
            Log.warn("Cannot retrieve available plugins. File must be readable: " + file.getName());
            return;
        }
        FileReader reader = null;
Gaston Dombiak's avatar
Gaston Dombiak committed
814
        try {
815 816
            reader = new FileReader(file);
            SAXReader xmlReader = new SAXReader();
817
            xmlReader.setEncoding("UTF-8");
818 819 820 821 822 823 824 825 826 827 828 829 830
            xmlResponse = xmlReader.read(reader);
        }
        catch (Exception e) {
            Log.error("Error reading available-plugins.xml", e);
            return;
        }
        finally {
            if (reader != null) {
                try {
                    reader.close();
                }
                catch (Exception e) {
                    // Do nothing
Gaston Dombiak's avatar
Gaston Dombiak committed
831 832 833
                }
            }
        }
834 835 836
        // Parse info and recreate available plugins
        Iterator it = xmlResponse.getRootElement().elementIterator("plugin");
        while (it.hasNext()) {
837
            Element plugin = (Element) it.next();
838 839 840 841 842 843
            String pluginName = plugin.attributeValue("name");
            String latestVersion = plugin.attributeValue("latest");
            String icon = plugin.attributeValue("icon");
            String readme = plugin.attributeValue("readme");
            String changelog = plugin.attributeValue("changelog");
            String url = plugin.attributeValue("url");
844
            String licenseType = plugin.attributeValue("licenseType");
845 846 847
            String description = plugin.attributeValue("description");
            String author = plugin.attributeValue("author");
            String minServerVersion = plugin.attributeValue("minServerVersion");
848
            String fileSize = plugin.attributeValue("fileSize");
849
            AvailablePlugin available = new AvailablePlugin(pluginName, description, latestVersion,
850
                    author, icon, changelog, readme, licenseType, minServerVersion, url, fileSize);
851 852
            // Add plugin to the list of available plugins at js.org
            availablePlugins.put(pluginName, available);
Gaston Dombiak's avatar
Gaston Dombiak committed
853 854
        }
    }
855 856 857 858 859 860 861 862 863

    /**
     * Returns a previously fetched list of updates.
     *
     * @return a previously fetched list of updates.
     */
    public Collection<Update> getPluginUpdates() {
        return pluginUpdates;
    }
Gaston Dombiak's avatar
Gaston Dombiak committed
864
}