package be.nikiroo.fanfix.library;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import javax.net.ssl.SSLException;
import be.nikiroo.fanfix.Instance;
import be.nikiroo.fanfix.data.MetaData;
import be.nikiroo.fanfix.data.Story;
import be.nikiroo.utils.Image;
import be.nikiroo.utils.Progress;
import be.nikiroo.utils.Version;
import be.nikiroo.utils.serial.server.ConnectActionClientObject;
/**
* This {@link BasicLibrary} will access a remote server to list the available
* stories, and download the ones you try to load to the local directory
* specified in the configuration.
*
* This remote library uses a custom fanfix:// protocol.
*
* @author niki
*/
public class RemoteLibrary extends BasicLibrary {
interface RemoteAction {
public void action(ConnectActionClientObject action) throws Exception;
}
class RemoteConnectAction extends ConnectActionClientObject {
public RemoteConnectAction() throws IOException {
super(host, port, key);
}
@Override
public Object send(Object data)
throws IOException, NoSuchFieldException, NoSuchMethodException,
ClassNotFoundException {
Object rep = super.send(data);
if (rep instanceof RemoteLibraryException) {
RemoteLibraryException remoteEx = (RemoteLibraryException) rep;
throw remoteEx.unwrapException();
}
return rep;
}
}
private String host;
private int port;
private final String key;
private final String subkey;
// informative only (server will make the actual checks)
private boolean rw;
/**
* Create a {@link RemoteLibrary} linked to the given server.
*
* Note that the key is structured:
* xxx(|yyy|wl)(|rw)
*
* Note that anything before the first pipe (|) character is
* considered to be the encryption key, anything after that character is
* called the subkey (including the other pipe characters and flags!).
*
* This is important because the subkey (including the pipe characters and
* flags) must be present as-is in the server configuration file to be
* allowed.
*
*
xxx: the encryption key used to communicate with the
* server
*
yyy: the secondary key
*
rw: flag to allow read and write access if it is not the
* default on this server
*
wl: flag to allow access to all the stories (bypassing the
* whitelist if it exists)
*
*
* Some examples:
*
*
my_key: normal connection, will take the default server
* options
*
my_key|agzyzz|wl: will ask to bypass the white list (if it
* exists)
*
my_key|agzyzz|rw: will ask read-write access (if the default
* is read-only)
*
my_key|agzyzz|wl|rw: will ask both read-write access and white
* list bypass
*
*
* @param key
* the key that will allow us to exchange information with the
* server
* @param host
* the host to contact or NULL for localhost
* @param port
* the port to contact it on
*/
public RemoteLibrary(String key, String host, int port) {
int index = -1;
if (key != null) {
index = key.indexOf('|');
}
if (index >= 0) {
this.key = key.substring(0, index);
this.subkey = key.substring(index + 1);
} else {
this.key = key;
this.subkey = "";
}
if (host.startsWith("fanfix://")) {
host = host.substring("fanfix://".length());
}
this.host = host;
this.port = port;
}
@Override
public String getLibraryName() {
return (rw ? "[READ-ONLY] " : "") + "fanfix://" + host + ":" + port;
}
@Override
public Status getStatus() {
Instance.getInstance().getTraceHandler()
.trace("Getting remote lib status...");
Status status = getStatusDo();
Instance.getInstance().getTraceHandler()
.trace("Remote lib status: " + status);
return status;
}
private Status getStatusDo() {
final Status[] result = new Status[1];
result[0] = Status.INVALID;
try {
new RemoteConnectAction() {
@Override
public void action(Version serverVersion) throws Exception {
Object rep = send(new Object[] { subkey, "PING" });
if ("r/w".equals(rep)) {
rw = true;
result[0] = Status.READ_WRITE;
} else if ("r/o".equals(rep)) {
rw = false;
result[0] = Status.READ_ONLY;
} else {
result[0] = Status.UNAUTHORIZED;
}
}
@Override
protected void onError(Exception e) {
if (e instanceof SSLException) {
result[0] = Status.UNAUTHORIZED;
} else {
result[0] = Status.UNAVAILABLE;
}
}
}.connect();
} catch (UnknownHostException e) {
result[0] = Status.INVALID;
} catch (IllegalArgumentException e) {
result[0] = Status.INVALID;
} catch (Exception e) {
result[0] = Status.UNAVAILABLE;
}
return result[0];
}
@Override
public Image getCover(final String luid) throws IOException {
final Image[] result = new Image[1];
connectRemoteAction(new RemoteAction() {
@Override
public void action(ConnectActionClientObject action)
throws Exception {
Object rep = action
.send(new Object[] { subkey, "GET_COVER", luid });
result[0] = (Image) rep;
}
});
return result[0];
}
@Override
public Image getCustomSourceCover(final String source) throws IOException {
return getCustomCover(source, "SOURCE");
}
@Override
public Image getCustomAuthorCover(final String author) throws IOException {
return getCustomCover(author, "AUTHOR");
}
// type: "SOURCE" or "AUTHOR"
private Image getCustomCover(final String source, final String type)
throws IOException {
final Image[] result = new Image[1];
connectRemoteAction(new RemoteAction() {
@Override
public void action(ConnectActionClientObject action)
throws Exception {
Object rep = action.send(new Object[] { subkey,
"GET_CUSTOM_COVER", type, source });
result[0] = (Image) rep;
}
});
return result[0];
}
@Override
public synchronized Story getStory(final String luid, Progress pg)
throws IOException {
final Progress pgF = pg;
final Story[] result = new Story[1];
connectRemoteAction(new RemoteAction() {
@Override
public void action(ConnectActionClientObject action)
throws Exception {
Progress pg = pgF;
if (pg == null) {
pg = new Progress();
}
Object rep = action
.send(new Object[] { subkey, "GET_STORY", luid });
MetaData meta = null;
if (rep instanceof MetaData) {
meta = (MetaData) rep;
if (meta.getWords() <= Integer.MAX_VALUE) {
pg.setMinMax(0, (int) meta.getWords());
}
}
List