com.aelitis.azureus.core.subs.impl.SubscriptionManagerImpl.java Source code

Java tutorial

Introduction

Here is the source code for com.aelitis.azureus.core.subs.impl.SubscriptionManagerImpl.java

Source

/*
 * Created on Jul 11, 2008
 * Created by Paul Gardner
 * 
 * Copyright 2008 Vuze, Inc.  All rights reserved.
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; version 2 of the License only.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
 */

package com.aelitis.azureus.core.subs.impl;

import java.io.*;
import java.net.InetSocketAddress;
import java.net.URL;
import java.security.KeyPair;
import java.util.*;
import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

import org.bouncycastle.util.encoders.Base64;
import org.gudy.azureus2.core3.config.COConfigurationManager;
import org.gudy.azureus2.core3.config.ParameterListener;
import org.gudy.azureus2.core3.internat.MessageText;
import org.gudy.azureus2.core3.torrent.TOTorrent;
import org.gudy.azureus2.core3.torrent.TOTorrentFactory;
import org.gudy.azureus2.core3.util.*;
import org.gudy.azureus2.plugins.PluginInterface;
import org.gudy.azureus2.plugins.download.*;
import org.gudy.azureus2.plugins.peers.PeerManager;
import org.gudy.azureus2.plugins.torrent.Torrent;
import org.gudy.azureus2.plugins.torrent.TorrentAttribute;
import org.gudy.azureus2.plugins.torrent.TorrentManager;
import org.gudy.azureus2.plugins.ui.UIManager;
import org.gudy.azureus2.plugins.ui.UIManagerEvent;
import org.gudy.azureus2.plugins.utils.DelayedTask;
import org.gudy.azureus2.plugins.utils.StaticUtilities;
import org.gudy.azureus2.plugins.utils.search.SearchException;
import org.gudy.azureus2.plugins.utils.search.SearchInstance;
import org.gudy.azureus2.plugins.utils.search.SearchObserver;
import org.gudy.azureus2.plugins.utils.search.SearchProvider;
import org.gudy.azureus2.plugins.utils.search.SearchResult;
import org.gudy.azureus2.pluginsimpl.local.PluginInitializer;
import org.gudy.azureus2.pluginsimpl.local.torrent.TorrentImpl;
import org.gudy.azureus2.pluginsimpl.local.utils.UtilitiesImpl;

import com.aelitis.azureus.core.AzureusCore;
import com.aelitis.azureus.core.AzureusCoreRunningListener;
import com.aelitis.azureus.core.AzureusCoreFactory;
import com.aelitis.azureus.core.custom.Customization;
import com.aelitis.azureus.core.custom.CustomizationManager;
import com.aelitis.azureus.core.custom.CustomizationManagerFactory;
import com.aelitis.azureus.core.dht.DHT;
import com.aelitis.azureus.core.lws.LightWeightSeed;
import com.aelitis.azureus.core.lws.LightWeightSeedManager;
import com.aelitis.azureus.core.messenger.config.PlatformSubscriptionsMessenger;
import com.aelitis.azureus.core.metasearch.Engine;
import com.aelitis.azureus.core.metasearch.MetaSearchListener;
import com.aelitis.azureus.core.metasearch.MetaSearchManagerFactory;
import com.aelitis.azureus.core.metasearch.impl.web.WebEngine;
import com.aelitis.azureus.core.metasearch.impl.web.rss.RSSEngine;
import com.aelitis.azureus.core.security.CryptoECCUtils;
import com.aelitis.azureus.core.subs.*;
import com.aelitis.azureus.core.subs.SubscriptionUtils.SubscriptionDownloadDetails;
import com.aelitis.azureus.core.torrent.PlatformTorrentUtils;
import com.aelitis.azureus.core.util.CopyOnWriteList;
import com.aelitis.azureus.core.vuzefile.*;
import com.aelitis.azureus.plugins.dht.*;
import com.aelitis.azureus.plugins.magnet.MagnetPlugin;
import com.aelitis.azureus.plugins.magnet.MagnetPluginProgressListener;
import com.aelitis.azureus.util.ImportExportUtils;
import com.aelitis.azureus.util.UrlFilter;
import com.aelitis.net.magneturi.MagnetURIHandler;

public class SubscriptionManagerImpl implements SubscriptionManager, AEDiagnosticsEvidenceGenerator {
    private static final String CONFIG_FILE = "subscriptions.config";
    private static final String LOGGER_NAME = "Subscriptions";

    private static final String CONFIG_MAX_RESULTS = "subscriptions.max.non.deleted.results";
    private static final String CONFIG_AUTO_START_DLS = "subscriptions.auto.start.downloads";
    private static final String CONFIG_AUTO_START_MIN_MB = "subscriptions.auto.start.min.mb";
    private static final String CONFIG_AUTO_START_MAX_MB = "subscriptions.auto.start.max.mb";

    private static final String CONFIG_RSS_ENABLE = "subscriptions.config.rss_enable";

    private static final String CONFIG_ENABLE_SEARCH = "subscriptions.config.search_enable";

    private static final String CONFIG_HIDE_SEARCH_TEMPLATES = "subscriptions.config.hide_search_templates";

    private static final String CONFIG_DL_SUBS_ENABLE = "subscriptions.config.dl_subs_enable";

    private static final int DELETE_UNUSED_AFTER_MILLIS = 2 * 7 * 24 * 60 * 60 * 1000;

    private static SubscriptionManagerImpl singleton;
    private static boolean pre_initialised;

    private static final int random_seed = RandomUtils.nextInt(256);

    public static void preInitialise() {
        synchronized (SubscriptionManagerImpl.class) {

            if (pre_initialised) {

                return;
            }

            pre_initialised = true;
        }

        VuzeFileHandler.getSingleton().addProcessor(new VuzeFileProcessor() {
            public void process(VuzeFile[] files, int expected_types) {
                for (int i = 0; i < files.length; i++) {

                    VuzeFile vf = files[i];

                    VuzeFileComponent[] comps = vf.getComponents();

                    for (int j = 0; j < comps.length; j++) {

                        VuzeFileComponent comp = comps[j];

                        int type = comp.getType();

                        if (type == VuzeFileComponent.COMP_TYPE_SUBSCRIPTION
                                || type == VuzeFileComponent.COMP_TYPE_SUBSCRIPTION_SINGLETON) {

                            try {
                                ((SubscriptionManagerImpl) getSingleton(false)).importSubscription(type,
                                        comp.getContent(),
                                        (expected_types & (VuzeFileComponent.COMP_TYPE_SUBSCRIPTION
                                                | VuzeFileComponent.COMP_TYPE_SUBSCRIPTION_SINGLETON)) == 0);

                                comp.setProcessed();

                            } catch (Throwable e) {

                                Debug.printStackTrace(e);
                            }
                        }
                    }
                }
            }
        });
    }

    public static SubscriptionManager getSingleton(boolean stand_alone) {
        preInitialise();

        synchronized (SubscriptionManagerImpl.class) {

            if (singleton != null) {

                return (singleton);
            }

            singleton = new SubscriptionManagerImpl(stand_alone);
        }

        // saw deadlock here when adding core listener while synced on class - rework
        // to avoid 

        if (!stand_alone) {

            singleton.initialise();
        }

        return (singleton);
    }

    private boolean started;

    private static final int TIMER_PERIOD = 30 * 1000;

    private static final int ASSOC_CHECK_PERIOD = 5 * 60 * 1000;
    private static final int ASSOC_CHECK_TICKS = ASSOC_CHECK_PERIOD / TIMER_PERIOD;

    private static final int SERVER_PUB_CHECK_PERIOD = 10 * 60 * 1000;
    private static final int SERVER_PUB_CHECK_TICKS = SERVER_PUB_CHECK_PERIOD / TIMER_PERIOD;

    private static final int TIDY_POT_ASSOC_PERIOD = 30 * 60 * 1000;
    private static final int TIDY_POT_ASSOC_TICKS = TIDY_POT_ASSOC_PERIOD / TIMER_PERIOD;

    private static final int SET_SELECTED_PERIOD = 23 * 60 * 60 * 1000;
    private static final int SET_SELECTED_FIRST_TICK = 3 * 60 * 1000 / TIMER_PERIOD;
    private static final int SET_SELECTED_TICKS = SET_SELECTED_PERIOD / TIMER_PERIOD;

    private static final Object SP_LAST_ATTEMPTED = new Object();
    private static final Object SP_CONSEC_FAIL = new Object();

    private AzureusCore azureus_core;

    private volatile DHTPlugin dht_plugin;

    private List<SubscriptionImpl> subscriptions = new ArrayList<SubscriptionImpl>();

    private boolean config_dirty;

    private static final int PUB_ASSOC_CONC_MAX = 3;

    private int publish_associations_active;

    private boolean publish_subscription_active;

    private TorrentAttribute ta_subs_download;
    private TorrentAttribute ta_subs_download_rd;
    private TorrentAttribute ta_subscription_info;
    private TorrentAttribute ta_category;

    private boolean periodic_lookup_in_progress;
    private int priority_lookup_pending;

    private CopyOnWriteList<SubscriptionManagerListener> listeners = new CopyOnWriteList<SubscriptionManagerListener>();

    private SubscriptionSchedulerImpl scheduler;

    private List<Object[]> potential_associations = new ArrayList<Object[]>();
    private Map<HashWrapper, Object[]> potential_associations2 = new HashMap<HashWrapper, Object[]>();

    private boolean meta_search_listener_added;

    private Pattern exclusion_pattern = Pattern.compile("azdev[0-9]+\\.azureus\\.com");

    private SubscriptionRSSFeed rss_publisher;

    private AEDiagnosticsLogger logger;

    protected SubscriptionManagerImpl(boolean stand_alone) {
        if (!stand_alone) {

            loadConfig();

            AEDiagnostics.addEvidenceGenerator(this);

            CustomizationManager cust_man = CustomizationManagerFactory.getSingleton();

            Customization cust = cust_man.getActiveCustomization();

            if (cust != null) {

                String cust_name = COConfigurationManager.getStringParameter("subscriptions.custom.name", "");
                String cust_version = COConfigurationManager.getStringParameter("subscriptions.custom.version",
                        "0");

                boolean new_name = !cust_name.equals(cust.getName());
                boolean new_version = org.gudy.azureus2.core3.util.Constants.compareVersions(cust_version,
                        cust.getVersion()) < 0;

                if (new_name || new_version) {

                    log("Customization: checking templates for " + cust.getName() + "/" + cust.getVersion());

                    try {
                        InputStream[] streams = cust.getResources(Customization.RT_SUBSCRIPTIONS);

                        for (int i = 0; i < streams.length; i++) {

                            InputStream is = streams[i];

                            try {
                                VuzeFile vf = VuzeFileHandler.getSingleton().loadVuzeFile(is);

                                if (vf != null) {

                                    VuzeFileComponent[] comps = vf.getComponents();

                                    for (int j = 0; j < comps.length; j++) {

                                        VuzeFileComponent comp = comps[j];

                                        int type = comp.getType();

                                        if (type == VuzeFileComponent.COMP_TYPE_SUBSCRIPTION
                                                || type == VuzeFileComponent.COMP_TYPE_SUBSCRIPTION_SINGLETON) {

                                            try {
                                                importSubscription(type, comp.getContent(), false);

                                                comp.setProcessed();

                                            } catch (Throwable e) {

                                                Debug.printStackTrace(e);
                                            }
                                        }
                                    }
                                }
                            } finally {

                                try {
                                    is.close();

                                } catch (Throwable e) {
                                }
                            }
                        }
                    } finally {

                        COConfigurationManager.setParameter("subscriptions.custom.name", cust.getName());
                        COConfigurationManager.setParameter("subscriptions.custom.version", cust.getVersion());
                    }
                }
            }

            scheduler = new SubscriptionSchedulerImpl(this);
        }
    }

    protected void initialise() {
        AzureusCoreFactory.addCoreRunningListener(new AzureusCoreRunningListener() {
            public void azureusCoreRunning(final AzureusCore core) {
                initWithCore(core);
            }
        });
    }

    protected void initWithCore(AzureusCore _core) {
        synchronized (this) {

            if (started) {

                return;
            }

            started = true;
        }

        azureus_core = _core;

        final PluginInterface default_pi = PluginInitializer.getDefaultInterface();

        rss_publisher = new SubscriptionRSSFeed(this, default_pi);

        TorrentManager tm = default_pi.getTorrentManager();

        ta_subs_download = tm.getPluginAttribute("azsubs.subs_dl");
        ta_subs_download_rd = tm.getPluginAttribute("azsubs.subs_dl_rd");
        ta_subscription_info = tm.getPluginAttribute("azsubs.subs_info");
        ta_category = tm.getAttribute(TorrentAttribute.TA_CATEGORY);

        PluginInterface dht_plugin_pi = AzureusCoreFactory.getSingleton().getPluginManager()
                .getPluginInterfaceByClass(DHTPlugin.class);

        if (dht_plugin_pi != null) {

            dht_plugin = (DHTPlugin) dht_plugin_pi.getPlugin();

            /*
            if ( Constants.isCVSVersion()){
                   
               addListener(
              new SubscriptionManagerListener()
              {
                 public void 
                 subscriptionAdded(
                    Subscription subscription ) 
                 {
                 }
                   
                 public void
                 subscriptionChanged(
                    Subscription      subscription )
                 {
                 }
                     
                 public void 
                 subscriptionRemoved(
                    Subscription subscription ) 
                 {
                 }
                     
                 public void 
                 associationsChanged(
                    byte[] hash )
                 {
                    System.out.println( "Subscriptions changed: " + ByteFormatter.encodeString( hash ));
                        
                    Subscription[] subs = getKnownSubscriptions( hash );
                     
                    for (int i=0;i<subs.length;i++){
                           
                       System.out.println( "    " + subs[i].getString());
                    }
                 }
              });   
            }
            */

            default_pi.getDownloadManager().addListener(new DownloadManagerListener() {
                public void downloadAdded(Download download) {
                    Torrent torrent = download.getTorrent();

                    if (torrent != null) {

                        byte[] hash = torrent.getHash();

                        Object[] entry;

                        synchronized (potential_associations2) {

                            entry = (Object[]) potential_associations2.remove(new HashWrapper(hash));
                        }

                        if (entry != null) {

                            SubscriptionImpl[] subs = (SubscriptionImpl[]) entry[0];

                            String subs_str = "";
                            for (int i = 0; i < subs.length; i++) {
                                subs_str += (i == 0 ? "" : ",") + subs[i].getName();
                            }

                            log("Applying deferred asocciation for " + ByteFormatter.encodeString(hash) + " -> "
                                    + subs_str);

                            recordAssociationsSupport(hash, subs, ((Boolean) entry[1]).booleanValue());
                        }
                    }
                }

                public void downloadRemoved(Download download) {
                }
            }, false);

            TorrentUtils.addTorrentAttributeListener(new TorrentUtils.torrentAttributeListener() {
                public void attributeSet(TOTorrent torrent, String attribute, Object value) {
                    if (attribute == TorrentUtils.TORRENT_AZ_PROP_OBTAINED_FROM) {

                        try {
                            checkPotentialAssociations(torrent.getHash(), (String) value);

                        } catch (Throwable e) {

                            Debug.printStackTrace(e);
                        }
                    }
                }
            });

            DelayedTask delayed_task = UtilitiesImpl.addDelayedTask("Subscriptions", new Runnable() {
                public void run() {
                    new AEThread2("Subscriptions:delayInit", true) {
                        public void run() {
                            asyncInit();
                        }
                    }.start();

                }

                protected void asyncInit() {
                    Download[] downloads = default_pi.getDownloadManager().getDownloads();

                    for (int i = 0; i < downloads.length; i++) {

                        Download download = downloads[i];

                        if (download.getBooleanAttribute(ta_subs_download)) {

                            Map rd = download.getMapAttribute(ta_subs_download_rd);

                            boolean delete_it;

                            if (rd == null) {

                                delete_it = true;

                            } else {

                                delete_it = !recoverSubscriptionUpdate(download, rd);
                            }

                            if (delete_it) {

                                removeDownload(download, true);
                            }
                        }
                    }

                    default_pi.getDownloadManager().addListener(new DownloadManagerListener() {
                        public void downloadAdded(final Download download) {
                            // if ever changed to handle non-persistent then you need to fix init deadlock
                            // potential with share-hoster plugin

                            if (download.isPersistent()) {

                                if (!dht_plugin.isInitialising()) {

                                    // if new download then we want to check out its subscription status 

                                    lookupAssociations(download.getMapAttribute(ta_subscription_info) == null);

                                } else {

                                    new AEThread2("Subscriptions:delayInit", true) {
                                        public void run() {
                                            lookupAssociations(
                                                    download.getMapAttribute(ta_subscription_info) == null);
                                        }
                                    }.start();
                                }
                            }
                        }

                        public void downloadRemoved(Download download) {
                        }
                    }, false);

                    for (int i = 0; i < PUB_ASSOC_CONC_MAX; i++) {

                        if (publishAssociations()) {

                            break;
                        }
                    }

                    publishSubscriptions();

                    COConfigurationManager.addParameterListener(CONFIG_MAX_RESULTS, new ParameterListener() {
                        public void parameterChanged(String name) {
                            final int max_results = COConfigurationManager.getIntParameter(CONFIG_MAX_RESULTS);

                            new AEThread2("Subs:max results changer", true) {
                                public void run() {
                                    checkMaxResults(max_results);
                                }
                            }.start();
                        }
                    });

                    SimpleTimer.addPeriodicEvent("SubscriptionChecker", TIMER_PERIOD, new TimerEventPerformer() {
                        private int ticks;

                        public void perform(TimerEvent event) {
                            ticks++;

                            checkStuff(ticks);
                        }
                    });
                }
            });

            delayed_task.queue();
        }

        if (isSearchEnabled()) {

            try {
                default_pi.getUtilities().registerSearchProvider(new SearchProvider() {
                    private Map<Integer, Object> properties = new HashMap<Integer, Object>();

                    {
                        properties.put(PR_NAME, MessageText.getString("ConfigView.section.Subscriptions"));

                        try {
                            URL url = MagnetURIHandler.getSingleton()
                                    .registerResource(new MagnetURIHandler.ResourceProvider() {
                                        public String getUID() {
                                            return (SubscriptionManager.class.getName() + ".2");
                                        }

                                        public String getFileType() {
                                            return ("png");
                                        }

                                        public byte[] getData() {
                                            InputStream is = getClass().getClassLoader().getResourceAsStream(
                                                    "com/aelitis/azureus/ui/images/subscription_icon_1616.png");

                                            if (is == null) {

                                                return (null);
                                            }

                                            try {
                                                ByteArrayOutputStream baos = new ByteArrayOutputStream();

                                                try {
                                                    byte[] buffer = new byte[8192];

                                                    while (true) {

                                                        int len = is.read(buffer);

                                                        if (len <= 0) {

                                                            break;
                                                        }

                                                        baos.write(buffer, 0, len);
                                                    }
                                                } finally {

                                                    is.close();
                                                }

                                                return (baos.toByteArray());

                                            } catch (Throwable e) {

                                                return (null);
                                            }
                                        }
                                    });

                            properties.put(PR_ICON_URL, url.toExternalForm());

                        } catch (Throwable e) {

                            Debug.out(e);
                        }
                    }

                    public SearchInstance search(Map<String, Object> search_parameters, SearchObserver observer)

                            throws SearchException {
                        try {
                            return (searchSubscriptions(search_parameters, observer));

                        } catch (Throwable e) {

                            throw (new SearchException("Search failed", e));
                        }
                    }

                    public Object getProperty(int property) {
                        return (properties.get(property));
                    }

                    public void setProperty(int property, Object value) {
                        properties.put(property, value);
                    }
                });

            } catch (Throwable e) {

                Debug.out("Failed to register search provider");
            }
        }
    }

    public SearchInstance searchSubscriptions(Map<String, Object> search_parameters, final SearchObserver observer)

            throws SearchException {
        final String term = (String) search_parameters.get(SearchProvider.SP_SEARCH_TERM);

        final SearchInstance si = new SearchInstance() {
            public void cancel() {
                Debug.out("Cancelled");
            }
        };

        if (term == null) {

            try {
                observer.complete();

            } catch (Throwable e) {

                Debug.out(e);
            }
        } else {

            new AEThread2("Subscriptions:search", true) {
                public void run() {
                    final Set<String> hashes = new HashSet<String>();

                    searchMatcher matcher = new searchMatcher(term);

                    try {
                        List<SubscriptionResult> matches = matchSubscriptionResults(matcher);

                        for (final SubscriptionResult result : matches) {

                            final Map result_properties = result.toPropertyMap();

                            byte[] hash = (byte[]) result_properties.get(SearchResult.PR_HASH);

                            if (hash != null) {

                                String hash_str = Base32.encode(hash);

                                if (hashes.contains(hash_str)) {

                                    continue;
                                }

                                hashes.add(hash_str);
                            }

                            SearchResult search_result = new SearchResult() {
                                public Object getProperty(int property_name) {
                                    return (result_properties.get(property_name));
                                }
                            };

                            try {
                                observer.resultReceived(si, search_result);

                            } catch (Throwable e) {

                                Debug.out(e);
                            }
                        }

                        Map<String, Object[]> template_matches = new HashMap<String, Object[]>();

                        Engine[] engines = MetaSearchManagerFactory.getSingleton().getMetaSearch().getEngines(false,
                                false);

                        Map<Subscription, List<String>> sub_dl_name_map = null;

                        for (Subscription sub : getSubscriptions(false)) {

                            if (!sub.isSearchTemplate()) {

                                continue;
                            }

                            String sub_name = sub.getName();

                            Engine sub_engine = sub.getEngine();

                            if (sub_engine.isActive() || !(sub_engine instanceof RSSEngine)) {

                                continue;
                            }

                            int pos = sub_name.indexOf(":");

                            String t_name = sub_name.substring(pos + 1);

                            pos = t_name.indexOf("(v");

                            int t_ver;

                            if (pos == -1) {

                                t_ver = 1;

                            } else {

                                String s = t_name.substring(pos + 2, t_name.length() - 1);

                                t_name = t_name.substring(0, pos);

                                try {

                                    t_ver = Integer.parseInt(s);

                                } catch (Throwable e) {

                                    t_ver = 1;
                                }
                            }

                            t_name = t_name.trim();

                            boolean skip = false;

                            for (Engine e : engines) {

                                if (e != sub_engine && e.sameLogicAs(sub_engine)) {

                                    skip = true;

                                    break;
                                }

                                if (e.getName().equalsIgnoreCase(t_name)) {

                                    if (e.getVersion() >= t_ver) {

                                        skip = true;
                                    }
                                }
                            }

                            if (skip) {

                                continue;
                            }

                            if (sub_dl_name_map == null) {

                                sub_dl_name_map = new HashMap<Subscription, List<String>>();

                                SubscriptionDownloadDetails[] sdds = SubscriptionUtils
                                        .getAllCachedDownloadDetails(azureus_core);

                                for (SubscriptionDownloadDetails sdd : sdds) {

                                    String name = sdd.getDownload().getDisplayName();

                                    if (matcher.matches(name)) {

                                        Subscription[] x = sdd.getSubscriptions();

                                        for (Subscription s : x) {

                                            List<String> g = sub_dl_name_map.get(s);

                                            if (g == null) {

                                                g = new ArrayList<String>();

                                                sub_dl_name_map.put(s, g);
                                            }

                                            g.add(name);
                                        }
                                    }
                                }
                            }

                            List<String> names = sub_dl_name_map.get(sub);

                            if (names == null) {

                                continue;
                            }

                            String key = t_name.toLowerCase();

                            Object[] entry = template_matches.get(key);

                            if (entry == null) {

                                entry = new Object[] { sub, t_ver };

                                template_matches.put(key, entry);

                            } else {

                                if (t_ver > (Integer) entry[1]) {

                                    entry[0] = sub;
                                    entry[1] = t_ver;
                                }
                            }
                        }

                        List<Subscription> interesting = new ArrayList<Subscription>();

                        for (Object[] entry : template_matches.values()) {

                            interesting.add((Subscription) entry[0]);
                        }

                        Collections.sort(interesting, new Comparator<Subscription>() {
                            public int compare(Subscription o1, Subscription o2) {
                                long res = o2.getCachedPopularity() - o1.getCachedPopularity();

                                if (res < 0) {
                                    return (-1);
                                } else if (res > 0) {
                                    return (1);
                                } else {
                                    return (0);
                                }
                            }
                        });

                        int added = 0;

                        for (final Subscription sub : interesting) {

                            if (added >= 3) {

                                break;
                            }

                            try {
                                String subs_url_str = ((RSSEngine) sub.getEngine()).getSearchUrl(true);

                                URL subs_url = new URL(subs_url_str);

                                final byte[] vf_bytes = FileUtil
                                        .readInputStreamAsByteArray(subs_url.openConnection().getInputStream());

                                VuzeFile vf = VuzeFileHandler.getSingleton().loadVuzeFile(vf_bytes);

                                if (MetaSearchManagerFactory.getSingleton().isImportable(vf)) {

                                    final URL url = MagnetURIHandler.getSingleton()
                                            .registerResource(new MagnetURIHandler.ResourceProvider() {
                                                public String getUID() {
                                                    return (SubscriptionManager.class.getName() + ".sid."
                                                            + sub.getID());
                                                }

                                                public String getFileType() {
                                                    return ("vuze");
                                                }

                                                public byte[] getData() {
                                                    return (vf_bytes);
                                                }
                                            });

                                    SearchResult search_result = new SearchResult() {
                                        public Object getProperty(int property_name) {
                                            if (property_name == SearchResult.PR_NAME) {

                                                return (sub.getName());

                                            } else if (property_name == SearchResult.PR_DOWNLOAD_LINK
                                                    || property_name == SearchResult.PR_DOWNLOAD_BUTTON_LINK) {

                                                return (url.toExternalForm());

                                            } else if (property_name == SearchResult.PR_PUB_DATE) {

                                                return (new Date(sub.getAddTime()));

                                            } else if (property_name == SearchResult.PR_SIZE) {

                                                return (1024L);

                                            } else if (property_name == SearchResult.PR_SEED_COUNT
                                                    || property_name == SearchResult.PR_VOTES) {

                                                return ((long) sub.getCachedPopularity());

                                            } else if (property_name == SearchResult.PR_RANK) {

                                                return (100L);
                                            }

                                            return (null);
                                        }
                                    };

                                    added++;

                                    try {
                                        observer.resultReceived(si, search_result);

                                    } catch (Throwable e) {

                                        Debug.out(e);
                                    }
                                }
                            } catch (Throwable e) {

                                Debug.out(e);
                            }
                        }
                    } catch (Throwable e) {

                        Debug.out(e);

                    } finally {

                        observer.complete();
                    }
                }
            }.start();
        }

        return (si);
    }

    private List<SubscriptionResult> matchSubscriptionResults(searchMatcher matcher) {
        List<SubscriptionResult> result = new ArrayList<SubscriptionResult>();

        for (Subscription sub : getSubscriptions(true)) {

            SubscriptionResult[] results = sub.getResults(false);

            for (SubscriptionResult r : results) {

                Map properties = r.toPropertyMap();

                String name = (String) properties.get(SearchResult.PR_NAME);

                if (name == null) {

                    continue;
                }

                if (matcher.matches(name)) {

                    result.add(r);
                }
            }
        }

        return (result);
    }

    protected void checkMaxResults(int max) {
        Subscription[] subs = getSubscriptions();

        for (int i = 0; i < subs.length; i++) {

            ((SubscriptionHistoryImpl) subs[i].getHistory()).checkMaxResults(max);
        }
    }

    public SubscriptionScheduler getScheduler() {
        return (scheduler);
    }

    public boolean isRSSPublishEnabled() {
        return (COConfigurationManager.getBooleanParameter(CONFIG_RSS_ENABLE, false));
    }

    public void setRSSPublishEnabled(boolean enabled) {
        COConfigurationManager.setParameter(CONFIG_RSS_ENABLE, enabled);
    }

    public boolean isSearchEnabled() {
        return (COConfigurationManager.getBooleanParameter(CONFIG_ENABLE_SEARCH, true));
    }

    public void setSearchEnabled(boolean enabled) {
        COConfigurationManager.setParameter(CONFIG_ENABLE_SEARCH, enabled);
    }

    public boolean hideSearchTemplates() {
        return (COConfigurationManager.getBooleanParameter(CONFIG_HIDE_SEARCH_TEMPLATES, true));
    }

    public boolean isSubsDownloadEnabled() {
        return (COConfigurationManager.getBooleanParameter(CONFIG_DL_SUBS_ENABLE, true));
    }

    public void setSubsDownloadEnabled(boolean enabled) {
        COConfigurationManager.setParameter(CONFIG_DL_SUBS_ENABLE, enabled);
    }

    public String getRSSLink() {
        return (rss_publisher.getFeedURL());
    }

    public Subscription create(String name, boolean public_subs, String json)

            throws SubscriptionException {
        name = getUniqueName(name);

        SubscriptionImpl subs = new SubscriptionImpl(this, name, public_subs, null, json,
                SubscriptionImpl.ADD_TYPE_CREATE);

        log("Created new subscription: " + subs.getString());

        if (subs.isPublic()) {

            updatePublicSubscription(subs);
        }

        return (addSubscription(subs));
    }

    public Subscription createSingletonRSS(String name, URL url, int check_interval_mins)

            throws SubscriptionException {
        return (createSingletonRSSSupport(name, url, true, check_interval_mins, SubscriptionImpl.ADD_TYPE_CREATE,
                true));
    }

    protected SubscriptionImpl lookupSingletonRSS(String name, URL url, boolean is_public, int check_interval_mins)

            throws SubscriptionException {
        checkURL(url);

        Map singleton_details = getSingletonMap(name, url, is_public, check_interval_mins);

        byte[] sid = SubscriptionBodyImpl.deriveSingletonShortID(singleton_details);

        return (getSubscriptionFromSID(sid));
    }

    protected Subscription createSingletonRSSSupport(String name, URL url, boolean is_public,
            int check_interval_mins, int add_type, boolean subscribe)

            throws SubscriptionException {
        checkURL(url);

        try {
            Subscription existing = lookupSingletonRSS(name, url, is_public, check_interval_mins);

            if (existing != null) {

                return (existing);
            }

            Engine engine = MetaSearchManagerFactory.getSingleton().getMetaSearch().createRSSEngine(name, url);

            String json = SubscriptionImpl.getSkeletonJSON(engine, check_interval_mins);

            Map singleton_details = getSingletonMap(name, url, is_public, check_interval_mins);

            SubscriptionImpl subs = new SubscriptionImpl(this, name, is_public, singleton_details, json, add_type);

            subs.setSubscribed(subscribe);

            log("Created new singleton subscription: " + subs.getString());

            subs = addSubscription(subs);

            return (subs);

        } catch (SubscriptionException e) {

            throw ((SubscriptionException) e);

        } catch (Throwable e) {

            throw (new SubscriptionException("Failed to create subscription", e));
        }
    }

    protected String getUniqueName(String name) {
        for (int i = 0; i < 1024; i++) {

            String test_name = name + (i == 0 ? "" : (" (" + i + ")"));

            if (getSubscriptionFromName(test_name) == null) {

                return (test_name);
            }
        }

        return (name);
    }

    protected Map getSingletonMap(String name, URL url, boolean is_public, int check_interval_mins)

            throws SubscriptionException {
        try {
            Map singleton_details = new HashMap();

            if (url.getProtocol().equalsIgnoreCase("vuze")) {

                // hack to minimise encoded url length for our own urls

                singleton_details.put("key", url.toExternalForm().getBytes(Constants.BYTE_ENCODING));

            } else {
                singleton_details.put("key", url.toExternalForm().getBytes("UTF-8"));
            }

            String name2 = name.length() > 64 ? name.substring(0, 64) : name;

            singleton_details.put("name", name2);

            if (check_interval_mins != SubscriptionHistoryImpl.DEFAULT_CHECK_INTERVAL_MINS) {

                singleton_details.put("ci", new Long(check_interval_mins));
            }

            return (singleton_details);

        } catch (Throwable e) {

            throw (new SubscriptionException("Failed to create subscription", e));
        }
    }

    protected SubscriptionImpl createSingletonSubscription(Map singleton_details, int add_type, boolean subscribe)

            throws SubscriptionException {
        try {
            String name = ImportExportUtils.importString(singleton_details, "name", "(Anonymous)");

            URL url = new URL(ImportExportUtils.importString(singleton_details, "key"));

            int check_interval_mins = (int) ImportExportUtils.importLong(singleton_details, "ci",
                    SubscriptionHistoryImpl.DEFAULT_CHECK_INTERVAL_MINS);

            // only defined type is singleton rss

            SubscriptionImpl s = (SubscriptionImpl) createSingletonRSSSupport(name, url, true, check_interval_mins,
                    add_type, subscribe);

            return (s);

        } catch (Throwable e) {

            log("Creation of singleton from " + singleton_details + " failed", e);

            throw (new SubscriptionException("Creation of singleton from " + singleton_details + " failed", e));
        }
    }

    public Subscription createRSS(String name, URL url, int check_interval_mins, Map user_data)

            throws SubscriptionException {
        checkURL(url);

        try {
            name = getUniqueName(name);

            Engine engine = MetaSearchManagerFactory.getSingleton().getMetaSearch().createRSSEngine(name, url);

            String json = SubscriptionImpl.getSkeletonJSON(engine, check_interval_mins);

            // engine name may have been modified so re-read it for subscription default

            SubscriptionImpl subs = new SubscriptionImpl(this, engine.getName(), engine.isPublic(), null, json,
                    SubscriptionImpl.ADD_TYPE_CREATE);

            if (user_data != null) {

                Iterator it = user_data.entrySet().iterator();

                while (it.hasNext()) {

                    Map.Entry entry = (Map.Entry) it.next();

                    subs.setUserData(entry.getKey(), entry.getValue());
                }
            }

            log("Created new subscription: " + subs.getString());

            subs = addSubscription(subs);

            if (subs.isPublic()) {

                updatePublicSubscription(subs);
            }

            return (subs);

        } catch (Throwable e) {

            throw (new SubscriptionException("Failed to create subscription", e));
        }
    }

    protected void checkURL(URL url)

            throws SubscriptionException {
        if (url.getHost().trim().length() == 0) {

            String protocol = url.getProtocol().toLowerCase();

            if (!(protocol.equals("azplug") || protocol.equals("file") || protocol.equals("vuze"))) {

                throw (new SubscriptionException("Invalid URL '" + url + "'"));
            }
        }
    }

    protected SubscriptionImpl addSubscription(SubscriptionImpl subs) {
        SubscriptionImpl existing;

        synchronized (this) {

            int index = Collections.binarySearch(subscriptions, subs, new Comparator<Subscription>() {
                public int compare(Subscription arg0, Subscription arg1) {
                    return arg0.getID().compareTo(arg1.getID());
                }
            });
            if (index < 0) {
                existing = null;
                index = -1 * index - 1; // best guess

                subscriptions.add(index, subs);

                saveConfig();
            } else {
                existing = (SubscriptionImpl) subscriptions.get(index);
            }
        }

        if (existing != null) {

            log("Attempted to add subscription when already present: " + subs.getString());

            subs.destroy();

            return (existing);
        }

        if (subs.isMine()) {

            addMetaSearchListener();
        }

        if (subs.getCachedPopularity() == -1) {

            try {
                subs.getPopularity(new SubscriptionPopularityListener() {
                    public void gotPopularity(long popularity) {
                    }

                    public void failed(SubscriptionException error) {
                    }
                });

            } catch (Throwable e) {

                log("", e);
            }
        }

        Iterator it = listeners.iterator();

        while (it.hasNext()) {

            try {
                ((SubscriptionManagerListener) it.next()).subscriptionAdded(subs);

            } catch (Throwable e) {

                Debug.printStackTrace(e);
            }
        }

        if (subs.isSubscribed() && subs.isPublic()) {

            setSelected(subs);
        }

        if (dht_plugin != null) {

            new AEThread2("Publish check", true) {
                public void run() {
                    publishSubscriptions();
                }
            }.start();
        }

        return (subs);
    }

    protected void addMetaSearchListener() {
        synchronized (this) {

            if (meta_search_listener_added) {

                return;
            }

            meta_search_listener_added = true;
        }

        MetaSearchManagerFactory.getSingleton().getMetaSearch().addListener(new MetaSearchListener() {
            public void engineAdded(Engine engine) {

            }

            public void engineUpdated(Engine engine) {
                synchronized (this) {

                    for (int i = 0; i < subscriptions.size(); i++) {

                        SubscriptionImpl subs = (SubscriptionImpl) subscriptions.get(i);

                        if (subs.isMine()) {

                            subs.engineUpdated(engine);
                        }
                    }
                }
            }

            public void engineRemoved(Engine engine) {

            }
        });
    }

    protected void changeSubscription(SubscriptionImpl subs) {
        if (!subs.isRemoved()) {

            Iterator it = listeners.iterator();

            while (it.hasNext()) {

                try {
                    ((SubscriptionManagerListener) it.next()).subscriptionChanged(subs);

                } catch (Throwable e) {

                    Debug.printStackTrace(e);
                }
            }
        }
    }

    protected void selectSubscription(SubscriptionImpl subs) {
        if (!subs.isRemoved()) {

            Iterator it = listeners.iterator();

            while (it.hasNext()) {

                try {
                    ((SubscriptionManagerListener) it.next()).subscriptionSelected(subs);

                } catch (Throwable e) {

                    Debug.printStackTrace(e);
                }
            }
        }
    }

    protected void removeSubscription(SubscriptionImpl subs) {
        synchronized (this) {

            if (subscriptions.remove(subs)) {

                saveConfig();

            } else {

                return;
            }
        }

        try {
            Engine engine = subs.getEngine(true);

            if (engine.getType() == Engine.ENGINE_TYPE_RSS) {

                engine.delete();

                log("Removed engine " + engine.getName() + " due to subscription removal");
            }

        } catch (Throwable e) {

            log("Failed to check for engine deletion", e);
        }

        Iterator<SubscriptionManagerListener> it = listeners.iterator();

        while (it.hasNext()) {

            try {
                it.next().subscriptionRemoved(subs);

            } catch (Throwable e) {

                Debug.printStackTrace(e);
            }
        }

        try {
            FileUtil.deleteResilientFile(getResultsFile(subs));

            File vuze_file = getVuzeFile(subs);

            vuze_file.delete();

            new File(vuze_file.getParent(), vuze_file.getName() + ".bak").delete();

        } catch (Throwable e) {

            log("Failed to delete results/vuze file", e);
        }
    }

    protected void updatePublicSubscription(SubscriptionImpl subs) {
        if (subs.isSingleton()) {

            // never update singletons

            subs.setServerPublished();

        } else {

            Long l_last_pub = (Long) subs.getUserData(SP_LAST_ATTEMPTED);
            Long l_consec_fail = (Long) subs.getUserData(SP_CONSEC_FAIL);

            if (l_last_pub != null && l_consec_fail != null) {

                long delay = SERVER_PUB_CHECK_PERIOD;

                for (int i = 0; i < l_consec_fail.longValue(); i++) {

                    delay <<= 1;

                    if (delay > 24 * 60 * 60 * 1000) {

                        break;
                    }
                }

                if (l_last_pub.longValue() + delay > SystemTime.getMonotonousTime()) {

                    return;
                }
            }

            try {
                File vf = getVuzeFile(subs);

                byte[] bytes = FileUtil.readFileAsByteArray(vf);

                byte[] encoded_subs = Base64.encode(bytes);

                PlatformSubscriptionsMessenger.updateSubscription(!subs.getServerPublished(), subs.getName(),
                        subs.getPublicKey(), subs.getPrivateKey(), subs.getShortID(), subs.getVersion(),
                        new String(encoded_subs));

                subs.setUserData(SP_LAST_ATTEMPTED, null);
                subs.setUserData(SP_CONSEC_FAIL, null);

                subs.setServerPublished();

                log("    Updated public subscription " + subs.getString());

            } catch (Throwable e) {

                log("    Failed to update public subscription " + subs.getString(), e);

                subs.setUserData(SP_LAST_ATTEMPTED, new Long(SystemTime.getMonotonousTime()));

                subs.setUserData(SP_CONSEC_FAIL,
                        new Long(l_consec_fail == null ? 1 : (l_consec_fail.longValue() + 1)));

                subs.setServerPublicationOutstanding();
            }
        }
    }

    protected void checkSingletonPublish(SubscriptionImpl subs)

            throws SubscriptionException {
        if (subs.getSingletonPublishAttempted()) {

            throw (new SubscriptionException("Singleton publish already attempted"));
        }

        subs.setSingletonPublishAttempted();

        try {
            File vf = getVuzeFile(subs);

            byte[] bytes = FileUtil.readFileAsByteArray(vf);

            byte[] encoded_subs = Base64.encode(bytes);

            // use a transient key-pair as we won't have the private key in general

            KeyPair kp = CryptoECCUtils.createKeys();

            byte[] public_key = CryptoECCUtils.keyToRawdata(kp.getPublic());
            byte[] private_key = CryptoECCUtils.keyToRawdata(kp.getPrivate());

            PlatformSubscriptionsMessenger.updateSubscription(true, subs.getName(), public_key, private_key,
                    subs.getShortID(), 1, new String(encoded_subs));

            log("    created singleton public subscription " + subs.getString());

        } catch (Throwable e) {

            throw (new SubscriptionException("Failed to publish singleton", e));
        }
    }

    protected void checkServerPublications(List subs) {
        for (int i = 0; i < subs.size(); i++) {

            SubscriptionImpl sub = (SubscriptionImpl) subs.get(i);

            if (sub.getServerPublicationOutstanding()) {

                updatePublicSubscription(sub);
            }
        }
    }

    protected void checkStuff(int ticks) {
        long now = SystemTime.getCurrentTime();

        List<SubscriptionImpl> subs;

        synchronized (this) {

            subs = new ArrayList<SubscriptionImpl>(subscriptions);
        }

        SubscriptionImpl expired_subs = null;

        for (int i = 0; i < subs.size(); i++) {

            SubscriptionImpl sub = subs.get(i);

            if (!(sub.isMine() || sub.isSubscribed())) {

                long age = now - sub.getAddTime();

                if (age > DELETE_UNUSED_AFTER_MILLIS) {

                    if (expired_subs == null || (sub.getAddTime() < expired_subs.getAddTime())) {

                        expired_subs = sub;
                    }

                    continue;
                }
            }

            sub.checkPublish();
        }

        if (expired_subs != null) {

            log("Removing unsubscribed subscription '" + expired_subs.getName() + "' as expired");

            expired_subs.remove();
        }

        if (ticks % ASSOC_CHECK_TICKS == 0) {

            lookupAssociations(false);
        }

        if (ticks % SERVER_PUB_CHECK_TICKS == 0) {

            checkServerPublications(subs);
        }

        if (ticks % TIDY_POT_ASSOC_TICKS == 0) {

            tidyPotentialAssociations();
        }

        if (ticks == SET_SELECTED_FIRST_TICK || ticks % SET_SELECTED_TICKS == 0) {

            setSelected(subs);
        }
    }

    public Subscription importSubscription(int type, Map map, boolean warn_user)

            throws SubscriptionException {
        boolean log_errors = true;

        try {
            try {
                if (type == VuzeFileComponent.COMP_TYPE_SUBSCRIPTION_SINGLETON) {

                    String name = new String((byte[]) map.get("name"), "UTF-8");

                    URL url = new URL(new String((byte[]) map.get("url"), "UTF-8"));

                    Long l_interval = (Long) map.get("check_interval_mins");

                    int check_interval_mins = l_interval == null
                            ? SubscriptionHistoryImpl.DEFAULT_CHECK_INTERVAL_MINS
                            : l_interval.intValue();

                    Long l_public = (Long) map.get("public");

                    boolean is_public = l_public == null ? true : l_public.longValue() == 1;

                    SubscriptionImpl existing = lookupSingletonRSS(name, url, is_public, check_interval_mins);

                    if (UrlFilter.getInstance().urlCanRPC(url.toExternalForm())) {

                        warn_user = false;
                    }

                    if (existing != null && existing.isSubscribed()) {

                        if (warn_user) {

                            UIManager ui_manager = StaticUtilities.getUIManager(120 * 1000);

                            String details = MessageText.getString("subscript.add.dup.desc",
                                    new String[] { existing.getName() });

                            ui_manager.showMessageBox("subscript.add.dup.title", "!" + details + "!",
                                    UIManagerEvent.MT_OK);
                        }

                        selectSubscription(existing);

                        return (existing);

                    } else {

                        if (warn_user) {

                            UIManager ui_manager = StaticUtilities.getUIManager(120 * 1000);

                            String details = MessageText.getString("subscript.add.desc", new String[] { name });

                            long res = ui_manager.showMessageBox("subscript.add.title", "!" + details + "!",
                                    UIManagerEvent.MT_YES | UIManagerEvent.MT_NO);

                            if (res != UIManagerEvent.MT_YES) {

                                log_errors = false;

                                throw (new SubscriptionException("User declined addition"));
                            }
                        }

                        if (existing == null) {

                            SubscriptionImpl new_subs = (SubscriptionImpl) createSingletonRSSSupport(name, url,
                                    is_public, check_interval_mins, SubscriptionImpl.ADD_TYPE_IMPORT, true);

                            log("Imported new singleton subscription: " + new_subs.getString());

                            return (new_subs);

                        } else {

                            existing.setSubscribed(true);

                            selectSubscription(existing);

                            return (existing);
                        }
                    }
                } else {

                    SubscriptionBodyImpl body = new SubscriptionBodyImpl(this, map);

                    SubscriptionImpl existing = getSubscriptionFromSID(body.getShortID());

                    if (existing != null && existing.isSubscribed()) {

                        if (existing.getVersion() >= body.getVersion()) {

                            log("Not upgrading subscription: " + existing.getString() + " as supplied ("
                                    + body.getVersion() + ") is not more recent than existing ("
                                    + existing.getVersion() + ")");

                            if (warn_user) {

                                UIManager ui_manager = StaticUtilities.getUIManager(120 * 1000);

                                String details = MessageText.getString("subscript.add.dup.desc",
                                        new String[] { existing.getName() });

                                ui_manager.showMessageBox("subscript.add.dup.title", "!" + details + "!",
                                        UIManagerEvent.MT_OK);
                            }
                            // we have a newer one, ignore

                            selectSubscription(existing);

                            return (existing);

                        } else {

                            if (warn_user) {

                                UIManager ui_manager = StaticUtilities.getUIManager(120 * 1000);

                                String details = MessageText.getString("subscript.add.upgrade.desc",
                                        new String[] { existing.getName() });

                                long res = ui_manager.showMessageBox("subscript.add.upgrade.title",
                                        "!" + details + "!", UIManagerEvent.MT_YES | UIManagerEvent.MT_NO);

                                if (res != UIManagerEvent.MT_YES) {

                                    throw (new SubscriptionException("User declined upgrade"));
                                }
                            }

                            log("Upgrading subscription: " + existing.getString());

                            existing.upgrade(body);

                            saveConfig();

                            subscriptionUpdated();

                            return (existing);
                        }
                    } else {

                        SubscriptionImpl new_subs = new SubscriptionImpl(this, body,
                                SubscriptionImpl.ADD_TYPE_IMPORT, true);

                        if (warn_user) {

                            UIManager ui_manager = StaticUtilities.getUIManager(120 * 1000);

                            String details = MessageText.getString("subscript.add.desc",
                                    new String[] { new_subs.getName() });

                            long res = ui_manager.showMessageBox("subscript.add.title", "!" + details + "!",
                                    UIManagerEvent.MT_YES | UIManagerEvent.MT_NO);

                            if (res != UIManagerEvent.MT_YES) {

                                throw (new SubscriptionException("User declined addition"));
                            }
                        }

                        log("Imported new subscription: " + new_subs.getString());

                        if (existing != null) {

                            existing.remove();
                        }

                        new_subs = addSubscription(new_subs);

                        return (new_subs);
                    }
                }
            } catch (Throwable e) {

                throw (new SubscriptionException("Subscription import failed", e));
            }
        } catch (SubscriptionException e) {

            if (warn_user && log_errors) {

                UIManager ui_manager = StaticUtilities.getUIManager(120 * 1000);

                String details = MessageText.getString("subscript.import.fail.desc",
                        new String[] { Debug.getNestedExceptionMessage(e) });

                ui_manager.showMessageBox("subscript.import.fail.title", "!" + details + "!", UIManagerEvent.MT_OK);
            }

            throw (e);
        }
    }

    public Subscription[] getSubscriptions() {
        synchronized (this) {

            return ((SubscriptionImpl[]) subscriptions.toArray(new SubscriptionImpl[subscriptions.size()]));
        }
    }

    public Subscription[] getSubscriptions(boolean subscribed_only) {
        if (!subscribed_only) {

            return (getSubscriptions());
        }

        List result = new ArrayList();

        synchronized (this) {

            for (int i = 0; i < subscriptions.size(); i++) {

                SubscriptionImpl subs = (SubscriptionImpl) subscriptions.get(i);

                if (subs.isSubscribed()) {

                    result.add(subs);
                }
            }
        }

        return ((SubscriptionImpl[]) result.toArray(new SubscriptionImpl[result.size()]));
    }

    public int getSubscriptionCount(boolean subscribed_only) {
        if (subscribed_only) {

            int total = 0;

            synchronized (this) {

                for (Subscription subs : subscriptions) {

                    if (subs.isSubscribed()) {

                        total++;
                    }
                }
            }

            return (total);

        } else {
            return (subscriptions.size());
        }
    }

    protected SubscriptionImpl getSubscriptionFromName(String name) {
        synchronized (this) {

            for (int i = 0; i < subscriptions.size(); i++) {

                SubscriptionImpl s = (SubscriptionImpl) subscriptions.get(i);

                if (s.getName().equalsIgnoreCase(name)) {

                    return (s);
                }
            }
        }

        return (null);
    }

    public Subscription getSubscriptionByID(String id) {
        synchronized (this) {

            int index = Collections.binarySearch(subscriptions, id, new Comparator() {
                public int compare(Object o1, Object o2) {
                    String id1 = (o1 instanceof Subscription) ? ((Subscription) o1).getID() : o1.toString();
                    String id2 = (o2 instanceof Subscription) ? ((Subscription) o2).getID() : o2.toString();
                    return id1.compareTo(id2);
                }
            });

            if (index >= 0) {
                return subscriptions.get(index);
            }
        }

        return null;
    }

    protected SubscriptionImpl getSubscriptionFromSID(byte[] sid) {
        return (SubscriptionImpl) getSubscriptionByID(Base32.encode(sid));
    }

    protected File getSubsDir()

            throws IOException {
        File dir = new File(SystemProperties.getUserPath());

        dir = new File(dir, "subs");

        if (!dir.exists()) {

            if (!dir.mkdirs()) {

                throw (new IOException("Failed to create '" + dir + "'"));
            }
        }

        return (dir);
    }

    protected File getVuzeFile(SubscriptionImpl subs)

            throws IOException {
        File dir = getSubsDir();

        return (new File(dir, ByteFormatter.encodeString(subs.getShortID()) + ".vuze"));
    }

    protected File getResultsFile(SubscriptionImpl subs)

            throws IOException {
        File dir = getSubsDir();

        return (new File(dir, ByteFormatter.encodeString(subs.getShortID()) + ".results"));
    }

    public int getKnownSubscriptionCount() {
        PluginInterface pi = PluginInitializer.getDefaultInterface();

        ByteArrayHashMap<String> results = new ByteArrayHashMap<String>();

        try {
            Download[] downloads = pi.getDownloadManager().getDownloads();

            for (Download download : downloads) {

                Map m = download.getMapAttribute(ta_subscription_info);

                if (m != null) {

                    List s = (List) m.get("s");

                    if (s != null && s.size() > 0) {

                        List result = new ArrayList(s.size());

                        for (int i = 0; i < s.size(); i++) {

                            byte[] sid = (byte[]) s.get(i);

                            results.put(sid, "");
                        }
                    }
                }
            }
        } catch (Throwable e) {

            log("Failed to get known subscriptions", e);
        }

        return (results.size());
    }

    public Subscription[] getKnownSubscriptions(byte[] hash) {
        PluginInterface pi = PluginInitializer.getDefaultInterface();

        try {
            Download download = pi.getDownloadManager().getDownload(hash);

            if (download != null) {

                Map m = download.getMapAttribute(ta_subscription_info);

                if (m != null) {

                    List s = (List) m.get("s");

                    if (s != null && s.size() > 0) {

                        List result = new ArrayList(s.size());

                        boolean hide_search = hideSearchTemplates();

                        for (int i = 0; i < s.size(); i++) {

                            byte[] sid = (byte[]) s.get(i);

                            SubscriptionImpl subs = getSubscriptionFromSID(sid);

                            if (subs != null) {

                                if (isVisible(subs)) {

                                    if (hide_search && subs.isSearchTemplate()) {

                                    } else {

                                        result.add(subs);
                                    }
                                }
                            }
                        }

                        return ((Subscription[]) result.toArray(new Subscription[result.size()]));
                    }
                }
            }
        } catch (Throwable e) {

            log("Failed to get known subscriptions", e);
        }

        return (new Subscription[0]);
    }

    protected boolean subscriptionExists(Download download, SubscriptionImpl subs) {
        byte[] sid = subs.getShortID();

        Map m = download.getMapAttribute(ta_subscription_info);

        if (m != null) {

            List s = (List) m.get("s");

            if (s != null && s.size() > 0) {

                for (int i = 0; i < s.size(); i++) {

                    byte[] x = (byte[]) s.get(i);

                    if (Arrays.equals(x, sid)) {

                        return (true);
                    }
                }
            }
        }

        return (false);
    }

    protected boolean isVisible(SubscriptionImpl subs) {
        // to avoid development links polluting production we filter out such subscriptions

        if (Constants.isCVSVersion() || subs.isSubscribed()) {

            return (true);
        }

        try {
            Engine engine = subs.getEngine(true);

            if (engine instanceof WebEngine) {

                String url = ((WebEngine) engine).getSearchUrl();

                try {
                    String host = new URL(url).getHost();

                    return (!exclusion_pattern.matcher(host).matches());

                } catch (Throwable e) {
                }
            }

            return (true);

        } catch (Throwable e) {

            log("isVisible failed for " + subs.getString(), e);

            return (false);
        }
    }

    public Subscription[] getLinkedSubscriptions(byte[] hash) {
        PluginInterface pi = PluginInitializer.getDefaultInterface();

        try {
            Download download = pi.getDownloadManager().getDownload(hash);

            if (download != null) {

                Map m = download.getMapAttribute(ta_subscription_info);

                if (m != null) {

                    List s = (List) m.get("s");

                    if (s != null && s.size() > 0) {

                        List result = new ArrayList(s.size());

                        for (int i = 0; i < s.size(); i++) {

                            byte[] sid = (byte[]) s.get(i);

                            SubscriptionImpl subs = getSubscriptionFromSID(sid);

                            if (subs != null) {

                                if (subs.hasAssociation(hash)) {

                                    result.add(subs);
                                }
                            }
                        }

                        return ((Subscription[]) result.toArray(new Subscription[result.size()]));
                    }
                }
            }
        } catch (Throwable e) {

            log("Failed to get known subscriptions", e);
        }

        return (new Subscription[0]);
    }

    protected void lookupAssociations(boolean high_priority) {
        synchronized (this) {

            if (periodic_lookup_in_progress) {

                if (high_priority) {

                    priority_lookup_pending++;
                }

                return;
            }

            periodic_lookup_in_progress = true;
        }

        try {
            PluginInterface pi = PluginInitializer.getDefaultInterface();

            Download[] downloads = pi.getDownloadManager().getDownloads();

            long now = SystemTime.getCurrentTime();

            long newest_time = 0;
            Download newest_download = null;

            for (int i = 0; i < downloads.length; i++) {

                Download download = downloads[i];

                if (download.getTorrent() == null || !download.isPersistent()) {

                    continue;
                }

                Map map = download.getMapAttribute(ta_subscription_info);

                if (map == null) {

                    map = new LightHashMap();

                } else {

                    map = new LightHashMap(map);
                }

                Long l_last_check = (Long) map.get("lc");

                long last_check = l_last_check == null ? 0 : l_last_check.longValue();

                if (last_check > now) {

                    last_check = now;

                    map.put("lc", new Long(last_check));

                    download.setMapAttribute(ta_subscription_info, map);
                }

                List subs = (List) map.get("s");

                int sub_count = subs == null ? 0 : subs.size();

                if (sub_count > 8) {

                    continue;
                }

                long create_time = download.getCreationTime();

                int time_between_checks = (sub_count + 1) * 24 * 60 * 60 * 1000
                        + (int) (create_time % 4 * 60 * 60 * 1000);

                if (now - last_check >= time_between_checks) {

                    if (create_time > newest_time) {

                        newest_time = create_time;
                        newest_download = download;
                    }
                }
            }

            if (newest_download != null) {

                byte[] hash = newest_download.getTorrent().getHash();

                log("Association lookup starts for " + newest_download.getName() + "/"
                        + ByteFormatter.encodeString(hash));

                lookupAssociationsSupport(hash, new SubscriptionLookupListener() {
                    public void found(byte[] hash, Subscription subscription) {
                    }

                    public void failed(byte[] hash, SubscriptionException error) {
                        log("Association lookup failed for " + ByteFormatter.encodeString(hash), error);

                        associationLookupComplete();
                    }

                    public void complete(byte[] hash, Subscription[] subs) {
                        log("Association lookup complete for " + ByteFormatter.encodeString(hash));

                        associationLookupComplete();
                    }
                });

            } else {

                associationLookupComplete();
            }
        } catch (Throwable e) {

            log("Association lookup check failed", e);

            associationLookupComplete();
        }
    }

    protected void associationLookupComplete() {
        boolean recheck;

        synchronized (SubscriptionManagerImpl.this) {

            periodic_lookup_in_progress = false;

            recheck = priority_lookup_pending > 0;

            if (recheck) {

                priority_lookup_pending--;
            }
        }

        if (recheck) {

            new AEThread2("SM:priAssLookup", true) {
                public void run() {
                    lookupAssociations(false);
                }
            }.start();
        }
    }

    protected void setSelected(List subs) {
        List sids = new ArrayList();
        List used_subs = new ArrayList();

        for (int i = 0; i < subs.size(); i++) {

            SubscriptionImpl sub = (SubscriptionImpl) subs.get(i);

            if (sub.isSubscribed()) {

                if (sub.isPublic()) {

                    used_subs.add(sub);

                    sids.add(sub.getShortID());

                } else {

                    checkInitialDownload(sub);
                }
            }
        }

        if (sids.size() > 0) {

            try {
                List[] result = PlatformSubscriptionsMessenger.setSelected(sids);

                List versions = result[0];
                List popularities = result[1];

                log("Popularity update: updated " + sids.size());

                final List dht_pops = new ArrayList();

                for (int i = 0; i < sids.size(); i++) {

                    SubscriptionImpl sub = (SubscriptionImpl) used_subs.get(i);

                    int latest_version = ((Long) versions.get(i)).intValue();

                    if (latest_version > sub.getVersion()) {

                        updateSubscription(sub, latest_version);

                    } else {

                        checkInitialDownload(sub);
                    }

                    if (latest_version > 0) {

                        try {
                            long pop = ((Long) popularities.get(i)).longValue();

                            if (pop >= 0 && pop != sub.getCachedPopularity()) {

                                sub.setCachedPopularity(pop);
                            }
                        } catch (Throwable e) {

                            log("Popularity update: Failed to extract popularity", e);
                        }
                    } else {

                        dht_pops.add(sub);
                    }
                }

                if (dht_pops.size() <= 3) {

                    for (int i = 0; i < dht_pops.size(); i++) {

                        updatePopularityFromDHT((SubscriptionImpl) dht_pops.get(i), false);
                    }
                } else {

                    new AEThread2("SM:asyncPop", true) {
                        public void run() {
                            for (int i = 0; i < dht_pops.size(); i++) {

                                updatePopularityFromDHT((SubscriptionImpl) dht_pops.get(i), true);
                            }
                        }
                    }.start();
                }
            } catch (Throwable e) {

                log("Popularity update: Failed to record selected subscriptions", e);
            }
        } else {

            log("Popularity update: No selected, public subscriptions");
        }
    }

    protected void checkUpgrade(SubscriptionImpl sub) {
        setSelected(sub);
    }

    protected void setSelected(final SubscriptionImpl sub) {
        if (sub.isSubscribed()) {

            if (sub.isPublic()) {

                new DelayedEvent("SM:setSelected", 0, new AERunnable() {
                    public void runSupport() {
                        try {
                            List sids = new ArrayList();

                            sids.add(sub.getShortID());

                            List[] result = PlatformSubscriptionsMessenger.setSelected(sids);

                            log("setSelected: " + sub.getName());

                            int latest_version = ((Long) result[0].get(0)).intValue();

                            if (latest_version == 0) {

                                if (sub.isSingleton()) {

                                    checkSingletonPublish(sub);
                                }
                            } else if (latest_version > sub.getVersion()) {

                                updateSubscription(sub, latest_version);

                            } else {

                                checkInitialDownload(sub);
                            }

                            if (latest_version > 0) {

                                try {
                                    long pop = ((Long) result[1].get(0)).longValue();

                                    if (pop >= 0 && pop != sub.getCachedPopularity()) {

                                        sub.setCachedPopularity(pop);
                                    }
                                } catch (Throwable e) {

                                    log("Popularity update: Failed to extract popularity", e);
                                }
                            } else {

                                updatePopularityFromDHT(sub, true);
                            }
                        } catch (Throwable e) {

                            log("setSelected: failed for " + sub.getName(), e);
                        }
                    }
                });
            } else {

                checkInitialDownload(sub);
            }
        }
    }

    protected void checkInitialDownload(SubscriptionImpl subs) {
        if (subs.getHistory().getLastScanTime() == 0) {

            scheduler.download(subs, true, new SubscriptionDownloadListener() {
                public void complete(Subscription subs) {
                    log("Initial download of " + subs.getName() + " complete");
                }

                public void failed(Subscription subs, SubscriptionException error) {
                    log("Initial download of " + subs.getName() + " failed", error);
                }
            });
        }
    }

    public SubscriptionAssociationLookup lookupAssociations(final byte[] hash,
            final SubscriptionLookupListener listener)

            throws SubscriptionException {
        if (dht_plugin != null && !dht_plugin.isInitialising()) {

            return (lookupAssociationsSupport(hash, listener));
        }

        final boolean[] cancelled = { false };

        final SubscriptionAssociationLookup[] actual_res = { null };

        final SubscriptionAssociationLookup res = new SubscriptionAssociationLookup() {
            public void cancel() {
                log("    Association lookup cancelled");

                synchronized (actual_res) {

                    cancelled[0] = true;

                    if (actual_res[0] != null) {

                        actual_res[0].cancel();
                    }
                }
            }
        };

        new AEThread2("SM:initwait", true) {
            public void run() {
                try {
                    SubscriptionAssociationLookup x = lookupAssociationsSupport(hash, listener);

                    synchronized (actual_res) {

                        actual_res[0] = x;

                        if (cancelled[0]) {

                            x.cancel();
                        }
                    }

                } catch (SubscriptionException e) {

                    listener.failed(hash, e);
                }

            }
        }.start();

        return (res);
    }

    protected SubscriptionAssociationLookup lookupAssociationsSupport(final byte[] hash,
            final SubscriptionLookupListener listener)

            throws SubscriptionException {
        log("Looking up associations for '" + ByteFormatter.encodeString(hash));

        final String key = "subscription:assoc:" + ByteFormatter.encodeString(hash);

        final boolean[] cancelled = { false };

        dht_plugin.get(key.getBytes(), "Subscription association read: " + ByteFormatter.encodeString(hash),
                DHTPlugin.FLAG_SINGLE_VALUE, 30, 60 * 1000, true, true, new DHTPluginOperationListener() {
                    private Map<HashWrapper, Integer> hits = new HashMap<HashWrapper, Integer>();
                    private AESemaphore hits_sem = new AESemaphore("Subs:lookup");
                    private List<Subscription> found_subscriptions = new ArrayList<Subscription>();

                    private boolean complete;

                    public void diversified() {
                    }

                    public void starts(byte[] key) {
                    }

                    public void valueRead(DHTPluginContact originator, DHTPluginValue value) {
                        if (isCancelled2()) {

                            return;
                        }

                        byte[] val = value.getValue();

                        if (val.length > 4) {

                            int ver = ((val[0] << 16) & 0xff0000) | ((val[1] << 8) & 0xff00) | (val[2] & 0xff);

                            byte[] sid = new byte[val.length - 4];

                            System.arraycopy(val, 4, sid, 0, sid.length);

                            HashWrapper hw = new HashWrapper(sid);

                            boolean new_sid = false;

                            synchronized (this) {

                                if (complete) {

                                    return;
                                }

                                Integer v = (Integer) hits.get(hw);

                                if (v != null) {

                                    if (ver > v.intValue()) {

                                        hits.put(hw, new Integer(ver));
                                    }
                                } else {

                                    new_sid = true;

                                    hits.put(hw, new Integer(ver));
                                }
                            }

                            if (new_sid) {

                                log("    Found subscription " + ByteFormatter.encodeString(sid) + " version "
                                        + ver);

                                // check if already subscribed

                                SubscriptionImpl subs = getSubscriptionFromSID(sid);

                                if (subs != null) {

                                    found_subscriptions.add(subs);

                                    try {
                                        listener.found(hash, subs);

                                    } catch (Throwable e) {

                                        Debug.printStackTrace(e);
                                    }

                                    hits_sem.release();

                                } else {

                                    lookupSubscription(hash, sid, ver, new subsLookupListener() {
                                        private boolean sem_done = false;

                                        public void found(byte[] hash, Subscription subscription) {
                                        }

                                        public void complete(byte[] hash, Subscription[] subscriptions) {
                                            done(subscriptions);
                                        }

                                        public void failed(byte[] hash, SubscriptionException error) {
                                            done(new Subscription[0]);
                                        }

                                        protected void done(Subscription[] subs) {
                                            if (isCancelled()) {

                                                return;
                                            }

                                            synchronized (this) {

                                                if (sem_done) {

                                                    return;
                                                }

                                                sem_done = true;
                                            }

                                            if (subs.length > 0) {

                                                found_subscriptions.add(subs[0]);

                                                try {
                                                    listener.found(hash, subs[0]);

                                                } catch (Throwable e) {

                                                    Debug.printStackTrace(e);
                                                }
                                            }

                                            hits_sem.release();
                                        }

                                        public boolean isCancelled() {
                                            return (isCancelled2());
                                        }
                                    });
                                }
                            }
                        }
                    }

                    public void valueWritten(DHTPluginContact target, DHTPluginValue value) {
                    }

                    public void complete(byte[] original_key, boolean timeout_occurred) {
                        new AEThread2("Subs:lookup wait", true) {
                            public void run() {
                                int num_hits;

                                synchronized (this) {

                                    if (complete) {

                                        return;
                                    }

                                    complete = true;

                                    num_hits = hits.size();
                                }

                                for (int i = 0; i < num_hits; i++) {

                                    if (isCancelled2()) {

                                        return;
                                    }

                                    hits_sem.reserve();
                                }

                                if (isCancelled2()) {

                                    return;
                                }

                                SubscriptionImpl[] s;

                                synchronized (this) {

                                    s = (SubscriptionImpl[]) found_subscriptions
                                            .toArray(new SubscriptionImpl[found_subscriptions.size()]);
                                }

                                log("    Association lookup complete - " + s.length + " found");

                                try {
                                    // record zero assoc here for completeness

                                    recordAssociations(hash, s, true);

                                } finally {

                                    listener.complete(hash, s);
                                }
                            }
                        }.start();
                    }

                    protected boolean isCancelled2() {
                        synchronized (cancelled) {

                            return (cancelled[0]);
                        }
                    }
                });

        return (new SubscriptionAssociationLookup() {
            public void cancel() {
                log("    Association lookup cancelled");

                synchronized (cancelled) {

                    cancelled[0] = true;
                }
            }
        });
    }

    interface subsLookupListener extends SubscriptionLookupListener {
        public boolean isCancelled();
    }

    protected void getPopularity(final SubscriptionImpl subs, final SubscriptionPopularityListener listener)

            throws SubscriptionException {
        try {
            long pop = PlatformSubscriptionsMessenger.getPopularityBySID(subs.getShortID());

            if (pop >= 0) {

                log("Got popularity of " + subs.getName() + " from platform: " + pop);

                listener.gotPopularity(pop);

                return;

            } else {

                // unknown sid - if singleton try to register for popularity tracking purposes

                if (subs.isSingleton()) {

                    try {
                        checkSingletonPublish(subs);

                    } catch (Throwable e) {
                    }

                    listener.gotPopularity(subs.isSubscribed() ? 1 : 0);

                    return;
                }
            }

        } catch (Throwable e) {

            log("Subscription lookup via platform failed", e);
        }

        getPopularityFromDHT(subs, listener, true);
    }

    protected void getPopularityFromDHT(final SubscriptionImpl subs, final SubscriptionPopularityListener listener,
            final boolean sync)

    {
        if (dht_plugin != null && !dht_plugin.isInitialising()) {

            getPopularitySupport(subs, listener, sync);

        } else {

            new AEThread2("SM:popwait", true) {
                public void run() {
                    getPopularitySupport(subs, listener, sync);
                }
            }.start();
        }
    }

    protected void updatePopularityFromDHT(final SubscriptionImpl subs, boolean sync) {
        getPopularityFromDHT(subs, new SubscriptionPopularityListener() {
            public void gotPopularity(long popularity) {
                subs.setCachedPopularity(popularity);
            }

            public void failed(SubscriptionException error) {
                log("Failed to update subscription popularity from DHT", error);
            }
        }, sync);
    }

    protected void getPopularitySupport(final SubscriptionImpl subs, final SubscriptionPopularityListener listener,
            final boolean sync) {
        log("Getting popularity of " + subs.getName() + " from DHT");

        byte[] hash = subs.getPublicationHash();

        final AESemaphore sem = new AESemaphore("SM:pop");

        final long[] result = { -1 };

        final int timeout = 15 * 1000;

        dht_plugin.get(hash, "Popularity lookup for subscription " + subs.getName(), DHT.FLAG_STATS, 5, timeout,
                false, true, new DHTPluginOperationListener() {
                    private boolean diversified;

                    private int hits = 0;

                    public void diversified() {
                        diversified = true;
                    }

                    public void starts(byte[] key) {
                    }

                    public void valueRead(DHTPluginContact originator, DHTPluginValue value) {
                        DHTPluginKeyStats stats = dht_plugin.decodeStats(value);

                        result[0] = Math.max(result[0], stats.getEntryCount());

                        hits++;

                        if (hits >= 3) {

                            done();
                        }
                    }

                    public void valueWritten(DHTPluginContact target, DHTPluginValue value) {

                    }

                    public void complete(byte[] key, boolean timeout_occurred) {
                        if (diversified) {

                            // TODO: fix?

                            result[0] *= 11;

                            if (result[0] == 0) {

                                result[0] = 10;
                            }
                        }

                        done();
                    }

                    protected void done() {
                        if (sync) {

                            sem.release();

                        } else {

                            if (result[0] == -1) {

                                log("Failed to get popularity of " + subs.getName() + " from DHT");

                                listener.failed(new SubscriptionException("Timeout"));

                            } else {

                                log("Get popularity of " + subs.getName() + " from DHT: " + result[0]);

                                listener.gotPopularity(result[0]);
                            }
                        }
                    }
                });

        if (sync) {

            sem.reserve(timeout);

            if (result[0] == -1) {

                log("Failed to get popularity of " + subs.getName() + " from DHT");

                listener.failed(new SubscriptionException("Timeout"));

            } else {

                log("Get popularity of " + subs.getName() + " from DHT: " + result[0]);

                listener.gotPopularity(result[0]);
            }
        }
    }

    protected void lookupSubscription(final byte[] association_hash, final byte[] sid, final int version,
            final subsLookupListener listener) {
        try {
            SubscriptionImpl subs = getSubscriptionFromPlatform(sid, SubscriptionImpl.ADD_TYPE_LOOKUP);

            log("Added temporary subscription: " + subs.getString());

            subs = addSubscription(subs);

            listener.complete(association_hash, new Subscription[] { subs });

            return;

        } catch (Throwable e) {

            if (listener.isCancelled()) {

                return;
            }

            final String sid_str = ByteFormatter.encodeString(sid);

            log("Subscription lookup via platform for " + sid_str + " failed", e);

            if (getSubscriptionDownloadCount() > 8) {

                log("Too many existing subscription downloads");

                listener.complete(association_hash, new Subscription[0]);

                return;
            }

            // fall back to DHT

            log("Subscription lookup via DHT starts for " + sid_str);

            final String key = "subscription:publish:" + ByteFormatter.encodeString(sid) + ":" + version;

            dht_plugin.get(key.getBytes(),
                    "Subscription lookup read: " + ByteFormatter.encodeString(sid) + ":" + version,
                    DHTPlugin.FLAG_SINGLE_VALUE, 12, 60 * 1000, false, true, new DHTPluginOperationListener() {
                        private boolean listener_handled;

                        public void diversified() {
                        }

                        public void starts(byte[] key) {
                        }

                        public void valueRead(DHTPluginContact originator, DHTPluginValue value) {
                            byte[] data = value.getValue();

                            try {
                                final Map details = decodeSubscriptionDetails(data);

                                if (SubscriptionImpl.getPublicationVersion(details) == version) {

                                    Map singleton_details = (Map) details.get("x");

                                    if (singleton_details == null) {

                                        synchronized (this) {

                                            if (listener_handled) {

                                                return;
                                            }

                                            listener_handled = true;
                                        }

                                        log("    found " + sid_str + ", non-singleton");

                                        new AEThread2("Subs:lookup download", true) {
                                            public void run() {
                                                downloadSubscription(association_hash,
                                                        SubscriptionImpl.getPublicationHash(details), sid, version,
                                                        SubscriptionImpl.getPublicationSize(details), listener);
                                            }
                                        }.start();

                                    } else {

                                        synchronized (this) {

                                            if (listener_handled) {

                                                return;
                                            }

                                            listener_handled = true;
                                        }

                                        log("    found " + sid_str + ", singleton");

                                        try {
                                            SubscriptionImpl subs = createSingletonSubscription(singleton_details,
                                                    SubscriptionImpl.ADD_TYPE_LOOKUP, false);

                                            listener.complete(association_hash, new Subscription[] { subs });

                                        } catch (Throwable e) {

                                            listener.failed(association_hash,
                                                    new SubscriptionException("Subscription creation failed", e));
                                        }
                                    }
                                } else {

                                    log("    found " + sid_str + " but version mismatch");

                                }
                            } catch (Throwable e) {

                                log("    found " + sid_str + " but verification failed", e);

                            }
                        }

                        public void valueWritten(DHTPluginContact target, DHTPluginValue value) {
                        }

                        public void complete(byte[] original_key, boolean timeout_occurred) {
                            if (listener.isCancelled()) {

                                return;
                            }

                            log("    " + sid_str + " complete");

                            synchronized (this) {

                                if (!listener_handled) {

                                    listener_handled = true;

                                    listener.complete(association_hash, new Subscription[0]);
                                }
                            }
                        }
                    });
        }
    }

    protected SubscriptionImpl getSubscriptionFromPlatform(byte[] sid, int add_type)

            throws SubscriptionException {
        try {
            PlatformSubscriptionsMessenger.subscriptionDetails details = PlatformSubscriptionsMessenger
                    .getSubscriptionBySID(sid);

            SubscriptionImpl res = getSubscriptionFromVuzeFileContent(sid, add_type, details.getContent());

            int pop = details.getPopularity();

            if (pop >= 0) {

                res.setCachedPopularity(pop);
            }

            return (res);

        } catch (SubscriptionException e) {

            throw (e);

        } catch (Throwable e) {

            throw (new SubscriptionException("Failed to read subscription from platform", e));
        }
    }

    protected SubscriptionImpl getSubscriptionFromVuzeFile(byte[] sid, int add_type, File file)

            throws SubscriptionException {
        VuzeFileHandler vfh = VuzeFileHandler.getSingleton();

        String file_str = file.getAbsolutePath();

        VuzeFile vf = vfh.loadVuzeFile(file_str);

        if (vf == null) {

            log("Failed to load vuze file from " + file_str);

            throw (new SubscriptionException("Failed to load vuze file from " + file_str));
        }

        return (getSubscriptionFromVuzeFile(sid, add_type, vf));
    }

    protected SubscriptionImpl getSubscriptionFromVuzeFileContent(byte[] sid, int add_type, String content)

            throws SubscriptionException {
        VuzeFileHandler vfh = VuzeFileHandler.getSingleton();

        VuzeFile vf = vfh.loadVuzeFile(Base64.decode(content));

        if (vf == null) {

            log("Failed to load vuze file from " + content);

            throw (new SubscriptionException("Failed to load vuze file from content"));
        }

        return (getSubscriptionFromVuzeFile(sid, add_type, vf));
    }

    protected SubscriptionImpl getSubscriptionFromVuzeFile(byte[] sid, int add_type, VuzeFile vf)

            throws SubscriptionException {
        VuzeFileComponent[] comps = vf.getComponents();

        for (int j = 0; j < comps.length; j++) {

            VuzeFileComponent comp = comps[j];

            if (comp.getType() == VuzeFileComponent.COMP_TYPE_SUBSCRIPTION) {

                Map map = comp.getContent();

                try {
                    SubscriptionBodyImpl body = new SubscriptionBodyImpl(SubscriptionManagerImpl.this, map);

                    SubscriptionImpl new_subs = new SubscriptionImpl(SubscriptionManagerImpl.this, body, add_type,
                            false);

                    if (Arrays.equals(new_subs.getShortID(), sid)) {

                        return (new_subs);
                    }
                } catch (Throwable e) {

                    log("Subscription decode failed", e);
                }
            }
        }

        throw (new SubscriptionException("Subscription not found"));
    }

    protected void downloadSubscription(final byte[] association_hash, byte[] torrent_hash, final byte[] sid,
            int version, int size, final subsLookupListener listener) {
        try {
            Object[] res = downloadTorrent(torrent_hash, size);

            if (listener.isCancelled()) {

                return;
            }

            if (res == null) {

                listener.complete(association_hash, new Subscription[0]);

                return;
            }

            downloadSubscription((TOTorrent) res[0], (InetSocketAddress) res[1], sid, version, "Subscription "
                    + ByteFormatter.encodeString(sid) + " for " + ByteFormatter.encodeString(association_hash),
                    new downloadListener() {
                        public void complete(File data_file) {
                            boolean reported = false;

                            try {
                                if (listener.isCancelled()) {

                                    return;
                                }

                                SubscriptionImpl subs = getSubscriptionFromVuzeFile(sid,
                                        SubscriptionImpl.ADD_TYPE_LOOKUP, data_file);

                                log("Added temporary subscription: " + subs.getString());

                                subs = addSubscription(subs);

                                listener.complete(association_hash, new Subscription[] { subs });

                                reported = true;

                            } catch (Throwable e) {

                                log("Subscription decode failed", e);

                            } finally {

                                if (!reported) {

                                    listener.complete(association_hash, new Subscription[0]);
                                }
                            }
                        }

                        public void complete(Download download, File torrent_file) {
                            File data_file = new File(download.getSavePath());

                            try {
                                removeDownload(download, false);

                                complete(data_file);

                            } catch (Throwable e) {

                                log("Failed to remove download", e);

                                listener.complete(association_hash, new Subscription[0]);

                            } finally {

                                torrent_file.delete();

                                data_file.delete();
                            }
                        }

                        public void failed(Throwable error) {
                            listener.complete(association_hash, new Subscription[0]);
                        }

                        public Map getRecoveryData() {
                            return (null);
                        }

                        public boolean isCancelled() {
                            return (listener.isCancelled());
                        }
                    });

        } catch (Throwable e) {

            log("Subscription download failed", e);

            listener.complete(association_hash, new Subscription[0]);
        }
    }

    protected int getSubscriptionDownloadCount() {
        PluginInterface pi = PluginInitializer.getDefaultInterface();

        Download[] downloads = pi.getDownloadManager().getDownloads();

        int res = 0;

        for (int i = 0; i < downloads.length; i++) {

            Download download = downloads[i];

            if (download.getBooleanAttribute(ta_subs_download)) {

                res++;
            }
        }

        return (res);
    }

    protected void associationAdded(SubscriptionImpl subscription, byte[] association_hash) {
        recordAssociations(association_hash, new SubscriptionImpl[] { subscription }, false);

        if (dht_plugin != null) {

            publishAssociations();
        }
    }

    protected void addPotentialAssociation(SubscriptionImpl subs, String result_id, String key) {
        if (key == null) {

            Debug.out("Attempt to add null key!");

            return;
        }

        log("Added potential association: " + subs.getName() + "/" + result_id + " -> " + key);

        synchronized (potential_associations) {

            potential_associations.add(new Object[] { subs, result_id, key, new Long(System.currentTimeMillis()) });

            if (potential_associations.size() > 512) {

                potential_associations.remove(0);
            }
        }
    }

    protected void checkPotentialAssociations(byte[] hash, String key) {
        log("Checking potential association: " + key + " -> " + ByteFormatter.encodeString(hash));

        SubscriptionImpl subs = null;
        String result_id = null;

        synchronized (potential_associations) {

            Iterator<Object[]> it = potential_associations.iterator();

            while (it.hasNext()) {

                Object[] entry = it.next();

                String this_key = (String) entry[2];

                // startswith as actual URL may have had additional parameters added such as azid

                if (key.startsWith(this_key)) {

                    subs = (SubscriptionImpl) entry[0];
                    result_id = (String) entry[1];

                    log("    key matched to subscription " + subs.getName() + "/" + result_id);

                    it.remove();

                    break;
                }
            }
        }

        if (subs == null) {

            log("    no potential associations found");

        } else {

            SubscriptionResult result = subs.getHistory().getResult(result_id);

            if (result != null) {

                log("    result found, marking as read");

                result.setRead(true);

            } else {

                log("    result not found");
            }

            log("    adding association");

            subs.addAssociation(hash);
        }
    }

    protected void tidyPotentialAssociations() {
        long now = SystemTime.getCurrentTime();

        synchronized (potential_associations) {

            Iterator it = potential_associations.iterator();

            while (it.hasNext() && potential_associations.size() > 16) {

                Object[] entry = (Object[]) it.next();

                long created = ((Long) entry[3]).longValue();

                if (created > now) {

                    entry[3] = new Long(now);

                } else if (now - created > 60 * 60 * 1000) {

                    SubscriptionImpl subs = (SubscriptionImpl) entry[0];

                    String result_id = (String) entry[1];
                    String key = (String) entry[2];

                    log("Removing expired potential association: " + subs.getName() + "/" + result_id + " -> "
                            + key);

                    it.remove();
                }
            }
        }

        synchronized (potential_associations2) {

            Iterator it = potential_associations2.entrySet().iterator();

            while (it.hasNext() && potential_associations2.size() > 16) {

                Map.Entry map_entry = (Map.Entry) it.next();

                byte[] hash = ((HashWrapper) map_entry.getKey()).getBytes();

                Object[] entry = (Object[]) map_entry.getValue();

                long created = ((Long) entry[2]).longValue();

                if (created > now) {

                    entry[2] = new Long(now);

                } else if (now - created > 60 * 60 * 1000) {

                    SubscriptionImpl[] subs = (SubscriptionImpl[]) entry[0];

                    String subs_str = "";

                    for (int i = 0; i < subs.length; i++) {
                        subs_str += (i == 0 ? "" : ",") + subs[i].getName();
                    }

                    log("Removing expired potential association: " + ByteFormatter.encodeString(hash) + " -> "
                            + subs_str);

                    it.remove();
                }
            }
        }
    }

    protected void recordAssociations(byte[] association_hash, SubscriptionImpl[] subscriptions,
            boolean full_lookup) {
        HashWrapper hw = new HashWrapper(association_hash);

        synchronized (potential_associations2) {

            potential_associations2.put(hw, new Object[] { subscriptions, new Boolean(full_lookup),
                    new Long(SystemTime.getCurrentTime()) });
        }

        if (recordAssociationsSupport(association_hash, subscriptions, full_lookup)) {

            synchronized (potential_associations2) {

                potential_associations2.remove(hw);
            }
        } else {

            log("Deferring association for " + ByteFormatter.encodeString(association_hash));
        }
    }

    protected boolean recordAssociationsSupport(byte[] association_hash, SubscriptionImpl[] subscriptions,
            boolean full_lookup) {
        PluginInterface pi = PluginInitializer.getDefaultInterface();

        boolean download_found = false;
        boolean changed = false;

        try {
            Download download = pi.getDownloadManager().getDownload(association_hash);

            if (download != null) {

                if (subscriptions.length > 0) {

                    String category = subscriptions[0].getCategory();

                    if (category != null) {

                        String existing = download.getAttribute(ta_category);

                        if (existing == null) {

                            download.setAttribute(ta_category, category);
                        }
                    }
                }

                download_found = true;

                Map map = download.getMapAttribute(ta_subscription_info);

                if (map == null) {

                    map = new LightHashMap();

                } else {

                    map = new LightHashMap(map);
                }

                List s = (List) map.get("s");

                for (int i = 0; i < subscriptions.length; i++) {

                    byte[] sid = subscriptions[i].getShortID();

                    if (s == null) {

                        s = new ArrayList();

                        s.add(sid);

                        changed = true;

                        map.put("s", s);

                    } else {

                        boolean found = false;

                        for (int j = 0; j < s.size(); j++) {

                            byte[] existing = (byte[]) s.get(j);

                            if (Arrays.equals(sid, existing)) {

                                found = true;

                                break;
                            }
                        }

                        if (!found) {

                            s.add(sid);

                            changed = true;
                        }
                    }
                }

                if (full_lookup) {

                    map.put("lc", new Long(SystemTime.getCurrentTime()));

                    changed = true;
                }

                if (changed) {

                    download.setMapAttribute(ta_subscription_info, map);
                }
            }
        } catch (Throwable e) {

            log("Failed to record associations", e);
        }

        if (changed) {

            Iterator it = listeners.iterator();

            while (it.hasNext()) {

                try {
                    ((SubscriptionManagerListener) it.next()).associationsChanged(association_hash);

                } catch (Throwable e) {

                    Debug.printStackTrace(e);
                }
            }
        }

        return (download_found);
    }

    protected boolean publishAssociations() {
        SubscriptionImpl subs_to_publish = null;
        SubscriptionImpl.association assoc_to_publish = null;

        synchronized (this) {

            if (publish_associations_active >= PUB_ASSOC_CONC_MAX) {

                return (false);
            }

            publish_associations_active++;

            List shuffled_subs = new ArrayList(subscriptions);

            Collections.shuffle(shuffled_subs);

            for (int i = 0; i < shuffled_subs.size(); i++) {

                SubscriptionImpl sub = (SubscriptionImpl) shuffled_subs.get(i);

                if (sub.isSubscribed() && sub.isPublic()) {

                    assoc_to_publish = sub.getAssociationForPublish();

                    if (assoc_to_publish != null) {

                        subs_to_publish = sub;

                        break;
                    }
                }
            }
        }

        if (assoc_to_publish != null) {

            publishAssociation(subs_to_publish, assoc_to_publish);

            return (false);
        } else {

            log("Publishing Associations Complete");

            synchronized (this) {

                publish_associations_active--;
            }

            return (true);
        }
    }

    protected void publishAssociation(final SubscriptionImpl subs, final SubscriptionImpl.association assoc) {
        log("Checking association '" + subs.getString() + "' -> '" + assoc.getString() + "'");

        byte[] sub_id = subs.getShortID();
        int sub_version = subs.getVersion();

        byte[] assoc_hash = assoc.getHash();

        final String key = "subscription:assoc:" + ByteFormatter.encodeString(assoc_hash);

        final byte[] put_value = new byte[sub_id.length + 4];

        System.arraycopy(sub_id, 0, put_value, 4, sub_id.length);

        put_value[0] = (byte) (sub_version >> 16);
        put_value[1] = (byte) (sub_version >> 8);
        put_value[2] = (byte) sub_version;
        put_value[3] = (byte) subs.getFixedRandom();

        dht_plugin.get(key.getBytes(), "Subscription association read: " + ByteFormatter.encodeString(assoc_hash),
                DHTPlugin.FLAG_SINGLE_VALUE, 30, 60 * 1000, false, false, new DHTPluginOperationListener() {
                    private int hits;
                    private boolean diversified;
                    private int max_ver;

                    public void diversified() {
                        diversified = true;
                    }

                    public void starts(byte[] key) {
                    }

                    public void valueRead(DHTPluginContact originator, DHTPluginValue value) {
                        byte[] val = value.getValue();

                        if (val.length == put_value.length) {

                            boolean diff = false;

                            for (int i = 4; i < val.length; i++) {

                                if (val[i] != put_value[i]) {

                                    diff = true;

                                    break;
                                }
                            }

                            if (!diff) {

                                hits++;

                                int ver = ((val[0] << 16) & 0xff0000) | ((val[1] << 8) & 0xff00) | (val[2] & 0xff);

                                if (ver > max_ver) {

                                    max_ver = ver;
                                }
                            }
                        }
                    }

                    public void valueWritten(DHTPluginContact target, DHTPluginValue value) {
                    }

                    public void complete(byte[] original_key, boolean timeout_occurred) {
                        log("Checked association '" + subs.getString() + "' -> '" + assoc.getString()
                                + "' - max_ver=" + max_ver + ",hits=" + hits + ",div=" + diversified);

                        if (max_ver > subs.getVersion()) {

                            if (!subs.isMine()) {

                                updateSubscription(subs, max_ver);
                            }
                        }

                        if (hits < 10 && !diversified) {

                            log("    Publishing association '" + subs.getString() + "' -> '" + assoc.getString()
                                    + "', existing=" + hits);

                            byte flags = DHTPlugin.FLAG_ANON;

                            if (hits < 3 && !diversified) {

                                flags |= DHTPlugin.FLAG_PRECIOUS;
                            }

                            dht_plugin.put(key.getBytes(),
                                    "Subscription association write: " + ByteFormatter.encodeString(assoc.getHash())
                                            + " -> " + ByteFormatter.encodeString(subs.getShortID()) + ":"
                                            + subs.getVersion(),
                                    put_value, flags, new DHTPluginOperationListener() {
                                        public void diversified() {
                                        }

                                        public void starts(byte[] key) {
                                        }

                                        public void valueRead(DHTPluginContact originator, DHTPluginValue value) {
                                        }

                                        public void valueWritten(DHTPluginContact target, DHTPluginValue value) {
                                        }

                                        public void complete(byte[] key, boolean timeout_occurred) {
                                            log("        completed '" + subs.getString() + "' -> '"
                                                    + assoc.getString() + "'");

                                            publishNext();
                                        }
                                    });
                        } else {

                            log("    Not publishing association '" + subs.getString() + "' -> '" + assoc.getString()
                                    + "', existing =" + hits);

                            publishNext();
                        }
                    }

                    protected void publishNext() {
                        synchronized (this) {

                            publish_associations_active--;
                        }

                        publishAssociations();
                    }
                });
    }

    protected void subscriptionUpdated() {
        if (dht_plugin != null) {

            publishSubscriptions();
        }
    }

    protected void publishSubscriptions() {
        List shuffled_subs;

        synchronized (this) {

            if (publish_subscription_active) {

                return;
            }

            shuffled_subs = new ArrayList(subscriptions);

            publish_subscription_active = true;
        }

        boolean publish_initiated = false;

        try {
            Collections.shuffle(shuffled_subs);

            for (int i = 0; i < shuffled_subs.size(); i++) {

                SubscriptionImpl sub = (SubscriptionImpl) shuffled_subs.get(i);

                if (sub.isSubscribed() && sub.isPublic() && !sub.getPublished()) {

                    sub.setPublished(true);

                    publishSubscription(sub);

                    publish_initiated = true;

                    break;
                }
            }
        } finally {

            if (!publish_initiated) {

                log("Publishing Subscriptions Complete");

                synchronized (this) {

                    publish_subscription_active = false;
                }
            }
        }
    }

    protected void publishSubscription(final SubscriptionImpl subs) {
        log("Checking subscription publication '" + subs.getString() + "'");

        byte[] sub_id = subs.getShortID();
        int sub_version = subs.getVersion();

        final String key = "subscription:publish:" + ByteFormatter.encodeString(sub_id) + ":" + sub_version;

        dht_plugin.get(key.getBytes(),
                "Subscription presence read: " + ByteFormatter.encodeString(sub_id) + ":" + sub_version,
                DHTPlugin.FLAG_SINGLE_VALUE, 24, 60 * 1000, false, false, new DHTPluginOperationListener() {
                    private int hits;
                    private boolean diversified;

                    public void diversified() {
                        diversified = true;
                    }

                    public void starts(byte[] key) {
                    }

                    public void valueRead(DHTPluginContact originator, DHTPluginValue value) {
                        byte[] data = value.getValue();

                        try {
                            Map details = decodeSubscriptionDetails(data);

                            if (subs.getVerifiedPublicationVersion(details) == subs.getVersion()) {

                                hits++;
                            }
                        } catch (Throwable e) {

                        }
                    }

                    public void valueWritten(DHTPluginContact target, DHTPluginValue value) {
                    }

                    public void complete(byte[] original_key, boolean timeout_occurred) {
                        log("Checked subscription publication '" + subs.getString() + "' - hits=" + hits + ",div="
                                + diversified);

                        if (hits < 10 && !diversified) {

                            log("    Publishing subscription '" + subs.getString() + ", existing=" + hits);

                            try {
                                byte[] put_value = encodeSubscriptionDetails(subs);

                                if (put_value.length < DHTPlugin.MAX_VALUE_SIZE) {

                                    byte flags = DHTPlugin.FLAG_SINGLE_VALUE;

                                    if (hits < 3 && !diversified) {

                                        flags |= DHTPlugin.FLAG_PRECIOUS;
                                    }

                                    dht_plugin.put(key.getBytes(),
                                            "Subscription presence write: "
                                                    + ByteFormatter.encodeString(subs.getShortID()) + ":"
                                                    + subs.getVersion(),
                                            put_value, flags, new DHTPluginOperationListener() {
                                                public void diversified() {
                                                }

                                                public void starts(byte[] key) {
                                                }

                                                public void valueRead(DHTPluginContact originator,
                                                        DHTPluginValue value) {
                                                }

                                                public void valueWritten(DHTPluginContact target,
                                                        DHTPluginValue value) {
                                                }

                                                public void complete(byte[] key, boolean timeout_occurred) {
                                                    log("        completed '" + subs.getString() + "'");

                                                    publishNext();
                                                }
                                            });

                                } else {

                                    publishNext();
                                }
                            } catch (Throwable e) {

                                Debug.printStackTrace(e);

                                publishNext();
                            }

                        } else {

                            log("    Not publishing subscription '" + subs.getString() + "', existing =" + hits);

                            publishNext();
                        }
                    }

                    protected void publishNext() {
                        synchronized (this) {

                            publish_subscription_active = false;
                        }

                        publishSubscriptions();
                    }
                });
    }

    protected void updateSubscription(final SubscriptionImpl subs, final int new_version) {
        log("Subscription " + subs.getString() + " - higher version found: " + new_version);

        if (!subs.canAutoUpgradeCheck()) {

            log("    Checked too recently or not updateable, ignoring");

            return;
        }

        if (subs.getHighestUserPromptedVersion() >= new_version) {

            log("    User has already been prompted for version " + new_version + " so ignoring");

            return;
        }

        byte[] sub_id = subs.getShortID();

        try {
            PlatformSubscriptionsMessenger.subscriptionDetails details = PlatformSubscriptionsMessenger
                    .getSubscriptionBySID(sub_id);

            if (!askIfCanUpgrade(subs, new_version)) {

                return;
            }

            VuzeFileHandler vfh = VuzeFileHandler.getSingleton();

            VuzeFile vf = vfh.loadVuzeFile(Base64.decode(details.getContent()));

            vfh.handleFiles(new VuzeFile[] { vf }, VuzeFileComponent.COMP_TYPE_SUBSCRIPTION);

            return;

        } catch (Throwable e) {

            log("Failed to read subscription from platform, trying DHT");
        }

        log("Checking subscription '" + subs.getString() + "' upgrade to version " + new_version);

        final String key = "subscription:publish:" + ByteFormatter.encodeString(sub_id) + ":" + new_version;

        dht_plugin.get(key.getBytes(),
                "Subscription update read: " + ByteFormatter.encodeString(sub_id) + ":" + new_version,
                DHTPlugin.FLAG_SINGLE_VALUE, 12, 60 * 1000, false, false, new DHTPluginOperationListener() {
                    private byte[] verified_hash;
                    private int verified_size;

                    public void diversified() {
                    }

                    public void starts(byte[] key) {
                    }

                    public void valueRead(DHTPluginContact originator, DHTPluginValue value) {
                        byte[] data = value.getValue();

                        try {
                            Map details = decodeSubscriptionDetails(data);

                            if (verified_hash == null
                                    && subs.getVerifiedPublicationVersion(details) == new_version) {

                                verified_hash = SubscriptionImpl.getPublicationHash(details);
                                verified_size = SubscriptionImpl.getPublicationSize(details);
                            }

                        } catch (Throwable e) {

                        }
                    }

                    public void valueWritten(DHTPluginContact target, DHTPluginValue value) {
                    }

                    public void complete(byte[] original_key, boolean timeout_occurred) {
                        if (verified_hash != null) {

                            log("    Subscription '" + subs.getString() + " upgrade verified as authentic");

                            updateSubscription(subs, new_version, verified_hash, verified_size);

                        } else {

                            log("    Subscription '" + subs.getString() + " upgrade not verified");
                        }
                    }
                });
    }

    protected byte[] encodeSubscriptionDetails(SubscriptionImpl subs)

            throws IOException {
        Map details = subs.getPublicationDetails();

        // inject a random element so we can count occurrences properly (as the DHT logic
        // removes duplicates)

        details.put("!", new Long(random_seed));

        byte[] encoded = BEncoder.encode(details);

        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        GZIPOutputStream os = new GZIPOutputStream(baos);

        os.write(encoded);

        os.close();

        byte[] compressed = baos.toByteArray();

        byte header;
        byte[] data;

        if (compressed.length < encoded.length) {

            header = 1;
            data = compressed;
        } else {

            header = 0;
            data = encoded;
        }

        byte[] result = new byte[data.length + 1];

        result[0] = header;

        System.arraycopy(data, 0, result, 1, data.length);

        return (result);
    }

    protected Map decodeSubscriptionDetails(byte[] data)

            throws IOException {
        byte[] to_decode;

        if (data[0] == 0) {

            to_decode = new byte[data.length - 1];

            System.arraycopy(data, 1, to_decode, 0, data.length - 1);

        } else {

            GZIPInputStream is = new GZIPInputStream(new ByteArrayInputStream(data, 1, data.length - 1));

            to_decode = FileUtil.readInputStreamAsByteArray(is);

            is.close();
        }

        Map res = BDecoder.decode(to_decode);

        // remove any injected random seed

        res.remove("!");

        return (res);
    }

    protected void updateSubscription(final SubscriptionImpl subs, final int update_version,
            final byte[] update_hash, final int update_size) {
        log("Subscription " + subs.getString() + " - update hash=" + ByteFormatter.encodeString(update_hash)
                + ", size=" + update_size);

        new AEThread2("SubsUpdate", true) {
            public void run() {
                try {
                    Object[] res = downloadTorrent(update_hash, update_size);

                    if (res != null) {

                        updateSubscription(subs, update_version, (TOTorrent) res[0], (InetSocketAddress) res[1]);
                    }
                } catch (Throwable e) {

                    log("    update failed", e);
                }
            }
        }.start();
    }

    protected Object[] downloadTorrent(byte[] hash, int update_size) {
        if (!isSubsDownloadEnabled()) {

            log("    Can't download subscription " + Base32.encode(hash) + " as feature disabled");

            return (null);
        }

        final MagnetPlugin magnet_plugin = getMagnetPlugin();

        if (magnet_plugin == null) {

            log("    Can't download, no magnet plugin");

            return (null);
        }

        try {
            final InetSocketAddress[] sender = { null };

            byte[] torrent_data = magnet_plugin.download(new MagnetPluginProgressListener() {
                public void reportSize(long size) {
                }

                public void reportActivity(String str) {
                    log("    MagnetDownload: " + str);
                }

                public void reportCompleteness(int percent) {
                }

                public void reportContributor(InetSocketAddress address) {
                    synchronized (sender) {

                        sender[0] = address;
                    }
                }

                public boolean verbose() {
                    return (false);
                }
            }, hash, "", new InetSocketAddress[0], 300 * 1000);

            if (torrent_data == null) {

                log("    download failed - timeout");

                return (null);
            }

            log("Subscription torrent downloaded");

            TOTorrent torrent = TOTorrentFactory.deserialiseFromBEncodedByteArray(torrent_data);

            // update size is just that of signed content, torrent itself is .vuze file
            // so take this into account

            if (torrent.getSize() > update_size + 10 * 1024) {

                log("Subscription download abandoned, torrent size is " + torrent.getSize()
                        + ", underlying data size is " + update_size);

                return (null);
            }

            if (torrent.getSize() > 4 * 1024 * 1024) {

                log("Subscription download abandoned, torrent size is too large (" + torrent.getSize() + ")");

                return (null);
            }

            synchronized (sender) {

                return (new Object[] { torrent, sender[0] });
            }

        } catch (Throwable e) {

            log("    download failed", e);

            return (null);
        }
    }

    protected void downloadSubscription(final TOTorrent torrent, final InetSocketAddress peer, byte[] subs_id,
            int version, String name, final downloadListener listener) {
        try {
            // testing purposes, see if local exists

            LightWeightSeed lws = LightWeightSeedManager.getSingleton().get(new HashWrapper(torrent.getHash()));

            if (lws != null) {

                log("Light weight seed found");

                listener.complete(lws.getDataLocation());

            } else {
                String sid = ByteFormatter.encodeString(subs_id);

                File dir = getSubsDir();

                dir = new File(dir, "temp");

                if (!dir.exists()) {

                    if (!dir.mkdirs()) {

                        throw (new IOException("Failed to create dir '" + dir + "'"));
                    }
                }

                final File torrent_file = new File(dir, sid + "_" + version + ".torrent");
                final File data_file = new File(dir, sid + "_" + version + ".vuze");

                PluginInterface pi = PluginInitializer.getDefaultInterface();

                final DownloadManager dm = pi.getDownloadManager();

                Download download = dm.getDownload(torrent.getHash());

                if (download == null) {

                    log("Adding download for subscription '" + new String(torrent.getName()) + "'");

                    boolean is_update = getSubscriptionFromSID(subs_id) != null;

                    PlatformTorrentUtils.setContentTitle(torrent,
                            (is_update ? "Update" : "Download") + " for subscription '" + name + "'");

                    // PlatformTorrentUtils.setContentThumbnail(torrent, thumbnail);

                    TorrentUtils.setFlag(torrent, TorrentUtils.TORRENT_FLAG_LOW_NOISE, true);

                    Torrent t = new TorrentImpl(torrent);

                    t.setDefaultEncoding();

                    t.writeToFile(torrent_file);

                    download = dm.addDownload(t, torrent_file, data_file);

                    download.setFlag(Download.FLAG_DISABLE_AUTO_FILE_MOVE, true);

                    download.setBooleanAttribute(ta_subs_download, true);

                    Map rd = listener.getRecoveryData();

                    if (rd != null) {

                        download.setMapAttribute(ta_subs_download_rd, rd);
                    }
                } else {

                    log("Existing download found for subscription '" + new String(torrent.getName()) + "'");
                }

                final Download f_download = download;

                final TimerEventPeriodic[] event = { null };

                event[0] = SimpleTimer.addPeriodicEvent("SM:cancelTimer", 10 * 1000, new TimerEventPerformer() {
                    private long start_time = SystemTime.getMonotonousTime();

                    public void perform(TimerEvent ev) {
                        boolean kill = false;

                        try {
                            Download download = dm.getDownload(torrent.getHash());

                            if (listener.isCancelled() || download == null) {

                                kill = true;

                            } else {

                                int state = download.getState();

                                if (state == Download.ST_ERROR) {

                                    log("Download entered error state, removing");

                                    kill = true;

                                } else {

                                    long now = SystemTime.getMonotonousTime();

                                    long running_for = now - start_time;

                                    if (running_for > 2 * 60 * 1000) {

                                        DownloadScrapeResult scrape = download.getLastScrapeResult();

                                        if (scrape == null || scrape.getSeedCount() <= 0) {

                                            log("Download has no seeds, removing");

                                            kill = true;
                                        }
                                    } else if (running_for > 4 * 60 * 1000) {

                                        if (download.getStats().getDownloaded() == 0) {

                                            log("Download has zero downloaded, removing");

                                            kill = true;
                                        }
                                    } else if (running_for > 10 * 60 * 1000) {

                                        log("Download hasn't completed in permitted time, removing");

                                        kill = true;
                                    }
                                }
                            }
                        } catch (Throwable e) {

                            log("Download failed", e);

                            kill = true;
                        }

                        if (kill && event[0] != null) {

                            try {
                                event[0].cancel();

                                if (!listener.isCancelled()) {

                                    listener.failed(new SubscriptionException("Download abandoned"));
                                }
                            } finally {

                                removeDownload(f_download, true);

                                torrent_file.delete();
                            }
                        }
                    }
                });

                download.addCompletionListener(new DownloadCompletionListener() {
                    public void onCompletion(Download d) {
                        listener.complete(d, torrent_file);
                    }
                });

                if (download.isComplete()) {

                    listener.complete(download, torrent_file);

                } else {

                    download.setForceStart(true);

                    if (peer != null) {

                        download.addPeerListener(new DownloadPeerListener() {
                            public void peerManagerAdded(Download download, PeerManager peer_manager) {
                                InetSocketAddress tcp = AddressUtils.adjustTCPAddress(peer, true);
                                InetSocketAddress udp = AddressUtils.adjustUDPAddress(peer, true);

                                log("    Injecting peer into download: " + tcp);

                                peer_manager.addPeer(tcp.getAddress().getHostAddress(), tcp.getPort(),
                                        udp.getPort(), true);
                            }

                            public void peerManagerRemoved(Download download, PeerManager peer_manager) {
                            }
                        });
                    }
                }
            }
        } catch (Throwable e) {

            log("Failed to add download", e);

            listener.failed(e);
        }
    }

    protected interface downloadListener {
        public void complete(File data_file);

        public void complete(Download download, File torrent_file);

        public void failed(Throwable error);

        public Map getRecoveryData();

        public boolean isCancelled();
    }

    protected void updateSubscription(final SubscriptionImpl subs, final int new_version, TOTorrent torrent,
            InetSocketAddress peer) {
        log("Subscription " + subs.getString() + " - update torrent: " + new String(torrent.getName()));

        if (!askIfCanUpgrade(subs, new_version)) {

            return;
        }

        downloadSubscription(torrent, peer, subs.getShortID(), new_version, subs.getName(), new downloadListener() {
            public void complete(File data_file) {
                updateSubscription(subs, data_file);
            }

            public void complete(Download download, File torrent_file) {
                updateSubscription(subs, download, torrent_file, new File(download.getSavePath()));
            }

            public void failed(Throwable error) {
                log("Failed to download subscription", error);
            }

            public Map getRecoveryData() {
                Map rd = new HashMap();

                rd.put("sid", subs.getShortID());
                rd.put("ver", new Long(new_version));

                return (rd);
            }

            public boolean isCancelled() {
                return (false);
            }
        });
    }

    protected boolean askIfCanUpgrade(SubscriptionImpl subs, int new_version) {
        subs.setHighestUserPromptedVersion(new_version);

        UIManager ui_manager = StaticUtilities.getUIManager(120 * 1000);

        String details = MessageText.getString("subscript.add.upgradeto.desc",
                new String[] { String.valueOf(new_version), subs.getName() });

        long res = ui_manager.showMessageBox("subscript.add.upgrade.title", "!" + details + "!",
                UIManagerEvent.MT_YES | UIManagerEvent.MT_NO);

        if (res != UIManagerEvent.MT_YES) {

            log("    User declined upgrade");

            return (false);
        }

        return (true);
    }

    protected boolean recoverSubscriptionUpdate(Download download, final Map rd) {
        byte[] sid = (byte[]) rd.get("sid");
        int version = ((Long) rd.get("ver")).intValue();

        final SubscriptionImpl subs = getSubscriptionFromSID(sid);

        if (subs == null) {

            log("Can't recover '" + download.getName() + "' - subscription " + ByteFormatter.encodeString(sid)
                    + " not found");

            return (false);
        }

        downloadSubscription(((TorrentImpl) download.getTorrent()).getTorrent(), null, subs.getShortID(), version,
                subs.getName(), new downloadListener() {
                    public void complete(File data_file) {
                        updateSubscription(subs, data_file);
                    }

                    public void complete(Download download, File torrent_file) {
                        updateSubscription(subs, download, torrent_file, new File(download.getSavePath()));
                    }

                    public void failed(Throwable error) {
                        log("Failed to download subscription", error);
                    }

                    public Map getRecoveryData() {
                        return (rd);
                    }

                    public boolean isCancelled() {
                        return (false);
                    }
                });

        return (true);
    }

    protected void updateSubscription(SubscriptionImpl subs, Download download, File torrent_file, File data_file) {
        try {
            removeDownload(download, false);

            try {
                updateSubscription(subs, data_file);

            } finally {

                if (!data_file.delete()) {

                    log("Failed to delete update file '" + data_file + "'");
                }

                if (!torrent_file.delete()) {

                    log("Failed to delete update torrent '" + torrent_file + "'");
                }
            }
        } catch (Throwable e) {

            log("Failed to remove update download", e);
        }
    }

    protected void removeDownload(Download download, boolean remove_data) {
        try {
            download.stop();

        } catch (Throwable e) {
        }

        try {
            download.remove(true, remove_data);

            log("Removed download '" + download.getName() + "'");

        } catch (Throwable e) {

            log("Failed to remove download '" + download.getName() + "'", e);
        }
    }

    protected void updateSubscription(SubscriptionImpl subs, File data_location) {
        log("Updating subscription '" + subs.getString() + " using '" + data_location + "'");

        VuzeFileHandler vfh = VuzeFileHandler.getSingleton();

        VuzeFile vf = vfh.loadVuzeFile(data_location.getAbsolutePath());

        vfh.handleFiles(new VuzeFile[] { vf }, VuzeFileComponent.COMP_TYPE_SUBSCRIPTION);
    }

    protected MagnetPlugin getMagnetPlugin() {
        PluginInterface pi = AzureusCoreFactory.getSingleton().getPluginManager()
                .getPluginInterfaceByClass(MagnetPlugin.class);

        if (pi == null) {

            return (null);
        }

        return ((MagnetPlugin) pi.getPlugin());
    }

    protected Engine getEngine(SubscriptionImpl subs, Map json_map, boolean local_only)

            throws SubscriptionException {
        long id = ((Long) json_map.get("engine_id")).longValue();

        Engine engine = MetaSearchManagerFactory.getSingleton().getMetaSearch().getEngine(id);

        if (engine != null) {

            return (engine);
        }

        if (!local_only) {

            try {
                if (id >= 0 && id < Integer.MAX_VALUE) {

                    log("Engine " + id + " not present, loading");

                    // vuze template but user hasn't yet loaded it

                    try {
                        engine = MetaSearchManagerFactory.getSingleton().getMetaSearch().addEngine(id);

                        return (engine);

                    } catch (Throwable e) {

                        throw (new SubscriptionException("Failed to load engine '" + id + "'", e));
                    }
                }
            } catch (Throwable e) {

                log("Failed to load search template", e);
            }
        }

        engine = subs.extractEngine(json_map, id);

        if (engine != null) {

            return (engine);
        }

        throw (new SubscriptionException("Failed to extract engine id " + id));
    }

    protected SubscriptionResultImpl[] loadResults(SubscriptionImpl subs) {
        List results = new ArrayList();

        try {
            File f = getResultsFile(subs);

            Map map = FileUtil.readResilientFile(f);

            List list = (List) map.get("results");

            if (list != null) {

                SubscriptionHistoryImpl history = (SubscriptionHistoryImpl) subs.getHistory();

                for (int i = 0; i < list.size(); i++) {

                    Map result_map = (Map) list.get(i);

                    try {
                        SubscriptionResultImpl result = new SubscriptionResultImpl(history, result_map);

                        results.add(result);

                    } catch (Throwable e) {

                        log("Failed to decode result '" + result_map + "'", e);
                    }
                }
            }

        } catch (Throwable e) {

            log("Failed to load results for '" + subs.getName() + "' - continuing with empty result set", e);
        }

        return ((SubscriptionResultImpl[]) results.toArray(new SubscriptionResultImpl[results.size()]));
    }

    protected void setCategoryOnExisting(SubscriptionImpl subscription, String old_category, String new_category) {
        PluginInterface default_pi = PluginInitializer.getDefaultInterface();

        Download[] downloads = default_pi.getDownloadManager().getDownloads();

        for (Download d : downloads) {

            if (subscriptionExists(d, subscription)) {

                String existing = d.getAttribute(ta_category);

                if (existing == null || existing.equals(old_category)) {

                    d.setAttribute(ta_category, new_category);
                }
            }
        }
    }

    public int getMaxNonDeletedResults() {
        return (COConfigurationManager.getIntParameter(CONFIG_MAX_RESULTS));
    }

    public void setMaxNonDeletedResults(int max) {
        if (max != getMaxNonDeletedResults()) {

            COConfigurationManager.setParameter(CONFIG_MAX_RESULTS, max);
        }
    }

    public boolean getAutoStartDownloads() {
        return (COConfigurationManager.getBooleanParameter(CONFIG_AUTO_START_DLS));
    }

    public void setAutoStartDownloads(boolean auto_start) {
        if (auto_start != getAutoStartDownloads()) {

            COConfigurationManager.setParameter(CONFIG_AUTO_START_DLS, auto_start);
        }
    }

    public int getAutoStartMinMB() {
        return (COConfigurationManager.getIntParameter(CONFIG_AUTO_START_MIN_MB));
    }

    public void setAutoStartMinMB(int mb) {
        if (mb != getAutoStartMinMB()) {

            COConfigurationManager.setParameter(CONFIG_AUTO_START_MIN_MB, mb);
        }
    }

    public int getAutoStartMaxMB() {
        return (COConfigurationManager.getIntParameter(CONFIG_AUTO_START_MAX_MB));
    }

    public void setAutoStartMaxMB(int mb) {
        if (mb != getAutoStartMaxMB()) {

            COConfigurationManager.setParameter(CONFIG_AUTO_START_MAX_MB, mb);
        }
    }

    protected boolean shouldAutoStart(Torrent torrent) {
        if (getAutoStartDownloads()) {

            long min = getAutoStartMinMB() * 1024 * 1024L;
            long max = getAutoStartMaxMB() * 1024 * 1024L;

            if (min <= 0 && max <= 0) {

                return (true);
            }

            long size = torrent.getSize();

            if (min > 0 && size < min) {

                return (false);
            }

            if (max > 0 && size > max) {

                return (false);
            }

            return (true);

        } else {

            return (false);
        }
    }

    protected void saveResults(SubscriptionImpl subs, SubscriptionResultImpl[] results) {
        try {
            File f = getResultsFile(subs);

            Map map = new HashMap();

            List list = new ArrayList(results.length);

            map.put("results", list);

            for (int i = 0; i < results.length; i++) {

                list.add(results[i].toBEncodedMap());
            }

            FileUtil.writeResilientFile(f, map);

        } catch (Throwable e) {

            log("Failed to save results for '" + subs.getName(), e);
        }
    }

    private void loadConfig() {
        if (!FileUtil.resilientConfigFileExists(CONFIG_FILE)) {

            return;
        }

        log("Loading configuration");

        boolean some_are_mine = false;

        synchronized (this) {

            Map map = FileUtil.readResilientConfigFile(CONFIG_FILE);

            List l_subs = (List) map.get("subs");

            if (l_subs != null) {

                for (int i = 0; i < l_subs.size(); i++) {

                    Map m = (Map) l_subs.get(i);

                    try {
                        SubscriptionImpl sub = new SubscriptionImpl(this, m);

                        int index = Collections.binarySearch(subscriptions, sub, new Comparator<Subscription>() {
                            public int compare(Subscription arg0, Subscription arg1) {
                                return arg0.getID().compareTo(arg1.getID());
                            }
                        });
                        if (index < 0) {
                            index = -1 * index - 1; // best guess

                            subscriptions.add(index, sub);
                        }

                        if (sub.isMine()) {

                            some_are_mine = true;
                        }

                        log("    loaded " + sub.getString());

                    } catch (Throwable e) {

                        log("Failed to import subscription from " + m, e);
                    }
                }
            }
        }

        if (some_are_mine) {

            addMetaSearchListener();
        }
    }

    protected void configDirty(SubscriptionImpl subs) {
        changeSubscription(subs);

        configDirty();
    }

    protected void configDirty() {
        synchronized (this) {

            if (config_dirty) {

                return;
            }

            config_dirty = true;

            new DelayedEvent("Subscriptions:save", 5000, new AERunnable() {
                public void runSupport() {
                    synchronized (this) {

                        if (!config_dirty) {

                            return;
                        }

                        saveConfig();
                    }
                }
            });
        }
    }

    protected void saveConfig() {
        log("Saving configuration");

        synchronized (this) {

            config_dirty = false;

            if (subscriptions.size() == 0) {

                FileUtil.deleteResilientConfigFile(CONFIG_FILE);

            } else {

                Map map = new HashMap();

                List l_subs = new ArrayList();

                map.put("subs", l_subs);

                Iterator it = subscriptions.iterator();

                while (it.hasNext()) {

                    SubscriptionImpl sub = (SubscriptionImpl) it.next();

                    try {
                        l_subs.add(sub.toMap());

                    } catch (Throwable e) {

                        log("Failed to save subscription " + sub.getString(), e);
                    }
                }

                FileUtil.writeResilientConfigFile(CONFIG_FILE, map);
            }
        }
    }

    protected synchronized AEDiagnosticsLogger getLogger() {
        if (logger == null) {

            logger = AEDiagnostics.getLogger(LOGGER_NAME);
        }

        return (logger);
    }

    public void log(String s, Throwable e) {
        AEDiagnosticsLogger diag_logger = getLogger();

        diag_logger.log(s);
        diag_logger.log(e);
    }

    public void log(String s) {
        AEDiagnosticsLogger diag_logger = getLogger();

        diag_logger.log(s);
    }

    public void addListener(SubscriptionManagerListener listener) {
        listeners.add(listener);
    }

    public void removeListener(SubscriptionManagerListener listener) {
        listeners.remove(listener);
    }

    public void generate(IndentWriter writer) {
        writer.println("Subscriptions");

        try {
            writer.indent();

            Subscription[] subs = getSubscriptions();

            for (int i = 0; i < subs.length; i++) {

                SubscriptionImpl sub = (SubscriptionImpl) subs[i];

                sub.generate(writer);
            }

        } finally {

            writer.exdent();
        }
    }

    private class searchMatcher {
        private String[] bits;
        private int[] bit_types;
        private Pattern[] bit_patterns;

        protected searchMatcher(String term) {
            bits = Constants.PAT_SPLIT_SPACE.split(term.toLowerCase());

            bit_types = new int[bits.length];
            bit_patterns = new Pattern[bits.length];

            for (int i = 0; i < bits.length; i++) {

                String bit = bits[i] = bits[i].trim();

                if (bit.length() > 0) {

                    char c = bit.charAt(0);

                    if (c == '+') {

                        bit_types[i] = 1;

                        bit = bits[i] = bit.substring(1);

                    } else if (c == '-') {

                        bit_types[i] = 2;

                        bit = bits[i] = bit.substring(1);
                    }

                    if (bit.startsWith("(") && bit.endsWith((")"))) {

                        bit = bit.substring(1, bit.length() - 1);

                        try {
                            bit_patterns[i] = Pattern.compile(bit, Pattern.CASE_INSENSITIVE);

                        } catch (Throwable e) {
                        }
                    } else if (bit.contains("|")) {

                        try {
                            bit_patterns[i] = Pattern.compile(bit, Pattern.CASE_INSENSITIVE);

                        } catch (Throwable e) {
                        }
                    }
                }
            }
        }

        public boolean matches(String str) {
            // term is made up of space separated bits - all bits must match
            // each bit can be prefixed by + or -, a leading - means 'bit doesn't match'. + doesn't mean anything
            // each bit (with prefix removed) can be "(" regexp ")"
            // if bit isn't regexp but has "|" in it it is turned into a regexp so a|b means 'a or b'

            str = str.toLowerCase();

            boolean match = true;
            boolean at_least_one = false;

            for (int i = 0; i < bits.length; i++) {

                String bit = bits[i];

                if (bit.length() > 0) {

                    boolean hit;

                    if (bit_patterns[i] == null) {

                        hit = str.contains(bit);

                    } else {

                        hit = bit_patterns[i].matcher(str).find();
                    }

                    int type = bit_types[i];

                    if (hit) {

                        if (type == 2) {

                            match = false;

                            break;

                        } else {

                            at_least_one = true;

                        }
                    } else {

                        if (type == 2) {

                            at_least_one = true;

                        } else {

                            match = false;

                            break;
                        }
                    }
                }
            }

            boolean res = match && at_least_one;

            return (res);
        }
    }

    public static void main(String[] args) {
        final String NAME = "lalalal";
        final String URL_STR = "http://www.vuze.com/feed/publisher/ALL/1";

        try {
            //AzureusCoreFactory.create();
            /*
            Subscription subs = 
               getSingleton(true).createSingletonRSS(
              NAME,
              new URL( URL_STR ),
              240 );
                
            subs.getVuzeFile().write( new File( "C:\\temp\\srss.vuze" ));
                
            subs.remove();
            */

            VuzeFile vf = VuzeFileHandler.getSingleton().create();

            Map map = new HashMap();

            map.put("name", NAME);
            map.put("url", URL_STR);
            map.put("public", new Long(0));
            map.put("check_interval_mins", new Long(345));

            vf.addComponent(VuzeFileComponent.COMP_TYPE_SUBSCRIPTION_SINGLETON, map);

            vf.write(new File("C:\\temp\\srss_2.vuze"));

        } catch (Throwable e) {

            e.printStackTrace();
        }
    }
}