package recommender.impl.webservice;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.util.Collection;
import java.util.SortedSet;
import java.util.TreeSet;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.params.HttpConnectionManagerParams;
import org.apache.commons.httpclient.util.IdleConnectionTimeoutThread;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.DisposableBean;
import recommender.core.interfaces.RecommenderConnector;
import recommender.core.interfaces.model.RecommendationResult;
import recommender.core.interfaces.renderer.RecommendationRenderer;
import recommender.core.util.RecommendationResultComparator;
import recommender.impl.database.IdleClosingConnectionManager;
/**
* Class for encapsulating webservice queries to recommenders
*
* @author fei
*
* @param <E> the recommendation entity
* @param <R> the recommendation
*/
public class WebserviceRecommender<E, R extends RecommendationResult> implements RecommenderConnector<E, R>, DisposableBean {
private static final Log log = LogFactory.getLog(WebserviceRecommender.class);
private static final int SOCKET_TIMEOUT_MS = 10000;
private static final int HTTP_CONNECTION_TIMEOUT_MS = 1000;
private static final long IDLE_TIMEOUT_MS = 3000;
private boolean trusted;
private final HttpClient client;
// service's address
private URL address;
// serializes entity
private RecommendationRenderer<E, R> renderer;
// ConnectionManager
private final IdleClosingConnectionManager connectionManager;
private final IdleConnectionTimeoutThread idleConnectionHandler;
/**
* default constructor
*/
public WebserviceRecommender() {
// Create an instance of HttpClient.
connectionManager = new IdleClosingConnectionManager();
client = new HttpClient(connectionManager);
// set default timeouts
final HttpConnectionManagerParams connectionParams = connectionManager.getParams();
connectionParams.setSoTimeout(SOCKET_TIMEOUT_MS);
connectionParams.setConnectionTimeout(HTTP_CONNECTION_TIMEOUT_MS);
connectionManager.setParams(connectionParams);
log.debug("MAXCONNECTIONS: "+connectionParams.getMaxTotalConnections());
log.debug("MAXCONNECTIONSPERHOST: "+connectionParams.getDefaultMaxConnectionsPerHost());
// handle idle connections
connectionManager.closeIdleConnections(IDLE_TIMEOUT_MS);
idleConnectionHandler = new IdleConnectionTimeoutThread();
idleConnectionHandler.addConnectionManager(connectionManager);
idleConnectionHandler.start();
}
/**
* Constructor
* @param renderer
* @param address
*/
public WebserviceRecommender(final RecommendationRenderer<E, R> renderer, final URL address) {
this(renderer);
this.setAddress(address);
}
/**
* inits the recommender
* @param renderer
*/
public WebserviceRecommender(final RecommendationRenderer<E, R> renderer) {
this();
this.renderer = renderer;
}
/**
* @return the address
*/
public URL getAddress() {
return this.address;
}
/**
* @param address the address to set
*/
public void setAddress(URL address) {
this.address = address;
}
/**
* @param renderer the renderer to serialize recommendation entities and recommendation results
*/
@Override
public void setRecommendationRenderer(RecommendationRenderer<E, R> renderer) {
this.renderer = renderer;
}
@Override
public void addRecommendation(final Collection<R> recommendationResults, final E entity) {
// render entity
// FIXME: choose buffer size
final StringWriter sw = new StringWriter(100);
renderEntity(entity, sw);
// Create a method instance.
final NameValuePair[] data = { new NameValuePair(ID_RECQUERY, sw.toString()) };
// Create a method instance and send request
final PostMethod cnct = new PostMethod(getAddress().toString() + "/" + METHOD_GETRECOMMENDEDTAGS);
cnct.setRequestBody(data);
final InputStreamReader input = sendRequest(cnct);
// Deal with the response.
SortedSet<R> result = null;
if (input != null) {
try {
result = renderer.parseRecommendationResultList(input);
} catch (final Exception e) {
log.error("Error parsing recommender response (" + getAddress().toString() + ").", e);
result = null;
} finally {
try {
input.close();
} catch (IOException e) {
log.error("error while closing input stream", e);
}
}
}
if (result != null) {
recommendationResults.addAll(result);
}
cnct.releaseConnection();
}
@Override
public SortedSet<R> getRecommendation(final E entity) {
final SortedSet<R> retVal = new TreeSet<R>(new RecommendationResultComparator<R>());
addRecommendation(retVal, entity);
return retVal;
}
@Override
public void setFeedback(final E entity, final R result) {
// render entity
final StringWriter sw = new StringWriter(100);
renderEntity(entity, sw);
// create a method instance.
final NameValuePair[] data = { new NameValuePair(ID_FEEDBACK, sw.toString()) };
// send request
final PostMethod cnct = new PostMethod(getAddress().toString() + "/" + METHOD_SETFEEDBACK);
cnct.setRequestBody(data);
final InputStreamReader input = sendRequest(cnct);
// Deal with the response.
if (input != null) {
// TODO: check for response code before parsing the state
final String status = renderer.parseStat(input);
log.info("Feedback status: " + status);
try {
input.close();
} catch (IOException e) {
log.error("error while closing connection ", e);
}
}
cnct.releaseConnection();
}
@Override
public String getInfo() {
return "Webservice";
}
@Override
public String getId() {
return getAddress().toString();
}
@Override
public void setNumberOfResultsToRecommend(int numberOfResultsToRecommend) {
// nothing to do
}
/* (non-Javadoc)
* @see recommender.core.interfaces.RecommenderConnector#getRecommendationRenderer()
*/
@Override
public RecommendationRenderer<E, R> getRecommendationRenderer() {
return this.renderer;
}
private void renderEntity(final E entity, final StringWriter sw) {
renderer.serializeRecommendationEntity(sw, entity);
}
private InputStreamReader sendRequest(final PostMethod cnct) {
InputStreamReader input = null;
try {
// Execute the method.
final int statusCode = client.executeMethod(cnct);
if (statusCode != HttpStatus.SC_OK) {
log.error("Method at " + getAddress().toString() + " failed: " + cnct.getStatusLine());
} else {
// Read the response body.
// responseBody = cnct.getResponseBody();
input = new InputStreamReader(cnct.getResponseBodyAsStream(), "UTF-8");
}
} catch (final HttpException e) {
log.fatal("Fatal protocol violation("+getAddress()+"): " + e.getMessage(), e);
} catch (final UnsupportedEncodingException ex) {
// returns InputStream with default encoding if a exception
// is thrown with utf-8 support
log.fatal("Encoding error("+getAddress()+"): " + ex.getMessage(), ex);
} catch (final IOException e) {
log.fatal("Fatal transport error("+getAddress()+"): " + e.getMessage(), e);
} catch (final Exception e) {
log.fatal("Unknown error ("+getAddress()+")", e);
}
// all done.
return input;
}
@Override
public void destroy() throws Exception {
// needed to prevent a failing spring-context to start more and more threads
if (idleConnectionHandler != null) {
idleConnectionHandler.shutdown();
}
}
/**
* @return the trusted
*/
@Override
public boolean isTrusted() {
return this.trusted;
}
/**
* @param trusted the trusted to set
*/
public void setTrusted(boolean trusted) {
this.trusted = trusted;
}
}