Browse Source

Starting a rewrite of the client connection layer.

rewrite-connect
Eric Kok 7 years ago
parent
commit
3417f8033e
  1. 2
      build.gradle
  2. 14
      connect/build.gradle
  3. 45
      connect/src/main/java/org/transdroid/connect/Configuration.java
  4. 30
      connect/src/main/java/org/transdroid/connect/clients/Client.java
  5. 13
      connect/src/main/java/org/transdroid/connect/clients/ClientSpec.java
  6. 11
      connect/src/main/java/org/transdroid/connect/clients/Feature.java
  7. 143
      connect/src/main/java/org/transdroid/connect/clients/rtorrent/Rtorrent.java
  8. 20
      connect/src/main/java/org/transdroid/connect/clients/rtorrent/Service.java
  9. 29
      connect/src/main/java/org/transdroid/connect/clients/rtorrent/TorrentSpec.java
  10. 100
      connect/src/main/java/org/transdroid/connect/model/Torrent.java
  11. 14
      connect/src/main/java/org/transdroid/connect/model/TorrentStatus.java
  12. 50
      connect/src/main/java/org/transdroid/connect/util/OkHttpBuilder.java
  13. 29
      connect/src/main/java/org/transdroid/connect/util/RxUtil.java
  14. 11
      connect/src/main/java/org/transdroid/connect/util/StringUtil.java
  15. 57
      connect/src/test/java/org/transdroid/connect/clients/rtorrent/RtorrentTest.java
  16. 4
      gradle/wrapper/gradle-wrapper.properties
  17. 2
      settings.gradle

2
build.gradle

@ -3,7 +3,7 @@ buildscript { @@ -3,7 +3,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.0'
classpath 'com.android.tools.build:gradle:2.2.3'
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}

14
connect/build.gradle

@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
apply plugin: 'java'
dependencies {
compile 'com.jakewharton.retrofit:retrofit2-rxjava2-adapter:1.0.0'
compile 'com.squareup.okhttp3:logging-interceptor:3.5.0'
compile 'com.github.erickok:retrofit-xmlrpc:master-SNAPSHOT'
compile 'com.burgstaller:okhttp-digest:1.10'
testCompile 'junit:junit:4.12'
testCompile 'com.google.truth:truth:0.31'
}
sourceCompatibility = "1.7"
targetCompatibility = "1.7"

45
connect/src/main/java/org/transdroid/connect/Configuration.java

@ -0,0 +1,45 @@ @@ -0,0 +1,45 @@
package org.transdroid.connect;
import com.burgstaller.okhttp.digest.Credentials;
import org.transdroid.connect.clients.Client;
import org.transdroid.connect.clients.ClientSpec;
import org.transdroid.connect.util.StringUtil;
public final class Configuration {
private final Client client;
private final String baseUrl;
private final String endpoint;
private final Credentials credentials;
private final boolean loggingEnabled;
public Configuration(Client client, String baseUrl, String endpoint, String user, String password, boolean loggingEnabled) {
this.client = client;
this.baseUrl = baseUrl;
this.endpoint = endpoint;
this.credentials = (!StringUtil.isEmpty(user) && password != null) ? new Credentials(user, password) : null;
this.loggingEnabled = loggingEnabled;
}
public String baseUrl() {
return baseUrl;
}
public String endpoint() {
return endpoint;
}
public boolean loggingEnabled() {
return loggingEnabled;
}
public Credentials credentials() {
return credentials;
}
public ClientSpec create() {
return client.create(this);
}
}

30
connect/src/main/java/org/transdroid/connect/clients/Client.java

@ -0,0 +1,30 @@ @@ -0,0 +1,30 @@
package org.transdroid.connect.clients;
import org.transdroid.connect.Configuration;
import org.transdroid.connect.clients.rtorrent.Rtorrent;
import java.util.Set;
public enum Client {
RTORRENT {
@Override
public ClientSpec create(Configuration configuration) {
return new Rtorrent(configuration);
}
@Override
Set<Feature> features() {
return Rtorrent.FEATURES;
}
};
public abstract ClientSpec create(Configuration configuration);
abstract Set<Feature> features();
public boolean supports(Feature feature) {
return features().contains(feature);
}
}

13
connect/src/main/java/org/transdroid/connect/clients/ClientSpec.java

@ -0,0 +1,13 @@ @@ -0,0 +1,13 @@
package org.transdroid.connect.clients;
import org.transdroid.connect.model.Torrent;
import io.reactivex.Flowable;
public interface ClientSpec {
Flowable<String> clientVersion();
Flowable<Torrent> torrents();
}

11
connect/src/main/java/org/transdroid/connect/clients/Feature.java

@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
package org.transdroid.connect.clients;
public enum Feature {
VERSION,
STARTING,
STOPPING,
RESUMING,
PAUSING
}

143
connect/src/main/java/org/transdroid/connect/clients/rtorrent/Rtorrent.java

@ -0,0 +1,143 @@ @@ -0,0 +1,143 @@
package org.transdroid.connect.clients.rtorrent;
import com.jakewharton.retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
import org.transdroid.connect.Configuration;
import org.transdroid.connect.clients.ClientSpec;
import org.transdroid.connect.clients.Feature;
import org.transdroid.connect.model.Torrent;
import org.transdroid.connect.model.TorrentStatus;
import org.transdroid.connect.util.OkHttpBuilder;
import org.transdroid.connect.util.RxUtil;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import io.reactivex.Flowable;
import io.reactivex.functions.Function;
import nl.nl2312.xmlrpc.Nothing;
import nl.nl2312.xmlrpc.XmlRpcConverterFactory;
import retrofit2.Retrofit;
public final class Rtorrent implements ClientSpec {
public static final Set<Feature> FEATURES = new HashSet<>();
{
FEATURES.add(Feature.VERSION);
FEATURES.add(Feature.STARTING);
FEATURES.add(Feature.STOPPING);
FEATURES.add(Feature.RESUMING);
FEATURES.add(Feature.PAUSING);
}
private final Configuration configuration;
private final Service service;
public Rtorrent(Configuration configuration) {
this.configuration = configuration;
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(configuration.baseUrl())
.client(new OkHttpBuilder(configuration).build())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(XmlRpcConverterFactory.create())
.build();
this.service = retrofit.create(Service.class);
}
@Override
public Flowable<String> clientVersion() {
return service.clientVersion(configuration.endpoint(), Nothing.NOTHING);
}
@Override
public Flowable<Torrent> torrents() {
return service.torrents(
configuration.endpoint(),
"",
"main",
"d.hash=",
"d.name=",
"d.state=",
"d.down.rate=",
"d.up.rate=",
"d.peers_connected=",
"d.peers_not_connected=",
"d.bytes_done=",
"d.up.total=",
"d.size_bytes=",
"d.left_bytes=",
"d.creation_date=",
"d.complete=",
"d.is_active=",
"d.is_hash_checking=",
"d.base_path=",
"d.base_filename=",
"d.message=",
"d.custom=addtime",
"d.custom=seedingtime",
"d.custom1=",
"d.peers_complete=",
"d.peers_accounted=")
.compose(RxUtil.<TorrentSpec>asList())
.map(new Function<TorrentSpec, Torrent>() {
@Override
public Torrent apply(TorrentSpec torrentSpec) throws Exception {
return new Torrent(
torrentSpec.hash.hashCode(),
torrentSpec.hash,
torrentSpec.name,
torrentStatus(torrentSpec.state, torrentSpec.isComplete, torrentSpec.isActive, torrentSpec.isHashChecking),
torrentSpec.basePath.substring(0, torrentSpec.basePath.indexOf(torrentSpec.baseFilename)),
(int) torrentSpec.downloadRate,
(int) torrentSpec.uploadRate,
(int) torrentSpec.seedersConnected,
(int) (torrentSpec.peersConnected + torrentSpec.peersNotConnected),
(int) torrentSpec.leechersConnected,
(int) (torrentSpec.peersConnected + torrentSpec.peersNotConnected),
torrentSpec.downloadRate > 0 ? (torrentSpec.bytesleft / torrentSpec.downloadRate) : Torrent.UNKNOWN,
torrentSpec.bytesDone,
torrentSpec.bytesUploaded,
torrentSpec.bytesTotal,
torrentSpec.bytesDone / torrentSpec.bytesTotal,
0F,
torrentSpec.label,
torrentTimeAdded(torrentSpec.timeAdded, torrentSpec.timeCreated),
torrentTimeFinished(torrentSpec.timeFinished),
torrentSpec.errorMessage
);
}
});
}
private TorrentStatus torrentStatus(long state, long complete, long active, long checking) {
if (state == 0) {
return TorrentStatus.QUEUED;
} else if (active == 1) {
if (complete == 1) {
return TorrentStatus.SEEDING;
} else {
return TorrentStatus.DOWNLOADING;
}
} else if (checking == 1) {
return TorrentStatus.CHECKING;
} else {
return TorrentStatus.PAUSED;
}
}
private Date torrentTimeAdded(String timeAdded, long timeCreated) {
if (timeAdded != null || timeAdded.trim().length() != 0) {
return new Date(Long.parseLong(timeAdded.trim()) * 1000L);
}
return new Date(timeCreated * 1000L);
}
private Date torrentTimeFinished(String timeFinished) {
if (timeFinished == null || timeFinished.trim().length() == 0)
return null;
return new Date(Long.parseLong(timeFinished.trim()) * 1000L);
}
}

20
connect/src/main/java/org/transdroid/connect/clients/rtorrent/Service.java

@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
package org.transdroid.connect.clients.rtorrent;
import io.reactivex.Flowable;
import nl.nl2312.xmlrpc.Nothing;
import nl.nl2312.xmlrpc.XmlRpc;
import retrofit2.http.Body;
import retrofit2.http.POST;
import retrofit2.http.Path;
interface Service {
@XmlRpc("system.client_version")
@POST("{endpoint}")
Flowable<String> clientVersion(@Path("endpoint") String endpoint, @Body Nothing nothing);
@XmlRpc("d.multicall2")
@POST("{endpoint}")
Flowable<TorrentSpec[]> torrents(@Path("endpoint") String endpoint, @Body String... fields);
}

29
connect/src/main/java/org/transdroid/connect/clients/rtorrent/TorrentSpec.java

@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
package org.transdroid.connect.clients.rtorrent;
public final class TorrentSpec {
public String hash;
public String name;
public long state;
public long downloadRate;
public long uploadRate;
public long peersConnected;
public long peersNotConnected;
public long bytesDone;
public long bytesUploaded;
public long bytesTotal;
public long bytesleft;
public long timeCreated;
public long isComplete;
public long isActive;
public long isHashChecking;
public String basePath;
public String baseFilename;
public String errorMessage;
public String timeAdded;
public String timeFinished;
public String label;
public long seedersConnected;
public long leechersConnected;
}

100
connect/src/main/java/org/transdroid/connect/model/Torrent.java

@ -0,0 +1,100 @@ @@ -0,0 +1,100 @@
package org.transdroid.connect.model;
import java.util.Calendar;
import java.util.Date;
public final class Torrent {
public static final long UNKNOWN = -1L;
private final long id;
private final String hash;
private final String name;
private final TorrentStatus statusCode;
private final String locationDir;
private final int rateDownload;
private final int rateUpload;
private final int seedersConnected;
private final int seedersKnown;
private final int leechersConnected;
private final int leechersKnown;
private final long eta;
private final long downloadedEver;
private final long uploadedEver;
private final long totalSize;
private final float partDone;
private final float available;
private final String label;
private final Date dateAdded;
private final Date dateDone;
private final String error;
public Torrent(long id,
String hash,
String name,
TorrentStatus statusCode,
String locationDir,
int rateDownload,
int rateUpload,
int seedersConnected,
int seedersKnown,
int leechersConnected,
int leechersKnown,
long eta,
long downloadedEver,
long uploadedEver,
long totalSize,
float partDone,
float available,
String label,
Date dateAdded,
Date realDateDone,
String error) {
this.id = id;
this.hash = hash;
this.name = name;
this.statusCode = statusCode;
this.locationDir = locationDir;
this.rateDownload = rateDownload;
this.rateUpload = rateUpload;
this.seedersConnected = seedersConnected;
this.seedersKnown = seedersKnown;
this.leechersConnected = leechersConnected;
this.leechersKnown = leechersKnown;
this.eta = eta;
this.downloadedEver = downloadedEver;
this.uploadedEver = uploadedEver;
this.totalSize = totalSize;
this.partDone = partDone;
this.available = available;
this.label = label;
this.dateAdded = dateAdded;
if (realDateDone != null) {
this.dateDone = realDateDone;
} else {
if (this.partDone == 1) {
// Finished but no finished date: set so move to bottom of list
Calendar cal = Calendar.getInstance();
cal.clear();
cal.set(1900, Calendar.DECEMBER, 31);
this.dateDone = cal.getTime();
} else if (eta == -1 || eta == -2) {
// UNknown eta: move to the top of the list
this.dateDone = new Date(Long.MAX_VALUE);
} else {
Calendar cal = Calendar.getInstance();
cal.add(Calendar.SECOND, (int) eta);
this.dateDone = cal.getTime();
}
}
this.error = error;
}
}

14
connect/src/main/java/org/transdroid/connect/model/TorrentStatus.java

@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
package org.transdroid.connect.model;
public enum TorrentStatus {
WAITING,
CHECKING,
DOWNLOADING,
SEEDING,
PAUSED,
QUEUED,
ERROR,
UNKNOWN;
}

50
connect/src/main/java/org/transdroid/connect/util/OkHttpBuilder.java

@ -0,0 +1,50 @@ @@ -0,0 +1,50 @@
package org.transdroid.connect.util;
import com.burgstaller.okhttp.AuthenticationCacheInterceptor;
import com.burgstaller.okhttp.CachingAuthenticatorDecorator;
import com.burgstaller.okhttp.DispatchingAuthenticator;
import com.burgstaller.okhttp.basic.BasicAuthenticator;
import com.burgstaller.okhttp.digest.CachingAuthenticator;
import com.burgstaller.okhttp.digest.DigestAuthenticator;
import org.transdroid.connect.Configuration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
public final class OkHttpBuilder {
private final Configuration configuration;
public OkHttpBuilder(Configuration configuration) {
this.configuration = configuration;
}
public OkHttpClient build() {
OkHttpClient.Builder okhttp = new OkHttpClient.Builder();
if (configuration.loggingEnabled()) {
HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
okhttp.addInterceptor(loggingInterceptor);
}
if (configuration.credentials() != null) {
BasicAuthenticator basicAuthenticator = new BasicAuthenticator(configuration.credentials());
DigestAuthenticator digestAuthenticator = new DigestAuthenticator(configuration.credentials());
DispatchingAuthenticator authenticator = new DispatchingAuthenticator.Builder()
.with("digest", digestAuthenticator)
.with("basic", basicAuthenticator)
.build();
Map<String, CachingAuthenticator> authCache = new ConcurrentHashMap<>();
okhttp.authenticator(new CachingAuthenticatorDecorator(authenticator, authCache));
okhttp.addInterceptor(new AuthenticationCacheInterceptor(authCache));
}
return okhttp.build();
}
}

29
connect/src/main/java/org/transdroid/connect/util/RxUtil.java

@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
package org.transdroid.connect.util;
import org.reactivestreams.Publisher;
import java.util.Arrays;
import io.reactivex.Flowable;
import io.reactivex.FlowableTransformer;
import io.reactivex.functions.Function;
public final class RxUtil {
private RxUtil() {}
public static <T> FlowableTransformer<T[], T> asList() {
return new FlowableTransformer<T[], T>() {
@Override
public Publisher<T> apply(Flowable<T[]> upstream) {
return upstream.flatMapIterable(new Function<T[], Iterable<T>>() {
@Override
public Iterable<T> apply(T[] ts) throws Exception {
return Arrays.asList(ts);
}
});
}
};
}
}

11
connect/src/main/java/org/transdroid/connect/util/StringUtil.java

@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
package org.transdroid.connect.util;
public final class StringUtil {
private StringUtil() {}
public static boolean isEmpty(String string) {
return string == null || string.equals("");
}
}

57
connect/src/test/java/org/transdroid/connect/clients/rtorrent/RtorrentTest.java

@ -0,0 +1,57 @@ @@ -0,0 +1,57 @@
package org.transdroid.connect.clients.rtorrent;
import org.junit.Before;
import org.junit.Test;
import org.transdroid.connect.Configuration;
import org.transdroid.connect.clients.Client;
import org.transdroid.connect.clients.ClientSpec;
import org.transdroid.connect.clients.Feature;
import org.transdroid.connect.model.Torrent;
import java.io.IOException;
import java.util.List;
import io.reactivex.functions.Predicate;
import static com.google.common.truth.Truth.assertThat;
public final class RtorrentTest {
private ClientSpec rtorrent;
@Before
public void setUp() {
Configuration configuration = new Configuration(Client.RTORRENT, "http://localhost:8008/", "RPC2", null, null, true);
rtorrent = configuration.create();
}
@Test
public void features() {
assertThat(Client.RTORRENT.supports(Feature.VERSION)).isTrue();
assertThat(Client.RTORRENT.supports(Feature.STARTING)).isTrue();
assertThat(Client.RTORRENT.supports(Feature.STOPPING)).isTrue();
assertThat(Client.RTORRENT.supports(Feature.RESUMING)).isTrue();
assertThat(Client.RTORRENT.supports(Feature.PAUSING)).isTrue();
}
@Test
public void clientVersion() throws IOException {
rtorrent.clientVersion()
.test()
.assertValue("0.9.6");
}
@Test
public void torrents() throws IOException {
rtorrent.torrents()
.toList()
.test()
.assertValue(new Predicate<List<Torrent>>() {
@Override
public boolean test(List<Torrent> torrents) throws Exception {
return torrents.size() > 0;
}
});
}
}

4
gradle/wrapper/gradle-wrapper.properties vendored

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
#Wed Jan 20 12:20:00 CET 2016
#Sat Jan 21 11:09:39 CET 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip

2
settings.gradle

@ -1 +1 @@ @@ -1 +1 @@
include ':app'
include ':app', ':connect'

Loading…
Cancel
Save