package com.flexmls.flexmls_api; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.commons.codec.digest.DigestUtils; import org.apache.log4j.Logger; /** * Client class for communicating with the flexmls restful interface. Abstracts the HTTP, * authentication, and request signing layers to ease communication with the service. * * @param <U> The parameter type accepted by the client implementation. The value can be anything * so long as the <pre>stringifyParameterKeys()</pre> method converts it to a String for consumption. */ public abstract class BaseClient<U> implements HttpActions<Response, U>{ private static Logger logger = Logger.getLogger(BaseClient.class); private Configuration config = null; private Connection<Response> connection = null; private Connection<Response> secure = null; private Session session = null; /** * Configure the client with general settings, and connection implementations * @param config * @param defaultConnection HTTP connection * @param secureConnection SSL based HTTP connection (for authentication at least) */ public BaseClient(Configuration config, Connection<Response> defaultConnection, Connection<Response> secureConnection) { super(); this.config = config; this.secure = secureConnection; this.connection = defaultConnection; } /** * Configure the client with general settings, and default connection settings * @param config */ public BaseClient(Configuration config) { super(); this.config = config; this.secure = new ConnectionApacheHttps(config); this.connection = config.isSsl() ? secure : new ConnectionApacheHttp(config); } @Override public Response get(String path, Map<U, String> options) throws FlexmlsApiClientException { return new ReAuthable("GET", path, stringifyParameterKeys(options)) { @Override public Response run(String path, String body) throws FlexmlsApiClientException { return connection.get(path); } }.execute(); } @Override public Response post(String path, String body, Map<U, String> options) throws FlexmlsApiClientException { return new ReAuthable("POST", path, body, stringifyParameterKeys(options)) { @Override public Response run(String path, String body) throws FlexmlsApiClientException { return connection.post(path,body); } }.execute(); } @Override public Response put(String path, String body, Map<U, String> options) throws FlexmlsApiClientException { return new ReAuthable("PUT", path, body, stringifyParameterKeys(options)) { @Override public Response run(String path, String body) throws FlexmlsApiClientException { return connection.put(path,body); } }.execute(); } @Override public Response delete(String path, Map<U, String> options) throws FlexmlsApiClientException { return new ReAuthable("DELETE", path, stringifyParameterKeys(options)) { @Override public Response run(String path, String body) throws FlexmlsApiClientException { return connection.delete(path); } }.execute(); } abstract Map<String, String> stringifyParameterKeys(Map<U, String> parms); protected void log(String action, String path) { if(logger.isDebugEnabled()){ logger.debug("Request: [" + action + "] - " + path); } } protected void reauth() throws FlexmlsApiClientException { if(session == null || session.isExpired()){ authenticate(); } } Session authenticate() throws FlexmlsApiClientException { StringBuffer b = new StringBuffer(config.getApiSecret()); b.append("ApiKey").append(config.getApiKey()); String signature = sign(b.toString()); String path = authPath(signature); log("AUTH-POST", path); Response response = secure.post(path,""); List<Session> sessions = response.getResults(Session.class); if(sessions.isEmpty()){ throw new FlexmlsApiClientException("Service error. No session returned for service authentication."); } Session s = sessions.get(0); setSession(s); return s; } private String authPath(String sig) { StringBuffer b = new StringBuffer(); b.append("/").append(config.getVersion()).append("/session?"); b.append("ApiKey=").append(config.getApiKey()); b.append("&ApiSig=").append(sig); return b.toString(); } protected Map<String,String> sessionParams() { Map<String, String> params = new HashMap<String, String>(); if(config.getApiUser() != null){ params.put("ApiUser", config.getApiUser()); } params.put("AuthToken", session.getToken()); return params; } protected String requestPath(String path, String signature, Map<String, String> params) { StringBuffer b = new StringBuffer(); b.append("/").append(config.getVersion()).append(path).append("?"); b.append("ApiSig").append("=").append(signature); for (String key : params.keySet()) { b.append("&").append(key).append("=").append(encode(params.get(key))); } return b.toString(); } protected String encode(String s){ try { return URLEncoder.encode(s, "UTF-8"); } catch (UnsupportedEncodingException e) { // Unlikely to happen, but notify at least and resume without encoding. logger.error("Unable to encode url parameters as utf-8.", e); } return s; } protected String sign(String s) { return DigestUtils.md5Hex(s); } protected String signToken(String path, Map<String, String> options, String body) { StringBuffer b = new StringBuffer(config.getApiSecret()); b.append("ApiKey").append(config.getApiKey()); b.append("ServicePath/").append(config.getVersion()).append(path); b.append(buildParamString(options)); b.append(body); return sign(b.toString()); } protected String buildParamString(Map<String, String> params) { List<String> list = new ArrayList<String>(params.keySet()); Collections.sort(list); StringBuffer buffer = new StringBuffer(); for (String key : list) { buffer.append(key).append(encode(params.get(key))); } return buffer.toString(); } protected String setupRequest(String path, String body, Map<String, String> options) { Map<String, String> params = sessionParams(); params.putAll(options); String sig = signToken(path, params, body); return requestPath(path, sig, params); } public void getSession(Session session) { this.session = session; } protected void setSession(Session session) { this.session = session; } public Configuration getConfig() { return config; } protected void setConfig(Configuration config) { this.config = config; } abstract class ReAuthable { private static final int MAX_RETRIES = 1; private String command; private String path; private String body = ""; private Map<String, String> options; public ReAuthable(String command, String path, String body, Map<String, String> options) { super(); this.command = command; this.path = path; this.body = body; this.options = options; } public ReAuthable(String command, String path, Map<String, String> options) { this(command, path, "", options); } public Response execute() throws FlexmlsApiClientException { reauth(); String apiPath = setupRequest(path, body, options); log(command, apiPath); int retries = 0; while(retries <= MAX_RETRIES){ try { return run(apiPath, body); } catch (FlexmlsApiException e) { if(retries <= MAX_RETRIES && ApiCode.SESSION_EXPIRED.equals(e.getCode())){ authenticate(); } else { throw e; } } retries++; } throw new FlexmlsApiClientException("Session expired and maximum number of authentication attempts reached."); } protected abstract Response run(String path, String body) throws FlexmlsApiClientException; } }