package com.robonobo.midas.client;
import static com.robonobo.common.util.TextUtil.*;
import static java.lang.Math.*;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.*;
import org.apache.http.auth.*;
import org.apache.http.client.methods.*;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import com.google.protobuf.AbstractMessage;
import com.google.protobuf.GeneratedMessage;
import com.robonobo.common.concurrent.CatchingRunnable;
import com.robonobo.common.exceptions.SeekInnerCalmException;
import com.robonobo.common.http.PreemptiveHttpClient;
import com.robonobo.common.serialization.*;
import com.robonobo.core.api.model.*;
import com.robonobo.core.metadata.*;
public class MidasClientService extends AbstractMetadataService {
static final Pattern URL_PATTERN = Pattern.compile("^http://([\\w\\.]+?):?(\\d*)/.*$");
LinkedList<Request> requests = new LinkedList<Request>();
PreemptiveHttpClient http;
Log log = LogFactory.getLog(getClass());
private MidasClientConfig cfg;
AuthScope midasAuthScope;
int numThreads;
int runningTasks = 0;
ExecutorService executor;
long nextFetchTaskId = 1;
Map<Long, FetchTask> fetchTasks = new HashMap<Long, MidasClientService.FetchTask>();
boolean stopped = false;
public MidasClientService() {
addHardDependency("core.http");
}
@Override
public String getName() {
return "Midas Metadata Service";
}
@Override
public void startup() throws Exception {
String baseUrl = rbnb.getConfig().getMidasUrl();
Matcher m = URL_PATTERN.matcher(baseUrl);
if (!m.matches())
throw new SeekInnerCalmException("midas url " + baseUrl + "does not match expected pattern");
cfg = new MidasClientConfig(baseUrl);
String midasHost = m.group(1);
String portStr = m.group(2);
int midasPort;
// Note that the httpclient preemptive auth breaks if we set this to 80, instead we have to use -1 :-P :-P :-P
if (isEmpty(portStr) || portStr.equals("80"))
midasPort = -1;
else
midasPort = Integer.parseInt(portStr);
midasAuthScope = new AuthScope(midasHost, midasPort);
// Run midas requests in a different thread pool
numThreads = rbnb.getConfig().getMidasThreadPoolSize();
executor = Executors.newFixedThreadPool(numThreads);
http = rbnb.getHttpService().getClient();
// Initially we handle requests serially - makes for a better ux to have the playlists load one at a time rather
// than a big pause then all at once
fetchOrder = RequestFetchOrder.Serial;
}
@Override
public void shutdown() throws Exception {
stopped = true;
executor.shutdownNow();
}
@Override
public void setCredentials(String username, String password) {
// Set these details on our http client
http.getCredentialsProvider().setCredentials(midasAuthScope, new UsernamePasswordCredentials(username, password));
// Tell our fetchers to refresh their httpcontexts (which contain cached creds)
synchronized (this) {
for (FetchTask ft : fetchTasks.values()) {
ft.refreshContext = true;
}
}
}
@Override
public void fetchStreams(Collection<String> sids, StreamCallback callback) {
addRequest(new GetStreamRequest(cfg, sids, callback));
}
@Override
public void putStream(Stream s, StreamCallback callback) {
addRequest(new PutStreamRequest(cfg, s, callback));
}
@Override
public void fetchUser(long userId, UserCallback callback) {
addRequest(new GetUsersRequest(cfg, userId, callback));
}
@Override
public void fetchUsers(Collection<Long> userIds, UserCallback callback) {
addRequest(new GetUsersRequest(cfg, userIds, callback));
}
@Override
public void fetchUserForLogin(String email, String password, UserCallback callback) {
addRequest(new LoginRequest(cfg, email, password, callback));
}
@Override
public void fetchUserConfig(long userId, UserConfigCallback callback) {
addRequest(new GetUserConfigRequest(cfg, userId, callback));
}
@Override
public void updateUserConfig(UserConfig uc, UserConfigCallback callback) {
addRequest(new PutUserConfigRequest(cfg, uc, callback));
}
@Override
public void fetchPlaylist(long playlistId, PlaylistCallback callback) {
addRequest(new GetPlaylistRequest(cfg, playlistId, callback));
}
@Override
public void fetchPlaylists(Collection<Long> playlistIds, PlaylistCallback callback) {
addRequest(new GetPlaylistRequest(cfg, playlistIds, callback));
}
@Override
public void updatePlaylist(Playlist p, PlaylistCallback callback) {
addRequest(new PutPlaylistRequest(cfg, p, callback));
}
@Override
public void postPlaylistUpdateToService(String service, long playlistId, String msg, PlaylistCallback callback) {
addRequest(new PlaylistServiceUpdateRequest(cfg, service, playlistId, msg, callback));
}
@Override
public void deletePlaylist(Playlist p, PlaylistCallback callback) {
addRequest(new DeletePlaylistRequest(cfg, p.getPlaylistId(), callback));
}
@Override
public void sharePlaylist(Playlist p, Collection<Long> shareFriendIds, Collection<String> friendEmails, PlaylistCallback callback) {
addRequest(new SharePlaylistRequest(cfg, p.getPlaylistId(), shareFriendIds, friendEmails, callback));
}
@Override
public void newComment(Comment c, CommentCallback callback) {
addRequest(new NewCommentRequest(cfg, c, callback));
}
@Override
public void getAllComments(String itemType, long itemId, Date since, AllCommentsCallback callback) {
addRequest(new GetAllCommentsRequest(cfg, itemType, itemId, since, callback));
}
@Override
public void deleteComment(long commentId, CommentCallback callback) {
addRequest(new DeleteCommentRequest(cfg, commentId, callback));
}
@Override
public void addFriends(Collection<String> friendEmails, GeneralCallback callback) {
addRequest(new AddFriendsRequest(cfg, friendEmails, callback));
}
@Override
public void fetchLibrary(long userId, Date lastUpdated, LibraryCallback callback) {
addRequest(new GetLibraryRequest(cfg, userId, lastUpdated, callback));
}
@Override
public void addToLibrary(long userId, Library addedLib, LibraryCallback callback) {
addRequest(new AddToLibraryRequest(cfg, userId, addedLib, callback));
}
@Override
public void deleteFromLibrary(long userId, Library delLib, LibraryCallback callback) {
addRequest(new DeleteFromLibraryRequest(cfg, userId, delLib, callback));
}
@Override
public void search(String query, int firstResult, SearchCallback callback) {
addRequest(new SearchRequest(cfg, query, firstResult, callback));
}
private synchronized void addRequest(Request r) {
// Add to the front for best responsiveness
requests.addFirst(r);
int numToStart = min(r.remaining(), (numThreads - runningTasks));
runningTasks += numToStart;
for (int i = 0; i < numToStart; i++) {
executor.execute(new FetchTask());
}
}
private void getFromUrl(HttpContext context, AbstractMessage.Builder<?> bldr, String url, String un, String pwd) throws IOException, SerializationException {
log.debug("Getting object from " + url);
HttpGet get = new HttpGet(url);
Credentials restoreCreds = null;
// If we are being supplied with a username & password, store our old creds and restore them afterwards, and use
// a different httpcontext for this request to avoid using cached auth
if (un != null) {
restoreCreds = http.getCredentialsProvider().getCredentials(midasAuthScope);
http.getCredentialsProvider().setCredentials(midasAuthScope, new UsernamePasswordCredentials(un, pwd));
}
HttpEntity body = null;
try {
HttpResponse resp = http.execute(get, context);
body = resp.getEntity();
int statusCode = resp.getStatusLine().getStatusCode();
switch (statusCode) {
case 200:
if (bldr != null) {
InputStream is = body.getContent();
try {
bldr.mergeFrom(is);
} finally {
is.close();
}
}
return;
case 404:
throw new ResourceNotFoundException("Server could not find resource for " + url);
case 401:
throw new UnauthorizedException("Server did not allow us to access url " + url + " with supplied credentials");
case 500:
throw new IOException("Unable to get object from url '" + url + "', server said: " + EntityUtils.toString(body));
case 503:
throw new TemporarilyUnavailableException("Server temporarily unavailable at url '" + url + "'");
default:
throw new IOException("Url '" + url + "' replied with status " + statusCode);
}
} finally {
if (restoreCreds != null)
http.getCredentialsProvider().setCredentials(midasAuthScope, restoreCreds);
if (body != null)
EntityUtils.consume(body);
}
}
private void putToUrl(HttpContext context, GeneratedMessage msg, String url, AbstractMessage.Builder bldr) throws IOException {
log.debug("Putting object to " + url);
HttpPut put = new HttpPut(url);
put.setEntity(new ByteArrayEntity(msg.toByteArray()));
HttpEntity body = null;
try {
HttpResponse resp = http.execute(put, context);
body = resp.getEntity();
switch (resp.getStatusLine().getStatusCode()) {
case 200:
if (bldr != null) {
InputStream is = body.getContent();
try {
bldr.mergeFrom(is);
} finally {
is.close();
}
}
return;
default:
throw new IOException("Server replied with status " + resp.getStatusLine().getStatusCode() + ": " + EntityUtils.toString(body));
}
} finally {
if (body != null)
EntityUtils.consume(body);
}
}
private void deleteAtUrl(HttpContext context, String url) throws IOException {
log.debug("Deleting object at " + url);
HttpDelete del = new HttpDelete(url);
HttpEntity body = null;
try {
HttpResponse resp = http.execute(del, context);
body = resp.getEntity();
if (resp.getStatusLine().getStatusCode() != 200)
throw new IOException("Failed to delete object at " + url + ", status code was " + resp.getStatusLine().getStatusCode());
} finally {
if (body != null)
EntityUtils.consume(body);
}
}
class FetchTask extends CatchingRunnable {
long taskId;
HttpContext context;
boolean refreshContext = true;
public FetchTask() {
synchronized (MidasClientService.this) {
taskId = nextFetchTaskId++;
fetchTasks.put(taskId, this);
}
}
@Override
public String toString() {
return "Midas fetcher " + taskId;
}
@Override
public void doRun() throws Exception {
while (true) {
if (refreshContext) {
context = http.newPreemptiveContext(new HttpHost(midasAuthScope.getHost(), midasAuthScope.getPort()));
refreshContext = false;
}
Request r;
Params p;
synchronized (MidasClientService.this) {
if (requests.size() == 0) {
runningTasks--;
fetchTasks.remove(taskId);
return;
}
r = requests.removeFirst();
p = r.getNextParams();
if (p == null)
continue;
if (r.remaining() > 0) {
if (fetchOrder == RequestFetchOrder.Serial)
requests.addFirst(r);
else
requests.addLast(r); // Parallel
}
}
try {
switch (p.op) {
case Get:
// If we are passing different credentials, use a different httpcontext to avoid reusing the old
// ones
HttpContext c = context;
if (p.username != null)
c = http.newPreemptiveContext(new HttpHost(midasAuthScope.getHost(), midasAuthScope.getPort()));
getFromUrl(c, p.resultBldr, p.url, p.username, p.password);
break;
case Put:
putToUrl(context, p.sendMsg, p.url, p.resultBldr);
break;
case Delete:
deleteAtUrl(context, p.url);
break;
}
if (p.resultBldr == null)
r.success(null);
else
r.success(p.resultBldr.build());
} catch (Exception e) {
if (stopped)
return;
r.error(p, e);
}
}
}
}
}