Java tutorial
/* * #%L * Alfresco Remote API * %% * Copyright (C) 2005 - 2016 Alfresco Software Limited * %% * This file is part of the Alfresco software. * If the software was purchased under a paid Alfresco license, the terms of * the paid license agreement will prevail. Otherwise, the software is * provided under the following open source license terms: * * Alfresco is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Alfresco is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Alfresco. If not, see <http://www.gnu.org/licenses/>. * #L% */ package org.alfresco.rest.api.tests.client; import java.io.IOException; import java.io.UnsupportedEncodingException; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.HttpMethod; import org.apache.commons.httpclient.HttpState; import org.apache.commons.httpclient.HttpStatus; import org.apache.commons.httpclient.URIException; import org.apache.commons.httpclient.UsernamePasswordCredentials; import org.apache.commons.httpclient.auth.AuthScope; import org.apache.commons.httpclient.methods.EntityEnclosingMethod; import org.apache.commons.httpclient.methods.PostMethod; import org.apache.commons.httpclient.methods.StringRequestEntity; import org.json.simple.JSONObject; public class AuthenticatedHttp extends AbstractHttp { private static final String JSON_TICKET_KEY = "ticket"; private static final String TICKET_CREDENTIAL_PLACEHOLDER = "ROLE_TICKET"; /** * URL for obtaining an alfresco-ticket */ private String LOGIN_URL = "/alfresco/service/api/login"; private String LOGIN_JSON_USERNAME = "username"; private String LOGIN_JSON_PASSWORD = "password"; public static final String MIME_TYPE_JSON = "application/json"; public static final String HEADER_ACCEPT = "Accept"; private HttpClientProvider httpProvider; private AuthenticationDetailsProvider authDetailProvider; private boolean ticketBasedAuthentication; /** * @param httpProvider provider class for http-client * @param authenticationDetailsProvider provider for authentication details */ public AuthenticatedHttp(HttpClientProvider httpProvider, AuthenticationDetailsProvider authenticationDetailsProvider) { this.httpProvider = httpProvider; this.authDetailProvider = authenticationDetailsProvider; this.ticketBasedAuthentication = false; } /** * Enable ticket-based authentication. If set to false, BASIC Authentication will * be used instead. Defaults to false. * * @param ticketBasedAuthentication whether or not to use ticket for authentication */ public void setTicketBasedAuthentication(boolean ticketBasedAuthentication) { this.ticketBasedAuthentication = ticketBasedAuthentication; } /** * @return the {@link HttpClientProvider} used by this class. */ public HttpClientProvider getHttpProvider() { return this.httpProvider; } /** * @return the {@link AuthenticationDetailsProvider} used by this class. */ public AuthenticationDetailsProvider getAuthDetailProvider() { return this.authDetailProvider; } /** * Execute the given method, authenticated as the given user. Automatically closes the * response-stream to release the connection. If response should be extracted, this should be done * in the {@link HttpRequestCallback}. * * @param method method to execute * @param userName name of user to authenticate * @param callback called after http-call is executed. When callback returns, the * response stream is closed, so all respose-related operations should be done in the callback. Can be null. * @return result returned by the callback or status-code if no callback is given. */ public <T extends Object> T executeHttpMethodAuthenticated(HttpMethod method, String userName, HttpRequestCallback<T> callback) { if (ticketBasedAuthentication) { return executeWithTicketAuthentication(method, userName, authDetailProvider.getPasswordForUser(userName), callback); } else { return executeWithBasicAuthentication(method, userName, authDetailProvider.getPasswordForUser(userName), callback); } } public <T extends Object> T executeHttpMethodAuthenticated(HttpMethod method, String userName, String password, HttpRequestCallback<T> callback) { if (ticketBasedAuthentication) { return executeWithTicketAuthentication(method, userName, password, callback); } else { return executeWithBasicAuthentication(method, userName, password, callback); } } /** * Execute the given method, authenticated as the Alfresco Administrator. * * @param method method to execute * @param callback called after http-call is executed. When callback returns, the * response stream is closed, so all respose-related operations should be done in the callback. Can be null. * @return result returned by the callback or null if no callback is given. */ public <T extends Object> T executeHttpMethodAsAdmin(HttpMethod method, HttpRequestCallback<T> callback) { if (ticketBasedAuthentication) { return executeWithTicketAuthentication(method, authDetailProvider.getAdminUserName(), authDetailProvider.getAdminPassword(), callback); } else { return executeWithBasicAuthentication(method, authDetailProvider.getAdminUserName(), authDetailProvider.getAdminPassword(), callback); } } /** * Execute the given method, authenticated as the given user using Basic Authentication. * @param method method to execute * @param userName name of user to authenticate (note: if null then attempts to run with no authentication - eq. Quick/Shared Link test) * @param callback called after http-call is executed. When callback returns, the * response stream is closed, so all respose-related operations should be done in the callback. Can be null. * @return result returned by the callback or null if no callback is given. */ private <T extends Object> T executeWithBasicAuthentication(HttpMethod method, String userName, String password, HttpRequestCallback<T> callback) { try { HttpState state = new HttpState(); if (userName != null) { state.setCredentials(new AuthScope(null, AuthScope.ANY_PORT), new UsernamePasswordCredentials(userName, password)); } httpProvider.getHttpClient().executeMethod(null, method, state); if (callback != null) { return callback.onCallSuccess(method); } // No callback used, return null return null; } catch (Throwable t) { boolean handled = false; // Delegate to callback to handle error. If not available, throw exception if (callback != null) { handled = callback.onError(method, t); } if (!handled) { throw new RuntimeException("Error while executing HTTP-call (" + method.getPath() + ")", t); } return null; } finally { method.releaseConnection(); } } /** * Execute the given method, authenticated as the given user using ticket-based authentication. * @param method method to execute * @param userName name of user to authenticate * @return status-code resulting from the request */ private <T extends Object> T executeWithTicketAuthentication(HttpMethod method, String userName, String password, HttpRequestCallback<T> callback) { String ticket = authDetailProvider.getTicketForUser(userName); if (ticket == null) { ticket = fetchLoginTicket(userName, password); authDetailProvider.updateTicketForUser(userName, ticket); } try { HttpState state = applyTicketToMethod(method, ticket); // Try executing the method int result = httpProvider.getHttpClient().executeMethod(null, method, state); if (result == HttpStatus.SC_UNAUTHORIZED || result == HttpStatus.SC_FORBIDDEN) { method.releaseConnection(); if (!method.validate()) { throw new RuntimeException( "Ticket re-authentication failed for user " + userName + " (HTTPMethod not reusable)"); } // Fetch new ticket, store and apply to HttpMethod ticket = fetchLoginTicket(userName, userName); authDetailProvider.updateTicketForUser(userName, ticket); state = applyTicketToMethod(method, ticket); // Run method agian with new ticket result = httpProvider.getHttpClient().executeMethod(null, method, state); } if (callback != null) { return callback.onCallSuccess(method); } return null; } catch (Throwable t) { boolean handled = false; // Delegate to callback to handle error. If not available, throw exception if (callback != null) { handled = callback.onError(method, t); } if (!handled) { throw new RuntimeException("Error while executing HTTP-call (" + method.getPath() + ")", t); } return null; } finally { method.releaseConnection(); } } /** * Add the ticket to the method. In case of {@link EntityEnclosingMethod}s (which don't * support Query-parameters), the ticket is added as Username in BASIC Authentication, * this is a supported way of passing in ticket into Alfresco. * * @param method method to apply * @param ticket ticket to apply * * @return a {@link HttpState} object to use. Null, if no specific state should be used. */ private HttpState applyTicketToMethod(HttpMethod method, String ticket) throws URIException { // POST and PUT methods don't support Query-params, use Basic Authentication to pass // in the ticket (ROLE_TICKET) for all methods. HttpState state = new HttpState(); state.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(TICKET_CREDENTIAL_PLACEHOLDER, ticket)); return state; } /** * Adds the JSON as request-body the the method and sets the correct * content-type. * @param method EntityEnclosingMethod * @param object JSONObject */ private void populateRequestBody(EntityEnclosingMethod method, JSONObject object) { try { method.setRequestEntity(new StringRequestEntity(object.toJSONString(), MIME_TYPE_JSON, "UTF-8")); } catch (UnsupportedEncodingException error) { // This will never happen! throw new RuntimeException("All hell broke loose, a JVM that doesn't have UTF-8 encoding..."); } } /** * Perform the login-call to obtain a ticket. * * @param userName user to log in * @return ticket to use for authentication. * @throws RuntimeException when no ticket can be obtained for the user. */ @SuppressWarnings("unchecked") private String fetchLoginTicket(String userName, String password) { String url = httpProvider.getFullAlfrescoUrlForPath(LOGIN_URL); PostMethod loginMethod = null; try { loginMethod = new PostMethod(url); loginMethod.setRequestHeader(HEADER_ACCEPT, MIME_TYPE_JSON); // Populate resuest body JSONObject requestBody = new JSONObject(); requestBody.put(LOGIN_JSON_USERNAME, userName); requestBody.put(LOGIN_JSON_PASSWORD, password); populateRequestBody(loginMethod, requestBody); HttpClient client = httpProvider.getHttpClient(); // Since no authentication info is available yet, no need to use a // custom HostConfiguration for the login-call client.executeMethod(loginMethod); if (loginMethod.getStatusCode() == HttpStatus.SC_OK) { // Extract the ticket JSONObject data = getDataFromResponse(loginMethod); if (data == null) { throw new RuntimeException("Failed to login to Alfresco with user " + userName + " (No JSON-data found in response)"); } // Extract the actual ticket String ticket = getString(data, JSON_TICKET_KEY, null); if (ticket == null) { throw new RuntimeException("Failed to login to Alfresco with user " + userName + " (No ticket found in JSON-response)"); } return ticket; } else { // Unable to login throw new RuntimeException("Failed to login to Alfresco with user " + userName + " (" + loginMethod.getStatusCode() + loginMethod.getStatusLine().getReasonPhrase() + ")"); } } catch (IOException ioe) { // Something went wrong when sending request throw new RuntimeException("Failed to login to Alfresco with user " + userName, ioe); } finally { if (loginMethod != null) { try { loginMethod.releaseConnection(); } catch (Throwable t) { // Ignore this to prevent swallowing potential original exception } } } } /** * Callback used when executing HTTP-request. After this has been called, * the response-stream is closed automatically. * * @author Frederik Heremans */ public interface HttpRequestCallback<T extends Object> { /** * Called when call was successful. * @param method the method executed which can be used to extract response from. * @return any result extracted from the response body. */ T onCallSuccess(HttpMethod method) throws Exception; /** * Called when an error occurs when sending the request. * @param method the method executed * @param t optional exception that caused the error * @return true, if error is handled. False, if an exception should be thrown. */ boolean onError(HttpMethod method, Throwable t); } }