/*
* ProActive Parallel Suite(TM):
* The Open Source library for parallel and distributed
* Workflows & Scheduling, Orchestration, Cloud Automation
* and Big Data Analysis on Enterprise Grids & Clouds.
*
* Copyright (c) 2007 - 2017 ActiveEon
* Contact: contact@activeeon.com
*
* This library is free software: you can redistribute it and/or
* modify it under the terms of the GNU Affero General Public License
* as published by the Free Software Foundation: version 3 of
* the License.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* If needed, contact us to obtain a release under GPL Version 2 or 3
* or a different license than the AGPL.
*/
package org.ow2.proactive_grid_cloud_portal.scheduler.client;
import static org.apache.commons.io.FileUtils.copyInputStreamToFile;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.HttpURLConnection;
import java.net.URLEncoder;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import javax.ws.rs.Consumes;
import javax.ws.rs.ProcessingException;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.*;
import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.Provider;
import org.codehaus.jackson.map.DeserializationConfig;
import org.codehaus.jackson.map.ObjectMapper;
import org.jboss.resteasy.client.jaxrs.ClientHttpEngine;
import org.jboss.resteasy.client.jaxrs.ResteasyClient;
import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
import org.jboss.resteasy.client.jaxrs.ResteasyWebTarget;
import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataOutput;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.ow2.proactive_grid_cloud_portal.common.SchedulerRestInterface;
import org.ow2.proactive_grid_cloud_portal.common.exceptionmapper.ExceptionToJson;
import org.ow2.proactive_grid_cloud_portal.dataspace.dto.ListFile;
import org.ow2.proactive_grid_cloud_portal.scheduler.client.utils.Zipper;
import org.ow2.proactive_grid_cloud_portal.scheduler.dto.JobIdData;
import org.ow2.proactive_grid_cloud_portal.scheduler.exception.NotConnectedRestException;
import com.google.common.collect.Maps;
import com.google.common.io.Files;
import com.google.common.net.UrlEscapers;
public class SchedulerRestClient {
private SchedulerRestInterface scheduler;
private String restEndpointURL;
private ClientHttpEngine httpEngine;
private ResteasyProviderFactory providerFactory;
public SchedulerRestClient(String restEndpointURL) {
this(restEndpointURL, null);
}
public SchedulerRestClient(String restEndpointURL, ClientHttpEngine httpEngine) {
this.restEndpointURL = restEndpointURL;
this.httpEngine = httpEngine;
providerFactory = ResteasyProviderFactory.getInstance();
if (!providerFactory.isRegistered(JacksonContextResolver.class)) {
providerFactory.registerProvider(JacksonContextResolver.class);
}
scheduler = createRestProxy(providerFactory, restEndpointURL, httpEngine);
}
public JobIdData submitXml(String sessionId, InputStream jobXml) throws Exception {
return submitXml(sessionId, jobXml, null);
}
public JobIdData submitXml(String sessionId, InputStream jobXml, Map<String, String> variables) throws Exception {
return submit(sessionId, jobXml, MediaType.APPLICATION_XML_TYPE, variables);
}
public JobIdData submitJobArchive(String sessionId, InputStream jobArchive) throws Exception {
return submitJobArchive(sessionId, jobArchive, null);
}
public JobIdData submitJobArchive(String sessionId, InputStream jobArchive, Map<String, String> variables)
throws Exception {
return submit(sessionId, jobArchive, MediaType.APPLICATION_OCTET_STREAM_TYPE, variables);
}
public boolean pushFile(String sessionId, String space, String path, String fileName, InputStream fileContent)
throws Exception {
String uriTmpl = (new StringBuilder(restEndpointURL)).append(addSlashIfMissing(restEndpointURL))
.append("scheduler/dataspace/")
.append(space)
.append(URLEncoder.encode(path, "UTF-8"))
.toString();
ResteasyClient client = new ResteasyClientBuilder().httpEngine(httpEngine)
.providerFactory(providerFactory)
.build();
ResteasyWebTarget target = client.target(uriTmpl);
MultipartFormDataOutput formData = new MultipartFormDataOutput();
formData.addFormData("fileName", fileName, MediaType.TEXT_PLAIN_TYPE);
formData.addFormData("fileContent", fileContent, MediaType.APPLICATION_OCTET_STREAM_TYPE);
GenericEntity<MultipartFormDataOutput> entity = new GenericEntity<MultipartFormDataOutput>(formData) {
};
Response response = target.request()
.header("sessionid", sessionId)
.post(Entity.entity(entity, MediaType.MULTIPART_FORM_DATA_TYPE));
if (response.getStatus() != HttpURLConnection.HTTP_OK) {
if (response.getStatus() == HttpURLConnection.HTTP_UNAUTHORIZED) {
throw new NotConnectedRestException("User not authenticated or session timeout.");
} else {
throwException(String.format("File upload failed. Status code: %d", response.getStatus()), response);
}
}
return response.readEntity(Boolean.class);
}
public void pullFile(String sessionId, String space, String path, String outputPath) throws Exception {
String uriTmpl = (new StringBuilder(restEndpointURL)).append(addSlashIfMissing(restEndpointURL))
.append("scheduler/dataspace/")
.append(space)
.append(URLEncoder.encode(path, "UTF-8"))
.toString();
ResteasyClient client = new ResteasyClientBuilder().httpEngine(httpEngine)
.providerFactory(providerFactory)
.build();
ResteasyWebTarget target = client.target(uriTmpl);
Response response = target.request().header("sessionid", sessionId).get();
if (response.getStatus() != HttpURLConnection.HTTP_OK) {
if (response.getStatus() == HttpURLConnection.HTTP_UNAUTHORIZED) {
throw new NotConnectedRestException("User not authenticated or session timeout.");
} else {
throwException(String.format("Cannot retrieve the file. Status code: %s", response.getStatus()),
response);
}
}
try {
File file = new File(outputPath);
if (response.hasEntity()) {
copyInputStreamToFile(response.readEntity(InputStream.class), file);
} else {
// creates an empty file
file.createNewFile();
}
} catch (Exception e) {
throw e;
} finally {
if (response != null) {
response.close();
}
if (!client.isClosed()) {
client.close();
}
}
}
public boolean upload(String sessionId, File file, List<String> includes, List<String> excludes,
String dataspacePath, final String path) throws Exception {
StringBuffer uriTmpl = (new StringBuffer()).append(restEndpointURL)
.append(addSlashIfMissing(restEndpointURL))
.append("data/")
.append(dataspacePath)
.append('/')
.append(escapeUrlPathSegment(path));
ResteasyClient client = new ResteasyClientBuilder().httpEngine(httpEngine)
.providerFactory(providerFactory)
.build();
ResteasyWebTarget target = client.target(uriTmpl.toString());
Response response = null;
try {
response = target.request()
.header("sessionid", sessionId)
.put(Entity.entity(new CompressedStreamingOutput(file, includes, excludes),
new Variant(MediaType.APPLICATION_OCTET_STREAM_TYPE,
(Locale) null,
encoding(file))));
if (response.getStatus() != HttpURLConnection.HTTP_CREATED) {
if (response.getStatus() == HttpURLConnection.HTTP_UNAUTHORIZED) {
throw new NotConnectedRestException("User not authenticated or session timeout.");
} else {
throwException(String.format("File upload failed. Status code: %d", response.getStatus()),
response);
}
}
return true;
} finally {
if (response != null) {
response.close();
}
}
}
public boolean upload(String sessionId, StreamingOutput output, String encoding, String dataspace, String path)
throws Exception {
StringBuffer uriTmpl = (new StringBuffer()).append(restEndpointURL)
.append(addSlashIfMissing(restEndpointURL))
.append("data/")
.append(dataspace);
ResteasyClient client = new ResteasyClientBuilder().httpEngine(httpEngine)
.providerFactory(providerFactory)
.build();
ResteasyWebTarget target = client.target(uriTmpl.toString()).path(path);
Response response = null;
try {
response = target.request()
.header("sessionid", sessionId)
.put(Entity.entity(output,
new Variant(MediaType.APPLICATION_OCTET_STREAM_TYPE,
(Locale) null,
encoding)));
if (response.getStatus() != HttpURLConnection.HTTP_CREATED) {
if (response.getStatus() == HttpURLConnection.HTTP_UNAUTHORIZED) {
throw new NotConnectedRestException("User not authenticated or session timeout.");
} else {
throwException(String.format("File upload failed. Status code: %d" + response.getStatus()),
response);
}
}
return true;
} finally {
if (response != null) {
response.close();
}
}
}
public boolean download(String sessionId, String dataspacePath, String path, List<String> includes,
List<String> excludes, String outputPath) throws Exception {
return download(sessionId, dataspacePath, path, includes, excludes, new File(outputPath));
}
public boolean download(String sessionId, String dataspacePath, String path, List<String> includes,
List<String> excludes, File outputFile) throws Exception {
StringBuffer uriTmpl = (new StringBuffer()).append(restEndpointURL)
.append(addSlashIfMissing(restEndpointURL))
.append("data/")
.append(dataspacePath)
.append('/');
ResteasyClient client = new ResteasyClientBuilder().httpEngine(httpEngine)
.providerFactory(providerFactory)
.build();
ResteasyWebTarget target = client.target(uriTmpl.toString()).path(path);
if (includes != null && !includes.isEmpty()) {
target = target.queryParam("includes", includes.toArray(new Object[includes.size()]));
}
if (excludes != null && !excludes.isEmpty()) {
target = target.queryParam("excludes", excludes.toArray(new Object[excludes.size()]));
}
Response response = null;
try {
response = target.request().header("sessionid", sessionId).acceptEncoding("*", "gzip", "zip").get();
if (response.getStatus() != HttpURLConnection.HTTP_OK) {
if (response.getStatus() == HttpURLConnection.HTTP_UNAUTHORIZED) {
throw new NotConnectedRestException("User not authenticated or session timeout.");
} else {
throwException(String.format("Cannot retrieve the file. Status code: %d", response.getStatus()),
response);
}
}
if (response.hasEntity()) {
InputStream is = response.readEntity(InputStream.class);
if (isGZipEncoded(response)) {
if (outputFile.exists() && outputFile.isDirectory()) {
outputFile = new File(outputFile, response.getHeaderString("x-pds-pathname"));
}
Zipper.GZIP.unzip(is, outputFile);
} else if (isZipEncoded(response)) {
Zipper.ZIP.unzip(is, outputFile);
} else {
File container = outputFile.getParentFile();
if (!container.exists()) {
container.mkdirs();
}
Files.asByteSink(outputFile).writeFrom(is);
}
} else {
outputFile.createNewFile();
}
} finally {
if (response != null) {
response.close();
}
}
return true;
}
public boolean delete(String sessionId, String dataspacePath, String path, List<String> includes,
List<String> excludes) throws Exception {
StringBuffer uriTmpl = (new StringBuffer()).append(restEndpointURL)
.append(addSlashIfMissing(restEndpointURL))
.append("data/")
.append(dataspacePath)
.append('/');
ResteasyClient client = new ResteasyClientBuilder().httpEngine(httpEngine)
.providerFactory(providerFactory)
.build();
ResteasyWebTarget target = client.target(uriTmpl.toString()).path(path);
if (includes != null && !includes.isEmpty()) {
target = target.queryParam("includes", includes.toArray(new Object[includes.size()]));
}
if (excludes != null && !excludes.isEmpty()) {
target = target.queryParam("excludes", excludes.toArray(new Object[excludes.size()]));
}
Response response = null;
try {
response = target.request().header("sessionid", sessionId).delete();
if (response.getStatus() != HttpURLConnection.HTTP_NO_CONTENT) {
if (response.getStatus() == HttpURLConnection.HTTP_UNAUTHORIZED) {
throw new NotConnectedRestException("User not authenticated or session timeout.");
} else {
throwException(String.format("Cannot delete file(s). Status code: %s", response.getStatus()),
response);
}
}
return true;
} finally {
if (response != null) {
response.close();
}
}
}
public ListFile list(String sessionId, String dataspacePath, String pathname) throws Exception {
StringBuffer uriTmpl = (new StringBuffer()).append(restEndpointURL)
.append(addSlashIfMissing(restEndpointURL))
.append("data/")
.append(dataspacePath)
.append('/');
ResteasyClient client = new ResteasyClientBuilder().httpEngine(httpEngine)
.providerFactory(providerFactory)
.build();
ResteasyWebTarget target = client.target(uriTmpl.toString()).path(pathname).queryParam("comp", "list");
Response response = null;
try {
response = target.request().header("sessionid", sessionId).get();
if (response.getStatus() != HttpURLConnection.HTTP_OK) {
if (response.getStatus() == HttpURLConnection.HTTP_UNAUTHORIZED) {
throw new NotConnectedRestException("User not authenticated or session timeout.");
} else {
throwException(String.format("Cannot list the specified location: %s", pathname), response);
}
}
return response.readEntity(ListFile.class);
} finally {
if (response != null) {
response.close();
}
}
}
public Map<String, Object> metadata(String sessionId, String dataspacePath, String pathname) throws Exception {
StringBuffer uriTmpl = (new StringBuffer()).append(restEndpointURL)
.append(addSlashIfMissing(restEndpointURL))
.append("data/")
.append(dataspacePath)
.append(escapeUrlPathSegment(pathname));
ResteasyClient client = new ResteasyClientBuilder().httpEngine(httpEngine)
.providerFactory(providerFactory)
.build();
ResteasyWebTarget target = client.target(uriTmpl.toString());
Response response = null;
try {
response = target.request().header("sessionid", sessionId).head();
if (response.getStatus() != HttpURLConnection.HTTP_OK) {
if (response.getStatus() == HttpURLConnection.HTTP_UNAUTHORIZED) {
throw new NotConnectedRestException("User not authenticated or session timeout.");
} else {
throwException(String.format("Cannot get metadata from %s in %s.", pathname, dataspacePath),
response);
}
}
MultivaluedMap<String, Object> headers = response.getHeaders();
Map<String, Object> metaMap = Maps.newHashMap();
if (headers.containsKey(HttpHeaders.LAST_MODIFIED)) {
metaMap.put(HttpHeaders.LAST_MODIFIED, headers.getFirst(HttpHeaders.LAST_MODIFIED));
}
return metaMap;
} finally {
if (response != null) {
response.close();
}
}
}
private JobIdData submit(String sessionId, InputStream job, MediaType mediaType, Map<String, String> variables)
throws Exception {
String uriTmpl = restEndpointURL + addSlashIfMissing(restEndpointURL) + "scheduler/submit";
ResteasyClient client = new ResteasyClientBuilder().httpEngine(httpEngine)
.providerFactory(providerFactory)
.build();
ResteasyWebTarget target = client.target(uriTmpl);
if (variables != null) {
for (String key : variables.keySet()) {
target = target.matrixParam(key, variables.get(key));
}
}
MultipartFormDataOutput formData = new MultipartFormDataOutput();
formData.addFormData("file", job, mediaType);
GenericEntity<MultipartFormDataOutput> entity = new GenericEntity<MultipartFormDataOutput>(formData) {
};
Response response = target.request()
.header("sessionid", sessionId)
.post(Entity.entity(entity, MediaType.MULTIPART_FORM_DATA_TYPE));
if (response.getStatus() != HttpURLConnection.HTTP_OK) {
if (response.getStatus() == HttpURLConnection.HTTP_UNAUTHORIZED) {
throw new NotConnectedRestException("User not authenticated or session timeout.");
} else {
throwException(String.format("Job submission failed status code: %d", response.getStatus()), response);
}
}
return response.readEntity(JobIdData.class);
}
private String addSlashIfMissing(String url) {
return url.endsWith("/") ? "" : "/";
}
private boolean isGZipEncoded(Response response) {
return "gzip".equals(response.getHeaderString(HttpHeaders.CONTENT_ENCODING));
}
private boolean isZipEncoded(Response response) {
return "zip".equals(response.getHeaderString(HttpHeaders.CONTENT_ENCODING));
}
private String escapeUrlPathSegment(String unescaped) {
return UrlEscapers.urlPathSegmentEscaper().escape(unescaped);
}
public SchedulerRestInterface getScheduler() {
return scheduler;
}
private void throwException(String errorMessage, Response response) {
Exception serverException = null;
try {
serverException = rebuildServerSideException(response.readEntity(ExceptionToJson.class));
} catch (Exception ignorable) {
}
throw new RuntimeException(errorMessage, serverException);
}
private static SchedulerRestInterface createRestProxy(ResteasyProviderFactory provider, String restEndpointURL,
ClientHttpEngine httpEngine) {
ResteasyClient client = new ResteasyClientBuilder().providerFactory(provider).httpEngine(httpEngine).build();
ResteasyWebTarget target = client.target(restEndpointURL);
SchedulerRestInterface schedulerRestClient = target.proxy(SchedulerRestInterface.class);
return createExceptionProxy(schedulerRestClient);
}
private static SchedulerRestInterface createExceptionProxy(final SchedulerRestInterface scheduler) {
return (SchedulerRestInterface) Proxy.newProxyInstance(SchedulerRestInterface.class.getClassLoader(),
new Class[] { SchedulerRestInterface.class },
new RestClientExceptionHandler(scheduler));
}
private static class RestClientExceptionHandler implements InvocationHandler {
private final SchedulerRestInterface scheduler;
public RestClientExceptionHandler(SchedulerRestInterface scheduler) {
this.scheduler = scheduler;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
return method.invoke(scheduler, args);
} catch (InvocationTargetException targetException) {
if (targetException.getTargetException() instanceof WebApplicationException) {
WebApplicationException clientException = (WebApplicationException) targetException.getTargetException();
try {
ExceptionToJson json = clientException.getResponse().readEntity(ExceptionToJson.class);
// here we take the server side exception and recreate it on the client side
throw rebuildServerSideException(json);
} catch (ProcessingException couldNotReadJsonException) {
// rethrow server side exception as runtime exception but do not transform it
throw clientException;
} catch (IllegalStateException couldNotReadJsonException) {
// rethrow server side exception as runtime exception but do not transform it
throw clientException;
}
}
// rethrow real exception as runtime (client side exception)
throw new RuntimeException(targetException.getTargetException());
}
}
}
@Provider
@Consumes({ MediaType.APPLICATION_JSON, "text/json" })
@Produces({ MediaType.APPLICATION_JSON, "text/json" })
public static class JacksonContextResolver implements ContextResolver<ObjectMapper> {
@Override
public ObjectMapper getContext(Class<?> objectType) {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return objectMapper;
}
}
private static Exception rebuildServerSideException(ExceptionToJson json)
throws IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException {
Throwable serverException = json.getException();
String exceptionClassName = json.getExceptionClass();
String errMsg = json.getErrorMessage();
if (errMsg == null) {
errMsg = "An error has occurred.";
}
if (serverException != null && exceptionClassName != null) {
Class<?> exceptionClass = toClass(exceptionClassName);
if (exceptionClass != null) {
// wrap the exception serialized in JSON inside an
// instance of
// the server exception class
Constructor<?> constructor = getConstructor(exceptionClass, Throwable.class);
if (constructor != null) {
return (Exception) constructor.newInstance(serverException);
}
constructor = getConstructor(exceptionClass, String.class);
if (constructor != null) {
Exception built = (Exception) constructor.newInstance(errMsg);
built.setStackTrace(serverException.getStackTrace());
return built;
}
}
}
Exception built = new Exception(errMsg);
if (serverException != null) {
built.setStackTrace(serverException.getStackTrace());
}
return built;
}
private static Class<?> toClass(String className) {
try {
return Class.forName(className);
} catch (ClassNotFoundException e) {
return null;
}
}
private static Constructor<?> getConstructor(Class<?> clazz, Class<?>... paramTypes) {
try {
return clazz.getConstructor(paramTypes);
} catch (NoSuchMethodException e) {
return null;
}
}
private static String encoding(File file) throws FileNotFoundException {
return file.isDirectory() ? "zip" : (Zipper.isZipFile(file)) ? null : "gzip";
}
private static class CompressedStreamingOutput implements StreamingOutput {
private File file;
private List<String> includes;
private List<String> excludes;
public CompressedStreamingOutput(File file, List<String> includes, List<String> excludes) {
this.file = file;
this.includes = includes;
this.excludes = excludes;
}
@Override
public void write(OutputStream outputStream) throws IOException, WebApplicationException {
if (file.isFile()) {
if (Zipper.isZipFile(file)) {
Files.asByteSource(file).copyTo(outputStream);
} else {
Zipper.GZIP.zip(file, outputStream);
}
} else {
Zipper.ZIP.zip(file, includes, excludes, outputStream);
}
}
}
}