/*
* Copyright (C) 2014 Shashank Tulsyan
*
* 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, either version 3 of the License, or
* (at your option) any later version.
*
* 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, see <http://www.gnu.org/licenses/>.
*/
package neembuu.vfs.connection.jdimpl.checks;
import neembuu.vfs.connection.checks.ContentSampleListener;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;
import neembuu.util.Throwables;
import neembuu.vfs.connection.Connection;
import neembuu.vfs.connection.NewConnectionParams;
import neembuu.vfs.connection.TransientConnectionListener;
import neembuu.vfs.connection.checks.CanSeek;
import neembuu.vfs.connection.checks.SeekingAbilityImpl;
import neembuu.vfs.connection.jdimpl.JDHTTPConnection;
import neembuu.vfs.connection.jdimpl.JD_DownloadManager;
/**
*
* @author Shashank Tulsyan
*/
public class CheckSeekingCapability {
private final JD_DownloadManager jddm;
private final AtomicReference<byte[]> firstSample = new AtomicReference<byte[]>(null);
private final AtomicReference<byte[]> offsetSample = new AtomicReference<byte[]>(null);
private final SeekingAbilityImpl sai = new SeekingAbilityImpl();
private static final Logger l = Logger.getLogger(CheckSeekingCapability.class.getName());
public CheckSeekingCapability(JD_DownloadManager jddm) {
this.jddm = jddm;
}
public SeekingAbilityImpl seekingAbility() { return sai; }
private void check(final NewConnectionParams triggeringCP)throws IOException{
NewConnectionParams ncp = createNewParams(triggeringCP);
final JDHTTPConnection c = new JDHTTPConnection(jddm, ncp);
c.setContentSampleListener(getOffsetSampleListener(triggeringCP,c));
try{c.connectAndSupply();}
catch(Throwable t){
l.log(
Level.SEVERE, "Seek checking connection closed ", t);
}
}
private void proceedChecking(NewConnectionParams triggeringCP, long offset){
final byte[]a = firstSample.get();
final byte[]a2 = offsetSample.get();
if(!checkNullOfEmpty(a, true, "first")){
sai.update(CanSeek.NO,offset);
return;
}
if(!checkNullOfEmpty(a2, false,"offset")){
sai.update(CanSeek.NO,offset);
return;
}
if(contentsEqual(firstSample.get(), offsetSample.get())){
sai.update(CanSeek.NO,offset); // assumption
// actually the content headers should be checked.
offsetSample.set(null);//we will make another check attempt some time later
}else {
sai.update(CanSeek.YES,offset);
}
}
private boolean contentsEqual(byte[]a,byte[]a2){
int length = Math.min(a.length, a2.length);
for (int i=0; i<length; i++)
if (a[i] != a2[i])
return false;
return true;
}
private boolean checkNullOfEmpty(byte[]a,boolean emptyCheck,String name){
if(a==null){
l.log(Level.SEVERE, "Cannot seek since "
+name+ " sample is null");
return false;
}
if(!emptyCheck)return true;
boolean nonEmpty = false;
for (byte b : a) {
if(b!=0){
nonEmpty=true;break;
}
}
if(!nonEmpty){
l.log(Level.SEVERE, "Cannot seek since "
+name+ " sample is empty");
return false;
}//buffer looks empty, > 50% is filled with null
return true;
}
private NewConnectionParams createNewParams(NewConnectionParams triggeringCP){
return new NewConnectionParams.Builder()
.copyFrom(triggeringCP)
.setDownloadThreadLogger(l)
.setDownloadDataChannel(new FakeDownloadDataChannel(triggeringCP.getDownloadDataChannel()))
.setOffset(Math.min(2048, triggeringCP.getReadRequestState().fileSize()))
.setTransientConnectionListener(new FakeTransientConnectionListener(
triggeringCP.getTransientConnectionListener(),
l))
.build();
}
private ContentSampleListener getOffsetSampleListener(final NewConnectionParams triggeringCP,
final JDHTTPConnection c){
return new ContentSampleListener() {
@Override public void receiveContentSample(byte[] b,long offset) {
offsetSampleChecker(triggeringCP, b, offset);
if(c!=null){ c.abort(); }
}
};
}
private void offsetSampleChecker(final NewConnectionParams triggeringCP,byte[]b,long offset){
if(!offsetSample.compareAndSet(null, b)){
l.log(Level.SEVERE, "Receiving content offset sample more than once. new->{0}", offset);
}
proceedChecking(triggeringCP,offset);
}
public ContentSampleListener getFirstSampleListener(final NewConnectionParams triggeringCP){
return new ContentSampleListener() {
@Override public void receiveContentSample(final byte[] b,final long offset) {
String c = new String(b, 0, Math.min(b.length,1024),Charset.forName("UTF-8"));
if(!firstSample.compareAndSet(null, b)){
l.log(Level.SEVERE, "Receiving content sample more than once. new->{0}", c);
if(sai.get()!=CanSeek.NO || sai.get()!=CanSeek.YES){
Throwables.start(new Runnable() { @Override public void run() {
offsetSampleChecker(triggeringCP, b, offset);
}}, "Offset sample checker thread 2 {" + triggeringCP + "}");
}
}else {
l.log(Level.SEVERE, "Received content sample {0}", c);
startCheckThread(triggeringCP);
}
}};
}
private void startCheckThread(final NewConnectionParams triggeringCP){
Throwables.start(new Runnable() { @Override public void run() {
try { check(triggeringCP); }
catch(Exception a){l
.log(Level.SEVERE,"Failed to check seeking ability",a);}
}}, "Checking seeking ability {" + triggeringCP + "}");
}
private final class FakeTransientConnectionListener implements TransientConnectionListener{
private final TransientConnectionListener inspiration;
private final Logger l;
public FakeTransientConnectionListener(TransientConnectionListener inspiration, Logger l) {
this.inspiration = inspiration; this.l = l;
}
@Override public void describeState(String state) {
inspiration.describeState(state);
}
@Override public void reportNumberOfRetriesMadeSoFar(int numberOfretries) {
l.log(Level.INFO, "Fake connection retry attempt{0}", numberOfretries);
}
@Override public void successful(Connection c, NewConnectionParams ncp) {
l.log(Level.SEVERE, "Success event send {0} earlier value={1}", new Object[]{ncp, sai.get()});
}
@Override public void failed(Throwable reason, NewConnectionParams ncp) {
l.log(Level.SEVERE, "Fake connection "+ncp+ "failed ", reason);
sai.update(CanSeek.NO,ncp.getOffset());
}
}
}