package uk.ac.ebi.fg.myequivalents.webservices.client; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.xml.transform.TransformerFactoryConfigurationError; import org.apache.commons.lang.StringUtils; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; import org.apache.http.StatusLine; import org.apache.http.client.HttpClient; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpPost; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.message.BasicNameValuePair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import uk.ac.ebi.fg.myequivalents.access_control.model.User; import uk.ac.ebi.fg.myequivalents.exceptions.SecurityException; import uk.ac.ebi.fg.myequivalents.managers.interfaces.MyEquivalentsManager; import uk.ac.ebi.fg.myequivalents.utils.ManagerUtils; import uk.ac.ebi.utils.io.IOUtils; import com.sun.jersey.api.client.Client; import com.sun.jersey.api.client.ClientResponse; import com.sun.jersey.api.client.UniformInterfaceException; import com.sun.jersey.api.client.WebResource; import com.sun.jersey.api.representation.Form; /** * This is a base class to implement myEquivalents managers as web service manager clients. * All the stuff related to the web service are based on the Jersey library (and hence REST and JAX-RS). * * <dl><dt>date</dt><dd>30 Aug 2013</dd></dl> * @author Marco Brandizi * */ public abstract class MyEquivalentsWSClient implements MyEquivalentsManager { protected final String baseUrl; protected String email = null, apiPassword = null; protected final Logger log = LoggerFactory.getLogger ( this.getClass () ); /** * <p>All the invocations provided by this client will be routed at the web service base address passed here. The default * it 'https://localhost:8080/ws', /ws is usually the path where the web service package locates its implementation.</p> * * <p>We recommend to user HTTPS connections and POST requests, which hide passwords from the request URL</p> */ public MyEquivalentsWSClient ( String baseUrl ) { super (); baseUrl = StringUtils.trimToNull ( baseUrl ); if ( baseUrl == null ) baseUrl = "https://localhost:8080/myequivalents/ws"; else if ( baseUrl.charAt ( baseUrl.length () - 1 ) == '/' ) baseUrl = baseUrl.substring ( 0, baseUrl.length () - 2 ); this.baseUrl = baseUrl; } public MyEquivalentsWSClient () { this ( null ); } /** * Usually every manager is bound to a URL which starts with {@link #baseUrl} (i.e., the URL set by the constructor, * plus this path. */ protected abstract String getServicePath (); /** * @see #setAuthenticationCredentials(String, String, boolean). */ @Override public User setAuthenticationCredentials ( String email, String apiPassword ) throws SecurityException { return setAuthenticationCredentials ( email, apiPassword, false ); } /** * {@link #setAuthenticationCredentials(String, String)} doesn't verify the credentials immediately, but only * when a manager's operations are requested. If you wish, you can verify the user in advance, by using this method * with connectServer = true. * */ public User setAuthenticationCredentials ( String email, String apiPassword, boolean connectServer ) throws SecurityException { this.email = StringUtils.trimToNull ( email ); this.apiPassword = StringUtils.trimToNull ( apiPassword ); if ( !connectServer ) return null; Form req = prepareReq (); return invokeWsReq ( "/perms", "/login", req, User.class ); } /** * Prepares a request for the web service, by putting user credential parameters (which were set via manager * constructor, or {@link #setAuthenticationCredentials(String, String, boolean)}) into it. * * This is used by manager methods to initialise a web service request. */ protected Form prepareReq () { Form req = new Form (); if ( this.email != null ) req.add ( "login", this.email ); if ( this.apiPassword != null ) req.add ( "login-secret", this.apiPassword ); return req; } /** * Invokes a myEquivalents operation from the myEq web service. * * @param servicePath is added to {@link #baseUrl}, the web service base URL passed to the manager constructor * @param reqPath is added to {@link #baseUrl} and servicePath, to build the final service operation's URL * @param req The request as specified by Jersey's {@link Form} * @param targetClass service is supposed to return an instance of this. Null means 'void' * @return and instance of targetClass, based on what the web service operation yields back for the current request. */ private <T> T invokeWsReq ( String servicePath, String reqPath, Form req, Class<T> targetClass ) { try { if ( log.isTraceEnabled () ) { Map<String, Object> debugReq = new HashMap<String, Object> ( req ); if ( debugReq.get ( "login-secret" ) != null ) debugReq.put ( "login-secret", "***" ); if ( debugReq.get ( "login-pwd" ) != null ) debugReq.put ( "login-pwd", "***" ); if ( debugReq.get ( "secret" ) != null ) debugReq.put ( "secret", "***" ); if ( debugReq.get ( "password" ) != null ) debugReq.put ( "password", "***" ); log.trace ( "Requested web service: {}\n: {}", reqPath, debugReq ); } Client cli = Client.create (); WebResource wr = cli.resource ( this.baseUrl + servicePath + reqPath ); WebResource.Builder builder = wr.accept ( MediaType.APPLICATION_XML_TYPE ); if ( targetClass == null ) { builder.post ( req ); return null; } else return builder.post ( targetClass, req ); } catch ( UniformInterfaceException ex ) { // Check if we got security exception ClientResponse.Status status = ex.getResponse ().getClientResponseStatus (); String msg = status.getReasonPhrase () + " [" + status.getStatusCode () + "]"; if ( status.getStatusCode () == Response.Status.FORBIDDEN.getStatusCode () ) // Emulate the server-side triggering of a security error. This is apparently the only way to do that via HTTP throw new SecurityException ( "Security problem with the myEquivalents web service: " + msg, ex ); else throw new RuntimeException ( "Problem with myEquivalents web service: " + msg, ex ); } } /** * Uses {@link #getServicePath()} */ protected <T> T invokeWsReq ( String reqPath, Form req, Class<T> targetClass ) { return invokeWsReq ( getServicePath (), reqPath, req, targetClass ); } /** * Invokes {@link #invokeWsReq(String, Form, Class)} with targetClass = null, i.e., assuming the server returns 'void'. */ protected void invokeVoidWsReq ( String reqPath, Form req ) { invokeWsReq ( reqPath, req, null ); } /** * Assumes a return value of type string, which can be parsed as an {@link Integer}. */ protected int invokeIntWsReq ( String reqPath, Form req ) { String sresult = invokeWsReq ( reqPath, req, String.class ); return Integer.parseInt ( sresult ); } /** * Assumes a return value of type string, which can be parsed as an {@link Boolean}. */ protected boolean invokeBooleanWsReq ( String reqPath, Form req ) { String sresult = invokeWsReq ( reqPath, req, String.class ); return Boolean.parseBoolean ( sresult ); } /** * This is used to get back a service operation's invocation in raw format (only XML is currently supported). */ protected String getRawResult ( String reqPath, Form req, String outputFormat ) { try { return IOUtils.readInputFully ( new InputStreamReader ( this.getRawResultAsStream ( reqPath, req, outputFormat ) )); } catch ( IOException ex ) { throw new RuntimeException ( String.format ( "Error while executing the web request: '%s': %s", this.baseUrl + getServicePath () + reqPath, ex.getMessage () ), ex ); } } protected InputStream getRawResultAsStream ( String reqPath, Form req, String outputFormat ) { try { outputFormat = StringUtils.trimToNull ( outputFormat ); if ( outputFormat == null ) outputFormat = "xml"; else ManagerUtils.checkOutputFormat ( outputFormat ); String acceptValue = MediaType.APPLICATION_XML; // TODO: more options in future // Request via straight POST request // HttpClient client = new DefaultHttpClient (); HttpPost post = new HttpPost ( this.baseUrl + getServicePath () + reqPath ); // Params need to be converted this way List<NameValuePair> params = new ArrayList<NameValuePair> (); for ( Map.Entry<String, List<String>> entry: req.entrySet () ) { String pname = entry.getKey (); for ( String val: entry.getValue () ) params.add ( new BasicNameValuePair ( pname, val ) ); } post.setEntity ( new UrlEncodedFormEntity ( params, "UTF-8" ) ); post.setHeader ( "Accept", acceptValue ); // GO! HttpResponse response = client.execute ( post ); // Check if the result is a security exception, emulate that on client side, in case it is StatusLine statusLine = response.getStatusLine (); if ( statusLine.getStatusCode () == Response.Status.FORBIDDEN.getStatusCode () ) throw new SecurityException ( "Security problem with the myEquivalents web service: " + statusLine.getReasonPhrase () ); // Check the result HttpEntity entity = response.getEntity (); if ( entity == null ) throw new IllegalStateException ( "No answer from the HTTP request while executing '" + post.getURI () + "'" ); // Read the result in XML format. return entity.getContent (); } catch ( IllegalArgumentException | IOException | TransformerFactoryConfigurationError ex ) { throw new RuntimeException ( String.format ( "Error while executing the web request: '%s': %s", this.baseUrl + getServicePath () + reqPath, ex.getMessage () ), ex ); } } /** * Resets user's email and password to null. */ @Override public void close () { email = apiPassword = null; } }