Skip to content

Commit

Permalink
Merge branch 'develop' into 10517-dataset-types #10517
Browse files Browse the repository at this point in the history
  • Loading branch information
pdurbin committed Aug 22, 2024
2 parents 9c44b30 + 2e633c1 commit 987df46
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 40 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed dataverses/{identifier}/metadatablocks endpoint to not return fields marked as displayOnCreate=true if there is an input level with include=false, when query parameters returnDatasetFieldTypes=true and onlyDisplayedOnCreate=true are set.
1 change: 0 additions & 1 deletion doc/sphinx-guides/source/_templates/navbar.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">About <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a target="_blank" href="https://dataverse.org/about">About the Project</a></li>
<li><a target="_blank" href="https://dataverse.org/add-data">Add Data</a></li>
<li><a target="_blank" href="https://dataverse.org/blog">Blog</a></li>
<li><a target="_blank" href="https://dataverse.org/presentations">Presentations</a></li>
<li><a target="_blank" href="https://dataverse.org/publications">Publications</a></li>
Expand Down
122 changes: 122 additions & 0 deletions src/main/java/edu/harvard/iq/dataverse/DatasetFieldServiceBean.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import jakarta.persistence.PersistenceException;
import jakarta.persistence.TypedQuery;

import jakarta.persistence.criteria.*;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.lang3.StringUtils;
Expand Down Expand Up @@ -851,4 +852,125 @@ public String getFieldLanguage(String languages, String localeCode) {
}
return null;
}

public List<DatasetFieldType> findAllDisplayedOnCreateInMetadataBlock(MetadataBlock metadataBlock) {
CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();
CriteriaQuery<DatasetFieldType> criteriaQuery = criteriaBuilder.createQuery(DatasetFieldType.class);

Root<MetadataBlock> metadataBlockRoot = criteriaQuery.from(MetadataBlock.class);
Root<DatasetFieldType> datasetFieldTypeRoot = criteriaQuery.from(DatasetFieldType.class);

Predicate requiredInDataversePredicate = buildRequiredInDataversePredicate(criteriaBuilder, datasetFieldTypeRoot);

criteriaQuery.where(
criteriaBuilder.and(
criteriaBuilder.equal(metadataBlockRoot.get("id"), metadataBlock.getId()),
datasetFieldTypeRoot.in(metadataBlockRoot.get("datasetFieldTypes")),
criteriaBuilder.or(
criteriaBuilder.isTrue(datasetFieldTypeRoot.get("displayOnCreate")),
requiredInDataversePredicate
)
)
);

criteriaQuery.select(datasetFieldTypeRoot).distinct(true);

TypedQuery<DatasetFieldType> typedQuery = em.createQuery(criteriaQuery);
return typedQuery.getResultList();
}

public List<DatasetFieldType> findAllInMetadataBlockAndDataverse(MetadataBlock metadataBlock, Dataverse dataverse, boolean onlyDisplayedOnCreate) {
CriteriaBuilder criteriaBuilder = em.getCriteriaBuilder();
CriteriaQuery<DatasetFieldType> criteriaQuery = criteriaBuilder.createQuery(DatasetFieldType.class);

Root<MetadataBlock> metadataBlockRoot = criteriaQuery.from(MetadataBlock.class);
Root<DatasetFieldType> datasetFieldTypeRoot = criteriaQuery.from(DatasetFieldType.class);
Root<Dataverse> dataverseRoot = criteriaQuery.from(Dataverse.class);

// Join Dataverse with DataverseFieldTypeInputLevel on the "dataverseFieldTypeInputLevels" attribute, using a LEFT JOIN.
Join<Dataverse, DataverseFieldTypeInputLevel> datasetFieldTypeInputLevelJoin = dataverseRoot.join("dataverseFieldTypeInputLevels", JoinType.LEFT);

// Define a predicate to include DatasetFieldTypes that are marked as included in the input level.
Predicate includedAsInputLevelPredicate = criteriaBuilder.and(
criteriaBuilder.equal(datasetFieldTypeRoot, datasetFieldTypeInputLevelJoin.get("datasetFieldType")),
criteriaBuilder.isTrue(datasetFieldTypeInputLevelJoin.get("include"))
);

// Define a predicate to include DatasetFieldTypes that are marked as required in the input level.
Predicate requiredAsInputLevelPredicate = criteriaBuilder.and(
criteriaBuilder.equal(datasetFieldTypeRoot, datasetFieldTypeInputLevelJoin.get("datasetFieldType")),
criteriaBuilder.isTrue(datasetFieldTypeInputLevelJoin.get("required"))
);

// Create a subquery to check for the absence of a specific DataverseFieldTypeInputLevel.
Subquery<Long> subquery = criteriaQuery.subquery(Long.class);
Root<DataverseFieldTypeInputLevel> subqueryRoot = subquery.from(DataverseFieldTypeInputLevel.class);
subquery.select(criteriaBuilder.literal(1L))
.where(
criteriaBuilder.equal(subqueryRoot.get("dataverse"), dataverseRoot),
criteriaBuilder.equal(subqueryRoot.get("datasetFieldType"), datasetFieldTypeRoot)
);

// Define a predicate to exclude DatasetFieldTypes that have no associated input level (i.e., the subquery does not return a result).
Predicate hasNoInputLevelPredicate = criteriaBuilder.not(criteriaBuilder.exists(subquery));

// Define a predicate to include the required fields in Dataverse.
Predicate requiredInDataversePredicate = buildRequiredInDataversePredicate(criteriaBuilder, datasetFieldTypeRoot);

// Define a predicate for displaying DatasetFieldTypes on create.
// If onlyDisplayedOnCreate is true, include fields that:
// - Are either marked as displayed on create OR marked as required, OR
// - Are required according to the input level.
// Otherwise, use an always-true predicate (conjunction).
Predicate displayedOnCreatePredicate = onlyDisplayedOnCreate
? criteriaBuilder.or(
criteriaBuilder.or(
criteriaBuilder.isTrue(datasetFieldTypeRoot.get("displayOnCreate")),
requiredInDataversePredicate
),
requiredAsInputLevelPredicate
)
: criteriaBuilder.conjunction();

// Build the final WHERE clause by combining all the predicates.
criteriaQuery.where(
criteriaBuilder.equal(dataverseRoot.get("id"), dataverse.getId()), // Match the Dataverse ID.
criteriaBuilder.equal(metadataBlockRoot.get("id"), metadataBlock.getId()), // Match the MetadataBlock ID.
metadataBlockRoot.in(dataverseRoot.get("metadataBlocks")), // Ensure the MetadataBlock is part of the Dataverse.
datasetFieldTypeRoot.in(metadataBlockRoot.get("datasetFieldTypes")), // Ensure the DatasetFieldType is part of the MetadataBlock.
criteriaBuilder.or(includedAsInputLevelPredicate, hasNoInputLevelPredicate), // Include DatasetFieldTypes based on the input level predicates.
displayedOnCreatePredicate // Apply the display-on-create filter if necessary.
);

criteriaQuery.select(datasetFieldTypeRoot).distinct(true);

return em.createQuery(criteriaQuery).getResultList();
}

private Predicate buildRequiredInDataversePredicate(CriteriaBuilder criteriaBuilder, Root<DatasetFieldType> datasetFieldTypeRoot) {
// Predicate to check if the current DatasetFieldType is required.
Predicate isRequired = criteriaBuilder.isTrue(datasetFieldTypeRoot.get("required"));

// Subquery to check if the parentDatasetFieldType is required or null.
// We need this check to avoid including conditionally required fields.
Subquery<Boolean> subquery = criteriaBuilder.createQuery(Boolean.class).subquery(Boolean.class);
Root<DatasetFieldType> parentRoot = subquery.from(DatasetFieldType.class);

subquery.select(criteriaBuilder.literal(true))
.where(
criteriaBuilder.equal(parentRoot, datasetFieldTypeRoot.get("parentDatasetFieldType")),
criteriaBuilder.or(
criteriaBuilder.isNull(parentRoot.get("required")),
criteriaBuilder.isTrue(parentRoot.get("required"))
)
);

// Predicate to check that either the parentDatasetFieldType meets the condition or doesn't exist (is null).
Predicate parentCondition = criteriaBuilder.or(
criteriaBuilder.exists(subquery),
criteriaBuilder.isNull(datasetFieldTypeRoot.get("parentDatasetFieldType"))
);

return criteriaBuilder.and(isRequired, parentCondition);
}
}
39 changes: 17 additions & 22 deletions src/main/java/edu/harvard/iq/dataverse/util/json/JsonPrinter.java
Original file line number Diff line number Diff line change
Expand Up @@ -633,31 +633,26 @@ public static JsonObjectBuilder json(MetadataBlock metadataBlock) {
}

public static JsonObjectBuilder json(MetadataBlock metadataBlock, boolean printOnlyDisplayedOnCreateDatasetFieldTypes, Dataverse ownerDataverse) {
JsonObjectBuilder jsonObjectBuilder = jsonObjectBuilder();
jsonObjectBuilder.add("id", metadataBlock.getId());
jsonObjectBuilder.add("name", metadataBlock.getName());
jsonObjectBuilder.add("displayName", metadataBlock.getDisplayName());
jsonObjectBuilder.add("displayOnCreate", metadataBlock.isDisplayOnCreate());
JsonObjectBuilder jsonObjectBuilder = jsonObjectBuilder()
.add("id", metadataBlock.getId())
.add("name", metadataBlock.getName())
.add("displayName", metadataBlock.getDisplayName())
.add("displayOnCreate", metadataBlock.isDisplayOnCreate());

JsonObjectBuilder fieldsBuilder = Json.createObjectBuilder();
Set<DatasetFieldType> datasetFieldTypes = new TreeSet<>(metadataBlock.getDatasetFieldTypes());

for (DatasetFieldType datasetFieldType : datasetFieldTypes) {
Long datasetFieldTypeId = datasetFieldType.getId();
boolean requiredAsInputLevelInOwnerDataverse = ownerDataverse != null && ownerDataverse.isDatasetFieldTypeRequiredAsInputLevel(datasetFieldTypeId);
boolean includedAsInputLevelInOwnerDataverse = ownerDataverse != null && ownerDataverse.isDatasetFieldTypeIncludedAsInputLevel(datasetFieldTypeId);
boolean isNotInputLevelInOwnerDataverse = ownerDataverse != null && !ownerDataverse.isDatasetFieldTypeInInputLevels(datasetFieldTypeId);
Set<DatasetFieldType> datasetFieldTypes;

DatasetFieldType parentDatasetFieldType = datasetFieldType.getParentDatasetFieldType();
boolean isRequired = parentDatasetFieldType == null ? datasetFieldType.isRequired() : parentDatasetFieldType.isRequired();

boolean displayCondition = printOnlyDisplayedOnCreateDatasetFieldTypes
? (datasetFieldType.isDisplayOnCreate() || isRequired || requiredAsInputLevelInOwnerDataverse)
: ownerDataverse == null || includedAsInputLevelInOwnerDataverse || isNotInputLevelInOwnerDataverse;
if (ownerDataverse != null) {
datasetFieldTypes = new TreeSet<>(datasetFieldService.findAllInMetadataBlockAndDataverse(
metadataBlock, ownerDataverse, printOnlyDisplayedOnCreateDatasetFieldTypes));
} else {
datasetFieldTypes = printOnlyDisplayedOnCreateDatasetFieldTypes
? new TreeSet<>(datasetFieldService.findAllDisplayedOnCreateInMetadataBlock(metadataBlock))
: new TreeSet<>(metadataBlock.getDatasetFieldTypes());
}

if (displayCondition) {
fieldsBuilder.add(datasetFieldType.getName(), json(datasetFieldType, ownerDataverse));
}
JsonObjectBuilder fieldsBuilder = Json.createObjectBuilder();
for (DatasetFieldType datasetFieldType : datasetFieldTypes) {
fieldsBuilder.add(datasetFieldType.getName(), json(datasetFieldType, ownerDataverse));
}

jsonObjectBuilder.add("fields", fieldsBuilder);
Expand Down
59 changes: 43 additions & 16 deletions src/test/java/edu/harvard/iq/dataverse/api/DataversesIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@

import static jakarta.ws.rs.core.Response.Status.*;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasItemInArray;
import static org.junit.jupiter.api.Assertions.*;
Expand Down Expand Up @@ -702,9 +701,9 @@ public void testListMetadataBlocks() {
Response setMetadataBlocksResponse = UtilIT.setMetadataBlocks(dataverseAlias, Json.createArrayBuilder().add("citation").add("astrophysics"), apiToken);
setMetadataBlocksResponse.then().assertThat().statusCode(OK.getStatusCode());

String[] testInputLevelNames = {"geographicCoverage", "country", "city"};
boolean[] testRequiredInputLevels = {false, true, false};
boolean[] testIncludedInputLevels = {false, true, true};
String[] testInputLevelNames = {"geographicCoverage", "country", "city", "notesText"};
boolean[] testRequiredInputLevels = {false, true, false, false};
boolean[] testIncludedInputLevels = {false, true, true, false};
Response updateDataverseInputLevelsResponse = UtilIT.updateDataverseInputLevels(dataverseAlias, testInputLevelNames, testRequiredInputLevels, testIncludedInputLevels, apiToken);
updateDataverseInputLevelsResponse.then().assertThat().statusCode(OK.getStatusCode());

Expand Down Expand Up @@ -774,17 +773,22 @@ public void testListMetadataBlocks() {
// Check dataset fields for the updated input levels are retrieved
int geospatialMetadataBlockIndex = actualMetadataBlockDisplayName1.equals("Geospatial Metadata") ? 0 : actualMetadataBlockDisplayName2.equals("Geospatial Metadata") ? 1 : 2;

// Since the included property of notesText is set to false, we should retrieve the total number of fields minus one
int citationMetadataBlockIndex = geospatialMetadataBlockIndex == 0 ? 1 : 0;
listMetadataBlocksResponse.then().assertThat()
.body(String.format("data[%d].fields.size()", citationMetadataBlockIndex), equalTo(78));

// Since the included property of geographicCoverage is set to false, we should retrieve the total number of fields minus one
listMetadataBlocksResponse.then().assertThat()
.body(String.format("data[%d].fields.size()", geospatialMetadataBlockIndex), equalTo(10));

String actualMetadataField1 = listMetadataBlocksResponse.then().extract().path(String.format("data[%d].fields.geographicCoverage.name", geospatialMetadataBlockIndex));
String actualMetadataField2 = listMetadataBlocksResponse.then().extract().path(String.format("data[%d].fields.country.name", geospatialMetadataBlockIndex));
String actualMetadataField3 = listMetadataBlocksResponse.then().extract().path(String.format("data[%d].fields.city.name", geospatialMetadataBlockIndex));
String actualGeospatialMetadataField1 = listMetadataBlocksResponse.then().extract().path(String.format("data[%d].fields.geographicCoverage.name", geospatialMetadataBlockIndex));
String actualGeospatialMetadataField2 = listMetadataBlocksResponse.then().extract().path(String.format("data[%d].fields.country.name", geospatialMetadataBlockIndex));
String actualGeospatialMetadataField3 = listMetadataBlocksResponse.then().extract().path(String.format("data[%d].fields.city.name", geospatialMetadataBlockIndex));

assertNull(actualMetadataField1);
assertNotNull(actualMetadataField2);
assertNotNull(actualMetadataField3);
assertNull(actualGeospatialMetadataField1);
assertNotNull(actualGeospatialMetadataField2);
assertNotNull(actualGeospatialMetadataField3);

// Existent dataverse and onlyDisplayedOnCreate=true and returnDatasetFieldTypes=true
listMetadataBlocksResponse = UtilIT.listMetadataBlocks(dataverseAlias, true, true, apiToken);
Expand All @@ -807,13 +811,27 @@ public void testListMetadataBlocks() {
listMetadataBlocksResponse.then().assertThat()
.body(String.format("data[%d].fields.size()", geospatialMetadataBlockIndex), equalTo(1));

actualMetadataField1 = listMetadataBlocksResponse.then().extract().path(String.format("data[%d].fields.geographicCoverage.name", geospatialMetadataBlockIndex));
actualMetadataField2 = listMetadataBlocksResponse.then().extract().path(String.format("data[%d].fields.country.name", geospatialMetadataBlockIndex));
actualMetadataField3 = listMetadataBlocksResponse.then().extract().path(String.format("data[%d].fields.city.name", geospatialMetadataBlockIndex));
actualGeospatialMetadataField1 = listMetadataBlocksResponse.then().extract().path(String.format("data[%d].fields.geographicCoverage.name", geospatialMetadataBlockIndex));
actualGeospatialMetadataField2 = listMetadataBlocksResponse.then().extract().path(String.format("data[%d].fields.country.name", geospatialMetadataBlockIndex));
actualGeospatialMetadataField3 = listMetadataBlocksResponse.then().extract().path(String.format("data[%d].fields.city.name", geospatialMetadataBlockIndex));

assertNull(actualGeospatialMetadataField1);
assertNotNull(actualGeospatialMetadataField2);
assertNull(actualGeospatialMetadataField3);

citationMetadataBlockIndex = geospatialMetadataBlockIndex == 0 ? 1 : 0;

// notesText has displayOnCreate=true but has include=false, so should not be retrieved
String notesTextCitationMetadataField = listMetadataBlocksResponse.then().extract().path(String.format("data[%d].fields.notesText.name", citationMetadataBlockIndex));
assertNull(notesTextCitationMetadataField);

// producerName is a conditionally required field, so should not be retrieved
String producerNameCitationMetadataField = listMetadataBlocksResponse.then().extract().path(String.format("data[%d].fields.producerName.name", citationMetadataBlockIndex));
assertNull(producerNameCitationMetadataField);

assertNull(actualMetadataField1);
assertNotNull(actualMetadataField2);
assertNull(actualMetadataField3);
// author is a required field, so should be retrieved
String authorCitationMetadataField = listMetadataBlocksResponse.then().extract().path(String.format("data[%d].fields.author.name", citationMetadataBlockIndex));
assertNotNull(authorCitationMetadataField);

// User has no permissions on the requested dataverse
Response createSecondUserResponse = UtilIT.createRandomUser();
Expand All @@ -825,6 +843,15 @@ public void testListMetadataBlocks() {

listMetadataBlocksResponse = UtilIT.listMetadataBlocks(secondDataverseAlias, true, true, apiToken);
listMetadataBlocksResponse.then().assertThat().statusCode(UNAUTHORIZED.getStatusCode());

// List metadata blocks from Root
listMetadataBlocksResponse = UtilIT.listMetadataBlocks("root", true, true, apiToken);
listMetadataBlocksResponse.then().assertThat().statusCode(OK.getStatusCode());
listMetadataBlocksResponse.then().assertThat()
.statusCode(OK.getStatusCode())
.body("data[0].displayName", equalTo("Citation Metadata"))
.body("data[0].fields", not(equalTo(null)))
.body("data.size()", equalTo(1));
}

@Test
Expand Down
Loading

0 comments on commit 987df46

Please sign in to comment.