• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

CeON / dataverse / 1370

26 Jun 2024 06:59AM UTC coverage: 25.503% (-0.004%) from 25.507%
1370

push

jenkins

web-flow
Closes #2496: Make the banner link optional (#2503)

17782 of 69725 relevant lines covered (25.5%)

0.26 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/dataverse-webapp/src/main/java/edu/harvard/iq/dataverse/dataset/tab/DatasetFilesTab.java
1
package edu.harvard.iq.dataverse.dataset.tab;
2

3
import com.google.common.collect.Lists;
4
import edu.harvard.iq.dataverse.DataFileServiceBean;
5
import edu.harvard.iq.dataverse.DataverseRequestServiceBean;
6
import edu.harvard.iq.dataverse.DataverseSession;
7
import edu.harvard.iq.dataverse.EjbDataverseEngine;
8
import edu.harvard.iq.dataverse.PermissionServiceBean;
9
import edu.harvard.iq.dataverse.PermissionsWrapper;
10
import edu.harvard.iq.dataverse.common.BundleUtil;
11
import edu.harvard.iq.dataverse.dataaccess.ImageThumbConverter;
12
import edu.harvard.iq.dataverse.datacapturemodule.DataCaptureModuleUtil;
13
import edu.harvard.iq.dataverse.datacapturemodule.ScriptRequestResponse;
14
import edu.harvard.iq.dataverse.datafile.file.FileMetadataService;
15
import edu.harvard.iq.dataverse.datafile.file.dto.LazyFileMetadataModel;
16
import edu.harvard.iq.dataverse.datafile.page.FileDownloadHelper;
17
import edu.harvard.iq.dataverse.datafile.page.FileDownloadRequestHelper;
18
import edu.harvard.iq.dataverse.datafile.page.RequestedDownloadType;
19
import edu.harvard.iq.dataverse.dataset.EmbargoAccessService;
20
import edu.harvard.iq.dataverse.engine.command.exception.CommandException;
21
import edu.harvard.iq.dataverse.engine.command.exception.IllegalCommandException;
22
import edu.harvard.iq.dataverse.engine.command.impl.CreateNewDatasetCommand;
23
import edu.harvard.iq.dataverse.engine.command.impl.RequestRsyncScriptCommand;
24
import edu.harvard.iq.dataverse.engine.command.impl.UpdateDatasetVersionCommand;
25
import edu.harvard.iq.dataverse.externaltools.ExternalToolServiceBean;
26
import edu.harvard.iq.dataverse.guestbook.GuestbookResponseDialog;
27
import edu.harvard.iq.dataverse.guestbook.GuestbookResponseServiceBean;
28
import edu.harvard.iq.dataverse.license.TermsOfUseFormMapper;
29
import edu.harvard.iq.dataverse.mail.confirmemail.ConfirmEmailServiceBean;
30
import edu.harvard.iq.dataverse.persistence.datafile.DataFile;
31
import edu.harvard.iq.dataverse.persistence.datafile.DataFileCategory;
32
import edu.harvard.iq.dataverse.persistence.datafile.DataFileTag;
33
import edu.harvard.iq.dataverse.persistence.datafile.ExternalTool;
34
import edu.harvard.iq.dataverse.persistence.datafile.FileMetadata;
35
import edu.harvard.iq.dataverse.persistence.datafile.license.FileTermsOfUse;
36
import edu.harvard.iq.dataverse.persistence.datafile.license.TermsOfUseForm;
37
import edu.harvard.iq.dataverse.persistence.dataset.Dataset;
38
import edu.harvard.iq.dataverse.persistence.dataset.DatasetLock;
39
import edu.harvard.iq.dataverse.persistence.dataset.DatasetVersion;
40
import edu.harvard.iq.dataverse.persistence.user.AuthenticatedUser;
41
import edu.harvard.iq.dataverse.search.SearchServiceBean.SortOrder;
42
import edu.harvard.iq.dataverse.settings.SettingsServiceBean;
43
import edu.harvard.iq.dataverse.util.FileSortFieldAndOrder;
44
import edu.harvard.iq.dataverse.util.FileUtil;
45
import edu.harvard.iq.dataverse.util.JsfHelper;
46
import edu.harvard.iq.dataverse.util.PrimefacesUtil;
47
import edu.harvard.iq.dataverse.util.StringUtil;
48
import edu.harvard.iq.dataverse.validation.DatasetFieldValidationService;
49
import edu.harvard.iq.dataverse.validation.field.FieldValidationResult;
50
import io.vavr.Tuple;
51
import io.vavr.Tuple2;
52
import org.apache.commons.lang3.StringUtils;
53
import org.omnifaces.cdi.ViewScoped;
54
import org.primefaces.component.datatable.DataTable;
55
import org.primefaces.event.SelectEvent;
56
import org.primefaces.event.ToggleSelectEvent;
57
import org.primefaces.event.UnselectEvent;
58
import org.primefaces.event.data.PageEvent;
59

60
import javax.ejb.EJBException;
61
import javax.faces.context.FacesContext;
62
import javax.inject.Inject;
63
import javax.inject.Named;
64
import javax.servlet.ServletOutputStream;
65
import javax.servlet.http.HttpServletResponse;
66
import javax.validation.ConstraintViolation;
67
import java.io.IOException;
68
import java.io.Serializable;
69
import java.text.SimpleDateFormat;
70
import java.util.ArrayList;
71
import java.util.Collection;
72
import java.util.HashMap;
73
import java.util.HashSet;
74
import java.util.List;
75
import java.util.Map;
76
import java.util.Set;
77
import java.util.logging.Level;
78
import java.util.logging.Logger;
79
import java.util.stream.Collectors;
80

81
import static java.util.stream.Collectors.joining;
82
import static java.util.stream.Collectors.toList;
83

84
@ViewScoped
×
85
@Named("datasetFilesTab")
86
public class DatasetFilesTab implements Serializable {
87

88
    private static final Logger logger = Logger.getLogger(DatasetFilesTab.class.getCanonicalName());
×
89

90
    public static final String DOWNLOAD_POPUP_DIALOG = "downloadPopup";
91
    public static final String SELECT_FILES_FOR_DOWNLAOD_DIALOG = "selectFilesForDownload";
92
    public static final String DOWNLOAD_MIXED_DIALOG = "downloadMixed";
93
    public static final String DOWNLOAD_INVALID_DIALOG = "downloadInvalid";
94

95
    public static final String SELECT_FILES_FOR_REQUEST_ACCESS_DIALOG = "selectFilesForRequestAccess";
96
    public static final String REQUEST_ACCESS_DIALOG = "requestAccessPopup";
97

98

99
    private DataFileServiceBean datafileService;
100
    private GuestbookResponseServiceBean guestbookResponseService;
101
    private ExternalToolServiceBean externalToolService;
102
    private EjbDataverseEngine commandEngine;
103
    private FileDownloadHelper fileDownloadHelper;
104
    private FileDownloadRequestHelper fileDownloadRequestHelper;
105
    private FileMetadataService fileMetadataService;
106
    private TermsOfUseFormMapper termsOfUseFormMapper;
107
    private EmbargoAccessService embargoAccess;
108
    private ImageThumbConverter imageThumbConverter;
109

110
    private PermissionServiceBean permissionService;
111
    private PermissionsWrapper permissionsWrapper;
112
    private SettingsServiceBean settingsService;
113

114
    private DataverseSession session;
115
    private DataverseRequestServiceBean dvRequestService;
116
    private DatasetFilesTabFacade datasetFilesTabFacade;
117

118
    private RequestedDownloadType requestedDownloadType;
119
    private GuestbookResponseDialog guestbookResponseDialog;
120
    private ConfirmEmailServiceBean confirmEmailService;
121
    private DatasetFieldValidationService fieldValidationService;
122

123
    private Dataset dataset;
124
    private DatasetVersion workingVersion;
125

126
    private Boolean hasTabular = false;
×
127
    /**
128
     * In the file listing, the page the user is on. This is zero-indexed so if
129
     * the user clicks page 2 in the UI, this will be 1.
130
     */
131
    private int filePaginatorPage;
132
    private int rowsPerPage;
133

134
    private List<FileMetadata> selectedFileMetadataForView = new ArrayList<>();
×
135

136
    private HashSet<Long> selectedFileIds = new HashSet<>();
×
137

138
    private LazyFileMetadataModel fileMetadatasSearch;
139

140
    private boolean selectAllFiles;
141

142
    private String fileLabelSearchTerm;
143

144
    private Integer fileSize;
145

146
    /**
147
     * The contents of the script.
148
     */
149
    private String rsyncScript = "";
×
150

151
    private String rsyncScriptFilename;
152

153

154
    private Map<Long, String> datafileThumbnailsMap = new HashMap<>();
×
155

156
    private List<ExternalTool> configureTools = new ArrayList<>();
×
157
    private List<ExternalTool> exploreTools = new ArrayList<>();
×
158
    private List<ExternalTool> previewTools = new ArrayList<>();
×
159
    private Map<Long, List<ExternalTool>> configureToolsByFileId = new HashMap<>();
×
160
    private Map<Long, List<ExternalTool>> exploreToolsByFileId = new HashMap<>();
×
161
    private Map<Long, List<ExternalTool>> previewToolsByFileId = new HashMap<>();
×
162
    private DataTable fileDataTable;
163

164
    private List<FileMetadata> selectedDownloadableFiles;
165

166
    private List<String> selectedNonDownloadableFiles;
167

168
    private boolean downloadOriginal = false;
×
169

170
    private List<String> categoriesByName;
171

172
    private boolean bulkFileDeleteInProgress = false;
×
173

174
    private List<DataFile> filesToBeDeleted = new ArrayList<>();
×
175

176
    private Boolean lockedFromDownloadVar;
177
    private Boolean lockedFromEditsVar;
178
    private boolean lockedDueToDcmUpload;
179

180
    private boolean fileAccessRequestMultiButtonRequired;
181
    private boolean fileAccessRequestMultiSignUpButtonRequired;
182
    private boolean downloadButtonAvailable;
183
    private boolean csvDownloadAvailable;
184

185
    // -------------------- CONSTRUCTORS --------------------
186

187
    @Deprecated
188
    public DatasetFilesTab() { }
×
189

190
    @Inject
191
    public DatasetFilesTab(FileDownloadHelper fileDownloadHelper, DataFileServiceBean datafileService,
192
                           PermissionServiceBean permissionService, PermissionsWrapper permissionsWrapper,
193
                           DataverseRequestServiceBean dvRequestService, DataverseSession session,
194
                           GuestbookResponseServiceBean guestbookResponseService, EmbargoAccessService embargoAccess,
195
                           SettingsServiceBean settingsService, EjbDataverseEngine commandEngine,
196
                           ExternalToolServiceBean externalToolService, TermsOfUseFormMapper termsOfUseFormMapper,
197
                           FileDownloadRequestHelper fileDownloadRequestHelper, RequestedDownloadType requestedDownloadType,
198
                           GuestbookResponseDialog guestbookResponseDialog, ImageThumbConverter imageThumbConverter,
199
                           FileMetadataService fileMetadataService, DatasetFilesTabFacade datasetFilesTabFacade,
200
                           ConfirmEmailServiceBean confirmEmailService, DatasetFieldValidationService fieldValidationService) {
×
201
        this.fileDownloadHelper = fileDownloadHelper;
×
202
        this.datafileService = datafileService;
×
203
        this.permissionService = permissionService;
×
204
        this.permissionsWrapper = permissionsWrapper;
×
205
        this.dvRequestService = dvRequestService;
×
206
        this.session = session;
×
207
        this.fileDownloadRequestHelper = fileDownloadRequestHelper;
×
208
        this.guestbookResponseService = guestbookResponseService;
×
209
        this.embargoAccess = embargoAccess;
×
210
        this.settingsService = settingsService;
×
211
        this.commandEngine = commandEngine;
×
212
        this.externalToolService = externalToolService;
×
213
        this.termsOfUseFormMapper = termsOfUseFormMapper;
×
214
        this.requestedDownloadType = requestedDownloadType;
×
215
        this.guestbookResponseDialog = guestbookResponseDialog;
×
216
        this.fileMetadataService = fileMetadataService;
×
217
        this.imageThumbConverter = imageThumbConverter;
×
218
        this.datasetFilesTabFacade = datasetFilesTabFacade;
×
219
        this.confirmEmailService = confirmEmailService;
×
220
        this.fieldValidationService = fieldValidationService;
×
221
    }
×
222

223
    public void init(DatasetVersion workingVersion) {
224
        this.dataset = workingVersion.getDataset();
×
225
        this.workingVersion = workingVersion;
×
226
        rowsPerPage = 10;
×
227
        fileMetadatasSearch = new LazyFileMetadataModel(fileMetadataService, workingVersion.getId());
×
228

229
        logger.fine("Checking if rsync support is enabled.");
×
230
        Dataset tempDataset = datasetFilesTabFacade.retrieveDataset(this.dataset.getId());
×
231
        if (DataCaptureModuleUtil.rsyncSupportEnabled(settingsService.getValueForKey(SettingsServiceBean.Key.UploadMethods))
×
232
                && tempDataset.getFiles().isEmpty()) { //only check for rsync if no files exist
×
233
            try {
234
                ScriptRequestResponse scriptRequestResponse = commandEngine.submit(new RequestRsyncScriptCommand(dvRequestService
×
235
                                                                                                                         .getDataverseRequest(), tempDataset));
×
236
                logger.fine("script: " + scriptRequestResponse.getScript());
×
237
                if (scriptRequestResponse.getScript() != null && !scriptRequestResponse.getScript().isEmpty()) {
×
238
                    rsyncScript = scriptRequestResponse.getScript();
×
239
                    rsyncScriptFilename = "upload-" + workingVersion.getDataset().getIdentifier() + ".bash";
×
240
                    rsyncScriptFilename = rsyncScriptFilename.replace("/", "_");
×
241
                }
242
            } catch (RuntimeException ex) {
×
243
                logger.warning("Problem getting rsync script: " + ex.getLocalizedMessage());
×
244
            }
×
245
        }
246

247
        for (DataFile f : tempDataset.getFiles()) {
×
248
            if (f.isTabularData()) {
×
249
                hasTabular = true;
×
250
                break;
×
251
            }
252
        }
×
253

254
        if (tempDataset.isLockedFor(DatasetLock.Reason.DcmUpload)) {
×
255
            lockedDueToDcmUpload = false;
×
256
        }
257

258
        configureTools = externalToolService.findByType(ExternalTool.Type.CONFIGURE);
×
259
        exploreTools = externalToolService.findByType(ExternalTool.Type.EXPLORE);
×
260
        previewTools = externalToolService.findByType(ExternalTool.Type.PREVIEW);
×
261

262
        guestbookResponseDialog.initForDatasetVersion(workingVersion);
×
263

264
        updateMultipleFileOptionFlags();
×
265
    }
×
266

267
    public boolean isFilesTabDisplayable() {
268
        assert dataset != null;
×
269
        assert embargoAccess != null;
×
270
        return !embargoAccess.isRestrictedByEmbargo(dataset);
×
271
    }
272

273
    // -------------------- GETTERS --------------------
274

275
    public DataTable getFileDataTable() {
276
        return fileDataTable;
×
277
    }
278

279
    public LazyFileMetadataModel getFileMetadatasSearch() {
280
        return fileMetadatasSearch;
×
281
    }
282

283
    public Dataset getDataset() {
284
        return dataset;
×
285
    }
286

287
    public DatasetVersion getWorkingVersion() {
288
        return workingVersion;
×
289
    }
290

291
    public int getFilePaginatorPage() {
292
        return filePaginatorPage;
×
293
    }
294

295
    public int getRowsPerPage() {
296
        return rowsPerPage;
×
297
    }
298

299
    public List<FileMetadata> getSelectedFileMetadataForView() {
300
        return this.fileMetadatasSearch.getWrappedData().stream()
×
301
                                       .filter(fileMetadata -> containsFileId(fileMetadata.getId()))
×
302
                                       .collect(toList());
×
303
    }
304

305
    public String getFileLabelSearchTerm() {
306
        return fileLabelSearchTerm;
×
307
    }
308

309
    public List<String> getSelectedNonDownloadableFiles() {
310
        return selectedNonDownloadableFiles;
×
311
    }
312

313
    public boolean isHasTabular() {
314
        return hasTabular;
×
315
    }
316

317
    public boolean isCsvDownloadAvailable() {
318
        return csvDownloadAvailable;
×
319
    }
320

321
    public FileDownloadHelper getFileDownloadHelper() {
322
        return fileDownloadHelper;
×
323
    }
324

325
    public boolean isLockedDueToDcmUpload() {
326
        return lockedDueToDcmUpload;
×
327
    }
328

329
    public boolean isFileAccessRequestMultiButtonRequired() {
330
        return fileAccessRequestMultiButtonRequired;
×
331
    }
332

333
    public boolean isFileAccessRequestMultiSignUpButtonRequired() {
334
        return fileAccessRequestMultiSignUpButtonRequired;
×
335
    }
336

337
    // -------------------- LOGIC --------------------
338

339
    public void refreshPaginator() {
340
        FacesContext facesContext = FacesContext.getCurrentInstance();
×
341
        org.primefaces.component.datatable.DataTable dt = (org.primefaces.component.datatable.DataTable) facesContext
×
342
                .getViewRoot()
×
343
                .findComponent("datasetForm:tabView:filesTable");
×
344
        filePaginatorPage = dt.getPage();
×
345
        rowsPerPage = dt.getRowsToRender();
×
346
        session.setFilesPerPage(dt.getRowsToRender());
×
347
    }
×
348

349
    public void fileListingPaginatorListener(PageEvent event) {
350
        filePaginatorPage = event.getPage();
×
351
        if (StringUtils.isNotEmpty(fileLabelSearchTerm)) {
×
352
            updateFileSearch();
×
353
        }
354
    }
×
355

356
    public void clearSelection() {
357
        selectedFileIds.clear();
×
358
    }
×
359

360
    public void toggleAllSelected(ToggleSelectEvent event) {
361
        List<FileMetadata> currentFilesOnPage = fileMetadatasSearch.getWrappedData();
×
362

363
        if (event.isSelected()) {
×
364
            currentFilesOnPage.forEach(fm -> selectedFileIds.add(fm.getId()));
×
365
        } else {
366
            currentFilesOnPage.forEach(fm -> selectedFileIds.remove(fm.getId()));
×
367
        }
368
        this.selectAllFiles = !this.selectAllFiles;
×
369
    }
×
370

371
    public void updateFileSearch() {
372
        Tuple2<List<FileMetadata>, List<Long>> foundFileMetadata = selectFileMetadatasForDisplayWithPaging(this.fileLabelSearchTerm);
×
373
        fileMetadatasSearch.setWrappedData(foundFileMetadata._1);
×
374
        fileMetadatasSearch.setLoadedData(foundFileMetadata._1);
×
375
        fileMetadatasSearch.setRowCount(foundFileMetadata._2.size());
×
376
        fileMetadatasSearch.setLoadedSearchDataIds(foundFileMetadata._2);
×
377
        updateFileSearchStatus(fileLabelSearchTerm);
×
378
    }
×
379

380
    public void updateFileSearchStatus(String searchTerm) {
381
        fileMetadatasSearch.setLoadedSearchData(!searchTerm.isEmpty());
×
382
    }
×
383

384
    public boolean isThumbnailAvailable(FileMetadata fileMetadata) {
385

386
        // new and optimized logic:
387
        // - check download permission here (should be cached - so it's free!)
388
        // - only then check if the thumbnail is available/exists.
389
        // then cache the results!
390
        Long dataFileId = fileMetadata.getDataFile().getId();
×
391

392
        if (datafileThumbnailsMap.containsKey(dataFileId)) {
×
393
            return !"".equals(datafileThumbnailsMap.get(dataFileId));
×
394
        }
395

396
        if (!FileUtil.isThumbnailSupported(fileMetadata.getDataFile())) {
×
397
            datafileThumbnailsMap.put(dataFileId, "");
×
398
            return false;
×
399
        }
400

401
        if (!this.fileDownloadHelper.canUserDownloadFile(fileMetadata)) {
×
402
            datafileThumbnailsMap.put(dataFileId, "");
×
403
            return false;
×
404
        }
405

406
        String thumbnailAsBase64 = imageThumbConverter.getImageThumbnailAsBase64(fileMetadata.getDataFile(),
×
407
                ImageThumbConverter.DEFAULT_THUMBNAIL_SIZE);
408

409
        if (!StringUtil.isEmpty(thumbnailAsBase64)) {
×
410
            datafileThumbnailsMap.put(dataFileId, thumbnailAsBase64);
×
411
            return true;
×
412
        }
413

414
        datafileThumbnailsMap.put(dataFileId, "");
×
415
        return false;
×
416
    }
417

418
    public void downloadRsyncScript() {
419
        FacesContext ctx = FacesContext.getCurrentInstance();
×
420
        HttpServletResponse response = (HttpServletResponse) ctx.getExternalContext().getResponse();
×
421
        response.setContentType("application/download");
×
422

423
        String contentDispositionString;
424

425
        contentDispositionString = "attachment;filename=" + rsyncScriptFilename;
×
426
        response.setHeader("Content-Disposition", contentDispositionString);
×
427

428
        try {
429
            ServletOutputStream out = response.getOutputStream();
×
430
            out.write(rsyncScript.getBytes());
×
431
            out.flush();
×
432
            ctx.responseComplete();
×
433
        } catch (IOException e) {
×
434
            String error = "Problem getting bytes from rsync script: " + e;
×
435
            logger.warning(error);
×
436
            return;
×
437
        }
×
438

439
        // If the script has been successfully downloaded, lock the dataset:
440
        String lockInfoMessage = "script downloaded";
×
441
        DatasetLock lock = datasetFilesTabFacade.addDatasetLock(dataset.getId(), DatasetLock.Reason.DcmUpload,
×
442
                session.getUser() != null ? ((AuthenticatedUser) session.getUser()).getId() : null,
×
443
                lockInfoMessage);
444
        if (lock != null) {
×
445
            dataset.addLock(lock);
×
446
        } else {
447
            logger.log(Level.WARNING, "Failed to lock the dataset (dataset id={0})", dataset.getId());
×
448
        }
449
    }
×
450

451
    public String getDataFileThumbnailAsBase64(FileMetadata fileMetadata) {
452
        return datafileThumbnailsMap.get(fileMetadata.getDataFile().getId());
×
453
    }
454

455
    /*
456
       TODO/OPTIMIZATION: This is still costing us N SELECT FROM GuestbookResponse queries,
457
       where N is the number of files. This could of course be replaced by a query that'll
458
       look up all N at once... Not sure if it's worth it; especially now that N
459
       will always be 10, for the initial page load. -- L.A. 4.2.1
460
     */
461
    public Long getGuestbookResponseCount(FileMetadata fileMetadata) {
462
        return guestbookResponseService.getCountGuestbookResponsesByDataFileId(fileMetadata.getDataFile().getId());
×
463
    }
464

465
    public boolean isRequestAccessPopupRequired(DatasetVersion workingVersion, List<FileMetadata> restrictedFiles) {
466
        return workingVersion.isReleased() && !restrictedFiles.isEmpty();
×
467
    }
468

469
    public List<ExternalTool> getConfigureToolsForDataFile(Long fileId) {
470
        return getCachedToolsForDataFile(fileId, ExternalTool.Type.CONFIGURE);
×
471
    }
472

473
    public List<ExternalTool> getExploreToolsForDataFile(Long fileId) {
474
        return getCachedToolsForDataFile(fileId, ExternalTool.Type.EXPLORE);
×
475
    }
476

477
    public List<ExternalTool> getPreviewToolsForDataFile(Long fileId) {
478
        return getCachedToolsForDataFile(fileId, ExternalTool.Type.PREVIEW);
×
479
    }
480

481
    public boolean shouldAllowRestrictedFileRequest() {
482
        return confirmEmailService.hasEffectivelyUnconfirmedMail(session.getUser());
×
483
    }
484

485
    // Another convenience method - to cache Update Permission on the dataset:
486
    public boolean canUpdateDataset() {
487
        return permissionsWrapper.canCurrentUserUpdateDataset(dataset);
×
488
    }
489

490
    private void updateMultipleFileOptionFlags() {
491

492
        boolean versionContainsNonDownloadableFiles = datasetFilesTabFacade.isVersionContainsNonDownloadableFiles(workingVersion.getId());
×
493
        boolean versionContainsDownloadableFiles = datasetFilesTabFacade.isVersionContainsDownloadableFiles(workingVersion.getId());
×
494

495
        if (versionContainsNonDownloadableFiles) {
×
496
            fileAccessRequestMultiButtonRequired = session.getUser().isAuthenticated();
×
497
            fileAccessRequestMultiSignUpButtonRequired = !session.getUser().isAuthenticated();
×
498
        }
499
        downloadButtonAvailable = versionContainsDownloadableFiles;
×
500
        csvDownloadAvailable = workingVersion.isReleased() && (dataset.getGuestbook() == null || !dataset.getGuestbook().isEnabled());
×
501
    }
×
502

503
    public String requestAccessMultipleFiles() {
504

505
        if (countSelectedFiles() == 0) {
×
506
            PrimefacesUtil.showDialog(SELECT_FILES_FOR_REQUEST_ACCESS_DIALOG);
×
507
            return "";
×
508
        }
509

510
        boolean anyFileAccessPopupRequired = false;
×
511
        fileDownloadRequestHelper.clearRequestAccessFiles();
×
512

513
        List<FileMetadata> restrictedSelectedFiles = fileMetadataService.findRestrictedFileMetadata(selectedFileIds);
×
514

515
        if (isRequestAccessPopupRequired(workingVersion, restrictedSelectedFiles)) {
×
516
            restrictedSelectedFiles.stream()
×
517
                                   .map(FileMetadata::getDataFile)
×
518
                                   .forEach(fileDownloadRequestHelper::addFileForRequestAccess);
×
519
            anyFileAccessPopupRequired = true;
×
520
        }
521

522
        if (anyFileAccessPopupRequired) {
×
523
            PrimefacesUtil.showDialog(REQUEST_ACCESS_DIALOG);
×
524
            return "";
×
525
        }
526

527
        //No popup required
528
        fileDownloadRequestHelper.requestAccessIndirect();
×
529
        return "";
×
530
    }
531

532
    public void validateFilesForDownload(boolean downloadOriginal) {
533
        selectedDownloadableFiles = new ArrayList<>();
×
534
        selectedNonDownloadableFiles = new ArrayList<>();
×
535
        this.downloadOriginal = downloadOriginal;
×
536

537
        if (this.selectedFileIds.isEmpty()) {
×
538
            PrimefacesUtil.showDialog(SELECT_FILES_FOR_DOWNLAOD_DIALOG);
×
539
            return;
×
540
        }
541
        List<FileMetadata> fetchedFileMetadata = fileMetadataService.findFileMetadata(selectedFileIds.toArray(new Long[0]));
×
542

543
        for (FileMetadata fmd : fetchedFileMetadata) {
×
544
            if (this.fileDownloadHelper.canUserDownloadFile(fmd)) {
×
545
                selectedDownloadableFiles.add(fmd);
×
546
            } else {
547
                selectedNonDownloadableFiles.add(fmd.getLabel());
×
548
            }
549
        }
×
550

551
        // If some of the files were restricted and we had to drop them off the
552
        // list, and NONE of the files are left on the downloadable list
553
        // - we show them a "you're out of luck" popup:
554
        if (selectedDownloadableFiles.isEmpty()) {
×
555
            PrimefacesUtil.showDialog(DOWNLOAD_INVALID_DIALOG);
×
556
            return;
×
557
        }
558

559
        // If we have a bunch of files that we can download, AND there were no files
560
        // that we had to take off the list, because of permissions - we can
561
        // either send the user directly to the download API (if no guestbook/terms
562
        // popup is required), or send them to the download popup:
563
        if (selectedNonDownloadableFiles.isEmpty()) {
×
564
            fileDownloadHelper.requestDownloadWithFiles(selectedDownloadableFiles, downloadOriginal);
×
565
            return;
×
566
        }
567

568
        // ... and if some files were restricted, but some are downloadable,
569
        // we are showing them this "you are somewhat in luck" popup; that will
570
        // then direct them to the download, or popup, as needed:
571
        PrimefacesUtil.showDialog(DOWNLOAD_MIXED_DIALOG);
×
572

573
    }
×
574

575
    public void startMultipleFileDownload() {
576
        // There's a chance that this is not really a batch download - i.e.,
577
        // there may only be one file on the downloadable list. But the fileDownloadHelper
578
        // method below will check for that, and will redirect to the single download, if
579
        // that's the case. -- L.A.
580
        fileDownloadHelper.requestDownloadWithFiles(selectedDownloadableFiles, downloadOriginal);
×
581
    }
×
582

583
    public void startDatasetFilesDownload(boolean downloadOriginal) {
584
        fileDownloadHelper.requestDownloadOfWholeDataset(workingVersion, downloadOriginal);
×
585
    }
×
586

587
    public void startDatasetFilesDownloadAsCSV() {
588
        if (csvDownloadAvailable) {
×
589
            fileDownloadHelper.requestDownloadOfWholeDatasetAsCSV(workingVersion);
×
590
        }
591
    }
×
592

593
    public boolean isDownloadButtonAvailable() {
594
        return downloadButtonAvailable;
×
595
    }
596

597
    public String editFileMetadata() {
598
        // If there are no files selected, return an empty string - which
599
        // means, do nothing, don't redirect anywhere, stay on this page.
600
        // The dialogue telling the user to select at least one file will
601
        // be shown to them by an onclick javascript method attached to the
602
        // filemetadata edit button on the page.
603
        // -- L.A. 4.2.1
604
        if (selectedFileIds.isEmpty()) {
×
605
            return "";
×
606
        }
607
        return "/editdatafiles.xhtml?selectedFileIds=" + joinDataFileIdsFromFileMetadata() + "&datasetId=" + dataset.getId() + "&faces-redirect=true";
×
608
    }
609

610

611
    /* This method handles saving both "tabular file tags" and
612
     * "file categories" (which are also considered "tags" in 4.0)
613
     */
614
    public void saveFileTagsAndCategories(Collection<FileMetadata> selectedFiles, Collection<String> selectedFileMetadataTags,
615
                                          Collection<String> selectedDataFileTags, boolean removeUnusedTags) {
616

617
        if (bulkUpdateCheckVersion()) {
×
618
            workingVersion = fetchEditDatasetVersion();
×
619
            save(workingVersion, false);
×
620
            selectedFiles = fetchSelectedFilesForVersion(workingVersion);
×
621
        }
622

623
        DatasetVersion updatedVersion = datasetFilesTabFacade.updateFileTagsAndCategories(workingVersion.getId(),
×
624
                                              selectedFiles, selectedFileMetadataTags, selectedDataFileTags);
625
        addSuccessMessage();
×
626

627
        if (removeUnusedTags) {
×
628
            removeUnusedFileTagsFromDataset();
×
629
        }
630

631
        save(updatedVersion);
×
632
        try {
633
            FacesContext.getCurrentInstance().getExternalContext().redirect(returnToDraftVersion());
×
634
        } catch (IOException e) {
×
635
            logger.info("Failed to issue a redirect to draft version url.");
×
636
        }
×
637
    }
×
638

639
    public String deleteFilesAndSave() {
640
        bulkFileDeleteInProgress = true;
×
641
        filesToBeDeleted = bulkUpdateCheckVersion()
×
642
                ? fetchSelectedFilesForVersion(fetchEditDatasetVersion()).stream()
×
643
                    .map(FileMetadata::getDataFile)
×
644
                    .collect(toList())
×
645
                : datafileService.findDataFilesByFileMetadataIds(selectedFileIds);
×
646

647
        return save(workingVersion);
×
648
    }
649

650
    public String saveTermsOfUse(TermsOfUseForm termsOfUseForm) {
651
        FileTermsOfUse termsOfUse = termsOfUseFormMapper.mapToFileTermsOfUse(termsOfUseForm);
×
652

653
        if (bulkUpdateCheckVersion()) {
×
654
            DatasetVersion newDraft = fetchEditDatasetVersion();
×
655
            List<FileMetadata> fetchedFileMetadata = fetchSelectedFilesForVersion(newDraft);
×
656
            updateTermsOfUseForVersion(newDraft, termsOfUse, fetchedFileMetadata);
×
657
            save(newDraft);
×
658
        } else {
×
659
            List<FileMetadata> fetchedFileMetadata = fileMetadataService.findFileMetadata(selectedFileIds.toArray(new Long[0]));
×
660
            DatasetVersion updatedVersion = datasetFilesTabFacade.updateTermsOfUse(workingVersion.getId(), termsOfUse, fetchedFileMetadata);
×
661
            save(updatedVersion);
×
662
        }
663
        return returnToDraftVersion();
×
664
    }
665

666
    public boolean isLocked() {
667
        return dataset.isLocked();
×
668
    }
669

670
    // TODO: investigate why this method was needed in the first place?
671
    // It appears that it was written under the assumption that downloads
672
    // should not be allowed when a dataset is locked... (why?)
673
    // There are calls to the method throghout the file-download-buttons fragment;
674
    // except the way it's done there, it's actually disregarded (??) - so the
675
    // download buttons ARE always enabled. The only place where this method is
676
    // honored is on the batch (mutliple file) download buttons in filesFragment.xhtml.
677
    // As I'm working on #4000, I've been asked to re-enable the batch download
678
    // buttons there as well, even when the dataset is locked. I'm doing that - but
679
    // I feel we should probably figure out why we went to the trouble of creating
680
    // this code in the first place... is there some reason we are forgetting now,
681
    // why we do actually want to disable downloads on locked datasets???
682
    // -- L.A. Aug. 2018
683
    public boolean isLockedFromDownload() {
684
        if (lockedFromDownloadVar == null) {
×
685
            try {
686
                permissionService.checkDownloadFileLock(dataset, dvRequestService.getDataverseRequest(),
×
687
                        new CreateNewDatasetCommand(dataset, dvRequestService.getDataverseRequest()));
×
688
                lockedFromDownloadVar = false;
×
689
            } catch (IllegalCommandException ex) {
×
690
                lockedFromDownloadVar = true;
×
691
                return true;
×
692
            }
×
693
        }
694
        return lockedFromDownloadVar;
×
695
    }
696

697
    public boolean isLockedFromEdits() {
698
        if (lockedFromEditsVar == null) {
×
699
            try {
700
                permissionService.checkEditDatasetLock(dataset, dvRequestService.getDataverseRequest(),
×
701
                        new UpdateDatasetVersionCommand(dataset, dvRequestService.getDataverseRequest()));
×
702
                lockedFromEditsVar = false;
×
703
            } catch (IllegalCommandException ex) {
×
704
                lockedFromEditsVar = true;
×
705
            }
×
706
        }
707
        return lockedFromEditsVar;
×
708
    }
709

710
    public boolean isAllFilesSelected() {
711
        return selectedFileIds.size() >= allCurrentFilesCount();
×
712
    }
713

714
    public String getEmbargoDateForDisplay() {
715
        SimpleDateFormat format = new SimpleDateFormat(settingsService.getValueForKey(SettingsServiceBean.Key.DefaultDateFormat));
×
716
        return format.format(dataset.getEmbargoDate().getOrNull());
×
717
    }
718

719
    public void onRowSelectByCheckbox(SelectEvent event) {
720
        FileMetadata selectedFile = (FileMetadata) event.getObject();
×
721
        selectedFileIds.add(selectedFile.getId());
×
722
        setSelectAllFiles(false);
×
723
    }
×
724

725
    public void onRowUnSelectByCheckbox(UnselectEvent event) {
726
        FileMetadata unselectedFile = (FileMetadata) event.getObject();
×
727
        selectedFileIds.remove(unselectedFile.getId());
×
728
        setSelectAllFiles(false);
×
729
    }
×
730

731
    public void selectAllFiles() {
732
        selectedFileIds.addAll(fileMetadatasSearch.getCurrentlyLoadedDataIds());
×
733
    }
×
734

735
    public long countSelectedFiles() {
736
        return selectedFileIds.size();
×
737
    }
738

739
    public List<FileMetadata> retrieveSelectedFiles() {
740

741
        if (bulkUpdateCheckVersion()) {
×
742
            return fetchSelectedFilesForVersion(fetchEditDatasetVersion());
×
743
        }
744

745
        return selectedFileIds.isEmpty()
×
746
                ? new ArrayList<>()
747
                : fileMetadataService.findFileMetadata(selectedFileIds.toArray(new Long[0]));
×
748
    }
749

750
    public int getFileSize() {
751
        fileSize = fileSize == null ? datasetFilesTabFacade.fileSize(workingVersion.getId()) : fileSize;
×
752
        return fileSize;
×
753
    }
754

755
    public int getFileRows(int defaultValue) {
756
        return session.getFilesPerPage() == 0 ? defaultValue : session.getFilesPerPage();
×
757
    }
758

759
    // -------------------- PRIVATE --------------------
760

761
    private void updateTermsOfUseForVersion(DatasetVersion dsv, FileTermsOfUse termsOfUse, List<FileMetadata> fetchedFileMetadata) {
762
        fetchedFileMetadata.forEach(fileMetadata -> fileMetadata.setTermsOfUse(termsOfUse.createCopy()));
×
763

764
        dsv.getFileMetadatas().stream()
×
765
                .filter(fileMetadata -> containsDataFile(fetchedFileMetadata, fileMetadata.getDataFile().getId()))
×
766
                .forEach(fileMetadata -> fileMetadata.setTermsOfUse(termsOfUse));
×
767
    }
×
768

769
    private boolean containsDataFile(List<FileMetadata> fileMetadatas, Long dataFileId) {
770
        return fileMetadatas.stream()
×
771
                .map(fileMetadata -> fileMetadata.getDataFile().getId())
×
772
                .anyMatch(fileId -> fileId.equals(dataFileId));
×
773
    }
774

775
    private boolean containsFileId(Long id) {
776
        return selectedFileIds.stream()
×
777
                              .anyMatch(storedId -> storedId.equals(id));
×
778
    }
779

780
    private long allCurrentFilesCount() {
781
        return fileMetadatasSearch.getAllAvailableFilesCount();
×
782
    }
783

784
    private void addSuccessMessage() {
785
        String successMessage = BundleUtil.getStringFromBundle("file.assignedTabFileTags.success");
×
786
        logger.fine(successMessage);
×
787
        JsfHelper.addFlashSuccessMessage(successMessage);
×
788
    }
×
789

790
    private void setTagsForTabularData(Collection<String> selectedDataFileTags, FileMetadata fmd) {
791
        fmd.getDataFile().getTags().clear();
×
792

793
        selectedDataFileTags.forEach(selectedTag -> {
×
794
            DataFileTag tag = new DataFileTag();
×
795
            tag.setTypeByLabel(selectedTag);
×
796
            tag.setDataFile(fmd.getDataFile());
×
797
            fmd.getDataFile().addTag(tag);
×
798
        });
×
799
    }
×
800

801
    private Tuple2<List<FileMetadata>, List<Long>> selectFileMetadatasForDisplayWithPaging(String searchTerm) {
802

803
        List<Long> searchResultsIdList = datafileService
×
804
                .findFileMetadataIdsByDatasetVersionIdLabelSearchTerm(workingVersion.getId(), searchTerm,
×
805
                        new FileSortFieldAndOrder("", SortOrder.asc))
806
                .stream()
×
807
                .map(Integer::longValue)
×
808
                .collect(toList());
×
809

810
        if (searchTerm != null && !searchTerm.equals("")) {
×
811
            List<FileMetadata> accessibleFileMetadataSorted = fileMetadataService.findSearchedAccessibleFileMetadataSorted(
×
812
                    workingVersion.getId(), filePaginatorPage, rowsPerPage, searchTerm);
×
813
            return Tuple.of(accessibleFileMetadataSorted, searchResultsIdList);
×
814
        }
815

816
        return Tuple.of(getAccessibleFilesMetadataWithPaging(), searchResultsIdList);
×
817
    }
818

819
    private List<FileMetadata> getAccessibleFilesMetadataWithPaging() {
820
        if (permissionsWrapper.canViewUnpublishedDataset(dataset)
×
821
                && !workingVersion.getDataset().hasActiveEmbargo()) {
×
822
            return fileMetadataService.findAccessibleFileMetadataSorted(workingVersion.getId(), filePaginatorPage, rowsPerPage);
×
823
        }
824

825
        return Lists.newArrayList();
×
826
    }
827

828
    private List<ExternalTool> getCachedToolsForDataFile(Long fileId, ExternalTool.Type type) {
829
        Map<Long, List<ExternalTool>> cachedToolsByFileId = new HashMap<>();
×
830
        List<ExternalTool> externalTools = new ArrayList<>();
×
831
        switch (type) {
×
832
            case EXPLORE:
833
                cachedToolsByFileId = exploreToolsByFileId;
×
834
                externalTools = exploreTools;
×
835
                break;
×
836
            case CONFIGURE:
837
                cachedToolsByFileId = configureToolsByFileId;
×
838
                externalTools = configureTools;
×
839
                break;
×
840
            case PREVIEW:
841
                cachedToolsByFileId = previewToolsByFileId;
×
842
                externalTools = previewTools;
×
843
            default:
844
                break;
845
        }
846
        List<ExternalTool> cachedTools = cachedToolsByFileId.get(fileId);
×
847
        if (cachedTools != null) { // if already queried before and added to list
×
848
            return cachedTools;
×
849
        }
850
        DataFile dataFile = datafileService.find(fileId);
×
851
        cachedTools = externalToolService.findExternalToolsByFileAndVersion(externalTools, dataFile, workingVersion);
×
852
        cachedToolsByFileId.put(fileId, cachedTools); // add to map so we don't have to do the lifting again
×
853
        return cachedTools;
×
854
    }
855

856
    private String joinDataFileIdsFromFileMetadata() {
857
        return datafileService.findDataFilesByFileMetadataIds(selectedFileIds).stream()
×
858
                              .map(dataFile -> dataFile.getId().toString())
×
859
                              .collect(joining(","));
×
860
    }
861

862
    private List<FileMetadata> fetchSelectedFilesForVersion(DatasetVersion version) {
863
        List<DataFile> selectedDataFiles = selectedFileIds.isEmpty() ? new ArrayList<>() :
×
864
                datafileService.findDataFilesByFileMetadataIds(selectedFileIds);
×
865

866
        return version.getFileMetadatas().stream()
×
867
                            .filter(fileMetadata -> selectedDataFiles.contains(fileMetadata.getDataFile()))
×
868
                            .collect(Collectors.toList());
×
869
    }
870

871
    private DatasetVersion fetchEditDatasetVersion() {
872
        Dataset fetchedDataset = datasetFilesTabFacade.retrieveDataset(dataset.getId());
×
873
        return fetchedDataset.getEditVersion();
×
874
    }
875

876
    private boolean bulkUpdateCheckVersion() {
877
        return workingVersion.isReleased();
×
878
    }
879

880
    /*
881
    Remove unused file tags
882
    When updating datafile tags see if any custom tags are not in use.
883
    Remove them
884
    */
885
    private void removeUnusedFileTagsFromDataset() {
886
        categoriesByName = new ArrayList<>(datasetFilesTabFacade.fetchCategoriesByName(workingVersion.getId()));
×
887

888
        List<DataFileCategory> datasetFileCategoriesToRemove = new ArrayList<>();
×
889
        List<DataFileCategory> categories = datasetFilesTabFacade.retrieveDatasetFileCategories(dataset.getId());
×
890
        for (DataFileCategory test : categories) {
×
891
            boolean remove = true;
×
892
            for (String catByName : categoriesByName) {
×
893
                if (catByName.equals(test.getName())) {
×
894
                    remove = false;
×
895
                    break;
×
896
                }
897
            }
×
898
            if (remove) {
×
899
                datasetFileCategoriesToRemove.add(test);
×
900
            }
901
        }
×
902

903
        if (!datasetFileCategoriesToRemove.isEmpty()) {
×
904
            datasetFilesTabFacade.removeDatasetFileCategories(dataset.getId(), datasetFileCategoriesToRemove);
×
905
        }
906

907
    }
×
908

909
    private String save(DatasetVersion updatedVersion) {
910
        return save(updatedVersion, true);
×
911
    }
912

913
    private String save(DatasetVersion updatedVersion, boolean printBannerMessage) {
914

915
        // Validate
916
        List<FieldValidationResult> fieldValidationResults = fieldValidationService.validateFieldsOfDatasetVersion(updatedVersion);
×
917
        Set<ConstraintViolation<FileMetadata>> constraintViolations = updatedVersion.validateFileMetadata();
×
918
        if (!fieldValidationResults.isEmpty() || !constraintViolations.isEmpty()) {
×
919
            JsfHelper.addErrorMessage(BundleUtil.getStringFromBundle("dataset.message.validationError"), "");
×
920
            return "";
×
921
        }
922

923
        // Use the Create or Update command to save the dataset:
924
        UpdateDatasetVersionCommand cmd;
925
        Map<Long, String> deleteStorageLocations = null;
×
926

927
        try {
928
            if (!filesToBeDeleted.isEmpty()) {
×
929
                deleteStorageLocations = datafileService.getPhysicalFilesToDelete(filesToBeDeleted);
×
930
            }
931

932
            cmd = new UpdateDatasetVersionCommand(updatedVersion.getDataset(), dvRequestService.getDataverseRequest(), filesToBeDeleted);
×
933
            cmd.setValidateLenient(true);
×
934

935
            Dataset submittedDataset = commandEngine.submit(cmd);
×
936
            dataset = datasetFilesTabFacade.retrieveDataset(submittedDataset.getId());
×
937
            logger.fine("Successfully executed SaveDatasetCommand.");
×
938
        } catch (EJBException ex) {
×
939
            StringBuilder error = new StringBuilder();
×
940
            error.append(ex).append(" ");
×
941
            error.append(ex.getMessage()).append(" ");
×
942
            Throwable cause = ex;
×
943
            while (cause.getCause() != null) {
×
944
                cause = cause.getCause();
×
945
                error.append(cause).append(" ");
×
946
                error.append(cause.getMessage()).append(" ");
×
947
            }
948
            logger.log(Level.FINE, "Couldn''t save dataset: {0}", error.toString());
×
949
            populateDatasetUpdateFailureMessage();
×
950
            return returnToDraftVersion();
×
951
        } catch (CommandException ex) {
×
952
            // FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, "Dataset Save Failed", " - " + ex.toString()));
953
            logger.log(Level.SEVERE, "CommandException, when attempting to update the dataset: " + ex.getMessage(), ex);
×
954
            populateDatasetUpdateFailureMessage();
×
955
            return returnToDraftVersion();
×
956
        }
×
957

958
        // Have we just deleted some draft datafiles (successfully)?
959
        // finalize the physical file deletes:
960
        // (DataFileService will double-check that the datafiles no
961
        // longer exist in the database, before attempting to delete
962
        // the physical files)
963

964
        if (deleteStorageLocations != null) {
×
965
            datafileService.finalizeFileDeletes(deleteStorageLocations);
×
966
        }
967

968
        if (printBannerMessage) {
×
969
            // must have been a bulk file update or delete:
970
            if (bulkFileDeleteInProgress) {
×
971
                JsfHelper.addFlashSuccessMessage(BundleUtil.getStringFromBundle("dataset.message.bulkFileDeleteSuccess"));
×
972
            } else {
973
                JsfHelper.addFlashSuccessMessage(BundleUtil.getStringFromBundle("dataset.message.bulkFileUpdateSuccess"));
×
974
            }
975
        }
976
        bulkFileDeleteInProgress = false;
×
977

978
        logger.fine("Redirecting to the Dataset page.");
×
979

980
        return returnToDraftVersion();
×
981
    }
982

983
    private void populateDatasetUpdateFailureMessage() {
984
        // that must have been a bulk file update or delete:
985
        if (bulkFileDeleteInProgress) {
×
986
            JsfHelper.addFlashErrorMessage(BundleUtil.getStringFromBundle("dataset.message.bulkFileDeleteFailure"));
×
987
        } else {
988
            JsfHelper.addFlashErrorMessage(BundleUtil.getStringFromBundle("dataset.message.filesFailure"));
×
989
        }
990
        bulkFileDeleteInProgress = false;
×
991
    }
×
992

993
    private String returnToDraftVersion() {
994
        return "/dataset.xhtml?persistentId=" + dataset.getGlobalIdString() + "&version=DRAFT" + "&faces-redirect=true";
×
995
    }
996

997
    // -------------------- SETTERS --------------------
998

999
    public void setFileDataTable(DataTable fileDataTable) {
1000
        this.fileDataTable = fileDataTable;
×
1001
    }
×
1002

1003
    /**
1004
     * Workaround for LazyDataModel in order to preserve selection across pages.
1005
     */
1006
    public void setSelectedFileMetadataForView(List<FileMetadata> selectedFileMetadataForView) { }
×
1007

1008
    public void setSelectAllFiles(boolean selectAllFiles) {
1009
        this.selectAllFiles = selectAllFiles;
×
1010
    }
×
1011

1012
    public void setFileLabelSearchTerm(String fileLabelSearchTerm) {
1013
        this.fileLabelSearchTerm = StringUtils.trimToEmpty(fileLabelSearchTerm);
×
1014
    }
×
1015
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc