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

Java tutorial

Introduction

Here is the source code for com.aelitis.azureus.core.subs.impl.SubscriptionImpl.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.File;
import java.io.IOException;
import java.net.URL;
import java.security.KeyPair;
import java.util.*;

import org.bouncycastle.util.encoders.Base64;
import org.gudy.azureus2.core3.internat.MessageText;
import org.gudy.azureus2.core3.torrent.TOTorrent;
import org.gudy.azureus2.core3.torrent.TOTorrentCreator;
import org.gudy.azureus2.core3.torrent.TOTorrentFactory;
import org.gudy.azureus2.core3.util.AEThread2;
import org.gudy.azureus2.core3.util.BDecoder;
import org.gudy.azureus2.core3.util.BEncoder;
import org.gudy.azureus2.core3.util.Base32;
import org.gudy.azureus2.core3.util.ByteFormatter;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.FileUtil;
import org.gudy.azureus2.core3.util.HashWrapper;
import org.gudy.azureus2.core3.util.IndentWriter;
import org.gudy.azureus2.core3.util.LightHashMap;
import org.gudy.azureus2.core3.util.SystemTime;
import org.gudy.azureus2.core3.util.TorrentUtils;
import org.json.simple.JSONObject;

import com.aelitis.azureus.core.lws.LightWeightSeed;
import com.aelitis.azureus.core.lws.LightWeightSeedAdapter;
import com.aelitis.azureus.core.lws.LightWeightSeedManager;
import com.aelitis.azureus.core.metasearch.Engine;
import com.aelitis.azureus.core.metasearch.MetaSearchManagerFactory;
import com.aelitis.azureus.core.security.CryptoECCUtils;
import com.aelitis.azureus.core.subs.Subscription;
import com.aelitis.azureus.core.subs.SubscriptionException;
import com.aelitis.azureus.core.subs.SubscriptionHistory;
import com.aelitis.azureus.core.subs.SubscriptionListener;
import com.aelitis.azureus.core.subs.SubscriptionManager;
import com.aelitis.azureus.core.subs.SubscriptionPopularityListener;
import com.aelitis.azureus.core.subs.SubscriptionResult;
import com.aelitis.azureus.core.util.CopyOnWriteList;
import com.aelitis.azureus.core.vuzefile.VuzeFile;
import com.aelitis.azureus.core.vuzefile.VuzeFileHandler;
import com.aelitis.azureus.util.ImportExportUtils;
import com.aelitis.azureus.util.JSONUtils;

public class SubscriptionImpl implements Subscription {
    public static final int ADD_TYPE_CREATE = 1;
    public static final int ADD_TYPE_IMPORT = 2;
    public static final int ADD_TYPE_LOOKUP = 3;

    private static final int MAX_ASSOCIATIONS = 256;
    private static final int MIN_RECENT_ASSOC_TO_RETAIN = 16;

    //private static final byte[] GENERIC_PUBLIC_KEY       = {(byte)0x04,(byte)0xd0,(byte)0x1a,(byte)0xd9,(byte)0xb9,(byte)0x99,(byte)0xd8,(byte)0x49,(byte)0x15,(byte)0x5f,(byte)0xe9,(byte)0x6b,(byte)0x3c,(byte)0xd8,(byte)0x18,(byte)0x81,(byte)0xf7,(byte)0x92,(byte)0x15,(byte)0x3f,(byte)0x24,(byte)0xaa,(byte)0x35,(byte)0x6f,(byte)0x52,(byte)0x01,(byte)0x79,(byte)0x2e,(byte)0x93,(byte)0xf6,(byte)0xf1,(byte)0x57,(byte)0x13,(byte)0x2a,(byte)0x3c,(byte)0x31,(byte)0x66,(byte)0xa5,(byte)0x34,(byte)0x9f,(byte)0x79,(byte)0x62,(byte)0x04,(byte)0x31,(byte)0x68,(byte)0x37,(byte)0x8f,(byte)0x77,(byte)0x5c};
    // private static final byte[] GENERIC_PRIVATE_KEY    = {(byte)0x71,(byte)0xc3,(byte)0xe8,(byte)0x6c,(byte)0x56,(byte)0xbb,(byte)0x30,(byte)0x14,(byte)0x9e,(byte)0x19,(byte)0xa5,(byte)0x3d,(byte)0xcb,(byte)0x47,(byte)0xbb,(byte)0x6d,(byte)0x57,(byte)0x57,(byte)0xd3,(byte)0x59,(byte)0xce,(byte)0x8f,(byte)0x79,(byte)0xe5};

    protected static byte[] intToBytes(int version) {
        return (new byte[] { (byte) (version >> 24), (byte) (version >> 16), (byte) (version >> 8),
                (byte) version });
    }

    protected static int bytesToInt(byte[] bytes) {
        return ((bytes[0] << 24) & 0xff000000 | (bytes[1] << 16) & 0x00ff0000 | (bytes[2] << 8) & 0x0000ff00
                | bytes[3] & 0x000000ff);
    }

    private SubscriptionManagerImpl manager;

    private byte[] public_key;
    private byte[] private_key;

    private String name;
    private String name_ex;

    private int version;
    private int az_version;

    private boolean is_public;
    private Map singleton_details;

    private byte[] hash;
    private byte[] sig;
    private int sig_data_size;

    private int add_type;
    private long add_time;

    private boolean is_subscribed;

    private int highest_prompted_version;

    private byte[] short_id;

    private String id;

    private List associations = new ArrayList();

    private int fixed_random;

    private long popularity = -1;

    private long last_auto_upgrade_check = -1;
    private boolean published;

    private boolean server_published;
    private boolean server_publication_outstanding;

    private boolean singleton_sp_attempted;

    private LightWeightSeed lws;
    private int lws_skip_check;

    private boolean destroyed;

    private Map history_map;
    private Map schedule_map;

    private Map user_data = new LightHashMap();

    private final SubscriptionHistoryImpl history;

    private String referer;

    private CopyOnWriteList listeners = new CopyOnWriteList();

    private Map verify_cache_details;
    private boolean verify_cache_result;

    private String creator_ref;
    private String category;

    protected static String getSkeletonJSON(Engine engine, int check_interval_mins) {
        JSONObject map = new JSONObject();

        map.put("engine_id", new Long(engine.getId()));

        map.put("search_term", "");

        map.put("filters", new HashMap());

        map.put("options", new HashMap());

        Map schedule = new HashMap();

        schedule.put("interval", new Long(check_interval_mins));

        List days = new ArrayList();

        for (int i = 1; i <= 7; i++) {

            days.add(String.valueOf(i));
        }

        schedule.put("days", days);

        map.put("schedule", schedule);

        embedEngines(map, engine);

        return (JSONUtils.encodeToJSON(map));
    }

    // new subs constructor

    protected SubscriptionImpl(SubscriptionManagerImpl _manager, String _name, boolean _public,
            Map _singleton_details, String _json_content, int _add_type)

            throws SubscriptionException {
        manager = _manager;

        history_map = new HashMap();

        history = new SubscriptionHistoryImpl(manager, this);

        name = _name;
        is_public = _public;
        singleton_details = _singleton_details;

        version = 1;
        az_version = AZ_VERSION;

        add_type = _add_type;
        add_time = SystemTime.getCurrentTime();

        is_subscribed = true;

        try {
            KeyPair kp = CryptoECCUtils.createKeys();

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

            fixed_random = new Random().nextInt();

            init();

            String json_content = embedEngines(_json_content);

            SubscriptionBodyImpl body = new SubscriptionBodyImpl(manager, name, is_public, json_content, public_key,
                    version, az_version, singleton_details);

            syncToBody(body);

        } catch (Throwable e) {

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

    // cache detail constructor

    protected SubscriptionImpl(SubscriptionManagerImpl _manager, Map map)

            throws IOException {
        manager = _manager;

        fromMap(map);

        history = new SubscriptionHistoryImpl(manager, this);

        init();
    }

    // import constructor

    protected SubscriptionImpl(SubscriptionManagerImpl _manager, SubscriptionBodyImpl _body, int _add_type,
            boolean _is_subscribed)

            throws SubscriptionException {
        manager = _manager;

        history_map = new HashMap();

        history = new SubscriptionHistoryImpl(manager, this);

        syncFromBody(_body);

        add_type = _add_type;
        add_time = SystemTime.getCurrentTime();

        is_subscribed = _is_subscribed;

        fixed_random = new Random().nextInt();

        init();

        syncToBody(_body);
    }

    protected void syncFromBody(SubscriptionBodyImpl body)

            throws SubscriptionException {
        public_key = body.getPublicKey();
        version = body.getVersion();
        az_version = body.getAZVersion();

        name = body.getName();
        is_public = body.isPublic();
        singleton_details = body.getSingletonDetails();

        if (az_version > AZ_VERSION) {

            throw (new SubscriptionException(
                    MessageText.getString("subscription.version.bad", new String[] { name })));
        }
    }

    protected void syncToBody(SubscriptionBodyImpl body)

            throws SubscriptionException {
        // this picks up latest values of version, name + is_public from here

        body.writeVuzeFile(this);

        hash = body.getHash();
        sig = body.getSig();
        sig_data_size = body.getSigDataSize();
    }

    protected Map toMap()

            throws IOException {
        synchronized (this) {

            Map map = new HashMap();

            map.put("name", name.getBytes("UTF-8"));

            map.put("public_key", public_key);

            map.put("version", new Long(version));

            map.put("az_version", new Long(az_version));

            map.put("is_public", new Long(is_public ? 1 : 0));

            if (singleton_details != null) {

                map.put("sin_details", singleton_details);
                map.put("spa", new Long(singleton_sp_attempted ? 1 : 0));
            }

            // body data

            map.put("hash", hash);
            map.put("sig", sig);
            map.put("sig_data_size", new Long(sig_data_size));

            // local data

            if (private_key != null) {

                map.put("private_key", private_key);
            }

            map.put("add_type", new Long(add_type));
            map.put("add_time", new Long(add_time));

            map.put("subscribed", new Long(is_subscribed ? 1 : 0));

            map.put("pop", new Long(popularity));

            map.put("rand", new Long(fixed_random));

            map.put("hupv", new Long(highest_prompted_version));

            map.put("sp", new Long(server_published ? 1 : 0));
            map.put("spo", new Long(server_publication_outstanding ? 1 : 0));

            if (associations.size() > 0) {

                List l_assoc = new ArrayList();

                map.put("assoc", l_assoc);

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

                    association assoc = (association) associations.get(i);

                    Map m = new HashMap();

                    l_assoc.add(m);

                    m.put("h", assoc.getHash());
                    m.put("w", new Long(assoc.getWhen()));
                }
            }

            map.put("history", history_map);

            if (creator_ref != null) {

                map.put("cref", creator_ref.getBytes("UTF-8"));
            }

            if (category != null) {

                map.put("cat", category.getBytes("UTF-8"));
            }

            return (map);
        }
    }

    protected void fromMap(Map map)

            throws IOException {
        name = new String((byte[]) map.get("name"), "UTF-8");
        public_key = (byte[]) map.get("public_key");
        private_key = (byte[]) map.get("private_key");
        version = ((Long) map.get("version")).intValue();
        az_version = (int) ImportExportUtils.importLong(map, "az_version", AZ_VERSION);
        is_public = ((Long) map.get("is_public")).intValue() == 1;
        singleton_details = (Map) map.get("sin_details");

        hash = (byte[]) map.get("hash");
        sig = (byte[]) map.get("sig");
        sig_data_size = ((Long) map.get("sig_data_size")).intValue();

        fixed_random = ((Long) map.get("rand")).intValue();

        add_type = ((Long) map.get("add_type")).intValue();
        add_time = ((Long) map.get("add_time")).longValue();

        is_subscribed = ((Long) map.get("subscribed")).intValue() == 1;

        popularity = ((Long) map.get("pop")).longValue();

        highest_prompted_version = ((Long) map.get("hupv")).intValue();

        server_published = ((Long) map.get("sp")).intValue() == 1;
        server_publication_outstanding = ((Long) map.get("spo")).intValue() == 1;

        Long l_spa = (Long) map.get("spa");

        if (l_spa != null) {
            singleton_sp_attempted = l_spa.longValue() == 1;
        }

        List l_assoc = (List) map.get("assoc");

        if (l_assoc != null) {

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

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

                byte[] hash = (byte[]) m.get("h");
                long when = ((Long) m.get("w")).longValue();

                associations.add(new association(hash, when));
            }
        }

        history_map = (Map) map.get("history");

        if (history_map == null) {

            history_map = new HashMap();
        }

        byte[] b_cref = (byte[]) map.get("cref");

        if (b_cref != null) {

            creator_ref = new String(b_cref, "UTF-8");
        }

        byte[] b_cat = (byte[]) map.get("cat");

        if (b_cat != null) {

            category = new String(b_cat, "UTF-8");
        }
    }

    protected Map getScheduleConfig() {
        if (schedule_map == null) {

            try {
                Map map = JSONUtils.decodeJSON(getJSON());

                schedule_map = (Map) map.get("schedule");

                if (schedule_map == null) {

                    schedule_map = new HashMap();
                }
            } catch (Throwable e) {

                log("Failed to load schedule", e);

                schedule_map = new HashMap();
            }
        }

        return (schedule_map);
    }

    protected Map getHistoryConfig() {
        return (history_map);
    }

    protected void updateHistoryConfig(Map _history_map) {
        history_map = _history_map;

        fireChanged();
    }

    protected void upgrade(SubscriptionBodyImpl body)

            throws SubscriptionException {
        // pick up details from the body (excluding json that is maintained in body only)

        syncFromBody(body);

        // write to file

        syncToBody(body);

        fireChanged();
    }

    protected void init() {
        short_id = SubscriptionBodyImpl.deriveShortID(public_key, singleton_details);
        id = null;
    }

    public boolean isSingleton() {
        return (singleton_details != null);
    }

    public boolean isShareable() {
        try {
            return (getEngine().isShareable() && !isSingleton());

        } catch (Throwable e) {

            Debug.printStackTrace(e);

            return (false);
        }
    }

    public boolean isSearchTemplate() {
        return (getName().startsWith("Search Template:"));
    }

    protected Map getSingletonDetails() {
        return (singleton_details);
    }

    protected boolean getSingletonPublishAttempted() {
        return (singleton_sp_attempted);
    }

    protected void setSingletonPublishAttempted() {
        if (!singleton_sp_attempted) {

            singleton_sp_attempted = true;

            manager.configDirty(this);
        }
    }

    public String getName() {
        return (name);
    }

    public void setName(String _name)

            throws SubscriptionException {
        if (!name.equals(_name)) {

            boolean ok = false;

            String old_name = name;
            int old_version = version;

            try {
                name = _name;

                version++;

                SubscriptionBodyImpl body = new SubscriptionBodyImpl(manager, this);

                syncToBody(body);

                versionUpdated(body, false);

                ok = true;

            } finally {

                if (!ok) {

                    name = old_name;
                    version = old_version;
                }
            }

            fireChanged();
        }
    }

    public String getNameEx() {
        if (name_ex == null) {

            try {
                Map map = JSONUtils.decodeJSON(getJSON());

                String search_term = (String) map.get("search_term");
                Map filters = (Map) map.get("filters");

                Engine engine = manager.getEngine(this, map, true);

                String engine_name = engine.getNameEx();

                if (name.startsWith(engine_name)) {

                    name_ex = name;

                } else if (engine_name.startsWith(name)) {

                    name_ex = engine_name;

                } else {

                    name_ex = name + ": " + engine.getNameEx();
                }

                if (search_term != null && search_term.length() > 0) {

                    name_ex += ", query=" + search_term;
                }

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

                    name_ex += ", filters=" + new SubscriptionResultFilter(filters).getString();
                }

            } catch (Throwable e) {

                name_ex = name + ": " + Debug.getNestedExceptionMessage(e);
            }
        }

        return (name_ex);
    }

    public long getAddTime() {
        return (add_time);
    }

    public boolean isPublic() {
        return (is_public);
    }

    public void setPublic(boolean _is_public)

            throws SubscriptionException {
        if (is_public != _is_public) {

            boolean ok = false;

            boolean old_public = is_public;
            int old_version = version;

            try {
                is_public = _is_public;

                version++;

                SubscriptionBodyImpl body = new SubscriptionBodyImpl(manager, this);

                syncToBody(body);

                versionUpdated(body, false);

                ok = true;

            } finally {

                if (!ok) {

                    version = old_version;
                    is_public = old_public;
                }
            }

            fireChanged();
        }
    }

    protected boolean getServerPublicationOutstanding() {
        return (server_publication_outstanding);
    }

    protected void setServerPublicationOutstanding() {
        if (!server_publication_outstanding) {

            server_publication_outstanding = true;

            fireChanged();
        }
    }

    protected void setServerPublished() {
        if (server_publication_outstanding || !server_published) {

            server_published = true;
            server_publication_outstanding = false;

            fireChanged();
        }
    }

    protected boolean getServerPublished() {
        return (server_published);
    }

    public String getJSON()

            throws SubscriptionException {
        try {
            SubscriptionBodyImpl body = new SubscriptionBodyImpl(manager, this);

            return (body.getJSON());

        } catch (Throwable e) {

            history.setFatalError(Debug.getNestedExceptionMessage(e));

            if (e instanceof SubscriptionException) {

                throw ((SubscriptionException) e);
            }

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

    public boolean setJSON(String _json)

            throws SubscriptionException {
        String json = embedEngines(_json);

        SubscriptionBodyImpl body = new SubscriptionBodyImpl(manager, this);

        String old_json = body.getJSON();

        if (!json.equals(old_json)) {

            boolean ok = false;

            int old_version = version;

            try {
                version++;

                body.setJSON(json);

                syncToBody(body);

                versionUpdated(body, true);

                referer = null;

                ok = true;

            } finally {

                if (!ok) {

                    version = old_version;
                }
            }

            fireChanged();

            return (true);
        }

        return (false);
    }

    protected String embedEngines(String json_in) {
        // see if we need to embed private search templates

        Map map = JSONUtils.decodeJSON(json_in);

        long engine_id = ((Long) map.get("engine_id")).longValue();

        String json_out = json_in;

        if (engine_id >= Integer.MAX_VALUE || engine_id < 0) {

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

            if (engine == null) {

                log("Private search template with id '" + engine_id + "' not found!!!!");

            } else {

                try {
                    embedEngines(map, engine);

                    json_out = JSONUtils.encodeToJSON(map);

                    log("Embedded private search template '" + engine.getName() + "'");

                } catch (Throwable e) {

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

        return (json_out);
    }

    protected static void embedEngines(Map map, Engine engine) {
        Map engines = new HashMap();

        map.put("engines", engines);

        Map engine_map = new HashMap();

        try {

            String engine_str = new String(Base64.encode(BEncoder.encode(engine.exportToBencodedMap())), "UTF-8");

            engine_map.put("content", engine_str);

            engines.put(String.valueOf(engine.getId()), engine_map);

        } catch (Throwable e) {

            Debug.out(e);
        }
    }

    protected Engine extractEngine(Map json_map, long id) {
        Map engines = (Map) json_map.get("engines");

        if (engines != null) {

            Map engine_map = (Map) engines.get(String.valueOf(id));

            if (engine_map != null) {

                String engine_str = (String) engine_map.get("content");

                try {

                    Map map = BDecoder.decode(Base64.decode(engine_str.getBytes("UTF-8")));

                    return (MetaSearchManagerFactory.getSingleton().getMetaSearch().importFromBEncodedMap(map));

                } catch (Throwable e) {

                    log("failed to import engine", e);
                }
            }
        }

        return (null);
    }

    public Engine getEngine()

            throws SubscriptionException {
        return (getEngine(true));
    }

    protected Engine getEngine(boolean local_only)

            throws SubscriptionException {
        Map map = JSONUtils.decodeJSON(getJSON());

        return (manager.getEngine(this, map, local_only));
    }

    protected void engineUpdated(Engine engine) {
        try {
            String json = getJSON();

            Map map = JSONUtils.decodeJSON(json);

            long id = ((Long) map.get("engine_id")).longValue();

            if (id == engine.getId()) {

                if (setJSON(json)) {

                    log("Engine has been updated, saved");
                }
            }
        } catch (Throwable e) {

            log("Engine update failed", e);
        }
    }

    public boolean setDetails(String _name, boolean _is_public, String _json)

            throws SubscriptionException {
        _json = embedEngines(_json);

        SubscriptionBodyImpl body = new SubscriptionBodyImpl(manager, this);

        String old_json = body.getJSON();

        boolean json_changed = !_json.equals(old_json);

        if (!_name.equals(name) || _is_public != is_public || json_changed) {

            boolean ok = false;

            String old_name = name;
            boolean old_public = is_public;
            int old_version = version;

            try {
                is_public = _is_public;
                name = _name;

                body.setJSON(_json);

                version++;

                syncToBody(body);

                versionUpdated(body, json_changed);

                ok = true;

            } finally {

                if (!ok) {

                    version = old_version;
                    is_public = old_public;
                    name = old_name;
                }
            }

            fireChanged();

            return (true);
        }

        return (false);
    }

    protected void versionUpdated(SubscriptionBodyImpl body, boolean json_changed) {
        if (json_changed) {

            try {
                Map map = JSONUtils.decodeJSON(body.getJSON());

                schedule_map = (Map) map.get("schedule");

            } catch (Throwable e) {
            }
        }

        name_ex = null;

        if (is_public) {

            manager.updatePublicSubscription(this);

            setPublished(false);

            synchronized (this) {

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

                    ((association) associations.get(i)).setPublished(false);
                }
            }
        }
    }

    public byte[] getPublicKey() {
        return (public_key);
    }

    public byte[] getShortID() {
        return (short_id);
    }

    public String getID() {
        if (id == null) {
            id = Base32.encode(getShortID());
        }
        return (id);
    }

    protected byte[] getPrivateKey() {
        return (private_key);
    }

    protected int getFixedRandom() {
        return (fixed_random);
    }

    public int getVersion() {
        return (version);
    }

    public int getAZVersion() {
        return (az_version);
    }

    protected void setHighestUserPromptedVersion(int v) {
        if (v < version) {

            v = version;
        }

        if (highest_prompted_version != v) {

            highest_prompted_version = v;

            fireChanged();
        }
    }

    protected int getHighestUserPromptedVersion() {
        return (highest_prompted_version);
    }

    public int getHighestVersion() {
        return (Math.max(version, highest_prompted_version));
    }

    public void resetHighestVersion() {
        if (highest_prompted_version > 0) {

            highest_prompted_version = 0;

            fireChanged();

            manager.checkUpgrade(this);
        }
    }

    public boolean isMine() {
        if (private_key == null) {

            return (false);
        }

        if (isSingleton() && add_type != ADD_TYPE_CREATE) {

            return (false);
        }

        return (true);
    }

    public boolean isUpdateable() {
        return (private_key != null);
    }

    public boolean isSubscribed() {
        return (is_subscribed);
    }

    public void setSubscribed(boolean s) {
        if (is_subscribed != s) {

            is_subscribed = s;

            if (is_subscribed) {

                manager.setSelected(this);

            } else {

                reset();
            }

            fireChanged();
        }
    }

    public boolean isAutoDownloadSupported() {
        return (history.isAutoDownloadSupported());
    }

    public void getPopularity(final SubscriptionPopularityListener listener)

            throws SubscriptionException {
        new AEThread2("subs:popwait", true) {
            public void run() {
                try {
                    manager.getPopularity(SubscriptionImpl.this, new SubscriptionPopularityListener() {
                        public void gotPopularity(long pop) {
                            if (pop != popularity) {

                                popularity = pop;

                                fireChanged();
                            }

                            listener.gotPopularity(popularity);
                        }

                        public void failed(SubscriptionException e) {
                            if (popularity == -1) {

                                listener.failed(new SubscriptionException("Failed to read popularity", e));

                            } else {

                                listener.gotPopularity(popularity);
                            }
                        }
                    });

                } catch (Throwable e) {

                    if (popularity == -1) {

                        listener.failed(new SubscriptionException("Failed to read popularity", e));

                    } else {

                        listener.gotPopularity(popularity);
                    }
                }
            }
        }.start();
    }

    public long getCachedPopularity() {
        return (popularity);
    }

    protected void setCachedPopularity(long pop) {
        if (pop != popularity) {

            popularity = pop;

            fireChanged();
        }
    }

    public String getReferer() {
        if (referer == null) {

            try {
                Map map = JSONUtils.decodeJSON(getJSON());

                Engine engine = manager.getEngine(this, map, false);

                if (engine != null) {

                    referer = engine.getReferer();
                }
            } catch (Throwable e) {

                log("Failed to get referer", e);
            }

            if (referer == null) {

                referer = "";
            }
        }

        return (referer);
    }

    protected void checkPublish() {
        synchronized (this) {

            if (destroyed) {

                return;
            }

            // singleton's not available for upgrade

            if (isSingleton()) {

                return;
            }

            // nothing to do for unsubscribed ones

            if (!isSubscribed()) {

                return;
            }

            if (popularity > 100) {

                // one off test on whether to track so we have around 100 active

                if (lws_skip_check == 2) {

                    return;

                } else if (lws_skip_check == 0) {

                    if (new Random().nextInt((int) ((popularity + 99) / 100)) == 0) {

                        lws_skip_check = 1;

                    } else {

                        lws_skip_check = 2;

                        return;
                    }
                }
            }

            if (hash != null) {

                boolean create = false;

                if (lws == null) {

                    create = true;

                } else {

                    if (!Arrays.equals(lws.getHash().getBytes(), hash)) {

                        lws.remove();

                        create = true;
                    }
                }

                if (create) {

                    try {
                        File original_data_location = manager.getVuzeFile(this);

                        if (original_data_location.exists()) {

                            // make a version based filename to avoid issues regarding multiple
                            // versions

                            final File versioned_data_location = new File(original_data_location.getParent(),
                                    original_data_location.getName() + "." + getVersion());

                            if (!versioned_data_location.exists()) {

                                if (!FileUtil.copyFile(original_data_location, versioned_data_location)) {

                                    throw (new Exception(
                                            "Failed to copy file to '" + versioned_data_location + "'"));
                                }
                            }

                            lws = LightWeightSeedManager.getSingleton().add(getName(), new HashWrapper(hash),
                                    TorrentUtils.getDecentralisedEmptyURL(), versioned_data_location,
                                    new LightWeightSeedAdapter() {
                                        public TOTorrent getTorrent(byte[] hash, URL announce_url,
                                                File data_location)

                                                throws Exception {
                                            log(" - generating torrent: " + Debug.getCompressedStackTrace());

                                            TOTorrentCreator creator = TOTorrentFactory
                                                    .createFromFileOrDirWithFixedPieceLength(data_location,
                                                            announce_url, 256 * 1024);

                                            TOTorrent t = creator.create();

                                            t.setHashOverride(hash);

                                            return (t);
                                        }
                                    });
                        }

                    } catch (Throwable e) {

                        log("Failed to create light-weight-seed", e);
                    }
                }
            }
        }
    }

    protected synchronized boolean canAutoUpgradeCheck() {
        if (isSingleton()) {

            return (false);
        }

        long now = SystemTime.getMonotonousTime();

        if (last_auto_upgrade_check == -1 || now - last_auto_upgrade_check > 4 * 60 * 60 * 1000) {

            last_auto_upgrade_check = now;

            return (true);
        }

        return (false);
    }

    public void addAssociation(byte[] hash) {
        synchronized (this) {

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

                association assoc = (association) associations.get(i);

                if (Arrays.equals(assoc.getHash(), hash)) {

                    return;
                }
            }

            associations.add(new association(hash, SystemTime.getCurrentTime()));

            if (associations.size() > MAX_ASSOCIATIONS) {

                associations.remove(new Random().nextInt(MAX_ASSOCIATIONS - MIN_RECENT_ASSOC_TO_RETAIN));
            }
        }

        fireChanged();

        manager.associationAdded(this, hash);
    }

    public boolean hasAssociation(byte[] hash) {
        synchronized (this) {

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

                association assoc = (association) associations.get(i);

                if (Arrays.equals(assoc.getHash(), hash)) {

                    return (true);
                }
            }
        }

        return (false);
    }

    public void addPotentialAssociation(String result_id, String key) {
        manager.addPotentialAssociation(this, result_id, key);
    }

    public int getAssociationCount() {
        synchronized (this) {

            return (associations.size());
        }
    }

    protected association getAssociationForPublish() {
        synchronized (this) {

            int num_assoc = associations.size();

            // first set in order of most recent

            for (int i = num_assoc - 1; i >= Math.max(0, num_assoc - MIN_RECENT_ASSOC_TO_RETAIN); i--) {

                association assoc = (association) associations.get(i);

                if (!assoc.getPublished()) {

                    assoc.setPublished(true);

                    return (assoc);
                }
            }

            // remaining randomised

            int rem = associations.size() - MIN_RECENT_ASSOC_TO_RETAIN;

            if (rem > 0) {

                List l = new ArrayList(associations.subList(0, rem));

                Collections.shuffle(l);

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

                    association assoc = (association) l.get(i);

                    if (!assoc.getPublished()) {

                        assoc.setPublished(true);

                        return (assoc);
                    }
                }
            }
        }

        return (null);
    }

    protected boolean getPublished() {
        return (published);
    }

    protected void setPublished(boolean b) {
        published = b;
    }

    protected int getVerifiedPublicationVersion(Map details) {
        // singleton versions always 1 and each instance has separate private key so
        // verification will always fail so save to just return current version

        if (isSingleton()) {

            return (getVersion());
        }

        if (!verifyPublicationDetails(details)) {

            return (-1);
        }

        return (getPublicationVersion(details));
    }

    protected static int getPublicationVersion(Map details) {
        return (((Long) details.get("v")).intValue());
    }

    protected byte[] getPublicationHash() {
        return (hash);
    }

    protected static byte[] getPublicationHash(Map details) {
        return ((byte[]) details.get("h"));
    }

    protected static int getPublicationSize(Map details) {
        return (((Long) details.get("z")).intValue());
    }

    protected Map getPublicationDetails() {
        Map result = new HashMap();

        result.put("v", new Long(version));

        if (singleton_details == null) {

            result.put("h", hash);
            result.put("z", new Long(sig_data_size));
            result.put("s", sig);

        } else {

            result.put("x", singleton_details);
        }

        return (result);
    }

    protected boolean verifyPublicationDetails(Map details) {
        synchronized (this) {

            if (BEncoder.mapsAreIdentical(verify_cache_details, details)) {

                return (verify_cache_result);
            }
        }

        byte[] hash = (byte[]) details.get("h");
        int version = ((Long) details.get("v")).intValue();
        int size = ((Long) details.get("z")).intValue();
        byte[] sig = (byte[]) details.get("s");

        boolean result = SubscriptionBodyImpl.verify(public_key, hash, version, size, sig);

        synchronized (this) {

            verify_cache_details = details;
            verify_cache_result = result;
        }

        return (result);
    }

    public void setCreatorRef(String ref) {
        creator_ref = ref;

        fireChanged();
    }

    public String getCreatorRef() {
        return (creator_ref);
    }

    public void setCategory(String _category) {
        if (_category == null && category == null) {

            return;
        }

        if (_category != null && category != null && _category.equals(category)) {

            return;
        }

        manager.setCategoryOnExisting(this, category, _category);

        category = _category;

        fireChanged();
    }

    public String getCategory() {
        return (category);
    }

    protected void fireChanged() {
        manager.configDirty(this);

        Iterator it = listeners.iterator();

        while (it.hasNext()) {

            try {
                ((SubscriptionListener) it.next()).subscriptionChanged(this);

            } catch (Throwable e) {

                Debug.printStackTrace(e);
            }
        }
    }

    protected void fireDownloaded(boolean was_auto) {

        Iterator it = listeners.iterator();

        while (it.hasNext()) {

            try {
                ((SubscriptionListener) it.next()).subscriptionDownloaded(this, was_auto);

            } catch (Throwable e) {

                Debug.printStackTrace(e);
            }
        }
    }

    public void addListener(SubscriptionListener l) {
        listeners.add(l);
    }

    public void removeListener(SubscriptionListener l) {
        listeners.remove(l);
    }

    public SubscriptionHistory getHistory() {
        return (history);
    }

    public SubscriptionManager getManager() {
        return (manager);
    }

    public VuzeFile getVuzeFile()

            throws SubscriptionException {
        try {
            return (VuzeFileHandler.getSingleton().loadVuzeFile(manager.getVuzeFile(this).getAbsolutePath()));

        } catch (Throwable e) {

            throw (new SubscriptionException("Failed to get Vuze file", e));
        }
    }

    protected void destroy() {
        LightWeightSeed l;

        synchronized (this) {

            destroyed = true;

            l = lws;
        }

        if (l != null) {

            l.remove();
        }
    }

    public void reset() {
        getHistory().reset();

        try {
            getEngine().reset();

        } catch (Throwable e) {

            Debug.printStackTrace(e);
        }
    }

    public void remove() {
        destroy();

        manager.removeSubscription(this);
    }

    protected boolean isRemoved() {
        synchronized (this) {

            return (destroyed);
        }
    }

    public SubscriptionResult[] getResults(boolean include_deleted) {
        return (getHistory().getResults(include_deleted));
    }

    public void setUserData(Object key, Object data) {
        synchronized (user_data) {

            if (data == null) {

                user_data.remove(key);

            } else {

                user_data.put(key, data);
            }
        }
    }

    public Object getUserData(Object key) {
        synchronized (user_data) {

            return (user_data.get(key));
        }
    }

    protected void log(String str) {
        manager.log(getString() + ": " + str);
    }

    protected void log(String str, Throwable e) {
        manager.log(getString() + ": " + str, e);
    }

    public String getString() {
        return ("name=" + name + ",sid=" + ByteFormatter.encodeString(short_id) + ",ver=" + version + ",pub="
                + is_public + ",mine=" + isMine() + ",sub=" + is_subscribed
                + (is_subscribed ? (",hist={" + history.getString() + "}") : "") + ",pop=" + popularity
                + (server_publication_outstanding ? ",spo=true" : ""));
    }

    protected void generate(IndentWriter writer) {
        String engine_str;

        try {

            engine_str = "" + getEngine().getId();

        } catch (Throwable e) {

            engine_str = Debug.getNestedExceptionMessage(e);
        }

        writer.println(getString() + ": engine=" + engine_str);

        try {
            writer.indent();

            synchronized (this) {

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

                    ((association) associations.get(i)).generate(writer);
                }
            }
        } finally {

            writer.exdent();
        }
    }

    protected static class association {
        private byte[] hash;
        private long when;
        private boolean published;

        protected association(byte[] _hash, long _when) {
            hash = _hash;
            when = _when;
        }

        protected byte[] getHash() {
            return (hash);
        }

        protected long getWhen() {
            return (when);
        }

        protected boolean getPublished() {
            return (published);
        }

        protected void setPublished(boolean b) {
            published = b;
        }

        protected String getString() {
            return (ByteFormatter.encodeString(hash) + ", pub=" + published);
        }

        protected void generate(IndentWriter writer) {
            writer.println(getString());
        }
    }
}