Package com.aelitis.azureus.core.content

Source Code of com.aelitis.azureus.core.content.RelatedContentManager

/*
* Created on Jul 8, 2009
* Created by Paul Gardner
*
* Copyright 2009 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.content;

import java.lang.ref.WeakReference;
import java.net.InetSocketAddress;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;

import org.gudy.azureus2.core3.config.COConfigurationManager;
import org.gudy.azureus2.core3.config.ParameterListener;
import org.gudy.azureus2.core3.download.DownloadManagerState;
import org.gudy.azureus2.core3.torrent.TOTorrent;
import org.gudy.azureus2.core3.util.AERunnable;
import org.gudy.azureus2.core3.util.AESemaphore;
import org.gudy.azureus2.core3.util.AEThread2;
import org.gudy.azureus2.core3.util.AsyncDispatcher;
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.ByteArrayHashMap;
import org.gudy.azureus2.core3.util.ByteFormatter;
import org.gudy.azureus2.core3.util.Constants;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.FileUtil;
import org.gudy.azureus2.core3.util.RandomUtils;
import org.gudy.azureus2.core3.util.SHA1Simple;
import org.gudy.azureus2.core3.util.SimpleTimer;
import org.gudy.azureus2.core3.util.StringInterner;
import org.gudy.azureus2.core3.util.SystemTime;
import org.gudy.azureus2.core3.util.TimerEvent;
import org.gudy.azureus2.core3.util.TimerEventPerformer;
import org.gudy.azureus2.core3.util.TorrentUtils;
import org.gudy.azureus2.core3.util.UrlUtils;
import org.gudy.azureus2.plugins.PluginInterface;
import org.gudy.azureus2.plugins.PluginListener;
import org.gudy.azureus2.plugins.ddb.DistributedDatabase;
import org.gudy.azureus2.plugins.ddb.DistributedDatabaseContact;
import org.gudy.azureus2.plugins.ddb.DistributedDatabaseException;
import org.gudy.azureus2.plugins.ddb.DistributedDatabaseKey;
import org.gudy.azureus2.plugins.ddb.DistributedDatabaseProgressListener;
import org.gudy.azureus2.plugins.ddb.DistributedDatabaseTransferHandler;
import org.gudy.azureus2.plugins.ddb.DistributedDatabaseTransferType;
import org.gudy.azureus2.plugins.ddb.DistributedDatabaseValue;
import org.gudy.azureus2.plugins.download.Download;
import org.gudy.azureus2.plugins.download.DownloadManager;
import org.gudy.azureus2.plugins.download.DownloadManagerListener;
import org.gudy.azureus2.plugins.torrent.Torrent;
import org.gudy.azureus2.plugins.torrent.TorrentAttribute;
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.PluginCoreUtils;

import com.aelitis.azureus.core.AzureusCore;
import com.aelitis.azureus.core.cnetwork.ContentNetwork;
import com.aelitis.azureus.core.dht.DHT;
import com.aelitis.azureus.core.dht.transport.DHTTransportContact;
import com.aelitis.azureus.core.dht.transport.udp.DHTTransportUDP;
import com.aelitis.azureus.core.torrent.PlatformTorrentUtils;
import com.aelitis.azureus.core.util.CopyOnWriteList;
import com.aelitis.azureus.core.util.FeatureAvailability;
import com.aelitis.azureus.core.util.bloom.BloomFilter;
import com.aelitis.azureus.core.util.bloom.BloomFilterFactory;
import com.aelitis.azureus.plugins.dht.DHTPlugin;
import com.aelitis.azureus.plugins.dht.DHTPluginContact;
import com.aelitis.azureus.plugins.dht.DHTPluginOperationListener;
import com.aelitis.azureus.plugins.dht.DHTPluginValue;
import com.aelitis.azureus.util.ImportExportUtils;

public class
RelatedContentManager
  implements DistributedDatabaseTransferHandler
{
  private static final boolean   TRACE = false;
  public static final boolean  DISABLE_ALL_UI  = !Constants.isCVSVersion() && COConfigurationManager.getStringParameter("ui", "az3").equals("az3");

  private static final int  MAX_HISTORY          = 16;
  private static final int  MAX_TITLE_LENGTH      = 80;
  private static final int  MAX_CONCURRENT_PUBLISH    = 2;
  private static final int  MAX_REMOTE_SEARCH_RESULTS  = 30;
  private static final int  MAX_REMOTE_SEARCH_CONTACTS  = 50;
  private static final int  MAX_REMOTE_SEARCH_MILLIS  = 25*1000;
 
  private static final int  TEMPORARY_SPACE_DELTA  = 50;
 
  private static final int  MAX_RANK  = 100;
 
  private static final String  CONFIG_FILE         = "rcm.config";
  private static final String  PERSIST_DEL_FILE       = "rcmx.config";
 
  private static final String  CONFIG_TOTAL_UNREAD  = "rcm.numunread.cache";
 
  private static RelatedContentManager  singleton;
  private static AzureusCore        core;
 
  public static synchronized void
  preInitialise(
    AzureusCore    _core )
  {
    core    = _core;
  }
 
  public static synchronized RelatedContentManager
  getSingleton()
 
    throws ContentException
  {
    if ( singleton == null ){
     
      singleton = new RelatedContentManager();
    }
   
    return( singleton );
  }
 
 
  private PluginInterface         plugin_interface;
  private TorrentAttribute         ta_networks;
  private DHTPlugin            dht_plugin;

  private long  global_random_id = -1;
 
  private LinkedList<DownloadInfo>    download_infos1   = new LinkedList<DownloadInfo>();
  private LinkedList<DownloadInfo>    download_infos2   = new LinkedList<DownloadInfo>();
 
  private ByteArrayHashMapEx<DownloadInfo>  download_info_map  = new ByteArrayHashMapEx<DownloadInfo>();
  private Set<String>              download_priv_set  = new HashSet<String>();
 
  private final boolean  enabled;
 
  private int    max_search_level;
  private int    max_results;
 
  private AtomicInteger  temporary_space = new AtomicInteger();
 
  private int publishing_count = 0;
 
  private CopyOnWriteList<RelatedContentManagerListener>  listeners = new CopyOnWriteList<RelatedContentManagerListener>();
 
  private AESemaphore initialisation_complete_sem = new AESemaphore( "RCM:init" );

  private static final int TIMER_PERIOD      = 30*1000;
  private static final int CONFIG_SAVE_PERIOD    = 60*1000;
  private static final int CONFIG_SAVE_TICKS    = CONFIG_SAVE_PERIOD/TIMER_PERIOD;
  private static final int PUBLISH_CHECK_PERIOD  = 30*1000;
  private static final int PUBLISH_CHECK_TICKS  = PUBLISH_CHECK_PERIOD/TIMER_PERIOD;
  private static final int SECONDARY_LOOKUP_PERIOD  = 15*60*1000;
  private static final int SECONDARY_LOOKUP_TICKS    = SECONDARY_LOOKUP_PERIOD/TIMER_PERIOD;
  private static final int REPUBLISH_PERIOD      = 8*60*60*1000;
  private static final int REPUBLISH_TICKS      = REPUBLISH_PERIOD/TIMER_PERIOD;

  private static final int INITIAL_PUBLISH_DELAY  = 3*60*1000;
  private static final int INITIAL_PUBLISH_TICKS  = INITIAL_PUBLISH_DELAY/TIMER_PERIOD;
 
 
 
  private static final int CONFIG_DISCARD_MILLIS  = 60*1000;
 
  private ContentCache        content_cache_ref;
  private WeakReference<ContentCache>  content_cache;
 
  private boolean    content_dirty;
  private long    last_config_access;   
  private int      content_discard_ticks;
 
  private AtomicInteger  total_unread = new AtomicInteger( COConfigurationManager.getIntParameter( CONFIG_TOTAL_UNREAD, 0 ));
 
  private AsyncDispatcher  content_change_dispatcher = new AsyncDispatcher();
 
  private static final int SECONDARY_LOOKUP_CACHE_MAX = 10;
 
  private LinkedList<SecondaryLookup> secondary_lookups = new LinkedList<SecondaryLookup>();
 
  private boolean  secondary_lookup_in_progress;
  private long  secondary_lookup_complete_time;
 
  private DistributedDatabase    ddb;
  private RCMSearchXFer      transfer_type = new RCMSearchXFer();
 
  private boolean  persist;
 
  {
    COConfigurationManager.addAndFireParameterListener(
      "rcm.persist",
      new ParameterListener()
      {
        public void
        parameterChanged(
          String parameterName )
        {
          persist = COConfigurationManager.getBooleanParameter( "rcm.persist" );
        }
      });
  }
  protected
  RelatedContentManager()
 
    throws ContentException
  {
    if ( !FeatureAvailability.isRCMEnabled()){
     
      enabled    = false;
     
      return;
    }
   
    enabled = true;
   
    try{
      if ( core == null ){
       
        throw( new ContentException( "getSingleton called before pre-initialisation" ));
      }
     
      while( global_random_id == -1 ){
       
        global_random_id = COConfigurationManager.getLongParameter( "rcm.random.id", -1 );
       
        if ( global_random_id == -1 ){
         
          global_random_id = RandomUtils.nextLong();
         
          COConfigurationManager.setParameter( "rcm.random.id", global_random_id );
        }
      }
       
      plugin_interface = core.getPluginManager().getDefaultPluginInterface();
     
      ta_networks   = plugin_interface.getTorrentManager().getAttribute( TorrentAttribute.TA_NETWORKS );
     
      COConfigurationManager.addAndFireParameterListeners(
        new String[]{
          "rcm.ui.enabled",
          "rcm.max_search_level",
          "rcm.max_results",
        },
        new ParameterListener()
        {
          public void
          parameterChanged(
            String name )
          {
            max_search_level   = COConfigurationManager.getIntParameter( "rcm.max_search_level", 3 );
            max_results       = COConfigurationManager.getIntParameter( "rcm.max_results", 500 );
          }
        });
     
      plugin_interface.getUtilities().createDelayedTask(new AERunnable() {
        public void runSupport() {
          SimpleTimer.addEvent(
              "rcm.delay.init",
              SystemTime.getOffsetTime( 15*1000 ),
              new TimerEventPerformer()
              {
                public void
                perform(
                  TimerEvent event )
                {
                  delayedInit();
                }
              });
        }
      }).queue();
     
    }catch( Throwable e ){
     
      initialisation_complete_sem.releaseForever();
     
      if ( e instanceof ContentException ){
       
        throw((ContentException)e);
      }
     
      throw( new ContentException( "Initialisation failed", e ));
    }
  }
 
  private void delayedInit() {
   
    plugin_interface.addListener(
      new PluginListener()
      {
        public void
        initializationComplete()
        {
          if ( !persist ){
           
            deleteRelatedContent();
          }
         
          try{
            PluginInterface dht_pi =
              plugin_interface.getPluginManager().getPluginInterfaceByClass(
                    DHTPlugin.class );
     
            if ( dht_pi != null ){
       
              dht_plugin = (DHTPlugin)dht_pi.getPlugin();

              if ( !dht_plugin.isEnabled()){
               
                return;
              }
             
              DownloadManager dm = plugin_interface.getDownloadManager();
             
              Download[] downloads = dm.getDownloads();
             
              addDownloads( downloads, true );
             
              dm.addListener(
                new DownloadManagerListener()
                {
                  public void
                  downloadAdded(
                    Download  download )
                  {
                    addDownloads( new Download[]{ download }, false );
                  }
                 
                  public void
                  downloadRemoved(
                    Download  download )
                  {
                  }
                },
                false );
             
              SimpleTimer.addPeriodicEvent(
                "RCM:publisher",
                TIMER_PERIOD,
                new TimerEventPerformer()
                {
                  private int  tick_count;
                 
                  public void
                  perform(
                    TimerEvent event )
                  {
                    tick_count++;

                    if ( tick_count == 1 ){
                     
                      try{
                        ddb = plugin_interface.getDistributedDatabase();
                     
                        ddb.addTransferHandler( transfer_type, RelatedContentManager.this );
                       
                      }catch( Throwable e ){
                       
                        // Debug.out( e );
                      }
                    }
                   
                    if ( enabled ){
                       
                      if ( tick_count >= INITIAL_PUBLISH_TICKS ){
                       
                        if ( tick_count % PUBLISH_CHECK_TICKS == 0 ){
                       
                          publish();
                        }
                       
                        if ( tick_count % SECONDARY_LOOKUP_TICKS == 0 ){

                          secondaryLookup();
                        }
                       
                        if ( tick_count % REPUBLISH_TICKS == 0 ){

                          republish();
                        }
                       
                        if ( tick_count % CONFIG_SAVE_TICKS == 0 ){
                         
                          saveRelatedContent();
                        }
                      }
                    }
                  }
                });
            }                   
          }finally{
             
            initialisation_complete_sem.releaseForever();
          }
        }
       
        public void
        closedownInitiated()
        {
          saveRelatedContent();
        }
       
        public void
        closedownComplete()
        {
        }
      });
  }
 
  public boolean
  isEnabled()
  {
    return( enabled );
  }
   
  public int
  getMaxSearchLevel()
  {
    return( max_search_level );
  }
 
  public void
  setMaxSearchLevel(
    int    _level )
  {
    COConfigurationManager.setParameter( "rcm.max_search_level", _level );
  }
 
  public int
  getMaxResults()
  {
    return( max_results );
  }
 
  public void
  setMaxResults(
    int    _max )
  {
    COConfigurationManager.setParameter( "rcm.max_results", _max );
   
    enforceMaxResults( false );
  }
 
  protected void
  addDownloads(
    Download[]    downloads,
    boolean      initialising )
  {
    synchronized( this ){
 
      List<DownloadInfo>  new_info = new ArrayList<DownloadInfo>( downloads.length );
     
      for ( Download download: downloads ){
       
        try{
          if ( !download.isPersistent()){
           
            continue;
          }
         
          Torrent  torrent = download.getTorrent();
 
          if ( torrent == null ){
           
            continue;
          }
         
          byte[]  hash = torrent.getHash();

          if ( download_info_map.containsKey( hash )){
           
            continue;
          }
         
          String[]  networks = download.getListAttribute( ta_networks );
         
          if ( networks == null ){
           
            continue;
          }
           
          boolean  public_net = false;
         
          for (int i=0;i<networks.length;i++){
           
            if ( networks[i].equalsIgnoreCase( "Public" )){
               
              public_net  = true;
             
              break;
            }
          }
         
          TOTorrent to_torrent = PluginCoreUtils.unwrap( torrent );
         
          if ( public_net && !TorrentUtils.isReallyPrivate( to_torrent )){
           
            DownloadManagerState state = PluginCoreUtils.unwrap( download ).getDownloadState();

            if ( state.getFlag(DownloadManagerState.FLAG_LOW_NOISE )){
             
              continue;
            }
           
            long rand = global_random_id ^ state.getLongParameter( DownloadManagerState.PARAM_RANDOM_SEED );           
           
            long cache = state.getLongAttribute( DownloadManagerState.AT_SCRAPE_CACHE );

            int  seeds_leechers;
           
            if ( cache == -1 ){
             
              seeds_leechers = -1;
             
            }else{
             
              int seeds     = (int)((cache>>32)&0x00ffffff);
              int leechers   = (int)(cache&0x00ffffff);
             
              seeds_leechers   = (int)((seeds<<16)|(leechers&0xffff));
            }

            DownloadInfo info =
              new DownloadInfo(
                hash,
                hash,
                download.getName(),
                (int)rand,
                torrent.isPrivate()?StringInterner.intern(torrent.getAnnounceURL().getHost()):null,
                0,
                false,
                torrent.getSize(),
                (int)( to_torrent.getCreationDate()/(60*60)),
                seeds_leechers,
                (byte)PlatformTorrentUtils.getContentNetworkID( to_torrent ));
           
            new_info.add( info );
           
            if ( initialising || download_infos1.size() == 0 ){
             
              download_infos1.add( info );
             
            }else{
             
              download_infos1.add( RandomUtils.nextInt( download_infos1.size()), info );
            }
           
            download_infos2.add( info );
           
            download_info_map.put( hash, info );
           
            if ( info.getTracker() != null ){
             
              download_priv_set.add( getPrivateInfoKey( info ));
            }
          }
        }catch( Throwable e ){
         
          Debug.out( e );
        }
      }
     
      List<Map<String,Object>> history = (List<Map<String,Object>>)COConfigurationManager.getListParameter( "rcm.dlinfo.history", new ArrayList<Map<String,Object>>());
     
      if ( initialising ){
   
        int padd = MAX_HISTORY - download_info_map.size();
       
        for ( int i=0;i<history.size() && padd > 0;i++ ){
         
          try{
            DownloadInfo info = deserialiseDI((Map<String,Object>)history.get(i), null);
           
            if ( info != null && !download_info_map.containsKey( info.getHash())){
             
              download_info_map.put( info.getHash(), info );
             
              if ( info.getTracker() != null ){
               
                download_priv_set.add( getPrivateInfoKey( info ));
              }
             
              download_infos1.add( info );
              download_infos2.add( info );
             
              padd--;
            }
          }catch( Throwable e ){
           
          }
        }
       
        Collections.shuffle( download_infos1 );
       
      }else{
       
        if ( new_info.size() > 0 ){
         
          for ( DownloadInfo info: new_info ){
           
            Map<String,Object> map = serialiseDI( info, null );
             
            if ( map != null ){
             
              history.add( map )
            }
          }
         
          while( history.size() > MAX_HISTORY ){
           
            history.remove(0);
          }
         
          COConfigurationManager.setParameter( "rcm.dlinfo.history", history );
        }
      }
    }
  }
 
  protected void
  republish()
  {
    synchronized( this ){

      if( publishing_count > 0 ){
       
        return;
      }
     
      if ( download_infos1.isEmpty()){
       
        List<DownloadInfo> list = download_info_map.values();
       
        download_infos1.addAll( list );
        download_infos2.addAll( list );
       
        Collections.shuffle( download_infos1 );
      }
    }
  }
 
  protected void
  publish()
  {
    while( true ){
     
      DownloadInfo  info1 = null;
      DownloadInfo  info2 = null;

      synchronized( this ){
 
        if ( publishing_count >= MAX_CONCURRENT_PUBLISH ){
         
          return;
        }
       
        if ( download_infos1.isEmpty() || download_info_map.size() == 1 ){
         
          return;
        }
             
        info1 = download_infos1.removeFirst();
       
        Iterator<DownloadInfo> it = download_infos2.iterator();
       
        while( it.hasNext()){
         
          info2 = it.next();
         
          if ( info1 != info2 || download_infos2.size() == 1 ){
           
            it.remove();
           
            break;
          }
        }
       
        if ( info1 == info2 ){
                 
          info2 = download_info_map.getRandomValueExcluding( info1 );
         
          if ( info2 == null || info1 == info2 ){
           
            // Debug.out( "Inconsistent!" );
           
            return;
          }
        }
       
        publishing_count++;
      }
     
      try{
        publish( info1, info2 );
       
      }catch( Throwable e ){
       
        synchronized( this ){

          publishing_count--;
        }
       
        Debug.out( e );
      }
    }
  }
 
  protected void
  publishNext()
  {
    synchronized( this ){

      publishing_count--;
     
      if ( publishing_count < 0 ){
       
          // shouldn't happen but whatever
       
        publishing_count = 0;
      }
    }
   
    publish();
  }
 
  protected void
  publish(
    final DownloadInfo  from_info,
    final DownloadInfo  to_info )
 
    throws Exception
  {     
    final String from_hash  = ByteFormatter.encodeString( from_info.getHash());
    final String to_hash  = ByteFormatter.encodeString( to_info.getHash());
   
    final byte[] key_bytes  = ( "az:rcm:assoc:" + from_hash ).getBytes( "UTF-8" );
   
    String title = to_info.getTitle();
   
    if ( title.length() > MAX_TITLE_LENGTH ){
     
      title = title.substring( 0, MAX_TITLE_LENGTH );
    }
   
    Map<String,Object> map = new HashMap<String,Object>();
   
    map.put( "d", title );
    map.put( "r", new Long( Math.abs( to_info.getRand()%1000 )));
   
    String  tracker = to_info.getTracker();
   
    if ( tracker == null ){
     
      map.put( "h", to_info.getHash());
     
    }else{
     
      map.put( "t", tracker );
    }

    if ( to_info.getLevel() == 0 ){
     
      try{
        Download d = to_info.getRelatedToDownload();
     
        if ( d != null ){
         
          Torrent torrent = d.getTorrent();
         
          if ( torrent != null ){
           
            long cnet = PlatformTorrentUtils.getContentNetworkID( PluginCoreUtils.unwrap( torrent ));
           
            if ( cnet != ContentNetwork.CONTENT_NETWORK_UNKNOWN ){
             
              map.put( "c", new Long( cnet ));
            }
           
            long secs = torrent.getCreationDate();
           
            long hours = secs/(60*60);
           
            if ( hours > 0 ){
             
              map.put( "p", new Long( hours ));
            }
          }
                   
          int leechers   = -1;
          int seeds     = -1;
         
          long cache = PluginCoreUtils.unwrap( d ).getDownloadState().getLongAttribute( DownloadManagerState.AT_SCRAPE_CACHE );
           
          if ( cache != -1 ){
             
            seeds     = (int)((cache>>32)&0x00ffffff);
            leechers   = (int)(cache&0x00ffffff);
          }
         
          if ( leechers > 0 ){
            map.put( "l", new Long( leechers ));
          }
          if ( seeds > 0 ){
            map.put( "z", new Long( seeds ));
          }         
        }
      }catch( Throwable e ){   
      }
    }
   
    long  size = to_info.getSize();
   
    if ( size != 0 ){
     
      map.put( "s", new Long( size ));
    }
   
    final byte[] map_bytes = BEncoder.encode( map );
   
    final int max_hits = 30;
       
    dht_plugin.get(
        key_bytes,
        "Content relationship read: " + from_hash,
        DHTPlugin.FLAG_SINGLE_VALUE,
        max_hits,
        30*1000,
        false,
        false,
        new DHTPluginOperationListener()
        {
          private boolean diversified;
          private int    hits;
         
          private Set<String>  entries = new HashSet<String>();
         
          public void
          starts(
            byte[]        key )
          {
          }
         
          public void
          diversified()
          {
            diversified = true;
          }
         
          public void
          valueRead(
            DHTPluginContact  originator,
            DHTPluginValue    value )
          {
            try{
              Map<String,Object> map = (Map<String,Object>)BDecoder.decode( value.getValue());
             
              String  title = new String((byte[])map.get( "d" ), "UTF-8" );
             
              String  tracker  = null;
             
              byte[]  hash   = (byte[])map.get( "h" );
             
              if ( hash == null ){
               
                tracker = new String((byte[])map.get( "t" ), "UTF-8" );
              }
             
              int  rand = ((Long)map.get( "r" )).intValue();
             
              String  key = title + " % " + rand;
             
              synchronized( entries ){
             
                if ( entries.contains( key )){
                 
                  return;
                }
               
                entries.add( key );
              }
             
              Long  l_size = (Long)map.get( "s" );
             
              long  size = l_size==null?0:l_size.longValue();
             
              Long  cnet     = (Long)map.get( "c" );
              Long  published   = (Long)map.get( "p" );
              Long  leechers   = (Long)map.get( "l" );
              Long  seeds     = (Long)map.get( "z" );
             
              // System.out.println( "p=" + published + ", l=" + leechers + ", s=" + seeds );
             
              int  seeds_leechers;
             
              if ( leechers == null && seeds == null ){
               
                seeds_leechers = -1;
               
              }else if ( leechers == null ){
               
                seeds_leechers = seeds.intValue()<<16;
               
              }else if ( seeds == null ){
               
                seeds_leechers = leechers.intValue()&0xffff;
               
              }else{
               
                seeds_leechers = (seeds.intValue()<<16)|(leechers.intValue()&0xffff);
              }
               
              analyseResponse(
                new DownloadInfo(
                    from_info.getHash(), hash, title, rand, tracker, 1, false, size,
                    published==null?0:published.intValue(),
                    seeds_leechers,
                    (byte)(cnet==null?ContentNetwork.CONTENT_NETWORK_UNKNOWN:cnet.byteValue())),
                null );
             
            }catch( Throwable e ){             
            }
           
            hits++;
          }
         
          public void
          valueWritten(
            DHTPluginContact  target,
            DHTPluginValue    value )
          {
           
          }
         
          public void
          complete(
            byte[]        key,
            boolean        timeout_occurred )
          {
            boolean  do_it;
           
            // System.out.println( from_hash + ": hits=" + hits + ", div=" + diversified );
           
            if ( diversified || hits >= 10 ){
             
              do_it = false;
             
            }else if ( hits <= 5 ){
             
              do_it = true;
                           
            }else{
                         
              do_it = RandomUtils.nextInt( hits - 5 + 1 ) == 0;
            }
             
            if ( do_it ){
             
              try{
                dht_plugin.put(
                    key_bytes,
                    "Content relationship: " +  from_hash + " -> " + to_hash,
                    map_bytes,
                    DHTPlugin.FLAG_ANON,
                    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 )
                      {
                        publishNext();
                      }
                    });
              }catch( Throwable e ){
               
                Debug.printStackTrace(e);
               
                publishNext();
              }
            }else{
             
              publishNext();
            }
          }
        });
  }
     
  public void
  lookupContent(
    final byte[]            hash,
    final RelatedContentLookupListener  listener )
 
    throws ContentException
  {
    if ( hash == null ){
     
      throw( new ContentException( "hash is null" ));
    }

    if (   !initialisation_complete_sem.isReleasedForever() ||
        ( dht_plugin != null && dht_plugin.isInitialising())){
     
      AsyncDispatcher dispatcher = new AsyncDispatcher();
 
      dispatcher.dispatch(
        new AERunnable()
        {
          public void
          runSupport()
          {
            try{
              initialisation_complete_sem.reserve();
             
              lookupContentSupport( hash, 0, true, listener );
             
            }catch( ContentException e ){
             
              Debug.out( e );
            }
          }
        });
    }else{
     
      lookupContentSupport( hash, 0, true, listener );
    }
  }
 
  private void
  lookupContentSupport(
    final byte[]            from_hash,
    final int              level,
    final boolean            explicit,
    final RelatedContentLookupListener  listener )
 
    throws ContentException
  {
    try{
      if ( dht_plugin == null ){
       
        throw( new ContentException( "DHT plugin unavailable" ));
      }
     
      final String from_hash_str  = ByteFormatter.encodeString( from_hash );
   
      final byte[] key_bytes  = ( "az:rcm:assoc:" + from_hash_str ).getBytes( "UTF-8" );
     
      final int max_hits = 30;
     
      dht_plugin.get(
          key_bytes,
          "Content relationship read: " + from_hash_str,
          DHTPlugin.FLAG_SINGLE_VALUE,
          max_hits,
          60*1000,
          false,
          true,
          new DHTPluginOperationListener()
          {
            private Set<String>  entries = new HashSet<String>();
           
            private RelatedContentManagerListener manager_listener =
              new RelatedContentManagerListener()
              {
              private Set<RelatedContent>  content_list = new HashSet<RelatedContent>();
             
                public void
                contentFound(
                  RelatedContent[]  content )
                {
                  handle( content );
                }
 
                public void
                contentChanged(
                  RelatedContent[]  content )
                {
                  handle( content );
                }
               
                public void
                contentRemoved(
                  RelatedContent[]   content )
                {
                }
               
                public void
                contentChanged()
                {                 
                }
               
                public void
                contentReset()
                {
                }
               
                private void
                handle(
                  RelatedContent[]  content )
                {
                  synchronized( content_list ){
                   
                    if ( content_list.contains( content )){
                     
                      return;
                    }
                   
                    for ( RelatedContent c: content ){
                   
                      content_list.add( c );
                    }
                  }
                 
                  listener.contentFound( content );
                }
              };
           
            public void
            starts(
              byte[]        key )
            {
              if ( listener != null ){
               
                try{
                  listener.lookupStart();
                 
                }catch( Throwable e ){
                 
                  Debug.out( e );
                }
              }
            }
           
            public void
            diversified()
            {
            }
           
            public void
            valueRead(
              DHTPluginContact  originator,
              DHTPluginValue    value )
            {
              try{
                Map<String,Object> map = (Map<String,Object>)BDecoder.decode( value.getValue());
               
                String  title = new String((byte[])map.get( "d" ), "UTF-8" );
               
                String  tracker  = null;
               
                byte[]  hash   = (byte[])map.get( "h" );
               
                if ( hash == null ){
                 
                  tracker = new String((byte[])map.get( "t" ), "UTF-8" );
                }
               
                int  rand = ((Long)map.get( "r" )).intValue();
               
                String  key = title + " % " + rand;
               
                synchronized( entries ){
               
                  if ( entries.contains( key )){
                   
                    return;
                  }
                 
                  entries.add( key );
                }
               
                Long  l_size = (Long)map.get( "s" );
               
                long  size = l_size==null?0:l_size.longValue();

                Long  cnet     = (Long)map.get( "c" );
                Long  published   = (Long)map.get( "p" );
                Long  leechers   = (Long)map.get( "l" );
                Long  seeds     = (Long)map.get( "z" );

                int  seeds_leechers;
               
                if ( leechers == null && seeds == null ){
                 
                  seeds_leechers = -1;
                 
                }else if ( leechers == null ){
                 
                  seeds_leechers = seeds.intValue()<<16;
                 
                }else if ( seeds == null ){
                 
                  seeds_leechers = leechers.intValue()&0xffff;
                 
                }else{
                 
                  seeds_leechers = (seeds.intValue()<<16)|(leechers.intValue()&0xffff);
                }
                analyseResponse(
                  new DownloadInfo(
                    from_hash, hash, title, rand, tracker, level+1, explicit, size,
                    published==null?0:published.intValue(),
                    seeds_leechers,
                    (byte)(cnet==null?ContentNetwork.CONTENT_NETWORK_UNKNOWN:cnet.byteValue())),
                  listener==null?null:manager_listener );
               
              }catch( Throwable e ){ 
              }
            }
           
            public void
            valueWritten(
              DHTPluginContact  target,
              DHTPluginValue    value )
            {
             
            }
           
            public void
            complete(
              byte[]        key,
              boolean        timeout_occurred )
            {
              if ( listener != null ){
               
                try{
                  listener.lookupComplete();
                 
                }catch( Throwable e ){
                 
                  Debug.out( e );
                }
              }
            }       
          });
    }catch( Throwable e ){
   
      ContentException  ce;
     
      if ( ( e instanceof ContentException )){
       
        ce = (ContentException)e;
       
      }else{
        ce = new ContentException( "Lookup failed", e );
      }
     
      if ( listener != null ){
       
        try{
          listener.lookupFailed( ce );
         
        }catch( Throwable f ){
         
          Debug.out( f );
        }
      }
     
      throw( ce );
    }
  }
 
  protected void
  popuplateSecondaryLookups(
    ContentCache  content_cache )
  {
    Random rand = new Random();

    secondary_lookups.clear();
   
      // stuff in a couple primarys
   
    List<DownloadInfo> primaries = download_info_map.values();
   
    int  primary_count = primaries.size();
   
    int  primaries_to_add;
   
    if ( primary_count < 2 ){
     
      primaries_to_add = 0;
     
    }else if ( primary_count < 5 ){
     
      if ( rand.nextInt(4) == 0 ){
       
        primaries_to_add = 1;
       
      }else{
       
        primaries_to_add = 0;
      }
    }else if ( primary_count < 10 ){
     
      primaries_to_add = 1;
     
    }else{
     
      primaries_to_add = 2;
    }
   
    if ( primaries_to_add > 0 ){
     
      Set<DownloadInfo> added = new HashSet<DownloadInfo>();
     
      for (int i=0;i<primaries_to_add;i++){
       
        DownloadInfo info = primaries.get( rand.nextInt( primaries.size()));
       
        if ( !added.contains( info )){
         
          added.add( info );
         
          secondary_lookups.addLast(new SecondaryLookup(info.getHash(), info.getLevel()));
        }
      }
    }
   
    Map<String,DownloadInfo>    related_content      = content_cache.related_content;

    Iterator<DownloadInfo> it = related_content.values().iterator();
   
    List<DownloadInfo> secondary_cache_temp = new ArrayList<DownloadInfo>( related_content.size());

    while( it.hasNext()){
     
      DownloadInfo di = it.next();
     
      if ( di.getHash() != null && di.getLevel() < max_search_level ){
         
        secondary_cache_temp.add( di );
      }
    }
           
    final int cache_size = Math.min( secondary_cache_temp.size(), SECONDARY_LOOKUP_CACHE_MAX - secondary_lookups.size());
   
    if ( cache_size > 0 ){
           
      for( int i=0;i<cache_size;i++){
       
        int index = rand.nextInt( secondary_cache_temp.size());
       
        DownloadInfo x = secondary_cache_temp.get( index );
       
        secondary_cache_temp.set( index, secondary_cache_temp.get(i));
       
        secondary_cache_temp.set( i, x );
      }
     
      for ( int i=0;i<cache_size;i++){
       
        DownloadInfo x = secondary_cache_temp.get(i);
       
        secondary_lookups.addLast(new SecondaryLookup(x.getHash(), x.getLevel()));
      }
    }
  }
 
  protected void
  secondaryLookup()
  {
    SecondaryLookup sl;
   
    long  now = SystemTime.getMonotonousTime();
   
    synchronized( this ){
     
      if ( secondary_lookup_in_progress ){
       
        return;
      }
   
      if ( now - secondary_lookup_complete_time < SECONDARY_LOOKUP_PERIOD ){
       
        return;
      }
     
      if ( secondary_lookups.size() == 0 ){
     
        ContentCache cc = content_cache==null?null:content_cache.get();

        if ( cc == null ){
         
            // this will populate the cache
         
          cc = loadRelatedContent();
         
        }else{
         
          popuplateSecondaryLookups( cc );
        }
      }

      if ( secondary_lookups.size() == 0 ){
       
        return;
      }
           
      sl = secondary_lookups.removeFirst();

      secondary_lookup_in_progress = true;
    }
   
    try{
      lookupContentSupport(
        sl.getHash(),
        sl.getLevel(),
        false,
        new RelatedContentLookupListener()
        {
          public void
          lookupStart()
          { 
          }
         
          public void
          contentFound(
            RelatedContent[]  content )
          { 
          }
         
          public void
          lookupComplete()
          {
            next();
          }
         
          public void
          lookupFailed(
            ContentException   error )
          {
            next();
          }
         
          protected void
          next()
          {
            final SecondaryLookup next_sl;
           
            synchronized( RelatedContentManager.this ){
             
              if ( secondary_lookups.size() == 0 ){
               
                secondary_lookup_in_progress = false;
               
                secondary_lookup_complete_time = SystemTime.getMonotonousTime();
               
                return;
               
              }else{
               
                next_sl = secondary_lookups.removeFirst();
              }
            }
           
            final RelatedContentLookupListener listener = this;
           
            SimpleTimer.addEvent(
              "RCM:SLDelay",
              SystemTime.getOffsetTime( 30*1000 ),
              new TimerEventPerformer()
              {
                public void
                perform(
                  TimerEvent event )
                {
                  try{         
                    lookupContentSupport( next_sl.getHash(), next_sl.getLevel(), false, listener );
                   
                  }catch( Throwable e ){
                   
                    Debug.out( e );
                   
                    synchronized( RelatedContentManager.this ){
                     
                      secondary_lookup_in_progress = false;
                     
                      secondary_lookup_complete_time = SystemTime.getMonotonousTime();
                    }
                  }
                }
              });
          }
        });
     
    }catch( Throwable e ){
     
      Debug.out( e );
     
      synchronized( this ){
       
        secondary_lookup_in_progress = false;
       
        secondary_lookup_complete_time = now;
      }
    }
  }
 
  protected void
  contentChanged(
    final DownloadInfo    info )
  {
    setConfigDirty();
   
    content_change_dispatcher.dispatch(
      new AERunnable()
      {
        public void
        runSupport()
        {
          for ( RelatedContentManagerListener l: listeners ){
           
            try{
              l.contentChanged( new RelatedContent[]{ info });
             
            }catch( Throwable e ){
             
              Debug.out( e );
            }
          }
        }
      });
  }
 
  protected void
  contentChanged(
    boolean  is_dirty )
  {
    if ( is_dirty ){
   
      setConfigDirty();
    }
   
    content_change_dispatcher.dispatch(
      new AERunnable()
      {
        public void
        runSupport()
        {
          for ( RelatedContentManagerListener l: listeners ){
           
            try{
              l.contentChanged();
             
            }catch( Throwable e ){
             
              Debug.out( e );
            }
          }
        }
      });
  }
 
  public void
  delete(
    RelatedContent[]  content )
  {
    synchronized( this ){
     
      ContentCache content_cache = loadRelatedContent();
     
      delete( content, content_cache, true );
    }
  }
 
  protected void
  delete(
    final RelatedContent[]  content,
    ContentCache      content_cache,
    boolean          persistent )
  {
    if ( persistent ){
   
      addPersistentlyDeleted( content );
    }
   
    Map<String,DownloadInfo> related_content = content_cache.related_content;

    Iterator<DownloadInfo> it = related_content.values().iterator();
   
    while( it.hasNext()){
   
      DownloadInfo di = it.next();
     
      for ( RelatedContent c: content ){
       
        if ( c == di ){
         
          it.remove();
         
          if ( di.isUnread()){
           
            decrementUnread();
          }
        }
      }
    }
   
    ByteArrayHashMapEx<ArrayList<DownloadInfo>> related_content_map = content_cache.related_content_map;
   
    List<byte[]> delete = new ArrayList<byte[]>();
   
    for ( byte[] key: related_content_map.keys()){
     
      ArrayList<DownloadInfo>  infos = related_content_map.get( key );
     
      for ( RelatedContent c: content ){

        if ( infos.remove( c )){
         
          if ( infos.size() == 0 ){
           
            delete.add( key );
           
            break;
          }
        }
      }
    }
   
    for ( byte[] key: delete ){
     
      related_content_map.remove( key );
    }
   
    setConfigDirty();
   
    content_change_dispatcher.dispatch(
        new AERunnable()
        {
          public void
          runSupport()
          {
            for ( RelatedContentManagerListener l: listeners ){
             
              try{
                l.contentRemoved( content );

              }catch( Throwable e ){
               
                Debug.out( e );
              }
            }
          }
        });
  }
 
  protected String
  getPrivateInfoKey(
    RelatedContent    info )
  {
    return( info.getTitle() + ":" + info.getTracker());
  }
 
  protected void
  analyseResponse(
    DownloadInfo            to_info,
    final RelatedContentManagerListener  listener )
  {
    try{     
      synchronized( this ){
       
        byte[] target = to_info.getHash();
       
        String  key;
       
        if ( target != null ){
         
          if ( download_info_map.containsKey( target )){
           
              // target refers to downoad we already have
           
            return;
          }
         
          key = Base32.encode( target );
         
        }else{
         
          key = getPrivateInfoKey( to_info );
         
          if ( download_priv_set.contains( key )){
           
              // target refers to downoad we already have
           
            return;
          }
        }
       
        if ( isPersistentlyDeleted( to_info )){
         
          return;
        }
       
        ContentCache  content_cache = loadRelatedContent();
       
        DownloadInfo  target_info = null;
       
        boolean  changed_content = false;
        boolean  new_content   = false;
       
       
        target_info = content_cache.related_content.get( key );
       
        if ( target_info == null ){
               
          if ( enoughSpaceFor( content_cache, to_info )){
         
            target_info = to_info;

            content_cache.related_content.put( key, target_info );
           
            byte[] from_hash = to_info.getRelatedToHash();
           
            ArrayList<DownloadInfo> links = content_cache.related_content_map.get( from_hash );
           
            if ( links == null ){
             
              links = new ArrayList<DownloadInfo>(1);
             
              content_cache.related_content_map.put( from_hash, links );
            }
           
            links.add( target_info );
           
            links.trimToSize();
           
            target_info.setPublic( content_cache );
           
            if ( secondary_lookups.size() < SECONDARY_LOOKUP_CACHE_MAX ){
             
              byte[]  hash   = target_info.getHash();
              int    level  = target_info.getLevel();
             
              if ( hash != null && level < max_search_level ){
               
                secondary_lookups.add( new SecondaryLookup( hash, level ));
              }
            }
           
            new_content = true;
          }
         
        }else{
         
            // we already know about this, see if new info
         
          changed_content = target_info.addInfo( to_info );
        }

        if ( target_info != null ){
         
          final RelatedContent[]  f_target   = new RelatedContent[]{ target_info };
          final boolean      f_change  = changed_content;
         
          final boolean something_changed = changed_content || new_content;
             
          if ( something_changed ){
         
            setConfigDirty();
          }
         
          content_change_dispatcher.dispatch(
            new AERunnable()
            {
              public void
              runSupport()
              {
                if ( something_changed ){
                 
                  for ( RelatedContentManagerListener l: listeners ){
                   
                    try{
                      if ( f_change ){
                       
                        l.contentChanged( f_target );
                       
                      }else{
                       
                        l.contentFound( f_target );
                      }
                    }catch( Throwable e ){
                     
                      Debug.out( e );
                    }
                  }
                }
               
                if ( listener != null ){
                 
                  try{
                    if ( f_change ){
                     
                      listener.contentChanged( f_target );
                     
                    }else{
                     
                      listener.contentFound( f_target );
                    }
                  }catch( Throwable e ){
                   
                    Debug.out( e );
                  }
                }
              }
            });
        }
      }
     
    }catch( Throwable e ){
     
      Debug.out( e );
    }
  }
 
  protected boolean
  enoughSpaceFor(
    ContentCache  content_cache,
    DownloadInfo  fi )
  {
    Map<String,DownloadInfo> related_content = content_cache.related_content;
   
    if ( related_content.size() < max_results + temporary_space.get()){
     
      return( true );
    }
   
    Iterator<Map.Entry<String,DownloadInfo>>  it = related_content.entrySet().iterator();
       
    int  level     = fi.getLevel();
   
      // delete oldest at highest level >= level with minimum rank
 
    Map<Integer,DownloadInfo>  oldest_per_rank = new HashMap<Integer, DownloadInfo>();
   
    int  min_rank = Integer.MAX_VALUE;
   
    while( it.hasNext()){
     
      Map.Entry<String,DownloadInfo> entry = it.next();
     
      DownloadInfo info = entry.getValue();
     
      if ( info.isExplicit()){
       
        continue;
      }
     
      int  info_level = info.getLevel();
     
      if ( info_level >= level ){
       
        if ( info_level > level ){
         
          level = info_level;
         
          min_rank = Integer.MAX_VALUE;
         
          oldest_per_rank.clear();
        }
       
        int  rank = info.getRank();
       
        if ( rank < min_rank ){
         
          min_rank = rank;
        }
       
        DownloadInfo oldest = oldest_per_rank.get( rank );
       
        if ( oldest == null ){
         
          oldest_per_rank.put( rank, info );
         
        }else{
         
          if ( info.getLastSeenSecs() < oldest.getLastSeenSecs()){
           
            oldest_per_rank.put( rank, info );
          }
        }
      }
    }
   
    DownloadInfo to_remove = oldest_per_rank.get( min_rank );
   
    if ( to_remove != null ){
         
      delete( new RelatedContent[]{ to_remove }, content_cache, false );
     
      return( true );
    }
   
    return( false );
  }
 
  public RelatedContent[]
  getRelatedContent()
  {
    synchronized( this ){

      ContentCache  content_cache = loadRelatedContent();
     
      return( content_cache.related_content.values().toArray( new DownloadInfo[ content_cache.related_content.size()]));
    }
  }
 
  public void
  reset()
  {
    reset( true );
  }
 
  protected void
  reset(
    boolean  reset_perm_dels )
  {
    synchronized( this ){
     
      ContentCache cc = content_cache==null?null:content_cache.get();
     
      if ( cc == null ){
       
        FileUtil.deleteResilientConfigFile( CONFIG_FILE );
       
      }else{
     
        cc.related_content     = new HashMap<String,DownloadInfo>();
        cc.related_content_map   = new ByteArrayHashMapEx<ArrayList<DownloadInfo>>();
      }
     
      download_infos1.clear();
      download_infos2.clear();
     
      List<DownloadInfo> list = download_info_map.values();
     
      download_infos1.addAll( list );
      download_infos2.addAll( list );
     
      Collections.shuffle( download_infos1 );
     
      total_unread.set( 0 );
     
      if ( reset_perm_dels ){
     
        resetPersistentlyDeleted();
      }
     
      setConfigDirty();
    }
   
    content_change_dispatcher.dispatch(
        new AERunnable()
        {
          public void
          runSupport()
          {
            for ( RelatedContentManagerListener l: listeners ){
             
              l.contentReset();
            }
          }
        });
  }
 
  protected List<RelatedContent>
  matchContent(
    String    term )
  {
      // 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'
   
    List<RelatedContent>  result = new ArrayList<RelatedContent>();
   
    RelatedContent[] content = getRelatedContent();
   
    String[]   bits = Constants.PAT_SPLIT_SPACE.split(term.toLowerCase());

    int[]    bit_types     = new int[bits.length];
    Pattern[]  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 ){
          }
        }
      }
    }
     
   
    for ( final RelatedContent c: content ){
     
      String title = c.getTitle().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 = title.contains( bit );
           
          }else{
         
            hit = bit_patterns[i].matcher( title ).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;
            }
          }
        }
      }
     
      if ( match && at_least_one ){
       
        result.add( c );
      }
    }
   
    return( result );
  }
 
  public SearchInstance
  searchRCM(
    Map<String,Object>    search_parameters,
    final SearchObserver  observer )
 
    throws SearchException
  {
    initialisation_complete_sem.reserve();

    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 ){
   
      observer.complete();
     
    }else{
   
      new AEThread2( "RCM:search", true )
      {
        public void
        run()
        {
          final Set<String>  hashes = new HashSet<String>();
         
          try{       
            List<RelatedContent>  matches = matchContent( term );
             
            for ( final RelatedContent c: matches ){
             
              final byte[] hash = c.getHash();
             
              if ( hash == null ){
               
                continue;
              }
             
              hashes.add( Base32.encode( hash ));
             
              SearchResult result =
                new SearchResult()
                {
                  public Object
                  getProperty(
                    int    property_name )
                  {
                    if ( property_name == SearchResult.PR_NAME ){
                     
                      return( c.getTitle());
                     
                    }else if ( property_name == SearchResult.PR_SIZE ){
                     
                      return( c.getSize());
                     
                    }else if ( property_name == SearchResult.PR_HASH ){
                     
                      return( hash );
                     
                    }else if ( property_name == SearchResult.PR_RANK ){
                     
                        // this rank isn't that accurate, scale down
                     
                      return( new Long( c.getRank() / 4 ));
                     
                    }else if ( property_name == SearchResult.PR_SEED_COUNT ){
                     
                      return( new Long( c.getSeeds()));
                     
                    }else if ( property_name == SearchResult.PR_LEECHER_COUNT ){
                     
                      return( new Long( c.getLeechers()));
                     
                    }else if ( property_name == SearchResult.PR_SUPER_SEED_COUNT ){
                     
                      if ( c.getContentNetwork() != ContentNetwork.CONTENT_NETWORK_UNKNOWN ){
                       
                        return( new Long( 1 ));
                       
                      }else{
                       
                        return( new Long( 0 ));
                      }
                    }else if ( property_name == SearchResult.PR_PUB_DATE ){
                       
                      long  date = c.getPublishDate();
                     
                      if ( date <= 0 ){
                       
                        return( null );
                      }
                     
                      return( new Date( date ));
                     
                    }else if (   property_name == SearchResult.PR_DOWNLOAD_LINK ||
                          property_name == SearchResult.PR_DOWNLOAD_BUTTON_LINK ){
                     
                      byte[] hash = c.getHash();
                     
                      if ( hash != null ){
                       
                        return( UrlUtils.getMagnetURI( hash ));
                      }
                    }
                   
                    return( null );
                  }
                };
               
              observer.resultReceived( si, result );
            }
          }finally{
           
            try{
              DHT[]  dhts = dht_plugin.getDHTs();
 
              Set<InetSocketAddress>  addresses = new HashSet<InetSocketAddress>();
             
              for ( DHT dht: dhts ){
             
                DHTTransportContact[] contacts = dht.getTransport().getReachableContacts();
               
                for ( DHTTransportContact c: contacts ){
                 
                  if ( c.getProtocolVersion() >= DHTTransportUDP.PROTOCOL_VERSION_REPLICATION_CONTROL ){
                   
                    addresses.add( c.getAddress());
                  }
                }
              }
             
              if ( addresses.size() < MAX_REMOTE_SEARCH_CONTACTS ){
               
                for ( DHT dht: dhts ){
                 
                  DHTTransportContact[] contacts = dht.getTransport().getRecentContacts();
 
                  for ( DHTTransportContact c: contacts ){
                   
                    if ( c.getProtocolVersion() >= DHTTransportUDP.PROTOCOL_VERSION_REPLICATION_CONTROL ){
                     
                      addresses.add( c.getAddress());
                     
                      if ( addresses.size() >= MAX_REMOTE_SEARCH_CONTACTS ){
                       
                        break;
                      }
                    }
                  }
                 
                  if ( addresses.size() >= MAX_REMOTE_SEARCH_CONTACTS ){
                   
                    break;
                  }
                }
              }
             
              List<InetSocketAddress>  list = new ArrayList<InetSocketAddress>( addresses );
             
              Collections.shuffle( list );
             
              List<DistributedDatabaseContact>  ddb_contacts = new ArrayList<DistributedDatabaseContact>();
             
              for (int i=0;i<Math.min( list.size(), MAX_REMOTE_SEARCH_CONTACTS );i++){
               
                try{       
                  ddb_contacts.add( ddb.importContact( list.get(i), DHTTransportUDP.PROTOCOL_VERSION_REPLICATION_CONTROL ));
                 
                }catch( Throwable e ){
                }
              }
             
              long  start    = SystemTime.getMonotonousTime();
              long  max      = MAX_REMOTE_SEARCH_MILLIS;
             
              final AESemaphore  sem = new AESemaphore( "RCM:rems" );
             
              int  sent = 0;
             
              final int[]      done = {0};
             
              for (int i=0;i<ddb_contacts.size();i++){
               
                final DistributedDatabaseContact c = ddb_contacts.get( i );
                               
                new AEThread2( "RCM:rems", true )
                {
                  public void
                  run()
                  {
                    try{
                      sendRemoteSearch( si, hashes, c, term, observer );
                                           
                    }finally{
                     
                      synchronized( done ){
                     
                        done[0]++;
                      }
                     
                      sem.release();
                    }
                  }
                }.start();
               
                sent++;
               
                synchronized( done ){
                 
                  if ( done[0] >= ddb_contacts.size() / 2 ){
                   
                    start    = SystemTime.getMonotonousTime();
                    max      = 5*1000;
                   
                    break;
                  }
                }
               
                if ( i > 10 ){
                 
                  try{
                    Thread.sleep( 250 );
                   
                  }catch( Throwable e ){
                  }
                }
              }
             
              for (int i=0;i<sent;i++){
               
                if ( done[0] > sent*4/5 ){
                 
                  break;
                }
               
                long  elapsed = SystemTime.getMonotonousTime() - start;
               
                if ( elapsed < max ){
                 
                  sem.reserve( max - elapsed );
                 
                }else{
                 
                  break;
                }
              }
            }finally{
                             
              observer.complete();
            }
          }
        }
      }.start();
    }
   
    return( si );
  }
 
  protected void
  sendRemoteSearch(
    SearchInstance          si,
    Set<String>            hashes,
    DistributedDatabaseContact    contact,
    String              term,
    SearchObserver          observer )
  {
    try{
      Map<String,Object>  request = new HashMap<String,Object>();
     
      request.put( "t", term );
   
      DistributedDatabaseKey key = ddb.createKey( BEncoder.encode( request ));
     
      DistributedDatabaseValue value =
        contact.read(
          new DistributedDatabaseProgressListener()
          {
            public void
            reportSize(
              long  size )
            { 
            }
           
            public void
            reportActivity(
              String  str )
            { 
            }
           
            public void
            reportCompleteness(
              int    percent )
            {
            }
          },
          transfer_type,
          key,
          10000 );
     
      // System.out.println( "search result=" + value );
     
      if ( value == null ){
       
        return;
      }
     
      Map<String,Object> reply = (Map<String,Object>)BDecoder.decode((byte[])value.getValue( byte[].class ));
     
      List<Map<String,Object>>  list = (List<Map<String,Object>>)reply.get( "l" );
     
      for ( final Map<String,Object> map: list ){
       
        final String title = ImportExportUtils.importString( map, "n" );
       
        final byte[] hash = (byte[])map.get( "h" );
       
        if ( hash == null ){
         
          continue;
        }
       
        String  hash_str = Base32.encode( hash );
         
        if ( hashes.contains( hash_str )){
         
          continue;
        }
       
        hashes.add( hash_str );

        SearchResult result =
          new SearchResult()
          {
            public Object
            getProperty(
              int    property_name )
            {
              try{
                if ( property_name == SearchResult.PR_NAME ){
                 
                  return( title );
                 
                }else if ( property_name == SearchResult.PR_SIZE ){
                 
                  return( ImportExportUtils.importLong( map, "s" ));
                 
                }else if ( property_name == SearchResult.PR_HASH ){
                 
                  return( hash );
                 
                }else if ( property_name == SearchResult.PR_RANK ){
                 
                  return( ImportExportUtils.importLong( map, "r" ) / 4 );
                 
                }else if ( property_name == SearchResult.PR_SUPER_SEED_COUNT ){
                 
                  long cnet = ImportExportUtils.importLong( map, "c", ContentNetwork.CONTENT_NETWORK_UNKNOWN );
                 
                  if ( cnet == ContentNetwork.CONTENT_NETWORK_UNKNOWN ){
                   
                    return( 0L );
                   
                  }else{
                   
                    return( 1L );
                  }
                }else if ( property_name == SearchResult.PR_SEED_COUNT ){
                 
                  return( ImportExportUtils.importLong( map, "z" ));
                 
                }else if ( property_name == SearchResult.PR_LEECHER_COUNT ){
                 
                  return( ImportExportUtils.importLong( map, "l" ));
                 
                }else if ( property_name == SearchResult.PR_PUB_DATE ){
                 
                  long date = ImportExportUtils.importLong( map, "p", 0 )*60*60*1000L;
                 
                  if ( date <= 0 ){
                   
                    return( null );
                  }
                 
                  return( new Date( date ));
                 
                }else if (   property_name == SearchResult.PR_DOWNLOAD_LINK ||
                      property_name == SearchResult.PR_DOWNLOAD_BUTTON_LINK ){
                 
                  byte[] hash = (byte[])map.get( "h" );
                 
                  if ( hash != null ){
                   
                    return( UrlUtils.getMagnetURI( hash ));
                  }
                }
              }catch( Throwable e ){
              }
             
              return( null );
            }
          };
         
        observer.resultReceived( si, result );
      }
    }catch( Throwable e ){
    }
  }
 
  protected Map<String,Object>
  receiveRemoteSearch(
    Map<String,Object>    request )
  {
    Map<String,Object>  response = new HashMap<String,Object>();
   
    try{
      String  term = ImportExportUtils.importString( request, "t" );
   
      if ( term != null ){
       
        List<RelatedContent>  matches = matchContent( term );

        if ( matches.size() > MAX_REMOTE_SEARCH_RESULTS ){
         
          Collections.sort(
            matches,
            new Comparator<RelatedContent>()
            {
              public int
              compare(
                RelatedContent o1,
                RelatedContent o2)
              {
                return( o2.getRank() - o1.getRank());
              }
            });
        }
       
        List<Map<String,Object>> list = new ArrayList<Map<String,Object>>();
       
        for (int i=0;i<Math.min( matches.size(),MAX_REMOTE_SEARCH_RESULTS);i++){
         
          RelatedContent  c = matches.get(i);
         
          Map<String,Object>  map = new HashMap<String, Object>();
         
          list.add( map );
         
          ImportExportUtils.exportString( map, "n", c.getTitle());
          ImportExportUtils.exportLong( map, "s", c.getSize());
          ImportExportUtils.exportLong( map, "r", c.getRank());
          ImportExportUtils.exportLong( map, "d", c.getLastSeenSecs());
          ImportExportUtils.exportLong( map, "p", c.getPublishDate()/(60*60*1000));
          ImportExportUtils.exportLong( map, "l", c.getLeechers());
          ImportExportUtils.exportLong( map, "z", c.getSeeds());
          ImportExportUtils.exportLong( map, "c", c.getContentNetwork());
         
          byte[] hash = c.getHash();
         
          if ( hash != null ){
           
            map.put( "h", hash );
          }
        }
       
        response.put( "l", list );
      }
    }catch( Throwable e ){
    }
   
    return( response );
  }
 
  public DistributedDatabaseValue
  read(
    DistributedDatabaseContact      contact,
    DistributedDatabaseTransferType    type,
    DistributedDatabaseKey        ddb_key )
 
    throws DistributedDatabaseException
  {
    Object  o_key = ddb_key.getKey();
   
    try{
      byte[]  key = (byte[])o_key;
     
        // TODO bloom
     
      Map<String,Object>  request = BDecoder.decode( key );
     
      Map<String,Object>  result = receiveRemoteSearch( request );
     
      return( ddb.createValue( BEncoder.encode( result )));
     
    }catch( Throwable e ){
     
      Debug.out( e );
     
      return( null );
    }
  }
 
  public void
  write(
    DistributedDatabaseContact      contact,
    DistributedDatabaseTransferType    type,
    DistributedDatabaseKey        key,
    DistributedDatabaseValue      value )
 
    throws DistributedDatabaseException
  {
  }
 
  protected void
  setConfigDirty()
  {
    synchronized( this ){
     
      content_dirty  = true;
    }
  }
 
  protected ContentCache
  loadRelatedContent()
  {
    boolean  fire_event = false;
   
    try{
      synchronized( this ){
 
        last_config_access = SystemTime.getMonotonousTime();
 
        ContentCache cc = content_cache==null?null:content_cache.get();
       
        if ( cc == null ){
       
          if ( TRACE ){
            System.out.println( "rcm: load new" );
          }
         
          fire_event = true;
         
          cc = new ContentCache();
   
          content_cache = new WeakReference<ContentCache>( cc );
         
          try{
            int  new_total_unread = 0;
   
            if ( FileUtil.resilientConfigFileExists( CONFIG_FILE )){
                     
              Map map = FileUtil.readResilientConfigFile( CONFIG_FILE );
             
              Map<String,DownloadInfo>            related_content      = cc.related_content;
              ByteArrayHashMapEx<ArrayList<DownloadInfo>>    related_content_map    = cc.related_content_map;
   
             
              Map<String,String>  rcm_map = (Map<String,String>)map.get( "rcm" );
             
              Object  rc_map_stuff   = map.get( "rc" );
             
              if ( rc_map_stuff != null && rcm_map != null ){
               
                Map<Integer,DownloadInfo> id_map = new HashMap<Integer, DownloadInfo>();
                 
                if ( rc_map_stuff instanceof Map ){
                 
                    // migration from when it was a Map with non-ascii key issues
                 
                  Map<String,Map<String,Object>>  rc_map   = (Map<String,Map<String,Object>>)rc_map_stuff;

                  for ( Map.Entry<String,Map<String,Object>> entry: rc_map.entrySet()){
                   
                    try{
                   
                      String  key = entry.getKey();
                   
                      Map<String,Object>  info_map = entry.getValue();
                                   
                      DownloadInfo info = deserialiseDI( info_map, cc );
                     
                      if ( info.isUnread()){
                       
                        new_total_unread++;
                      }
                     
                      related_content.put( key, info );
                     
                      int  id = ((Long)info_map.get( "_i" )).intValue();
       
                      id_map.put( id, info );
                     
                    }catch( Throwable e ){
                     
                      Debug.out( e );
                    }
                  }
                }else{
                 
                  List<Map<String,Object>>  rc_map_list   = (List<Map<String,Object>>)rc_map_stuff;

                  for ( Map<String,Object> info_map: rc_map_list ){
                   
                    try{
                   
                      String  key = new String((byte[])info_map.get( "_k" ), "UTF-8" );
                                                       
                      DownloadInfo info = deserialiseDI( info_map, cc );
                     
                      if ( info.isUnread()){
                       
                        new_total_unread++;
                      }
                     
                      related_content.put( key, info );
                     
                      int  id = ((Long)info_map.get( "_i" )).intValue();
       
                      id_map.put( id, info );
                     
                    }catch( Throwable e ){
                     
                      Debug.out( e );
                    }
                  }
                }
                             
                if ( rcm_map.size() != 0 && id_map.size() != 0 ){
                 
                  for ( String key: rcm_map.keySet()){
                   
                    try{
                      byte[]  hash = Base32.decode( key );
                     
                      int[]  ids = ImportExportUtils.importIntArray( rcm_map, key );
                     
                      if ( ids == null || ids.length == 0 ){
                       
                        // Debug.out( "Inconsistent - no ids" );
                       
                      }else{
                       
                        ArrayList<DownloadInfo>  di_list = new ArrayList<DownloadInfo>(ids.length);
                       
                        for ( int id: ids ){
                         
                          DownloadInfo di = id_map.get( id );
                         
                          if ( di == null ){
                           
                            // Debug.out( "Inconsistent: id " + id + " missing" );
                           
                          }else{
                           
                              // we don't currently remember all originators, just one that works
                             
                            di.setRelatedToHash( hash );
                           
                            di_list.add( di );
                          }
                        }
                       
                        if ( di_list.size() > 0 ){
                         
                          related_content_map.put( hash, di_list );
                        }
                      }
                    }catch( Throwable e ){
                     
                      Debug.out( e );
                    }
                  }
                }
               
                Iterator<DownloadInfo> it = related_content.values().iterator();
               
                while( it.hasNext()){
                 
                  DownloadInfo di = it.next();
                 
                  if ( di.getRelatedToHash() == null ){
               
                    // Debug.out( "Inconsistent: info not referenced" );
                   
                    if ( di.isUnread()){
                     
                      new_total_unread--;
                    }
                   
                    it.remove();
                  }
                }
               
                popuplateSecondaryLookups( cc );
              }
            }
           
            if ( total_unread.get() != new_total_unread ){
                           
              // Debug.out( "total_unread - inconsistent (" + total_unread + "/" + new_total_unread + ")" );
             
              total_unread.set( new_total_unread );
             
              COConfigurationManager.setParameter( CONFIG_TOTAL_UNREAD, new_total_unread );
            }
          }catch( Throwable e ){
           
            Debug.out( e );
          }
         
          enforceMaxResults( cc, false );

        }else{
         
          if ( TRACE ){
            System.out.println( "rcm: load existing" );
          }
        }
       
        content_cache_ref = cc;
               
        return( cc );
      }
    }finally{
     
      if ( fire_event ){
       
        contentChanged( false );
      }
    }
  }
 
  protected void
  saveRelatedContent()
  {
    synchronized( this ){
       
      COConfigurationManager.setParameter( CONFIG_TOTAL_UNREAD, total_unread.get());
     
      long  now = SystemTime.getMonotonousTime();;
     
      ContentCache cc = content_cache==null?null:content_cache.get();
     
      if ( !content_dirty ){
         
        if ( cc != null  ){
         
          if ( now - last_config_access > CONFIG_DISCARD_MILLIS ){
         
            if ( content_cache_ref != null ){
             
              content_discard_ticks = 0;
            }
           
            if ( TRACE ){
              System.out.println( "rcm: discard: tick count=" + content_discard_ticks++ );
            }
           
            content_cache_ref  = null;
          }
        }else{
         
          if ( TRACE ){
            System.out.println( "rcm: discarded" );
          }
        }
       
        return;
      }
     
      last_config_access = now;
     
      content_dirty  = false;
     
      if ( cc == null ){
       
        // Debug.out( "RCM: cache inconsistent" );
       
      }else{

        if ( persist ){
          if ( TRACE ){
            System.out.println( "rcm: save" );
          }
         
          Map<String,DownloadInfo>            related_content      = cc.related_content;
          ByteArrayHashMapEx<ArrayList<DownloadInfo>>    related_content_map    = cc.related_content_map;
 
          if ( related_content.size() == 0 ){
           
            FileUtil.deleteResilientConfigFile( CONFIG_FILE );
           
          }else{
           
            Map<String,Object>  map = new HashMap<String, Object>();
           
            Set<Map.Entry<String,DownloadInfo>> rcs = related_content.entrySet();
                     
            List<Map<String,Object>> rc_map_list = new ArrayList<Map<String, Object>>( rcs.size());
           
            map.put( "rc", rc_map_list );
           
            int    id = 0;
           
            Map<DownloadInfo,Integer>  info_map = new HashMap<DownloadInfo, Integer>();
           
            for ( Map.Entry<String,DownloadInfo> entry: rcs ){
                       
              DownloadInfo  info = entry.getValue();
                         
              Map<String,Object> di_map = serialiseDI( info, cc );
             
              if ( di_map != null ){
               
                info_map.put( info, id );
 
                di_map.put( "_i", new Long( id ));
                di_map.put( "_k", entry.getKey());
               
                if ( rc_map_list.add( di_map ));
   
                id++; 
              }
            }
           
            Map<String,Object> rcm_map = new HashMap<String, Object>();
 
            map.put( "rcm", rcm_map );
                     
            for ( byte[] hash: related_content_map.keys()){
             
              List<DownloadInfo> dis = related_content_map.get( hash );
             
              int[] ids = new int[dis.size()];
             
              int  pos = 0;
             
              for ( DownloadInfo di: dis ){
               
                Integer  index = info_map.get( di );
               
                if ( index == null ){
                 
                  // Debug.out( "inconsistent: info missing for " + di );
                 
                  break;
                 
                }else{
                 
                  ids[pos++] = index;
                }
              }
             
              if ( pos == ids.length ){
             
                ImportExportUtils.exportIntArray( rcm_map, Base32.encode( hash), ids );
              }
            }
           
            FileUtil.writeResilientConfigFile( CONFIG_FILE, map );
          }
        }else{
         
          deleteRelatedContent();
        }
      }
    }
  }
 
  private void
  deleteRelatedContent()
  {
    FileUtil.deleteResilientConfigFile( CONFIG_FILE );
    FileUtil.deleteResilientConfigFile( PERSIST_DEL_FILE );
  }
 
  public int
  getNumUnread()
  {
    return( total_unread.get());
  }
 
  public void
  setAllRead()
  {
    boolean  changed = false;
   
    synchronized( this ){
     
      DownloadInfo[] content = (DownloadInfo[])getRelatedContent();
     
      for ( DownloadInfo c: content ){
       
        if ( c.isUnread()){
       
          changed = true;
         
          c.setUnreadInternal( false );
        }
      }
     
      total_unread.set( 0 );
    }
   
    if ( changed ){
   
      contentChanged( true );
    }
  }
 
  public void
  deleteAll()
  { 
    synchronized( this ){

      ContentCache  content_cache = loadRelatedContent();
     
      addPersistentlyDeleted( content_cache.related_content.values().toArray( new DownloadInfo[ content_cache.related_content.size()]));
   
      reset( false );
    }
  }
 
  protected void
  incrementUnread()
  {
    total_unread.incrementAndGet();
  }
 
  protected void
  decrementUnread()
  {
    synchronized( this ){
     
      int val = total_unread.decrementAndGet();
     
      if ( val < 0 ){
       
        // Debug.out( "inconsistent" );
       
        total_unread.set( 0 );
      }
    }
  }
 
  protected Download
  getDownload(
    byte[]  hash )
  {
    try{
      return( plugin_interface.getDownloadManager().getDownload( hash ));
     
    }catch( Throwable e ){
     
      return( null );
    }
  }
 
  private static final int PD_BLOOM_INITIAL_SIZE    = 1000;
  private static final int PD_BLOOM_INCREMENT_SIZE  = 1000;
 
 
  private BloomFilter  persist_del_bloom;
 
  protected byte[]
  getPermDelKey(
    RelatedContent  info )
  {
    byte[]  bytes = info.getHash();
   
    if ( bytes == null ){
     
      try{
        bytes = new SHA1Simple().calculateHash( getPrivateInfoKey(info).getBytes( "ISO-8859-1" ));
       
      }catch( Throwable e ){
       
        Debug.out( e );
       
        return( null );
      }
    }
   
    byte[] key = new byte[8];
   
    System.arraycopy( bytes, 0, key, 0, 8 );
   
    return( key );
  }
 
  protected List<byte[]>
  loadPersistentlyDeleted()
  {
    List<byte[]> entries = null;
   
    if ( FileUtil.resilientConfigFileExists( PERSIST_DEL_FILE )){
       
      Map<String,Object> map = (Map<String,Object>)FileUtil.readResilientConfigFile( PERSIST_DEL_FILE );
       
      entries = (List<byte[]>)map.get( "entries" );
    }
 
    if ( entries == null ){
     
      entries = new ArrayList<byte[]>(0);
    }
   
    return( entries );
  }
 
  protected void
  addPersistentlyDeleted(
    RelatedContent[]  content )
  {   
    if ( content.length == 0 ){
     
      return;
    }
 
    List<byte[]> entries = loadPersistentlyDeleted();
   
    List<byte[]> new_keys = new ArrayList<byte[]>( content.length );
   
    for ( RelatedContent rc: content ){
     
      byte[] key = getPermDelKey( rc );
     
      new_keys.add( key );
     
      entries.add( key );
    }
   
    Map<String,Object>  map = new HashMap<String, Object>();
   
    map.put( "entries", entries );
   
    FileUtil.writeResilientConfigFile( PERSIST_DEL_FILE, map );
   
    if ( persist_del_bloom != null ){
     
      if ( persist_del_bloom.getSize() / ( persist_del_bloom.getEntryCount() + content.length ) < 10 ){
   
        persist_del_bloom = BloomFilterFactory.createAddOnly( Math.max( PD_BLOOM_INITIAL_SIZE, persist_del_bloom.getSize() *10 + PD_BLOOM_INCREMENT_SIZE + content.length  ));
       
        for ( byte[] k: entries ){
         
          persist_del_bloom.add( k );
        }
      }else{
       
        for ( byte[] k: new_keys ){
         
          persist_del_bloom.add( k );
        }
      }
    }
  }
 
  protected boolean
  isPersistentlyDeleted(
    RelatedContent    content )
  {
    if ( persist_del_bloom == null ){
     
      List<byte[]> entries = loadPersistentlyDeleted();
     
      persist_del_bloom = BloomFilterFactory.createAddOnly( Math.max( PD_BLOOM_INITIAL_SIZE, entries.size()*10 + PD_BLOOM_INCREMENT_SIZE ));

      for ( byte[] k: entries ){
       
        persist_del_bloom.add( k );
      }
    }
   
    byte[]  key = getPermDelKey( content );
   
    return( persist_del_bloom.contains( key ));
  }

  protected void
  resetPersistentlyDeleted()
  {
    FileUtil.deleteResilientConfigFile( PERSIST_DEL_FILE );
   
    persist_del_bloom = BloomFilterFactory.createAddOnly( PD_BLOOM_INITIAL_SIZE );
  }
 
  public void
  reserveTemporarySpace()
  {
    temporary_space.addAndGet( TEMPORARY_SPACE_DELTA );
  }
 
  public void
  releaseTemporarySpace()
  {
    boolean  reset_explicit = temporary_space.addAndGet( -TEMPORARY_SPACE_DELTA ) == 0;
   
    enforceMaxResults( reset_explicit );
  }
 
  protected void
  enforceMaxResults(
    boolean reset_explicit )
  {
    synchronized( this ){
 
      ContentCache  content_cache = loadRelatedContent();
     
      enforceMaxResults( content_cache, reset_explicit );
    }
  }
 
  protected void
  enforceMaxResults(
    ContentCache    content_cache,
    boolean        reset_explicit
  {
    Map<String,DownloadInfo>    related_content      = content_cache.related_content;

    int num_to_remove = related_content.size() - ( max_results + temporary_space.get());
   
    if ( num_to_remove > 0 ){
     
      List<DownloadInfo>  infos = new ArrayList<DownloadInfo>(related_content.values());
       
      if ( reset_explicit ){
       
        for ( DownloadInfo info: infos ){
         
          if ( info.isExplicit()){
           
            info.setExplicit( false );
          }
        }
      }
     
      Collections.sort(
        infos,
        new Comparator<DownloadInfo>()
        {
          public int
          compare(
            DownloadInfo o1,
            DownloadInfo o2)
          {
            int res = o2.getLevel() - o1.getLevel();
           
            if ( res != 0 ){
             
              return( res );
            }
           
            res = o1.getRank() - o2.getRank();
           
            if ( res != 0 ){
             
              return( res );
            }
           
            return( o1.getLastSeenSecs() - o2.getLastSeenSecs());
          }
        });

      List<RelatedContent> to_remove = new ArrayList<RelatedContent>();
     
      for (int i=0;i<Math.min( num_to_remove, infos.size());i++ ){
       
        to_remove.add( infos.get(i));
      }
     
      if ( to_remove.size() > 0 ){
         
        delete( to_remove.toArray( new RelatedContent[to_remove.size()]), content_cache, false );
      }
    }
  }
 
  public void
  addListener(
    RelatedContentManagerListener    listener )
  {
    listeners.add( listener );
  }
 
  public void
  removeListener(
    RelatedContentManagerListener    listener )
  {
    listeners.remove( listener );
  }
 
  protected static class
  ByteArrayHashMapEx<T>
    extends ByteArrayHashMap<T>
  {
      public T
      getRandomValueExcluding(
        T  excluded )
      {
        int  num = RandomUtils.nextInt( size );
       
        T result = null;
       
          for (int j = 0; j < table.length; j++) {
           
             Entry<T> e = table[j];
            
             while( e != null ){
              
                  T value = e.value;
                  
                  if ( value != excluded ){
                   
                    result = value;
                  }
                 
                  if ( num <= 0 && result != null ){
                   
                    return( result );
                  }
                 
                  num--;
                 
                  e = e.next;
            }
        }
     
          return( result );
      }
  }
 
  private Map<String,Object>
  serialiseDI(
    DownloadInfo      info,
    ContentCache      cc )
  {
    try{
      Map<String,Object> info_map = new HashMap<String,Object>();
     
      info_map.put( "h", info.getHash());
     
      ImportExportUtils.exportString( info_map, "d", info.getTitle());
      ImportExportUtils.exportInt( info_map, "r", info.getRand());
      ImportExportUtils.exportString( info_map, "t", info.getTracker());
      ImportExportUtils.exportLong( info_map, "z", info.getSize());
     
      ImportExportUtils.exportInt( info_map, "p", (int)( info.getPublishDate()/(60*60*1000)));
      ImportExportUtils.exportInt( info_map, "q", (info.getSeeds()<<16)|(info.getLeechers()&0xffff));
      ImportExportUtils.exportInt( info_map, "c", (int)info.getContentNetwork());

      if ( cc != null ){
             
        ImportExportUtils.exportBoolean( info_map, "u", info.isUnread());
        ImportExportUtils.exportIntArray( info_map, "l", info.getRandList());
        ImportExportUtils.exportInt( info_map, "s", info.getLastSeenSecs());
        ImportExportUtils.exportInt( info_map, "e", info.getLevel());
      }
     
      return( info_map );
     
    }catch( Throwable e ){
     
      Debug.out( e );
     
      return( null );
    }
  }
 
  private DownloadInfo
  deserialiseDI(
    Map<String,Object>    info_map,
    ContentCache      cc )
  {
    try{
      byte[]  hash   = (byte[])info_map.get("h");
      String  title  = ImportExportUtils.importString( info_map, "d" );
      int    rand  = ImportExportUtils.importInt( info_map, "r" );
      String  tracker  = ImportExportUtils.importString( info_map, "t" );
      long  size  = ImportExportUtils.importLong( info_map, "z" );
     
      int    date       =  ImportExportUtils.importInt( info_map, "p", 0 );
      int    seeds_leechers   =  ImportExportUtils.importInt( info_map, "q", -1 );
      byte  cnet       =  (byte)ImportExportUtils.importInt( info_map, "c", (int)ContentNetwork.CONTENT_NETWORK_UNKNOWN );
     
      if ( cc == null ){
     
        return( new DownloadInfo( hash, hash, title, rand, tracker, 0, false, size, date, seeds_leechers, cnet ));
       
      }else{
       
        boolean unread = ImportExportUtils.importBoolean( info_map, "u" );
       
        int[] rand_list = ImportExportUtils.importIntArray( info_map, "l" );
       
        int  last_seen = ImportExportUtils.importInt( info_map, "s" );
       
        int  level = ImportExportUtils.importInt( info_map, "e" );
       
        return( new DownloadInfo( hash, title, rand, tracker, unread, rand_list, last_seen, level, size, date, seeds_leechers, cnet, cc ));
      }
    }catch( Throwable e ){
     
      Debug.out( e );
     
      return( null );
    }
  }
 
  protected class
  DownloadInfo
    extends RelatedContent
  {
    final private int    rand;
   
    private boolean      unread  = true;
    private int[]      rand_list;
    private int        last_seen;
    private int        level;
    private boolean      explicit;
   
      // we *need* this reference here to maange garbage collection correctly
   
    private ContentCache  cc;
   
    protected
    DownloadInfo(
      byte[]    _related_to,
      byte[]    _hash,
      String    _title,
      int      _rand,
      String    _tracker,
      int      _level,
      boolean    _explicit,
      long    _size,
      int      _date,
      int      _seeds_leechers,
      byte    _cnet )
    {
      super( _related_to, _title, _hash, _tracker, _size, _date, _seeds_leechers, _cnet );
     
      rand    = _rand;
      level    = _level;
      explicit  = _explicit;
     
      updateLastSeen();
    }
   
    protected
    DownloadInfo(
      byte[]      _hash,
      String      _title,
      int        _rand,
      String      _tracker,
      boolean      _unread,
      int[]      _rand_list,
      int        _last_seen,
      int        _level,
      long      _size,
      int        _date,
      int        _seeds_leechers,
      byte      _cnet,
      ContentCache  _cc )
    {
      super( _title, _hash, _tracker, _size, _date, _seeds_leechers, _cnet );
     
      rand    = _rand;
      unread    = _unread;
      rand_list  = _rand_list;
      last_seen  = _last_seen;
      level    = _level;
      cc      = _cc;
     
      if ( rand_list != null ){
       
        if ( rand_list.length > MAX_RANK ){
         
          int[] temp = new int[ MAX_RANK ];
         
          System.arraycopy( rand_list, 0, temp, 0, MAX_RANK );
           
          rand_list = temp;
        }
      }
    }
   
    protected boolean
    addInfo(
      DownloadInfo    info )
    {
      boolean  result = false;
     
      synchronized( this ){
   
        updateLastSeen();
       
        int r = info.getRand();
       
        if ( rand_list == null ){
         
          rand_list = new int[]{ r };
                   
          result  = true;
         
        }else{
         
          boolean  match = false;
         
          for (int i=0;i<rand_list.length;i++){
           
            if ( rand_list[i] == r ){
             
              match = true;
             
              break;
            }
          }
         
          if ( !match && rand_list.length < MAX_RANK ){
           
            int  len = rand_list.length;
           
            int[]  new_rand_list = new int[len+1];
           
            System.arraycopy( rand_list, 0, new_rand_list, 0, len );
           
            new_rand_list[len] = r;
           
            rand_list = new_rand_list;
           
            result = true;
          }
        }
       
        if ( info.getLevel() < level ){
         
          level = info.getLevel();
         
          result = true;
        }
       
        long cn =  info.getContentNetwork();
       
        if (   cn != ContentNetwork.CONTENT_NETWORK_UNKNOWN &&
            getContentNetwork() == ContentNetwork.CONTENT_NETWORK_UNKNOWN ){
         
          setContentNetwork( cn );
        }
       
        int sl = info.getSeedsLeechers();
       
        if ( sl != -1 && sl != getSeedsLeechers()){
         
          setSeedsLeechers( sl );
         
          result = true;
        }
       
        int  d = info.getDateHours();
       
        if ( d > 0 && d != getDateHours()){
         
          setDateHours( d );
         
          result = true;
        }
      }
     
      return( result );
    }
   
    public int
    getLevel()
    {
      return( level );
    }
   
    protected boolean
    isExplicit()
    {
      return( explicit );
    }
   
    protected void
    setExplicit(
      boolean    b )
    {
      explicit  = b;
    }
   
    protected void
    updateLastSeen()
    {
        // persistence of this is piggy-backed on other saves to limit resource usage
        // only therefore a vague measure

      last_seen  = (int)( SystemTime.getCurrentTime()/1000 );
    }
   
    public int
    getRank()
    {
      return( rand_list==null?0:rand_list.length );
    }
   
    public boolean
    isUnread()
    {
      return( unread );
    }
   
    protected void
    setPublic(
      ContentCache  _cc )
    {
      cc  = _cc;
     
      if ( unread ){
       
        incrementUnread();
      }
     
      rand_list = new int[]{ rand };
    }
   
    public int
    getLastSeenSecs()
    {
      return( last_seen );
    }
   
    protected void
    setUnreadInternal(
      boolean  _unread )
    {
      synchronized( this ){

        unread = _unread;
      }
    }
   
    public void
    setUnread(
      boolean  _unread )
    {
      boolean  changed = false;
     
      synchronized( this ){
       
        if ( unread != _unread ){
       
          unread = _unread;
         
          changed = true;
        }
      }
     
      if ( changed ){
     
        if ( _unread ){
         
          incrementUnread();
         
        }else{
         
          decrementUnread();
        }
       
        contentChanged( this );
      }
    }
   
    protected int
    getRand()
    {
      return( rand );
    }
   
    protected int[]
    getRandList()
    {
      return( rand_list );
    }
   
    public Download
    getRelatedToDownload()
    {
      try{
        return( getDownload( getRelatedToHash()));
       
      }catch( Throwable e ){
       
        Debug.out( e );
       
        return( null );
      }
    }
   
    public void
    delete()
    {
      RelatedContentManager.this.delete( new RelatedContent[]{ this });
    }
   
    public String
    getString()
    {
      return( super.getString() + ", " + rand + ", rl=" + rand_list + ", last_seen=" + last_seen + ", level=" + level );
    }
  }
 
  private static class
  ContentCache
  {
    private Map<String,DownloadInfo>            related_content      = new HashMap<String, DownloadInfo>();
    private ByteArrayHashMapEx<ArrayList<DownloadInfo>>    related_content_map    = new ByteArrayHashMapEx<ArrayList<DownloadInfo>>();
  }
 
  private static class
  SecondaryLookup
  {
    final private byte[]  hash;
    final private int    level;
   
    protected
    SecondaryLookup(
      byte[]    _hash,
      int      _level )
    {
      hash  = _hash;
      level  = _level;
    }
   
    protected byte[]
    getHash()
    {
      return( hash );
    }
   
    protected int
    getLevel()
    {
      return( level );
    }
  }
 
  protected class
  RCMSearchXFer
    implements DistributedDatabaseTransferType
 
  }
}
TOP

Related Classes of com.aelitis.azureus.core.content.RelatedContentManager

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.