package eu.europeana.cloud.mcs.driver; import eu.europeana.cloud.common.response.ErrorInfo; import eu.europeana.cloud.common.web.ParamConstants; import eu.europeana.cloud.mcs.driver.exception.DriverException; import eu.europeana.cloud.mcs.driver.filter.ECloudBasicAuthFilter; import eu.europeana.cloud.service.mcs.exception.CannotModifyPersistentRepresentationException; import eu.europeana.cloud.service.mcs.exception.FileNotExistsException; import eu.europeana.cloud.service.mcs.exception.MCSException; import eu.europeana.cloud.service.mcs.exception.RepresentationNotExistsException; import eu.europeana.cloud.service.mcs.exception.WrongContentRangeException; import org.glassfish.jersey.client.JerseyClientBuilder; import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature; import org.glassfish.jersey.media.multipart.FormDataMultiPart; import org.glassfish.jersey.media.multipart.MultiPartFeature; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.ws.rs.client.Client; import javax.ws.rs.client.Entity; import javax.ws.rs.client.Invocation; import javax.ws.rs.client.Invocation.Builder; import javax.ws.rs.client.WebTarget; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import static eu.europeana.cloud.common.web.ParamConstants.H_RANGE; /** * Exposes API related to files. */ public class FileServiceClient extends MCSClient { private final Client client; private static final Logger logger = LoggerFactory.getLogger(FileServiceClient.class); //records/CLOUDID/representations/REPRESENTATIONNAME/versions/VERSION/files/ private static final String filesPath = "records/{" + ParamConstants.P_CLOUDID + "}/representations/{" + ParamConstants.P_REPRESENTATIONNAME + "}/versions/{" + ParamConstants.P_VER + "}/files"; //records/CLOUDID/representations/REPRESENTATIONNAME/versions/VERSION/files/FILENAME/ private static final String filePath = filesPath + "/{" + ParamConstants.P_FILENAME + "}"; /** * Constructs a FileServiceClient * * @param baseUrl url of the MCS Rest Service */ public FileServiceClient(String baseUrl) { super(baseUrl); client = JerseyClientBuilder.newClient().register(MultiPartFeature.class); } /** * Creates instance of FileServiceClient. Same as {@link #FileServiceClient(String)} * but includes username and password to perform authenticated requests. * * @param baseUrl URL of the MCS Rest Service */ public FileServiceClient(String baseUrl, final String username, final String password) { super(baseUrl); client = JerseyClientBuilder.newClient() .register(MultiPartFeature.class) .register(HttpAuthenticationFeature.basicBuilder().credentials(username, password).build()); } /** * Function returns file content. * * @param cloudId id of returned file. * @param representationName representation name of returned file. * @param version version of returned file. * @param fileName name of file. * @return InputStream returned content. * @throws RepresentationNotExistsException when requested representation (or representation version) does not exist. * @throws FileNotExistsException when requested file does not exist. * @throws DriverException call to service has not succeeded because of server side error. * @throws MCSException on unexpected situations. */ public InputStream getFile(String cloudId, String representationName, String version, String fileName) throws RepresentationNotExistsException, FileNotExistsException, DriverException, MCSException, IOException { WebTarget target = client.target(baseUrl).path(filePath).resolveTemplate(ParamConstants.P_CLOUDID, cloudId) .resolveTemplate(ParamConstants.P_REPRESENTATIONNAME, representationName) .resolveTemplate(ParamConstants.P_VER, version).resolveTemplate(ParamConstants.P_FILENAME, fileName); Builder requset = target.request(); Response response = null; try { response = requset.get(); if (response.getStatus() == Response.Status.OK.getStatusCode()) { InputStream contentResponse = response.readEntity(InputStream.class); return copiedInputStream(contentResponse); } else { ErrorInfo errorInfo = response.readEntity(ErrorInfo.class); throw MCSExceptionProvider.generateException(errorInfo); } } finally { closeResponse(response); } } /** * Function returns file content. By setting range parameter one can retrieve only a part of content. * * @param cloudId id of returned file. * @param representationName representation name of returned file. * @param version version of returned file. * @param fileName name of file. * @param range range of bytes to return. Range header can be found in Hypertext Transfer Protocol HTTP/1.1, section * 14.35 Range). * @return InputStream returned content. * @throws RepresentationNotExistsException when requested representation (or representation version) does not exist. * @throws FileNotExistsException when requested file does not exist. * @throws WrongContentRangeException when wrong value in "Range" header. * @throws DriverException call to service has not succeeded because of server side error. * @throws MCSException on unexpected situations. */ public InputStream getFile(String cloudId, String representationName, String version, String fileName, String range) throws RepresentationNotExistsException, FileNotExistsException, WrongContentRangeException, DriverException, MCSException, IOException { WebTarget target = client.target(baseUrl).path(filePath).resolveTemplate(ParamConstants.P_CLOUDID, cloudId) .resolveTemplate(ParamConstants.P_REPRESENTATIONNAME, representationName) .resolveTemplate(ParamConstants.P_VER, version).resolveTemplate(ParamConstants.P_FILENAME, fileName); Builder request = target.request().header(H_RANGE, range); Response response = null; try { response = request.get(); if (response.getStatus() == Response.Status.PARTIAL_CONTENT.getStatusCode()) { InputStream contentResponse = response.readEntity(InputStream.class); return copiedInputStream(contentResponse); } else { ErrorInfo errorInfo = response.readEntity(ErrorInfo.class); throw MCSExceptionProvider.generateException(errorInfo); } } finally { closeResponse(response); } } /** * Function returns file content. */ public InputStream getFile(String fileUrl) throws RepresentationNotExistsException, FileNotExistsException, WrongContentRangeException, DriverException, MCSException, IOException { Response response = null; try { response = client.target(fileUrl).request().get(); if (response.getStatus() == Response.Status.OK.getStatusCode()) { InputStream contentResponse = response.readEntity(InputStream.class); return copiedInputStream(contentResponse); } else { ErrorInfo errorInfo = response.readEntity(ErrorInfo.class); throw MCSExceptionProvider.generateException(errorInfo); } } finally { closeResponse(response); } } /** * Uploads file content with checking checksum. * * @param cloudId id of uploaded file. * @param representationName representation name of uploaded file. * @param version version of uploaded file. * @param data InputStream (content) of uploaded file. * @param mediaType mediaType of uploaded file. * @param expectedMd5 expected MD5 checksum. * @return URI to uploaded file. * @throws IOException when incorrect MD5 checksum. * @throws RepresentationNotExistsException when representation does not exist in specified version. * @throws CannotModifyPersistentRepresentationException when specified representation version is persistent and modifying its files is not allowed. * @throws DriverException call to service has not succeeded because of server side error. * @throws MCSException on unexpected situations. */ public URI uploadFile(String cloudId, String representationName, String version, InputStream data, String mediaType, String expectedMd5) throws IOException, RepresentationNotExistsException, CannotModifyPersistentRepresentationException, DriverException, MCSException { WebTarget target = client.target(baseUrl).path(filesPath).resolveTemplate(ParamConstants.P_CLOUDID, cloudId) .resolveTemplate(ParamConstants.P_REPRESENTATIONNAME, representationName) .resolveTemplate(ParamConstants.P_VER, version); FormDataMultiPart multipart = new FormDataMultiPart().field(ParamConstants.F_FILE_MIME, mediaType).field( ParamConstants.F_FILE_DATA, data, MediaType.APPLICATION_OCTET_STREAM_TYPE); Builder request = target.request(); Response response = null; try { response = request.post(Entity.entity(multipart, multipart.getMediaType())); if (response.getStatus() == Status.CREATED.getStatusCode()) { if (!expectedMd5.equals(response.getEntityTag().getValue())) { throw new IOException("Incorrect MD5 checksum"); } return response.getLocation(); } else { ErrorInfo errorInfo = response.readEntity(ErrorInfo.class); throw MCSExceptionProvider.generateException(errorInfo); } } finally { closeResponse(response); } } /** * Uploads file content without checking checksum. * * @param cloudId id of uploaded file. * @param representationName representation name of uploaded file. * @param version version of uploaded file. * @param data InputStream (content) of uploaded file. * @param mediaType mediaType of uploaded file. * @return URI of uploaded file. * @throws RepresentationNotExistsException when representation does not exist in specified version. * @throws CannotModifyPersistentRepresentationException when specified representation version is persistent and modifying its files is not allowed. * @throws DriverException call to service has not succeeded because of server side error. * @throws MCSException on unexpected situations. */ public URI uploadFile(String cloudId, String representationName, String version, InputStream data, String mediaType) throws RepresentationNotExistsException, CannotModifyPersistentRepresentationException, DriverException, MCSException { WebTarget target = client.target(baseUrl).path(filesPath).resolveTemplate(ParamConstants.P_CLOUDID, cloudId) .resolveTemplate(ParamConstants.P_REPRESENTATIONNAME, representationName) .resolveTemplate(ParamConstants.P_VER, version); FormDataMultiPart multipart = new FormDataMultiPart().field(ParamConstants.F_FILE_MIME, mediaType).field( ParamConstants.F_FILE_DATA, data, MediaType.APPLICATION_OCTET_STREAM_TYPE); Builder request = target.request(); Response response = null; try { response = request.post(Entity.entity(multipart, multipart.getMediaType())); if (response.getStatus() == Status.CREATED.getStatusCode()) { return response.getLocation(); } else { ErrorInfo errorInfo = response.readEntity(ErrorInfo.class); throw MCSExceptionProvider.generateException(errorInfo); } } finally { closeResponse(response); } } /** * Uploads file content without checking checksum. * * @param cloudId id of uploaded file. * @param representationName representation name of uploaded file. * @param version version of uploaded file. * @param data InputStream (content) of uploaded file. * @param mediaType mediaType of uploaded file. * @param fileName user file name * @return URI of uploaded file. * @throws RepresentationNotExistsException when representation does not exist in specified version. * @throws CannotModifyPersistentRepresentationException when specified representation version is persistent and modifying its files is not allowed. * @throws DriverException call to service has not succeeded because of server side error. * @throws MCSException on unexpected situations. */ public URI uploadFile(String cloudId, String representationName, String version, String fileName, InputStream data, String mediaType) throws RepresentationNotExistsException, CannotModifyPersistentRepresentationException, DriverException, MCSException { WebTarget target = client.target(baseUrl).path(filesPath).resolveTemplate(ParamConstants.P_CLOUDID, cloudId) .resolveTemplate(ParamConstants.P_REPRESENTATIONNAME, representationName) .resolveTemplate(ParamConstants.P_VER, version); FormDataMultiPart multipart = new FormDataMultiPart().field(ParamConstants.F_FILE_MIME, mediaType).field( ParamConstants.F_FILE_DATA, data, MediaType.APPLICATION_OCTET_STREAM_TYPE).field(ParamConstants.F_FILE_NAME, fileName); Invocation.Builder request = target.request(); Response response = null; try { response = request.post(Entity.entity(multipart, multipart.getMediaType())); if (response.getStatus() == Response.Status.CREATED.getStatusCode()) { return response.getLocation(); } else { ErrorInfo errorInfo = response.readEntity(ErrorInfo.class); throw MCSExceptionProvider.generateException(errorInfo); } } finally { closeResponse(response); } } /** * Uploads file content without checking checksum. * * @param versionUrl Path to the version where the file will be uploaded to. * For example: * "http://ecloud.eanadev.org:8080/ecloud-service-mcs/records/L9WSPSMVQ85/representations/edm/versions/b17c4f60-70d0-11e4-8fe1-00163eefc9c8" */ public URI uploadFile(String versionUrl, InputStream data, String mediaType) throws RepresentationNotExistsException, CannotModifyPersistentRepresentationException, DriverException, MCSException { String filesPath = "/files"; FormDataMultiPart multipart = new FormDataMultiPart().field(ParamConstants.F_FILE_MIME, mediaType).field( ParamConstants.F_FILE_DATA, data, MediaType.APPLICATION_OCTET_STREAM_TYPE); Response response = null; try { response = client.target(versionUrl + filesPath).request().post(Entity.entity(multipart, multipart.getMediaType())); if (response.getStatus() == Status.CREATED.getStatusCode()) { return response.getLocation(); } else { ErrorInfo errorInfo = response.readEntity(ErrorInfo.class); throw MCSExceptionProvider.generateException(errorInfo); } } finally { closeResponse(response); } } /** * Modifies existed file with checking checksum. * * @param cloudId id of modifying file. * @param representationName representation name of modifying file. * @param version version of modifying file. * @param data InputStream (content) of modifying file. * @param mediaType mediaType of modifying file. * @param fileName name of modifying file. * @param expectedMd5 expected MD5 checksum. * @return URI to modified file. * @throws IOException when checksum is incorrect. * @throws RepresentationNotExistsException when representation does not exist in specified version. * @throws CannotModifyPersistentRepresentationException when specified representation version is persistent and modifying its files is not allowed. * @throws DriverException call to service has not succeeded because of server side error. * @throws MCSException on unexpected situations. */ public URI modyfiyFile(String cloudId, String representationName, String version, InputStream data, String mediaType, String fileName, String expectedMd5) throws IOException, RepresentationNotExistsException, CannotModifyPersistentRepresentationException, DriverException, MCSException { WebTarget target = client.target(baseUrl).path(filePath).resolveTemplate(ParamConstants.P_CLOUDID, cloudId) .resolveTemplate(ParamConstants.P_REPRESENTATIONNAME, representationName) .resolveTemplate(ParamConstants.P_VER, version).resolveTemplate(ParamConstants.P_FILENAME, fileName); FormDataMultiPart multipart = new FormDataMultiPart().field(ParamConstants.F_FILE_MIME, mediaType).field( ParamConstants.F_FILE_DATA, data, MediaType.APPLICATION_OCTET_STREAM_TYPE); Response response = null; try { response = target.request().put(Entity.entity(multipart, multipart.getMediaType())); if (response.getStatus() == Status.NO_CONTENT.getStatusCode()) { if (!expectedMd5.equals(response.getEntityTag().getValue())) { throw new IOException("Incorrect MD5 checksum"); } return response.getLocation(); } else { ErrorInfo errorInfo = response.readEntity(ErrorInfo.class); throw MCSExceptionProvider.generateException(errorInfo); } } finally { closeResponse(response); } } public URI modifyFile(String fileUrl, InputStream data, String mediaType) throws IOException, RepresentationNotExistsException, CannotModifyPersistentRepresentationException, DriverException, MCSException { WebTarget target = client.target(fileUrl); FormDataMultiPart multipart = new FormDataMultiPart().field(ParamConstants.F_FILE_MIME, mediaType).field( ParamConstants.F_FILE_DATA, data, MediaType.APPLICATION_OCTET_STREAM_TYPE); Response response = null; try { response = target.request().put(Entity.entity(multipart, multipart.getMediaType())); if (response.getStatus() == Status.NO_CONTENT.getStatusCode()) { return response.getLocation(); } else { ErrorInfo errorInfo = response.readEntity(ErrorInfo.class); throw MCSExceptionProvider.generateException(errorInfo); } } finally { closeResponse(response); } } /** * Deletes existed file. * * @param cloudId id of deleting file. * @param representationName representation name of deleting file. * @param version version of deleting file. * @param fileName name of deleting file. * @throws RepresentationNotExistsException when representation does not exist in specified version. * @throws FileNotExistsException when requested file does not exist. * @throws CannotModifyPersistentRepresentationException when specified representation version is persistent and modifying its files is not allowed. * @throws DriverException call to service has not succeeded because of server side error. * @throws MCSException on unexpected situations. */ public void deleteFile(String cloudId, String representationName, String version, String fileName) throws RepresentationNotExistsException, FileNotExistsException, CannotModifyPersistentRepresentationException, DriverException, MCSException { WebTarget target = client.target(baseUrl).path(filePath).resolveTemplate(ParamConstants.P_CLOUDID, cloudId) .resolveTemplate(ParamConstants.P_REPRESENTATIONNAME, representationName) .resolveTemplate(ParamConstants.P_VER, version).resolveTemplate(ParamConstants.P_FILENAME, fileName); Response response = null; try { response = target.request().delete(); if (response.getStatus() != Response.Status.NO_CONTENT.getStatusCode()) { ErrorInfo errorInfo = response.readEntity(ErrorInfo.class); throw MCSExceptionProvider.generateException(errorInfo); } } finally { closeResponse(response); } } /** * Retrieve file uri from parameters. * * @param cloudId id of file. * @param representationName representation name of file. * @param version version of file. * @param fileName name of file. * @return file URI */ public URI getFileUri(String cloudId, String representationName, String version, String fileName) { WebTarget target = client.target(baseUrl).path(filePath) .resolveTemplate(ParamConstants.P_CLOUDID, cloudId) .resolveTemplate(ParamConstants.P_REPRESENTATIONNAME, representationName) .resolveTemplate(ParamConstants.P_VER, version).resolveTemplate(ParamConstants.P_FILENAME, fileName); return target.getUri(); } /** * Retrieve parts of file uri. * <p> * Examples: * From this string "http://ecloud.eanadev.org:8080/ecloud-service-mcs/records/L9WSPSMVQ85/representations/edm/versions/b17c4f60-70d0/files" * Retrieve: {"CLOUDID": "L9WSPSMVQ85", * "REPRESENTATIONNAME": "edm", * "VERSION": "b17c4f60-70d0", * "FILENAME": null * } * <p/> * From this string "http://ecloud.eanadev.org:8080/ecloud-service-mcs/records/L9WSPSMVQ85/representations/edm/versions/b17c4f60-70d0/files/file1" * Retrieve: {"CLOUDID": "L9WSPSMVQ85", * "REPRESENTATIONNAME": "edm", * "VERSION": "b17c4f60-70d0", * "FILENAME": "file1" * } * </p> * * @param uri Address of file/files * @return Map with indexes: CLOUDID, REPRESENTATIONNAME, VERSION, FILENAME(potentially null) */ public static Map<String, String> parseFileUri(String uri) { Pattern p = Pattern.compile(".*/records/([^/]+)/representations/([^/]+)/versions/([^/]+)/files/(.*)"); Matcher m = p.matcher(uri); if (m.find()) { Map<String, String> ret = new HashMap<>(); ret.put(ParamConstants.P_CLOUDID, m.group(1)); ret.put(ParamConstants.P_REPRESENTATIONNAME, m.group(2)); ret.put(ParamConstants.P_VER, m.group(3)); ret.put(ParamConstants.P_FILENAME, m.group(4)); return ret; } else { return null; } } /** * Client will use provided authorization header for all requests; * * @param headerValue authorization header value * @return */ public void useAuthorizationHeader(final String headerValue) { client.register(new ECloudBasicAuthFilter(headerValue)); } private InputStream copiedInputStream(InputStream originIS) throws IOException { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); int nRead; byte[] data = new byte[16384]; while ((nRead = originIS.read(data, 0, data.length)) != -1) { buffer.write(data, 0, nRead); } buffer.flush(); return new ByteArrayInputStream(buffer.toByteArray()); } private void closeResponse(Response response) { if (response != null) { response.close(); } } @Override protected void finalize() throws Throwable { client.close(); } }