Alon Albert
7 years ago
30 changed files with 3046 additions and 14 deletions
@ -0,0 +1,50 @@ |
|||||||
|
package deluge; |
||||||
|
|
||||||
|
import java.io.Closeable; |
||||||
|
import java.io.IOException; |
||||||
|
import java.io.InputStream; |
||||||
|
import java.io.OutputStream; |
||||||
|
|
||||||
|
public class Util |
||||||
|
{ |
||||||
|
|
||||||
|
public static void close(Closeable closeable) throws IOException |
||||||
|
{ |
||||||
|
if (closeable == null) |
||||||
|
{ |
||||||
|
return; |
||||||
|
} |
||||||
|
closeable.close(); |
||||||
|
} |
||||||
|
|
||||||
|
public static long copy(InputStream from, OutputStream to) throws IOException |
||||||
|
{ |
||||||
|
try |
||||||
|
{ |
||||||
|
try |
||||||
|
{ |
||||||
|
byte[] buf = new byte[4000]; |
||||||
|
long total = 0; |
||||||
|
while (true) |
||||||
|
{ |
||||||
|
int r = from.read(buf); |
||||||
|
if (r == -1) |
||||||
|
{ |
||||||
|
break; |
||||||
|
} |
||||||
|
to.write(buf, 0, r); |
||||||
|
total += r; |
||||||
|
} |
||||||
|
return total; |
||||||
|
} |
||||||
|
finally |
||||||
|
{ |
||||||
|
Util.close(to); |
||||||
|
} |
||||||
|
} |
||||||
|
finally |
||||||
|
{ |
||||||
|
Util.close(from); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,30 @@ |
|||||||
|
package deluge.api; |
||||||
|
|
||||||
|
public class DelugeException extends Exception |
||||||
|
{ |
||||||
|
private static final long serialVersionUID = 1L; |
||||||
|
|
||||||
|
final public String exceptionType; |
||||||
|
final public String exceptionMsg; |
||||||
|
final public String traceback; |
||||||
|
|
||||||
|
public DelugeException(String type, String msg, String trace) |
||||||
|
{ |
||||||
|
this.exceptionType = type; |
||||||
|
this.exceptionMsg = msg; |
||||||
|
this.traceback = trace; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void printStackTrace() |
||||||
|
{ |
||||||
|
System.err.println(toString()); |
||||||
|
System.err.println(this.traceback); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String toString() |
||||||
|
{ |
||||||
|
return DelugeException.class.getCanonicalName() + " " + this.exceptionType + " (" + this.exceptionMsg + ")"; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,78 @@ |
|||||||
|
package deluge.api; |
||||||
|
|
||||||
|
import java.util.concurrent.CountDownLatch; |
||||||
|
import java.util.concurrent.ExecutionException; |
||||||
|
import java.util.concurrent.Future; |
||||||
|
import java.util.concurrent.TimeUnit; |
||||||
|
import java.util.concurrent.TimeoutException; |
||||||
|
|
||||||
|
import deluge.impl.AsyncResponse; |
||||||
|
|
||||||
|
public class DelugeFuture<V> extends AsyncResponse<V, DelugeException> implements Future<V> |
||||||
|
{ |
||||||
|
private final CountDownLatch latch = new CountDownLatch(1); |
||||||
|
private V value; |
||||||
|
|
||||||
|
public DelugeFuture() |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
public boolean cancel(boolean mayInterruptIfRunning) |
||||||
|
{ |
||||||
|
// TODO Auto-generated method stub
|
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
public V get() throws InterruptedException, ExecutionException |
||||||
|
{ |
||||||
|
this.latch.await(); |
||||||
|
return this.value; |
||||||
|
} |
||||||
|
|
||||||
|
public V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException |
||||||
|
{ |
||||||
|
if (!this.latch.await(timeout, unit)) |
||||||
|
{ |
||||||
|
throw new TimeoutException(); |
||||||
|
} |
||||||
|
return this.value; |
||||||
|
} |
||||||
|
|
||||||
|
public boolean isCancelled() |
||||||
|
{ |
||||||
|
// TODO Auto-generated method stub
|
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
public boolean isDone() |
||||||
|
{ |
||||||
|
return this.value != null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onError(DelugeException error) |
||||||
|
{ |
||||||
|
setValue(null); |
||||||
|
super.onError(error); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onResponse(V response) |
||||||
|
{ |
||||||
|
setValue(response); |
||||||
|
super.onResponse(response); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onServerError(Exception exception) |
||||||
|
{ |
||||||
|
setValue(null); |
||||||
|
super.onServerError(exception); |
||||||
|
} |
||||||
|
|
||||||
|
public void setValue(V val) |
||||||
|
{ |
||||||
|
this.value = val; |
||||||
|
this.latch.countDown(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
package deluge.api; |
||||||
|
|
||||||
|
public abstract class ResponseCallback<R, E extends Exception> |
||||||
|
{ |
||||||
|
public void onError(E error) |
||||||
|
{ |
||||||
|
error.printStackTrace(); |
||||||
|
} |
||||||
|
|
||||||
|
public abstract void onResponse(R response); |
||||||
|
|
||||||
|
public void onServerError(Exception exception) |
||||||
|
{ |
||||||
|
exception.printStackTrace(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,20 @@ |
|||||||
|
package deluge.api.response; |
||||||
|
|
||||||
|
import java.io.IOException; |
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
import deluge.api.DelugeException; |
||||||
|
|
||||||
|
public class IntegerResponse extends Response |
||||||
|
{ |
||||||
|
|
||||||
|
public IntegerResponse(List<Object> data) throws IOException, DelugeException |
||||||
|
{ |
||||||
|
super(data); |
||||||
|
} |
||||||
|
|
||||||
|
public Integer getReturnValue() |
||||||
|
{ |
||||||
|
return (Integer) this.returnValue.get(2); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,62 @@ |
|||||||
|
package deluge.api.response; |
||||||
|
|
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
import deluge.api.DelugeException; |
||||||
|
|
||||||
|
public abstract class Response |
||||||
|
{ |
||||||
|
protected List<Object> returnValue; |
||||||
|
|
||||||
|
protected final int RPC_RESPONSE = 1; |
||||||
|
protected final int RPC_ERROR = 2; |
||||||
|
protected final int RPC_EVENT = 3; |
||||||
|
|
||||||
|
public Response(List<Object> decodedObj) throws DelugeException |
||||||
|
{ |
||||||
|
rawData(decodedObj); |
||||||
|
} |
||||||
|
|
||||||
|
public int getMessageType() |
||||||
|
{ |
||||||
|
return (Integer) this.returnValue.get(0); |
||||||
|
} |
||||||
|
|
||||||
|
public int getRequestId() |
||||||
|
{ |
||||||
|
return (Integer) this.returnValue.get(1); |
||||||
|
} |
||||||
|
|
||||||
|
public abstract Object getReturnValue(); |
||||||
|
|
||||||
|
private void process() throws DelugeException |
||||||
|
{ |
||||||
|
if (getMessageType() == this.RPC_ERROR) |
||||||
|
{ |
||||||
|
@SuppressWarnings("unchecked") |
||||||
|
final List<String> params = (List<String>) this.returnValue.get(2); |
||||||
|
final String type = params.get(0); |
||||||
|
final String msg = params.get(1); |
||||||
|
final String trace = params.get(2); |
||||||
|
|
||||||
|
throw new DelugeException(type, msg, trace); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public void rawData(List<Object> decodedObj) throws DelugeException |
||||||
|
{ |
||||||
|
this.returnValue = decodedObj; |
||||||
|
process(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String toString() |
||||||
|
{ |
||||||
|
String str = "Response { "; |
||||||
|
|
||||||
|
str += this.returnValue.toString(); |
||||||
|
|
||||||
|
str += " }"; |
||||||
|
return str; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,6 @@ |
|||||||
|
package deluge.api.response; |
||||||
|
|
||||||
|
public enum ReturnType |
||||||
|
{ |
||||||
|
INTEGER, TORRENTS_STATUS |
||||||
|
} |
@ -0,0 +1,23 @@ |
|||||||
|
package deluge.api.response; |
||||||
|
|
||||||
|
import java.io.IOException; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Map; |
||||||
|
|
||||||
|
import deluge.api.DelugeException; |
||||||
|
|
||||||
|
public class TorrentsStatusResponse extends Response |
||||||
|
{ |
||||||
|
|
||||||
|
public TorrentsStatusResponse(List<Object> data) throws IOException, DelugeException |
||||||
|
{ |
||||||
|
super(data); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
@SuppressWarnings("unchecked") |
||||||
|
public Map<String, Map<String, Object>> getReturnValue() |
||||||
|
{ |
||||||
|
return (Map<String, Map<String, Object>>) this.returnValue.get(2); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,58 @@ |
|||||||
|
package deluge.impl; |
||||||
|
|
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
import deluge.api.ResponseCallback; |
||||||
|
|
||||||
|
public class AsyncResponse<R, E extends Exception> extends ResponseCallback<R, E> |
||||||
|
{ |
||||||
|
private final List<ResponseCallback<R, E>> callbacks; |
||||||
|
|
||||||
|
public AsyncResponse() |
||||||
|
{ |
||||||
|
this.callbacks = new ArrayList<ResponseCallback<R, E>>(); |
||||||
|
} |
||||||
|
|
||||||
|
public void addCallback(ResponseCallback<R, E> callback) |
||||||
|
{ |
||||||
|
this.callbacks.add(callback); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onError(E error) |
||||||
|
{ |
||||||
|
for (final ResponseCallback<R, E> cb : this.callbacks) |
||||||
|
{ |
||||||
|
cb.onError(error); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onResponse(R response) |
||||||
|
{ |
||||||
|
for (final ResponseCallback<R, E> cb : this.callbacks) |
||||||
|
{ |
||||||
|
cb.onResponse(response); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onServerError(Exception exception) |
||||||
|
{ |
||||||
|
for (final ResponseCallback<R, E> cb : this.callbacks) |
||||||
|
{ |
||||||
|
cb.onServerError(exception); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public void removeCallback(ResponseCallback<R, E> callback) |
||||||
|
{ |
||||||
|
this.callbacks.remove(callback); |
||||||
|
} |
||||||
|
|
||||||
|
public void then(ResponseCallback<R, E> callback) |
||||||
|
{ |
||||||
|
addCallback(callback); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,81 @@ |
|||||||
|
package deluge.impl; |
||||||
|
|
||||||
|
import java.io.IOException; |
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
import se.dimovski.rencode.Rencode; |
||||||
|
import deluge.api.DelugeException; |
||||||
|
import deluge.api.DelugeFuture; |
||||||
|
import deluge.api.response.IntegerResponse; |
||||||
|
import deluge.api.response.Response; |
||||||
|
import deluge.api.response.TorrentsStatusResponse; |
||||||
|
import deluge.impl.net.Session.DataCallback; |
||||||
|
|
||||||
|
public class DataHandler implements DataCallback |
||||||
|
{ |
||||||
|
|
||||||
|
@SuppressWarnings("unchecked") |
||||||
|
public void dataRecived(byte[] data) |
||||||
|
{ |
||||||
|
Integer requestId = null; |
||||||
|
List<Object> decodedObj; |
||||||
|
try |
||||||
|
{ |
||||||
|
decodedObj = (List<Object>) Rencode.decode(data); |
||||||
|
requestId = (Integer) decodedObj.get(1); |
||||||
|
|
||||||
|
sendSpecificResponse(requestId, decodedObj); |
||||||
|
} |
||||||
|
catch (final IOException e) |
||||||
|
{ |
||||||
|
e.printStackTrace(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@SuppressWarnings("unchecked") |
||||||
|
private void sendSpecificResponse(Integer requestId, List<Object> decodedObj) |
||||||
|
{ |
||||||
|
|
||||||
|
final OngoingRequest req = OngoingRequests.remove(requestId); |
||||||
|
|
||||||
|
try |
||||||
|
{ |
||||||
|
switch (req.getType()) |
||||||
|
{ |
||||||
|
case INTEGER: |
||||||
|
{ |
||||||
|
final DelugeFuture<IntegerResponse> fut = (DelugeFuture<IntegerResponse>) req.getFuture(); |
||||||
|
fut.onResponse(new IntegerResponse(decodedObj)); |
||||||
|
} |
||||||
|
break; |
||||||
|
case TORRENTS_STATUS: |
||||||
|
{ |
||||||
|
final DelugeFuture<TorrentsStatusResponse> fut = (DelugeFuture<TorrentsStatusResponse>) req |
||||||
|
.getFuture(); |
||||||
|
fut.onResponse(new TorrentsStatusResponse(decodedObj)); |
||||||
|
} |
||||||
|
break; |
||||||
|
default: |
||||||
|
{ |
||||||
|
throw new UnsupportedOperationException("Unknown Request: " + req.getType()); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
catch (final DelugeException e) |
||||||
|
{ |
||||||
|
final DelugeFuture<Response> fut = (DelugeFuture<Response>) req.getFuture(); |
||||||
|
fut.onError(e); |
||||||
|
} |
||||||
|
catch (final Exception e) |
||||||
|
{ |
||||||
|
final DelugeFuture<Response> fut = (DelugeFuture<Response>) req.getFuture(); |
||||||
|
fut.onServerError(e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void onError() |
||||||
|
{ |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,24 @@ |
|||||||
|
package deluge.impl; |
||||||
|
|
||||||
|
public class DelugeClient |
||||||
|
{ |
||||||
|
public static DelugeSession getSession(String host) |
||||||
|
{ |
||||||
|
final String[] parts = host.split(":"); |
||||||
|
final int port = parts.length < 2 ? DelugeClient.DEFAULT_PORT : Integer.parseInt(parts[1]); |
||||||
|
return DelugeClient.getSession(parts[0], port); |
||||||
|
} |
||||||
|
|
||||||
|
public static DelugeSession getSession(String host, int port) |
||||||
|
{ |
||||||
|
return DelugeSession.connect(host, port); |
||||||
|
} |
||||||
|
|
||||||
|
public static DelugeSession getSessionDefault() |
||||||
|
{ |
||||||
|
return DelugeClient.getSession("127.0.0.1", DelugeClient.DEFAULT_PORT); |
||||||
|
} |
||||||
|
|
||||||
|
public static final int DEFAULT_PORT = 58846; |
||||||
|
|
||||||
|
} |
@ -0,0 +1,100 @@ |
|||||||
|
package deluge.impl; |
||||||
|
|
||||||
|
import java.io.IOException; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Map; |
||||||
|
|
||||||
|
import deluge.api.DelugeFuture; |
||||||
|
import deluge.api.response.IntegerResponse; |
||||||
|
import deluge.api.response.ReturnType; |
||||||
|
import deluge.api.response.TorrentsStatusResponse; |
||||||
|
import deluge.impl.net.Session; |
||||||
|
import deluge.impl.net.TorrentField; |
||||||
|
|
||||||
|
public class DelugeSession |
||||||
|
{ |
||||||
|
public static DelugeSession connect(String host, int port) |
||||||
|
{ |
||||||
|
final Session session = new Session(host, port); |
||||||
|
try |
||||||
|
{ |
||||||
|
session.listen(new DataHandler()); |
||||||
|
} |
||||||
|
catch (final IOException e) |
||||||
|
{ |
||||||
|
e.printStackTrace(); |
||||||
|
} |
||||||
|
return new DelugeSession(session); |
||||||
|
} |
||||||
|
|
||||||
|
private final Session session; |
||||||
|
|
||||||
|
private DelugeSession(Session session) |
||||||
|
|
||||||
|
{ |
||||||
|
this.session = session; |
||||||
|
} |
||||||
|
|
||||||
|
public DelugeFuture<TorrentsStatusResponse> getTorrentsStatus(Map<Object, Object> filter, TorrentField[] fields) |
||||||
|
{ |
||||||
|
final DelugeFuture<TorrentsStatusResponse> future = new DelugeFuture<TorrentsStatusResponse>(); |
||||||
|
final Request request = RequestFactory.getTorrentsStatus(filter, fields); |
||||||
|
send(request, ReturnType.TORRENTS_STATUS, future); |
||||||
|
return future; |
||||||
|
} |
||||||
|
|
||||||
|
public DelugeFuture<IntegerResponse> login(String username, String password) |
||||||
|
{ |
||||||
|
final DelugeFuture<IntegerResponse> future = new DelugeFuture<IntegerResponse>(); |
||||||
|
final Request request = new Request("daemon.login", Util.objects(username, password)); |
||||||
|
send(request, ReturnType.INTEGER, future); |
||||||
|
return future; |
||||||
|
} |
||||||
|
|
||||||
|
public DelugeFuture<IntegerResponse> pauseTorrent(List<String> torrentIds) |
||||||
|
{ |
||||||
|
final DelugeFuture<IntegerResponse> future = new DelugeFuture<IntegerResponse>(); |
||||||
|
final Request request = new Request("core.pause_torrent", Util.objects(torrentIds)); |
||||||
|
send(request, ReturnType.INTEGER, future); |
||||||
|
return future; |
||||||
|
} |
||||||
|
|
||||||
|
public DelugeFuture<IntegerResponse> resumeTorrent(List<String> torrentIds) |
||||||
|
{ |
||||||
|
final DelugeFuture<IntegerResponse> future = new DelugeFuture<IntegerResponse>(); |
||||||
|
final Request request = new Request("core.resume_torrent", Util.objects(torrentIds)); |
||||||
|
send(request, ReturnType.INTEGER, future); |
||||||
|
return future; |
||||||
|
} |
||||||
|
|
||||||
|
public DelugeFuture<IntegerResponse> addTorrentFile(String name, String encodedContents, Map<String, Object> options) |
||||||
|
{ |
||||||
|
final DelugeFuture<IntegerResponse> future = new DelugeFuture<IntegerResponse>(); |
||||||
|
Request request = new Request("core.add_torrent_file", Util.objects(name, encodedContents, options)); |
||||||
|
send(request, ReturnType.INTEGER, future); |
||||||
|
return future; |
||||||
|
} |
||||||
|
|
||||||
|
public DelugeFuture<IntegerResponse> removeTorrent(String torrentId, Boolean removeData) |
||||||
|
{ |
||||||
|
final DelugeFuture<IntegerResponse> future = new DelugeFuture<IntegerResponse>(); |
||||||
|
Request request = new Request("core.remove_torrent", Util.objects(torrentId, removeData)); |
||||||
|
send(request, ReturnType.INTEGER, future); |
||||||
|
return future; |
||||||
|
} |
||||||
|
|
||||||
|
private void send(Request request, ReturnType type, Object future) |
||||||
|
{ |
||||||
|
OngoingRequests.put(request.getRequestId(), type, future); |
||||||
|
|
||||||
|
try |
||||||
|
{ |
||||||
|
this.session.send(request.toByteArray()); |
||||||
|
} |
||||||
|
catch (final IOException e) |
||||||
|
{ |
||||||
|
e.printStackTrace(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
package deluge.impl; |
||||||
|
|
||||||
|
import deluge.api.response.ReturnType; |
||||||
|
|
||||||
|
public class OngoingRequest |
||||||
|
{ |
||||||
|
private final ReturnType type; |
||||||
|
private final Object future; |
||||||
|
|
||||||
|
public OngoingRequest(ReturnType type, Object future) |
||||||
|
{ |
||||||
|
this.type = type; |
||||||
|
this.future = future; |
||||||
|
} |
||||||
|
|
||||||
|
public Object getFuture() |
||||||
|
{ |
||||||
|
return this.future; |
||||||
|
} |
||||||
|
|
||||||
|
public ReturnType getType() |
||||||
|
{ |
||||||
|
return this.type; |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,23 @@ |
|||||||
|
package deluge.impl; |
||||||
|
|
||||||
|
import java.util.Map; |
||||||
|
import java.util.concurrent.ConcurrentHashMap; |
||||||
|
|
||||||
|
import deluge.api.response.ReturnType; |
||||||
|
|
||||||
|
public class OngoingRequests |
||||||
|
{ |
||||||
|
public static void put(int requestId, ReturnType type, Object future) |
||||||
|
{ |
||||||
|
final OngoingRequest ongoing = new OngoingRequest(type, future); |
||||||
|
OngoingRequests.mOngoingRequests.put(requestId, ongoing); |
||||||
|
} |
||||||
|
|
||||||
|
public static OngoingRequest remove(int requestId) |
||||||
|
{ |
||||||
|
return OngoingRequests.mOngoingRequests.remove(requestId); |
||||||
|
} |
||||||
|
|
||||||
|
private static Map<Integer, OngoingRequest> mOngoingRequests = new ConcurrentHashMap<Integer, OngoingRequest>(); |
||||||
|
|
||||||
|
} |
@ -0,0 +1,56 @@ |
|||||||
|
package deluge.impl; |
||||||
|
|
||||||
|
import java.io.IOException; |
||||||
|
import java.util.HashMap; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.concurrent.atomic.AtomicInteger; |
||||||
|
|
||||||
|
import se.dimovski.rencode.Rencode; |
||||||
|
|
||||||
|
public class Request |
||||||
|
{ |
||||||
|
private static AtomicInteger requestCounter = new AtomicInteger(); |
||||||
|
|
||||||
|
private final Integer requestId; |
||||||
|
private final String method; |
||||||
|
private final Object[] args; |
||||||
|
private final Map<Object, Object> kwargs; |
||||||
|
|
||||||
|
public Request(String method) |
||||||
|
{ |
||||||
|
this(method, new Object[0]); |
||||||
|
} |
||||||
|
|
||||||
|
public Request(String method, Object[] args) |
||||||
|
{ |
||||||
|
this(method, args, new HashMap<Object, Object>()); |
||||||
|
} |
||||||
|
|
||||||
|
public Request(String method, Object[] args, Map<Object, Object> kwargs) |
||||||
|
{ |
||||||
|
this.requestId = Request.requestCounter.getAndIncrement(); |
||||||
|
this.method = method; |
||||||
|
this.args = args; |
||||||
|
this.kwargs = kwargs; |
||||||
|
} |
||||||
|
|
||||||
|
public Integer getRequestId() |
||||||
|
{ |
||||||
|
return this.requestId; |
||||||
|
} |
||||||
|
|
||||||
|
public byte[] toByteArray() |
||||||
|
{ |
||||||
|
final Object obj = new Object[] { new Object[] { this.requestId, this.method, this.args, this.kwargs } }; |
||||||
|
try |
||||||
|
{ |
||||||
|
return Rencode.encode(obj); |
||||||
|
} |
||||||
|
catch (final IOException e) |
||||||
|
{ |
||||||
|
// TODO Auto-generated catch block
|
||||||
|
e.printStackTrace(); |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,44 @@ |
|||||||
|
package deluge.impl; |
||||||
|
|
||||||
|
import java.util.Map; |
||||||
|
|
||||||
|
import deluge.impl.net.TorrentField; |
||||||
|
|
||||||
|
public class RequestFactory |
||||||
|
{ |
||||||
|
|
||||||
|
public static Request getSessionState() |
||||||
|
{ |
||||||
|
return new Request("core.get_session_state"); |
||||||
|
} |
||||||
|
|
||||||
|
public static Request getTorrentsStatus() |
||||||
|
{ |
||||||
|
return RequestFactory.getTorrentsStatus(null, null); |
||||||
|
} |
||||||
|
|
||||||
|
public static Request getTorrentsStatus(Map<Object, Object> filter) |
||||||
|
{ |
||||||
|
return RequestFactory.getTorrentsStatus(filter, null); |
||||||
|
} |
||||||
|
|
||||||
|
public static Request getTorrentsStatus(Map<Object, Object> filter, TorrentField[] fields) |
||||||
|
{ |
||||||
|
Object[] fieldNames = new Object[0]; |
||||||
|
if (fields != null) |
||||||
|
{ |
||||||
|
fieldNames = new Object[fields.length]; |
||||||
|
for (int i = 0; i < fields.length; i++) |
||||||
|
{ |
||||||
|
fieldNames[i] = fields[i].toString(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return new Request("core.get_torrents_status", Util.objects(filter, fieldNames)); |
||||||
|
} |
||||||
|
|
||||||
|
public static Request getTorrentsStatus(TorrentField[] fields) |
||||||
|
{ |
||||||
|
return RequestFactory.getTorrentsStatus(null, null); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
package deluge.impl; |
||||||
|
|
||||||
|
import deluge.impl.net.TorrentField; |
||||||
|
|
||||||
|
public class Util |
||||||
|
{ |
||||||
|
public static TorrentField[] fields(TorrentField... fields) |
||||||
|
{ |
||||||
|
return fields; |
||||||
|
} |
||||||
|
|
||||||
|
public static Object[] objects(Object... objects) |
||||||
|
{ |
||||||
|
return objects; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,23 @@ |
|||||||
|
package deluge.impl.net; |
||||||
|
|
||||||
|
import java.security.cert.CertificateException; |
||||||
|
import java.security.cert.X509Certificate; |
||||||
|
|
||||||
|
import javax.net.ssl.X509TrustManager; |
||||||
|
|
||||||
|
public class AcceptAllTrustManager implements X509TrustManager |
||||||
|
{ |
||||||
|
|
||||||
|
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
public X509Certificate[] getAcceptedIssuers() |
||||||
|
{ |
||||||
|
return null; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,32 @@ |
|||||||
|
package deluge.impl.net; |
||||||
|
|
||||||
|
import java.util.concurrent.ExecutorService; |
||||||
|
import java.util.concurrent.Executors; |
||||||
|
|
||||||
|
public class ResponseExecutor |
||||||
|
{ |
||||||
|
|
||||||
|
ExecutorService mExecutor; |
||||||
|
|
||||||
|
public ResponseExecutor() |
||||||
|
{ |
||||||
|
this.mExecutor = Executors.newFixedThreadPool(20); |
||||||
|
} |
||||||
|
|
||||||
|
public void execute(Runnable task) |
||||||
|
{ |
||||||
|
this.mExecutor.execute(task); |
||||||
|
} |
||||||
|
|
||||||
|
public void shutdown() |
||||||
|
{ |
||||||
|
if (this.mExecutor != null) |
||||||
|
{ |
||||||
|
this.mExecutor.shutdown(); |
||||||
|
while (!this.mExecutor.isTerminated()) |
||||||
|
{ |
||||||
|
} |
||||||
|
System.out.println("Finished all threads"); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,41 @@ |
|||||||
|
package deluge.impl.net; |
||||||
|
|
||||||
|
import java.io.IOException; |
||||||
|
import java.net.UnknownHostException; |
||||||
|
import java.security.KeyManagementException; |
||||||
|
import java.security.NoSuchAlgorithmException; |
||||||
|
|
||||||
|
import javax.net.ssl.HandshakeCompletedEvent; |
||||||
|
import javax.net.ssl.HandshakeCompletedListener; |
||||||
|
import javax.net.ssl.SSLContext; |
||||||
|
import javax.net.ssl.SSLSocket; |
||||||
|
import javax.net.ssl.TrustManager; |
||||||
|
|
||||||
|
public class SSL3Socket |
||||||
|
{ |
||||||
|
|
||||||
|
public static SSLSocket createSSLv3Socket(String address, int port) throws KeyManagementException, |
||||||
|
UnknownHostException, IOException, NoSuchAlgorithmException |
||||||
|
{ |
||||||
|
final TrustManager[] trustAllCerts = new TrustManager[] { new AcceptAllTrustManager() }; |
||||||
|
|
||||||
|
final SSLContext sc = SSLContext.getInstance("SSLv3"); |
||||||
|
sc.init(null, trustAllCerts, new java.security.SecureRandom()); |
||||||
|
|
||||||
|
final SSLSocket mySocket = (SSLSocket) sc.getSocketFactory().createSocket(address, port); |
||||||
|
|
||||||
|
final String[] protocols = { "SSLv3", "TLSv1" }; |
||||||
|
mySocket.setEnabledProtocols(protocols); |
||||||
|
|
||||||
|
mySocket.addHandshakeCompletedListener(new HandshakeCompletedListener() |
||||||
|
{ |
||||||
|
|
||||||
|
public void handshakeCompleted(HandshakeCompletedEvent event) |
||||||
|
{ |
||||||
|
System.out.println("Handshake complete"); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
return mySocket; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,245 @@ |
|||||||
|
package deluge.impl.net; |
||||||
|
|
||||||
|
import java.io.BufferedInputStream; |
||||||
|
import java.io.BufferedOutputStream; |
||||||
|
import java.io.ByteArrayInputStream; |
||||||
|
import java.io.ByteArrayOutputStream; |
||||||
|
import java.io.IOException; |
||||||
|
import java.io.InputStream; |
||||||
|
import java.io.OutputStream; |
||||||
|
import java.io.UnsupportedEncodingException; |
||||||
|
import java.util.concurrent.ArrayBlockingQueue; |
||||||
|
import java.util.concurrent.BlockingQueue; |
||||||
|
import java.util.concurrent.CountDownLatch; |
||||||
|
import java.util.concurrent.TimeUnit; |
||||||
|
import java.util.zip.DataFormatException; |
||||||
|
import java.util.zip.Deflater; |
||||||
|
import java.util.zip.DeflaterOutputStream; |
||||||
|
import java.util.zip.Inflater; |
||||||
|
import java.util.zip.InflaterInputStream; |
||||||
|
|
||||||
|
import javax.net.ssl.SSLSocket; |
||||||
|
|
||||||
|
import deluge.Util; |
||||||
|
|
||||||
|
public class Session |
||||||
|
{ |
||||||
|
public interface DataCallback |
||||||
|
{ |
||||||
|
public void dataRecived(byte[] data); |
||||||
|
} |
||||||
|
|
||||||
|
public static byte[] decompressByteArray(byte[] data) throws IOException |
||||||
|
{ |
||||||
|
final InputStream from = new InflaterInputStream(new ByteArrayInputStream(data)); |
||||||
|
final ByteArrayOutputStream to = new ByteArrayOutputStream(); |
||||||
|
Util.copy(from, to); |
||||||
|
byte[] output = to.toByteArray(); |
||||||
|
from.close(); |
||||||
|
to.close(); |
||||||
|
return output; |
||||||
|
} |
||||||
|
|
||||||
|
private static byte[] decompress(byte[] input) throws DataFormatException |
||||||
|
{ |
||||||
|
final ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
||||||
|
|
||||||
|
final Inflater decompresser = new Inflater(false); |
||||||
|
|
||||||
|
decompresser.setInput(input, 0, input.length); |
||||||
|
final byte[] result = new byte[1024]; |
||||||
|
while (!decompresser.finished()) |
||||||
|
{ |
||||||
|
final int resultLength = decompresser.inflate(result); |
||||||
|
baos.write(result, 0, resultLength); |
||||||
|
} |
||||||
|
decompresser.end(); |
||||||
|
|
||||||
|
final byte[] returnValue = baos.toByteArray(); |
||||||
|
try |
||||||
|
{ |
||||||
|
baos.close(); |
||||||
|
} |
||||||
|
catch (final IOException e) |
||||||
|
{ |
||||||
|
} |
||||||
|
return returnValue; |
||||||
|
} |
||||||
|
|
||||||
|
private final BlockingQueue<byte[]> queue = new ArrayBlockingQueue<byte[]>(50); |
||||||
|
private SSLSocket mySocket; |
||||||
|
String myAddress; |
||||||
|
int myPort; |
||||||
|
Thread sender = null; |
||||||
|
|
||||||
|
public final CountDownLatch latch = new CountDownLatch(1); |
||||||
|
|
||||||
|
public Session(String address, int port) |
||||||
|
{ |
||||||
|
this.myAddress = address; |
||||||
|
this.myPort = port; |
||||||
|
} |
||||||
|
|
||||||
|
public byte[] compress(byte[] data) throws IOException |
||||||
|
{ |
||||||
|
|
||||||
|
final ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
||||||
|
|
||||||
|
final Deflater d = new Deflater(); |
||||||
|
final DeflaterOutputStream dout = new DeflaterOutputStream(baos, d); |
||||||
|
dout.write(data); |
||||||
|
dout.close(); |
||||||
|
|
||||||
|
final byte[] output = baos.toByteArray(); |
||||||
|
baos.close(); |
||||||
|
return output; |
||||||
|
} |
||||||
|
|
||||||
|
private void createSocket() |
||||||
|
{ |
||||||
|
if (this.mySocket == null) |
||||||
|
{ |
||||||
|
try |
||||||
|
{ |
||||||
|
this.mySocket = SSL3Socket.createSSLv3Socket(this.myAddress, this.myPort); |
||||||
|
this.mySocket.startHandshake(); |
||||||
|
} |
||||||
|
catch (final Exception e1) |
||||||
|
{ |
||||||
|
e1.printStackTrace(); |
||||||
|
this.mySocket = null; |
||||||
|
} |
||||||
|
} |
||||||
|
this.latch.countDown(); |
||||||
|
} |
||||||
|
|
||||||
|
public void listen(final DataCallback cb) throws IOException |
||||||
|
{ |
||||||
|
new Thread(new Runnable() |
||||||
|
{ |
||||||
|
|
||||||
|
public void run() |
||||||
|
{ |
||||||
|
createSocket(); |
||||||
|
System.out.println("Listening Thread started"); |
||||||
|
try |
||||||
|
{ |
||||||
|
while (Session.this.mySocket != null) |
||||||
|
{ |
||||||
|
|
||||||
|
final InputStream inputStream = new BufferedInputStream(Session.this.mySocket.getInputStream()); |
||||||
|
|
||||||
|
final ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
||||||
|
|
||||||
|
int bytesRead; |
||||||
|
final byte[] buffer = new byte[1024]; |
||||||
|
while ((bytesRead = inputStream.read(buffer)) != -1) |
||||||
|
{ |
||||||
|
baos.write(buffer); |
||||||
|
|
||||||
|
if (bytesRead < 1024) |
||||||
|
{ |
||||||
|
final byte[] unpacked = Session.decompressByteArray(baos.toByteArray()); |
||||||
|
baos.reset(); |
||||||
|
cb.dataRecived(unpacked); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
catch (final UnsupportedEncodingException e) |
||||||
|
{ |
||||||
|
// TODO Auto-generated catch block
|
||||||
|
e.printStackTrace(); |
||||||
|
} |
||||||
|
catch (final IOException e) |
||||||
|
{ |
||||||
|
// TODO Auto-generated catch block
|
||||||
|
e.printStackTrace(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
}).start(); |
||||||
|
|
||||||
|
try |
||||||
|
{ |
||||||
|
this.latch.await(3, TimeUnit.SECONDS); |
||||||
|
} |
||||||
|
catch (final InterruptedException e) |
||||||
|
{ |
||||||
|
} |
||||||
|
if (this.mySocket == null) |
||||||
|
{ |
||||||
|
throw new IOException(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public void send(byte[] request) throws IOException |
||||||
|
{ |
||||||
|
if (this.sender == null) |
||||||
|
{ |
||||||
|
sender(); |
||||||
|
} |
||||||
|
try |
||||||
|
{ |
||||||
|
this.queue.put(request); |
||||||
|
} |
||||||
|
catch (final InterruptedException e) |
||||||
|
{ |
||||||
|
e.printStackTrace(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public void sender() throws IOException |
||||||
|
{ |
||||||
|
this.sender = new Thread(new Runnable() |
||||||
|
{ |
||||||
|
|
||||||
|
public void run() |
||||||
|
{ |
||||||
|
createSocket(); |
||||||
|
System.out.println("Sending Thread started"); |
||||||
|
try |
||||||
|
{ |
||||||
|
while (Session.this.mySocket != null) |
||||||
|
{ |
||||||
|
byte[] packedData; |
||||||
|
try |
||||||
|
{ |
||||||
|
final byte[] x = Session.this.queue.take(); |
||||||
|
packedData = compress(x); |
||||||
|
|
||||||
|
final OutputStream out = new BufferedOutputStream(Session.this.mySocket.getOutputStream()); |
||||||
|
out.write(packedData); |
||||||
|
out.flush(); |
||||||
|
} |
||||||
|
catch (final InterruptedException e) |
||||||
|
{ |
||||||
|
e.printStackTrace(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
catch (final IOException e) |
||||||
|
{ |
||||||
|
// TODO Auto-generated catch block
|
||||||
|
e.printStackTrace(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
}); |
||||||
|
this.sender.start(); |
||||||
|
|
||||||
|
try |
||||||
|
{ |
||||||
|
this.latch.await(3, TimeUnit.SECONDS); |
||||||
|
} |
||||||
|
catch (final InterruptedException e) |
||||||
|
{ |
||||||
|
} |
||||||
|
if (this.mySocket == null) |
||||||
|
{ |
||||||
|
throw new IOException(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,34 @@ |
|||||||
|
package deluge.impl.net; |
||||||
|
|
||||||
|
public enum TorrentField |
||||||
|
{ |
||||||
|
ACTIVE_TIME("active_time"), ALL_TIME_DOWNLOAD("all_time_download"), COMPACT("compact"), DISTRIBUTED_COPIES( |
||||||
|
"distributed_copies"), DOWNLOAD_PAYLOAD_RATE("download_payload_rate"), FILE_PRIORITIES("file_priorities"), HASH( |
||||||
|
"hash"), IS_AUTO_MANAGED("is_auto_managed"), IS_FINISHED("is_finished"), MAX_CONNECTIONS("max_connections"), MAX_DOWNLOAD_SPEED( |
||||||
|
"max_download_speed"), MAX_UPLOAD_SLOTS("max_upload_slots"), MAX_UPLOAD_SPEED("max_upload_speed"), MESSAGE( |
||||||
|
"message"), MOVE_ON_COMPLETED_PATH("move_on_completed_path"), MOVE_ON_COMPLETED("move_on_completed"), MOVE_COMPLETED_PATH( |
||||||
|
"move_completed_path"), MOVE_COMPLETED("move_completed"), NEXT_ANNOUNCE("next_announce"), NUM_PEERS( |
||||||
|
"num_peers"), NUM_SEEDS("num_seeds"), PAUSED("paused"), PRIORITIZE_FIRST_LAST("prioritize_first_last"), PROGRESS( |
||||||
|
"progress"), REMOVE_AT_RATIO("remove_at_ratio"), SAVE_PATH("save_path"), SEEDING_TIME("seeding_time"), SEEDS_PEERS_RATIO( |
||||||
|
"seeds_peers_ratio"), SEED_RANK("seed_rank"), STATE("state"), STOP_AT_RATIO("stop_at_ratio"), STOP_RATIO( |
||||||
|
"stop_ratio"), TIME_ADDED("time_added"), TOTAL_DONE("total_done"), TOTAL_PAYLOAD_DOWNLOAD( |
||||||
|
"total_payload_download"), TOTAL_PAYLOAD_UPLOAD("total_payload_upload"), TOTAL_PEERS("total_peers"), TOTAL_SEEDS( |
||||||
|
"total_seeds"), TOTAL_UPLOADED("total_uploaded"), TOTAL_WANTED("total_wanted"), TRACKER("tracker"), TRACKERS( |
||||||
|
"trackers"), TRACKER_STATUS("tracker_status"), UPLOAD_PAYLOAD_RATE("upload_payload_rate"), COMMENT( |
||||||
|
"comment"), ETA("eta"), FILE_PROGRESS("file_progress"), FILES("files"), IS_SEED("is_seed"), NAME("name"), NUM_FILES( |
||||||
|
"num_files"), NUM_PIECES("num_pieces"), PEERS("peers"), PIECE_LENGTH("piece_length"), PRIVATE("private"), QUEUE( |
||||||
|
"queue"), RATIO("ratio"), TOTAL_SIZE("total_size"), TRACKER_HOST("tracker_host"), LABEL("label"); |
||||||
|
|
||||||
|
private final String value; |
||||||
|
|
||||||
|
TorrentField(String str) |
||||||
|
{ |
||||||
|
this.value = str; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String toString() |
||||||
|
{ |
||||||
|
return this.value; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,876 @@ |
|||||||
|
/* |
||||||
|
* This file is part of Transdroid <http://www.transdroid.org>
|
||||||
|
* |
||||||
|
* Transdroid 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. |
||||||
|
* |
||||||
|
* Transdroid 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 Transdroid. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
* |
||||||
|
*/ |
||||||
|
package org.transdroid.daemon.Deluge; |
||||||
|
|
||||||
|
import com.android.internalcopy.http.multipart.FilePart; |
||||||
|
import com.android.internalcopy.http.multipart.MultipartEntity; |
||||||
|
import com.android.internalcopy.http.multipart.Part; |
||||||
|
|
||||||
|
import org.apache.http.HttpEntity; |
||||||
|
import org.apache.http.HttpResponse; |
||||||
|
import org.apache.http.client.methods.HttpGet; |
||||||
|
import org.apache.http.client.methods.HttpPost; |
||||||
|
import org.apache.http.cookie.Cookie; |
||||||
|
import org.apache.http.entity.StringEntity; |
||||||
|
import org.apache.http.impl.client.DefaultHttpClient; |
||||||
|
import org.apache.http.protocol.HTTP; |
||||||
|
import org.json.JSONArray; |
||||||
|
import org.json.JSONException; |
||||||
|
import org.json.JSONObject; |
||||||
|
import org.transdroid.core.gui.log.Log; |
||||||
|
import org.transdroid.daemon.Daemon; |
||||||
|
import org.transdroid.daemon.DaemonException; |
||||||
|
import org.transdroid.daemon.DaemonException.ExceptionType; |
||||||
|
import org.transdroid.daemon.DaemonSettings; |
||||||
|
import org.transdroid.daemon.IDaemonAdapter; |
||||||
|
import org.transdroid.daemon.Label; |
||||||
|
import org.transdroid.daemon.Priority; |
||||||
|
import org.transdroid.daemon.Torrent; |
||||||
|
import org.transdroid.daemon.TorrentDetails; |
||||||
|
import org.transdroid.daemon.TorrentFile; |
||||||
|
import org.transdroid.daemon.TorrentStatus; |
||||||
|
import org.transdroid.daemon.task.AddByFileTask; |
||||||
|
import org.transdroid.daemon.task.AddByMagnetUrlTask; |
||||||
|
import org.transdroid.daemon.task.AddByUrlTask; |
||||||
|
import org.transdroid.daemon.task.DaemonTask; |
||||||
|
import org.transdroid.daemon.task.DaemonTaskFailureResult; |
||||||
|
import org.transdroid.daemon.task.DaemonTaskResult; |
||||||
|
import org.transdroid.daemon.task.DaemonTaskSuccessResult; |
||||||
|
import org.transdroid.daemon.task.GetFileListTask; |
||||||
|
import org.transdroid.daemon.task.GetFileListTaskSuccessResult; |
||||||
|
import org.transdroid.daemon.task.GetTorrentDetailsTask; |
||||||
|
import org.transdroid.daemon.task.GetTorrentDetailsTaskSuccessResult; |
||||||
|
import org.transdroid.daemon.task.PauseTask; |
||||||
|
import org.transdroid.daemon.task.RemoveTask; |
||||||
|
import org.transdroid.daemon.task.ResumeTask; |
||||||
|
import org.transdroid.daemon.task.RetrieveTask; |
||||||
|
import org.transdroid.daemon.task.RetrieveTaskSuccessResult; |
||||||
|
import org.transdroid.daemon.task.SetDownloadLocationTask; |
||||||
|
import org.transdroid.daemon.task.SetFilePriorityTask; |
||||||
|
import org.transdroid.daemon.task.SetLabelTask; |
||||||
|
import org.transdroid.daemon.task.SetTrackersTask; |
||||||
|
import org.transdroid.daemon.task.SetTransferRatesTask; |
||||||
|
import org.transdroid.daemon.util.HttpHelper; |
||||||
|
|
||||||
|
import java.io.File; |
||||||
|
import java.io.FileNotFoundException; |
||||||
|
import java.io.IOException; |
||||||
|
import java.io.InputStream; |
||||||
|
import java.net.URI; |
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.Date; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.concurrent.ExecutionException; |
||||||
|
|
||||||
|
import deluge.api.DelugeFuture; |
||||||
|
import deluge.api.response.TorrentsStatusResponse; |
||||||
|
import deluge.impl.DelugeClient; |
||||||
|
import deluge.impl.DelugeSession; |
||||||
|
import deluge.impl.net.TorrentField; |
||||||
|
|
||||||
|
/** |
||||||
|
* The daemon adapter from the Deluge torrent client using deluged API directly. |
||||||
|
* @author alon.albert |
||||||
|
*/ |
||||||
|
public class DelugeDirectAdapter implements IDaemonAdapter { |
||||||
|
|
||||||
|
private static final String LOG_NAME = "Deluge daemon"; |
||||||
|
|
||||||
|
private static final String PATH_TO_RPC = "/json"; |
||||||
|
private static final String PATH_TO_UPLOAD = "/upload"; |
||||||
|
|
||||||
|
private static final String RPC_ID = "id"; |
||||||
|
private static final String RPC_METHOD = "method"; |
||||||
|
private static final String RPC_PARAMS = "params"; |
||||||
|
private static final String RPC_RESULT = "result"; |
||||||
|
private static final String RPC_TORRENTS = "torrents"; |
||||||
|
private static final String RPC_FILE = "file"; |
||||||
|
private static final String RPC_FILES = "files"; |
||||||
|
private static final String RPC_SESSION_ID = "_session_id"; |
||||||
|
|
||||||
|
private static final String RPC_METHOD_LOGIN = "auth.login"; |
||||||
|
private static final String RPC_METHOD_GET = "web.update_ui"; |
||||||
|
private static final String RPC_METHOD_STATUS = "core.get_torrent_status"; |
||||||
|
private static final String RPC_METHOD_ADD = "core.add_torrent_url"; |
||||||
|
private static final String RPC_METHOD_ADD_MAGNET = "core.add_torrent_magnet"; |
||||||
|
private static final String RPC_METHOD_ADD_FILE = "web.add_torrents"; |
||||||
|
private static final String RPC_METHOD_REMOVE = "core.remove_torrent"; |
||||||
|
private static final String RPC_METHOD_PAUSE = "core.pause_torrent"; |
||||||
|
private static final String RPC_METHOD_PAUSE_ALL = "core.pause_all_torrents"; |
||||||
|
private static final String RPC_METHOD_RESUME = "core.resume_torrent"; |
||||||
|
private static final String RPC_METHOD_RESUME_ALL = "core.resume_all_torrents"; |
||||||
|
private static final String RPC_METHOD_SETCONFIG = "core.set_config"; |
||||||
|
private static final String RPC_METHOD_SETFILE = "core.set_torrent_file_priorities"; |
||||||
|
//private static final String RPC_METHOD_SETOPTIONS = "core.set_torrent_options";
|
||||||
|
private static final String RPC_METHOD_MOVESTORAGE = "core.move_storage"; |
||||||
|
private static final String RPC_METHOD_SETTRACKERS = "core.set_torrent_trackers"; |
||||||
|
private static final String RPC_METHOD_FORCERECHECK = "core.force_recheck"; |
||||||
|
private static final String RPC_METHOD_SETLABEL = "label.set_torrent"; |
||||||
|
|
||||||
|
private static final String RPC_NAME = "name"; |
||||||
|
private static final String RPC_STATUS = "state"; |
||||||
|
private static final String RPC_MESSAGE = "message"; |
||||||
|
private static final String RPC_SAVEPATH = "save_path"; |
||||||
|
private static final String RPC_MAXDOWNLOAD = "max_download_speed"; |
||||||
|
private static final String RPC_MAXUPLOAD = "max_upload_speed"; |
||||||
|
|
||||||
|
private static final String RPC_RATEDOWNLOAD = "download_payload_rate"; |
||||||
|
private static final String RPC_RATEUPLOAD = "upload_payload_rate"; |
||||||
|
private static final String RPC_NUMSEEDS = "num_seeds"; |
||||||
|
private static final String RPC_TOTALSEEDS = "total_seeds"; |
||||||
|
private static final String RPC_NUMPEERS = "num_peers"; |
||||||
|
private static final String RPC_TOTALPEERS = "total_peers"; |
||||||
|
private static final String RPC_ETA = "eta"; |
||||||
|
private static final String RPC_TIMEADDED = "time_added"; |
||||||
|
|
||||||
|
private static final String RPC_DOWNLOADEDEVER = "total_done"; |
||||||
|
private static final String RPC_UPLOADEDEVER = "total_uploaded"; |
||||||
|
private static final String RPC_TOTALSIZE = "total_size"; |
||||||
|
private static final String RPC_PARTDONE = "progress"; |
||||||
|
private static final String RPC_LABEL = "label"; |
||||||
|
private static final String RPC_TRACKERS = "trackers"; |
||||||
|
private static final String RPC_TRACKER_STATUS = "tracker_status"; |
||||||
|
private static final TorrentField[] RETREIVE_FIELDS = new TorrentField[] { |
||||||
|
TorrentField.NAME, |
||||||
|
TorrentField.STATE, |
||||||
|
TorrentField.SAVE_PATH, |
||||||
|
TorrentField.DOWNLOAD_PAYLOAD_RATE, |
||||||
|
TorrentField.UPLOAD_PAYLOAD_RATE, |
||||||
|
TorrentField.NUM_PEERS, |
||||||
|
TorrentField.NUM_SEEDS, |
||||||
|
TorrentField.TOTAL_PEERS, |
||||||
|
TorrentField.TOTAL_SEEDS, |
||||||
|
TorrentField.ETA, |
||||||
|
TorrentField.TOTAL_DONE, |
||||||
|
TorrentField.TOTAL_UPLOADED, |
||||||
|
TorrentField.TOTAL_SIZE, |
||||||
|
TorrentField.PROGRESS, |
||||||
|
TorrentField.LABEL, |
||||||
|
TorrentField.MESSAGE, |
||||||
|
TorrentField.TIME_ADDED, |
||||||
|
TorrentField.TRACKER_STATUS, |
||||||
|
}; |
||||||
|
private static final String[] RPC_FIELDS_ARRAY = |
||||||
|
new String[]{RPC_NAME, RPC_STATUS, RPC_SAVEPATH, RPC_RATEDOWNLOAD, RPC_RATEUPLOAD, RPC_NUMPEERS, |
||||||
|
RPC_NUMSEEDS, RPC_TOTALPEERS, RPC_TOTALSEEDS, RPC_ETA, RPC_DOWNLOADEDEVER, RPC_UPLOADEDEVER, |
||||||
|
RPC_TOTALSIZE, RPC_PARTDONE, RPC_LABEL, RPC_MESSAGE, RPC_TIMEADDED, RPC_TRACKER_STATUS}; |
||||||
|
private static final String RPC_DETAILS = "files"; |
||||||
|
private static final String RPC_INDEX = "index"; |
||||||
|
private static final String RPC_PATH = "path"; |
||||||
|
private static final String RPC_SIZE = "size"; |
||||||
|
private static final String RPC_FILEPROGRESS = "file_progress"; |
||||||
|
private static final String RPC_FILEPRIORITIES = "file_priorities"; |
||||||
|
private DaemonSettings settings; |
||||||
|
private DefaultHttpClient httpclient; |
||||||
|
private DelugeSession delugeSession; |
||||||
|
private Cookie sessionCookie; |
||||||
|
private int version = -1; |
||||||
|
|
||||||
|
public DelugeDirectAdapter(DaemonSettings settings) { |
||||||
|
this.settings = settings; |
||||||
|
} |
||||||
|
|
||||||
|
public JSONArray addTorrentByFile(String file, Log log) throws JSONException, IOException, DaemonException { |
||||||
|
|
||||||
|
String url = buildWebUIUrl() + PATH_TO_UPLOAD; |
||||||
|
|
||||||
|
log.d(LOG_NAME, "Uploading a file to the Deluge daemon: " + url); |
||||||
|
|
||||||
|
// Initialise the HTTP client
|
||||||
|
if (httpclient == null) { |
||||||
|
initialise(); |
||||||
|
} |
||||||
|
|
||||||
|
// Setup client using POST
|
||||||
|
HttpPost httppost = new HttpPost(url); |
||||||
|
File upload = new File(URI.create(file)); |
||||||
|
Part[] parts = {new FilePart(RPC_FILE, upload)}; |
||||||
|
httppost.setEntity(new MultipartEntity(parts, httppost.getParams())); |
||||||
|
|
||||||
|
// Make request
|
||||||
|
HttpResponse response = httpclient.execute(httppost); |
||||||
|
|
||||||
|
// Read JSON response
|
||||||
|
InputStream instream = response.getEntity().getContent(); |
||||||
|
String result = HttpHelper.convertStreamToString(instream); |
||||||
|
|
||||||
|
// If the upload succeeded, add the torrent file on the server
|
||||||
|
// For this we need the file name, which is now send as a JSON object like:
|
||||||
|
// {"files": ["/tmp/delugeweb/tmp00000.torrent"], "success": true}
|
||||||
|
String remoteFile = (new JSONObject(result)).getJSONArray(RPC_FILES).getString(0); |
||||||
|
JSONArray params = new JSONArray(); |
||||||
|
JSONArray files = new JSONArray(); |
||||||
|
JSONObject fileu = new JSONObject(); |
||||||
|
fileu.put("path", remoteFile); |
||||||
|
fileu.put("options", new JSONArray()); |
||||||
|
files.put(fileu); |
||||||
|
params.put(files); |
||||||
|
|
||||||
|
return params; |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public DaemonTaskResult executeTask(Log log, DaemonTask task) { |
||||||
|
|
||||||
|
try { |
||||||
|
ensureVersion(log); |
||||||
|
|
||||||
|
JSONArray params = new JSONArray(); |
||||||
|
|
||||||
|
// Array of the fields needed for files listing calls
|
||||||
|
JSONArray ffields = new JSONArray(); |
||||||
|
ffields.put(RPC_DETAILS); |
||||||
|
ffields.put(RPC_FILEPROGRESS); |
||||||
|
ffields.put(RPC_FILEPRIORITIES); |
||||||
|
|
||||||
|
switch (task.getMethod()) { |
||||||
|
case Retrieve: |
||||||
|
final TorrentsStatusResponse torrents = delugeSession.getTorrentsStatus(null, RETREIVE_FIELDS).get(); |
||||||
|
int id = 0; |
||||||
|
for (Map.Entry<String, Map<String, Object>> entry : torrents.getReturnValue().entrySet()) { |
||||||
|
final String hash = entry.getKey(); |
||||||
|
final Map<String, Object> values = entry.getValue(); |
||||||
|
new Torrent( |
||||||
|
id, |
||||||
|
hash, |
||||||
|
(String) values.get(TorrentField.NAME.toString()), |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
) |
||||||
|
} |
||||||
|
torrents.get().getReturnValue() |
||||||
|
return new RetrieveTaskSuccessResult((RetrieveTask) task, |
||||||
|
parseJsonRetrieveTorrents(result.getJSONObject(RPC_RESULT)), |
||||||
|
parseJsonRetrieveLabels(result.getJSONObject(RPC_RESULT))); |
||||||
|
|
||||||
|
case GetTorrentDetails: |
||||||
|
|
||||||
|
// Array of the fields needed for files listing calls
|
||||||
|
JSONArray dfields = new JSONArray(); |
||||||
|
dfields.put(RPC_TRACKERS); |
||||||
|
dfields.put(RPC_TRACKER_STATUS); |
||||||
|
|
||||||
|
// Request file listing of a torrent
|
||||||
|
params.put(task.getTargetTorrent().getUniqueID()); // torrent_id
|
||||||
|
params.put(dfields); // keys
|
||||||
|
|
||||||
|
JSONObject dinfo = makeRequest(buildRequest(RPC_METHOD_STATUS, params), log); |
||||||
|
return new GetTorrentDetailsTaskSuccessResult((GetTorrentDetailsTask) task, |
||||||
|
parseJsonTorrentDetails(dinfo.getJSONObject(RPC_RESULT))); |
||||||
|
|
||||||
|
case GetFileList: |
||||||
|
|
||||||
|
// Request file listing of a torrent
|
||||||
|
params.put(task.getTargetTorrent().getUniqueID()); // torrent_id
|
||||||
|
params.put(ffields); // keys
|
||||||
|
|
||||||
|
JSONObject finfo = makeRequest(buildRequest(RPC_METHOD_STATUS, params), log); |
||||||
|
return new GetFileListTaskSuccessResult((GetFileListTask) task, |
||||||
|
parseJsonFileListing(finfo.getJSONObject(RPC_RESULT), task.getTargetTorrent())); |
||||||
|
|
||||||
|
case AddByFile: |
||||||
|
|
||||||
|
// Request to add a torrent by local .torrent file
|
||||||
|
String file = ((AddByFileTask) task).getFile(); |
||||||
|
makeRequest(buildRequest(RPC_METHOD_ADD_FILE, addTorrentByFile(file, log)), log); |
||||||
|
return new DaemonTaskSuccessResult(task); |
||||||
|
|
||||||
|
case AddByUrl: |
||||||
|
|
||||||
|
// Request to add a torrent by URL
|
||||||
|
String url = ((AddByUrlTask) task).getUrl(); |
||||||
|
params.put(url); |
||||||
|
params.put(new JSONArray()); |
||||||
|
|
||||||
|
makeRequest(buildRequest(RPC_METHOD_ADD, params), log); |
||||||
|
return new DaemonTaskSuccessResult(task); |
||||||
|
|
||||||
|
case AddByMagnetUrl: |
||||||
|
|
||||||
|
// Request to add a magnet link by URL
|
||||||
|
String magnet = ((AddByMagnetUrlTask) task).getUrl(); |
||||||
|
params.put(magnet); |
||||||
|
params.put(new JSONArray()); |
||||||
|
|
||||||
|
makeRequest(buildRequest(RPC_METHOD_ADD_MAGNET, params), log); |
||||||
|
return new DaemonTaskSuccessResult(task); |
||||||
|
|
||||||
|
case Remove: |
||||||
|
|
||||||
|
// Remove a torrent
|
||||||
|
RemoveTask removeTask = (RemoveTask) task; |
||||||
|
params.put(removeTask.getTargetTorrent().getUniqueID()); |
||||||
|
params.put(removeTask.includingData()); |
||||||
|
makeRequest(buildRequest(RPC_METHOD_REMOVE, params), log); |
||||||
|
return new DaemonTaskSuccessResult(task); |
||||||
|
|
||||||
|
case Pause: |
||||||
|
|
||||||
|
// Pause a torrent
|
||||||
|
PauseTask pauseTask = (PauseTask) task; |
||||||
|
makeRequest(buildRequest(RPC_METHOD_PAUSE, ((new JSONArray()) |
||||||
|
.put((new JSONArray()).put(pauseTask.getTargetTorrent().getUniqueID())))), log); |
||||||
|
return new DaemonTaskSuccessResult(task); |
||||||
|
|
||||||
|
case PauseAll: |
||||||
|
|
||||||
|
// Resume all torrents
|
||||||
|
makeRequest(buildRequest(RPC_METHOD_PAUSE_ALL, null), log); |
||||||
|
return new DaemonTaskSuccessResult(task); |
||||||
|
|
||||||
|
case Resume: |
||||||
|
|
||||||
|
// Resume a torrent
|
||||||
|
ResumeTask resumeTask = (ResumeTask) task; |
||||||
|
makeRequest(buildRequest(RPC_METHOD_RESUME, ((new JSONArray()) |
||||||
|
.put((new JSONArray()).put(resumeTask.getTargetTorrent().getUniqueID())))), log); |
||||||
|
return new DaemonTaskSuccessResult(task); |
||||||
|
|
||||||
|
case ResumeAll: |
||||||
|
|
||||||
|
// Resume all torrents
|
||||||
|
makeRequest(buildRequest(RPC_METHOD_RESUME_ALL, null), log); |
||||||
|
return new DaemonTaskSuccessResult(task); |
||||||
|
|
||||||
|
case SetFilePriorities: |
||||||
|
|
||||||
|
// Set the priorities of files in a specific torrent
|
||||||
|
SetFilePriorityTask prioTask = (SetFilePriorityTask) task; |
||||||
|
|
||||||
|
// We first need a listing of all the files (because we can only set the priorities all at once)
|
||||||
|
params.put(task.getTargetTorrent().getUniqueID()); // torrent_id
|
||||||
|
params.put(ffields); // keys
|
||||||
|
JSONObject pinfo = makeRequest(buildRequest(RPC_METHOD_STATUS, params), log); |
||||||
|
ArrayList<TorrentFile> pfiles = |
||||||
|
parseJsonFileListing(pinfo.getJSONObject(RPC_RESULT), prioTask.getTargetTorrent()); |
||||||
|
|
||||||
|
// Now prepare the new list of priorities
|
||||||
|
params = new JSONArray(); |
||||||
|
params.put(task.getTargetTorrent().getUniqueID()); // torrent_id
|
||||||
|
JSONArray pfields = new JSONArray(); |
||||||
|
// Override the priorities in the just retrieved list of all files
|
||||||
|
for (TorrentFile pfile : pfiles) { |
||||||
|
Priority newPriority = pfile.getPriority(); |
||||||
|
for (TorrentFile forFile : prioTask.getForFiles()) { |
||||||
|
if (forFile.getKey().equals(pfile.getKey())) { |
||||||
|
// This is a file that we want to assign a new priority to
|
||||||
|
newPriority = prioTask.getNewPriority(); |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
pfields.put(convertPriority(newPriority)); |
||||||
|
} |
||||||
|
params.put(pfields); // keys
|
||||||
|
|
||||||
|
// Make a single call to set the priorities on all files at once
|
||||||
|
makeRequest(buildRequest(RPC_METHOD_SETFILE, params), log); |
||||||
|
return new DaemonTaskSuccessResult(task); |
||||||
|
|
||||||
|
case SetDownloadLocation: |
||||||
|
|
||||||
|
// Set the download location of some torrent
|
||||||
|
SetDownloadLocationTask sdlTask = (SetDownloadLocationTask) task; |
||||||
|
// This works, but does not move the torrent
|
||||||
|
//makeRequest(buildRequest(RPC_METHOD_SETOPTIONS, buildSetTorrentOptions(
|
||||||
|
// sdlTask.getTargetTorrent().getUniqueID(), RPC_DOWNLOADLOCATION, sdlTask.getNewLocation())));
|
||||||
|
params.put(new JSONArray().put(task.getTargetTorrent().getUniqueID())); |
||||||
|
params.put(sdlTask.getNewLocation()); |
||||||
|
makeRequest(buildRequest(RPC_METHOD_MOVESTORAGE, params), log); |
||||||
|
return new DaemonTaskSuccessResult(task); |
||||||
|
|
||||||
|
case SetTransferRates: |
||||||
|
|
||||||
|
// Request to set the maximum transfer rates
|
||||||
|
SetTransferRatesTask ratesTask = (SetTransferRatesTask) task; |
||||||
|
JSONObject map = new JSONObject(); |
||||||
|
map.put(RPC_MAXUPLOAD, (ratesTask.getUploadRate() == null ? -1 : ratesTask.getUploadRate())); |
||||||
|
map.put(RPC_MAXDOWNLOAD, (ratesTask.getDownloadRate() == null ? -1 : ratesTask.getDownloadRate())); |
||||||
|
|
||||||
|
makeRequest(buildRequest(RPC_METHOD_SETCONFIG, (new JSONArray()).put(map)), log); |
||||||
|
return new DaemonTaskSuccessResult(task); |
||||||
|
|
||||||
|
case SetLabel: |
||||||
|
|
||||||
|
// Request to set the label
|
||||||
|
SetLabelTask labelTask = (SetLabelTask) task; |
||||||
|
params.put(task.getTargetTorrent().getUniqueID()); |
||||||
|
params.put(labelTask.getNewLabel() == null ? "" : labelTask.getNewLabel()); |
||||||
|
makeRequest(buildRequest(RPC_METHOD_SETLABEL, params), log); |
||||||
|
return new DaemonTaskSuccessResult(task); |
||||||
|
|
||||||
|
case SetTrackers: |
||||||
|
|
||||||
|
// Set the trackers of some torrent
|
||||||
|
SetTrackersTask trackersTask = (SetTrackersTask) task; |
||||||
|
JSONArray trackers = new JSONArray(); |
||||||
|
// Build an JSON arrays of objcts that each have a tier (order) number and an url
|
||||||
|
for (int i = 0; i < trackersTask.getNewTrackers().size(); i++) { |
||||||
|
JSONObject trackerObj = new JSONObject(); |
||||||
|
trackerObj.put("tier", i); |
||||||
|
trackerObj.put("url", trackersTask.getNewTrackers().get(i)); |
||||||
|
trackers.put(trackerObj); |
||||||
|
} |
||||||
|
params.put(new JSONArray().put(task.getTargetTorrent().getUniqueID())); |
||||||
|
params.put(trackers); |
||||||
|
makeRequest(buildRequest(RPC_METHOD_SETTRACKERS, params), log); |
||||||
|
return new DaemonTaskSuccessResult(task); |
||||||
|
|
||||||
|
case ForceRecheck: |
||||||
|
|
||||||
|
// Pause a torrent
|
||||||
|
makeRequest(buildRequest(RPC_METHOD_FORCERECHECK, |
||||||
|
((new JSONArray()).put((new JSONArray()).put(task.getTargetTorrent().getUniqueID())))), |
||||||
|
log); |
||||||
|
return new DaemonTaskSuccessResult(task); |
||||||
|
|
||||||
|
default: |
||||||
|
return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.MethodUnsupported, |
||||||
|
task.getMethod() + " is not supported by " + getType())); |
||||||
|
} |
||||||
|
} catch (JSONException e) { |
||||||
|
return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.ParsingFailed, e.toString())); |
||||||
|
} catch (DaemonException e) { |
||||||
|
return new DaemonTaskFailureResult(task, e); |
||||||
|
} catch (FileNotFoundException e) { |
||||||
|
return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.FileAccessError, e.toString())); |
||||||
|
} catch (IOException e) { |
||||||
|
return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.FileAccessError, e.toString())); |
||||||
|
} catch (InterruptedException e) { |
||||||
|
return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.FileAccessError, e.toString())); |
||||||
|
} catch (ExecutionException e) { |
||||||
|
e.printStackTrace(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/*private JSONArray buildSetTorrentOptions(String torrent, String key, String value) throws JSONException { |
||||||
|
JSONArray params = new JSONArray(); |
||||||
|
params.put(new JSONArray().put(torrent)); // torrent_id
|
||||||
|
JSONObject sdlmap = new JSONObject(); |
||||||
|
// Set the option setting to the torrent
|
||||||
|
sdlmap.put(key, value); |
||||||
|
params.put(sdlmap); // options
|
||||||
|
return params; |
||||||
|
}*/ |
||||||
|
|
||||||
|
private void ensureVersion(Log log) throws DaemonException { |
||||||
|
if (version > 0) { |
||||||
|
return; |
||||||
|
} |
||||||
|
// We still need to retrieve the version number from the server
|
||||||
|
// Do this by getting the web interface main html page and trying to parse the version number
|
||||||
|
// Format is something like '<title>Deluge: Web UI 1.3.6</title>'
|
||||||
|
if (httpclient == null) { |
||||||
|
initialise(); |
||||||
|
} |
||||||
|
try { |
||||||
|
HttpResponse response = httpclient.execute(new HttpGet(buildWebUIUrl() + "/")); |
||||||
|
String main = HttpHelper.convertStreamToString(response.getEntity().getContent()); |
||||||
|
String titleStartText = "<title>Deluge: Web UI "; |
||||||
|
String titleEndText = "</title>"; |
||||||
|
int titleStart = main.indexOf(titleStartText); |
||||||
|
int titleEnd = main.indexOf(titleEndText, titleStart); |
||||||
|
if (titleStart >= 0 && titleEnd > titleStart) { |
||||||
|
// String found: now parse a version like 2.9.7 as a number like 20907 (allowing 10 places for each .)
|
||||||
|
String[] parts = main.substring(titleStart + titleStartText.length(), titleEnd).split("\\."); |
||||||
|
if (parts.length > 0) { |
||||||
|
version = Integer.parseInt(parts[0]) * 100 * 100; |
||||||
|
if (parts.length > 1) { |
||||||
|
version += Integer.parseInt(parts[1]) * 100; |
||||||
|
if (parts.length > 2) { |
||||||
|
// For the last part only read until a non-numeric character is read
|
||||||
|
// For example version 3.0.0-alpha5 is read as version code 30000
|
||||||
|
String numbers = ""; |
||||||
|
for (char c : parts[2].toCharArray()) { |
||||||
|
if (Character.isDigit(c)) |
||||||
|
// Still a number; add it to the numbers string
|
||||||
|
{ |
||||||
|
numbers += Character.toString(c); |
||||||
|
} else { |
||||||
|
// No longer reading numbers; stop reading
|
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
version += Integer.parseInt(numbers); |
||||||
|
return; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} catch (NumberFormatException e) { |
||||||
|
log.d(LOG_NAME, "Error parsing the Deluge version code as number: " + e.toString()); |
||||||
|
// Continue though, ignoring the version number
|
||||||
|
} catch (Exception e) { |
||||||
|
log.d(LOG_NAME, "Error: " + e.toString()); |
||||||
|
throw new DaemonException(ExceptionType.ConnectionError, e.toString()); |
||||||
|
} |
||||||
|
// Unable to establish version number; assume an old version by setting it to version 1
|
||||||
|
version = 10000; |
||||||
|
} |
||||||
|
|
||||||
|
private JSONObject buildRequest(String sendMethod, JSONArray params) throws JSONException { |
||||||
|
|
||||||
|
// Build request for method
|
||||||
|
JSONObject request = new JSONObject(); |
||||||
|
request.put(RPC_METHOD, sendMethod); |
||||||
|
request.put(RPC_PARAMS, (params == null) ? new JSONArray() : params); |
||||||
|
request.put(RPC_ID, 2); |
||||||
|
return request; |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
private synchronized JSONObject makeRequest(JSONObject data, Log log) throws DaemonException { |
||||||
|
|
||||||
|
try { |
||||||
|
|
||||||
|
// Initialise the HTTP client
|
||||||
|
if (httpclient == null) { |
||||||
|
initialise(); |
||||||
|
} |
||||||
|
|
||||||
|
// Login first?
|
||||||
|
if (sessionCookie == null) { |
||||||
|
|
||||||
|
// Build login object
|
||||||
|
String extraPass = settings.getExtraPassword(); |
||||||
|
if (extraPass == null) { |
||||||
|
extraPass = ""; |
||||||
|
} |
||||||
|
JSONObject loginRequest = new JSONObject(); |
||||||
|
loginRequest.put(RPC_METHOD, RPC_METHOD_LOGIN); |
||||||
|
loginRequest.put(RPC_PARAMS, (new JSONArray()).put(extraPass)); |
||||||
|
loginRequest.put(RPC_ID, 1); |
||||||
|
|
||||||
|
// Set POST URL and data
|
||||||
|
HttpPost httppost = new HttpPost(buildWebUIUrl() + PATH_TO_RPC); |
||||||
|
httppost.setHeader("content-type", "application/json"); |
||||||
|
StringEntity se = new StringEntity(loginRequest.toString()); |
||||||
|
httppost.setEntity(se); |
||||||
|
|
||||||
|
// Execute
|
||||||
|
HttpResponse response = httpclient.execute(httppost); |
||||||
|
InputStream instream = response.getEntity().getContent(); |
||||||
|
|
||||||
|
// Retrieve session ID
|
||||||
|
if (!httpclient.getCookieStore().getCookies().isEmpty()) { |
||||||
|
for (Cookie cookie : httpclient.getCookieStore().getCookies()) { |
||||||
|
if (cookie.getName().equals(RPC_SESSION_ID)) { |
||||||
|
sessionCookie = cookie; |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Still no session cookie?
|
||||||
|
if (sessionCookie == null) { |
||||||
|
// Set error message and cancel the action that was requested
|
||||||
|
throw new DaemonException(ExceptionType.AuthenticationFailure, |
||||||
|
"Password error? Server time difference? No (valid) cookie in response and JSON was: " + |
||||||
|
HttpHelper.convertStreamToString(instream)); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
// Regular action
|
||||||
|
|
||||||
|
// Set POST URL and data
|
||||||
|
HttpPost httppost = new HttpPost(buildWebUIUrl() + PATH_TO_RPC); |
||||||
|
httppost.setHeader("content-type", "application/json"); |
||||||
|
StringEntity se = new StringEntity(data.toString(), HTTP.UTF_8); |
||||||
|
httppost.setEntity(se); |
||||||
|
|
||||||
|
// Set session cookie, if it was not in the httpclient object yet
|
||||||
|
boolean cookiePresent = false; |
||||||
|
for (Cookie cookie : httpclient.getCookieStore().getCookies()) { |
||||||
|
if (cookie.getName().equals(RPC_SESSION_ID)) { |
||||||
|
cookiePresent = true; |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
if (!cookiePresent) { |
||||||
|
httpclient.getCookieStore().addCookie(sessionCookie); |
||||||
|
} |
||||||
|
|
||||||
|
// Execute
|
||||||
|
HttpResponse response = httpclient.execute(httppost); |
||||||
|
|
||||||
|
HttpEntity entity = response.getEntity(); |
||||||
|
if (entity != null) { |
||||||
|
|
||||||
|
// Read JSON response
|
||||||
|
InputStream instream = entity.getContent(); |
||||||
|
String result = HttpHelper.convertStreamToString(instream); |
||||||
|
JSONObject json = new JSONObject(result); |
||||||
|
instream.close(); |
||||||
|
|
||||||
|
log.d(LOG_NAME, "Success: " + |
||||||
|
(result.length() > 300 ? result.substring(0, 300) + "... (" + result.length() + " chars)" : |
||||||
|
result)); |
||||||
|
|
||||||
|
// Return JSON object
|
||||||
|
return json; |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
// No result?
|
||||||
|
throw new DaemonException(ExceptionType.UnexpectedResponse, "No HTTP entity in response object."); |
||||||
|
|
||||||
|
} catch (JSONException e) { |
||||||
|
log.d(LOG_NAME, "Error: " + e.toString()); |
||||||
|
throw new DaemonException(ExceptionType.UnexpectedResponse, e.toString()); |
||||||
|
} catch (Exception e) { |
||||||
|
log.d(LOG_NAME, "Error: " + e.toString()); |
||||||
|
throw new DaemonException(ExceptionType.ConnectionError, e.toString()); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Instantiates an HTTP client with proper credentials that can be used for all Transmission requests. |
||||||
|
* @throws DaemonException On missing settings |
||||||
|
*/ |
||||||
|
private void initialise() throws DaemonException { |
||||||
|
|
||||||
|
httpclient = HttpHelper.createStandardHttpClient(settings, |
||||||
|
settings.getUsername() != null && !settings.getUsername().equals("")); |
||||||
|
httpclient.addRequestInterceptor(HttpHelper.gzipRequestInterceptor); |
||||||
|
httpclient.addResponseInterceptor(HttpHelper.gzipResponseInterceptor); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Build the URL of the Transmission web UI from the user settings. |
||||||
|
* @return The URL of the RPC API |
||||||
|
*/ |
||||||
|
private String buildWebUIUrl() { |
||||||
|
return (settings.getSsl() ? "https://" : "http://") + settings.getAddress() + ":" + settings.getPort() + |
||||||
|
(settings.getFolder() == null ? "" : settings.getFolder()); |
||||||
|
} |
||||||
|
|
||||||
|
private ArrayList<Torrent> parseJsonRetrieveTorrents(JSONObject response) throws JSONException, DaemonException { |
||||||
|
|
||||||
|
// Parse response
|
||||||
|
ArrayList<Torrent> torrents = new ArrayList<Torrent>(); |
||||||
|
if (response.isNull(RPC_TORRENTS)) { |
||||||
|
throw new DaemonException(ExceptionType.NotConnected, |
||||||
|
"Web interface probably not connected to a daemon yet, because 'torrents' is null: " + |
||||||
|
response.toString()); |
||||||
|
} |
||||||
|
JSONObject objects = response.getJSONObject(RPC_TORRENTS); |
||||||
|
JSONArray names = objects.names(); |
||||||
|
if (names != null) { |
||||||
|
for (int j = 0; j < names.length(); j++) { |
||||||
|
|
||||||
|
JSONObject tor = objects.getJSONObject(names.getString(j)); |
||||||
|
// Add the parsed torrent to the list
|
||||||
|
TorrentStatus status = convertDelugeState(tor.getString(RPC_STATUS)); |
||||||
|
String error = tor.getString(RPC_MESSAGE); |
||||||
|
if (tor.getString(RPC_TRACKER_STATUS).indexOf("Error") > 0) { |
||||||
|
error += (error.length() > 0 ? "\n" : "") + tor.getString(RPC_TRACKER_STATUS); |
||||||
|
//status = TorrentStatus.Error; // Don't report this as blocking error
|
||||||
|
} |
||||||
|
// @formatter:off
|
||||||
|
torrents.add(new Torrent(j, |
||||||
|
names.getString(j), |
||||||
|
tor.getString(RPC_NAME), |
||||||
|
status, |
||||||
|
tor.getString(RPC_SAVEPATH) + settings.getOS().getPathSeperator(), |
||||||
|
tor.getInt(RPC_RATEDOWNLOAD), |
||||||
|
tor.getInt(RPC_RATEUPLOAD), |
||||||
|
tor.getInt(RPC_NUMSEEDS), |
||||||
|
tor.getInt(RPC_TOTALSEEDS), |
||||||
|
tor.getInt(RPC_NUMPEERS), |
||||||
|
tor.getInt(RPC_TOTALPEERS), |
||||||
|
tor.getInt(RPC_ETA), |
||||||
|
tor.getLong(RPC_DOWNLOADEDEVER), |
||||||
|
tor.getLong(RPC_UPLOADEDEVER), |
||||||
|
tor.getLong(RPC_TOTALSIZE), |
||||||
|
((float) tor.getDouble(RPC_PARTDONE)) / 100f, // Percentage to [0..1]
|
||||||
|
0f, // Not available
|
||||||
|
tor.has(RPC_LABEL)? tor.getString(RPC_LABEL): null, |
||||||
|
tor.has(RPC_TIMEADDED)? new Date((long) (tor.getDouble(RPC_TIMEADDED) * 1000L)): null, |
||||||
|
null, // Not available
|
||||||
|
error, |
||||||
|
settings.getType())); |
||||||
|
// @formatter:on
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Return the list
|
||||||
|
return torrents; |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
private ArrayList<Label> parseJsonRetrieveLabels(JSONObject response) throws JSONException { |
||||||
|
|
||||||
|
// Get the labels, of they exist (which is dependent on the plugin)
|
||||||
|
if (!response.has("filters")) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
JSONObject filters = response.getJSONObject("filters"); |
||||||
|
if (!filters.has("label")) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
JSONArray labels = filters.getJSONArray("label"); |
||||||
|
|
||||||
|
// Parse response
|
||||||
|
ArrayList<Label> allLabels = new ArrayList<Label>(); |
||||||
|
for (int i = 0; i < labels.length(); i++) { |
||||||
|
JSONArray labelAndCount = labels.getJSONArray(i); |
||||||
|
if (labelAndCount.getString(0).equals("All")) { |
||||||
|
continue; // Ignore the 'All' filter, which is not an actual label
|
||||||
|
} |
||||||
|
allLabels.add(new Label(labelAndCount.getString(0), labelAndCount.getInt(1))); |
||||||
|
} |
||||||
|
return allLabels; |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
private ArrayList<TorrentFile> parseJsonFileListing(JSONObject response, Torrent torrent) throws JSONException { |
||||||
|
|
||||||
|
// Parse response
|
||||||
|
ArrayList<TorrentFile> files = new ArrayList<TorrentFile>(); |
||||||
|
JSONArray objects = response.getJSONArray(RPC_DETAILS); |
||||||
|
JSONArray progress = response.getJSONArray(RPC_FILEPROGRESS); |
||||||
|
JSONArray priorities = response.getJSONArray(RPC_FILEPRIORITIES); |
||||||
|
if (objects != null) { |
||||||
|
for (int j = 0; j < objects.length(); j++) { |
||||||
|
|
||||||
|
JSONObject file = objects.getJSONObject(j); |
||||||
|
// Add the parsed torrent to the list
|
||||||
|
// @formatter:off
|
||||||
|
files.add(new TorrentFile( |
||||||
|
"" + file.getInt(RPC_INDEX), |
||||||
|
file.getString(RPC_PATH), |
||||||
|
file.getString(RPC_PATH), |
||||||
|
torrent.getLocationDir() + file.getString(RPC_PATH), |
||||||
|
file.getLong(RPC_SIZE), |
||||||
|
(long) (progress.getDouble(j) * file.getLong(RPC_SIZE)), |
||||||
|
convertDelugePriority(priorities.getInt(j)))); |
||||||
|
// @formatter:on
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Return the list
|
||||||
|
return files; |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
private Priority convertDelugePriority(int priority) { |
||||||
|
if (version >= 10303) { |
||||||
|
// Priority codes changes from Deluge 1.3.3 onwards
|
||||||
|
switch (priority) { |
||||||
|
case 0: |
||||||
|
return Priority.Off; |
||||||
|
case 1: |
||||||
|
return Priority.Low; |
||||||
|
case 7: |
||||||
|
return Priority.High; |
||||||
|
default: |
||||||
|
return Priority.Normal; |
||||||
|
} |
||||||
|
} else { |
||||||
|
switch (priority) { |
||||||
|
case 0: |
||||||
|
return Priority.Off; |
||||||
|
case 2: |
||||||
|
return Priority.Normal; |
||||||
|
case 5: |
||||||
|
return Priority.High; |
||||||
|
default: |
||||||
|
return Priority.Low; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private int convertPriority(Priority priority) { |
||||||
|
if (version >= 10303) { |
||||||
|
// Priority codes changes from Deluge 1.3.3 onwards
|
||||||
|
switch (priority) { |
||||||
|
case Off: |
||||||
|
return 0; |
||||||
|
case Low: |
||||||
|
return 1; |
||||||
|
case High: |
||||||
|
return 7; |
||||||
|
default: |
||||||
|
return 5; |
||||||
|
} |
||||||
|
} else { |
||||||
|
switch (priority) { |
||||||
|
case Off: |
||||||
|
return 0; |
||||||
|
case Normal: |
||||||
|
return 2; |
||||||
|
case High: |
||||||
|
return 5; |
||||||
|
default: |
||||||
|
return 1; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private TorrentStatus convertDelugeState(String state) { |
||||||
|
// Deluge sends a string with status code
|
||||||
|
if (state.compareTo("Paused") == 0) { |
||||||
|
return TorrentStatus.Paused; |
||||||
|
} else if (state.compareTo("Seeding") == 0) { |
||||||
|
return TorrentStatus.Seeding; |
||||||
|
} else if (state.compareTo("Downloading") == 0 || state.compareTo("Active") == 0) { |
||||||
|
return TorrentStatus.Downloading; |
||||||
|
} else if (state.compareTo("Checking") == 0) { |
||||||
|
return TorrentStatus.Checking; |
||||||
|
} else if (state.compareTo("Queued") == 0) { |
||||||
|
return TorrentStatus.Queued; |
||||||
|
} |
||||||
|
return TorrentStatus.Unknown; |
||||||
|
} |
||||||
|
|
||||||
|
private TorrentDetails parseJsonTorrentDetails(JSONObject response) throws JSONException { |
||||||
|
|
||||||
|
// Parse response
|
||||||
|
List<String> trackers = new ArrayList<String>(); |
||||||
|
JSONArray trackerObjects = response.getJSONArray(RPC_TRACKERS); |
||||||
|
if (trackerObjects != null && trackerObjects.length() > 0) { |
||||||
|
for (int i = 0; i < trackerObjects.length(); i++) { |
||||||
|
trackers.add(trackerObjects.getJSONObject(i).getString("url")); |
||||||
|
} |
||||||
|
} |
||||||
|
List<String> errors = new ArrayList<String>(); |
||||||
|
String trackerStatus = response.getString(RPC_TRACKER_STATUS); |
||||||
|
errors.add(trackerStatus); |
||||||
|
|
||||||
|
return new TorrentDetails(trackers, errors); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Daemon getType() { |
||||||
|
return settings.getType(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public DaemonSettings getSettings() { |
||||||
|
return this.settings; |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,32 @@ |
|||||||
|
package se.dimovski.rencode; |
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream; |
||||||
|
import java.io.ByteArrayOutputStream; |
||||||
|
import java.io.IOException; |
||||||
|
import java.io.InputStream; |
||||||
|
|
||||||
|
public class Rencode |
||||||
|
{ |
||||||
|
|
||||||
|
public static Object decode(byte[] data) throws IOException |
||||||
|
{ |
||||||
|
final InputStream is = new ByteArrayInputStream(data); |
||||||
|
final RencodeInputStream inputStream = new RencodeInputStream(is); |
||||||
|
|
||||||
|
final Object decoded = inputStream.readObject(); |
||||||
|
inputStream.close(); |
||||||
|
|
||||||
|
return decoded; |
||||||
|
} |
||||||
|
|
||||||
|
public static byte[] encode(Object obj) throws IOException |
||||||
|
{ |
||||||
|
final ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
||||||
|
final RencodeOutputStream output = new RencodeOutputStream(baos); |
||||||
|
output.writeObject(obj); |
||||||
|
final byte[] encoded = baos.toByteArray(); |
||||||
|
output.close(); |
||||||
|
return encoded; |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,494 @@ |
|||||||
|
package se.dimovski.rencode; |
||||||
|
|
||||||
|
import java.io.DataInput; |
||||||
|
import java.io.EOFException; |
||||||
|
import java.io.FilterInputStream; |
||||||
|
import java.io.IOException; |
||||||
|
import java.io.InputStream; |
||||||
|
import java.io.UnsupportedEncodingException; |
||||||
|
import java.math.BigDecimal; |
||||||
|
import java.math.BigInteger; |
||||||
|
import java.nio.ByteBuffer; |
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.TreeMap; |
||||||
|
|
||||||
|
public class RencodeInputStream extends FilterInputStream implements DataInput |
||||||
|
{ |
||||||
|
/** |
||||||
|
* The charset that is being used for {@link String}s. |
||||||
|
*/ |
||||||
|
private final String charset; |
||||||
|
|
||||||
|
/** |
||||||
|
* Whether or not all byte-Arrays should be decoded as {@link String}s. |
||||||
|
*/ |
||||||
|
private final boolean decodeAsString; |
||||||
|
|
||||||
|
/** |
||||||
|
* Creates a {@link RencodeInputStream} with the default encoding. |
||||||
|
*/ |
||||||
|
public RencodeInputStream(InputStream in) |
||||||
|
{ |
||||||
|
this(in, Utils.UTF_8, false); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Creates a {@link RencodeInputStream} with the given encoding. |
||||||
|
*/ |
||||||
|
public RencodeInputStream(InputStream in, String charset) |
||||||
|
{ |
||||||
|
this(in, charset, false); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Creates a {@link RencodeInputStream} with the default encoding. |
||||||
|
*/ |
||||||
|
public RencodeInputStream(InputStream in, boolean decodeAsString) |
||||||
|
{ |
||||||
|
this(in, Utils.UTF_8, decodeAsString); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Creates a {@link RencodeInputStream} with the given encoding. |
||||||
|
*/ |
||||||
|
public RencodeInputStream(InputStream in, String charset, boolean decodeAsString) |
||||||
|
{ |
||||||
|
super(in); |
||||||
|
|
||||||
|
if (charset == null) |
||||||
|
{ |
||||||
|
throw new IllegalArgumentException("charset is null"); |
||||||
|
} |
||||||
|
|
||||||
|
this.charset = charset; |
||||||
|
this.decodeAsString = decodeAsString; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns the charset that is used to decode {@link String}s. The default |
||||||
|
* value is UTF-8. |
||||||
|
*/ |
||||||
|
public String getCharset() |
||||||
|
{ |
||||||
|
return charset; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns true if all byte-Arrays are being turned into {@link String}s. |
||||||
|
*/ |
||||||
|
public boolean isDecodeAsString() |
||||||
|
{ |
||||||
|
return decodeAsString; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Reads and returns an {@link Object}. |
||||||
|
*/ |
||||||
|
public Object readObject() throws IOException |
||||||
|
{ |
||||||
|
int token = readToken(); |
||||||
|
|
||||||
|
return readObject(token); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Reads and returns an {@link Object}. |
||||||
|
*/ |
||||||
|
protected Object readObject(int token) throws IOException |
||||||
|
{ |
||||||
|
if (token == TypeCode.DICTIONARY) |
||||||
|
{ |
||||||
|
return readMap0(Object.class); |
||||||
|
} |
||||||
|
else if (Utils.isFixedDictionary(token)) |
||||||
|
{ |
||||||
|
return readMap0(Object.class, token); |
||||||
|
} |
||||||
|
else if (token == TypeCode.LIST) |
||||||
|
{ |
||||||
|
return readList0(Object.class); |
||||||
|
} |
||||||
|
else if (Utils.isFixedList(token)) |
||||||
|
{ |
||||||
|
return readList0(Object.class, token); |
||||||
|
} |
||||||
|
else if (Utils.isNumber(token)) |
||||||
|
{ |
||||||
|
return readNumber0(token); |
||||||
|
} |
||||||
|
else if (token == TypeCode.FALSE || token == TypeCode.TRUE) |
||||||
|
{ |
||||||
|
return readBoolean0(token); |
||||||
|
} |
||||||
|
else if (token == TypeCode.NULL) |
||||||
|
{ |
||||||
|
return null; |
||||||
|
} |
||||||
|
else if (Utils.isDigit(token) || Utils.isFixedString(token)) |
||||||
|
{ |
||||||
|
return readString(token, charset); |
||||||
|
} |
||||||
|
|
||||||
|
throw new IOException("Not implemented: " + token); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Reads and returns a {@link Map}. |
||||||
|
*/ |
||||||
|
public Map<String, ?> readMap() throws IOException |
||||||
|
{ |
||||||
|
return readMap(Object.class); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Reads and returns a {@link Map}. |
||||||
|
*/ |
||||||
|
public <T> Map<String, T> readMap(Class<T> clazz) throws IOException |
||||||
|
{ |
||||||
|
int token = readToken(); |
||||||
|
|
||||||
|
if (token != TypeCode.DICTIONARY) |
||||||
|
{ |
||||||
|
throw new IOException(); |
||||||
|
} |
||||||
|
|
||||||
|
return readMap0(clazz); |
||||||
|
} |
||||||
|
|
||||||
|
private <T> Map<String, T> readMap0(Class<T> clazz) throws IOException |
||||||
|
{ |
||||||
|
Map<String, T> map = new TreeMap<String, T>(); |
||||||
|
int token = -1; |
||||||
|
while ((token = readToken()) != TypeCode.END) |
||||||
|
{ |
||||||
|
readMapItem(clazz, token, map); |
||||||
|
} |
||||||
|
|
||||||
|
return map; |
||||||
|
} |
||||||
|
|
||||||
|
private <T> Map<String, T> readMap0(Class<T> clazz, int token) throws IOException |
||||||
|
{ |
||||||
|
Map<String, T> map = new TreeMap<String, T>(); |
||||||
|
|
||||||
|
int count = token - TypeCode.EMBEDDED.DICT_START; |
||||||
|
for (int i = 0; i < count; i++) |
||||||
|
{ |
||||||
|
readMapItem(clazz, readToken(), map); |
||||||
|
} |
||||||
|
|
||||||
|
return map; |
||||||
|
} |
||||||
|
|
||||||
|
private <T> void readMapItem(Class<T> clazz, int token, Map<String, T> map) throws UnsupportedEncodingException, |
||||||
|
IOException |
||||||
|
{ |
||||||
|
String key = readString(token, charset); |
||||||
|
T value = clazz.cast(readObject()); |
||||||
|
|
||||||
|
map.put(key, value); |
||||||
|
} |
||||||
|
|
||||||
|
public int readToken() throws IOException |
||||||
|
{ |
||||||
|
int token = super.read(); |
||||||
|
if (token == -1) |
||||||
|
{ |
||||||
|
throw new EOFException(); |
||||||
|
} |
||||||
|
return token; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Reads and returns a {@link List}. |
||||||
|
*/ |
||||||
|
public List<?> readList() throws IOException |
||||||
|
{ |
||||||
|
return readList(Object.class); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Reads and returns a {@link List}. |
||||||
|
*/ |
||||||
|
public <T> List<T> readList(Class<T> clazz) throws IOException |
||||||
|
{ |
||||||
|
int token = readToken(); |
||||||
|
|
||||||
|
if (token != TypeCode.LIST) |
||||||
|
{ |
||||||
|
throw new IOException(); |
||||||
|
} |
||||||
|
|
||||||
|
return readList0(clazz); |
||||||
|
} |
||||||
|
|
||||||
|
private <T> List<T> readList0(Class<T> clazz) throws IOException |
||||||
|
{ |
||||||
|
List<T> list = new ArrayList<T>(); |
||||||
|
int token = -1; |
||||||
|
while ((token = readToken()) != TypeCode.END) |
||||||
|
{ |
||||||
|
list.add(clazz.cast(readObject(token))); |
||||||
|
} |
||||||
|
return list; |
||||||
|
} |
||||||
|
|
||||||
|
private <T> List<T> readList0(Class<T> clazz, int token) throws IOException |
||||||
|
{ |
||||||
|
List<T> list = new ArrayList<T>(); |
||||||
|
int length = token - TypeCode.EMBEDDED.LIST_START; |
||||||
|
for (int i = 0; i < length; i++) |
||||||
|
{ |
||||||
|
list.add(clazz.cast(readObject())); |
||||||
|
} |
||||||
|
return list; |
||||||
|
} |
||||||
|
|
||||||
|
public boolean readBoolean() throws IOException |
||||||
|
{ |
||||||
|
return readBoolean0(readToken()); |
||||||
|
} |
||||||
|
|
||||||
|
public boolean readBoolean0(int token) throws IOException |
||||||
|
{ |
||||||
|
if (token == TypeCode.FALSE) |
||||||
|
{ |
||||||
|
return false; |
||||||
|
} |
||||||
|
else if (token == TypeCode.TRUE) |
||||||
|
{ |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
throw new IOException(); |
||||||
|
} |
||||||
|
|
||||||
|
public byte readByte() throws IOException |
||||||
|
{ |
||||||
|
return (byte) readToken(); |
||||||
|
} |
||||||
|
|
||||||
|
public char readChar() throws IOException |
||||||
|
{ |
||||||
|
return (char) readToken(); |
||||||
|
} |
||||||
|
|
||||||
|
public double readDouble() throws IOException |
||||||
|
{ |
||||||
|
return readNumber().doubleValue(); |
||||||
|
} |
||||||
|
|
||||||
|
public float readFloat() throws IOException |
||||||
|
{ |
||||||
|
return readNumber().floatValue(); |
||||||
|
} |
||||||
|
|
||||||
|
public void readFully(byte[] dst) throws IOException |
||||||
|
{ |
||||||
|
readFully(dst, 0, dst.length); |
||||||
|
} |
||||||
|
|
||||||
|
public void readFully(byte[] dst, int off, int len) throws IOException |
||||||
|
{ |
||||||
|
int total = 0; |
||||||
|
|
||||||
|
while (total < len) |
||||||
|
{ |
||||||
|
int r = read(dst, total, len - total); |
||||||
|
if (r == -1) |
||||||
|
{ |
||||||
|
throw new EOFException(); |
||||||
|
} |
||||||
|
|
||||||
|
total += r; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public int readInt() throws IOException |
||||||
|
{ |
||||||
|
return readNumber().intValue(); |
||||||
|
} |
||||||
|
|
||||||
|
public String readLine() throws IOException |
||||||
|
{ |
||||||
|
return readString(); |
||||||
|
} |
||||||
|
|
||||||
|
public long readLong() throws IOException |
||||||
|
{ |
||||||
|
return readNumber().longValue(); |
||||||
|
} |
||||||
|
|
||||||
|
public short readShort() throws IOException |
||||||
|
{ |
||||||
|
return readNumber().shortValue(); |
||||||
|
} |
||||||
|
|
||||||
|
public String readUTF() throws IOException |
||||||
|
{ |
||||||
|
return readString(Utils.UTF_8); |
||||||
|
} |
||||||
|
|
||||||
|
public int readUnsignedByte() throws IOException |
||||||
|
{ |
||||||
|
return readByte() & 0xFF; |
||||||
|
} |
||||||
|
|
||||||
|
public int readUnsignedShort() throws IOException |
||||||
|
{ |
||||||
|
return readShort() & 0xFFFF; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Reads and returns a {@link Number}. |
||||||
|
*/ |
||||||
|
public Number readNumber() throws IOException |
||||||
|
{ |
||||||
|
int token = readToken(); |
||||||
|
|
||||||
|
if (!Utils.isNumber(token)) |
||||||
|
{ |
||||||
|
throw new IOException(); |
||||||
|
} |
||||||
|
|
||||||
|
return readNumber0(token); |
||||||
|
} |
||||||
|
|
||||||
|
private Number readNumber0(int token) throws IOException |
||||||
|
{ |
||||||
|
switch (token) |
||||||
|
{ |
||||||
|
case TypeCode.BYTE: |
||||||
|
return (int) readToBuffer(1).get(); |
||||||
|
case TypeCode.SHORT: |
||||||
|
return (int) readToBuffer(2).getShort(); |
||||||
|
case TypeCode.INT: |
||||||
|
return readToBuffer(4).getInt(); |
||||||
|
case TypeCode.LONG: |
||||||
|
return readToBuffer(8).getLong(); |
||||||
|
case TypeCode.FLOAT: |
||||||
|
return readToBuffer(4).getFloat(); |
||||||
|
case TypeCode.DOUBLE: |
||||||
|
return readToBuffer(8).getDouble(); |
||||||
|
|
||||||
|
case TypeCode.NUMBER: |
||||||
|
return readNumber0(); |
||||||
|
} |
||||||
|
if (Utils.isNegativeFixedNumber(token)) |
||||||
|
{ |
||||||
|
return TypeCode.EMBEDDED.INT_NEG_START - 1 - token; |
||||||
|
} |
||||||
|
else if (Utils.isPositiveFixedNumber(token)) |
||||||
|
{ |
||||||
|
return TypeCode.EMBEDDED.INT_POS_START + token; |
||||||
|
} |
||||||
|
|
||||||
|
throw new IOException("Unknown number. TypeCode: " + token); |
||||||
|
} |
||||||
|
|
||||||
|
private ByteBuffer readToBuffer(int count) throws IOException |
||||||
|
{ |
||||||
|
return ByteBuffer.wrap(readBytesFixed(count)); |
||||||
|
} |
||||||
|
|
||||||
|
private Number readNumber0() throws IOException |
||||||
|
{ |
||||||
|
StringBuilder buffer = new StringBuilder(); |
||||||
|
|
||||||
|
boolean decimal = false; |
||||||
|
|
||||||
|
int token = -1; |
||||||
|
while ((token = readToken()) != TypeCode.END) |
||||||
|
{ |
||||||
|
if (token == '.') |
||||||
|
{ |
||||||
|
decimal = true; |
||||||
|
} |
||||||
|
|
||||||
|
buffer.append((char) token); |
||||||
|
} |
||||||
|
|
||||||
|
try |
||||||
|
{ |
||||||
|
if (decimal) |
||||||
|
{ |
||||||
|
return new BigDecimal(buffer.toString()); |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
return new BigInteger(buffer.toString()); |
||||||
|
} |
||||||
|
} |
||||||
|
catch (NumberFormatException err) |
||||||
|
{ |
||||||
|
throw new IOException("NumberFormatException", err); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public int skipBytes(int n) throws IOException |
||||||
|
{ |
||||||
|
return (int) skip(n); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Reads and returns a byte-Array. |
||||||
|
*/ |
||||||
|
public byte[] readBytes() throws IOException |
||||||
|
{ |
||||||
|
int token = readToken(); |
||||||
|
|
||||||
|
return readBytes(token); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Reads and returns a {@link String}. |
||||||
|
*/ |
||||||
|
public String readString() throws IOException |
||||||
|
{ |
||||||
|
return readString(charset); |
||||||
|
} |
||||||
|
|
||||||
|
private String readString(String encoding) throws IOException |
||||||
|
{ |
||||||
|
return readString(readToken(), encoding); |
||||||
|
} |
||||||
|
|
||||||
|
private String readString(int token, String charset) throws IOException |
||||||
|
{ |
||||||
|
if (Utils.isFixedString(token)) |
||||||
|
{ |
||||||
|
int length = token - TypeCode.EMBEDDED.STR_START; |
||||||
|
return new String(readBytesFixed(length), charset); |
||||||
|
} |
||||||
|
return new String(readBytes(token), charset); |
||||||
|
} |
||||||
|
|
||||||
|
private byte[] readBytes(int token) throws IOException |
||||||
|
{ |
||||||
|
int length = readLength(token); |
||||||
|
return readBytesFixed(length); |
||||||
|
} |
||||||
|
|
||||||
|
private byte[] readBytesFixed(int count) throws IOException |
||||||
|
{ |
||||||
|
byte[] data = new byte[count]; |
||||||
|
readFully(data); |
||||||
|
return data; |
||||||
|
} |
||||||
|
|
||||||
|
private int readLength(int token) throws IOException |
||||||
|
{ |
||||||
|
StringBuilder buffer = new StringBuilder(); |
||||||
|
buffer.append((char) token); |
||||||
|
|
||||||
|
while ((token = readToken()) != TypeCode.LENGTH_DELIM) |
||||||
|
{ |
||||||
|
|
||||||
|
buffer.append((char) token); |
||||||
|
} |
||||||
|
|
||||||
|
return Integer.parseInt(buffer.toString()); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,404 @@ |
|||||||
|
package se.dimovski.rencode; |
||||||
|
|
||||||
|
import java.io.DataOutput; |
||||||
|
import java.io.FilterOutputStream; |
||||||
|
import java.io.IOException; |
||||||
|
import java.io.OutputStream; |
||||||
|
import java.lang.reflect.Array; |
||||||
|
import java.nio.ByteBuffer; |
||||||
|
import java.util.Collection; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.SortedMap; |
||||||
|
import java.util.TreeMap; |
||||||
|
|
||||||
|
public class RencodeOutputStream extends FilterOutputStream implements DataOutput |
||||||
|
{ |
||||||
|
|
||||||
|
/** |
||||||
|
* The {@link String} charset. |
||||||
|
*/ |
||||||
|
private final String charset; |
||||||
|
|
||||||
|
/** |
||||||
|
* Creates a {@link RencodeOutputStream} with the default charset. |
||||||
|
*/ |
||||||
|
public RencodeOutputStream(OutputStream out) |
||||||
|
{ |
||||||
|
this(out, Utils.UTF_8); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Creates a {@link RencodeOutputStream} with the given encoding. |
||||||
|
*/ |
||||||
|
public RencodeOutputStream(OutputStream out, String charset) |
||||||
|
{ |
||||||
|
super(out); |
||||||
|
|
||||||
|
if (charset == null) |
||||||
|
{ |
||||||
|
throw new NullPointerException("charset"); |
||||||
|
} |
||||||
|
|
||||||
|
this.charset = charset; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns the charset that is used to encode {@link String}s. The default |
||||||
|
* value is UTF-8. |
||||||
|
*/ |
||||||
|
public String getCharset() |
||||||
|
{ |
||||||
|
return charset; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Writes an {@link Object}. |
||||||
|
*/ |
||||||
|
public void writeObject(Object value) throws IOException |
||||||
|
{ |
||||||
|
if (value == null) |
||||||
|
{ |
||||||
|
writeNull(); |
||||||
|
} |
||||||
|
else if (value instanceof byte[]) |
||||||
|
{ |
||||||
|
writeBytes((byte[]) value); |
||||||
|
} |
||||||
|
else if (value instanceof Boolean) |
||||||
|
{ |
||||||
|
writeBoolean((Boolean) value); |
||||||
|
|
||||||
|
} |
||||||
|
else if (value instanceof Character) |
||||||
|
{ |
||||||
|
writeChar((Character) value); |
||||||
|
|
||||||
|
} |
||||||
|
else if (value instanceof Number) |
||||||
|
{ |
||||||
|
writeNumber((Number) value); |
||||||
|
|
||||||
|
} |
||||||
|
else if (value instanceof String) |
||||||
|
{ |
||||||
|
writeString((String) value); |
||||||
|
|
||||||
|
} |
||||||
|
else if (value instanceof Collection<?>) |
||||||
|
{ |
||||||
|
writeCollection((Collection<?>) value); |
||||||
|
|
||||||
|
} |
||||||
|
else if (value instanceof Map<?, ?>) |
||||||
|
{ |
||||||
|
writeMap((Map<?, ?>) value); |
||||||
|
|
||||||
|
} |
||||||
|
else if (value instanceof Enum<?>) |
||||||
|
{ |
||||||
|
writeEnum((Enum<?>) value); |
||||||
|
|
||||||
|
} |
||||||
|
else if (value.getClass().isArray()) |
||||||
|
{ |
||||||
|
writeArray(value); |
||||||
|
|
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
writeCustom(value); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Writes a null value |
||||||
|
*/ |
||||||
|
public void writeNull() throws IOException |
||||||
|
{ |
||||||
|
write(TypeCode.NULL); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Overwrite this method to write custom objects. The default implementation |
||||||
|
* throws an {@link IOException}. |
||||||
|
*/ |
||||||
|
protected void writeCustom(Object value) throws IOException |
||||||
|
{ |
||||||
|
throw new IOException("Cannot encode " + value); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Writes the given byte-Array |
||||||
|
*/ |
||||||
|
public void writeBytes(byte[] value) throws IOException |
||||||
|
{ |
||||||
|
writeBytes(value, 0, value.length); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Writes the given byte-Array |
||||||
|
*/ |
||||||
|
public void writeBytes(byte[] value, int offset, int length) throws IOException |
||||||
|
{ |
||||||
|
write(value, offset, length); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Writes a boolean |
||||||
|
*/ |
||||||
|
public void writeBoolean(boolean value) throws IOException |
||||||
|
{ |
||||||
|
write(value ? TypeCode.TRUE : TypeCode.FALSE); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Writes a char |
||||||
|
*/ |
||||||
|
public void writeChar(int value) throws IOException |
||||||
|
{ |
||||||
|
writeByte(value); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Writes a byte |
||||||
|
*/ |
||||||
|
public void writeByte(int value) throws IOException |
||||||
|
{ |
||||||
|
write(TypeCode.BYTE); |
||||||
|
write(value); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Writes a short |
||||||
|
*/ |
||||||
|
public void writeShort(int value) throws IOException |
||||||
|
{ |
||||||
|
write(TypeCode.SHORT); |
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(Utils.SHORT_BYTES).putShort((short) value); |
||||||
|
write(buffer.array()); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Writes an int |
||||||
|
*/ |
||||||
|
public void writeInt(int value) throws IOException |
||||||
|
{ |
||||||
|
write(TypeCode.INT); |
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(Utils.INTEGER_BYTES).putInt(value); |
||||||
|
write(buffer.array()); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Writes a long |
||||||
|
*/ |
||||||
|
public void writeLong(long value) throws IOException |
||||||
|
{ |
||||||
|
write(TypeCode.LONG); |
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(Utils.LONG_BYTES).putLong(value); |
||||||
|
write(buffer.array()); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Writes a float |
||||||
|
*/ |
||||||
|
public void writeFloat(float value) throws IOException |
||||||
|
{ |
||||||
|
write(TypeCode.FLOAT); |
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(Utils.FLOAT_BYTES).putFloat(value); |
||||||
|
write(buffer.array()); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Writes a double |
||||||
|
*/ |
||||||
|
public void writeDouble(double value) throws IOException |
||||||
|
{ |
||||||
|
write(TypeCode.DOUBLE); |
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(Utils.DOUBLE_BYTES).putDouble(value); |
||||||
|
write(buffer.array()); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Writes a {@link Number} |
||||||
|
*/ |
||||||
|
public void writeNumber(Number num) throws IOException |
||||||
|
{ |
||||||
|
if (num instanceof Float) |
||||||
|
{ |
||||||
|
writeFloat(num.floatValue()); |
||||||
|
} |
||||||
|
else if (num instanceof Double) |
||||||
|
{ |
||||||
|
writeDouble(num.doubleValue()); |
||||||
|
} |
||||||
|
if (0 <= num.intValue() && num.intValue() < TypeCode.EMBEDDED.INT_POS_COUNT) |
||||||
|
{ |
||||||
|
write(TypeCode.EMBEDDED.INT_POS_START + num.intValue()); |
||||||
|
} |
||||||
|
else if (-TypeCode.EMBEDDED.INT_NEG_COUNT <= num.intValue() && num.intValue() < 0) |
||||||
|
{ |
||||||
|
write(TypeCode.EMBEDDED.INT_NEG_START - 1 - num.intValue()); |
||||||
|
} |
||||||
|
else if (Byte.MIN_VALUE <= num.intValue() && num.intValue() < Byte.MAX_VALUE) |
||||||
|
{ |
||||||
|
writeByte(num.byteValue()); |
||||||
|
} |
||||||
|
else if (Short.MIN_VALUE <= num.intValue() && num.intValue() < Short.MAX_VALUE) |
||||||
|
{ |
||||||
|
writeShort(num.shortValue()); |
||||||
|
} |
||||||
|
else if (Integer.MIN_VALUE <= num.longValue() && num.longValue() < Integer.MAX_VALUE) |
||||||
|
{ |
||||||
|
writeInt(num.intValue()); |
||||||
|
} |
||||||
|
else if (Long.MIN_VALUE <= num.longValue() && num.longValue() < Long.MAX_VALUE) |
||||||
|
{ |
||||||
|
writeLong(num.longValue()); |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
String number = num.toString(); |
||||||
|
write(TypeCode.NUMBER); |
||||||
|
write(number.getBytes(charset)); |
||||||
|
write(TypeCode.END); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Writes a {@link String} |
||||||
|
*/ |
||||||
|
public void writeString(String value) throws IOException |
||||||
|
{ |
||||||
|
int len = value.length(); |
||||||
|
if (len < TypeCode.EMBEDDED.STR_COUNT) |
||||||
|
{ |
||||||
|
write(TypeCode.EMBEDDED.STR_START + len); |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
String lenString = Integer.toString(len); |
||||||
|
writeBytes(lenString.getBytes(charset)); |
||||||
|
write(TypeCode.LENGTH_DELIM); |
||||||
|
} |
||||||
|
|
||||||
|
writeBytes(value.getBytes(charset)); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Writes a {@link Collection}. |
||||||
|
*/ |
||||||
|
public void writeCollection(Collection<?> value) throws IOException |
||||||
|
{ |
||||||
|
boolean useEndToken = value.size() >= TypeCode.EMBEDDED.LIST_COUNT; |
||||||
|
if (useEndToken) |
||||||
|
{ |
||||||
|
write(TypeCode.LIST); |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
write(TypeCode.EMBEDDED.LIST_START + value.size()); |
||||||
|
} |
||||||
|
|
||||||
|
for (Object element : value) |
||||||
|
{ |
||||||
|
writeObject(element); |
||||||
|
} |
||||||
|
|
||||||
|
if (useEndToken) |
||||||
|
{ |
||||||
|
write(TypeCode.END); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Writes a {@link Map}. |
||||||
|
*/ |
||||||
|
public void writeMap(Map<?, ?> map) throws IOException |
||||||
|
{ |
||||||
|
if (!(map instanceof SortedMap<?, ?>)) |
||||||
|
{ |
||||||
|
map = new TreeMap<Object, Object>(map); |
||||||
|
} |
||||||
|
|
||||||
|
boolean untilEnd = map.size() >= TypeCode.EMBEDDED.DICT_COUNT; |
||||||
|
|
||||||
|
if (untilEnd) |
||||||
|
{ |
||||||
|
write(TypeCode.DICTIONARY); |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
write(TypeCode.EMBEDDED.DICT_START + map.size()); |
||||||
|
} |
||||||
|
|
||||||
|
for (Map.Entry<?, ?> entry : map.entrySet()) |
||||||
|
{ |
||||||
|
writeObject(entry.getKey()); |
||||||
|
writeObject(entry.getValue()); |
||||||
|
} |
||||||
|
|
||||||
|
if (untilEnd) |
||||||
|
{ |
||||||
|
write(TypeCode.END); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Writes an {@link Enum}. |
||||||
|
*/ |
||||||
|
public void writeEnum(Enum<?> value) throws IOException |
||||||
|
{ |
||||||
|
writeString(value.name()); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Writes an array |
||||||
|
*/ |
||||||
|
public void writeArray(Object value) throws IOException |
||||||
|
{ |
||||||
|
int length = Array.getLength(value); |
||||||
|
boolean useEndToken = length >= TypeCode.EMBEDDED.LIST_COUNT; |
||||||
|
if (useEndToken) |
||||||
|
{ |
||||||
|
write(TypeCode.LIST); |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
write(TypeCode.EMBEDDED.LIST_START + length); |
||||||
|
} |
||||||
|
|
||||||
|
for (int i = 0; i < length; i++) |
||||||
|
{ |
||||||
|
writeObject(Array.get(value, i)); |
||||||
|
} |
||||||
|
|
||||||
|
if (useEndToken) |
||||||
|
{ |
||||||
|
write(TypeCode.END); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Writes the given {@link String} |
||||||
|
*/ |
||||||
|
public void writeBytes(String value) throws IOException |
||||||
|
{ |
||||||
|
writeString(value); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Writes the given {@link String} |
||||||
|
*/ |
||||||
|
public void writeChars(String value) throws IOException |
||||||
|
{ |
||||||
|
writeString(value); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Writes an UTF encoded {@link String} |
||||||
|
*/ |
||||||
|
public void writeUTF(String value) throws IOException |
||||||
|
{ |
||||||
|
writeBytes(value.getBytes(Utils.UTF_8)); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,47 @@ |
|||||||
|
package se.dimovski.rencode; |
||||||
|
|
||||||
|
public class TypeCode |
||||||
|
{ |
||||||
|
// The bencode 'typecodes' such as i, d, etc have been
|
||||||
|
// extended and relocated on the base-256 character set.
|
||||||
|
public static final char LIST = 59; |
||||||
|
public static final char DICTIONARY = 60; |
||||||
|
public static final char NUMBER = 61; |
||||||
|
public static final char BYTE = 62; |
||||||
|
public static final char SHORT = 63; |
||||||
|
public static final char INT = 64; |
||||||
|
public static final char LONG = 65; |
||||||
|
public static final char FLOAT = 66; |
||||||
|
public static final char DOUBLE = 44; |
||||||
|
public static final char TRUE = 67; |
||||||
|
public static final char FALSE = 68; |
||||||
|
public static final char NULL = 69; |
||||||
|
public static final char END = 127; |
||||||
|
public static final char LENGTH_DELIM = ':'; |
||||||
|
|
||||||
|
/* |
||||||
|
* TypeCodes with embedded values/lengths |
||||||
|
*/ |
||||||
|
public static class EMBEDDED |
||||||
|
{ |
||||||
|
// Positive integers
|
||||||
|
public static final int INT_POS_START = 0; |
||||||
|
public static final int INT_POS_COUNT = 44; |
||||||
|
|
||||||
|
// Negative integers
|
||||||
|
public static final int INT_NEG_START = 70; |
||||||
|
public static final int INT_NEG_COUNT = 32; |
||||||
|
|
||||||
|
// Dictionaries
|
||||||
|
public static final int DICT_START = 102; |
||||||
|
public static final int DICT_COUNT = 25; |
||||||
|
|
||||||
|
// Strings
|
||||||
|
public static final int STR_START = 128; |
||||||
|
public static final int STR_COUNT = 64; |
||||||
|
|
||||||
|
// Lists
|
||||||
|
public static final int LIST_START = STR_START + STR_COUNT; |
||||||
|
public static final int LIST_COUNT = 64; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,74 @@ |
|||||||
|
package se.dimovski.rencode; |
||||||
|
|
||||||
|
public class Utils |
||||||
|
{ |
||||||
|
// Character Encodings
|
||||||
|
public final static String UTF_8 = "UTF-8"; |
||||||
|
public final static String ISO_8859 = "ISO-8859-1"; |
||||||
|
|
||||||
|
// Byte-lengths for types
|
||||||
|
public static final int SHORT_BYTES = Short.SIZE / Byte.SIZE; |
||||||
|
public static final int INTEGER_BYTES = Integer.SIZE / Byte.SIZE; |
||||||
|
public static final int LONG_BYTES = Long.SIZE / Byte.SIZE; |
||||||
|
public static final int FLOAT_BYTES = Float.SIZE / Byte.SIZE; |
||||||
|
public static final int DOUBLE_BYTES = Double.SIZE / Byte.SIZE; |
||||||
|
|
||||||
|
// Maximum length of integer when written as base 10 string.
|
||||||
|
public static final int MAX_INT_LENGTH = 64; |
||||||
|
|
||||||
|
private static boolean tokenInRange(int token, int start, int count) |
||||||
|
{ |
||||||
|
return start <= token && token < (start + count); |
||||||
|
} |
||||||
|
|
||||||
|
public static boolean isNumber(int token) |
||||||
|
{ |
||||||
|
switch (token) |
||||||
|
{ |
||||||
|
case TypeCode.NUMBER: |
||||||
|
case TypeCode.BYTE: |
||||||
|
case TypeCode.SHORT: |
||||||
|
case TypeCode.INT: |
||||||
|
case TypeCode.LONG: |
||||||
|
case TypeCode.FLOAT: |
||||||
|
case TypeCode.DOUBLE: |
||||||
|
return true; |
||||||
|
} |
||||||
|
return isFixedNumber(token); |
||||||
|
} |
||||||
|
|
||||||
|
public static boolean isFixedNumber(int token) |
||||||
|
{ |
||||||
|
return isPositiveFixedNumber(token) || isNegativeFixedNumber(token); |
||||||
|
} |
||||||
|
|
||||||
|
public static boolean isPositiveFixedNumber(int token) |
||||||
|
{ |
||||||
|
return tokenInRange(token, TypeCode.EMBEDDED.INT_POS_START, TypeCode.EMBEDDED.INT_POS_COUNT); |
||||||
|
} |
||||||
|
|
||||||
|
public static boolean isNegativeFixedNumber(int token) |
||||||
|
{ |
||||||
|
return tokenInRange(token, TypeCode.EMBEDDED.INT_NEG_START, TypeCode.EMBEDDED.INT_NEG_COUNT); |
||||||
|
} |
||||||
|
|
||||||
|
public static boolean isFixedList(int token) |
||||||
|
{ |
||||||
|
return tokenInRange(token, TypeCode.EMBEDDED.LIST_START, TypeCode.EMBEDDED.LIST_COUNT); |
||||||
|
} |
||||||
|
|
||||||
|
public static boolean isFixedDictionary(int token) |
||||||
|
{ |
||||||
|
return tokenInRange(token, TypeCode.EMBEDDED.DICT_START, TypeCode.EMBEDDED.DICT_COUNT); |
||||||
|
} |
||||||
|
|
||||||
|
public static boolean isFixedString(int token) |
||||||
|
{ |
||||||
|
return tokenInRange(token, TypeCode.EMBEDDED.STR_START, TypeCode.EMBEDDED.STR_COUNT); |
||||||
|
} |
||||||
|
|
||||||
|
public static boolean isDigit(int token) |
||||||
|
{ |
||||||
|
return '0' <= token && token <= '9'; |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue