diff --git a/doc/release-notes/8461-filecategories-config.md b/doc/release-notes/8461-filecategories-config.md new file mode 100644 index 00000000000..11275269a81 --- /dev/null +++ b/doc/release-notes/8461-filecategories-config.md @@ -0,0 +1,3 @@ +### The default file categories are now configurable + +The default list of the pre-defined file categories - "Documentation", "Data" and "Code" - can now be redefined via a database setting `:FileCategories`. Consult the [Database Settings](https://guides.dataverse.org/en/latest/installation/config.html) section of the Guides for more information. diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index 69bec48ed04..fd4ce4623d1 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -2576,3 +2576,18 @@ For example: ++++++++++++++++++++++++++++++++ When set to ``true``, this setting allows a superuser to publish and/or update Dataverse collections and datasets bypassing the external validation checks (specified by the settings above). In an event where an external script is reporting validation failures that appear to be in error, this option gives an admin with superuser privileges a quick way to publish the dataset or update a collection for the user. + +:FileCategories ++++++++++++++++ + +Overrides the default list of file categories that is used in the UI when adding tags to files. The default list is Documentation, Data, and Code. + +This setting is a comma-separated list of the new tags. + +To override the default list with Docs, Data, Code, and Workflow: + +``curl -X PUT -d 'Docs,Data,Code,Workflow' http://localhost:8080/api/admin/settings/:FileCategories`` + +To remove the override and go back to the default list: + +``curl -X PUT -d '' http://localhost:8080/api/admin/settings/:FileCategories`` \ No newline at end of file diff --git a/src/main/java/edu/harvard/iq/dataverse/DataFileCategoryServiceBean.java b/src/main/java/edu/harvard/iq/dataverse/DataFileCategoryServiceBean.java new file mode 100644 index 00000000000..3fa4691a6dd --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/DataFileCategoryServiceBean.java @@ -0,0 +1,87 @@ +package edu.harvard.iq.dataverse; + +import edu.harvard.iq.dataverse.settings.SettingsServiceBean; +import edu.harvard.iq.dataverse.util.BundleUtil; + +import javax.ejb.EJB; +import javax.ejb.Stateless; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * Service to manage the default values for Dataset file categories and allow + * to be overridden with a :FileCategories Settings configuration. + * + * @author adaybujeda + */ +@Stateless +public class DataFileCategoryServiceBean { + + public static final String FILE_CATEGORIES_KEY = ":FileCategories"; + + @EJB + private SettingsServiceBean settingsService; + + public List mergeDatasetFileCategories(List datasetFileCategories) { + List fileCategories = Optional.ofNullable(datasetFileCategories).orElse(Collections.emptyList()); + List defaultFileCategories = getFileCategories(); + + //avoid resizing + List mergedFileCategories = new ArrayList<>(defaultFileCategories.size() + fileCategories.size()); + + for(DataFileCategory category: fileCategories) { + mergedFileCategories.add(category.getName()); + } + + for(String defaultCategory: defaultFileCategories) { + if (!mergedFileCategories.contains(defaultCategory)) { + mergedFileCategories.add(defaultCategory); + } + } + + return mergedFileCategories; + } + + public List getFileCategories() { + List fileCategoriesOverride = getFileCategoriesOverride(); + return fileCategoriesOverride.isEmpty() ? getFileCategoriesDefault() : fileCategoriesOverride; + } + + private List getFileCategoriesDefault() { + // "Documentation", "Data" and "Code" are the 3 default categories that we + // present by default + return Arrays.asList( + BundleUtil.getStringFromBundle("dataset.category.documentation"), + BundleUtil.getStringFromBundle("dataset.category.data"), + BundleUtil.getStringFromBundle("dataset.category.code") + ); + } + + private List getFileCategoriesOverride() { + String applicationLanguage = BundleUtil.getCurrentLocale().getLanguage(); + Optional fileCategoriesOverride = Optional.ofNullable(settingsService.get(FILE_CATEGORIES_KEY)); + + if (fileCategoriesOverride.isPresent()) { + // There is an override, check if there is language specific value + String overrideValue = settingsService.get(FILE_CATEGORIES_KEY, applicationLanguage, fileCategoriesOverride.get()); + + return parseCategoriesString(overrideValue); + } + + return Collections.emptyList(); + } + + private List parseCategoriesString(String categoriesString) { + if (categoriesString == null) { + return Collections.emptyList(); + } + + String[] categories = categoriesString.split(","); + return Arrays.stream(categories).map(item -> item.trim()).filter(item -> !item.isBlank()).collect(Collectors.toUnmodifiableList()); + } + +} diff --git a/src/main/java/edu/harvard/iq/dataverse/Dataset.java b/src/main/java/edu/harvard/iq/dataverse/Dataset.java index 569a5cdfd2a..32ec6bdcf99 100644 --- a/src/main/java/edu/harvard/iq/dataverse/Dataset.java +++ b/src/main/java/edu/harvard/iq/dataverse/Dataset.java @@ -448,24 +448,6 @@ public void addFileCategory(DataFileCategory category) { dataFileCategories.add(category); } - public Collection getCategoriesByName() { - Collection ret = getCategoryNames(); - - // "Documentation", "Data" and "Code" are the 3 default categories that we - // present by default: - if (!ret.contains(BundleUtil.getStringFromBundle("dataset.category.documentation"))) { - ret.add(BundleUtil.getStringFromBundle("dataset.category.documentation")); - } - if (!ret.contains(BundleUtil.getStringFromBundle("dataset.category.data"))) { - ret.add(BundleUtil.getStringFromBundle("dataset.category.data")); - } - if (!ret.contains(BundleUtil.getStringFromBundle("dataset.category.code"))) { - ret.add(BundleUtil.getStringFromBundle("dataset.category.code")); - } - - return ret; - } - public void setCategoriesByName(List newCategoryNames) { if (newCategoryNames != null) { Collection oldCategoryNames = getCategoryNames(); diff --git a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java index 4a864ada2ae..51544aa4246 100644 --- a/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/DatasetPage.java @@ -64,9 +64,6 @@ import java.io.InputStream; import java.sql.Timestamp; import java.text.SimpleDateFormat; -import java.time.Instant; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; @@ -78,7 +75,6 @@ import java.util.Set; import java.util.Collection; import java.util.logging.Logger; -import java.util.stream.Collectors; import javax.ejb.EJB; import javax.ejb.EJBException; import javax.faces.application.FacesMessage; @@ -88,7 +84,6 @@ import javax.faces.view.ViewScoped; import javax.inject.Inject; import javax.inject.Named; -import javax.json.JsonObject; import org.apache.commons.lang3.StringUtils; import org.primefaces.event.FileUploadEvent; @@ -128,7 +123,6 @@ import javax.servlet.http.HttpServletResponse; import org.apache.commons.text.StringEscapeUtils; -import org.apache.commons.validator.routines.EmailValidator; import org.apache.commons.lang3.mutable.MutableBoolean; import org.apache.commons.io.IOUtils; import org.primefaces.component.selectonemenu.SelectOneMenu; @@ -251,6 +245,8 @@ public enum DisplayMode { EmbargoServiceBean embargoService; @Inject LicenseServiceBean licenseServiceBean; + @Inject + DataFileCategoryServiceBean dataFileCategoryService; private Dataset dataset = new Dataset(); @@ -4529,7 +4525,8 @@ public void setTabFileTagsByName(List tabFileTagsByName) { private void refreshCategoriesByName(){ categoriesByName= new ArrayList<>(); - for (String category: dataset.getCategoriesByName() ){ + List datasetFileCategories = dataFileCategoryService.mergeDatasetFileCategories(dataset.getCategories()); + for (String category: datasetFileCategories ){ categoriesByName.add(category); } refreshSelectedTags(); diff --git a/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java b/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java index 7250f85a493..393aa870bc8 100644 --- a/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java +++ b/src/main/java/edu/harvard/iq/dataverse/EditDatafilesPage.java @@ -138,6 +138,8 @@ public enum Referrer { SettingsWrapper settingsWrapper; @Inject LicenseServiceBean licenseServiceBean; + @Inject + DataFileCategoryServiceBean dataFileCategoryService; private Dataset dataset = new Dataset(); @@ -2737,7 +2739,8 @@ private void refreshSelectedTabFileTags() { private void refreshCategoriesByName(){ categoriesByName= new ArrayList<>(); - for (String category: dataset.getCategoriesByName() ){ + List datasetFileCategories = dataFileCategoryService.mergeDatasetFileCategories(dataset.getCategories()); + for (String category: datasetFileCategories ){ categoriesByName.add(category); } refreshSelectedTags(); diff --git a/src/test/java/edu/harvard/iq/dataverse/DataFileCategoryServiceBeanTest.java b/src/test/java/edu/harvard/iq/dataverse/DataFileCategoryServiceBeanTest.java new file mode 100644 index 00000000000..edeeea288bf --- /dev/null +++ b/src/test/java/edu/harvard/iq/dataverse/DataFileCategoryServiceBeanTest.java @@ -0,0 +1,155 @@ +package edu.harvard.iq.dataverse; + +import edu.harvard.iq.dataverse.settings.SettingsServiceBean; +import edu.harvard.iq.dataverse.util.BundleUtil; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * + * @author adaybujeda + */ +@RunWith(MockitoJUnitRunner.class) +public class DataFileCategoryServiceBeanTest { + + @Mock + private SettingsServiceBean settingsServiceBean; + @InjectMocks + private DataFileCategoryServiceBean target; + + @Test + public void getFileCategories_should_return_default_file_categories_in_expected_order_when_no_override_configured() { + Mockito.when(settingsServiceBean.get(DataFileCategoryServiceBean.FILE_CATEGORIES_KEY)).thenReturn(null); + + List result = target.getFileCategories(); + + MatcherAssert.assertThat(result.size(), Matchers.is(3)); + MatcherAssert.assertThat(result.get(0), Matchers.is("Documentation")); + MatcherAssert.assertThat(result.get(1), Matchers.is("Data")); + MatcherAssert.assertThat(result.get(2), Matchers.is("Code")); + } + + @Test + public void getFileCategories_should_return_default_file_categories_in_expected_order_when_empty_override_is_configured() { + Mockito.when(settingsServiceBean.get(DataFileCategoryServiceBean.FILE_CATEGORIES_KEY)).thenReturn(" "); + + List result = target.getFileCategories(); + + MatcherAssert.assertThat(result.size(), Matchers.is(3)); + MatcherAssert.assertThat(result.get(0), Matchers.is("Documentation")); + MatcherAssert.assertThat(result.get(1), Matchers.is("Data")); + MatcherAssert.assertThat(result.get(2), Matchers.is("Code")); + } + + @Test + public void getFileCategories_should_return_override_file_categories_from_settings_service() { + setup_override("Override01, Override02"); + + List result = target.getFileCategories(); + + MatcherAssert.assertThat(result.size(), Matchers.is(2)); + MatcherAssert.assertThat(result.get(0), Matchers.is("Override01")); + MatcherAssert.assertThat(result.get(1), Matchers.is("Override02")); + } + + @Test + public void getFileCategories_should_trim_override_values() { + setup_override(" Test Value 01 , Test Value 02 "); + + List result = target.getFileCategories(); + + MatcherAssert.assertThat(result.size(), Matchers.is(2)); + MatcherAssert.assertThat(result.get(0), Matchers.is("Test Value 01")); + MatcherAssert.assertThat(result.get(1), Matchers.is("Test Value 02")); + } + + @Test + public void getFileCategories_should_ignore_empty_override_values() { + setup_override(",value01,,value02,,value03,,"); + + List result = target.getFileCategories(); + + MatcherAssert.assertThat(result.size(), Matchers.is(3)); + MatcherAssert.assertThat(result.get(0), Matchers.is("value01")); + MatcherAssert.assertThat(result.get(1), Matchers.is("value02")); + MatcherAssert.assertThat(result.get(2), Matchers.is("value03")); + } + + @Test + public void mergeDatasetFileCategories_should_handle_null_datafile_categories() { + Mockito.when(settingsServiceBean.get(DataFileCategoryServiceBean.FILE_CATEGORIES_KEY)).thenReturn(null); + + List result = target.mergeDatasetFileCategories(null); + + MatcherAssert.assertThat(result.size(), Matchers.is(3)); + MatcherAssert.assertThat(result.get(0), Matchers.is("Documentation")); + MatcherAssert.assertThat(result.get(1), Matchers.is("Data")); + MatcherAssert.assertThat(result.get(2), Matchers.is("Code")); + } + + @Test + public void mergeDatasetFileCategories_should_add_dataset_values_first_then_default_categories() { + Mockito.when(settingsServiceBean.get(DataFileCategoryServiceBean.FILE_CATEGORIES_KEY)).thenReturn(null); + + List result = target.mergeDatasetFileCategories(setup_data_file_categories("dataset01", "dataset02")); + + MatcherAssert.assertThat(result.size(), Matchers.is(5)); + MatcherAssert.assertThat(result.get(0), Matchers.is("dataset01")); + MatcherAssert.assertThat(result.get(1), Matchers.is("dataset02")); + MatcherAssert.assertThat(result.get(2), Matchers.is("Documentation")); + MatcherAssert.assertThat(result.get(3), Matchers.is("Data")); + MatcherAssert.assertThat(result.get(4), Matchers.is("Code")); + } + + @Test + public void mergeDatasetFileCategories_should_add_dataset_values_first_then_override_categories() { + setup_override("override01, override02"); + + List result = target.mergeDatasetFileCategories(setup_data_file_categories("dataset01", "dataset02")); + + MatcherAssert.assertThat(result.size(), Matchers.is(4)); + MatcherAssert.assertThat(result.get(0), Matchers.is("dataset01")); + MatcherAssert.assertThat(result.get(1), Matchers.is("dataset02")); + MatcherAssert.assertThat(result.get(2), Matchers.is("override01")); + MatcherAssert.assertThat(result.get(3), Matchers.is("override02")); + } + + @Test + public void mergeDatasetFileCategories_should_ignore_duplicates() { + Mockito.when(settingsServiceBean.get(DataFileCategoryServiceBean.FILE_CATEGORIES_KEY)).thenReturn(null); + + List result = target.mergeDatasetFileCategories(setup_data_file_categories("Code", "Data", "Custom")); + + MatcherAssert.assertThat(result.size(), Matchers.is(4)); + MatcherAssert.assertThat(result.get(0), Matchers.is("Code")); + MatcherAssert.assertThat(result.get(1), Matchers.is("Data")); + MatcherAssert.assertThat(result.get(2), Matchers.is("Custom")); + MatcherAssert.assertThat(result.get(3), Matchers.is("Documentation")); + } + + private void setup_override(String overrideValue) { + String currentLang = BundleUtil.getCurrentLocale().getLanguage(); + + Mockito.when(settingsServiceBean.get(DataFileCategoryServiceBean.FILE_CATEGORIES_KEY)).thenReturn(overrideValue); + Mockito.when(settingsServiceBean.get(DataFileCategoryServiceBean.FILE_CATEGORIES_KEY, currentLang, overrideValue)).thenReturn(overrideValue); + } + + private List setup_data_file_categories(String... names) { + return Arrays.stream(names).map(name -> { + DataFileCategory dataFileCategory = new DataFileCategory(); + dataFileCategory.setName(name); + return dataFileCategory; + }).collect(Collectors.toList()); + } + +} \ No newline at end of file