Package freenet.clients.http

Source Code of freenet.clients.http.FProxyFetchInProgress

package freenet.clients.http;

import static java.util.concurrent.TimeUnit.SECONDS;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import freenet.client.ClientMetadata;
import freenet.client.DefaultMIMETypes;
import freenet.client.FetchContext;
import freenet.client.FetchException;
import freenet.client.FetchException.FetchExceptionMode;
import freenet.client.FetchResult;
import freenet.client.async.CacheFetchResult;
import freenet.client.async.ClientContext;
import freenet.client.async.ClientGetCallback;
import freenet.client.async.ClientGetter;
import freenet.client.async.PersistenceDisabledException;
import freenet.client.events.ClientEvent;
import freenet.client.events.ClientEventListener;
import freenet.client.events.ExpectedFileSizeEvent;
import freenet.client.events.ExpectedMIMEEvent;
import freenet.client.events.SendingToNetworkEvent;
import freenet.client.events.SplitfileProgressEvent;
import freenet.client.filter.ContentFilter;
import freenet.client.filter.FilterMIMEType;
import freenet.client.filter.UnknownContentTypeException;
import freenet.keys.FreenetURI;
import freenet.keys.USK;
import freenet.node.RequestClient;
import freenet.support.LogThresholdCallback;
import freenet.support.Logger;
import freenet.support.Logger.LogLevel;
import freenet.support.api.Bucket;
import freenet.support.io.Closer;

/**
* Fetching a page for a browser.
*
* LOCKING: The lock on this object is always taken last.
*/
public class FProxyFetchInProgress implements ClientEventListener, ClientGetCallback {
 
  /** What to do when we find data which matches the request but it has already been
   * filtered, assuming we want a filtered copy. */
  public enum REFILTER_POLICY {
    RE_FILTER, // Re-run the filter over data that has already been filtered. Probably requires allocating a new temp file.
    ACCEPT_OLD, // Accept the old filtered data. Only as safe as the filter when the data was originally downloaded.
    RE_FETCH // Fetch the data again. Unnecessary in most cases, avoids any possibility of filter artefacts.
  }
 
  private final REFILTER_POLICY refilterPolicy;
 
  private static volatile boolean logMINOR;
 
  static {
    Logger.registerLogThresholdCallback(new LogThresholdCallback() {
     
      @Override
      public void shouldUpdate() {
        logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
      }
    });
  }
 
  /** The key we are fetching */
  public final FreenetURI uri;
  /** The maximum size specified by the client */
  public final long maxSize;
  /** Fetcher */
  private final ClientGetter getter;
  /** Any request which is waiting for a progress screen or data.
   * We may want to wake requests separately in future. */
  private final ArrayList<FProxyFetchWaiter> waiters;
  private final ArrayList<FProxyFetchResult> results;
  /** Gets notified with every change*/
  private final List<FProxyFetchListener> listener=Collections.synchronizedList(new ArrayList<FProxyFetchListener>());
  /** The data, if we have it */
  private Bucket data;
  /** Creation time */
  private final long timeStarted;
  /** Finished? */
  private boolean finished;
  /** Size, if known */
  private long size;
  /** MIME type, if known */
  private String mimeType;
  /** Gone to network? */
  private boolean goneToNetwork;
  /** Total blocks */
  private int totalBlocks;
  /** Required blocks */
  private int requiredBlocks;
  /** Fetched blocks */
  private int fetchedBlocks;
  /** Failed blocks */
  private int failedBlocks;
  /** Fatally failed blocks */
  private int fatallyFailedBlocks;
  private int fetchedBlocksPreNetwork;
  /** Finalized the block set? */
  private boolean finalizedBlocks;
  /** Fetch failed */
  private FetchException failed;
  private boolean hasWaited;
  private boolean hasNotifiedFailure;
  /** Last time the fetch was accessed from the fproxy end */
  private long lastTouched;
  final FProxyFetchTracker tracker;
  /** Show even non-fatal failures for 5 seconds. Necessary for javascript to work,
   * because it fetches the page and then reloads it if it isn't a progress update. */
  private long timeFailed;
  /** If this is set, then it can be removed instantly, doesn't need to wait for 30sec*/
  private boolean requestImmediateCancel=false;
  private int fetched = 0;
  /** Stores the fetch context this class was created with*/
  private FetchContext fctx;
  private boolean cancelled = false;
  private final RequestClient rc;
 
  public FProxyFetchInProgress(FProxyFetchTracker tracker, FreenetURI key, long maxSize2, long identifier, ClientContext context, FetchContext fctx, RequestClient rc, REFILTER_POLICY refilter) {
    this.refilterPolicy = refilter;
    this.tracker = tracker;
    this.uri = key;
    this.maxSize = maxSize2;
    this.timeStarted = System.currentTimeMillis();
    this.fctx = fctx;
        this.rc = rc;
    FetchContext alteredFctx = new FetchContext(fctx, FetchContext.IDENTICAL_MASK);
    alteredFctx.maxOutputLength = fctx.maxTempLength = maxSize;
    alteredFctx.eventProducer.addEventListener(this);
    waiters = new ArrayList<FProxyFetchWaiter>();
    results = new ArrayList<FProxyFetchResult>();
    getter = new ClientGetter(this, uri, alteredFctx, FProxyToadlet.PRIORITY, null, null, null);
  }
 
  public synchronized FProxyFetchWaiter getWaiter() {
    lastTouched = System.currentTimeMillis();
    FProxyFetchWaiter waiter = new FProxyFetchWaiter(this);
    waiters.add(waiter);
    return waiter;
  }
 
  public FProxyFetchTracker getTracker() {
    return tracker;
  }
 
  public synchronized void addCustomWaiter(FProxyFetchWaiter waiter){
    waiters.add(waiter);
  }

  synchronized FProxyFetchResult innerGetResult(boolean hasWaited) {
    lastTouched = System.currentTimeMillis();
    FProxyFetchResult res;
    if(data != null)
      res = new FProxyFetchResult(this, data, mimeType, timeStarted, goneToNetwork, getETA(), hasWaited);
    else {
      res = new FProxyFetchResult(this, mimeType, size, timeStarted, goneToNetwork,
          totalBlocks, requiredBlocks, fetchedBlocks, failedBlocks, fatallyFailedBlocks, finalizedBlocks, failed, getETA(), hasWaited);
    }
    results.add(res);
    if(data != null || failed != null) {
      res.setFetchCount(fetched);
      fetched++;
    }
    return res;
  }

  public void start(ClientContext context) throws FetchException {
    try {
      if(!checkCache(context))
        context.start(getter);
    } catch (FetchException e) {
      synchronized(this) {
        this.failed = e;
        this.finished = true;
      }
    } catch (PersistenceDisabledException e) {
      // Impossible
      Logger.error(this, "Failed to start: "+e);
      synchronized(this) {
        this.failed = new FetchException(FetchExceptionMode.INTERNAL_ERROR, e);
        this.finished = true;
      }
    }
  }

  /** Look up the key in the downloads queue.
   * @return True if it was found and we don't need to start the request. */
  private boolean checkCache(ClientContext context) {
    // Fproxy uses lookupInstant() with mustCopy = false. I.e. it can reuse stuff unsafely. If the user frees it it's their fault.
    if(bogusUSK(context)) return false;
    CacheFetchResult result = context.downloadCache == null ? null : context.downloadCache.lookupInstant(uri, !fctx.filterData, false, null);
    if(result == null) return false;
    Bucket data = null;
    String mimeType = null;
    if((!fctx.filterData) && (!result.alreadyFiltered)) {
      if(fctx.overrideMIME == null || fctx.overrideMIME.equals(result.getMimeType())) {
        // Works as-is.
        // Any time we re-use old content we need to remove the tracker because it may not remain available.
        tracker.removeFetcher(this);
        onSuccess(result, null);
        return true;
      } else if(fctx.overrideMIME != null && !fctx.overrideMIME.equals(result.getMimeType())) {
        // Change the MIME type.
        tracker.removeFetcher(this);
        onSuccess(new FetchResult(new ClientMetadata(fctx.overrideMIME), result.asBucket()), null);
        return true;
      }
    } else if(result.alreadyFiltered) {
      if(refilterPolicy == REFILTER_POLICY.RE_FETCH || !fctx.filterData) {
        // Can't use it.
        return false;
      } else if(fctx.filterData) {
        if(shouldAcceptCachedFilteredData(fctx, result)) {
          if(refilterPolicy == REFILTER_POLICY.ACCEPT_OLD) {
            tracker.removeFetcher(this);
            onSuccess(result, null);
            return true;
          } // else re-filter
        } else
          return false;
      } else {
        return false;
      }
    }
    data = result.asBucket();
    mimeType = result.getMimeType();
    if(mimeType == null || mimeType.equals("")) mimeType = DefaultMIMETypes.DEFAULT_MIME_TYPE;
    if(fctx.overrideMIME != null && !result.alreadyFiltered)
      mimeType = fctx.overrideMIME;
    else if(fctx.overrideMIME != null && !mimeType.equals(fctx.overrideMIME)) {
      // Doesn't work.
      return false;
    }
    String fullMimeType = mimeType;
    mimeType = ContentFilter.stripMIMEType(mimeType);
    FilterMIMEType type = ContentFilter.getMIMEType(mimeType);
    if(type == null || ((!type.safeToRead) && type.readFilter == null)) {
      UnknownContentTypeException e = new UnknownContentTypeException(mimeType);
      data.free();
      onFailure(new FetchException(e.getFetchErrorCode(), data.size(), e, mimeType), null);
      return true;
    } else if(type.safeToRead) {
      tracker.removeFetcher(this);
      onSuccess(new FetchResult(new ClientMetadata(mimeType), data), null);
      return true;
    } else {
      // Try to filter it.
      Bucket output = null;
      InputStream is = null;
      OutputStream os = null;
      try {
        output = context.tempBucketFactory.makeBucket(-1);
        is = data.getInputStream();
        os = output.getOutputStream();
        ContentFilter.filter(is, os, fullMimeType, uri.toURI("/"), null, null, fctx.charset, context.linkFilterExceptionProvider);
        is.close();
        is = null;
        os.close();
        os = null;
        // Since we are not re-using the data bucket, we can happily stay in the FProxyFetchTracker.
        this.onSuccess(new FetchResult(new ClientMetadata(fullMimeType), output), null);
        output = null;
        return true;
      } catch (IOException e) {
        Logger.normal(this, "Failed filtering coalesced data in fproxy");
        // Failed. :|
        // Let it run normally.
        return false;
      } catch (URISyntaxException e) {
        Logger.error(this, "Impossible: "+e, e);
        return false;
      } finally {
        Closer.close(is);
        Closer.close(os);
        Closer.close(output);
        Closer.close(data);
      }
    }
  }

  /** If the key is a USK and a) we are requested to do an exhaustive search, or b)
   * there is a later version, then we can't use the download queue as a cache.
   * @return True if we can't use the download queue, false if we can. */
  private boolean bogusUSK(ClientContext context) {
    if(!uri.isUSK()) return false;
    long edition = uri.getSuggestedEdition();
    if(edition < 0)
      return true; // Need to do the fetch.
    USK usk;
    try {
      usk = USK.create(uri);
    } catch (MalformedURLException e) {
      return false; // Will fail later.
    }
    long ret = context.uskManager.lookupKnownGood(usk);
    if(ret == -1) return false;
    return ret > edition;
  }

  private boolean shouldAcceptCachedFilteredData(FetchContext fctx,
      CacheFetchResult result) {
    // FIXME allow the charset if it's the same
    if(fctx.charset != null) return false;
    if(fctx.overrideMIME == null) {
      return true;
    } else {
      String finalMIME = result.getMimeType();
      if(fctx.overrideMIME.equals(finalMIME))
        return true;
      else if(ContentFilter.stripMIMEType(finalMIME).equals(fctx.overrideMIME) && fctx.charset == null)
        return true;
      // FIXME we could make this work in a few more cases... it doesn't matter much though as usually people don't override the MIME type!
    }
    return false;
  }

  @Override
  public void receive(ClientEvent ce, ClientContext context) {
    try{
      if(ce instanceof SplitfileProgressEvent) {
        SplitfileProgressEvent split = (SplitfileProgressEvent) ce;
        synchronized(this) {
          int oldReq = requiredBlocks - (fetchedBlocks + failedBlocks + fatallyFailedBlocks);
          totalBlocks = split.totalBlocks;
          fetchedBlocks = split.succeedBlocks;
          requiredBlocks = split.minSuccessfulBlocks;
          failedBlocks = split.failedBlocks;
          fatallyFailedBlocks = split.fatallyFailedBlocks;
          finalizedBlocks = split.finalizedTotal;
          int req = requiredBlocks - (fetchedBlocks + failedBlocks + fatallyFailedBlocks);
          if(!(req > 1024 && oldReq <= 1024)) return;
        }
      } else if(ce instanceof SendingToNetworkEvent) {
        synchronized(this) {
          if(goneToNetwork) return;
          goneToNetwork = true;
          fetchedBlocksPreNetwork = fetchedBlocks;
        }
      } else if(ce instanceof ExpectedMIMEEvent) {
        synchronized(this) {
          this.mimeType = ((ExpectedMIMEEvent)ce).expectedMIMEType;
        }
        if(!goneToNetwork) return;
      } else if(ce instanceof ExpectedFileSizeEvent) {
        synchronized(this) {
          this.size = ((ExpectedFileSizeEvent)ce).expectedSize;
        }
        if(!goneToNetwork) return;
      } else return;
      wakeWaiters(false);
    }finally{
      for(FProxyFetchListener l:new ArrayList<FProxyFetchListener>(listener)){
        l.onEvent();
      }
    }
  }

  private void wakeWaiters(boolean finished) {
    FProxyFetchWaiter[] waiting;
    synchronized(this) {
      waiting = waiters.toArray(new FProxyFetchWaiter[waiters.size()]);
    }
    for(FProxyFetchWaiter w : waiting) {
      w.wakeUp(finished);
    }
    if(finished==true){
      for(FProxyFetchListener l:new ArrayList<FProxyFetchListener>(listener)){
        l.onEvent();
      }
    }
  }

  @Override
  public void onFailure(FetchException e, ClientGetter state) {
    synchronized(this) {
      this.failed = e;
      this.finished = true;
      this.timeFailed = System.currentTimeMillis();
    }
    wakeWaiters(true);
  }

  @Override
  public void onSuccess(FetchResult result, ClientGetter state) {
    Bucket droppedData = null;
    synchronized(this) {
      if(cancelled)
        droppedData = result.asBucket();
      else
        this.data = result.asBucket();
      this.mimeType = result.getMimeType();
      this.finished = true;
    }
    wakeWaiters(true);
    if(droppedData != null)
      droppedData.free();
  }

  public synchronized boolean hasData() {
    return data != null;
  }

  public synchronized boolean finished() {
    return finished;
  }

  public void close(FProxyFetchWaiter waiter) {
    synchronized(this) {
      waiters.remove(waiter);
      if(!results.isEmpty()) return;
      if(!waiters.isEmpty()) return;
    }
    tracker.queueCancel(this);
  }

  /** Keep for 30 seconds after last access */
  static final long LIFETIME = SECONDS.toMillis(30);
 
  /** Caller should take the lock on FProxyToadlet.fetchers, then call this
   * function, if it returns true then finish the cancel outside the lock.
   */
  public synchronized boolean canCancel() {
    if(!waiters.isEmpty()) return false;
    if(!results.isEmpty()) return false;
    if(!listener.isEmpty()) return false;
    if(lastTouched + LIFETIME >= System.currentTimeMillis() && !requestImmediateCancel) {
      if(logMINOR) Logger.minor(this, "Not able to cancel for "+this+" : "+uri+" : "+maxSize);
      return false;
    }
    if(logMINOR) Logger.minor(this, "Can cancel for "+this+" : "+uri+" : "+maxSize);
    return true;
  }
 
  /** Finish the cancel process, freeing the data if necessary. The fetch
   * must have been removed from the tracker already, so it won't be reused. */
  public void finishCancel() {
    if(logMINOR) Logger.minor(this, "Finishing cancel for "+this+" : "+uri+" : "+maxSize);
    try {
      getter.cancel(tracker.context);
    } catch (Throwable t) {
      // Ensure we get to the next bit
      Logger.error(this, "Failed to cancel: "+t, t);
    }
    Bucket d;
    synchronized(this) {
      d = data;
      cancelled = true;
    }
    if(d != null) {
      try {
        d.free();
      } catch (Throwable t) {
        // Ensure we get to the next bit
        Logger.error(this, "Failed to free: "+t, t);
      }
    }
  }

  public void close(FProxyFetchResult result) {
    synchronized(this) {
      results.remove(result);
      if(!results.isEmpty()) return;
      if(!waiters.isEmpty()) return;
    }
    tracker.queueCancel(this);
  }
 
  public synchronized long getETA() {
    if(!goneToNetwork) return -1;
    if(requiredBlocks <= 0) return -1;
    if(fetchedBlocks >= requiredBlocks) return -1;
    if(fetchedBlocks - fetchedBlocksPreNetwork < 5) return -1;
    return ((System.currentTimeMillis() - timeStarted) * (requiredBlocks - fetchedBlocksPreNetwork)) / (fetchedBlocks - fetchedBlocksPreNetwork);
  }

  public synchronized boolean notFinishedOrFatallyFinished() {
    if(data == null && failed == null) return true;
    if(failed != null && failed.isFatal()) return true;
    if(failed != null && !hasNotifiedFailure) {
      hasNotifiedFailure = true;
      return true;
    }
    if(failed != null && (System.currentTimeMillis() - timeFailed < 1000 || fetched < 2)) // Once for javascript and once for the user when it re-pulls.
      return true;
    return false;
  }
 
  public synchronized boolean hasNotifiedFailure() {
    return true;
  }

  public synchronized boolean hasWaited() {
    return hasWaited;
  }

  public synchronized void setHasWaited() {
    hasWaited = true;
  }
 
  /** Adds a listener that will be notified when a change occurs to this fetch
   * @param listener - The listener to be added*/
  public synchronized void addListener(FProxyFetchListener listener){
    if(logMINOR){
      Logger.minor(this,"Registered listener:"+listener);
    }
    this.listener.add(listener);
  }
 
  /** Removes a listener
   * @param listener - The listener to be removed*/
  public synchronized void removeListener(FProxyFetchListener listener){
    if(logMINOR){
      Logger.minor(this,"Removed listener:"+listener);
    }
    this.listener.remove(listener);
    if(logMINOR){
      Logger.minor(this,"can cancel now?:"+canCancel());
    }
  }
 
  /** Allows the fetch to be removed immediately*/
  public synchronized void requestImmediateCancel(){
    requestImmediateCancel=true;
  }

  public synchronized long lastTouched() {
    return lastTouched;
  }
 
  public boolean fetchContextEquivalent(FetchContext context) {
    if(this.fctx.filterData != context.filterData) return false;
    if(this.fctx.maxOutputLength != context.maxOutputLength) return false;
    if(this.fctx.maxTempLength != context.maxTempLength) return false;
    if(this.fctx.charset == null && context.charset != null) return false;
    if(this.fctx.charset != null && !this.fctx.charset.equals(context.charset)) return false;
    if(this.fctx.overrideMIME == null && context.overrideMIME != null) return false;
    if(this.fctx.overrideMIME != null && !this.fctx.overrideMIME.equals(context.overrideMIME)) return false;
    return true;
  }

    @Override
    public void onResume(ClientContext context) {
        throw new UnsupportedOperationException(); // Not persistent.
    }

    @Override
    public RequestClient getRequestClient() {
        return rc;
    }

}
TOP

Related Classes of freenet.clients.http.FProxyFetchInProgress

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.