/* * To change this license header, choose License Headers in Project Properties. * To change this template file, choose Tools | Templates * and open the template in the editor. */ package edu.harvard.iq.dataverse.api; import edu.harvard.iq.dataverse.BibtexCitation; import edu.harvard.iq.dataverse.DataFile; import edu.harvard.iq.dataverse.FileMetadata; import edu.harvard.iq.dataverse.DataFileServiceBean; import edu.harvard.iq.dataverse.DatasetVersion; import edu.harvard.iq.dataverse.DatasetVersionServiceBean; import edu.harvard.iq.dataverse.DatasetServiceBean; import edu.harvard.iq.dataverse.Dataverse; import edu.harvard.iq.dataverse.DataverseRequestServiceBean; import edu.harvard.iq.dataverse.DataverseServiceBean; import edu.harvard.iq.dataverse.DataverseSession; import edu.harvard.iq.dataverse.DataverseTheme; import edu.harvard.iq.dataverse.GuestbookResponse; import edu.harvard.iq.dataverse.GuestbookResponseServiceBean; import edu.harvard.iq.dataverse.PermissionServiceBean; import edu.harvard.iq.dataverse.authorization.Permission; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.PrivateUrlUser; import edu.harvard.iq.dataverse.authorization.users.GuestUser; import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.dataaccess.DataFileIO; import edu.harvard.iq.dataverse.dataaccess.DataFileZipper; import edu.harvard.iq.dataverse.dataaccess.FileAccessIO; import edu.harvard.iq.dataverse.dataaccess.OptionalAccessService; import edu.harvard.iq.dataverse.dataaccess.ImageThumbConverter; import edu.harvard.iq.dataverse.datavariable.DataVariable; import edu.harvard.iq.dataverse.datavariable.VariableServiceBean; import edu.harvard.iq.dataverse.export.DDIExportServiceBean; import edu.harvard.iq.dataverse.settings.SettingsServiceBean; import edu.harvard.iq.dataverse.util.SystemConfig; import edu.harvard.iq.dataverse.worldmapauth.WorldMapTokenServiceBean; import java.util.logging.Logger; import javax.ejb.EJB; import java.io.InputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.Properties; import java.util.logging.Level; import javax.inject.Inject; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.UriInfo; import javax.servlet.http.HttpServletResponse; import javax.ws.rs.NotFoundException; import javax.ws.rs.QueryParam; import javax.ws.rs.ServiceUnavailableException; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Response; import javax.ws.rs.core.StreamingOutput; /* Custom API exceptions [NOT YET IMPLEMENTED] import edu.harvard.iq.dataverse.api.exceptions.NotFoundException; import edu.harvard.iq.dataverse.api.exceptions.ServiceUnavailableException; import edu.harvard.iq.dataverse.api.exceptions.PermissionDeniedException; import edu.harvard.iq.dataverse.api.exceptions.AuthorizationRequiredException; */ /** * * @author Leonid Andreev * * The data (file) access API is based on the DVN access API v.1.0 (that came * with the v.3.* of the DVN app) and extended for DVN 4.0 to include some * extra fancy functionality, such as subsetting individual columns in tabular * data files and more. */ @Path("access") public class Access extends AbstractApiBean { private static final Logger logger = Logger.getLogger(Access.class.getCanonicalName()); @EJB DataFileServiceBean dataFileService; @EJB DatasetServiceBean datasetService; @EJB DatasetVersionServiceBean versionService; @EJB DataverseServiceBean dataverseService; @EJB VariableServiceBean variableService; @EJB SettingsServiceBean settingsService; @EJB SystemConfig systemConfig; @EJB DDIExportServiceBean ddiExportService; @EJB PermissionServiceBean permissionService; @Inject DataverseSession session; @EJB WorldMapTokenServiceBean worldMapTokenServiceBean; @Inject DataverseRequestServiceBean dvRequestService; @EJB GuestbookResponseServiceBean guestbookResponseService; private static final String API_KEY_HEADER = "X-Dataverse-key"; //@EJB // TODO: // versions? -- L.A. 4.0 beta 10 @Path("datafile/bundle/{fileId}") @GET @Produces({"application/zip"}) public BundleDownloadInstance datafileBundle(@PathParam("fileId") Long fileId, @QueryParam("key") String apiToken, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response) /*throws NotFoundException, ServiceUnavailableException, PermissionDeniedException, AuthorizationRequiredException*/ { DataFile df = dataFileService.find(fileId); if (df == null) { logger.warning("Access: datafile service could not locate a DataFile object for id "+fileId+"!"); throw new WebApplicationException(Response.Status.NOT_FOUND); } if (apiToken == null || apiToken.equals("")) { apiToken = headers.getHeaderString(API_KEY_HEADER); } // This will throw a WebApplicationException, with the correct // exit code, if access isn't authorized: checkAuthorization(df, apiToken); DownloadInfo dInfo = new DownloadInfo(df); BundleDownloadInstance downloadInstance = new BundleDownloadInstance(dInfo); FileMetadata fileMetadata = df.getFileMetadata(); DatasetVersion datasetVersion = df.getOwner().getLatestVersion(); downloadInstance.setFileCitationEndNote(datasetService.createCitationXML(datasetVersion, fileMetadata)); downloadInstance.setFileCitationRIS(datasetService.createCitationRIS(datasetVersion, fileMetadata)); downloadInstance.setFileCitationBibtex(new BibtexCitation(datasetVersion).toString()); ByteArrayOutputStream outStream = null; outStream = new ByteArrayOutputStream(); try { ddiExportService.exportDataFile( fileId, outStream, null, null); downloadInstance.setFileDDIXML(outStream.toString()); } catch (Exception ex) { // if we can't generate the DDI, it's ok; // we'll just generate the bundle without it. } return downloadInstance; } @Path("datafile/{fileId}") @GET @Produces({ "application/xml" }) public DownloadInstance datafile(@PathParam("fileId") Long fileId, @QueryParam("gbrecs") Boolean gbrecs, @QueryParam("key") String apiToken, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response) /*throws NotFoundException, ServiceUnavailableException, PermissionDeniedException, AuthorizationRequiredException*/ { DataFile df = dataFileService.find(fileId); GuestbookResponse gbr = null; /* if (gbrecs == null && df.isReleased()){ //commenting out for 4.6 SEK // gbr = guestbookResponseService.initDefaultGuestbookResponse(df.getOwner(), df, session); } */ if (df == null) { logger.warning("Access: datafile service could not locate a DataFile object for id "+fileId+"!"); throw new WebApplicationException(Response.Status.NOT_FOUND); } if (apiToken == null || apiToken.equals("")) { apiToken = headers.getHeaderString(API_KEY_HEADER); } // This will throw a WebApplicationException, with the correct // exit code, if access isn't authorized: checkAuthorization(df, apiToken); DownloadInfo dInfo = new DownloadInfo(df); if (dataFileService.thumbnailSupported(df)) { dInfo.addServiceAvailable(new OptionalAccessService("thumbnail", "image/png", "imageThumb=true", "Image Thumbnail (64x64)")); } if (df.isTabularData()) { String originalMimeType = df.getDataTable().getOriginalFileFormat(); dInfo.addServiceAvailable(new OptionalAccessService("original", originalMimeType, "format=original","Saved original (" + originalMimeType + ")")); dInfo.addServiceAvailable(new OptionalAccessService("R", "application/x-rlang-transport", "format=RData", "Data in R format")); dInfo.addServiceAvailable(new OptionalAccessService("preprocessed", "application/json", "format=prep", "Preprocessed data in JSON")); dInfo.addServiceAvailable(new OptionalAccessService("subset", "text/tab-separated-values", "variables=<LIST>", "Column-wise Subsetting")); } DownloadInstance downloadInstance = new DownloadInstance(dInfo); if (gbr != null){ downloadInstance.setGbr(gbr); downloadInstance.setDataverseRequestService(dvRequestService); downloadInstance.setCommand(engineSvc); } for (String key : uriInfo.getQueryParameters().keySet()) { String value = uriInfo.getQueryParameters().getFirst(key); if (downloadInstance.isDownloadServiceSupported(key, value)) { // this automatically sets the conversion parameters in // the download instance to key and value; // TODO: I should probably set these explicitly instead. if (downloadInstance.getConversionParam().equals("subset")) { String subsetParam = downloadInstance.getConversionParamValue(); String variableIdParams[] = subsetParam.split(","); if (variableIdParams != null && variableIdParams.length > 0) { logger.fine(variableIdParams.length + " tokens;"); for (int i = 0; i < variableIdParams.length; i++) { logger.fine("token: " + variableIdParams[i]); String token = variableIdParams[i].replaceFirst("^v", ""); Long variableId = null; try { variableId = new Long(token); } catch (NumberFormatException nfe) { variableId = null; } if (variableId != null) { logger.fine("attempting to look up variable id " + variableId); if (variableService != null) { DataVariable variable = variableService.find(variableId); if (variable != null) { if (downloadInstance.getExtraArguments() == null) { downloadInstance.setExtraArguments(new ArrayList<Object>()); } logger.fine("putting variable id "+variable.getId()+" on the parameters list of the download instance."); downloadInstance.getExtraArguments().add(variable); //if (!variable.getDataTable().getDataFile().getId().equals(sf.getId())) { //variableList.add(variable); //} } } else { logger.fine("variable service is null."); } } } } } break; } else { // Service unknown/not supported/bad arguments, etc.: // TODO: throw new ServiceUnavailableException(); } } /* * Provide content type header: * (this will be done by the InstanceWriter class - ?) */ /* Provide "Access-Control-Allow-Origin" header: * (may not be needed here... - that header was added specifically * to get the data exploration app to be able to access the metadata * API; may have been something specific to Vito's installation too * -- L.A.) */ response.setHeader("Access-Control-Allow-Origin", "*"); /* * Provide some browser-friendly headers: (?) */ //return retValue; return downloadInstance; } /* * Variants of the Access API calls for retrieving datafile-level * Metadata. */ // Metadata format defaults to DDI: @Path("datafile/{fileId}/metadata") @GET @Produces({"text/xml"}) public String tabularDatafileMetadata(@PathParam("fileId") Long fileId, @QueryParam("exclude") String exclude, @QueryParam("include") String include, @Context HttpHeaders header, @Context HttpServletResponse response) throws NotFoundException, ServiceUnavailableException /*, PermissionDeniedException, AuthorizationRequiredException*/ { return tabularDatafileMetadataDDI(fileId, exclude, include, header, response); } /* * This has been moved here, under /api/access, from the /api/meta hierarchy * which we are going to retire. */ @Path("datafile/{fileId}/metadata/ddi") @GET @Produces({"text/xml"}) public String tabularDatafileMetadataDDI(@PathParam("fileId") Long fileId, @QueryParam("exclude") String exclude, @QueryParam("include") String include, @Context HttpHeaders header, @Context HttpServletResponse response) throws NotFoundException, ServiceUnavailableException /*, PermissionDeniedException, AuthorizationRequiredException*/ { String retValue = ""; DataFile dataFile = null; //httpHeaders.add("Content-disposition", "attachment; filename=\"dataverse_files.zip\""); //httpHeaders.add("Content-Type", "application/zip; name=\"dataverse_files.zip\""); response.setHeader("Content-disposition", "attachment; filename=\"dataverse_files.zip\""); dataFile = dataFileService.find(fileId); if (dataFile == null) { throw new NotFoundException(); } String fileName = dataFile.getFileMetadata().getLabel().replaceAll("\\.tab$", "-ddi.xml"); response.setHeader("Content-disposition", "attachment; filename=\""+fileName+"\""); response.setHeader("Content-Type", "application/xml; name=\""+fileName+"\""); ByteArrayOutputStream outStream = null; outStream = new ByteArrayOutputStream(); try { ddiExportService.exportDataFile( fileId, outStream, exclude, include); retValue = outStream.toString(); } catch (Exception e) { // For whatever reason we've failed to generate a partial // metadata record requested. // We return Service Unavailable. throw new ServiceUnavailableException(); } response.setHeader("Access-Control-Allow-Origin", "*"); return retValue; } @Path("variable/{varId}/metadata/ddi") @GET @Produces({ "application/xml" }) public String dataVariableMetadataDDI(@PathParam("varId") Long varId, @QueryParam("exclude") String exclude, @QueryParam("include") String include, @Context HttpHeaders header, @Context HttpServletResponse response) /*throws NotFoundException, ServiceUnavailableException, PermissionDeniedException, AuthorizationRequiredException*/ { String retValue = ""; ByteArrayOutputStream outStream = null; try { outStream = new ByteArrayOutputStream(); ddiExportService.exportDataVariable( varId, outStream, exclude, include); } catch (Exception e) { // For whatever reason we've failed to generate a partial // metadata record requested. We simply return an empty string. return retValue; } retValue = outStream.toString(); response.setHeader("Access-Control-Allow-Origin", "*"); return retValue; } /* * "Preprocessed data" metadata format: * (this was previously provided as a "format conversion" option of the * file download form of the access API call) */ @Path("datafile/{fileId}/metadata/preprocessed") @GET @Produces({"text/xml"}) public DownloadInstance tabularDatafileMetadataPreprocessed(@PathParam("fileId") Long fileId, @QueryParam("key") String apiToken, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response) throws ServiceUnavailableException { DataFile df = dataFileService.find(fileId); if (df == null) { logger.warning("Access: datafile service could not locate a DataFile object for id "+fileId+"!"); throw new WebApplicationException(Response.Status.NOT_FOUND); } if (apiToken == null || apiToken.equals("")) { apiToken = headers.getHeaderString(API_KEY_HEADER); } // This will throw a WebApplicationException, with the correct // exit code, if access isn't authorized: checkAuthorization(df, apiToken); DownloadInfo dInfo = new DownloadInfo(df); if (df.isTabularData()) { dInfo.addServiceAvailable(new OptionalAccessService("preprocessed", "application/json", "format=prep", "Preprocessed data in JSON")); } else { throw new ServiceUnavailableException("Preprocessed Content Metadata requested on a non-tabular data file."); } DownloadInstance downloadInstance = new DownloadInstance(dInfo); if (downloadInstance.isDownloadServiceSupported("format", "prep")) { logger.fine("Preprocessed data for tabular file "+fileId); } response.setHeader("Access-Control-Allow-Origin", "*"); return downloadInstance; } /* * API method for downloading zipped bundles of multiple files: */ @Path("datafiles/{fileIds}") @GET @Produces({"application/zip"}) public /*ZippedDownloadInstance*/ Response datafiles(@PathParam("fileIds") String fileIds, @QueryParam("key") String apiTokenParam, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response) throws WebApplicationException /*throws NotFoundException, ServiceUnavailableException, PermissionDeniedException, AuthorizationRequiredException*/ { // create a Download Instance without, without a primary Download Info object: //ZippedDownloadInstance downloadInstance = new ZippedDownloadInstance(); long setLimit = systemConfig.getZipDownloadLimit(); if (!(setLimit > 0L)) { setLimit = DataFileZipper.DEFAULT_ZIPFILE_LIMIT; } long zipDownloadSizeLimit = setLimit; logger.fine("setting zip download size limit to " + zipDownloadSizeLimit + " bytes."); if (fileIds == null || fileIds.equals("")) { throw new WebApplicationException(Response.Status.BAD_REQUEST); } String apiToken = (apiTokenParam == null || apiTokenParam.equals("")) ? headers.getHeaderString(API_KEY_HEADER) : apiTokenParam; StreamingOutput stream = new StreamingOutput() { @Override public void write(OutputStream os) throws IOException, WebApplicationException { String fileIdParams[] = fileIds.split(","); DataFileZipper zipper = null; boolean accessToUnrestrictedFileAuthorized = false; String fileManifest = ""; long sizeTotal = 0L; if (fileIdParams != null && fileIdParams.length > 0) { logger.fine(fileIdParams.length + " tokens;"); for (int i = 0; i < fileIdParams.length; i++) { logger.fine("token: " + fileIdParams[i]); Long fileId = null; try { fileId = new Long(fileIdParams[i]); } catch (NumberFormatException nfe) { fileId = null; } if (fileId != null) { logger.fine("attempting to look up file id " + fileId); DataFile file = dataFileService.find(fileId); if (file != null) { if ((accessToUnrestrictedFileAuthorized && !file.isRestricted()) || isAccessAuthorized(file, apiToken)) { if (!file.isRestricted()) { accessToUnrestrictedFileAuthorized = true; } logger.fine("adding datafile (id=" + file.getId() + ") to the download list of the ZippedDownloadInstance."); //downloadInstance.addDataFile(file); if (zipper == null) { // This is the first file we can serve - so we now know that we are going to be able // to produce some output. zipper = new DataFileZipper(os); zipper.setFileManifest(fileManifest); response.setHeader("Content-disposition", "attachment; filename=\"dataverse_files.zip\""); response.setHeader("Content-Type", "application/zip; name=\"dataverse_files.zip\""); } if (sizeTotal + file.getFilesize() < zipDownloadSizeLimit) { sizeTotal += zipper.addFileToZipStream(file); } else { String fileName = file.getFileMetadata().getLabel(); String mimeType = file.getContentType(); zipper.addToManifest(fileName + " (" + mimeType + ") " + " skipped because the total size of the download bundle exceeded the limit of " + zipDownloadSizeLimit + " bytes.\r\n"); } } else { if (zipper == null) { fileManifest = fileManifest + file.getFileMetadata().getLabel() + " IS RESTRICTED AND CANNOT BE DOWNLOADED\r\n"; } else { zipper.addToManifest(file.getFileMetadata().getLabel() + " IS RESTRICTED AND CANNOT BE DOWNLOADED\r\n"); } } } else { // Or should we just drop it and make a note in the Manifest? throw new WebApplicationException(Response.Status.NOT_FOUND); } } } } else { throw new WebApplicationException(Response.Status.BAD_REQUEST); } if (zipper == null) { // If the DataFileZipper object is still NULL, it means that // there were file ids supplied - but none of the corresponding // files were accessible for this user. // In which casew we don't bother generating any output, and // just give them a 403: throw new WebApplicationException(Response.Status.FORBIDDEN); } // This will add the generated File Manifest to the zipped output, // then flush and close the stream: zipper.finalizeZipStream(); //os.flush(); //os.close(); } }; return Response.ok(stream).build(); } @Path("tempPreview/{fileSystemId}") @GET @Produces({"image/png"}) public InputStream tempPreview(@PathParam("fileSystemId") String fileSystemId, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response) /*throws NotFoundException, ServiceUnavailableException, PermissionDeniedException, AuthorizationRequiredException*/ { String filesRootDirectory = System.getProperty("dataverse.files.directory"); if (filesRootDirectory == null || filesRootDirectory.equals("")) { filesRootDirectory = "/tmp/files"; } String fileSystemName = filesRootDirectory + "/temp/" + fileSystemId; String mimeTypeParam = uriInfo.getQueryParameters().getFirst("mimetype"); String imageThumbFileName = null; if ("application/pdf".equals(mimeTypeParam)) { imageThumbFileName = ImageThumbConverter.generatePDFThumb(fileSystemName); } else { imageThumbFileName = ImageThumbConverter.generateImageThumb(fileSystemName); } // TODO: // double-check that this temporary preview thumbnail gets deleted // once the file is saved "for real". // (or maybe we shouldn't delete it - but instead move it into the // permanent location... so that it doesn't have to be generated again?) // -- L.A. Aug. 21 2014 // Update: // the temporary thumbnail file does get cleaned up now; // but yeay, maybe we should be saving it permanently instead, as // the above suggested... // -- L.A. Feb. 28 2015 if (imageThumbFileName == null) { return null; } /* removing the old, non-vector default icon: imageThumbFileName = getWebappImageResource(DEFAULT_FILE_ICON); } */ InputStream in; try { in = new FileInputStream(imageThumbFileName); } catch (Exception ex) { return null; } return in; } @Path("fileCardImage/{fileId}") @GET @Produces({ "image/png" }) public InputStream fileCardImage(@PathParam("fileId") Long fileId, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response) /*throws NotFoundException, ServiceUnavailableException, PermissionDeniedException, AuthorizationRequiredException*/ { DataFile df = dataFileService.find(fileId); if (df == null) { logger.warning("Preview: datafile service could not locate a DataFile object for id "+fileId+"!"); return null; } DataFileIO thumbnailDataAccess = null; try { DataFileIO dataAccess = df.getAccessObject(); if (dataAccess != null && dataAccess.isLocalFile()) { dataAccess.open(); if ("application/pdf".equalsIgnoreCase(df.getContentType()) || df.isImage() || "application/zipped-shapefile".equalsIgnoreCase(df.getContentType())) { thumbnailDataAccess = ImageThumbConverter.getImageThumb((FileAccessIO) dataAccess, 48); } } } catch (IOException ioEx) { return null; } if (thumbnailDataAccess != null && thumbnailDataAccess.getInputStream() != null) { return thumbnailDataAccess.getInputStream(); } return null; } // Note: // the Dataverse page is no longer using this method. @Path("dsCardImage/{versionId}") @GET @Produces({ "image/png" }) public InputStream dsCardImage(@PathParam("versionId") Long versionId, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response) /*throws NotFoundException, ServiceUnavailableException, PermissionDeniedException, AuthorizationRequiredException*/ { DatasetVersion datasetVersion = versionService.find(versionId); if (datasetVersion == null) { logger.warning("Preview: Version service could not locate a DatasetVersion object for id "+versionId+"!"); return null; } //String imageThumbFileName = null; DataFileIO thumbnailDataAccess = null; // First, check if this dataset has a designated thumbnail image: if (datasetVersion.getDataset() != null) { DataFile logoDataFile = datasetVersion.getDataset().getThumbnailFile(); if (logoDataFile != null) { try { DataFileIO dataAccess = logoDataFile.getAccessObject(); if (dataAccess != null && dataAccess.isLocalFile()) { dataAccess.open(); thumbnailDataAccess = ImageThumbConverter.getImageThumb((FileAccessIO) dataAccess, 48); } } catch (IOException ioEx) { thumbnailDataAccess = null; } } // If not, we'll try to use one of the files in this dataset version: /* if (thumbnailDataAccess == null) { if (!datasetVersion.getDataset().isHarvested()) { thumbnailDataAccess = getThumbnailForDatasetVersion(datasetVersion); } } if (thumbnailDataAccess != null && thumbnailDataAccess.getInputStream() != null) { return thumbnailDataAccess.getInputStream(); } */ } return null; } @Path("dvCardImage/{dataverseId}") @GET @Produces({ "image/png" }) public InputStream dvCardImage(@PathParam("dataverseId") Long dataverseId, @Context UriInfo uriInfo, @Context HttpHeaders headers, @Context HttpServletResponse response) /*throws NotFoundException, ServiceUnavailableException, PermissionDeniedException, AuthorizationRequiredException*/ { logger.info("entering dvCardImage"); Dataverse dataverse = dataverseService.find(dataverseId); if (dataverse == null) { logger.warning("Preview: Version service could not locate a DatasetVersion object for id "+dataverseId+"!"); return null; } String imageThumbFileName = null; // First, check if the dataverse has a defined logo: if (dataverse.getDataverseTheme()!=null && dataverse.getDataverseTheme().getLogo() != null && !dataverse.getDataverseTheme().getLogo().equals("")) { File dataverseLogoFile = getLogo(dataverse); if (dataverseLogoFile != null) { logger.info("dvCardImage: logo file found"); String logoThumbNailPath = null; InputStream in = null; try { if (dataverseLogoFile.exists()) { logoThumbNailPath = ImageThumbConverter.generateImageThumb(dataverseLogoFile.getAbsolutePath(), 48); if (logoThumbNailPath != null) { in = new FileInputStream(logoThumbNailPath); } } } catch (Exception ex) { in = null; } if (in != null) { logger.info("dvCardImage: successfully obtained thumbnail for dataverse logo."); return in; } } } // If there's no uploaded logo for this dataverse, go through its // [released] datasets and see if any of them have card images: // TODO: figure out if we want to be doing this! // (efficiency considerations...) -- L.A. 4.0 // And we definitely don't want to be doing this for harvested // dataverses: /* DataFileIO thumbnailDataAccess = null; if (!dataverse.isHarvested()) { for (Dataset dataset : datasetService.findPublishedByOwnerId(dataverseId)) { logger.info("dvCardImage: checking dataset "+dataset.getGlobalId()); if (dataset != null) { DatasetVersion releasedVersion = dataset.getReleasedVersion(); logger.info("dvCardImage: obtained released version "+releasedVersion.getTitle()); thumbnailDataAccess = getThumbnailForDatasetVersion(releasedVersion); if (thumbnailDataAccess != null) { logger.info("dvCardImage: obtained thumbnail for the version."); break; } } } } if (thumbnailDataAccess != null && thumbnailDataAccess.getInputStream() != null) { return thumbnailDataAccess.getInputStream(); } */ return null; } // helper methods: // What the method below does - going through all the files in the version - // is too expensive! Instead we are now selecting an available thumbnail and // giving the dataset card a direct link to that file thumbnail. -- L.A., 4.2.2 /* private DataFileIO getThumbnailForDatasetVersion(DatasetVersion datasetVersion) { logger.info("entering getThumbnailForDatasetVersion()"); DataFileIO thumbnailDataAccess = null; if (datasetVersion != null) { List<FileMetadata> fileMetadatas = datasetVersion.getFileMetadatas(); for (FileMetadata fileMetadata : fileMetadatas) { DataFile dataFile = fileMetadata.getDataFile(); logger.info("looking at file "+fileMetadata.getLabel()+" , file type "+dataFile.getContentType()); if (dataFile != null && dataFile.isImage()) { try { DataFileIO dataAccess = dataFile.getAccessObject(); if (dataAccess != null && dataAccess.isLocalFile()) { dataAccess.open(); thumbnailDataAccess = ImageThumbConverter.getImageThumb((FileAccessIO) dataAccess, 48); } } catch (IOException ioEx) { thumbnailDataAccess = null; } } if (thumbnailDataAccess != null) { logger.info("successfully generated thumbnail, returning."); break; } } } return thumbnailDataAccess; } */ // TODO: // put this method into the dataverseservice; use it there // -- L.A. 4.0 beta14 private File getLogo(Dataverse dataverse) { if (dataverse.getId() == null) { return null; } DataverseTheme theme = dataverse.getDataverseTheme(); if (theme != null && theme.getLogo() != null && !theme.getLogo().equals("")) { Properties p = System.getProperties(); String domainRoot = p.getProperty("com.sun.aas.instanceRoot"); if (domainRoot != null && !"".equals(domainRoot)) { return new File (domainRoot + File.separator + "docroot" + File.separator + "logos" + File.separator + dataverse.getLogoOwnerId() + File.separator + theme.getLogo()); } } return null; } /* removing: private String getWebappImageResource(String imageName) { String imageFilePath = null; String persistenceFilePath = null; java.net.URL persistenceFileUrl = Thread.currentThread().getContextClassLoader().getResource("META-INF/persistence.xml"); if (persistenceFileUrl != null) { persistenceFilePath = persistenceFileUrl.getDataFile(); if (persistenceFilePath != null) { persistenceFilePath = persistenceFilePath.replaceFirst("/[^/]*$", "/"); imageFilePath = persistenceFilePath + "../../../resources/images/" + imageName; return imageFilePath; } logger.warning("Null file path representation of the location of persistence.xml in the webapp root directory!"); } else { logger.warning("Could not find the location of persistence.xml in the webapp root directory!"); } return null; } */ // checkAuthorization is a convenience method; it calls the boolean method // isAccessAuthorized(), the actual workhorse, tand throws a 403 exception if not. private void checkAuthorization(DataFile df, String apiToken) throws WebApplicationException { if (!isAccessAuthorized(df, apiToken)) { throw new WebApplicationException(Response.Status.FORBIDDEN); } } private boolean isAccessAuthorized(DataFile df, String apiToken) { // First, check if the file belongs to a released Dataset version: boolean published = false; // TODO: // this very likely creates a ton of queries; put some thought into // optimizing this stuff? -- 4.2.1 // // update: it appears that we can finally trust the dvObject.isReleased() // method; so all this monstrous crawling through the filemetadatas, // below, may not be necessary anymore! - need to verify... L.A. 10.21.2015 // update: NO! we still can't just trust .isReleased(), for these purposes! // TODO: explain why. L.A. 10.29.2015 if (df.getOwner().getReleasedVersion() != null) { //logger.fine("file belongs to a dataset with a released version."); if (df.getOwner().getReleasedVersion().getFileMetadatas() != null) { //logger.fine("going through the list of filemetadatas that belong to the released version."); for (FileMetadata fm : df.getOwner().getReleasedVersion().getFileMetadatas()) { if (df.equals(fm.getDataFile())) { //logger.fine("found a match!"); published = true; } } } } // TODO: (IMPORTANT!) // Business logic like this should NOT be maintained in individual // application fragments. // At the moment it is duplicated here, and inside the Dataset page. // There are also stubs for file-level permission lookups and caching // inside Gustavo's view-scoped PermissionsWrapper. // All this logic needs to be moved to the PermissionServiceBean where it will be // centrally maintained; with the PermissionsWrapper providing // efficient cached lookups to the pages (that often need to make // repeated lookups on the same files). Care will need to be taken // to preserve the slight differences in logic utilized by the page and // this Access call (the page checks the restriction flag on the // filemetadata, not the datafile - as it needs to reflect the permission // status of the file in the version history). // I will open a 4.[34] ticket. // // -- L.A. 4.2.1 // We don't need to check permissions on files that are // from released Dataset versions and not restricted: boolean restricted = false; if (df.isRestricted()) { restricted = true; } else { // There is also a special case of a restricted file that only exists // in a draft version (i.e., a new file, that hasn't been published yet). // Such files must be considered restricted, for access purposes. I.e., // users with no download access to this particular file, but with the // permission to ViewUnpublished on the dataset, should NOT be allowed // to download it. // Up until 4.2.1 restricting unpublished files was only restricting them // in their Draft version fileMetadata, but not in the DataFile object. // (this is what we still want to do for published files; restricting those // only restricts them in the new draft FileMetadata; until it becomes the // published version, the restriction flag on the DataFile is what governs // the download authorization). //if (!published && df.getOwner().getVersions().size() == 1 && df.getOwner().getLatestVersion().isDraft()) { // !df.isReleased() really means just this: new file, only exists in a Draft version! if (!df.isReleased()) { if (df.getFileMetadata().isRestricted()) { restricted = true; } } } if (!restricted) { // And if they are not published, they can still be downloaded, if the user // has the permission to view unpublished versions! (this case will // be handled below) if (published) { return true; } } User user = null; /** * Authentication/authorization: * * note that the fragment below - that retrieves the session object * and tries to find the user associated with the session - is really * for logging/debugging purposes only; for practical purposes, it * would be enough to just call "permissionService.on(df).has(Permission.DownloadFile)" * and the method does just that, tries to authorize for the user in * the current session (or guest user, if no session user is available): */ if (session != null) { if (session.getUser() != null) { if (session.getUser().isAuthenticated()) { user = (AuthenticatedUser) session.getUser(); } else { logger.fine("User associated with the session is not an authenticated user."); if (session.getUser() instanceof PrivateUrlUser) { logger.fine("User associated with the session is a PrivateUrlUser user."); user = session.getUser(); } if (session.getUser() instanceof GuestUser) { logger.fine("User associated with the session is indeed a guest user."); } } } else { logger.fine("No user associated with the session."); } } else { logger.fine("Session is null."); } User apiTokenUser = null; if ((apiToken != null)&&(apiToken.length()!=64)) { // We'll also try to obtain the user information from the API token, // if supplied: try { logger.fine("calling apiTokenUser = findUserOrDie()..."); apiTokenUser = findUserOrDie(); } catch (WrappedResponse wr) { logger.log(Level.FINE, "Message from findUserOrDie(): {0}", wr.getMessage()); } if (apiTokenUser == null) { logger.warning("API token-based auth: Unable to find a user with the API token provided."); } } // OK, let's revisit the case of non-restricted files, this time in // an unpublished version: // (if (published) was already addressed above) if (!restricted) { // If the file is not published, they can still download the file, if the user // has the permission to view unpublished versions: if ( user != null ) { // used in JSF context if (permissionService.requestOn(dvRequestService.getDataverseRequest(), df.getOwner()).has(Permission.ViewUnpublishedDataset)) { // it's not unthinkable, that a null user (i.e., guest user) could be given // the ViewUnpublished permission! logger.log(Level.FINE, "Session-based auth: user {0} has access rights on the non-restricted, unpublished datafile.", user.getIdentifier()); return true; } } if (apiTokenUser != null) { // used in an API context if (permissionService.requestOn( createDataverseRequest(apiTokenUser), df.getOwner()).has(Permission.ViewUnpublishedDataset)) { logger.log(Level.FINE, "Session-based auth: user {0} has access rights on the non-restricted, unpublished datafile.", apiTokenUser.getIdentifier()); return true; } } // last option - guest user in either contexts // Guset user is impled by the code above. if ( permissionService.requestOn(dvRequestService.getDataverseRequest(), df.getOwner()).has(Permission.ViewUnpublishedDataset) ) { return true; } // We don't want to return false just yet. // If all else fails, we'll want to use the special WorldMapAuth // token authentication before we give up. //return false; } else { // OK, this is a restricted file. boolean hasAccessToRestrictedBySession = false; boolean hasAccessToRestrictedByToken = false; if (permissionService.on(df).has(Permission.DownloadFile)) { // Note: PermissionServiceBean.on(Datafile df) will obtain the // User from the Session object, just like in the code fragment // above. That's why it's not passed along as an argument. hasAccessToRestrictedBySession = true; } else if (apiTokenUser != null && permissionService.requestOn(createDataverseRequest(apiTokenUser), df).has(Permission.DownloadFile)) { hasAccessToRestrictedByToken = true; } if (hasAccessToRestrictedBySession || hasAccessToRestrictedByToken) { if (published) { if (hasAccessToRestrictedBySession) { if (user != null) { logger.log(Level.FINE, "Session-based auth: user {0} is granted access to the restricted, published datafile.", user.getIdentifier()); } else { logger.fine("Session-based auth: guest user is granted access to the restricted, published datafile."); } } else { logger.log(Level.FINE, "Token-based auth: user {0} is granted access to the restricted, published datafile.", apiTokenUser.getIdentifier()); } return true; } else { // if the file is NOT published, we will let them download the // file ONLY if they also have the permission to view // unpublished versions: // Note that the code below does not allow a case where it is the // session user that has the permission on the file, and the API token // user with the ViewUnpublished permission, or vice versa! if (hasAccessToRestrictedBySession) { if (permissionService.on(df.getOwner()).has(Permission.ViewUnpublishedDataset)) { if (user != null) { logger.log(Level.FINE, "Session-based auth: user {0} is granted access to the restricted, unpublished datafile.", user.getIdentifier()); } else { logger.fine("Session-based auth: guest user is granted access to the restricted, unpublished datafile."); } return true; } } else { if (apiTokenUser != null && permissionService.requestOn(createDataverseRequest(apiTokenUser), df.getOwner()).has(Permission.ViewUnpublishedDataset)) { logger.log(Level.FINE, "Token-based auth: user {0} is granted access to the restricted, unpublished datafile.", apiTokenUser.getIdentifier()); return true; } } } } } // And if all that failed, we'll still check if the download can be authorized based // on the special WorldMap token: if ((apiToken != null)&&(apiToken.length()==64)){ /* WorldMap token check - WorldMap tokens are 64 chars in length - Use the worldMapTokenServiceBean to verify token and check permissions against the requested DataFile */ if (!(this.worldMapTokenServiceBean.isWorldMapTokenAuthorizedForDataFileDownload(apiToken, df))){ return false; } // Yes! User may access file // logger.fine("WorldMap token-based auth: Token is valid for the requested datafile"); return true; } else if ((apiToken != null)&&(apiToken.length()!=64)) { // Will try to obtain the user information from the API token, // if supplied: try { logger.fine("calling user = findUserOrDie()..."); user = findUserOrDie(); } catch (WrappedResponse wr) { logger.log(Level.FINE, "Message from findUserOrDie(): {0}", wr.getMessage()); } if (user == null) { logger.warning("API token-based auth: Unable to find a user with the API token provided."); return false; } if (permissionService.requestOn(createDataverseRequest(user), df).has(Permission.DownloadFile)) { if (published) { logger.log(Level.FINE, "API token-based auth: User {0} has rights to access the datafile.", user.getIdentifier()); return true; } else { // if the file is NOT published, we will let them download the // file ONLY if they also have the permission to view // unpublished versions: if (permissionService.requestOn(createDataverseRequest(user), df.getOwner()).has(Permission.ViewUnpublishedDataset)) { logger.log(Level.FINE, "API token-based auth: User {0} has rights to access the (unpublished) datafile.", user.getIdentifier()); return true; } else { logger.log(Level.FINE, "API token-based auth: User {0} is not authorized to access the (unpublished) datafile.", user.getIdentifier()); } } } else { logger.log(Level.FINE, "API token-based auth: User {0} is not authorized to access the datafile.", user.getIdentifier()); } return false; } if (user != null) { logger.log(Level.FINE, "Session-based auth: user {0} has NO access rights on the requested datafile.", user.getIdentifier()); } if (apiTokenUser != null) { logger.log(Level.FINE, "Token-based auth: user {0} has NO access rights on the requested datafile.", apiTokenUser.getIdentifier()); } if (user == null && apiTokenUser == null) { logger.fine("Unauthenticated access: No guest access to the datafile."); } return false; } }