• 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

41.89
/dataverse-webapp/src/main/java/edu/harvard/iq/dataverse/util/FileUtil.java
1
/*
2
   Copyright (C) 2005-2012, by the President and Fellows of Harvard College.
3

4
   Licensed under the Apache License, Version 2.0 (the "License");
5
   you may not use this file except in compliance with the License.
6
   You may obtain a copy of the License at
7

8
         http://www.apache.org/licenses/LICENSE-2.0
9

10
   Unless required by applicable law or agreed to in writing, software
11
   distributed under the License is distributed on an "AS IS" BASIS,
12
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
   See the License for the specific language governing permissions and
14
   limitations under the License.
15

16
   Dataverse Network - A web application to share, preserve and analyze research data.
17
   Developed at the Institute for Quantitative Social Science, Harvard University.
18
   Version 3.0.
19
 */
20

21
package edu.harvard.iq.dataverse.util;
22

23

24
import com.google.common.base.Preconditions;
25
import edu.harvard.iq.dataverse.common.BundleUtil;
26
import edu.harvard.iq.dataverse.common.files.mime.ApplicationMimeType;
27
import edu.harvard.iq.dataverse.common.files.mime.ImageMimeType;
28
import edu.harvard.iq.dataverse.common.files.mime.PackageMimeType;
29
import edu.harvard.iq.dataverse.common.files.mime.TextMimeType;
30
import edu.harvard.iq.dataverse.datasetutility.FileExceedsMaxSizeException;
31
import edu.harvard.iq.dataverse.persistence.datafile.DataFile;
32
import edu.harvard.iq.dataverse.persistence.datafile.DataFile.ChecksumType;
33
import edu.harvard.iq.dataverse.persistence.datafile.FileMetadata;
34
import edu.harvard.iq.dataverse.persistence.datafile.license.FileTermsOfUse;
35
import edu.harvard.iq.dataverse.persistence.datafile.license.FileTermsOfUse.TermsOfUseType;
36
import edu.harvard.iq.dataverse.persistence.dataset.DatasetVersion;
37
import io.vavr.control.Try;
38
import org.apache.commons.lang.StringUtils;
39
import org.slf4j.Logger;
40
import org.slf4j.LoggerFactory;
41

42
import java.io.File;
43
import java.io.FileOutputStream;
44
import java.io.IOException;
45
import java.io.InputStream;
46
import java.io.OutputStream;
47
import java.nio.file.Files;
48
import java.nio.file.Path;
49
import java.nio.file.Paths;
50
import java.security.MessageDigest;
51
import java.security.NoSuchAlgorithmException;
52
import java.text.MessageFormat;
53
import java.util.Date;
54
import java.util.List;
55
import java.util.Locale;
56
import java.util.Optional;
57
import java.util.UUID;
58

59
import static edu.harvard.iq.dataverse.common.FileSizeUtil.bytesToHumanReadable;
60

61

62
/**
63
 * a 4.0 implementation of the DVN FileUtil;
64
 * it provides some of the functionality from the 3.6 implementation,
65
 * but the old code is ported creatively on the method-by-method basis.
66
 *
67
 * @author Leonid Andreev
68
 */
69
public class FileUtil implements java.io.Serializable {
×
70
    private static final Logger logger = LoggerFactory.getLogger(FileUtil.class);
1✔
71

72

73
    /**
74
     * This string can be prepended to a Base64-encoded representation of a PNG
75
     * file in order to imbed an image directly into an HTML page using the
76
     * "img" tag. See also https://en.wikipedia.org/wiki/Data_URI_scheme
77
     */
78
    public static final String DATA_URI_SCHEME = "data:image/png;base64,";
79

80
    // -------------------- LOGIC --------------------
81

82
    public static String replaceExtension(String originalName, String newExtension) {
83
        int extensionIndex = originalName.lastIndexOf(".");
×
84
        return extensionIndex != -1
×
85
                ? originalName.substring(0, extensionIndex) + "." + newExtension
×
86
                : originalName + "." + newExtension;
87
    }
88

89
    public static String getFacetFileTypeForIndex(DataFile dataFile, Locale locale) {
90
        String fileType = dataFile.getContentType();
×
91

92
        if (fileType.contains(";")) {
×
93
            fileType = fileType.substring(0, fileType.indexOf(";"));
×
94
        }
95
        if (fileType.split("/")[0].isEmpty()) {
×
96
            return BundleUtil.getStringFromNonDefaultBundleWithLocale("application/octet-stream", "MimeTypeFacets", locale);
×
97
        }
98

99
        return Optional.ofNullable(BundleUtil.getStringFromNonDefaultBundleWithLocale(fileType, "MimeTypeFacets", locale))
×
100
                .filter(bundleName -> !bundleName.isEmpty())
×
101
                .orElse(BundleUtil.getStringFromNonDefaultBundleWithLocale("application/octet-stream", "MimeTypeFacets", locale));
×
102
    }
103

104
    // from MD5Checksum.java
105
    public static String calculateChecksum(Path filePath, ChecksumType checksumType) {
106

107
        try (InputStream fis = Files.newInputStream(filePath)) {
1✔
108
            return FileUtil.calculateChecksum(fis, checksumType);
1✔
109
        } catch (IOException ex) {
×
110
            throw new RuntimeException(ex);
×
111
        }
112
    }
113

114
    // from MD5Checksum.java
115
    public static String calculateChecksum(InputStream in, ChecksumType checksumType) {
116
        MessageDigest md;
117
        try {
118
            // Use "SHA-1" (toString) rather than "SHA1", for example.
119
            md = MessageDigest.getInstance(checksumType.toString());
1✔
120
        } catch (NoSuchAlgorithmException e) {
×
121
            throw new RuntimeException(e);
×
122
        }
1✔
123

124
        byte[] dataBytes = new byte[1024];
1✔
125

126
        int nread;
127
        try {
128
            while ((nread = in.read(dataBytes)) != -1) {
1✔
129
                md.update(dataBytes, 0, nread);
1✔
130
            }
131
        } catch (IOException ex) {
×
132
            throw new RuntimeException(ex);
×
133
        } finally {
134
            try {
135
                in.close();
1✔
136
            } catch (Exception e) {
×
137
                logger.warn("Exception while closing stream: ", e);
×
138
            }
1✔
139
        }
140

141
        return checksumDigestToString(md.digest());
1✔
142
    }
143

144
    public static String calculateChecksum(byte[] dataBytes, ChecksumType checksumType) {
145
        MessageDigest md;
146
        try {
147
            // Use "SHA-1" (toString) rather than "SHA1", for example.
148
            md = MessageDigest.getInstance(checksumType.toString());
×
149
        } catch (NoSuchAlgorithmException e) {
×
150
            throw new RuntimeException(e);
×
151
        }
×
152
        md.update(dataBytes);
×
153
        return checksumDigestToString(md.digest());
×
154
    }
155

156
    private static String checksumDigestToString(byte[] digestBytes) {
157
        StringBuilder sb = new StringBuilder();
1✔
158
        for (byte digestByte : digestBytes) {
1✔
159
            sb.append(Integer.toString((digestByte & 0xff) + 0x100, 16).substring(1));
1✔
160
        }
161
        return sb.toString();
1✔
162
    }
163

164
    public static String generateOriginalExtension(String fileType) {
165
        if (fileType.equalsIgnoreCase("application/x-spss-sav")) {
1✔
166
            return ".sav";
×
167
        } else if (fileType.equalsIgnoreCase("application/x-spss-por")) {
1✔
168
            return ".por";
×
169
        } else if (fileType.equalsIgnoreCase("application/x-stata")) {
1✔
170
            return ".dta";
1✔
171
        } else if (fileType.equalsIgnoreCase("application/x-rlang-transport")) {
×
172
            return ".RData";
×
173
        } else if (fileType.equalsIgnoreCase("text/csv")) {
×
174
            return ".csv";
×
175
        } else if (fileType.equalsIgnoreCase("text/tsv")) {
×
176
            return ".tsv";
×
177
        } else if (fileType.equalsIgnoreCase("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")) {
×
178
            return ".xlsx";
×
179
        }
180

181
        return "";
×
182
    }
183

184

185
    public static boolean canIngestAsTabular(DataFile dataFile) {
186
        String mimeType = dataFile.getContentType();
×
187
        return canIngestAsTabular(mimeType);
×
188
    }
189

190
    public static boolean canIngestAsTabular(String mimeType) {
191
        if (mimeType == null) {
1✔
192
            return false;
×
193
        }
194

195
        boolean isMimeAmongIngestableAppTypes = ApplicationMimeType.retrieveIngestableMimes().stream()
1✔
196
                .anyMatch(appMime -> appMime.getMimeValue().equals(mimeType));
1✔
197

198
        boolean isMimeAmongIngestableTextTypes = TextMimeType.retrieveIngestableMimes().stream()
1✔
199
                .anyMatch(appMime -> appMime.getMimeValue().equals(mimeType));
1✔
200

201
        return isMimeAmongIngestableAppTypes || isMimeAmongIngestableTextTypes;
1✔
202
    }
203

204
    public static String getFilesTempDirectory() {
205
        String filesRootDirectory = SystemConfig.getFilesDirectoryStatic();
1✔
206
        String filesTempDirectory = filesRootDirectory + "/temp";
1✔
207

208
        if (!Files.exists(Paths.get(filesTempDirectory))) {
1✔
209
            /* Note that "createDirectories()" must be used - not
210
             * "createDirectory()", to make sure all the parent
211
             * directories that may not yet exist are created as well.
212
             */
213
            try {
214
                Files.createDirectories(Paths.get(filesTempDirectory));
1✔
215
            } catch (IOException ex) {
×
216
                throw new IllegalStateException("Failed to create files temp directory: " + filesTempDirectory, ex);
×
217
            }
1✔
218
        }
219
        return filesTempDirectory;
1✔
220
    }
221

222
    public static Path limitedInputStreamToTempFile(InputStream inputStream, Long fileSizeLimit)
223
            throws IOException, FileExceedsMaxSizeException {
224
        Path tempFile = Files.createTempFile(Paths.get(getFilesTempDirectory()), "tmp", "upload");
1✔
225

226
        try (OutputStream outStream = Files.newOutputStream(tempFile)) {
1✔
227
            logger.info("Will attempt to save the file as: " + tempFile.toString());
1✔
228
            byte[] buffer = new byte[8 * 1024];
1✔
229
            int bytesRead;
230
            long totalBytesRead = 0;
1✔
231
            while ((bytesRead = inputStream.read(buffer)) != -1) {
1✔
232
                totalBytesRead += bytesRead;
1✔
233
                if (fileSizeLimit != null && totalBytesRead > fileSizeLimit) {
1✔
234
                    try {
235
                        tempFile.toFile().delete();
1✔
236
                    } catch (Exception ex) {
×
237
                        logger.error("There was a problem with deleting temporary file");
×
238
                    }
1✔
239
                    throw new FileExceedsMaxSizeException(fileSizeLimit, MessageFormat.format(BundleUtil.getStringFromBundle("file.addreplace.error.file_exceeds_limit"),
1✔
240
                                                                               bytesToHumanReadable(fileSizeLimit)));
1✔
241
                }
242
                outStream.write(buffer, 0, bytesRead);
1✔
243
            }
244
        }
245
        return tempFile;
1✔
246
    }
247

248
    public static File inputStreamToFile(InputStream inputStream) throws IOException {
249
        return inputStreamToFile(inputStream, 1024);
×
250
    }
251

252
    public static File inputStreamToFile(InputStream inputStream, int bufferSize) throws IOException {
253
        if (inputStream == null) {
×
254
            logger.info("In inputStreamToFile but inputStream was null! Returning null rather than a File.");
×
255
            return null;
×
256
        }
257
        File file = File.createTempFile(UUID.randomUUID().toString(), UUID.randomUUID().toString());
×
258
        try (OutputStream outputStream = new FileOutputStream(file)) {
×
259
            byte[] bytes = new byte[bufferSize];
×
260
            int read;
261
            while ((read = inputStream.read(bytes)) != -1) {
×
262
                outputStream.write(bytes, 0, read);
×
263
            }
264
            return file;
×
265
        }
266
    }
267

268
    public static String generateStorageIdentifier() {
269
        UUID uid = UUID.randomUUID();
1✔
270

271
        logger.trace("UUID value: {}", uid.toString());
1✔
272

273
        // last 6 bytes, of the random UUID, in hex:
274
        String hexRandom = uid.toString().substring(24);
1✔
275

276
        logger.trace("UUID (last 6 bytes, 12 hex digits): {}", hexRandom);
1✔
277

278
        String hexTimestamp = Long.toHexString(new Date().getTime());
1✔
279

280
        logger.trace("(not UUID) timestamp in hex: {}", hexTimestamp);
1✔
281

282
        String storageIdentifier = hexTimestamp + "-" + hexRandom;
1✔
283

284
        logger.debug("Storage identifier (timestamp/UUID hybrid): {}", storageIdentifier);
1✔
285
        return storageIdentifier;
1✔
286
    }
287

288
    public static String getCiteDataFileFilename(String fileTitle, FileCitationExtension fileCitationExtension) {
289
        if (fileTitle == null || fileCitationExtension == null) {
×
290
            return null;
×
291
        }
292
        return fileTitle.endsWith("tab")
×
293
                ? fileTitle.replaceAll("\\.tab$", fileCitationExtension.getSuffix())
×
294
                : fileTitle + fileCitationExtension.getSuffix();
×
295
    }
296

297
    /**
298
     * @todo Consider returning not only the boolean but the human readable
299
     * reason why the popup is required, which could be used in the GUI to
300
     * elaborate on the text "This file cannot be downloaded publicly."
301
     */
302
    public static boolean isDownloadPopupRequired(DatasetVersion datasetVersion) {
303
        // Each of these conditions is sufficient reason to have to
304
        // present the user with the popup:
305
        if (datasetVersion == null) {
1✔
306
            logger.trace("Download popup required because datasetVersion is null.");
1✔
307
            return false;
1✔
308
        }
309
        // 0. if version is draft then Popup "not required"
310
        if (!datasetVersion.isReleased()) {
1✔
311
            logger.trace("Download popup required because datasetVersion has not been released.");
×
312
            return false;
×
313
        }
314

315
        // 1. Guest Book:
316
        if (datasetVersion.getDataset() != null && datasetVersion.getDataset().getGuestbook() != null && datasetVersion.getDataset().getGuestbook().isEnabled() && datasetVersion.getDataset().getGuestbook().getDataverse() != null) {
1✔
317
            logger.trace("Download popup required because of guestbook.");
×
318
            return true;
×
319
        }
320

321
        logger.trace("Download popup is not required.");
1✔
322
        return false;
1✔
323
    }
324

325
    public static boolean isRequestAccessPopupRequired(FileMetadata fileMetadata) {
326
        Preconditions.checkNotNull(fileMetadata);
×
327
        // Each of these conditions is sufficient reason to have to
328
        // present the user with the popup:
329

330
        //0. if version is draft then Popup "not required"
331
        if (!fileMetadata.getDatasetVersion().isReleased()) {
×
332
            logger.trace("Request access popup not required because datasetVersion has not been released.");
×
333
            return false;
×
334
        }
335

336
        // 1. Terms of Use:
337
        FileTermsOfUse termsOfUse = fileMetadata.getTermsOfUse();
×
338
        if (termsOfUse.getTermsOfUseType() == TermsOfUseType.RESTRICTED) {
×
339
            logger.trace("Request access popup required because file is restricted.");
×
340
            return true;
×
341
        }
342
        logger.trace("Request access popup is not required.");
×
343
        return false;
×
344
    }
345

346
    /**
347
     * Provide download URL if no Terms of Use, no guestbook, and not
348
     * restricted.
349
     */
350
    public static boolean isPubliclyDownloadable(FileMetadata fileMetadata) {
351
        if (fileMetadata == null) {
1✔
352
            return false;
×
353
        }
354
        if (fileMetadata.getTermsOfUse().getTermsOfUseType() == TermsOfUseType.RESTRICTED) {
1✔
355
            String msg = "Not publicly downloadable because the file is restricted.";
1✔
356
            logger.debug(msg);
1✔
357
            return false;
1✔
358
        }
359
        return !isDownloadPopupRequired(fileMetadata.getDatasetVersion());
1✔
360
        /*
361
         * @todo The user clicking publish may have a bad "Dude, where did
362
         * the file Download URL go" experience in the following scenario:
363
         *
364
         * - The user creates a dataset and uploads a file.
365
         *
366
         * - The user sets Terms of Use, which means a Download URL should
367
         * not be displayed.
368
         *
369
         * - While the dataset is in draft, the Download URL is displayed
370
         * due to the rule "Download popup required because datasetVersion
371
         * has not been released."
372
         *
373
         * - Once the dataset is published the Download URL disappears due
374
         * to the rule "Download popup required because of license or terms
375
         * of use."
376
         *
377
         * In short, the Download URL disappears on publish in the scenario
378
         * above, which is weird. We should probably attempt to see into the
379
         * future to when the dataset is published to see if the file will
380
         * be publicly downloadable or not.
381
         */
382
    }
383

384
    /**
385
     * This is what the UI displays for "Download URL" on the file landing page
386
     * (DOIs rather than file IDs.
387
     */
388
    public static String getPublicDownloadUrl(String dataverseSiteUrl, String persistentId, Long fileId) {
389
        String path = null;
×
390
        if (persistentId != null) {
×
391
            path = dataverseSiteUrl + "/api/access/datafile/:persistentId?persistentId=" + persistentId;
×
392
        } else if (fileId != null) {
×
393
            path = dataverseSiteUrl + "/api/access/datafile/" + fileId;
×
394
        } else {
395
            logger.info("In getPublicDownloadUrl but persistentId & fileId are both null!");
×
396
        }
397
        return path;
×
398
    }
399

400
    /**
401
     * The FileDownloadServiceBean operates on file IDs, not DOIs.
402
     */
403
    public static String getFileDownloadUrlPath(ApiDownloadType downloadType, Long fileId, boolean gbRecordsWritten) {
404
        Preconditions.checkNotNull(downloadType);
1✔
405
        Preconditions.checkNotNull(fileId);
1✔
406

407
        String fileDownloadUrl = "/api/access/datafile/" + fileId;
1✔
408
        if (downloadType == ApiDownloadType.BUNDLE) {
1✔
409
            fileDownloadUrl = "/api/access/datafile/bundle/" + fileId;
×
410
        }
411
        if (downloadType == ApiDownloadType.ORIGINAL) {
1✔
412
            fileDownloadUrl = "/api/access/datafile/" + fileId + "?format=original";
×
413
        }
414
        if (downloadType == ApiDownloadType.RDATA) {
1✔
415
            fileDownloadUrl = "/api/access/datafile/" + fileId + "?format=RData";
×
416
        }
417
        if (downloadType == ApiDownloadType.VAR) {
1✔
418
            fileDownloadUrl = "/api/access/datafile/" + fileId + "/metadata";
×
419
        }
420
        if (downloadType == ApiDownloadType.TAB) {
1✔
421
            fileDownloadUrl = "/api/access/datafile/" + fileId + "?format=tab";
×
422
        }
423
        if (gbRecordsWritten) {
1✔
424
            if (fileDownloadUrl.contains("?")) {
×
425
                fileDownloadUrl += "&gbrecs=true";
×
426
            } else {
427
                fileDownloadUrl += "?gbrecs=true";
×
428
            }
429
        }
430
        logger.debug("Returning file download url: " + fileDownloadUrl);
1✔
431
        return fileDownloadUrl;
1✔
432
    }
433

434
    public static String getBatchFilesDownloadUrlPath(List<Long> fileIds, boolean guestbookRecordsAlreadyWritten, ApiBatchDownloadType downloadType) {
435

436
        String fileDownloadUrl = "/api/access/datafiles/" + StringUtils.join(fileIds, ',');
×
437
        if (guestbookRecordsAlreadyWritten && downloadType == ApiBatchDownloadType.DEFAULT) {
×
438
            fileDownloadUrl += "?gbrecs=true";
×
439
        } else if (guestbookRecordsAlreadyWritten && downloadType == ApiBatchDownloadType.ORIGINAL) {
×
440
            fileDownloadUrl += "?gbrecs=true&format=original";
×
441
        } else if (!guestbookRecordsAlreadyWritten && downloadType == ApiBatchDownloadType.ORIGINAL) {
×
442
            fileDownloadUrl += "?format=original";
×
443
        }
444

445
        return fileDownloadUrl;
×
446
    }
447

448
    public static String getDownloadWholeDatasetUrlPath(DatasetVersion dsv, boolean guestbookRecordsAlreadyWritten, ApiBatchDownloadType downloadType) {
449

450
        String fileDownloadUrl = String.format("/api/datasets/%s/versions/%s/files/download", dsv.getDataset().getId(), dsv.getId());
×
451

452
        if (guestbookRecordsAlreadyWritten && downloadType == ApiBatchDownloadType.DEFAULT) {
×
453
            fileDownloadUrl += "?gbrecs=true";
×
454
        } else if (guestbookRecordsAlreadyWritten && downloadType == ApiBatchDownloadType.ORIGINAL) {
×
455
            fileDownloadUrl += "?gbrecs=true&format=original";
×
456
        } else if (!guestbookRecordsAlreadyWritten && downloadType == ApiBatchDownloadType.ORIGINAL) {
×
457
            fileDownloadUrl += "?format=original";
×
458
        }
459

460
        return fileDownloadUrl;
×
461
    }
462

463
    /**
464
     * Returns the download URL for the whole dataset as CSV file. Only available if dataset version has been released.
465
     * @return url or empty string if version unreleased
466
     */
467
    public static String getDownloadWholeDatasetAsCSVUrlPath(DatasetVersion dsv) {
468
        if (dsv.isReleased()) {
×
469
            String version = dsv.getVersionNumber() + "." + dsv.getMinorVersionNumber();
×
470
            return String.format("/api/datasets/%s/versions/%s/files/urls", dsv.getDataset().getId(), version);
×
471
        }
472

473
        return StringUtils.EMPTY;
×
474
    }
475

476
    /**
477
     * This method tells you if thumbnail generation is *supported*
478
     * on this type of file. i.e., if true, it does not guarantee that a thumbnail
479
     * can/will be generated; but it means that we can try.
480
     */
481
    public static boolean isThumbnailSupported(DataFile file) {
482
        if (file == null || file.isHarvested() || "".equals(file.getStorageIdentifier())) {
1✔
483
            return false;
×
484
        }
485
        String contentType = file.getContentType();
1✔
486

487
        // Some browsers (Chrome?) seem to identify FITS files as mime
488
        // type "image/fits" on upload; this is both incorrect (the official
489
        // mime type for FITS is "application/fits", and problematic: then
490
        // the file is identified as an image, and the page will attempt to
491
        // generate a preview - which of course is going to fail...
492
        if (ImageMimeType.FITSIMAGE.getMimeValue().equalsIgnoreCase(contentType)) {
1✔
493
            return false;
×
494
        }
495
        // besides most image/* types, we can generate thumbnails for
496
        // pdf and "world map" files:
497

498
        return contentType != null &&
1✔
499
                (contentType.startsWith("image/")
1✔
500
                        || contentType.equalsIgnoreCase("application/pdf")
1✔
501
                        || (file.isTabularData() && file.hasGeospatialTag())
1✔
502
                        || contentType.equalsIgnoreCase(ApplicationMimeType.GEO_SHAPE.getMimeValue()));
1✔
503
    }
504

505
    public static boolean isPackageFile(DataFile dataFile) {
506
        return PackageMimeType.DATAVERSE_PACKAGE.getMimeValue().equalsIgnoreCase(dataFile.getContentType());
×
507
    }
508

509
    public static byte[] getFileFromResources(String path) {
510
        return Try.of(() -> Files.readAllBytes(Paths.get(FileUtil.class.getResource(path).getPath())))
×
511
                .getOrElseThrow(throwable -> new RuntimeException("Unable to get file from resources", throwable));
×
512
    }
513

514
    // -------------------- INNER CLASSES --------------------
515

516
    public enum FileCitationExtension {
×
517
        ENDNOTE("-endnote", ".xml"),
×
518
        RIS(".ris"),
×
519
        BIBTEX(".bib");
×
520

521
        private final String text;
522
        private final String extension;
523

524
        FileCitationExtension(String text, String extension) {
×
525
            this.text = text;
×
526
            this.extension = extension;
×
527
        }
×
528

529
        FileCitationExtension(String extension) {
530
            this(StringUtils.EMPTY, extension);
×
531
        }
×
532

533
        public String getSuffix() {
534
            return text + extension;
×
535
        }
536

537
        public String getExtension() {
538
            return extension;
×
539
        }
540
    }
541

542
    public enum ApiBatchDownloadType {
×
543
        DEFAULT,
×
544
        ORIGINAL
×
545
    }
546

547
    public enum ApiDownloadType {
1✔
548
        DEFAULT,
1✔
549
        BUNDLE,
1✔
550
        ORIGINAL,
1✔
551
        RDATA,
1✔
552
        VAR,
1✔
553
        TAB
1✔
554
    }
555
}
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