Package freenet.client.async

Source Code of freenet.client.async.SingleFileInserter$SplitHandler

package freenet.client.async;

import java.io.IOException;
import java.io.Serializable;
import java.net.MalformedURLException;
import java.util.HashMap;

import freenet.client.ClientMetadata;
import freenet.client.InsertBlock;
import freenet.client.InsertContext;
import freenet.client.InsertException;
import freenet.client.InsertException.InsertExceptionMode;
import freenet.client.Metadata;
import freenet.client.MetadataUnresolvedException;
import freenet.client.ArchiveManager.ARCHIVE_TYPE;
import freenet.client.InsertContext.CompatibilityMode;
import freenet.client.Metadata.DocumentType;
import freenet.client.events.FinishedCompressionEvent;
import freenet.client.events.ExpectedHashesEvent;
import freenet.client.events.StartedCompressionEvent;
import freenet.crypt.HashResult;
import freenet.crypt.HashType;
import freenet.crypt.MultiHashOutputStream;
import freenet.keys.BaseClientKey;
import freenet.keys.CHKBlock;
import freenet.keys.FreenetURI;
import freenet.keys.SSKBlock;
import freenet.support.LogThresholdCallback;
import freenet.support.Logger;
import freenet.support.Logger.LogLevel;
import freenet.support.api.Bucket;
import freenet.support.api.LockableRandomAccessBuffer;
import freenet.support.api.RandomAccessBucket;
import freenet.support.compress.Compressor.COMPRESSOR_TYPE;
import freenet.support.io.BucketTools;
import freenet.support.io.NotPersistentBucket;
import freenet.support.io.NullOutputStream;
import freenet.support.io.ResumeFailedException;

/**
* Attempt to insert a file. May include metadata.
*
* This stage:
* Attempt to compress the file. Off-thread if it will take a while.
* Then hand it off to some combination of SingleBlockInserters and SplitFileInserters, possibly
* under the supervision of its own handler class SplitHandler.
*
* WARNING: Changing non-transient members on classes that are Serializable can result in
* losing uploads.
*/
class SingleFileInserter implements ClientPutState, Serializable {

    private static final long serialVersionUID = 1L;
    private static volatile boolean logMINOR;
  private static volatile boolean logDEBUG;
 
  static {
    Logger.registerLogThresholdCallback(new LogThresholdCallback() {
     
      @Override
      public void shouldUpdate() {
        logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
        logDEBUG = Logger.shouldLog(LogLevel.MINOR, this);
      }
    });
  }
 
  final BaseClientPutter parent;
  InsertBlock block;
  final InsertContext ctx;
  final boolean metadata;
  final PutCompletionCallback cb;
  final ARCHIVE_TYPE archiveType;
  /** If true, we are not the top level request, and should not
   * update our parent to point to us as current put-stage. */
  private final boolean reportMetadataOnly;
  public final Object token;
  private final boolean freeData; // this is being set, but never read ???
  private final String targetFilename;
  private final boolean persistent;
  private boolean started;
  private boolean cancelled;
  private final boolean forSplitfile;
  private final long origDataLength;
  private final long origCompressedDataLength;
  private HashResult[] origHashes;
  /** If true, use random crypto keys for CHKs. */
  private final byte[] forceCryptoKey;
  private final byte cryptoAlgorithm;
  private final boolean realTimeFlag;
  /** When positive, means we will return metadata rather than a URI, once the
   * metadata is under this length. If it is too short it is still possible to
   * return a URI, but we won't return both. */
  private final long metadataThreshold;
 
  // A persistent hashCode is helpful in debugging, and also means we can put
  // these objects into sets etc when we need to.
 
  private final int hashCode;
 
  @Override
  public int hashCode() {
    return hashCode;
  }

  /**
   * @param parent
   * @param cb
   * @param block
   * @param metadata
   * @param ctx
   * @param dontCompress
   * @param getCHKOnly
   * @param reportMetadataOnly If true, don't insert the metadata, just report it.
   * @param insertAsArchiveManifest If true, insert the metadata as an archive manifest.
   * @param freeData If true, free the data when possible.
   * @param targetFilename
   * @param earlyEncode If true, try to get a URI as quickly as possible.
   * @param metadataThreshold
   * @throws InsertException
   */
  SingleFileInserter(BaseClientPutter parent, PutCompletionCallback cb, InsertBlock block,
      boolean metadata, InsertContext ctx, boolean realTimeFlag, boolean dontCompress,
      boolean reportMetadataOnly, Object token, ARCHIVE_TYPE archiveType,
      boolean freeData, String targetFilename, boolean forSplitfile, boolean persistent, long origDataLength, long origCompressedDataLength, HashResult[] origHashes, byte cryptoAlgorithm, byte[] forceCryptoKey, long metadataThreshold) {
    hashCode = super.hashCode();
    this.reportMetadataOnly = reportMetadataOnly;
    this.token = token;
    this.parent = parent;
    this.block = block;
    this.ctx = ctx;
    this.realTimeFlag = realTimeFlag;
    this.metadata = metadata;
    this.cb = cb;
    this.archiveType = archiveType;
    this.freeData = freeData;
    this.targetFilename = targetFilename;
    this.persistent = persistent;
    this.forSplitfile = forSplitfile;
    this.origCompressedDataLength = origCompressedDataLength;
    this.origDataLength = origDataLength;
    this.origHashes = origHashes;
    this.forceCryptoKey = forceCryptoKey;
    this.cryptoAlgorithm = cryptoAlgorithm;
    this.metadataThreshold = metadataThreshold;
    if(logMINOR) Logger.minor(this, "Created "+this+" persistent="+persistent+" freeData="+freeData);
  }
 
  public void start(ClientContext context) throws InsertException {
    tryCompress(context);
  }

  void onCompressed(CompressionOutput output, ClientContext context) {
      synchronized(this) {
          if(started) {
              Logger.error(this, "Already started, not starting again", new Exception("error"));
              return;
          }
          if(cancelled) {
              Logger.error(this, "Already cancelled, not starting");
              return;
          }
      }
    try {
      onCompressedInner(output, context);
    } catch (InsertException e) {
      cb.onFailure(e, SingleFileInserter.this, context);
        } catch (Throwable t) {
            Logger.error(this, "Caught in OffThreadCompressor: "+t, t);
            System.err.println("Caught in OffThreadCompressor: "+t);
            t.printStackTrace();
            // Try to fail gracefully
      cb.onFailure(new InsertException(InsertExceptionMode.INTERNAL_ERROR, t, null), SingleFileInserter.this, context);
    }
  }
 
  void onCompressedInner(CompressionOutput output, ClientContext context) throws InsertException {
    HashResult[] hashes = output.hashes;
    long origSize = block.getData().size();
    byte[] hashThisLayerOnly = null;
    if(hashes != null && metadata) {
      hashThisLayerOnly = HashResult.get(hashes, HashType.SHA256);
      hashes = null; // Inherit origHashes
    }
    if(hashes != null) {
      if(logDEBUG) {
        Logger.debug(this, "Computed hashes for "+this+" for "+block.desiredURI+" size "+origSize);
        for(HashResult res : hashes) {
          Logger.debug(this, res.type.name()+" : "+res.hashAsHex());
        }
      }
      HashResult[] clientHashes = hashes;
      if(persistent) clientHashes = HashResult.copy(hashes);
      ctx.eventProducer.produceEvent(new ExpectedHashesEvent(clientHashes), context);
     
      // So it is passed on.
      origHashes = hashes;
    } else {
      hashes = origHashes; // Inherit so it goes all the way to the top.
    }
    RandomAccessBucket bestCompressedData = output.data;
    long bestCompressedDataSize = bestCompressedData.size();
    RandomAccessBucket data = bestCompressedData;
    COMPRESSOR_TYPE bestCodec = output.bestCodec;
   
    boolean shouldFreeData = freeData;
    if(bestCodec != null) {
      if(logMINOR) Logger.minor(this, "The best compression algorithm is "+bestCodec+ " we have gained"+ (100-(bestCompressedDataSize*100/origSize)) +"% ! ("+origSize+'/'+bestCompressedDataSize+')');
      shouldFreeData = true; // must be freed regardless of whether the original data was to be freed
      if(freeData) {
        block.getData().free();
      }
      block.nullData();
    } else {
      data = block.getData();
      bestCompressedDataSize = origSize;
    }

    int blockSize;
    int oneBlockCompressedSize;
   
    boolean isCHK = false;
    String type = block.desiredURI.getKeyType();
    boolean isUSK = false;
    if(type.equals("SSK") || type.equals("KSK") || (isUSK = type.equals("USK"))) {
      blockSize = SSKBlock.DATA_LENGTH;
      oneBlockCompressedSize = SSKBlock.MAX_COMPRESSED_DATA_LENGTH;
    } else if(type.equals("CHK")) {
      blockSize = CHKBlock.DATA_LENGTH;
      oneBlockCompressedSize = CHKBlock.MAX_COMPRESSED_DATA_LENGTH;
      isCHK = true;
    } else {
      throw new InsertException(InsertExceptionMode.INVALID_URI, "Unknown key type: "+type, null);
    }
   
    // Compressed data ; now insert it
    // We do NOT need to switch threads here: the actual compression is done by InsertCompressor on the RealCompressor thread,
    // which then switches either to the database thread or to a new executable to run this method.
   
    if(parent == cb) {
      short codecID = bestCodec == null ? -1 : bestCodec.metadataID;
      ctx.eventProducer.produceEvent(new FinishedCompressionEvent(codecID, origSize, bestCompressedDataSize), context);
      if(logMINOR) Logger.minor(this, "Compressed "+origSize+" to "+data.size()+" on "+this+" data = "+data);
    }
   
    // Insert it...
    short codecNumber = bestCodec == null ? -1 : bestCodec.metadataID;
    long compressedDataSize = data.size();
    boolean fitsInOneBlockAsIs = bestCodec == null ? compressedDataSize <= blockSize : compressedDataSize <= oneBlockCompressedSize;
    boolean fitsInOneCHK = bestCodec == null ? compressedDataSize <= CHKBlock.DATA_LENGTH : compressedDataSize <= CHKBlock.MAX_COMPRESSED_DATA_LENGTH;

    if((fitsInOneBlockAsIs || fitsInOneCHK) && origSize > Integer.MAX_VALUE)
      throw new InsertException(InsertExceptionMode.INTERNAL_ERROR, "2GB+ should not encode to one block!", null);

    boolean noMetadata = ((block.clientMetadata == null) || block.clientMetadata.isTrivial()) && targetFilename == null;
    if((noMetadata || metadata) && archiveType == null) {
      if(fitsInOneBlockAsIs) {
        if(persistent && (data instanceof NotPersistentBucket))
          data = fixNotPersistent(data, context);
        // Just insert it
        ClientPutState bi =
          createInserter(parent, data, codecNumber, ctx, cb, metadata, (int)origSize, -1, true, context, shouldFreeData, forSplitfile);
        if(logMINOR)
          Logger.minor(this, "Inserting without metadata: "+bi+" for "+this);
        cb.onTransition(this, bi, context);
        if(ctx.earlyEncode && bi instanceof SingleBlockInserter && isCHK)
          ((SingleBlockInserter)bi).getBlock(context, true);
        bi.schedule(context);
        if(!isUSK)
          cb.onBlockSetFinished(this, context);
        synchronized(this) {
            started = true;
        }
        if(persistent) {
          block.nullData();
          block = null;
        }
        return;
      }
    }
    if (fitsInOneCHK) {
      // Insert single block, then insert pointer to it
      if(persistent && (data instanceof NotPersistentBucket)) {
        data = fixNotPersistent(data, context);
      }
      if(reportMetadataOnly) {
        SingleBlockInserter dataPutter = new SingleBlockInserter(parent, data, codecNumber, FreenetURI.EMPTY_CHK_URI, ctx, realTimeFlag, cb, metadata, (int)origSize, -1, true, true, token, context, persistent, shouldFreeData, forSplitfile ? ctx.extraInsertsSplitfileHeaderBlock : ctx.extraInsertsSingleBlock, cryptoAlgorithm, forceCryptoKey);
        if(logMINOR)
          Logger.minor(this, "Inserting with metadata: "+dataPutter+" for "+this);
        Metadata meta = makeMetadata(archiveType, dataPutter.getURI(context), hashes);
        cb.onMetadata(meta, this, context);
        cb.onTransition(this, dataPutter, context);
        dataPutter.schedule(context);
        if(!isUSK)
          cb.onBlockSetFinished(this, context);
        synchronized(this) {
          // Don't delete them because they are being passed on.
          origHashes = null;
        }
      } else {
        MultiPutCompletionCallback mcb =
          new MultiPutCompletionCallback(cb, parent, token, persistent, false, ctx.earlyEncode);
        SingleBlockInserter dataPutter = new SingleBlockInserter(parent, data, codecNumber, FreenetURI.EMPTY_CHK_URI, ctx, realTimeFlag, mcb, metadata, (int)origSize, -1, true, false, token, context, persistent, shouldFreeData, forSplitfile ? ctx.extraInsertsSplitfileHeaderBlock : ctx.extraInsertsSingleBlock, cryptoAlgorithm, forceCryptoKey);
        if(logMINOR)
          Logger.minor(this, "Inserting data: "+dataPutter+" for "+this);
        Metadata meta = makeMetadata(archiveType, dataPutter.getURI(context), hashes);
        RandomAccessBucket metadataBucket;
        try {
          metadataBucket = meta.toBucket(context.getBucketFactory(persistent));
        } catch (IOException e) {
          Logger.error(this, "Caught "+e, e);
          throw new InsertException(InsertExceptionMode.BUCKET_ERROR, e, null);
        } catch (MetadataUnresolvedException e) {
          // Impossible, we're not inserting a manifest.
          Logger.error(this, "Caught "+e, e);
          throw new InsertException(InsertExceptionMode.INTERNAL_ERROR, "Got MetadataUnresolvedException in SingleFileInserter: "+e.toString(), null);
        }
        ClientPutState metaPutter = createInserter(parent, metadataBucket, (short) -1, ctx, mcb, true, (int)origSize, -1, true, context, true, false);
        if(logMINOR)
          Logger.minor(this, "Inserting metadata: "+metaPutter+" for "+this);
        mcb.addURIGenerator(metaPutter);
        mcb.add(dataPutter);
        cb.onTransition(this, mcb, context);
        Logger.minor(this, ""+mcb+" : data "+dataPutter+" meta "+metaPutter);
        mcb.arm(context);
        dataPutter.schedule(context);
        if(ctx.earlyEncode && metaPutter instanceof SingleBlockInserter)
          ((SingleBlockInserter)metaPutter).getBlock(context, true);
        metaPutter.schedule(context);
        if(!isUSK)
          cb.onBlockSetFinished(this, context);
        // Deleting origHashes is fine, we are done with them.
      }
      synchronized(this) {
          started = true;
      }
      if(persistent) {
        block.nullData();
        block = null;
      }
      return;
    }
    // Otherwise the file is too big to fit into one block
    // We therefore must make a splitfile
    // Job of SplitHandler: when the splitinserter has the metadata,
    // insert it. Then when the splitinserter has finished, and the
    // metadata insert has finished too, tell the master callback.
    LockableRandomAccessBuffer dataRAF;
        try {
            dataRAF = data.toRandomAccessBuffer();
        } catch (IOException e) {
            throw new InsertException(InsertExceptionMode.BUCKET_ERROR, e, null);
        }
    if(reportMetadataOnly) {
      SplitFileInserter sfi = new SplitFileInserter(persistent, parent, cb,
              dataRAF, shouldFreeData, ctx, context, origSize, bestCodec,
              block.clientMetadata, metadata, archiveType, cryptoAlgorithm, forceCryptoKey,
              hashThisLayerOnly, hashes, ctx.dontCompress, parent.getMinSuccessFetchBlocks(),
              parent.getTotalBlocks(), origDataLength, origCompressedDataLength,
              realTimeFlag, token);
      if(logMINOR)
        Logger.minor(this, "Inserting as splitfile: "+sfi+" for "+this);
      cb.onTransition(this, sfi, context);
      sfi.schedule(context);
      block.nullData();
      block.nullMetadata();
      synchronized(this) {
        // Don't delete them because they are being passed on.
        origHashes = null;
      }
    } else {
      CompatibilityMode cmode = ctx.getCompatibilityMode();
      boolean allowSizes = (cmode == CompatibilityMode.COMPAT_CURRENT || cmode.ordinal() >= CompatibilityMode.COMPAT_1255.ordinal());
      if(metadata) allowSizes = false;
      SplitHandler sh = new SplitHandler(origSize, compressedDataSize, allowSizes);
      SplitFileInserter sfi = new SplitFileInserter(persistent, parent, sh,
              dataRAF, shouldFreeData, ctx, context, origSize, bestCodec,
              block.clientMetadata, metadata, archiveType, cryptoAlgorithm, forceCryptoKey,
              hashThisLayerOnly, hashes, ctx.dontCompress, parent.getMinSuccessFetchBlocks(),
              parent.getTotalBlocks(), origDataLength, origCompressedDataLength,
              realTimeFlag, token);
      sh.sfi = sfi;
      if(logMINOR)
        Logger.minor(this, "Inserting as splitfile: "+sfi+" for "+sh+" for "+this);
      cb.onTransition(this, sh, context);
      sfi.schedule(context);
      synchronized(this) {
          started = true;
      }
      // SplitHandler will need this.origHashes.
    }
  }
 
  private RandomAccessBucket fixNotPersistent(RandomAccessBucket data, ClientContext context) throws InsertException {
    boolean skip = false;
    try {
      if(!skip) {
      if(logMINOR) Logger.minor(this, "Copying data from "+data+" length "+data.size());
      RandomAccessBucket newData = context.persistentBucketFactory.makeBucket(data.size());
      BucketTools.copy(data, newData);
      data.free();
      data = newData;
      }
    } catch (IOException e) {
      Logger.error(this, "Caught "+e+" while copying non-persistent data", e);
      throw new InsertException(InsertExceptionMode.BUCKET_ERROR, e, null);
    }
    // Note that SegmentedBCB *does* support splitting, so we don't need to do anything to the data
    // if it doesn't fit in a single block.
    return data;
  }

  private void tryCompress(ClientContext context) throws InsertException {
    // First, determine how small it needs to be
      RandomAccessBucket origData = block.getData();
      RandomAccessBucket data = origData;
    int blockSize;
    int oneBlockCompressedSize;
    boolean dontCompress = ctx.dontCompress;
   
    long origSize = data.size();
    String type = block.desiredURI.getKeyType().toUpperCase();
    if(type.equals("SSK") || type.equals("KSK") || type.equals("USK")) {
      blockSize = SSKBlock.DATA_LENGTH;
      oneBlockCompressedSize = SSKBlock.MAX_COMPRESSED_DATA_LENGTH;
    } else if(type.equals("CHK")) {
      blockSize = CHKBlock.DATA_LENGTH;
      oneBlockCompressedSize = CHKBlock.MAX_COMPRESSED_DATA_LENGTH;
    } else {
      throw new InsertException(InsertExceptionMode.INVALID_URI, "Unknown key type: "+type, null);
    }
   
    // We always want SHA256, even for small files.
    long wantHashes = 0;
    CompatibilityMode cmode = ctx.getCompatibilityMode();
    boolean atLeast1254 = (cmode == CompatibilityMode.COMPAT_CURRENT || cmode.ordinal() >= CompatibilityMode.COMPAT_1255.ordinal());
    if(atLeast1254) {
      // We verify this. We want it for *all* files.
      wantHashes |= HashType.SHA256.bitmask;
      // FIXME: If the user requests it, calculate the others for small files.
      // FIXME maybe the thresholds should be configurable.
      if(data.size() >= 1024*1024 && !metadata) {
        // SHA1 is common and MD5 is cheap.
        wantHashes |= HashType.SHA1.bitmask;
        wantHashes |= HashType.MD5.bitmask;
      }
      if(data.size() >= 4*1024*1024 && !metadata) {
        // Useful for cross-network, and cheap.
        wantHashes |= HashType.ED2K.bitmask;
        // Very widely supported for cross-network.
        wantHashes |= HashType.TTH.bitmask;
        // For completeness.
        wantHashes |= HashType.SHA512.bitmask;
      }
    }
    boolean tryCompress = (origSize > blockSize) && (!ctx.dontCompress) && (!dontCompress);
    if(tryCompress) {
      InsertCompressor.start(context, this, origData, oneBlockCompressedSize, context.getBucketFactory(persistent), persistent, wantHashes, !atLeast1254);
    } else {
      if(logMINOR) Logger.minor(this, "Not compressing "+origData+" size = "+origSize+" block size = "+blockSize);
      HashResult[] hashes = null;
      if(wantHashes != 0) {
        // Need to get the hashes anyway
        NullOutputStream nos = new NullOutputStream();
        MultiHashOutputStream hasher = new MultiHashOutputStream(nos, wantHashes);
        try {
          BucketTools.copyTo(data, hasher, data.size());
        } catch (IOException e) {
          throw new InsertException(InsertExceptionMode.BUCKET_ERROR, "I/O error generating hashes", e, null);
        }
        hashes = hasher.getResults();
      }
      final CompressionOutput output = new CompressionOutput(data, null, hashes);
      context.getJobRunner(persistent).queueNormalOrDrop(new PersistentJob() {

                @Override
                public boolean run(ClientContext context) {
                    onCompressed(output, context);
                    return true;
                }
         
      });
    }
  }
 
  private Metadata makeMetadata(ARCHIVE_TYPE archiveType, FreenetURI uri, HashResult[] hashes) {
    Metadata meta = null;
    boolean allowTopBlocks = origDataLength != 0;
    int req = 0;
    int total = 0;
    long data = 0;
    long compressed = 0;
    boolean topDontCompress = false;
    CompatibilityMode topCompatibilityMode = CompatibilityMode.COMPAT_UNKNOWN;
    if(allowTopBlocks) {
      req = parent.getMinSuccessFetchBlocks();
      total = parent.totalBlocks;
      topDontCompress = ctx.dontCompress;
      topCompatibilityMode = ctx.getCompatibilityMode();
      data = origDataLength;
      compressed = origCompressedDataLength;
    }
    if(archiveType != null)
      meta = new Metadata(DocumentType.ARCHIVE_MANIFEST, archiveType, null, uri, block.clientMetadata, data, compressed, req, total, topDontCompress, topCompatibilityMode, hashes);
    else // redirect
      meta = new Metadata(DocumentType.SIMPLE_REDIRECT, archiveType, null, uri, block.clientMetadata, data, compressed, req, total, topDontCompress, topCompatibilityMode, hashes);
    if(targetFilename != null) {
      HashMap<String, Object> hm = new HashMap<String, Object>();
      hm.put(targetFilename, meta);
      meta = Metadata.mkRedirectionManifestWithMetadata(hm);
    }
    return meta;
  }

  /**
   * Create an inserter, either for a USK or a single block.
   * @param forSplitfile Whether this insert is above a splitfile. This
   * affects whether we do multiple inserts of the same block. */
  private ClientPutState createInserter(BaseClientPutter parent, Bucket data, short compressionCodec,
      InsertContext ctx, PutCompletionCallback cb, boolean isMetadata, int sourceLength, int token,
      boolean addToParent, ClientContext context, boolean freeData, boolean forSplitfile) throws InsertException {
   
    FreenetURI uri = block.desiredURI;
    uri.checkInsertURI(); // will throw an exception if needed
   
    if(uri.getKeyType().equals("USK")) {
      try {
        return new USKInserter(parent, data, compressionCodec, uri, ctx, cb, isMetadata, sourceLength, token,
          addToParent, this.token, context, freeData, persistent, realTimeFlag, forSplitfile ? ctx.extraInsertsSplitfileHeaderBlock : ctx.extraInsertsSingleBlock, cryptoAlgorithm, forceCryptoKey);
      } catch (MalformedURLException e) {
        throw new InsertException(InsertExceptionMode.INVALID_URI, e, null);
      }
    } else {
      SingleBlockInserter sbi =
        new SingleBlockInserter(parent, data, compressionCodec, uri, ctx, realTimeFlag, cb, isMetadata, sourceLength, token,
            addToParent, false, this.token, context, persistent, freeData, forSplitfile ? ctx.extraInsertsSplitfileHeaderBlock : ctx.extraInsertsSingleBlock, cryptoAlgorithm, forceCryptoKey);
      // pass uri to SBI
      block.nullURI();
      return sbi;
    }
   
  }
 
  /**
   * When we get the metadata, start inserting it to our target key.
   * When we have inserted both the metadata and the splitfile,
   * call the master callback.
   */
  public class SplitHandler implements PutCompletionCallback, ClientPutState, Serializable {

        private static final long serialVersionUID = 1L;
        ClientPutState sfi;
    ClientPutState metadataPutter;
    boolean finished;
    boolean splitInsertSuccess;
    boolean metaInsertSuccess;
    boolean splitInsertSetBlocks;
    boolean metaInsertSetBlocks;
    boolean metaInsertStarted;
    boolean metaFetchable;
    final boolean persistent;
    final long origDataLength;
    final long origCompressedDataLength;
    private transient boolean resumed;
   
    // A persistent hashCode is helpful in debugging, and also means we can put
    // these objects into sets etc when we need to.
   
    private final int hashCode;
   
    @Override
    public int hashCode() {
      return hashCode;
    }

    /**
     * zero arg c'tor for db4o on jamvm
     */
    @SuppressWarnings("unused")
    private SplitHandler() {
      persistent = false;
      origDataLength = 0;
      origCompressedDataLength = 0;
      hashCode = 0;
    }

    public SplitHandler(long origDataLength, long origCompressedDataLength, boolean allowSizes) {
      // Default constructor
      this.persistent = SingleFileInserter.this.persistent;
      this.hashCode = super.hashCode();
      this.origDataLength = allowSizes ? origDataLength : 0;
      this.origCompressedDataLength = allowSizes ? origCompressedDataLength : 0;
    }

    @Override
    public synchronized void onTransition(ClientPutState oldState, ClientPutState newState, ClientContext context) {
      if(persistent) { // FIXME debug-point
        if(logMINOR) Logger.minor(this, "Transition: "+oldState+" -> "+newState);
      }
      if(oldState == sfi)
        sfi = newState;
      if(oldState == metadataPutter)
        metadataPutter = newState;
    }
   
    @Override
    public void onSuccess(ClientPutState state, ClientContext context) {
      if(logMINOR) Logger.minor(this, "onSuccess("+state+") for "+this);
      boolean lateStart = false;
      synchronized(this) {
        if(finished){
          return;
        }
        if(state == sfi) {
          if(logMINOR) Logger.minor(this, "Splitfile insert succeeded for "+this+" : "+state);
          splitInsertSuccess = true;
          if(!metaInsertSuccess && !metaInsertStarted) {
            lateStart = true;
            // Cannot remove yet because not created metadata inserter yet.
          } else {
            sfi = null;
            if(logMINOR) Logger.minor(this, "Metadata already started for "+this+" : success="+metaInsertSuccess+" started="+metaInsertStarted);
          }
        } else if(state == metadataPutter) {
          if(logMINOR) Logger.minor(this, "Metadata insert succeeded for "+this+" : "+state);
          metaInsertSuccess = true;
          metadataPutter = null;
        } else {
          Logger.error(this, "Unknown: "+state+" for "+this, new Exception("debug"));
        }
        if(splitInsertSuccess && metaInsertSuccess) {
          if(logMINOR) Logger.minor(this, "Both succeeded for "+this);
          finished = true;
          if(freeData)
            block.free();
          else {
            block.nullData();
          }
        }
      }
      if(lateStart) {
        if(startMetadata(context)) {
          synchronized(this) {
            sfi = null;
          }
        }
      }
      if(finished) {
        cb.onSuccess(this, context);
      }
    }

    @Override
    public void onFailure(InsertException e, ClientPutState state, ClientContext context) {
      boolean toFail = true;
      synchronized(this) {
        if(logMINOR)
          Logger.minor(this, "onFailure(): "+e+" on "+state+" on "+this+" sfi = "+sfi+" metadataPutter = "+metadataPutter);
        if(state == sfi) {
          sfi = null;
        } else if(state == metadataPutter) {
          metadataPutter = null;
        } else {
          Logger.error(this, "onFailure() on unknown state "+state+" on "+this, new Exception("debug"));
        }
        if(finished){
          toFail = false; // Already failed
        }
      }
      // fail() will cancel the other one, so we don't need to.
      // When it does, it will come back here, and we won't call fail(), because fail() has already set finished = true.
      if(toFail)
      fail(e, context);
    }

    @Override
    public void onMetadata(Metadata meta, ClientPutState state, ClientContext context) {
      InsertException e = null;
      if(logMINOR) Logger.minor(this, "Got metadata for "+this+" from "+state);
      synchronized(this) {
        if(finished) return;
        if(reportMetadataOnly) {
          if(state != sfi) {
            Logger.error(this, "Got metadata from unknown object "+state+" when expecting to report metadata");
            return;
          }
          metaInsertSuccess = true;
        } else if(state == metadataPutter) {
          Logger.error(this, "Got metadata for metadata");
          e = new InsertException(InsertExceptionMode.INTERNAL_ERROR, "Did not expect to get metadata for metadata inserter", null);
        } else if(state != sfi) {
          Logger.error(this, "Got metadata from unknown state "+state+" sfi="+sfi+" metadataPutter="+metadataPutter+" on "+this+" persistent="+persistent, new Exception("debug"));
          e = new InsertException(InsertExceptionMode.INTERNAL_ERROR, "Got metadata from unknown state", null);
        } else {
          // Already started metadata putter ? (in which case we've got the metadata twice)
          if(metadataPutter != null) return;
          if(metaInsertSuccess) return;
        }
      }
      if(reportMetadataOnly) {
        cb.onMetadata(meta, this, context);
        return;
      }
      if(e != null) {
        onFailure(e, state, context);
        return;
      }
     
      byte[] metaBytes;
      try {
        metaBytes = meta.writeToByteArray();
      } catch (MetadataUnresolvedException e1) {
        Logger.error(this, "Impossible: "+e1, e1);
        fail((InsertException)new InsertException(InsertExceptionMode.INTERNAL_ERROR, "MetadataUnresolvedException in SingleFileInserter.SplitHandler: "+e1, null).initCause(e1), context);
        return;
      }
     
      String metaPutterTargetFilename = targetFilename;
     
      if(targetFilename != null) {
       
        if(metaBytes.length <= Short.MAX_VALUE) {
          HashMap<String, Object> hm = new HashMap<String, Object>();
          hm.put(targetFilename, meta);
          meta = Metadata.mkRedirectionManifestWithMetadata(hm);
          metaPutterTargetFilename = null;
          try {
            metaBytes = meta.writeToByteArray();
          } catch (MetadataUnresolvedException e1) {
            Logger.error(this, "Impossible (2): "+e1, e1);
            fail((InsertException)new InsertException(InsertExceptionMode.INTERNAL_ERROR, "MetadataUnresolvedException in SingleFileInserter.SplitHandler(2): "+e1, null).initCause(e1), context);
            return;
          }
        }
      }
     
      RandomAccessBucket metadataBucket;
      try {
        metadataBucket = BucketTools.makeImmutableBucket(context.getBucketFactory(persistent), metaBytes);
      } catch (IOException e1) {
        InsertException ex = new InsertException(InsertExceptionMode.BUCKET_ERROR, e1, null);
        fail(ex, context);
        return;
      }
      ClientMetadata m = meta.getClientMetadata();
      CompatibilityMode cmode = ctx.getCompatibilityMode();
      if(!(cmode == CompatibilityMode.COMPAT_CURRENT || cmode.ordinal() >= CompatibilityMode.COMPAT_1255.ordinal()))
        m = null;
      if(metadataThreshold > 0 && metaBytes.length < metadataThreshold) {
        // FIXME what to do about m ???
        // I.e. do the other layers of metadata already include the content type?
        // It's probably already included in the splitfile, but need to check that, and test it.
        synchronized(this) {
          metaInsertSuccess = true;
        }
        cb.onMetadata(metadataBucket, state, context);
        return;
      }
      InsertBlock newBlock = new InsertBlock(metadataBucket, m, block.desiredURI);
      synchronized(this) {
          // Only the bottom layer in a multi-level splitfile pyramid has randomised keys. The rest are unpredictable anyway, and this ensures we only need to supply one key when reinserting.
          metadataPutter = new SingleFileInserter(parent, this, newBlock, true, ctx, realTimeFlag, false, false, token, archiveType, true, metaPutterTargetFilename, true, persistent, origDataLength, origCompressedDataLength, origHashes, cryptoAlgorithm, forceCryptoKey, metadataThreshold);
          if(origHashes != null) {
              // It gets passed on, and the last one deletes it.
              SingleFileInserter.this.origHashes = null;
          }
          // If EarlyEncode, then start the metadata insert ASAP, to get the key.
          // Otherwise, wait until the data is fetchable (to improve persistence).
          if(logMINOR)
              Logger.minor(this, "Created metadata putter for "+this+" : "+metadataPutter+" bucket "+metadataBucket+" size "+metadataBucket.size());
          if(!(ctx.earlyEncode || splitInsertSuccess)) return;
      }
      if(logMINOR) Logger.minor(this, "Putting metadata on "+metadataPutter+" from "+sfi+" ("+((SplitFileInserter)sfi).getLength()+ ')');
      if(!startMetadata(context)) {
        Logger.error(this, "onMetadata() yet unable to start metadata due to not having all URIs?!?!");
        fail(new InsertException(InsertExceptionMode.INTERNAL_ERROR, "onMetadata() yet unable to start metadata due to not having all URIs", null), context);
        return;
      }
      synchronized(this) {
        if(splitInsertSuccess && sfi != null) {
          sfi = null;
        }
      }
       
    }

    private void fail(InsertException e, ClientContext context) {
      if(logMINOR) Logger.minor(this, "Failing: "+e, e);
      ClientPutState oldSFI = null;
      ClientPutState oldMetadataPutter = null;
      synchronized(this) {
        if(finished){
          return;
        }
        finished = true;
        oldSFI = sfi;
        oldMetadataPutter = metadataPutter;
      }
      if(oldSFI != null)
        oldSFI.cancel(context);
      if(oldMetadataPutter != null)
        oldMetadataPutter.cancel(context);
      synchronized(this) {
        if(freeData)
          block.free();
        else {
          block.nullData();
        }
      }
      cb.onFailure(e, this, context);
    }

    @Override
    public BaseClientPutter getParent() {
      return parent;
    }

    @Override
    public void onEncode(BaseClientKey key, ClientPutState state, ClientContext context) {
      if(persistent) // FIXME debug-point
        if(logMINOR) Logger.minor(this, "onEncode() for "+this+" : "+state+" : "+key);
      synchronized(this) {
        if(state != metadataPutter) {
          if(logMINOR) Logger.minor(this, "ignored onEncode() for "+this+" : "+state);
          return;
        }
      }
      cb.onEncode(key, this, context);
    }

    @Override
    public void cancel(ClientContext context) {
      if(logMINOR) Logger.minor(this, "Cancelling "+this);
      ClientPutState oldSFI = null;
      ClientPutState oldMetadataPutter = null;
      synchronized(this) {
        oldSFI = sfi;
        oldMetadataPutter = metadataPutter;
      }
      if(oldSFI != null)
        oldSFI.cancel(context);
      if(oldMetadataPutter != null)
        oldMetadataPutter.cancel(context);
     
      // FIXME in the other cases, fail() and onSuccess(), we only free when
      // we set finished. But we haven't set finished here. Can we rely on
      // the callback and not do anything here? Note that it is in fact safe
      // to double-free, it's not safe to not free.
      if(freeData) {
        block.free();
      } else {
        block.nullData();
      }
    }

    @Override
    public void onBlockSetFinished(ClientPutState state, ClientContext context) {
      synchronized(this) {
        if(state == sfi)
          splitInsertSetBlocks = true;
        else if (state == metadataPutter)
          metaInsertSetBlocks = true;
        else
          if(logMINOR) Logger.minor(this, "Unrecognised: "+state+" in onBlockSetFinished()");
        if(!(splitInsertSetBlocks && metaInsertSetBlocks))
          return;
      }
      cb.onBlockSetFinished(this, context);
    }

    @Override
    public void schedule(ClientContext context) throws InsertException {
      sfi.schedule(context);
    }

    @Override
    public Object getToken() {
      return token;
    }

    @Override
    public void onFetchable(ClientPutState state) {

      if(persistent) // FIXME debug-point
        if(logMINOR) Logger.minor(this, "onFetchable on "+this);
     
      if(logMINOR) Logger.minor(this, "onFetchable("+state+ ')');
     
      boolean meta;
     
      synchronized(this) {
        meta = (state == metadataPutter);
        if(meta) {
          if(!metaInsertStarted) {
            Logger.error(this, "Metadata insert not started yet got onFetchable for it: "+state+" on "+this);
          }
          if(logMINOR) Logger.minor(this, "Metadata fetchable"+(metaFetchable?"":" already"));
          if(metaFetchable) return;
          metaFetchable = true;
        } else {
          if(state != sfi) {
            Logger.error(this, "onFetchable for unknown state "+state);
            return;
          }
          if(logMINOR) Logger.minor(this, "Data fetchable");
          if(metaInsertStarted) return;
        }
      }
     
      if(meta) {
        cb.onFetchable(this);
      }
    }
   
    /**
     * Start fetching metadata.
     * @param container
     * @param context
     * @return True unless we don't have all URI's and so can't remove sfi.
     */
    private boolean startMetadata(ClientContext context) {
      if(persistent) // FIXME debug-point
        if(logMINOR) Logger.minor(this, "startMetadata() on "+this);
      try {
        ClientPutState putter;
        synchronized(this) {
          if(metaInsertStarted) return true;
          putter = metadataPutter;
          if(putter == null) {
            if(logMINOR) Logger.minor(this, "Cannot start metadata yet: no metadataPutter");
          } else
            metaInsertStarted = true;
        }
        if(putter != null) {
          if(logMINOR) Logger.minor(this, "Starting metadata inserter: "+putter+" for "+this);
          putter.schedule(context);
          if(logMINOR) Logger.minor(this, "Started metadata inserter: "+putter+" for "+this);
          return true;
        } else {
          return false;
        }
      } catch (InsertException e1) {
        Logger.error(this, "Failing "+this+" : "+e1, e1);
        fail(e1, context);
        return true;
      }
    }
   
    @Override
    public void onMetadata(Bucket meta, ClientPutState state, ClientContext context) {
      if(logMINOR) Logger.minor(this, "Got metadata bucket for "+this+" from "+state);
      boolean freeIt = false;
      synchronized(this) {
        if(finished) return;
        if(state == metadataPutter) {
          // Okay, return it.
        } else if(state == sfi) {
          if(metadataPutter != null) {
            Logger.error(this, "Got metadata from "+sfi+" even though already started inserting metadata on the next layer on "+this+" !!");
            freeIt = true;
          } else {
            // Okay, return it.
            metaInsertSuccess = true; // Not going to start it now, so effectively it has succeeded.
          }
        } else if(reportMetadataOnly) {
          if(state != sfi) {
            Logger.error(this, "Got metadata from unknown object "+state+" when expecting to report metadata");
            return;
          }
          metaInsertSuccess = true;
        } else {
          Logger.error(this, "Got metadata from unknown object "+state);
          freeIt = true;
        }
      }
      if(freeIt) {
        meta.free();
        return;
      }
      cb.onMetadata(meta, this, context);
    }

        @Override
        public void onResume(ClientContext context) throws InsertException, ResumeFailedException {
            synchronized(this) {
                if(resumed) return;
                resumed = true;
            }
            if(sfi != null)
                sfi.onResume(context);
            if(metadataPutter != null)
                metadataPutter.onResume(context);
            if(sfi != null)
                sfi.schedule(context);
            if(metadataPutter != null) {
                if(ctx.earlyEncode || sfi == null || metaInsertStarted)
                    metadataPutter.schedule(context);
            }
        }

        @Override
        public void onShutdown(ClientContext context) {
            ClientPutState splitfileInserter;
            ClientPutState metadataInserter;
            synchronized(this) {
                splitfileInserter = sfi;
                metadataInserter = metadataPutter;
            }
            if(splitfileInserter != null)
                splitfileInserter.onShutdown(context);
            if(metadataInserter != null)
                metadataInserter.onShutdown(context);
        }
   
  }

  @Override
  public BaseClientPutter getParent() {
    return parent;
  }

  @Override
  public void cancel(ClientContext context) {
    if(logMINOR) Logger.minor(this, "Cancel "+this);
    synchronized(this) {
      if(cancelled) return;
      cancelled = true;
    }
    if(freeData) {
      block.free();
    }
    // Must call onFailure so get removeFrom()'ed
    cb.onFailure(new InsertException(InsertExceptionMode.CANCELLED), this, context);
  }

  @Override
  public void schedule(ClientContext context) throws InsertException {
    start(context);
  }

  @Override
  public Object getToken() {
    return token;
  }

  public void onStartCompression(COMPRESSOR_TYPE ctype, ClientContext context) {
    if(parent == cb) {
      if(ctx == null) throw new NullPointerException();
      if(ctx.eventProducer == null) throw new NullPointerException();
      ctx.eventProducer.produceEvent(new StartedCompressionEvent(ctype), context);
    }
  }
 
  synchronized boolean cancelled() {
    return cancelled;
  }
 
  synchronized boolean started() {
    return started;
  }
 
  private transient boolean resumed = false;

    @Override
    public final void onResume(ClientContext context) throws InsertException, ResumeFailedException {
        synchronized(this) {
            if(resumed) return;
            resumed = true;
        }
        if(block != null && block.getData() != null)
            block.getData().onResume(context);
        if(cb != null && cb != parent)
            cb.onResume(context);
        synchronized(this) {
            if(started || cancelled) return;
        }
        tryCompress(context);
    }

    @Override
    public void onShutdown(ClientContext context) {
        // Ignore.
    }

}
TOP

Related Classes of freenet.client.async.SingleFileInserter$SplitHandler

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.