Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
618ee4f
11744: CORS: echo request Origin and add Vary: Origin; sanitize CSV l…
ErykKul Aug 14, 2025
75066c5
Centralize CSV parsing (CsvUtil) + CORS origin echo & Vary header imp…
ErykKul Sep 29, 2025
ab19665
Make CORS origin list optional in CorsFilter initialization
ErykKul Sep 29, 2025
c0a00cf
Merge branch 'develop' into 11744-cors-echo-origin-vary
ErykKul Sep 29, 2025
6fbcdc4
Refactor GlobusOverlayAccessIO and CsvUtil for improved endpoint hand…
ErykKul Oct 1, 2025
012a09d
updated release note and comments
ErykKul Oct 1, 2025
48bbd53
test fixes
ErykKul Oct 1, 2025
16720f6
Clarify CORS requirements for browser-based external tools in documen…
ErykKul Oct 1, 2025
ec1bccb
Update CORS documentation to clarify configuration requirements and d…
ErykKul Oct 1, 2025
53c610e
Remove unused CSV lookup methods
ErykKul Oct 1, 2025
01f73c2
Update JvmSettings documentation to clarify CSV list return types
ErykKul Oct 1, 2025
8928d45
Refactor doc structure for improved readability and maintainability
ErykKul Oct 13, 2025
0ded3f9
Merge branch 'develop' into 11744-cors-echo-origin-vary
ErykKul Oct 13, 2025
4c34917
wording
ErykKul Oct 13, 2025
7208c83
Removed deprecated (and removed from code) AllowCors setting from doc
ErykKul Oct 13, 2025
fe15c0c
Fix formatting inconsistencies in dataset management documentation
ErykKul Oct 13, 2025
f376253
rename: CsvUtil -> ListSplitUtil
ErykKul Oct 14, 2025
ce8843e
Refactor CSV list lookup methods to join array elements before splitting
ErykKul Oct 14, 2025
d5e13d4
Rename CSV list lookup methods to use 'lookupSplittedList' for consis…
ErykKul Oct 14, 2025
c4671d8
revert whitespace changes done by automated formatting tool
ErykKul Oct 14, 2025
7a40183
revert whitespace-only changes done by automatic tool
ErykKul Oct 14, 2025
e27a84c
code cleanup
ErykKul Oct 14, 2025
0a9868d
code cleanup
ErykKul Oct 14, 2025
ff55c8a
revert whitespace changes done by automated formatting tool
ErykKul Oct 14, 2025
a8ec817
revert whitespace changes done by automated formatting tool
ErykKul Oct 14, 2025
3f8ff9a
Merge branch 'develop' into 11744-cors-echo-origin-vary
ErykKul Oct 14, 2025
2f6f6d3
revert whitespace changes done by automated formatting tool
ErykKul Oct 14, 2025
3404e70
revert whitespace changes done by automated formatting tool
ErykKul Oct 14, 2025
ed4e5fe
remove legacy dependency on SettingsServiceBean in CorsFilterTest
ErykKul Oct 14, 2025
a749db4
refactor: replace Arrays.stream with ListSplitUtil.split in CorsFilter
ErykKul Oct 14, 2025
83e4a10
refactor: replace ListSplitUtil.split with Arrays.stream for list pro…
ErykKul Oct 14, 2025
6c76282
Merge branch 'develop' into 11744-cors-echo-origin-vary
ErykKul Nov 4, 2025
2781a1a
Enhance JvmSettings: Add trimming options for lookupSplittedList meth…
ErykKul Nov 4, 2025
d1dce07
Merge branch 'develop' into 11744-cors-echo-origin-vary
ErykKul Nov 7, 2025
8cd6177
Merge branch 'develop' into 11744-cors-echo-origin-vary
ErykKul Nov 10, 2025
65cb6ab
Merge branch 'develop' into 11744-cors-echo-origin-vary
stevenwinship Nov 12, 2025
41b4c25
Merge branch 'develop' into 11744-cors-echo-origin-vary
stevenwinship Nov 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions doc/release-notes/11744-cors-echo-origin-vary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# 11744: CORS handling improvements

Modernizes CORS so browser integrations (previewers, external tools, JS clients) work correctly with multiple origins and proper caching.

## Highlights

- Echoes the request origin (`Access-Control-Allow-Origin`) when it matches `dataverse.cors.origin`.
- Adds `Vary: Origin` for per-origin responses (not for wildcard).
- Supports comma‑separated origin list; any `*` in the list = wildcard mode.
- CORS now only enabled when `dataverse.cors.origin` is set (removed `:AllowCors` no longer enables it).
- All comma-separated configuration settings (database properties and MicroProfile config) now ignore spaces around commas; tokens remain unchanged (no quote parsing). Examples: `dataverse.cors.methods`, `dataverse.cors.headers.allow`, `dataverse.cors.headers.expose`. See "Comma-separated configuration values" in the Installation Guide.
- Docs updated (Installation, Big Data Support, External Tools, File Previews); new tests cover edge cases.

## Admin Action

Set `dataverse.cors.origin` explicitly (required). Use explicit origins (not `*`) for credentialed requests. Ensure proxies keep `Vary: Origin`.

Examples:

```
dataverse.cors.origin=https://example.org
dataverse.cors.origin=https://libis.github.io,https://gdcc.github.io
dataverse.cors.origin=*
```

Optional (unquoted):

```
dataverse.cors.methods=GET, POST, OPTIONS, PUT, DELETE
```

## Compatibility

- Must configure `dataverse.cors.origin`; `:AllowCors` was deprecated and has now been removed.
- Any `*` triggers wildcard (no per-origin echo / no Vary header).

## Docs

See updated `dataverse.cors.origin` section and related notes in Big Data Support (S3), External Tools, and File Previews.

<!-- Maintainer note: The generic behavior for comma-separated settings has been documented centrally under Installation Guide > Configuration > "Comma-separated configuration values". Keep this item here as a cross-reference. -->
3 changes: 3 additions & 0 deletions doc/sphinx-guides/source/api/external-tools.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ Introduction

External tools are additional applications the user can access or open from your Dataverse installation to preview, explore, and manipulate data files and datasets. The term "external" is used to indicate that the tool is not part of the main Dataverse Software.

.. note::
Browser-based tools must have CORS explicitly enabled via :ref:`dataverse.cors.origin <dataverse.cors.origin>`. List every origin that will host your tool (or use ``*`` when a wildcard is acceptable). If an origin is not listed, the browser will block that tool's API requests even if the tool page itself loads.

Once you have created the external tool itself (which is most of the work!), you need to teach a Dataverse installation how to construct URLs that your tool needs to operate. For example, if you've deployed your tool to fabulousfiletool.com your tool might want the ID of a file and the siteUrl of the Dataverse installation like this: https://fabulousfiletool.com?fileId=42&siteUrl=https://demo.dataverse.org

In short, you will be creating a manifest in JSON format that describes not only how to construct URLs for your tool, but also what types of files your tool operates on, where it should appear in the Dataverse installation web interfaces, etc.
Expand Down
9 changes: 9 additions & 0 deletions doc/sphinx-guides/source/developers/big-data-support.rst
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ Allow CORS for S3 Buckets
**IMPORTANT:** One additional step that is required to enable direct uploads via a Dataverse installation and for direct download to work with previewers and direct upload to work with dvwebloader (:ref:`folder-upload`) is to allow cross site (CORS) requests on your S3 store.
The example below shows how to enable CORS rules (to support upload and download) on a bucket using the AWS CLI command line tool. Note that you may want to limit the AllowedOrigins and/or AllowedHeaders further. https://github.com/gdcc/dataverse-previewers/wiki/Using-Previewers-with-download-redirects-from-S3 has some additional information about doing this.

Dataverse itself will only emit the necessary ``Access-Control-*`` headers to browsers when CORS has been explicitly enabled via the JVM/MicroProfile setting :ref:`dataverse.cors.origin <dataverse.cors.origin>`. You must both:

* Configure an appropriate ``dataverse.cors.origin`` value (single origin, comma-separated list, or ``*``) on the Dataverse application server; and
* Configure a matching/compatible CORS policy on each S3 bucket (and any CDN/proxy in front of it) that will be used for direct upload or for redirect (download-redirect) operations consumed by previewers.

If you specify multiple origins in ``dataverse.cors.origin`` Dataverse will echo back the requesting origin (when it matches) and will include ``Vary: Origin`` so that shared caches do not serve one origin's response to another. If you configure ``*`` Dataverse will respond with ``Access-Control-Allow-Origin: *`` (note that browsers will not allow credentialed requests with a wildcard).

Make sure the bucket CORS configuration ``AllowedOrigins`` is at least as permissive as the origins you configure in ``dataverse.cors.origin``. If the bucket allows ``*`` but the Dataverse application only allows a subset, the browser will still enforce the more restrictive application response.

Comment thread
ErykKul marked this conversation as resolved.
If you'd like to check the CORS configuration on your bucket before making changes:

``aws s3api get-bucket-cors --bucket <BUCKET_NAME>``
Expand Down
45 changes: 28 additions & 17 deletions doc/sphinx-guides/source/installation/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,27 @@ Once you have finished securing and configuring your Dataverse installation, you
.. contents:: |toctitle|
:local:

.. _comma-separated-config-values:

Comma-separated configuration values
------------------------------------

Many configuration options (both MicroProfile/JVM settings and database settings) accept comma-separated lists. For all such settings, Dataverse applies consistent, lightweight parsing:

- Whitespace immediately around commas is ignored (e.g., ``GET, POST`` is equivalent to ``GET,POST``).
- Tokens are otherwise preserved exactly as typed. There is no quote parsing and no escape processing.
- Embedded commas within a token are not supported.

Examples include (but are not limited to):

- :ref:`dataverse.cors.origin <dataverse.cors.origin>`
- :ref:`dataverse.cors.methods <dataverse.cors.methods>`
- :ref:`dataverse.cors.headers.allow <dataverse.cors.headers.allow>`
- :ref:`dataverse.cors.headers.expose <dataverse.cors.headers.expose>`
- :ref:`:UploadMethods`

This behavior is implemented centrally and applies across all Dataverse settings that accept comma-separated values.

.. _securing-your-installation:

Securing Your Installation
Expand Down Expand Up @@ -3704,17 +3725,21 @@ The following settings control Cross-Origin Resource Sharing (CORS) for your Dat
dataverse.cors.origin
+++++++++++++++++++++

Allowed origins for CORS requests. The default with no value set is to not include CORS headers. However, if the deprecated :AllowCors setting is explicitly set to true the default is "\*" (all origins).
When the :AllowsCors setting is not used, you must set this setting to "\*" or a list of origins to enable CORS headers.
Allowed origins for CORS requests. If this setting is not defined, CORS headers are not added. Set to ``*`` to allow all origins (note that browsers will not allow credentialed requests with ``*``) or provide a comma-separated list of explicit origins.

Multiple origins can be specified as a comma-separated list.
Multiple origins can be specified as a comma-separated list (whitespace is ignored):

Example:

``./asadmin create-jvm-options '-Ddataverse.cors.origin=https://example.com,https://subdomain.example.com'``

Can also be set via any `supported MicroProfile Config API source`_, e.g. the environment variable ``DATAVERSE_CORS_ORIGIN``.

Behavior:

* When a list of origins is configured, Dataverse echoes the single matching request ``Origin`` value in ``Access-Control-Allow-Origin`` and adds ``Vary: Origin`` to support correct proxy/CDN caching.
* When ``*`` is configured, ``Access-Control-Allow-Origin: *`` is sent and ``Vary`` is not modified.

.. _dataverse.cors.methods:

dataverse.cors.methods
Expand Down Expand Up @@ -5028,20 +5053,6 @@ This can be helpful in situations where multiple organizations are sharing one D
or
``curl -X PUT -d '*' http://localhost:8080/api/admin/settings/:InheritParentRoleAssignments``

:AllowCors (Deprecated)
+++++++++++++++++++++++

.. note::
This setting is deprecated. Please use the JVM settings above instead.
This legacy setting will only be used if the newer JVM settings are not set.

Enable or disable support for Cross-Origin Resource Sharing (CORS) by setting ``:AllowCors`` to ``true`` or ``false``.

``curl -X PUT -d true http://localhost:8080/api/admin/settings/:AllowCors``

.. note::
New values for this setting will only be used after a server restart.

:ChronologicalDateFacets
++++++++++++++++++++++++

Expand Down
3 changes: 3 additions & 0 deletions doc/sphinx-guides/source/user/dataset-management.rst
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,9 @@ File Previews

Dataverse installations can add previewers for common file types uploaded by their research communities. The previews appear on the file page. If a preview tool for a specific file type is available, the preview will be created and will display automatically, after terms have been agreed to or a guestbook entry has been made, if necessary. File previews are not available for restricted files unless they are being accessed using a Preview URL. See also :ref:`previewUrl`. When the dataset license is not the default license, users will be prompted to accept the license/data use agreement before the preview is shown. See also :ref:`license-terms`.

.. note::
Some previewers run purely in the browser and make direct (JavaScript) requests back to the Dataverse API endpoints to retrieve file contents, metadata, or signed URLs. For these previewers to function when hosted on a different origin (e.g., a CDN or a separate previewer service), the Dataverse installation must have CORS enabled via :ref:`dataverse.cors.origin <dataverse.cors.origin>`. Administrators should configure the list of allowed origins to include the host serving the previewers.

Previewers are available for the following file types:

- Text
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import edu.harvard.iq.dataverse.settings.SettingsServiceBean;
import edu.harvard.iq.dataverse.util.ListSplitUtil;

/**
*
Expand Down Expand Up @@ -908,12 +909,12 @@ public String getFieldLanguage(String languages, String localeCode) {
// If the fields list of supported languages contains the current locale (e.g.
// the lang of the UI, or the current metadata input/display lang (tbd)), use
// that. Otherwise, return the first in the list
String[] langStrings = languages.split("\\s*,\\s*");
if (langStrings.length > 0) {
if (Arrays.asList(langStrings).contains(localeCode)) {
final List<String> langStrings = ListSplitUtil.split(languages);
if (!langStrings.isEmpty()) {
if (langStrings.contains(localeCode)) {
return localeCode;
} else {
return langStrings[0];
return langStrings.get(0);
}
}
return null;
Expand Down
17 changes: 9 additions & 8 deletions src/main/java/edu/harvard/iq/dataverse/FileMetadata.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
import edu.harvard.iq.dataverse.datavariable.VarGroup;
import edu.harvard.iq.dataverse.datavariable.VariableMetadata;
import edu.harvard.iq.dataverse.util.DateUtil;
import edu.harvard.iq.dataverse.util.ListSplitUtil;
import edu.harvard.iq.dataverse.util.StringUtil;
import java.util.HashSet;
import java.util.Set;
Expand Down Expand Up @@ -605,18 +606,18 @@ public int compare(FileMetadata o1, FileMetadata o2) {
}
};

static Map<String,Long> categoryMap=null;
static Map<String, Long> categoryMap = null;

public static void setCategorySortOrder(String categories) {
categoryMap=new HashMap<String, Long>();
long i=1;
for(String cat: categories.split(",\\s*")) {
categoryMap.put(cat.toUpperCase(), i);
i++;
}
categoryMap = new HashMap<String, Long>();
long i = 1;
for (String cat : ListSplitUtil.split(categories)) {
categoryMap.put(cat.toUpperCase(), i);
i++;
}
}

public static Map<String,Long> getCategorySortOrder() {
public static Map<String, Long> getCategorySortOrder() {
return categoryMap;
}

Expand Down
24 changes: 13 additions & 11 deletions src/main/java/edu/harvard/iq/dataverse/SettingsWrapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import edu.harvard.iq.dataverse.settings.SettingsServiceBean;
import edu.harvard.iq.dataverse.settings.SettingsServiceBean.Key;
import edu.harvard.iq.dataverse.util.BundleUtil;
import edu.harvard.iq.dataverse.util.ListSplitUtil;
import edu.harvard.iq.dataverse.util.StringUtil;
import edu.harvard.iq.dataverse.util.SystemConfig;
import edu.harvard.iq.dataverse.UserNotification.Type;
Expand Down Expand Up @@ -50,8 +51,7 @@
public class SettingsWrapper implements java.io.Serializable {

static final Logger logger = Logger.getLogger(SettingsWrapper.class.getCanonicalName());
public static final String COMMA_BETWEEN_OPTIONAL_WHITE_SPACE = "\\s*,\\s*";


@EJB
SettingsServiceBean settingsService;

Expand Down Expand Up @@ -393,10 +393,12 @@ public boolean isRsyncOnly() {
rsyncOnly = false;
} else {
String uploadMethods = getValueForKey(SettingsServiceBean.Key.UploadMethods);
if (uploadMethods==null){
if (uploadMethods == null) {
rsyncOnly = false;
} else {
rsyncOnly = Arrays.asList(uploadMethods.toLowerCase().split(COMMA_BETWEEN_OPTIONAL_WHITE_SPACE)).size() == 1 && uploadMethods.toLowerCase().equals(SystemConfig.FileUploadMethods.RSYNC.toString());
String normalizedUploadMethods = uploadMethods.toLowerCase();
rsyncOnly = ListSplitUtil.split(normalizedUploadMethods).size() == 1
&& normalizedUploadMethods.equals(SystemConfig.FileUploadMethods.RSYNC.toString());
}
}
}
Expand Down Expand Up @@ -424,11 +426,11 @@ public String getSupportTeamEmail() {

public Integer getUploadMethodsCount() {
if (uploadMethodsCount == null) {
String uploadMethods = getValueForKey(SettingsServiceBean.Key.UploadMethods);
if (uploadMethods==null){
String uploadMethods = getValueForKey(SettingsServiceBean.Key.UploadMethods);
if (uploadMethods == null) {
uploadMethodsCount = 0;
} else {
uploadMethodsCount = Arrays.asList(uploadMethods.toLowerCase().split(COMMA_BETWEEN_OPTIONAL_WHITE_SPACE)).size();
uploadMethodsCount = ListSplitUtil.split(uploadMethods).size();
}
}
return uploadMethodsCount;
Expand Down Expand Up @@ -502,7 +504,7 @@ public boolean shouldBeAnonymized(DatasetField df) {
if (anonymizedFieldTypes == null) {
anonymizedFieldTypes = new ArrayList<String>();
String names = get(SettingsServiceBean.Key.AnonymizedFieldTypeNames.toString(), "");
anonymizedFieldTypes.addAll(Arrays.asList(names.split(COMMA_BETWEEN_OPTIONAL_WHITE_SPACE)));
anonymizedFieldTypes.addAll(ListSplitUtil.split(names));
}
return anonymizedFieldTypes.contains(df.getDatasetFieldType().getName());
}
Expand Down Expand Up @@ -826,11 +828,11 @@ public String getMetricsUrl() {
}

private Boolean getUploadMethodAvailable(String method){
String uploadMethods = getValueForKey(SettingsServiceBean.Key.UploadMethods);
if (uploadMethods==null){
String uploadMethods = getValueForKey(SettingsServiceBean.Key.UploadMethods);
if (uploadMethods == null) {
return false;
} else {
return Arrays.asList(uploadMethods.toLowerCase().split(COMMA_BETWEEN_OPTIONAL_WHITE_SPACE)).contains(method);
return ListSplitUtil.splitToLowerCaseSet(uploadMethods).contains(method);
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/main/java/edu/harvard/iq/dataverse/api/Admin.java
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@
import edu.harvard.iq.dataverse.util.ArchiverUtil;
import edu.harvard.iq.dataverse.util.BundleUtil;
import edu.harvard.iq.dataverse.util.FileUtil;
import edu.harvard.iq.dataverse.util.ListSplitUtil;
import edu.harvard.iq.dataverse.util.SystemConfig;
import edu.harvard.iq.dataverse.util.URLTokenUtil;
import edu.harvard.iq.dataverse.util.UrlSignerUtil;
Expand Down Expand Up @@ -2243,7 +2244,7 @@ public Response addRoleAssignementsToChildren(@Context ContainerRequestContext c
boolean inheritAllRoles = false;
String rolesString = settingsSvc.getValueForKey(SettingsServiceBean.Key.InheritParentRoleAssignments, "");
if (rolesString.length() > 0) {
ArrayList<String> rolesToInherit = new ArrayList<String>(Arrays.asList(rolesString.split("\\s*,\\s*")));
ArrayList<String> rolesToInherit = new ArrayList<>(ListSplitUtil.split(rolesString));
if (!rolesToInherit.isEmpty()) {
if (rolesToInherit.contains("*")) {
inheritAllRoles = true;
Expand Down
6 changes: 4 additions & 2 deletions src/main/java/edu/harvard/iq/dataverse/api/Datasets.java
Original file line number Diff line number Diff line change
Expand Up @@ -5317,7 +5317,8 @@ public Response getPrivateUrlDatasetVersion(@PathParam("privateUrlToken") String
}
JsonObjectBuilder responseJson;
if (isAnonymizedAccess) {
List<String> anonymizedFieldTypeNamesList = new ArrayList<>(Arrays.asList(anonymizedFieldTypeNames.split(SettingsWrapper.COMMA_BETWEEN_OPTIONAL_WHITE_SPACE)));
// Use ListSplitUtil for consistent CSV parsing
List<String> anonymizedFieldTypeNamesList = new ArrayList<>(ListSplitUtil.split(anonymizedFieldTypeNames));
responseJson = json(dsv, anonymizedFieldTypeNamesList, true, returnOwners);
} else {
responseJson = json(dsv, null, true, returnOwners);
Expand All @@ -5343,7 +5344,8 @@ public Response getPreviewUrlDatasetVersion(@PathParam("previewUrlToken") String
}
JsonObjectBuilder responseJson;
if (isAnonymizedAccess) {
List<String> anonymizedFieldTypeNamesList = new ArrayList<>(Arrays.asList(anonymizedFieldTypeNames.split(SettingsWrapper.COMMA_BETWEEN_OPTIONAL_WHITE_SPACE)));
// Use ListSplitUtil for consistent CSV parsing
List<String> anonymizedFieldTypeNamesList = new ArrayList<>(ListSplitUtil.split(anonymizedFieldTypeNames));
responseJson = json(dsv, anonymizedFieldTypeNamesList, true, returnOwners);
} else {
responseJson = json(dsv, null, true, returnOwners);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser;
import edu.harvard.iq.dataverse.batch.jobs.importer.ImportMode;
import edu.harvard.iq.dataverse.settings.JvmSettings;
import edu.harvard.iq.dataverse.util.ListSplitUtil;
import org.apache.commons.io.filefilter.NotFileFilter;
import org.apache.commons.io.filefilter.WildcardFileFilter;

Expand All @@ -43,7 +44,6 @@
import java.io.FileFilter;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
Expand Down Expand Up @@ -152,8 +152,13 @@ public File readItem() {
* @return list of files
*/
private List<File> getFiles(final File directory) {
// create filter from job xml excludes property
FileFilter excludeFilter = new NotFileFilter(new WildcardFileFilter(Arrays.asList(excludes.split("\\s*,\\s*"))));
// create filter from job xml excludes property using builder to avoid deprecated constructors
final String[] excludedPatterns = ListSplitUtil.split(excludes).toArray(new String[0]);
FileFilter excludeFilter = new NotFileFilter(
WildcardFileFilter.builder()
.setWildcards(excludedPatterns)
.get()
);
List<File> files = new ArrayList<>();
File[] filesList = directory.listFiles(excludeFilter);
if (filesList != null) {
Expand Down
Loading