Content reports ported from DSpace 6.x (#8598)

* content Reports

* Fixed CheckStyle errors

* Fixed CheckStyle errors

* Fixed CheckStyle errors

* First batch of fixes: mainly Javadoc, and a bit of code re-engineering

* Fixed CheckStyle errors

* Fixed CheckStyle errors in dspace-server-webapp

* Applied requested changes for DSpace code conventions compliance

* Added GET endpoint to Filtered Items report

* Updated to latest version from main branch

* Fixed missing imports

* Fixed CheckStyle errors

* Fixed H2 database initialization

* Fixed unit tests and an integration test

* Fixed CheckStyle errors

* Fixed CheckStyle errors

* Fixed ItemServiceIT test

* Test without collection criterion

* Fixed max result count in integration test

* Disable findByMetadataQuery test to diagnose errors in other tests

* Disabled ContentReportRestRepositoryIT test to validate existing tests

* Re-enable test in dspace-api

* Re-enabled ContentReportRestRepositoryIT tests to diagnose failures

* Fixed item matching in the second test

* Fixed JSON path error

* Use projections to trigger embedding the owning collection in ItemRest only for the Filtered Items report

* Fixed usage of allowEmbedding() through non-null arguments

* Exclude owning collection from ItemRest when null/empty

* Trying an alternate way to discriminate report-based Item conversions

* Fixed embedded owning collection management

* Replaced ItemConverter with correct version

* Fixed Filtered Collections test in ContentReportRestRepositoryIT

* Fixed test

* Transferred owning collection to a separate class FilteredItemRest

* Rollback to DSpace repo version

* Fixed matcher for Filtered Collections summary

* Fixed matcher for Filtered Collections summary (take 2)

* Add printing mock request results to diagnose remaining problems

* Try logging output through System.err

* Cancelled attempt to print JSON results (does nothing)

* Attempt to fix ContentReportRestRepositoryIT tests

* Removed predefined UUIDs and handles

* Fixed import formatting

* Fixed expected results in ContentReportRestRepositoryIT

* Switched to a custom matcher for the Filtered Item report test

* Fixed import format

* Fixed JSON collection matching in Filtered Items test

* Fixed Filtered Items matcher

* Fixed expected result

* Fixed the test for now...

* Fixed test again

* Disabled non-working test

* Fixed a few typos

* Moved Filtered Collections report business logic to dspace-api

* Fixed outdated controller

* Fixed import and lost @Ignore annotation

* Retrieved a lost test correction

* Fixed Filtered Collections test

* Reverted to the last working version (except for 2nd test, which remains
disabled)

* Moved Filtered Items report business logic to dspace-api

* Fixed import style

* Added switch to enable/disable Content Reports

* Fixed an out-of-date class

* Removed unused imports

* Fixed activation configuration for Content Reports

* Added missing @Test annotation

* A forgotten Hibernate dialect configuration. I also removed obsolete
Oracle settings configuration.

* Switched to GET requests for Content Reports

* Switched to GET requests for Content Reports

* Fixed styling in imports

* Fixed imports

* Cleaned deprecated code

* Simplified regex since trim() method is invoked on each token thereafter

* Added Javadoc in the interface.

* Relocated Content Reports configuration into a new file

* Added "unauthorized" tests and cleaned up code repetitions

* Fixed parameter according to Javadoc

* Fixed Filtered Items test

* Use of @ConditionalOnProperty annotation

* Rolled back to manual parameter managing

* Second try on @ConditionalOnProperty, with proper test configuration

* Rolled back (again) to manual service activation checking (needed for
proper behaviour depending on activation and authorization)

* Eliminated inheritance between FilteredItemRest and ItemRest

* Re-established the type property in FilteredItemRest (and in
FilterCollectionRest for uniformity).

---------

Co-authored-by: Jean-François Morin <jean-francois.morin@bibl.ulaval.ca>
This commit is contained in:
jeffmorin
2024-02-28 12:48:16 -05:00
committed by GitHub
parent ee42ed5b3b
commit 1a3c0726dc
43 changed files with 3876 additions and 142 deletions

View File

@@ -49,6 +49,7 @@ import org.dspace.content.service.MetadataSchemaService;
import org.dspace.content.service.RelationshipService; import org.dspace.content.service.RelationshipService;
import org.dspace.content.service.WorkspaceItemService; import org.dspace.content.service.WorkspaceItemService;
import org.dspace.content.virtual.VirtualMetadataPopulator; import org.dspace.content.virtual.VirtualMetadataPopulator;
import org.dspace.contentreport.QueryPredicate;
import org.dspace.core.Constants; import org.dspace.core.Constants;
import org.dspace.core.Context; import org.dspace.core.Context;
import org.dspace.core.LogHelper; import org.dspace.core.LogHelper;
@@ -175,7 +176,6 @@ public class ItemServiceImpl extends DSpaceObjectServiceImpl<Item> implements It
private QAEventsDAO qaEventsDao; private QAEventsDAO qaEventsDao;
protected ItemServiceImpl() { protected ItemServiceImpl() {
super();
} }
@Override @Override
@@ -275,9 +275,8 @@ public class ItemServiceImpl extends DSpaceObjectServiceImpl<Item> implements It
+ template.getID())); + template.getID()));
return template; return template;
} else {
return collection.getTemplateItem();
} }
return collection.getTemplateItem();
} }
@Override @Override
@@ -1190,9 +1189,8 @@ public class ItemServiceImpl extends DSpaceObjectServiceImpl<Item> implements It
if (item.getOwningCollection() == null) { if (item.getOwningCollection() == null) {
if (!isInProgressSubmission(context, item)) { if (!isInProgressSubmission(context, item)) {
return true; return true;
} else {
return false;
} }
return false;
} }
return collectionService.canEditBoolean(context, item.getOwningCollection(), false); return collectionService.canEditBoolean(context, item.getOwningCollection(), false);
@@ -1284,8 +1282,8 @@ prevent the generation of resource policy entry values with null dspace_object a
if (!authorizeService if (!authorizeService
.isAnIdenticalPolicyAlreadyInPlace(context, dso, defaultPolicy.getGroup(), Constants.READ, .isAnIdenticalPolicyAlreadyInPlace(context, dso, defaultPolicy.getGroup(), Constants.READ,
defaultPolicy.getID()) && defaultPolicy.getID()) &&
(!appendMode && this.isNotAlreadyACustomRPOfThisTypeOnDSO(context, dso) || (!appendMode && isNotAlreadyACustomRPOfThisTypeOnDSO(context, dso) ||
appendMode && this.shouldBeAppended(context, dso, defaultPolicy))) { appendMode && shouldBeAppended(context, dso, defaultPolicy))) {
ResourcePolicy newPolicy = resourcePolicyService.clone(context, defaultPolicy); ResourcePolicy newPolicy = resourcePolicyService.clone(context, defaultPolicy);
newPolicy.setdSpaceObject(dso); newPolicy.setdSpaceObject(dso);
newPolicy.setAction(Constants.READ); newPolicy.setAction(Constants.READ);
@@ -1384,9 +1382,8 @@ prevent the generation of resource policy entry values with null dspace_object a
if (Item.ANY.equals(value)) { if (Item.ANY.equals(value)) {
return itemDAO.findByMetadataField(context, mdf, null, true); return itemDAO.findByMetadataField(context, mdf, null, true);
} else {
return itemDAO.findByMetadataField(context, mdf, value, true);
} }
return itemDAO.findByMetadataField(context, mdf, value, true);
} }
@Override @Override
@@ -1430,9 +1427,24 @@ prevent the generation of resource policy entry values with null dspace_object a
if (Item.ANY.equals(value)) { if (Item.ANY.equals(value)) {
return itemDAO.findByMetadataField(context, mdf, null, true); return itemDAO.findByMetadataField(context, mdf, null, true);
} else {
return itemDAO.findByMetadataField(context, mdf, value, true);
} }
return itemDAO.findByMetadataField(context, mdf, value, true);
}
@Override
public List<Item> findByMetadataQuery(Context context, List<QueryPredicate> queryPredicates,
List<UUID> collectionUuids, long offset, int limit)
throws SQLException {
return itemDAO.findByMetadataQuery(context, queryPredicates, collectionUuids, "text_value ~ ?",
offset, limit);
}
@Override
public long countForMetadataQuery(Context context, List<QueryPredicate> queryPredicates,
List<UUID> collectionUuids)
throws SQLException {
return itemDAO.countForMetadataQuery(context, queryPredicates, collectionUuids, "text_value ~ ?");
} }
@Override @Override
@@ -1498,20 +1510,19 @@ prevent the generation of resource policy entry values with null dspace_object a
Collection ownCollection = item.getOwningCollection(); Collection ownCollection = item.getOwningCollection();
if (ownCollection != null) { if (ownCollection != null) {
return ownCollection; return ownCollection;
} else {
InProgressSubmission inprogress = ContentServiceFactory.getInstance().getWorkspaceItemService()
.findByItem(context,
item);
if (inprogress == null) {
inprogress = WorkflowServiceFactory.getInstance().getWorkflowItemService().findByItem(context, item);
}
if (inprogress != null) {
return inprogress.getCollection();
}
// is a template item?
return item.getTemplateItemOf();
} }
InProgressSubmission inprogress = ContentServiceFactory.getInstance().getWorkspaceItemService()
.findByItem(context,
item);
if (inprogress == null) {
inprogress = WorkflowServiceFactory.getInstance().getWorkflowItemService().findByItem(context, item);
}
if (inprogress != null) {
return inprogress.getCollection();
}
// is a template item?
return item.getTemplateItemOf();
} }
@Override @Override
@@ -1611,9 +1622,8 @@ prevent the generation of resource policy entry values with null dspace_object a
try { try {
if (StringUtils.isNumeric(id)) { if (StringUtils.isNumeric(id)) {
return findByLegacyId(context, Integer.parseInt(id)); return findByLegacyId(context, Integer.parseInt(id));
} else {
return find(context, UUID.fromString(id));
} }
return find(context, UUID.fromString(id));
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
// Not a valid legacy ID or valid UUID // Not a valid legacy ID or valid UUID
return null; return null;

View File

@@ -11,11 +11,13 @@ import java.sql.SQLException;
import java.util.Date; import java.util.Date;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.UUID;
import org.dspace.content.Collection; import org.dspace.content.Collection;
import org.dspace.content.Community; import org.dspace.content.Community;
import org.dspace.content.Item; import org.dspace.content.Item;
import org.dspace.content.MetadataField; import org.dspace.content.MetadataField;
import org.dspace.contentreport.QueryPredicate;
import org.dspace.core.Context; import org.dspace.core.Context;
import org.dspace.eperson.EPerson; import org.dspace.eperson.EPerson;
@@ -27,12 +29,11 @@ import org.dspace.eperson.EPerson;
* @author kevinvandevelde at atmire.com * @author kevinvandevelde at atmire.com
*/ */
public interface ItemDAO extends DSpaceObjectLegacySupportDAO<Item> { public interface ItemDAO extends DSpaceObjectLegacySupportDAO<Item> {
public Iterator<Item> findAll(Context context, boolean archived) throws SQLException; Iterator<Item> findAll(Context context, boolean archived) throws SQLException;
public Iterator<Item> findAll(Context context, boolean archived, int limit, int offset) throws SQLException; Iterator<Item> findAll(Context context, boolean archived, int limit, int offset) throws SQLException;
@Deprecated @Deprecated Iterator<Item> findAll(Context context, boolean archived, boolean withdrawn) throws SQLException;
public Iterator<Item> findAll(Context context, boolean archived, boolean withdrawn) throws SQLException;
/** /**
* Find all items that are: * Find all items that are:
@@ -45,7 +46,7 @@ public interface ItemDAO extends DSpaceObjectLegacySupportDAO<Item> {
* @return iterator over all regular items. * @return iterator over all regular items.
* @throws SQLException if database error. * @throws SQLException if database error.
*/ */
public Iterator<Item> findAllRegularItems(Context context) throws SQLException; Iterator<Item> findAllRegularItems(Context context) throws SQLException;
/** /**
* Find all Items modified since a Date. * Find all Items modified since a Date.
@@ -55,10 +56,10 @@ public interface ItemDAO extends DSpaceObjectLegacySupportDAO<Item> {
* @return iterator over items * @return iterator over items
* @throws SQLException if database error * @throws SQLException if database error
*/ */
public Iterator<Item> findByLastModifiedSince(Context context, Date since) Iterator<Item> findByLastModifiedSince(Context context, Date since)
throws SQLException; throws SQLException;
public Iterator<Item> findBySubmitter(Context context, EPerson eperson) throws SQLException; Iterator<Item> findBySubmitter(Context context, EPerson eperson) throws SQLException;
/** /**
* Find all the items by a given submitter. The order is * Find all the items by a given submitter. The order is
@@ -70,19 +71,40 @@ public interface ItemDAO extends DSpaceObjectLegacySupportDAO<Item> {
* @return an iterator over the items submitted by eperson * @return an iterator over the items submitted by eperson
* @throws SQLException if database error * @throws SQLException if database error
*/ */
public Iterator<Item> findBySubmitter(Context context, EPerson eperson, boolean retrieveAllItems) Iterator<Item> findBySubmitter(Context context, EPerson eperson, boolean retrieveAllItems)
throws SQLException; throws SQLException;
public Iterator<Item> findBySubmitter(Context context, EPerson eperson, MetadataField metadataField, int limit) Iterator<Item> findBySubmitter(Context context, EPerson eperson, MetadataField metadataField, int limit)
throws SQLException; throws SQLException;
public Iterator<Item> findByMetadataField(Context context, MetadataField metadataField, String value, Iterator<Item> findByMetadataField(Context context, MetadataField metadataField, String value,
boolean inArchive) throws SQLException; boolean inArchive) throws SQLException;
public Iterator<Item> findByAuthorityValue(Context context, MetadataField metadataField, String authority, /**
* Returns all the Items that belong to the specified aollections (if any)
* and match the provided predicates.
* @param context The relevant DSpace context
* @param queryPredicates List of predicates that returned items are required to match
* @param collectionUuids UUIDs of the collections to search.
* If none are provided, the entire repository will be searched.
* @param regexClause Syntactic expression used to query the database using a regular expression
* (e.g.: "text_value ~ ?")
* @param offset The offset for the query
* @param limit Maximum number of items to return
* @return A list containing the items that match the provided criteria
* @throws SQLException if something goes wrong
*/
List<Item> findByMetadataQuery(Context context, List<QueryPredicate> queryPredicates,
List<UUID> collectionUuids, String regexClause,
long offset, int limit) throws SQLException;
long countForMetadataQuery(Context context, List<QueryPredicate> queryPredicates,
List<UUID> collectionUuids, String regexClause) throws SQLException;
Iterator<Item> findByAuthorityValue(Context context, MetadataField metadataField, String authority,
boolean inArchive) throws SQLException; boolean inArchive) throws SQLException;
public Iterator<Item> findArchivedByCollection(Context context, Collection collection, Integer limit, Iterator<Item> findArchivedByCollection(Context context, Collection collection, Integer limit,
Integer offset) throws SQLException; Integer offset) throws SQLException;
/** /**
@@ -95,7 +117,7 @@ public interface ItemDAO extends DSpaceObjectLegacySupportDAO<Item> {
* @return An iterator containing the items for which the constraints hold true * @return An iterator containing the items for which the constraints hold true
* @throws SQLException If something goes wrong * @throws SQLException If something goes wrong
*/ */
public Iterator<Item> findArchivedByCollectionExcludingOwning(Context context, Collection collection, Integer limit, Iterator<Item> findArchivedByCollectionExcludingOwning(Context context, Collection collection, Integer limit,
Integer offset) throws SQLException; Integer offset) throws SQLException;
/** /**
@@ -106,11 +128,11 @@ public interface ItemDAO extends DSpaceObjectLegacySupportDAO<Item> {
* @return The total amount of items that fit the constraints * @return The total amount of items that fit the constraints
* @throws SQLException If something goes wrong * @throws SQLException If something goes wrong
*/ */
public int countArchivedByCollectionExcludingOwning(Context context, Collection collection) throws SQLException; int countArchivedByCollectionExcludingOwning(Context context, Collection collection) throws SQLException;
public Iterator<Item> findAllByCollection(Context context, Collection collection) throws SQLException; Iterator<Item> findAllByCollection(Context context, Collection collection) throws SQLException;
public Iterator<Item> findAllByCollection(Context context, Collection collection, Integer limit, Integer offset) Iterator<Item> findAllByCollection(Context context, Collection collection, Integer limit, Integer offset)
throws SQLException; throws SQLException;
/** /**
@@ -123,7 +145,7 @@ public interface ItemDAO extends DSpaceObjectLegacySupportDAO<Item> {
* @return item count * @return item count
* @throws SQLException if database error * @throws SQLException if database error
*/ */
public int countItems(Context context, Collection collection, boolean includeArchived, boolean includeWithdrawn) int countItems(Context context, Collection collection, boolean includeArchived, boolean includeWithdrawn)
throws SQLException; throws SQLException;
/** /**
@@ -139,7 +161,7 @@ public interface ItemDAO extends DSpaceObjectLegacySupportDAO<Item> {
* @return item count * @return item count
* @throws SQLException if database error * @throws SQLException if database error
*/ */
public int countItems(Context context, List<Collection> collections, boolean includeArchived, int countItems(Context context, List<Collection> collections, boolean includeArchived,
boolean includeWithdrawn) throws SQLException; boolean includeWithdrawn) throws SQLException;
/** /**
@@ -153,7 +175,7 @@ public interface ItemDAO extends DSpaceObjectLegacySupportDAO<Item> {
* @return iterator over items * @return iterator over items
* @throws SQLException if database error * @throws SQLException if database error
*/ */
public Iterator<Item> findAll(Context context, boolean archived, Iterator<Item> findAll(Context context, boolean archived,
boolean withdrawn, boolean discoverable, Date lastModified) boolean withdrawn, boolean discoverable, Date lastModified)
throws SQLException; throws SQLException;
@@ -187,7 +209,7 @@ public interface ItemDAO extends DSpaceObjectLegacySupportDAO<Item> {
* @return count of items * @return count of items
* @throws SQLException if database error * @throws SQLException if database error
*/ */
public int countItems(Context context, EPerson submitter, boolean includeArchived, boolean includeWithdrawn) int countItems(Context context, EPerson submitter, boolean includeArchived, boolean includeWithdrawn)
throws SQLException; throws SQLException;
} }

View File

@@ -8,25 +8,36 @@
package org.dspace.content.dao.impl; package org.dspace.content.dao.impl;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.UUID;
import javax.persistence.Query; import javax.persistence.Query;
import javax.persistence.TemporalType; import javax.persistence.TemporalType;
import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaBuilder.In;
import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root; import javax.persistence.criteria.Root;
import javax.persistence.criteria.Subquery;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.dspace.content.Collection; import org.dspace.content.Collection;
import org.dspace.content.DSpaceObject_;
import org.dspace.content.Item; import org.dspace.content.Item;
import org.dspace.content.Item_; import org.dspace.content.Item_;
import org.dspace.content.MetadataField; import org.dspace.content.MetadataField;
import org.dspace.content.MetadataValue;
import org.dspace.content.MetadataValue_;
import org.dspace.content.dao.ItemDAO; import org.dspace.content.dao.ItemDAO;
import org.dspace.contentreport.QueryOperator;
import org.dspace.contentreport.QueryPredicate;
import org.dspace.core.AbstractHibernateDSODAO; import org.dspace.core.AbstractHibernateDSODAO;
import org.dspace.core.Context; import org.dspace.core.Context;
import org.dspace.eperson.EPerson; import org.dspace.eperson.EPerson;
import org.dspace.util.JpaCriteriaBuilderKit;
/** /**
* Hibernate implementation of the Database Access Object interface class for the Item object. * Hibernate implementation of the Database Access Object interface class for the Item object.
@@ -39,7 +50,6 @@ public class ItemDAOImpl extends AbstractHibernateDSODAO<Item> implements ItemDA
private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(ItemDAOImpl.class); private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(ItemDAOImpl.class);
protected ItemDAOImpl() { protected ItemDAOImpl() {
super();
} }
@Override @Override
@@ -163,6 +173,105 @@ public class ItemDAOImpl extends AbstractHibernateDSODAO<Item> implements ItemDA
return iterate(query); return iterate(query);
} }
@Override
public List<Item> findByMetadataQuery(Context context, List<QueryPredicate> queryPredicates,
List<UUID> collectionUuids, String regexClause, long offset, int limit) throws SQLException {
CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context);
CriteriaQuery<Item> criteriaQuery = getCriteriaQuery(criteriaBuilder, Item.class);
Root<Item> itemRoot = criteriaQuery.from(Item.class);
criteriaQuery.select(itemRoot);
List<Predicate> predicates = toPredicates(criteriaBuilder, criteriaQuery, itemRoot,
queryPredicates, collectionUuids, regexClause);
criteriaQuery.where(criteriaBuilder.and(predicates.stream().toArray(Predicate[]::new)));
criteriaQuery.orderBy(criteriaBuilder.asc(itemRoot.get(DSpaceObject_.id)));
criteriaQuery.groupBy(itemRoot.get(DSpaceObject_.id));
try {
return list(context, criteriaQuery, false, Item.class, limit, (int) offset);
} catch (Exception e) {
e.printStackTrace();
throw e;
}
}
@Override
public long countForMetadataQuery(Context context, List<QueryPredicate> queryPredicates,
List<UUID> collectionUuids, String regexClause) throws SQLException {
// Build the query infrastructure
CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context);
CriteriaQuery<Item> criteriaQuery = getCriteriaQuery(criteriaBuilder, Item.class);
// Select
Root<Item> itemRoot = criteriaQuery.from(Item.class);
// Apply the selected predicates
List<Predicate> predicates = toPredicates(criteriaBuilder, criteriaQuery, itemRoot,
queryPredicates, collectionUuids, regexClause);
criteriaQuery.where(criteriaBuilder.and(predicates.stream().toArray(Predicate[]::new)));
// Execute the query
return countLong(context, criteriaQuery, criteriaBuilder, itemRoot);
}
private <T> List<Predicate> toPredicates(CriteriaBuilder criteriaBuilder, CriteriaQuery<T> query,
Root<Item> root, List<QueryPredicate> queryPredicates,
List<UUID> collectionUuids, String regexClause) {
List<Predicate> predicates = new ArrayList<>();
if (!collectionUuids.isEmpty()) {
Subquery<Collection> scollQuery = query.subquery(Collection.class);
Root<Collection> collRoot = scollQuery.from(Collection.class);
In<UUID> inColls = criteriaBuilder.in(collRoot.get(DSpaceObject_.ID));
collectionUuids.forEach(inColls::value);
scollQuery.select(collRoot.get(DSpaceObject_.ID))
.where(criteriaBuilder.and(
criteriaBuilder.equal(collRoot.get(DSpaceObject_.ID),
root.get(Item_.OWNING_COLLECTION).get(DSpaceObject_.ID)),
collRoot.get(DSpaceObject_.ID).in(collectionUuids)
));
predicates.add(criteriaBuilder.exists(scollQuery));
}
for (int i = 0; i < queryPredicates.size(); i++) {
QueryPredicate predicate = queryPredicates.get(i);
QueryOperator op = predicate.getOperator();
if (op == null) {
log.warn("Skipping Invalid Operator: null");
continue;
}
if (op.getUsesRegex()) {
if (regexClause.isEmpty()) {
log.warn("Skipping Unsupported Regex Operator: " + op);
continue;
}
}
List<Predicate> mvPredicates = new ArrayList<>();
Subquery<MetadataValue> mvQuery = query.subquery(MetadataValue.class);
Root<MetadataValue> mvRoot = mvQuery.from(MetadataValue.class);
mvPredicates.add(criteriaBuilder.equal(
mvRoot.get(MetadataValue_.D_SPACE_OBJECT), root.get(DSpaceObject_.ID)));
if (!predicate.getFields().isEmpty()) {
In<MetadataField> inFields = criteriaBuilder.in(mvRoot.get(MetadataValue_.METADATA_FIELD));
predicate.getFields().forEach(inFields::value);
mvPredicates.add(inFields);
}
JpaCriteriaBuilderKit<MetadataValue> jpaKit = new JpaCriteriaBuilderKit<>(criteriaBuilder, mvQuery, mvRoot);
mvPredicates.add(op.buildJpaPredicate(predicate.getValue(), regexClause, jpaKit));
mvQuery.select(mvRoot.get(MetadataValue_.D_SPACE_OBJECT))
.where(mvPredicates.stream().toArray(Predicate[]::new));
if (op.getNegate()) {
predicates.add(criteriaBuilder.not(criteriaBuilder.exists(mvQuery)));
} else {
predicates.add(criteriaBuilder.exists(mvQuery));
}
}
log.debug(String.format("Running custom query with %d filters", queryPredicates.size()));
return predicates;
}
@Override @Override
public Iterator<Item> findByAuthorityValue(Context context, MetadataField metadataField, String authority, public Iterator<Item> findByAuthorityValue(Context context, MetadataField metadataField, String authority,
boolean inArchive) throws SQLException { boolean inArchive) throws SQLException {
@@ -197,22 +306,22 @@ public class ItemDAOImpl extends AbstractHibernateDSODAO<Item> implements ItemDA
public Iterator<Item> findArchivedByCollectionExcludingOwning(Context context, Collection collection, Integer limit, public Iterator<Item> findArchivedByCollectionExcludingOwning(Context context, Collection collection, Integer limit,
Integer offset) throws SQLException { Integer offset) throws SQLException {
CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context);
CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, Item.class); CriteriaQuery<Item> criteriaQuery = getCriteriaQuery(criteriaBuilder, Item.class);
Root<Item> itemRoot = criteriaQuery.from(Item.class); Root<Item> itemRoot = criteriaQuery.from(Item.class);
criteriaQuery.select(itemRoot); criteriaQuery.select(itemRoot);
criteriaQuery.where(criteriaBuilder.and( criteriaQuery.where(criteriaBuilder.and(
criteriaBuilder.notEqual(itemRoot.get(Item_.owningCollection), collection), criteriaBuilder.notEqual(itemRoot.get(Item_.owningCollection), collection),
criteriaBuilder.isMember(collection, itemRoot.get(Item_.collections)), criteriaBuilder.isMember(collection, itemRoot.get(Item_.collections)),
criteriaBuilder.isTrue(itemRoot.get(Item_.inArchive)))); criteriaBuilder.isTrue(itemRoot.get(Item_.inArchive))));
criteriaQuery.orderBy(criteriaBuilder.asc(itemRoot.get(Item_.id))); criteriaQuery.orderBy(criteriaBuilder.asc(itemRoot.get(DSpaceObject_.id)));
criteriaQuery.groupBy(itemRoot.get(Item_.id)); criteriaQuery.groupBy(itemRoot.get(DSpaceObject_.id));
return list(context, criteriaQuery, false, Item.class, limit, offset).iterator(); return list(context, criteriaQuery, false, Item.class, limit, offset).iterator();
} }
@Override @Override
public int countArchivedByCollectionExcludingOwning(Context context, Collection collection) throws SQLException { public int countArchivedByCollectionExcludingOwning(Context context, Collection collection) throws SQLException {
CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context);
CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, Item.class); CriteriaQuery<Item> criteriaQuery = getCriteriaQuery(criteriaBuilder, Item.class);
Root<Item> itemRoot = criteriaQuery.from(Item.class); Root<Item> itemRoot = criteriaQuery.from(Item.class);
criteriaQuery.select(itemRoot); criteriaQuery.select(itemRoot);
criteriaQuery.where(criteriaBuilder.and( criteriaQuery.where(criteriaBuilder.and(

View File

@@ -26,6 +26,7 @@ import org.dspace.content.Item;
import org.dspace.content.MetadataValue; import org.dspace.content.MetadataValue;
import org.dspace.content.Thumbnail; import org.dspace.content.Thumbnail;
import org.dspace.content.WorkspaceItem; import org.dspace.content.WorkspaceItem;
import org.dspace.contentreport.QueryPredicate;
import org.dspace.core.Context; import org.dspace.core.Context;
import org.dspace.discovery.SearchServiceException; import org.dspace.discovery.SearchServiceException;
import org.dspace.eperson.EPerson; import org.dspace.eperson.EPerson;
@@ -41,7 +42,7 @@ import org.dspace.eperson.Group;
public interface ItemService public interface ItemService
extends DSpaceObjectService<Item>, DSpaceObjectLegacySupportService<Item> { extends DSpaceObjectService<Item>, DSpaceObjectLegacySupportService<Item> {
public Thumbnail getThumbnail(Context context, Item item, boolean requireOriginal) throws SQLException; Thumbnail getThumbnail(Context context, Item item, boolean requireOriginal) throws SQLException;
/** /**
* Create a new item, with a new internal ID. Authorization is done * Create a new item, with a new internal ID. Authorization is done
@@ -53,7 +54,7 @@ public interface ItemService
* @throws SQLException if database error * @throws SQLException if database error
* @throws AuthorizeException if authorization error * @throws AuthorizeException if authorization error
*/ */
public Item create(Context context, WorkspaceItem workspaceItem) throws SQLException, AuthorizeException; Item create(Context context, WorkspaceItem workspaceItem) throws SQLException, AuthorizeException;
/** /**
* Create a new item, with a provided ID. Authorisation is done * Create a new item, with a provided ID. Authorisation is done
@@ -66,7 +67,7 @@ public interface ItemService
* @throws SQLException if database error * @throws SQLException if database error
* @throws AuthorizeException if authorization error * @throws AuthorizeException if authorization error
*/ */
public Item create(Context context, WorkspaceItem workspaceItem, UUID uuid) throws SQLException, AuthorizeException; Item create(Context context, WorkspaceItem workspaceItem, UUID uuid) throws SQLException, AuthorizeException;
/** /**
* Create an empty template item for this collection. If one already exists, * Create an empty template item for this collection. If one already exists,
@@ -80,7 +81,7 @@ public interface ItemService
* @throws SQLException if database error * @throws SQLException if database error
* @throws AuthorizeException if authorization error * @throws AuthorizeException if authorization error
*/ */
public Item createTemplateItem(Context context, Collection collection) throws SQLException, AuthorizeException; Item createTemplateItem(Context context, Collection collection) throws SQLException, AuthorizeException;
/** /**
* Get all the items in the archive. Only items with the "in archive" flag * Get all the items in the archive. Only items with the "in archive" flag
@@ -90,7 +91,7 @@ public interface ItemService
* @return an iterator over the items in the archive. * @return an iterator over the items in the archive.
* @throws SQLException if database error * @throws SQLException if database error
*/ */
public Iterator<Item> findAll(Context context) throws SQLException; Iterator<Item> findAll(Context context) throws SQLException;
/** /**
* Get all the items in the archive. Only items with the "in archive" flag * Get all the items in the archive. Only items with the "in archive" flag
@@ -102,7 +103,7 @@ public interface ItemService
* @return an iterator over the items in the archive. * @return an iterator over the items in the archive.
* @throws SQLException if database error * @throws SQLException if database error
*/ */
public Iterator<Item> findAll(Context context, Integer limit, Integer offset) throws SQLException; Iterator<Item> findAll(Context context, Integer limit, Integer offset) throws SQLException;
/** /**
* Get all "final" items in the archive, both archived ("in archive" flag) or * Get all "final" items in the archive, both archived ("in archive" flag) or
@@ -112,8 +113,7 @@ public interface ItemService
* @return an iterator over the items in the archive. * @return an iterator over the items in the archive.
* @throws SQLException if database error * @throws SQLException if database error
*/ */
@Deprecated @Deprecated Iterator<Item> findAllUnfiltered(Context context) throws SQLException;
public Iterator<Item> findAllUnfiltered(Context context) throws SQLException;
/** /**
* Find all items that are: * Find all items that are:
@@ -126,7 +126,7 @@ public interface ItemService
* @return iterator over all regular items. * @return iterator over all regular items.
* @throws SQLException if database error. * @throws SQLException if database error.
*/ */
public Iterator<Item> findAllRegularItems(Context context) throws SQLException; Iterator<Item> findAllRegularItems(Context context) throws SQLException;
/** /**
* Find all the items in the archive by a given submitter. The order is * Find all the items in the archive by a given submitter. The order is
@@ -137,7 +137,7 @@ public interface ItemService
* @return an iterator over the items submitted by eperson * @return an iterator over the items submitted by eperson
* @throws SQLException if database error * @throws SQLException if database error
*/ */
public Iterator<Item> findBySubmitter(Context context, EPerson eperson) Iterator<Item> findBySubmitter(Context context, EPerson eperson)
throws SQLException; throws SQLException;
/** /**
@@ -152,7 +152,7 @@ public interface ItemService
* @return an iterator over the items submitted by eperson * @return an iterator over the items submitted by eperson
* @throws SQLException if database error * @throws SQLException if database error
*/ */
public Iterator<Item> findBySubmitter(Context context, EPerson eperson, boolean retrieveAllItems) Iterator<Item> findBySubmitter(Context context, EPerson eperson, boolean retrieveAllItems)
throws SQLException; throws SQLException;
/** /**
@@ -164,7 +164,7 @@ public interface ItemService
* @return an iterator over the items submitted by eperson * @return an iterator over the items submitted by eperson
* @throws SQLException if database error * @throws SQLException if database error
*/ */
public Iterator<Item> findBySubmitterDateSorted(Context context, EPerson eperson, Integer limit) Iterator<Item> findBySubmitterDateSorted(Context context, EPerson eperson, Integer limit)
throws SQLException; throws SQLException;
/** /**
@@ -175,7 +175,7 @@ public interface ItemService
* @return an iterator over the items in the collection. * @return an iterator over the items in the collection.
* @throws SQLException if database error * @throws SQLException if database error
*/ */
public Iterator<Item> findByCollection(Context context, Collection collection) throws SQLException; Iterator<Item> findByCollection(Context context, Collection collection) throws SQLException;
/** /**
* Get all the archived items in this collection. The order is indeterminate. * Get all the archived items in this collection. The order is indeterminate.
@@ -187,7 +187,7 @@ public interface ItemService
* @return an iterator over the items in the collection. * @return an iterator over the items in the collection.
* @throws SQLException if database error * @throws SQLException if database error
*/ */
public Iterator<Item> findByCollection(Context context, Collection collection, Integer limit, Integer offset) Iterator<Item> findByCollection(Context context, Collection collection, Integer limit, Integer offset)
throws SQLException; throws SQLException;
/** /**
@@ -200,7 +200,7 @@ public interface ItemService
* @return an iterator over the items in the collection. * @return an iterator over the items in the collection.
* @throws SQLException if database error * @throws SQLException if database error
*/ */
public Iterator<Item> findByCollectionMapping(Context context, Collection collection, Integer limit, Integer offset) Iterator<Item> findByCollectionMapping(Context context, Collection collection, Integer limit, Integer offset)
throws SQLException; throws SQLException;
/** /**
@@ -211,7 +211,7 @@ public interface ItemService
* @return an iterator over the items in the collection. * @return an iterator over the items in the collection.
* @throws SQLException if database error * @throws SQLException if database error
*/ */
public int countByCollectionMapping(Context context, Collection collection) throws SQLException; int countByCollectionMapping(Context context, Collection collection) throws SQLException;
/** /**
* Get all the items (including private and withdrawn) in this collection. The order is indeterminate. * Get all the items (including private and withdrawn) in this collection. The order is indeterminate.
@@ -223,7 +223,7 @@ public interface ItemService
* @param offset offset value * @param offset offset value
* @throws SQLException if database error * @throws SQLException if database error
*/ */
public Iterator<Item> findAllByCollection(Context context, Collection collection, Integer limit, Integer offset) Iterator<Item> findAllByCollection(Context context, Collection collection, Integer limit, Integer offset)
throws SQLException; throws SQLException;
/** /**
@@ -234,7 +234,7 @@ public interface ItemService
* @return an iterator over the items in the collection. * @return an iterator over the items in the collection.
* @throws SQLException if database error * @throws SQLException if database error
*/ */
public Iterator<Item> findInArchiveOrWithdrawnDiscoverableModifiedSince(Context context, Date since) Iterator<Item> findInArchiveOrWithdrawnDiscoverableModifiedSince(Context context, Date since)
throws SQLException; throws SQLException;
/** /**
@@ -244,7 +244,7 @@ public interface ItemService
* @return an iterator over the items in the collection. * @return an iterator over the items in the collection.
* @throws SQLException if database error * @throws SQLException if database error
*/ */
public Iterator<Item> findInArchiveOrWithdrawnNonDiscoverableModifiedSince(Context context, Date since) Iterator<Item> findInArchiveOrWithdrawnNonDiscoverableModifiedSince(Context context, Date since)
throws SQLException; throws SQLException;
/** /**
@@ -255,7 +255,7 @@ public interface ItemService
* @return an iterator over the items in the collection. * @return an iterator over the items in the collection.
* @throws SQLException if database error * @throws SQLException if database error
*/ */
public Iterator<Item> findAllByCollection(Context context, Collection collection) throws SQLException; Iterator<Item> findAllByCollection(Context context, Collection collection) throws SQLException;
/** /**
* See whether this Item is contained by a given Collection. * See whether this Item is contained by a given Collection.
@@ -265,7 +265,7 @@ public interface ItemService
* @return true if {@code collection} contains this Item. * @return true if {@code collection} contains this Item.
* @throws SQLException if database error * @throws SQLException if database error
*/ */
public boolean isIn(Item item, Collection collection) throws SQLException; boolean isIn(Item item, Collection collection) throws SQLException;
/** /**
* Get the communities this item is in. Returns an unordered array of the * Get the communities this item is in. Returns an unordered array of the
@@ -277,7 +277,7 @@ public interface ItemService
* @return the communities this item is in. * @return the communities this item is in.
* @throws SQLException if database error * @throws SQLException if database error
*/ */
public List<Community> getCommunities(Context context, Item item) throws SQLException; List<Community> getCommunities(Context context, Item item) throws SQLException;
/** /**
@@ -288,7 +288,7 @@ public interface ItemService
* @return the bundles in an unordered array * @return the bundles in an unordered array
* @throws SQLException if database error * @throws SQLException if database error
*/ */
public List<Bundle> getBundles(Item item, String name) throws SQLException; List<Bundle> getBundles(Item item, String name) throws SQLException;
/** /**
* Add an existing bundle to this item. This has immediate effect. * Add an existing bundle to this item. This has immediate effect.
@@ -299,7 +299,7 @@ public interface ItemService
* @throws SQLException if database error * @throws SQLException if database error
* @throws AuthorizeException if authorization error * @throws AuthorizeException if authorization error
*/ */
public void addBundle(Context context, Item item, Bundle bundle) throws SQLException, AuthorizeException; void addBundle(Context context, Item item, Bundle bundle) throws SQLException, AuthorizeException;
/** /**
* Remove a bundle. This may result in the bundle being deleted, if the * Remove a bundle. This may result in the bundle being deleted, if the
@@ -312,7 +312,7 @@ public interface ItemService
* @throws AuthorizeException if authorization error * @throws AuthorizeException if authorization error
* @throws IOException if IO error * @throws IOException if IO error
*/ */
public void removeBundle(Context context, Item item, Bundle bundle) throws SQLException, AuthorizeException, void removeBundle(Context context, Item item, Bundle bundle) throws SQLException, AuthorizeException,
IOException; IOException;
/** /**
@@ -325,7 +325,7 @@ public interface ItemService
* @throws AuthorizeException if authorization error * @throws AuthorizeException if authorization error
* @throws IOException if IO error * @throws IOException if IO error
*/ */
public void removeAllBundles(Context context, Item item) throws AuthorizeException, SQLException, IOException; void removeAllBundles(Context context, Item item) throws AuthorizeException, SQLException, IOException;
/** /**
* Create a single bitstream in a new bundle. Provided as a convenience * Create a single bitstream in a new bundle. Provided as a convenience
@@ -340,7 +340,7 @@ public interface ItemService
* @throws IOException if IO error * @throws IOException if IO error
* @throws SQLException if database error * @throws SQLException if database error
*/ */
public Bitstream createSingleBitstream(Context context, InputStream is, Item item, String name) Bitstream createSingleBitstream(Context context, InputStream is, Item item, String name)
throws AuthorizeException, IOException, SQLException; throws AuthorizeException, IOException, SQLException;
/** /**
@@ -354,7 +354,7 @@ public interface ItemService
* @throws IOException if IO error * @throws IOException if IO error
* @throws SQLException if database error * @throws SQLException if database error
*/ */
public Bitstream createSingleBitstream(Context context, InputStream is, Item item) Bitstream createSingleBitstream(Context context, InputStream is, Item item)
throws AuthorizeException, IOException, SQLException; throws AuthorizeException, IOException, SQLException;
/** /**
@@ -367,7 +367,7 @@ public interface ItemService
* @return non-internal bitstreams. * @return non-internal bitstreams.
* @throws SQLException if database error * @throws SQLException if database error
*/ */
public List<Bitstream> getNonInternalBitstreams(Context context, Item item) throws SQLException; List<Bitstream> getNonInternalBitstreams(Context context, Item item) throws SQLException;
/** /**
* Remove just the DSpace license from an item This is useful to update the * Remove just the DSpace license from an item This is useful to update the
@@ -382,7 +382,7 @@ public interface ItemService
* @throws AuthorizeException if authorization error * @throws AuthorizeException if authorization error
* @throws IOException if IO error * @throws IOException if IO error
*/ */
public void removeDSpaceLicense(Context context, Item item) throws SQLException, AuthorizeException, void removeDSpaceLicense(Context context, Item item) throws SQLException, AuthorizeException,
IOException; IOException;
/** /**
@@ -394,7 +394,7 @@ public interface ItemService
* @throws AuthorizeException if authorization error * @throws AuthorizeException if authorization error
* @throws IOException if IO error * @throws IOException if IO error
*/ */
public void removeLicenses(Context context, Item item) throws SQLException, AuthorizeException, IOException; void removeLicenses(Context context, Item item) throws SQLException, AuthorizeException, IOException;
/** /**
* Withdraw the item from the archive. It is kept in place, and the content * Withdraw the item from the archive. It is kept in place, and the content
@@ -405,7 +405,7 @@ public interface ItemService
* @throws SQLException if database error * @throws SQLException if database error
* @throws AuthorizeException if authorization error * @throws AuthorizeException if authorization error
*/ */
public void withdraw(Context context, Item item) throws SQLException, AuthorizeException; void withdraw(Context context, Item item) throws SQLException, AuthorizeException;
/** /**
@@ -416,7 +416,7 @@ public interface ItemService
* @throws SQLException if database error * @throws SQLException if database error
* @throws AuthorizeException if authorization error * @throws AuthorizeException if authorization error
*/ */
public void reinstate(Context context, Item item) throws SQLException, AuthorizeException; void reinstate(Context context, Item item) throws SQLException, AuthorizeException;
/** /**
* Return true if this Collection 'owns' this item * Return true if this Collection 'owns' this item
@@ -425,7 +425,7 @@ public interface ItemService
* @param collection Collection * @param collection Collection
* @return true if this Collection owns this item * @return true if this Collection owns this item
*/ */
public boolean isOwningCollection(Item item, Collection collection); boolean isOwningCollection(Item item, Collection collection);
/** /**
* remove all of the policies for item and replace them with a new list of * remove all of the policies for item and replace them with a new list of
@@ -439,7 +439,7 @@ public interface ItemService
* @throws SQLException if database error * @throws SQLException if database error
* @throws AuthorizeException if authorization error * @throws AuthorizeException if authorization error
*/ */
public void replaceAllItemPolicies(Context context, Item item, List<ResourcePolicy> newpolicies) void replaceAllItemPolicies(Context context, Item item, List<ResourcePolicy> newpolicies)
throws SQLException, throws SQLException,
AuthorizeException; AuthorizeException;
@@ -455,7 +455,7 @@ public interface ItemService
* @throws SQLException if database error * @throws SQLException if database error
* @throws AuthorizeException if authorization error * @throws AuthorizeException if authorization error
*/ */
public void replaceAllBitstreamPolicies(Context context, Item item, List<ResourcePolicy> newpolicies) void replaceAllBitstreamPolicies(Context context, Item item, List<ResourcePolicy> newpolicies)
throws SQLException, AuthorizeException; throws SQLException, AuthorizeException;
@@ -469,7 +469,7 @@ public interface ItemService
* @throws SQLException if database error * @throws SQLException if database error
* @throws AuthorizeException if authorization error * @throws AuthorizeException if authorization error
*/ */
public void removeGroupPolicies(Context context, Item item, Group group) throws SQLException, AuthorizeException; void removeGroupPolicies(Context context, Item item, Group group) throws SQLException, AuthorizeException;
/** /**
* Remove all policies on an item and its contents, and replace them with * Remove all policies on an item and its contents, and replace them with
@@ -484,7 +484,7 @@ public interface ItemService
* draconian, but default policies must be enforced. * draconian, but default policies must be enforced.
* @throws AuthorizeException if authorization error * @throws AuthorizeException if authorization error
*/ */
public void inheritCollectionDefaultPolicies(Context context, Item item, Collection collection) void inheritCollectionDefaultPolicies(Context context, Item item, Collection collection)
throws java.sql.SQLException, AuthorizeException; throws java.sql.SQLException, AuthorizeException;
/** /**
@@ -503,7 +503,7 @@ public interface ItemService
* draconian, but default policies must be enforced. * draconian, but default policies must be enforced.
* @throws AuthorizeException if authorization error * @throws AuthorizeException if authorization error
*/ */
public void inheritCollectionDefaultPolicies(Context context, Item item, Collection collection, void inheritCollectionDefaultPolicies(Context context, Item item, Collection collection,
boolean overrideItemReadPolicies) boolean overrideItemReadPolicies)
throws java.sql.SQLException, AuthorizeException; throws java.sql.SQLException, AuthorizeException;
@@ -516,14 +516,14 @@ public interface ItemService
* already applied to the bundle/bitstream. Collection's policies are inherited * already applied to the bundle/bitstream. Collection's policies are inherited
* if there are no other policies defined or if the append mode is defined by * if there are no other policies defined or if the append mode is defined by
* the configuration via the core.authorization.installitem.inheritance-read.append-mode property * the configuration via the core.authorization.installitem.inheritance-read.append-mode property
* *
* @param context DSpace context object * @param context DSpace context object
* @param item Item to adjust policies on * @param item Item to adjust policies on
* @param collection Collection * @param collection Collection
* @throws SQLException If database error * @throws SQLException If database error
* @throws AuthorizeException If authorization error * @throws AuthorizeException If authorization error
*/ */
public void adjustBundleBitstreamPolicies(Context context, Item item, Collection collection) void adjustBundleBitstreamPolicies(Context context, Item item, Collection collection)
throws SQLException, AuthorizeException; throws SQLException, AuthorizeException;
/** /**
@@ -544,7 +544,7 @@ public interface ItemService
* @throws SQLException If database error * @throws SQLException If database error
* @throws AuthorizeException If authorization error * @throws AuthorizeException If authorization error
*/ */
public void adjustBundleBitstreamPolicies(Context context, Item item, Collection collection, void adjustBundleBitstreamPolicies(Context context, Item item, Collection collection,
boolean replaceReadRPWithCollectionRP) boolean replaceReadRPWithCollectionRP)
throws SQLException, AuthorizeException; throws SQLException, AuthorizeException;
@@ -565,7 +565,7 @@ public interface ItemService
* @throws SQLException If database error * @throws SQLException If database error
* @throws AuthorizeException If authorization error * @throws AuthorizeException If authorization error
*/ */
public void adjustBitstreamPolicies(Context context, Item item, Collection collection, Bitstream bitstream) void adjustBitstreamPolicies(Context context, Item item, Collection collection, Bitstream bitstream)
throws SQLException, AuthorizeException; throws SQLException, AuthorizeException;
/** /**
@@ -587,7 +587,7 @@ public interface ItemService
* @throws SQLException If database error * @throws SQLException If database error
* @throws AuthorizeException If authorization error * @throws AuthorizeException If authorization error
*/ */
public void adjustBitstreamPolicies(Context context, Item item, Collection collection, Bitstream bitstream, void adjustBitstreamPolicies(Context context, Item item, Collection collection, Bitstream bitstream,
boolean replaceReadRPWithCollectionRP) boolean replaceReadRPWithCollectionRP)
throws SQLException, AuthorizeException; throws SQLException, AuthorizeException;
@@ -599,14 +599,14 @@ public interface ItemService
* inherited as appropriate. Collection's policies are inherited if there are no * inherited as appropriate. Collection's policies are inherited if there are no
* other policies defined or if the append mode is defined by the configuration * other policies defined or if the append mode is defined by the configuration
* via the core.authorization.installitem.inheritance-read.append-mode property * via the core.authorization.installitem.inheritance-read.append-mode property
* *
* @param context DSpace context object * @param context DSpace context object
* @param item Item to adjust policies on * @param item Item to adjust policies on
* @param collection Collection * @param collection Collection
* @throws SQLException If database error * @throws SQLException If database error
* @throws AuthorizeException If authorization error * @throws AuthorizeException If authorization error
*/ */
public void adjustItemPolicies(Context context, Item item, Collection collection) void adjustItemPolicies(Context context, Item item, Collection collection)
throws SQLException, AuthorizeException; throws SQLException, AuthorizeException;
/** /**
@@ -625,7 +625,7 @@ public interface ItemService
* @throws SQLException If database error * @throws SQLException If database error
* @throws AuthorizeException If authorization error * @throws AuthorizeException If authorization error
*/ */
public void adjustItemPolicies(Context context, Item item, Collection collection, void adjustItemPolicies(Context context, Item item, Collection collection,
boolean replaceReadRPWithCollectionRP) boolean replaceReadRPWithCollectionRP)
throws SQLException, AuthorizeException; throws SQLException, AuthorizeException;
@@ -640,7 +640,7 @@ public interface ItemService
* @throws AuthorizeException if authorization error * @throws AuthorizeException if authorization error
* @throws IOException if IO error * @throws IOException if IO error
*/ */
public void move(Context context, Item item, Collection from, Collection to) void move(Context context, Item item, Collection from, Collection to)
throws SQLException, AuthorizeException, IOException; throws SQLException, AuthorizeException, IOException;
/** /**
@@ -655,7 +655,7 @@ public interface ItemService
* @throws AuthorizeException if authorization error * @throws AuthorizeException if authorization error
* @throws IOException if IO error * @throws IOException if IO error
*/ */
public void move(Context context, Item item, Collection from, Collection to, boolean inheritDefaultPolicies) void move(Context context, Item item, Collection from, Collection to, boolean inheritDefaultPolicies)
throws SQLException, AuthorizeException, IOException; throws SQLException, AuthorizeException, IOException;
/** /**
@@ -666,7 +666,7 @@ public interface ItemService
* bitstreams inside * bitstreams inside
* @throws SQLException if database error * @throws SQLException if database error
*/ */
public boolean hasUploadedFiles(Item item) throws SQLException; boolean hasUploadedFiles(Item item) throws SQLException;
/** /**
* Get the collections this item is not in. * Get the collections this item is not in.
@@ -676,7 +676,7 @@ public interface ItemService
* @return the collections this item is not in, if any. * @return the collections this item is not in, if any.
* @throws SQLException if database error * @throws SQLException if database error
*/ */
public List<Collection> getCollectionsNotLinked(Context context, Item item) throws SQLException; List<Collection> getCollectionsNotLinked(Context context, Item item) throws SQLException;
/** /**
* return TRUE if context's user can edit item, false otherwise * return TRUE if context's user can edit item, false otherwise
@@ -686,7 +686,7 @@ public interface ItemService
* @return boolean true = current user can edit item * @return boolean true = current user can edit item
* @throws SQLException if database error * @throws SQLException if database error
*/ */
public boolean canEdit(Context context, Item item) throws java.sql.SQLException; boolean canEdit(Context context, Item item) throws java.sql.SQLException;
/** /**
* return TRUE if context's user can create new version of the item, false * return TRUE if context's user can create new version of the item, false
@@ -697,7 +697,7 @@ public interface ItemService
* @return boolean true = current user can create new version of the item * @return boolean true = current user can create new version of the item
* @throws SQLException if database error * @throws SQLException if database error
*/ */
public boolean canCreateNewVersion(Context context, Item item) throws SQLException; boolean canCreateNewVersion(Context context, Item item) throws SQLException;
/** /**
* Returns an iterator of in archive items possessing the passed metadata field, or only * Returns an iterator of in archive items possessing the passed metadata field, or only
@@ -712,7 +712,7 @@ public interface ItemService
* @throws SQLException if database error * @throws SQLException if database error
* @throws AuthorizeException if authorization error * @throws AuthorizeException if authorization error
*/ */
public Iterator<Item> findArchivedByMetadataField(Context context, String schema, Iterator<Item> findArchivedByMetadataField(Context context, String schema,
String element, String qualifier, String element, String qualifier,
String value) throws SQLException, AuthorizeException; String value) throws SQLException, AuthorizeException;
@@ -727,7 +727,7 @@ public interface ItemService
* @throws SQLException if database error * @throws SQLException if database error
* @throws AuthorizeException if authorization error * @throws AuthorizeException if authorization error
*/ */
public Iterator<Item> findArchivedByMetadataField(Context context, String metadataField, String value) Iterator<Item> findArchivedByMetadataField(Context context, String metadataField, String value)
throws SQLException, AuthorizeException; throws SQLException, AuthorizeException;
/** /**
@@ -744,10 +744,42 @@ public interface ItemService
* @throws AuthorizeException if authorization error * @throws AuthorizeException if authorization error
* @throws IOException if IO error * @throws IOException if IO error
*/ */
public Iterator<Item> findByMetadataField(Context context, Iterator<Item> findByMetadataField(Context context,
String schema, String element, String qualifier, String value) String schema, String element, String qualifier, String value)
throws SQLException, AuthorizeException, IOException; throws SQLException, AuthorizeException, IOException;
/**
* Returns a list of items that match the given predicates, within the
* specified collections, if any. This querying method is used by the
* Filtered Items report functionality.
* @param context DSpace context object
* @param queryPredicates metadata field predicates
* @param collectionUuids UUIDs of the collections to search
* @param offset position in the list to start returning items
* @param limit maximum number of items to return
* @return a list of matching items in the specified collections,
* or in any collection if no collection UUIDs are provided
* @throws SQLException if a database error occurs
*/
List<Item> findByMetadataQuery(Context context, List<QueryPredicate> queryPredicates,
List<UUID> collectionUuids, long offset, int limit)
throws SQLException;
/**
* Returns the total number of items that match the given predicates, within the
* specified collections, if any. This querying method is used for pagination by the
* Filtered Items report functionality.
* @param context DSpace context object
* @param queryPredicates metadata field predicates
* @param collectionUuids UUIDs of the collections to search
* @return the total number of matching items in the specified collections,
* or in any collection if no collection UUIDs are provided
* @throws SQLException if a database error occurs
*/
long countForMetadataQuery(Context context, List<QueryPredicate> queryPredicates,
List<UUID> collectionUuids)
throws SQLException;
/** /**
* Find all the items in the archive with a given authority key value * Find all the items in the archive with a given authority key value
* in the indicated metadata field. * in the indicated metadata field.
@@ -762,12 +794,12 @@ public interface ItemService
* @throws AuthorizeException if authorization error * @throws AuthorizeException if authorization error
* @throws IOException if IO error * @throws IOException if IO error
*/ */
public Iterator<Item> findByAuthorityValue(Context context, Iterator<Item> findByAuthorityValue(Context context,
String schema, String element, String qualifier, String value) String schema, String element, String qualifier, String value)
throws SQLException, AuthorizeException; throws SQLException, AuthorizeException;
public Iterator<Item> findByMetadataFieldAuthority(Context context, String mdString, String authority) Iterator<Item> findByMetadataFieldAuthority(Context context, String mdString, String authority)
throws SQLException, AuthorizeException; throws SQLException, AuthorizeException;
/** /**
@@ -779,7 +811,7 @@ public interface ItemService
* @param item item * @param item item
* @return true or false * @return true or false
*/ */
public boolean isItemListedForUser(Context context, Item item); boolean isItemListedForUser(Context context, Item item);
/** /**
* counts items in the given collection * counts items in the given collection
@@ -789,7 +821,7 @@ public interface ItemService
* @return total items * @return total items
* @throws SQLException if database error * @throws SQLException if database error
*/ */
public int countItems(Context context, Collection collection) throws SQLException; int countItems(Context context, Collection collection) throws SQLException;
/** /**
* counts all items in the given collection including withdrawn items * counts all items in the given collection including withdrawn items
@@ -799,7 +831,7 @@ public interface ItemService
* @return total items * @return total items
* @throws SQLException if database error * @throws SQLException if database error
*/ */
public int countAllItems(Context context, Collection collection) throws SQLException; int countAllItems(Context context, Collection collection) throws SQLException;
/** /**
* Find all Items modified since a Date. * Find all Items modified since a Date.
@@ -809,7 +841,7 @@ public interface ItemService
* @return iterator over items * @return iterator over items
* @throws SQLException if database error * @throws SQLException if database error
*/ */
public Iterator<Item> findByLastModifiedSince(Context context, Date last) Iterator<Item> findByLastModifiedSince(Context context, Date last)
throws SQLException; throws SQLException;
/** /**
@@ -820,7 +852,7 @@ public interface ItemService
* @return total items * @return total items
* @throws SQLException if database error * @throws SQLException if database error
*/ */
public int countItems(Context context, Community community) throws SQLException; int countItems(Context context, Community community) throws SQLException;
/** /**
* counts all items in the given community including withdrawn * counts all items in the given community including withdrawn
@@ -830,7 +862,7 @@ public interface ItemService
* @return total items * @return total items
* @throws SQLException if database error * @throws SQLException if database error
*/ */
public int countAllItems(Context context, Community community) throws SQLException; int countAllItems(Context context, Community community) throws SQLException;
/** /**
* counts all items * counts all items
@@ -877,7 +909,7 @@ public interface ItemService
* @throws SQLException * @throws SQLException
* @throws SearchServiceException * @throws SearchServiceException
*/ */
public List<Item> findItemsWithEdit(Context context, int offset, int limit) List<Item> findItemsWithEdit(Context context, int offset, int limit)
throws SQLException, SearchServiceException; throws SQLException, SearchServiceException;
/** /**
@@ -887,7 +919,7 @@ public interface ItemService
* @throws SQLException * @throws SQLException
* @throws SearchServiceException * @throws SearchServiceException
*/ */
public int countItemsWithEdit(Context context) throws SQLException, SearchServiceException; int countItemsWithEdit(Context context) throws SQLException, SearchServiceException;
/** /**
* Check if the supplied item is an inprogress submission * Check if the supplied item is an inprogress submission
@@ -947,7 +979,7 @@ public interface ItemService
* relationships. * relationships.
* @return metadata fields that match the parameters * @return metadata fields that match the parameters
*/ */
public List<MetadataValue> getMetadata(Item item, String schema, String element, String qualifier, List<MetadataValue> getMetadata(Item item, String schema, String element, String qualifier,
String lang, boolean enableVirtualMetadata); String lang, boolean enableVirtualMetadata);
/** /**
@@ -955,7 +987,7 @@ public interface ItemService
* @param item the item. * @param item the item.
* @return the label of the entity type, taken from the item metadata, or null if not found. * @return the label of the entity type, taken from the item metadata, or null if not found.
*/ */
public String getEntityTypeLabel(Item item); String getEntityTypeLabel(Item item);
/** /**
* Retrieve the entity type of the given item. * Retrieve the entity type of the given item.
@@ -963,6 +995,6 @@ public interface ItemService
* @param item the item. * @param item the item.
* @return the entity type of the given item, or null if not found. * @return the entity type of the given item, or null if not found.
*/ */
public EntityType getEntityType(Context context, Item item) throws SQLException; EntityType getEntityType(Context context, Item item) throws SQLException;
} }

View File

@@ -0,0 +1,190 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.contentreport;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import org.apache.logging.log4j.Logger;
import org.dspace.content.Collection;
import org.dspace.content.Community;
import org.dspace.content.Item;
import org.dspace.content.MetadataField;
import org.dspace.content.service.CollectionService;
import org.dspace.content.service.ItemService;
import org.dspace.content.service.MetadataFieldService;
import org.dspace.contentreport.service.ContentReportService;
import org.dspace.core.Context;
import org.dspace.services.ConfigurationService;
import org.springframework.beans.factory.annotation.Autowired;
public class ContentReportServiceImpl implements ContentReportService {
private static final Logger log = org.apache.logging.log4j.LogManager
.getLogger(ContentReportServiceImpl.class);
@Autowired
protected ConfigurationService configurationService;
@Autowired
private CollectionService collectionService;
@Autowired
private ItemService itemService;
@Autowired
private MetadataFieldService metadataFieldService;
/**
* Returns <code>true<</code> if Content Reports are enabled.
* @return <code>true<</code> if Content Reports are enabled
*/
@Override
public boolean getEnabled() {
return configurationService.getBooleanProperty("contentreport.enable");
}
/**
* Retrieves item statistics per collection according to a set of Boolean filters.
* @param context DSpace context
* @param filters Set of filters
* @return a list of collections with the requested statistics for each of them
*/
@Override
public List<FilteredCollection> findFilteredCollections(Context context, java.util.Collection<Filter> filters) {
List<FilteredCollection> colls = new ArrayList<>();
try {
List<Collection> collections = collectionService.findAll(context);
for (Collection collection : collections) {
FilteredCollection coll = new FilteredCollection();
coll.setHandle(collection.getHandle());
coll.setLabel(collection.getName());
Community community = collection.getCommunities().stream()
.findFirst()
.orElse(null);
if (community != null) {
coll.setCommunityLabel(community.getName());
coll.setCommunityHandle(community.getHandle());
}
colls.add(coll);
Iterator<Item> items = itemService.findAllByCollection(context, collection);
int nbTotalItems = 0;
while (items.hasNext()) {
Item item = items.next();
nbTotalItems++;
boolean matchesAllFilters = true;
for (Filter filter : filters) {
if (filter.testItem(context, item)) {
coll.addValue(filter, 1);
} else {
// This ensures the requested filter is present in the collection record
// even when there are no matching items.
coll.addValue(filter, 0);
matchesAllFilters = false;
}
}
if (matchesAllFilters) {
coll.addAllFiltersValue(1);
}
}
coll.setTotalItems(nbTotalItems);
coll.seal();
}
} catch (SQLException e) {
log.error("SQLException trying to receive filtered collections statistics", e);
}
return colls;
}
/**
* Retrieves a list of items according to a set of criteria.
* @param context DSpace context
* @param query structured query to find items against
* @return a list of items filtered according to the provided query
*/
@Override
public FilteredItems findFilteredItems(Context context, FilteredItemsQuery query) {
FilteredItems report = new FilteredItems();
List<QueryPredicate> predicates = query.getQueryPredicates();
List<UUID> collectionUuids = getUuidsFromStrings(query.getCollections());
Set<Filter> filters = query.getFilters();
try {
List<Item> items = itemService.findByMetadataQuery(context, predicates, collectionUuids,
query.getOffset(), query.getPageLimit());
items.stream()
.filter(item -> filters.stream().allMatch(f -> f.testItem(context, item)))
.forEach(report::addItem);
} catch (SQLException e) {
log.error(e.getMessage(), e);
}
try {
long count = itemService.countForMetadataQuery(context, predicates, collectionUuids);
report.setItemCount(count);
} catch (SQLException e) {
log.error(e.getMessage(), e);
}
return report;
}
/**
* Converts a metadata field name to a list of {@link MetadataField} instances
* (one if no wildcards are used, possibly more otherwise).
* @param context DSpace context
* @param metadataField field to search for
* @return a corresponding list of {@link MetadataField} entries
*/
@Override
public List<MetadataField> getMetadataFields(org.dspace.core.Context context, String metadataField)
throws SQLException {
List<MetadataField> fields = new ArrayList<>();
if ("*".equals(metadataField)) {
return fields;
}
String schema = "";
String element = "";
String qualifier = null;
String[] parts = metadataField.split("\\.");
if (parts.length > 0) {
schema = parts[0];
}
if (parts.length > 1) {
element = parts[1];
}
if (parts.length > 2) {
qualifier = parts[2];
}
if (Item.ANY.equals(qualifier)) {
fields.addAll(metadataFieldService.findFieldsByElementNameUnqualified(context, schema, element));
} else {
MetadataField mf = metadataFieldService.findByElement(context, schema, element, qualifier);
if (mf != null) {
fields.add(mf);
}
}
return fields;
}
private static List<UUID> getUuidsFromStrings(List<String> collSel) {
List<UUID> uuids = new ArrayList<>();
for (String s: collSel) {
try {
uuids.add(UUID.fromString(s));
} catch (IllegalArgumentException e) {
log.warn("Invalid collection UUID: " + s);
}
}
return uuids;
}
}

View File

@@ -0,0 +1,399 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.contentreport;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiPredicate;
import java.util.stream.Collectors;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.dspace.authorize.factory.AuthorizeServiceFactory;
import org.dspace.authorize.service.AuthorizeService;
import org.dspace.content.Bundle;
import org.dspace.content.Item;
import org.dspace.contentreport.ItemFilterUtil.BundleName;
import org.dspace.core.Context;
import org.dspace.services.factory.DSpaceServicesFactory;
/**
* Available filters for the Filtered Collections and Filtered Items reports.
* In this enum, each item corresponds to a separate property, not values of
* a single property, hence the @JsonProperty applied to each of them.
* For each item, the annotation value is read through reflection and copied into
* the id property, which eliminates repetitions, hence reducing the risk or errors.
*
* @author Jean-François Morin (Université Laval)
*/
public enum Filter {
@JsonProperty("is_item")
IS_ITEM(FilterCategory.PROPERTY, (context, item) -> true),
@JsonProperty("is_withdrawn")
IS_WITHDRAWN(FilterCategory.PROPERTY, (context, item) -> item.isWithdrawn()),
@JsonProperty("is_not_withdrawn")
IS_NOT_WITHDRAWN(FilterCategory.PROPERTY, (context, item) -> !item.isWithdrawn()),
@JsonProperty("is_discoverable")
IS_DISCOVERABLE(FilterCategory.PROPERTY, (context, item) -> item.isDiscoverable()),
@JsonProperty("is_not_discoverable")
IS_NOT_DISCOVERABLE(FilterCategory.PROPERTY, (context, item) -> !item.isDiscoverable()),
/**
* Matches items having multiple original bitstreams.
*/
@JsonProperty("has_multiple_originals")
HAS_MULTIPLE_ORIGINALS(FilterCategory.BITSTREAM, (context, item) ->
ItemFilterUtil.countOriginalBitstream(item) > 1),
/**
* Matches items having no original bitstreams.
*/
@JsonProperty("has_no_originals")
HAS_NO_ORIGINALS(FilterCategory.BITSTREAM, (context, item) -> ItemFilterUtil.countOriginalBitstream(item) == 0),
/**
* Matches items having exactly one original bitstream.
*/
@JsonProperty("has_one_original")
HAS_ONE_ORIGINAL(FilterCategory.BITSTREAM, (context, item) -> ItemFilterUtil.countOriginalBitstream(item) == 1),
/**
* Matches items having bitstreams with a MIME type that matches one defined in the "rest.report-mime-document"
* configuration property.
*/
@JsonProperty("has_doc_original")
HAS_DOC_ORIGINAL(FilterCategory.BITSTREAM_MIME, (context, item) ->
ItemFilterUtil.countOriginalBitstreamMime(context, item, ItemFilterUtil.getDocumentMimeTypes()) > 0),
/**
* Matches items having bitstreams with a MIME type starting with "image" (e.g., image/jpeg, image/png).
*/
@JsonProperty("has_image_original")
HAS_IMAGE_ORIGINAL(FilterCategory.BITSTREAM_MIME, (context, item) ->
ItemFilterUtil.countOriginalBitstreamMimeStartsWith(context, item, "image") > 0),
/**
* Matches items having bitstreams with a MIME type other than document (cf. HAS_DOCUMENT above) or image
* (cf. HAS_IMAGE_ORIGINAL above).
*/
@JsonProperty("has_unsupp_type")
HAS_UNSUPPORTED_TYPE(FilterCategory.BITSTREAM_MIME, (context, item) -> {
int bitCount = ItemFilterUtil.countOriginalBitstream(item);
if (bitCount == 0) {
return false;
}
int docCount = ItemFilterUtil.countOriginalBitstreamMime(context, item, ItemFilterUtil.getDocumentMimeTypes());
int imgCount = ItemFilterUtil.countOriginalBitstreamMimeStartsWith(context, item, "image");
return (bitCount - docCount - imgCount) > 0;
}),
/**
* Matches items having bitstreams of multiple types (document, image, other).
*/
@JsonProperty("has_mixed_original")
HAS_MIXED_ORIGINAL(FilterCategory.BITSTREAM_MIME, (context, item) -> {
int countBit = ItemFilterUtil.countOriginalBitstream(item);
if (countBit <= 1) {
return false;
}
int countDoc = ItemFilterUtil.countOriginalBitstreamMime(context, item, ItemFilterUtil.getDocumentMimeTypes());
if (countDoc > 0) {
return countDoc != countBit;
}
int countImg = ItemFilterUtil.countOriginalBitstreamMimeStartsWith(context, item, "image");
if (countImg > 0) {
return countImg != countBit;
}
return false;
}),
@JsonProperty("has_pdf_original")
HAS_PDF_ORIGINAL(FilterCategory.BITSTREAM_MIME, (context, item) ->
ItemFilterUtil.countOriginalBitstreamMime(context, item, ItemFilterUtil.MIMES_PDF) > 0),
@JsonProperty("has_jpg_original")
HAS_JPEG_ORIGINAL(FilterCategory.BITSTREAM_MIME, (context, item) ->
ItemFilterUtil.countOriginalBitstreamMime(context, item, ItemFilterUtil.MIMES_JPG) > 0),
/**
* Matches items having at least one PDF of size less than 20 kb (configurable in rest.cfg).
*/
@JsonProperty("has_small_pdf")
HAS_SMALL_PDF(FilterCategory.BITSTREAM_MIME, (context, item) ->
ItemFilterUtil.countBitstreamSmallerThanMinSize(
context, BundleName.ORIGINAL, item, ItemFilterUtil.MIMES_PDF, "rest.report-pdf-min-size") > 0),
/**
* Matches items having at least one PDF of size more than 25 Mb (configurable in rest.cfg).
*/
@JsonProperty("has_large_pdf")
HAS_LARGE_PDF(FilterCategory.BITSTREAM_MIME, (context, item) ->
ItemFilterUtil.countBitstreamLargerThanMaxSize(
context, BundleName.ORIGINAL, item, ItemFilterUtil.MIMES_PDF, "rest.report-pdf-max-size") > 0),
/**
* Matches items having at least one non-text bitstream.
*/
@JsonProperty("has_doc_without_text")
HAS_DOC_WITHOUT_TEXT(FilterCategory.BITSTREAM_MIME, (context, item) -> {
int countDoc = ItemFilterUtil.countOriginalBitstreamMime(context, item, ItemFilterUtil.getDocumentMimeTypes());
if (countDoc == 0) {
return false;
}
int countText = ItemFilterUtil.countBitstream(BundleName.TEXT, item);
return countDoc > countText;
}),
/**
* Matches items having at least one image, but all of supported types.
*/
@JsonProperty("has_only_supp_image_type")
HAS_ONLY_SUPPORTED_IMAGE_TYPE(FilterCategory.MIME, (context, item) -> {
int imageCount = ItemFilterUtil.countOriginalBitstreamMimeStartsWith(context, item, "image/");
if (imageCount == 0) {
return false;
}
int suppImageCount = ItemFilterUtil.countOriginalBitstreamMime(
context, item, ItemFilterUtil.getSupportedImageMimeTypes());
return (imageCount == suppImageCount);
}),
/**
* Matches items having at least one image of an unsupported type.
*/
@JsonProperty("has_unsupp_image_type")
HAS_UNSUPPORTED_IMAGE_TYPE(FilterCategory.MIME, (context, item) -> {
int imageCount = ItemFilterUtil.countOriginalBitstreamMimeStartsWith(context, item, "image/");
if (imageCount == 0) {
return false;
}
int suppImageCount = ItemFilterUtil.countOriginalBitstreamMime(
context, item, ItemFilterUtil.getSupportedImageMimeTypes());
return (imageCount - suppImageCount) > 0;
}),
/**
* Matches items having at least one document, but all of supported types.
*/
@JsonProperty("has_only_supp_doc_type")
HAS_ONLY_SUPPORTED_DOC_TYPE(FilterCategory.MIME, (context, item) -> {
int docCount = ItemFilterUtil.countOriginalBitstreamMime(context, item, ItemFilterUtil.getDocumentMimeTypes());
if (docCount == 0) {
return false;
}
int suppDocCount = ItemFilterUtil.countOriginalBitstreamMime(
context, item, ItemFilterUtil.getSupportedDocumentMimeTypes());
return docCount == suppDocCount;
}),
/**
* Matches items having at least one document of an unsupported type.
*/
@JsonProperty("has_unsupp_doc_type")
HAS_UNSUPPORTED_DOC_TYPE(FilterCategory.MIME, (context, item) -> {
int docCount = ItemFilterUtil.countOriginalBitstreamMime(context, item, ItemFilterUtil.getDocumentMimeTypes());
if (docCount == 0) {
return false;
}
int suppDocCount = ItemFilterUtil.countOriginalBitstreamMime(
context, item, ItemFilterUtil.getSupportedDocumentMimeTypes());
return (docCount - suppDocCount) > 0;
}),
/**
* Matches items having at least one unsupported bundle.
*/
@JsonProperty("has_unsupported_bundle")
HAS_UNSUPPORTED_BUNDLE(FilterCategory.BUNDLE, (context, item) -> {
String[] bundleList = DSpaceServicesFactory.getInstance().getConfigurationService()
.getArrayProperty("rest.report-supp-bundles");
return ItemFilterUtil.hasUnsupportedBundle(item, bundleList);
}),
/**
* Matches items having at least one thumbnail of size less than 400 bytes (configurable in rest.cfg).
*/
@JsonProperty("has_small_thumbnail")
HAS_SMALL_THUMBNAIL(FilterCategory.BUNDLE, (context, item) ->
ItemFilterUtil.countBitstreamSmallerThanMinSize(
context, BundleName.THUMBNAIL, item, ItemFilterUtil.MIMES_JPG, "rest.report-thumbnail-min-size") > 0),
/**
* Matches items having at least one original without a thumbnail.
*/
@JsonProperty("has_original_without_thumbnail")
HAS_ORIGINAL_WITHOUT_THUMBNAIL(FilterCategory.BUNDLE, (context, item) -> {
int countBit = ItemFilterUtil.countOriginalBitstream(item);
if (countBit == 0) {
return false;
}
int countThumb = ItemFilterUtil.countBitstream(BundleName.THUMBNAIL, item);
return countBit > countThumb;
}),
/**
* Matches items having at least one non-JPEG thumbnail.
*/
@JsonProperty("has_invalid_thumbnail_name")
HAS_INVALID_THUMBNAIL_NAME(FilterCategory.BUNDLE, (context, item) -> {
List<String> originalNames = ItemFilterUtil.getBitstreamNames(BundleName.ORIGINAL, item);
List<String> thumbNames = ItemFilterUtil.getBitstreamNames(BundleName.THUMBNAIL, item);
if (thumbNames.size() != originalNames.size()) {
return false;
}
return originalNames.stream()
.anyMatch(name -> !thumbNames.contains(name + ".jpg") && !thumbNames.contains(name + ".jpeg"));
}),
/**
* Matches items having at least one non-generated thumbnail.
*/
@JsonProperty("has_non_generated_thumb")
HAS_NON_GENERATED_THUMBNAIL(FilterCategory.BUNDLE, (context, item) -> {
String[] generatedThumbDesc = DSpaceServicesFactory.getInstance().getConfigurationService()
.getArrayProperty("rest.report-gen-thumbnail-desc");
int countThumb = ItemFilterUtil.countBitstream(BundleName.THUMBNAIL, item);
if (countThumb == 0) {
return false;
}
int countGen = ItemFilterUtil.countBitstreamByDesc(BundleName.THUMBNAIL, item, generatedThumbDesc);
return (countThumb > countGen);
}),
/**
* Matches items having no licence-typed bitstreams.
*/
@JsonProperty("no_license")
NO_LICENSE(FilterCategory.BUNDLE, (context, item) ->
ItemFilterUtil.countBitstream(BundleName.LICENSE, item) == 0),
/**
* Matches items having licence documentation (a licence bitstream named other than license.txt).
*/
@JsonProperty("has_license_documentation")
HAS_LICENSE_DOCUMENTATION(FilterCategory.BUNDLE, (context, item) -> {
List<String> names = ItemFilterUtil.getBitstreamNames(BundleName.LICENSE, item);
return names.stream()
.anyMatch(name -> !name.equals("license.txt"));
}),
/**
* Matches items having at least one original with restricted access.
*/
@JsonProperty("has_restricted_original")
HAS_RESTRICTED_ORIGINAL(FilterCategory.PERMISSION, (context, item) -> {
return item.getBundles().stream()
.filter(bundle -> bundle.getName().equals(BundleName.ORIGINAL.name()))
.map(Bundle::getBitstreams)
.flatMap(List::stream)
.anyMatch(bit -> {
try {
if (!getAuthorizeService()
.authorizeActionBoolean(getAnonymousContext(), bit, org.dspace.core.Constants.READ)) {
return true;
}
} catch (SQLException e) {
getLog().warn("SQL Exception testing original bitstream access " + e.getMessage(), e);
}
return false;
});
}),
/**
* Matches items having at least one thumbnail with restricted access.
*/
@JsonProperty("has_restricted_thumbnail")
HAS_RESTRICTED_THUMBNAIL(FilterCategory.PERMISSION, (context, item) -> {
return item.getBundles().stream()
.filter(bundle -> bundle.getName().equals(BundleName.THUMBNAIL.name()))
.map(Bundle::getBitstreams)
.flatMap(List::stream)
.anyMatch(bit -> {
try {
if (!getAuthorizeService()
.authorizeActionBoolean(getAnonymousContext(), bit, org.dspace.core.Constants.READ)) {
return true;
}
} catch (SQLException e) {
getLog().warn("SQL Exception testing thumbnail bitstream access " + e.getMessage(), e);
}
return false;
});
}),
/**
* Matches items having metadata with restricted access.
*/
@JsonProperty("has_restricted_metadata")
HAS_RESTRICTED_METADATA(FilterCategory.PERMISSION, (context, item) -> {
try {
return !getAuthorizeService()
.authorizeActionBoolean(getAnonymousContext(), item, org.dspace.core.Constants.READ);
} catch (SQLException e) {
getLog().warn("SQL Exception testing item metadata access " + e.getMessage(), e);
return false;
}
});
private static final Logger log = LogManager.getLogger();
private static AuthorizeService authorizeService;
private static Context anonymousContext;
private String id;
private FilterCategory category;
private BiPredicate<Context, Item> itemTester;
Filter(FilterCategory category, BiPredicate<Context, Item> itemTester) {
try {
JsonProperty jp = getClass().getField(name()).getAnnotation(JsonProperty.class);
id = Optional.ofNullable(jp).map(JsonProperty::value).orElse(name());
} catch (Exception e) {
id = name();
}
this.category = category;
this.itemTester = itemTester;
}
public String getId() {
return id;
}
public FilterCategory getCategory() {
return category;
}
public boolean testItem(Context context, Item item) {
return itemTester.test(context, item);
}
private static Logger getLog() {
return log;
}
private static AuthorizeService getAuthorizeService() {
if (authorizeService == null) {
authorizeService = AuthorizeServiceFactory.getInstance().getAuthorizeService();
}
return authorizeService;
}
private static Context getAnonymousContext() {
if (anonymousContext == null) {
anonymousContext = new Context();
}
return anonymousContext;
}
@JsonCreator
public static Filter get(String id) {
return Arrays.stream(values())
.filter(item -> Objects.equals(item.id, id))
.findFirst()
.orElse(null);
}
public static Set<Filter> getFilters(String filters) {
String[] ids = Optional.ofNullable(filters).orElse("").split("[^a-z_]+");
Set<Filter> set = Arrays.stream(ids)
.map(Filter::get)
.filter(f -> f != null)
.collect(Collectors.toCollection(() -> EnumSet.noneOf(Filter.class)));
if (set == null) {
set = EnumSet.noneOf(Filter.class);
}
return set;
}
}

View File

@@ -0,0 +1,50 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.contentreport;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* Identifies the category/section of filters defined in the {@link Filter} enum.
* This enum will be used when/if the structured filter definitions are returned to
* the Angular layer through a REST endpoint.
*
* @author Jean-François Morin (Université Laval)
*/
public enum FilterCategory {
PROPERTY("property"),
BITSTREAM("bitstream"),
BITSTREAM_MIME("bitstream_mime"),
MIME("mime"),
BUNDLE("bundle"),
PERMISSION("permission");
private String id;
private List<Filter> filters;
FilterCategory(String id) {
this.id = id;
}
public String getId() {
return id;
}
public List<Filter> getFilters() {
if (filters == null) {
filters = Arrays.stream(Filter.values())
.filter(f -> f.getCategory() == this)
.collect(Collectors.toUnmodifiableList());
}
return filters;
}
}

View File

@@ -0,0 +1,219 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.contentreport;
import java.io.Serializable;
import java.util.EnumMap;
import java.util.Map;
import java.util.Optional;
/**
* This class represents an entry in the Filtered Collections report.
*
* @author Jean-François Morin (Université Laval)
*/
public class FilteredCollection implements Cloneable, Serializable {
private static final long serialVersionUID = -231735620268582719L;
/** Name of the collection */
private String label;
/** Handle of the collection, used to make it clickable from the generated report */
private String handle;
/** Name of the owning community */
private String communityLabel;
/** Handle of the owning community, used to make it clickable from the generated report */
private String communityHandle;
/** Total number of items in the collection */
private int totalItems;
/** Number of filtered items per requested filter in the collection */
private Map<Filter, Integer> values = new EnumMap<>(Filter.class);
/** Number of items in the collection that match all requested filters */
private int allFiltersValue;
/**
* Indicates whether this object is protected against further changes.
* This is used in computing summary data in the parent FilteredCollectionsRest class.
*/
private boolean sealed;
/**
* Shortcut method that builds a FilteredCollectionRest instance
* from its building blocks.
* @param label Name of the collection
* @param handle Handle of the collection
* @param communityLabel Name of the owning community
* @param communityHandle Handle of the owning community
* @param totalItems Total number of items in the collection
* @param allFiltersValue Number of items in the collection that match all requested filters
* @param values Number of filtered items per requested filter in the collection
* @param doSeal true if the collection must be sealed immediately
* @return a FilteredCollectionRest instance built from the provided parameters
*/
public static FilteredCollection of(String label, String handle,
String communityLabel, String communityHandle,
int totalItems, int allFiltersValue, Map<Filter, Integer> values, boolean doSeal) {
var coll = new FilteredCollection();
coll.label = label;
coll.handle = handle;
coll.communityLabel = communityLabel;
coll.communityHandle = communityHandle;
coll.totalItems = totalItems;
coll.allFiltersValue = allFiltersValue;
Optional.ofNullable(values).ifPresent(vs -> vs.forEach(coll::addValue));
if (doSeal) {
coll.seal();
}
return coll;
}
/**
* Returns the item counts per filter.
* If this object is sealed, a defensive copy will be returned.
*
* @return the item counts per filter
*/
public Map<Filter, Integer> getValues() {
if (sealed) {
return new EnumMap<>(values);
}
return values;
}
/**
* Increments a filtered item count for a given filter.
*
* @param filter Filter to add to the requested filters in this collection
* @param delta Number by which the filtered item count must be incremented
* for the requested filter
*/
public void addValue(Filter filter, int delta) {
checkSealed();
Integer oldValue = values.getOrDefault(filter, Integer.valueOf(0));
int newValue = oldValue.intValue() + delta;
values.put(filter, Integer.valueOf(newValue));
}
/**
* Sets all filtered item counts for this collection.
* The contents are copied into this object's internal Map, which is protected against
* further tampering with the provided Map.
*
* @param values Values that replace the current ones
*/
public void setValues(Map<? extends Filter, ? extends Integer> values) {
checkSealed();
this.values.clear();
this.values.putAll(values);
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
checkSealed();
this.label = label;
}
public String getHandle() {
return handle;
}
public void setHandle(String handle) {
checkSealed();
this.handle = handle;
}
public String getCommunityLabel() {
return communityLabel;
}
public void setCommunityLabel(String communityLabel) {
checkSealed();
this.communityLabel = communityLabel;
}
public String getCommunityHandle() {
return communityHandle;
}
public void setCommunityHandle(String communityHandle) {
checkSealed();
this.communityHandle = communityHandle;
}
public int getTotalItems() {
return totalItems;
}
public void setTotalItems(int totalItems) {
checkSealed();
this.totalItems = totalItems;
}
public int getAllFiltersValue() {
return allFiltersValue;
}
/**
* Increments the count of items matching all filters.
*
* @param delta Number by which the count must be incremented
*/
public void addAllFiltersValue(int delta) {
checkSealed();
allFiltersValue++;
}
/**
* Replaces the count of items matching all filters.
*
* @param allFiltersValue Number that replaces the current item count
*/
public void setAllFiltersValue(int allFiltersValue) {
checkSealed();
this.allFiltersValue = allFiltersValue;
}
public boolean getSealed() {
return sealed;
}
/**
* Seals this filtered collection object.
* No changes to this object can be made afterwards. Any attempt will throw
* an IllegalStateException.
*/
public void seal() {
sealed = true;
}
private void checkSealed() {
if (sealed) {
throw new IllegalStateException("This filtered collection record is sealed"
+ " and cannot be modified anymore. You can apply changes to a non-sealed clone.");
}
}
/**
* Returns a non-sealed clone of this filtered collection record.
*
* @return a new non-sealed FilteredCollectionRest instance containing
* all attribute values of this object
*/
@Override
public FilteredCollection clone() {
var clone = new FilteredCollection();
clone.label = label;
clone.handle = handle;
clone.values.putAll(values);
clone.allFiltersValue = allFiltersValue;
return clone;
}
}

View File

@@ -0,0 +1,107 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.contentreport;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
/**
* This class represents the complete result of a Filtered Collections report query.
* In addition to the list of FilteredCollection entries, it contains the lazily computed
* summary to be included in the completed report.
*
* @author Jean-François Morin (Université Laval)
*/
public class FilteredCollections implements Serializable {
private static final long serialVersionUID = 3622651208704009095L;
/** Collections included in the report */
private List<FilteredCollection> collections = new ArrayList<>();
/**
* Summary generated by adding up data for each filter included in the report.
* It will be regenerated if any non-sealed collection item is found in
* the {@link #collections} collection attribute.
*/
private FilteredCollection summary;
/**
* Shortcut method that builds a FilteredCollectionsRest instance
* from its building blocks.
* @param collections a list of FilteredCollectionRest instances
* @return a FilteredCollectionsRest instance built from the provided parameters
*/
public static FilteredCollections of(Collection<FilteredCollection> collections) {
var colls = new FilteredCollections();
Optional.ofNullable(collections).ifPresent(cs -> cs.stream().forEach(colls::addCollection));
return colls;
}
/**
* Returns a defensive copy of the collections included in this report.
*
* @return the collections included in this report
*/
public List<FilteredCollection> getCollections() {
return new ArrayList<>(collections);
}
/**
* Adds a {@link FilteredCollectionRest} object to this report.
*
* @param coll {@link FilteredCollectionRest} to add to this report
*/
public void addCollection(FilteredCollection coll) {
summary = null;
collections.add(coll);
}
/**
* Sets all collections for this report.
* The contents are copied into this object's internal list, which is protected against
* further tampering with the provided list.
*
* @param collections Values that replace the current ones
*/
public void setCollections(List<FilteredCollection> collections) {
summary = null;
this.collections.clear();
this.collections.addAll(collections);
}
/**
* Returns the report summary.
* If the summary has not been computed yet and/or the report includes non-sealed collections,
* it will be regenerated.
*
* @return the generated report summary
*/
public FilteredCollection getSummary() {
boolean needsRefresh = summary == null || collections.stream().anyMatch(c -> !c.getSealed());
if (needsRefresh) {
summary = new FilteredCollection();
for (var coll : collections) {
coll.getValues().forEach(summary::addValue);
}
int total = collections.stream()
.mapToInt(FilteredCollection::getTotalItems)
.sum();
summary.setTotalItems(total);
int allFilters = collections.stream()
.mapToInt(FilteredCollection::getAllFiltersValue)
.sum();
summary.setAllFiltersValue(allFilters);
summary.seal();
}
return summary;
}
}

View File

@@ -0,0 +1,70 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.contentreport;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import org.dspace.content.Item;
/**
* This class represents a list of items for a Filtered Items report query.
* Since the underlying list should correspond to only a page of results,
* the total number of items found through the query is included in this report.
*
* @author Jean-François Morin (Université Laval)
*/
public class FilteredItems implements Serializable {
private static final long serialVersionUID = 7980375013177658249L;
/** Items included in the report */
private List<Item> items = new ArrayList<>();
/** Total item count (for pagination) */
private long itemCount;
/**
* Returns a defensive copy of the items included in this report.
*
* @return the items included in this report
*/
public List<Item> getItems() {
return new ArrayList<>(items);
}
/**
* Adds an {@link ItemRest} object to this report.
*
* @param item {@link ItemRest} to add to this report
*/
public void addItem(Item item) {
items.add(item);
}
/**
* Sets all items for this report.
* The contents are copied into this object's internal list, which is protected
* against further tampering with the provided list.
*
* @param items Values that replace the current ones
*/
public void setItems(List<Item> items) {
this.items.clear();
this.items.addAll(items);
}
public long getItemCount() {
return itemCount;
}
public void setItemCount(long itemCount) {
this.itemCount = itemCount;
}
}

View File

@@ -0,0 +1,115 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.contentreport;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
/**
* Structured query contents for the Filtered Items report
* @author Jean-François Morin (Université Laval)
*/
public class FilteredItemsQuery {
private List<String> collections = new ArrayList<>();
private List<QueryPredicate> queryPredicates = new ArrayList<>();
private long offset;
private int pageLimit;
private Set<Filter> filters = EnumSet.noneOf(Filter.class);
private List<String> additionalFields = new ArrayList<>();
/**
* Shortcut method that builds a FilteredItemsQuery instance
* from its building blocks.
* @param collectionUuids collection UUIDs to add
* @param predicates query predicates used to filter existing items
* @param pageLimit number of items per page
* @param filters filters to apply to existing items
* The filters mapping to true will be applied, others (either missing or
* mapping to false) will not.
* @param additionalFields additional fields to display in the resulting report
* @return a FilteredItemsQuery instance built from the provided parameters
*/
public static FilteredItemsQuery of(Collection<String> collectionUuids,
Collection<QueryPredicate> predicates, long offset, int pageLimit,
Collection<Filter> filters, Collection<String> additionalFields) {
var query = new FilteredItemsQuery();
Optional.ofNullable(collectionUuids).ifPresent(query.collections::addAll);
Optional.ofNullable(predicates).ifPresent(query.queryPredicates::addAll);
query.offset = offset;
query.pageLimit = pageLimit;
Optional.ofNullable(filters).ifPresent(query.filters::addAll);
Optional.ofNullable(additionalFields).ifPresent(query.additionalFields::addAll);
return query;
}
public List<String> getCollections() {
return collections;
}
public void setCollections(List<String> collections) {
this.collections.clear();
if (collections != null) {
this.collections.addAll(collections);
}
}
public List<QueryPredicate> getQueryPredicates() {
return queryPredicates;
}
public void setQueryPredicates(List<QueryPredicate> queryPredicates) {
this.queryPredicates.clear();
if (queryPredicates != null) {
this.queryPredicates.addAll(queryPredicates);
}
}
public long getOffset() {
return offset;
}
public void setOffset(long offset) {
this.offset = offset;
}
public int getPageLimit() {
return pageLimit;
}
public void setPageLimit(int pageLimit) {
this.pageLimit = pageLimit;
}
public Set<Filter> getFilters() {
return filters;
}
public void setFilters(Set<Filter> filters) {
this.filters.clear();
if (filters != null) {
this.filters.addAll(filters);
}
}
public List<String> getAdditionalFields() {
return additionalFields;
}
public void setAdditionalFields(List<String> additionalFields) {
this.additionalFields.clear();
if (additionalFields != null) {
this.additionalFields.addAll(additionalFields);
}
}
}

View File

@@ -0,0 +1,353 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.contentreport;
import static org.dspace.content.Item.ANY;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Calendar;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.dspace.content.Bitstream;
import org.dspace.content.Bundle;
import org.dspace.content.Item;
import org.dspace.content.factory.ContentServiceFactory;
import org.dspace.content.service.ItemService;
import org.dspace.core.Context;
import org.dspace.services.factory.DSpaceServicesFactory;
/**
* Utility methods for applying some of the filters defined in the {@link Filter} enum.
*
* @author Jean-François Morin (Université Laval) (port to DSpace 7.x)
* @author Terry Brady, Georgetown University (original code in DSpace 6.x)
*/
public class ItemFilterUtil {
protected static ItemService itemService = ContentServiceFactory.getInstance().getItemService();
private static final Logger log = LogManager.getLogger(ItemFilterUtil.class);
public static final String[] MIMES_PDF = {"application/pdf"};
public static final String[] MIMES_JPG = {"image/jpeg"};
/**
* Supported bundle types.
* N.B.: Bundle names are used in metadata as they are named here.
* Do NOT change these names, the name() method is invoked at multiple
* locations in this class and enum Filter.
* If these names are to change, the name() invocations shall be changed
* so that they refer to these unchanged names, likely through a String property.
*/
enum BundleName {
ORIGINAL, TEXT, LICENSE, THUMBNAIL;
}
private ItemFilterUtil() {}
static String[] getDocumentMimeTypes() {
return DSpaceServicesFactory.getInstance().getConfigurationService()
.getArrayProperty("rest.report-mime-document");
}
static String[] getSupportedDocumentMimeTypes() {
return DSpaceServicesFactory.getInstance().getConfigurationService()
.getArrayProperty("rest.report-mime-document-supported");
}
static String[] getSupportedImageMimeTypes() {
return DSpaceServicesFactory.getInstance().getConfigurationService()
.getArrayProperty("rest.report-mime-document-image");
}
/**
* Counts the original bitstreams of a given item.
* @param item Provided item
* @return the number of original bitstreams in the item
*/
static int countOriginalBitstream(Item item) {
return countBitstream(BundleName.ORIGINAL, item);
}
/**
* Counts the bitstreams of a given item for a specific type.
* @param bundleName Type of bundle to filter bitstreams
* @param item Provided item
* @return the number of matching bitstreams in the item
*/
static int countBitstream(BundleName bundleName, Item item) {
return item.getBundles().stream()
.filter(bundle -> bundle.getName().equals(bundleName.name()))
.mapToInt(bundle -> bundle.getBitstreams().size())
.sum();
}
/**
* Retrieves the bitstream names of an given item for a specific bundle type.
* @param bundleName Type of bundle to filter bitstreams
* @param item Provided item
* @return the names of matching bitstreams in the item
*/
static List<String> getBitstreamNames(BundleName bundleName, Item item) {
return item.getBundles().stream()
.filter(bundle -> bundle.getName().equals(bundleName.name()))
.map(Bundle::getBitstreams)
.flatMap(List::stream)
.map(Bitstream::getName)
.collect(Collectors.toList());
}
/**
* Counts the original bitstreams of a given item matching one of a list of specific MIME types.
* @param context DSpace context
* @param item Provided item
* @param mimeList List of MIME types to filter bitstreams
* @return number of matching original bitstreams
*/
static int countOriginalBitstreamMime(Context context, Item item, String[] mimeList) {
return countBitstreamMime(context, BundleName.ORIGINAL, item, mimeList);
}
/**
* Counts the bitstreams of a given item for a specific type matching one of a list of specific MIME types.
* @param context DSpace context
* @param bundleName Type of bundle to filter bitstreams
* @param item Provided item
* @param mimeList List of MIME types to filter bitstreams
* @return number of matching bitstreams
*/
static int countBitstreamMime(Context context, BundleName bundleName, Item item, String[] mimeList) {
return item.getBundles().stream()
.filter(bundle -> bundle.getName().equals(bundleName.name()))
.map(Bundle::getBitstreams)
.flatMap(List::stream)
.mapToInt(bit -> {
int count = 0;
for (String mime : mimeList) {
try {
if (bit.getFormat(context).getMIMEType().equals(mime.trim())) {
count++;
}
} catch (SQLException e) {
log.error("Get format error for bitstream " + bit.getName());
}
}
return count;
})
.sum();
}
/**
* Counts the bitstreams of a given item for a specific type matching one of a list of specific descriptions.
* @param bundleName Type of bundle to filter bitstreams
* @param item Provided item
* @param descList List of descriptions to filter bitstreams
* @return number of matching bitstreams
*/
static int countBitstreamByDesc(BundleName bundleName, Item item, String[] descList) {
return item.getBundles().stream()
.filter(bundle -> bundle.getName().equals(bundleName.name()))
.map(Bundle::getBitstreams)
.flatMap(List::stream)
.filter(bit -> bit.getDescription() != null)
.mapToInt(bit -> {
int count = 0;
for (String desc : descList) {
String bitDesc = bit.getDescription();
if (bitDesc.equals(desc.trim())) {
count++;
}
}
return count;
})
.sum();
}
/**
* Counts the bitstreams of a given item smaller than a given size for a specific type
* matching one of a list of specific MIME types.
* @param context DSpace context
* @param bundleName Type of bundle to filter bitstreams
* @param item Provided item
* @param mimeList List of MIME types to filter bitstreams
* @param prop Configurable property providing the size to filter bitstreams
* @return number of matching bitstreams
*/
static int countBitstreamSmallerThanMinSize(
Context context, BundleName bundleName, Item item, String[] mimeList, String prop) {
long size = DSpaceServicesFactory.getInstance().getConfigurationService().getLongProperty(prop);
return item.getBundles().stream()
.filter(bundle -> bundle.getName().equals(bundleName.name()))
.map(Bundle::getBitstreams)
.flatMap(List::stream)
.mapToInt(bit -> {
int count = 0;
for (String mime : mimeList) {
try {
if (bit.getFormat(context).getMIMEType().equals(mime.trim())) {
if (bit.getSizeBytes() < size) {
count++;
}
}
} catch (SQLException e) {
log.error(e.getMessage(), e);
}
}
return count;
})
.sum();
}
/**
* Counts the bitstreams of a given item larger than a given size for a specific type
* matching one of a list of specific MIME types.
* @param context DSpace context
* @param bundleName Type of bundle to filter bitstreams
* @param item Provided item
* @param mimeList List of MIME types to filter bitstreams
* @param prop Configurable property providing the size to filter bitstreams
* @return number of matching bitstreams
*/
static int countBitstreamLargerThanMaxSize(
Context context, BundleName bundleName, Item item, String[] mimeList, String prop) {
long size = DSpaceServicesFactory.getInstance().getConfigurationService().getLongProperty(prop);
return item.getBundles().stream()
.filter(bundle -> bundle.getName().equals(bundleName.name()))
.map(Bundle::getBitstreams)
.flatMap(List::stream)
.mapToInt(bit -> {
int count = 0;
for (String mime : mimeList) {
try {
if (bit.getFormat(context).getMIMEType().equals(mime.trim())) {
if (bit.getSizeBytes() > size) {
count++;
}
}
} catch (SQLException e) {
log.error(e.getMessage(), e);
}
}
return count;
})
.sum();
}
/**
* Counts the original bitstreams of a given item whose MIME type starts with a specific prefix.
* @param context DSpace context
* @param item Provided item
* @param prefix Prefix to filter bitstreams
* @return number of matching original bitstreams
*/
static int countOriginalBitstreamMimeStartsWith(Context context, Item item, String prefix) {
return countBitstreamMimeStartsWith(context, BundleName.ORIGINAL, item, prefix);
}
/**
* Counts the bitstreams of a given item for a specific type whose MIME type starts with a specific prefix.
* @param context DSpace context
* @param bundleName Type of bundle to filter bitstreams
* @param item Provided item
* @param prefix Prefix to filter bitstreams
* @return number of matching bitstreams
*/
static int countBitstreamMimeStartsWith(Context context, BundleName bundleName, Item item, String prefix) {
return item.getBundles().stream()
.filter(bundle -> bundle.getName().equals(bundleName.name()))
.map(Bundle::getBitstreams)
.flatMap(List::stream)
.mapToInt(bit -> {
int count = 0;
try {
if (bit.getFormat(context).getMIMEType().startsWith(prefix)) {
count++;
}
} catch (SQLException e) {
log.error(e.getMessage(), e);
}
return count;
})
.sum();
}
/**
* Returns true if a given item has a bundle not matching a specific list of bundles.
* @param item Provided item
* @param bundleList List of bundle names to filter bundles
* @return true if the item has a (non-)matching bundle
*/
static boolean hasUnsupportedBundle(Item item, String[] bundleList) {
if (bundleList == null) {
return false;
}
Set<String> bundles = Arrays.stream(bundleList)
.collect(Collectors.toSet());
return item.getBundles().stream()
.anyMatch(bundle -> !bundles.contains(bundle.getName()));
}
static boolean hasOriginalBitstreamMime(Context context, Item item, String[] mimeList) {
return hasBitstreamMime(context, BundleName.ORIGINAL, item, mimeList);
}
static boolean hasBitstreamMime(Context context, BundleName bundleName, Item item, String[] mimeList) {
return countBitstreamMime(context, bundleName, item, mimeList) > 0;
}
/**
* Returns true if a given item has at least one field of a specific list whose value
* matches a provided regular expression.
* @param item Provided item
* @param fieldList List of fields to check
* @param regex Regular expression to check field values against
* @return true if there is at least one matching field, false otherwise
*/
static boolean hasMetadataMatch(Item item, String fieldList, Pattern regex) {
if ("*".equals(fieldList)) {
return itemService.getMetadata(item, ANY, ANY, ANY, ANY).stream()
.anyMatch(md -> regex.matcher(md.getValue()).matches());
}
return Arrays.stream(fieldList.split(","))
.map(field -> itemService.getMetadataByMetadataString(item, field.trim()))
.flatMap(List::stream)
.anyMatch(md -> regex.matcher(md.getValue()).matches());
}
/**
* Returns true if a given item has at all fields of a specific list whose values
* match a provided regular expression.
* @param item Provided item
* @param fieldList List of fields to check
* @param regex Regular expression to check field values against
* @return true if all specified fields match, false otherwise
*/
static boolean hasOnlyMetadataMatch(Item item, String fieldList, Pattern regex) {
if ("*".equals(fieldList)) {
return itemService.getMetadata(item, ANY, ANY, ANY, ANY).stream()
.allMatch(md -> regex.matcher(md.getValue()).matches());
}
return Arrays.stream(fieldList.split(","))
.map(field -> itemService.getMetadataByMetadataString(item, field.trim()))
.flatMap(List::stream)
.allMatch(md -> regex.matcher(md.getValue()).matches());
}
static boolean recentlyModified(Item item, int days) {
Calendar cal = Calendar.getInstance();
cal.add(Calendar.DATE, -days);
return cal.getTime().before(item.getLastModified());
}
}

View File

@@ -0,0 +1,131 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.contentreport;
import java.util.Arrays;
import java.util.function.BiFunction;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.Expression;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.apache.commons.lang3.function.TriFunction;
import org.dspace.content.MetadataValue;
import org.dspace.content.MetadataValue_;
import org.dspace.util.DSpacePostgreSQLDialect;
import org.dspace.util.JpaCriteriaBuilderKit;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.Property;
import org.hibernate.criterion.Restrictions;
import org.hibernate.type.StandardBasicTypes;
/**
* Operators available for creating predicates to query the
* Filtered Items report
* @author Jean-François Morin (Université Laval)
*/
public enum QueryOperator {
EXISTS("exists", true, false,
(val, regexClause) -> Property.forName("mv.value").isNotNull(),
(val, regexClause, jpaKit) -> jpaKit.criteriaBuilder().isNotNull(jpaKit.root().get(MetadataValue_.VALUE))),
DOES_NOT_EXIST("doesnt_exist", true, true,
(val, regexClause) -> EXISTS.buildPredicate(val, regexClause),
(val, regexClause, jpaKit) -> EXISTS.buildJpaPredicate(val, regexClause, jpaKit)),
EQUALS("equals", true, false,
(val, regexClause) -> Property.forName("mv.value").eq(val),
(val, regexClause, jpaKit) -> jpaKit.criteriaBuilder().equal(jpaKit.root().get(MetadataValue_.VALUE), val)),
DOES_NOT_EQUAL("not_equals", true, true,
(val, regexClause) -> EQUALS.buildPredicate(val, regexClause),
(val, regexClause, jpaKit) -> EQUALS.buildJpaPredicate(val, regexClause, jpaKit)),
LIKE("like", true, false,
(val, regexClause) -> Property.forName("mv.value").like(val),
(val, regexClause, jpaKit) -> jpaKit.criteriaBuilder().like(jpaKit.root().get(MetadataValue_.VALUE), val)),
NOT_LIKE("not_like", true, true,
(val, regexClause) -> LIKE.buildPredicate(val, regexClause),
(val, regexClause, jpaKit) -> LIKE.buildJpaPredicate(val, regexClause, jpaKit)),
CONTAINS("contains", true, false,
(val, regexClause) -> Property.forName("mv.value").like("%" + val + "%"),
(val, regexClause, jpaKit) -> LIKE.buildJpaPredicate("%" + val + "%", regexClause, jpaKit)),
DOES_NOT_CONTAIN("doesnt_contain", true, true,
(val, regexClause) -> CONTAINS.buildPredicate(val, regexClause),
(val, regexClause, jpaKit) -> CONTAINS.buildJpaPredicate(val, regexClause, jpaKit)),
MATCHES("matches", false, false,
(val, regexClause) -> Restrictions.sqlRestriction(regexClause, val, StandardBasicTypes.STRING),
(val, regexClause, jpaKit) -> regexPredicate(val, DSpacePostgreSQLDialect.REGEX_MATCHES, jpaKit)),
DOES_NOT_MATCH("doesnt_match", false, false,
(val, regexClause) -> Restrictions.not(Restrictions.sqlRestriction(
regexClause, val, StandardBasicTypes.STRING)),
(val, regexClause, jpaKit) -> regexPredicate(val, DSpacePostgreSQLDialect.REGEX_NOT_MATCHES, jpaKit));
private final String code;
/** Criteria builder for the old Hibernate API */
@Deprecated(forRemoval = true)
private final BiFunction<String, String, Criterion> criterionBuilder;
private final TriFunction<String, String, JpaCriteriaBuilderKit<MetadataValue>, Predicate> predicateBuilder;
private final boolean usesRegex;
private final boolean negate;
QueryOperator(String code, boolean usesRegex, boolean negate,
BiFunction<String, String, Criterion> criterionBuilder,
TriFunction<String, String, JpaCriteriaBuilderKit<MetadataValue>, Predicate> predicateBuilder) {
this.code = code;
this.usesRegex = usesRegex;
this.negate = negate;
this.criterionBuilder = criterionBuilder;
this.predicateBuilder = predicateBuilder;
}
@JsonProperty
public String getCode() {
return code;
}
public boolean getUsesRegex() {
return usesRegex;
}
public boolean getNegate() {
return negate;
}
public Criterion buildPredicate(String val, String regexClause) {
return criterionBuilder.apply(val, regexClause);
}
public Predicate buildJpaPredicate(String val, String regexClause, JpaCriteriaBuilderKit<MetadataValue> jpaKit) {
return predicateBuilder.apply(val, regexClause, jpaKit);
}
@JsonCreator
public static QueryOperator get(String code) {
return Arrays.stream(values())
.filter(item -> item.code.equalsIgnoreCase(code))
.findFirst()
.orElse(null);
}
public BiFunction<String, String, Criterion> getCriterionBuilder() {
return criterionBuilder;
}
private static Predicate regexPredicate(String val, String regexFunction,
JpaCriteriaBuilderKit<MetadataValue> jpaKit) {
// Source: https://stackoverflow.com/questions/24995881/use-regular-expressions-in-jpa-criteriabuilder
CriteriaBuilder builder = jpaKit.criteriaBuilder();
Expression<String> patternExpression = builder.<String>literal(val);
Path<String> path = jpaKit.root().get(MetadataValue_.VALUE);
// "matches" comes from the name of the regex function
// defined in class DSpacePostgreSQLDialect
return builder.equal(builder
.function(regexFunction, Boolean.class, path, patternExpression), Boolean.TRUE);
}
}

View File

@@ -0,0 +1,69 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.contentreport;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.dspace.content.MetadataField;
/**
* Data structure representing a query predicate used by the Filtered Items report
* to filter items to retrieve.
* @author Jean-François Morin (Université Laval)
*/
public class QueryPredicate {
private List<MetadataField> fields = new ArrayList<>();
private QueryOperator operator;
private String value;
/**
* Shortcut method that builds a QueryPredicate from a single field, an operator, and a value.
* @param field Predicate subject
* @param operator Predicate operator
* @param value Predicate object
* @return a QueryPredicate instance built from the provided parameters
*/
public static QueryPredicate of(MetadataField field, QueryOperator operator, String value) {
var predicate = new QueryPredicate();
predicate.fields.add(field);
predicate.operator = operator;
predicate.value = value;
return predicate;
}
/**
* Shortcut method that builds a QueryPredicate from a list of fields, an operator, and a value.
* @param fields Fields that form the predicate subject
* @param operator Predicate operator
* @param value Predicate object
* @return a QueryPredicate instance built from the provided parameters
*/
public static QueryPredicate of(Collection<MetadataField> fields, QueryOperator operator, String value) {
var predicate = new QueryPredicate();
predicate.fields.addAll(fields);
predicate.operator = operator;
predicate.value = value;
return predicate;
}
public List<MetadataField> getFields() {
return fields;
}
public QueryOperator getOperator() {
return operator;
}
public String getValue() {
return value;
}
}

View File

@@ -0,0 +1,55 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.contentreport.service;
import java.sql.SQLException;
import java.util.Collection;
import java.util.List;
import org.dspace.content.MetadataField;
import org.dspace.contentreport.Filter;
import org.dspace.contentreport.FilteredCollection;
import org.dspace.contentreport.FilteredItems;
import org.dspace.contentreport.FilteredItemsQuery;
import org.dspace.core.Context;
public interface ContentReportService {
/**
* Returns <code>true<</code> if Content Reports are enabled.
* @return <code>true<</code> if Content Reports are enabled
*/
boolean getEnabled();
/**
* Retrieves item statistics per collection according to a set of Boolean filters.
* @param context DSpace context
* @param filters Set of filters
* @return a list of collections with the requested statistics for each of them
*/
List<FilteredCollection> findFilteredCollections(Context context, Collection<Filter> filters);
/**
* Retrieves a list of items according to a set of criteria.
* @param context DSpace context
* @param query structured query to find items against
* @return a list of items filtered according to the provided query
*/
FilteredItems findFilteredItems(Context context, FilteredItemsQuery query);
/**
* Converts a metadata field name to a list of {@link MetadataField} instances
* (one if no wildcards are used, possibly more otherwise).
* @param context DSpace context
* @param metadataField field to search for
* @return a corresponding list of {@link MetadataField} entries
*/
List<MetadataField> getMetadataFields(org.dspace.core.Context context, String metadataField)
throws SQLException;
}

View File

@@ -0,0 +1,33 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.util;
import org.hibernate.dialect.PostgreSQL94Dialect;
import org.hibernate.dialect.function.SQLFunctionTemplate;
import org.hibernate.type.StandardBasicTypes;
/**
* PostgreSQL-specific dialect that adds regular expression support as a JPA function.
* @see org.dspace.contentreport.QueryOperator
* @author Jean-François Morin (Université Laval)
*/
public class DSpacePostgreSQLDialect extends PostgreSQL94Dialect {
public static final String REGEX_MATCHES = "matches";
public static final String REGEX_IMATCHES = "imatches";
public static final String REGEX_NOT_MATCHES = "not_matches";
public static final String REGEX_NOT_IMATCHES = "not_imatches";
public DSpacePostgreSQLDialect() {
registerFunction(REGEX_MATCHES, new SQLFunctionTemplate(StandardBasicTypes.BOOLEAN, "?1 ~ ?2"));
registerFunction(REGEX_IMATCHES, new SQLFunctionTemplate(StandardBasicTypes.BOOLEAN, "?1 ~* ?2"));
registerFunction(REGEX_NOT_MATCHES, new SQLFunctionTemplate(StandardBasicTypes.BOOLEAN, "?1 !~ ?2"));
registerFunction(REGEX_NOT_IMATCHES, new SQLFunctionTemplate(StandardBasicTypes.BOOLEAN, "?1 !~* ?2"));
}
}

View File

@@ -0,0 +1,49 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.util;
import javax.persistence.criteria.AbstractQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.Root;
/**
* Data structure containing the required objects to build criteria
* for a JPA query built using the JPA Criteria API.
* The getters match those generated by the JVM when using a record
* so that no API changes will be required when this class gets converted
* into a record when DSpace gets promoted to Java 17 or later.
* @author Jean-François Morin (Université Laval)
*/
// TODO: Convert this data structure into a record when DSpace gets promoted to Java 17 or later
public class JpaCriteriaBuilderKit<T> {
private CriteriaBuilder criteriaBuilder;
/** Can be a CriteriaQuery as well as a Subquery - both extend AbstractQuery. */
private AbstractQuery<T> query;
private Root<T> root;
public JpaCriteriaBuilderKit(CriteriaBuilder criteriaBuilder, AbstractQuery<T> query,
Root<T> root) {
this.criteriaBuilder = criteriaBuilder;
this.query = query;
this.root = root;
}
public CriteriaBuilder criteriaBuilder() {
return criteriaBuilder;
}
public AbstractQuery<T> query() {
return query;
}
public Root<T> root() {
return root;
}
}

View File

@@ -9,7 +9,10 @@ package org.dspace;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import java.sql.Connection;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Statement;
import javax.sql.DataSource;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
@@ -91,6 +94,14 @@ public class AbstractIntegrationTestWithDatabase extends AbstractDSpaceIntegrati
try { try {
// Update/Initialize the database to latest version (via Flyway) // Update/Initialize the database to latest version (via Flyway)
DatabaseUtils.updateDatabase(); DatabaseUtils.updateDatabase();
// Register custom functions in the H2 database
DataSource dataSource = DSpaceServicesFactory.getInstance()
.getServiceManager()
.getServiceByName("dataSource", DataSource.class);
try (Connection c = dataSource.getConnection(); Statement stmt = c.createStatement()) {
stmt.execute("CREATE ALIAS IF NOT EXISTS matches FOR 'org.dspace.util.DSpaceH2Dialect.matches'");
}
} catch (SQLException se) { } catch (SQLException se) {
log.error("Error initializing database", se); log.error("Error initializing database", se);
fail("Error initializing database: " + se.getMessage() fail("Error initializing database: " + se.getMessage()

View File

@@ -11,7 +11,9 @@ import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.hasSize;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
@@ -19,6 +21,9 @@ import java.io.InputStream;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
@@ -44,11 +49,15 @@ import org.dspace.content.Collection;
import org.dspace.content.Community; import org.dspace.content.Community;
import org.dspace.content.EntityType; import org.dspace.content.EntityType;
import org.dspace.content.Item; import org.dspace.content.Item;
import org.dspace.content.MetadataField;
import org.dspace.content.MetadataSchema;
import org.dspace.content.MetadataValue; import org.dspace.content.MetadataValue;
import org.dspace.content.Relationship; import org.dspace.content.Relationship;
import org.dspace.content.RelationshipType; import org.dspace.content.RelationshipType;
import org.dspace.content.WorkspaceItem; import org.dspace.content.WorkspaceItem;
import org.dspace.content.factory.ContentServiceFactory; import org.dspace.content.factory.ContentServiceFactory;
import org.dspace.contentreport.QueryOperator;
import org.dspace.contentreport.QueryPredicate;
import org.dspace.core.Constants; import org.dspace.core.Constants;
import org.dspace.eperson.Group; import org.dspace.eperson.Group;
import org.dspace.eperson.factory.EPersonServiceFactory; import org.dspace.eperson.factory.EPersonServiceFactory;
@@ -71,6 +80,9 @@ public class ItemServiceIT extends AbstractIntegrationTestWithDatabase {
protected ItemService itemService = ContentServiceFactory.getInstance().getItemService(); protected ItemService itemService = ContentServiceFactory.getInstance().getItemService();
protected InstallItemService installItemService = ContentServiceFactory.getInstance().getInstallItemService(); protected InstallItemService installItemService = ContentServiceFactory.getInstance().getInstallItemService();
protected WorkspaceItemService workspaceItemService = ContentServiceFactory.getInstance().getWorkspaceItemService(); protected WorkspaceItemService workspaceItemService = ContentServiceFactory.getInstance().getWorkspaceItemService();
protected MetadataSchemaService metadataSchemaService =
ContentServiceFactory.getInstance().getMetadataSchemaService();
protected MetadataFieldService metadataFieldService = ContentServiceFactory.getInstance().getMetadataFieldService();
protected MetadataValueService metadataValueService = ContentServiceFactory.getInstance().getMetadataValueService(); protected MetadataValueService metadataValueService = ContentServiceFactory.getInstance().getMetadataValueService();
protected VersioningService versioningService = VersionServiceFactory.getInstance().getVersionService(); protected VersioningService versioningService = VersionServiceFactory.getInstance().getVersionService();
protected AuthorizeService authorizeService = AuthorizeServiceFactory.getInstance().getAuthorizeService(); protected AuthorizeService authorizeService = AuthorizeServiceFactory.getInstance().getAuthorizeService();
@@ -78,6 +90,8 @@ public class ItemServiceIT extends AbstractIntegrationTestWithDatabase {
Community community; Community community;
Collection collection1; Collection collection1;
MetadataSchema schemaDC;
MetadataField fieldAuthor;
Item item; Item item;
@@ -99,6 +113,9 @@ public class ItemServiceIT extends AbstractIntegrationTestWithDatabase {
try { try {
context.turnOffAuthorisationSystem(); context.turnOffAuthorisationSystem();
schemaDC = metadataSchemaService.find(context, "dc");
fieldAuthor = metadataFieldService.findByElement(context, schemaDC, "contributor", "author");
community = CommunityBuilder.createCommunity(context) community = CommunityBuilder.createCommunity(context)
.build(); .build();
@@ -142,7 +159,7 @@ public class ItemServiceIT extends AbstractIntegrationTestWithDatabase {
// check the correct order using default method `getMetadata` // check the correct order using default method `getMetadata`
List<MetadataValue> defaultMetadata = List<MetadataValue> defaultMetadata =
this.itemService.getMetadata(item, dcSchema, contributorElement, authorQualifier, Item.ANY); itemService.getMetadata(item, dcSchema, contributorElement, authorQualifier, Item.ANY);
assertThat(defaultMetadata,hasSize(3)); assertThat(defaultMetadata,hasSize(3));
@@ -158,7 +175,7 @@ public class ItemServiceIT extends AbstractIntegrationTestWithDatabase {
// check the correct order using the method `getMetadata` without virtual fields // check the correct order using the method `getMetadata` without virtual fields
List<MetadataValue> nonVirtualMetadatas = List<MetadataValue> nonVirtualMetadatas =
this.itemService.getMetadata(item, dcSchema, contributorElement, authorQualifier, Item.ANY, false); itemService.getMetadata(item, dcSchema, contributorElement, authorQualifier, Item.ANY, false);
// if we don't reload the item the place order is not applied correctly // if we don't reload the item the place order is not applied correctly
// item = context.reloadEntity(item); // item = context.reloadEntity(item);
@@ -180,19 +197,19 @@ public class ItemServiceIT extends AbstractIntegrationTestWithDatabase {
item = context.reloadEntity(item); item = context.reloadEntity(item);
// now just add one metadata to be the last // now just add one metadata to be the last
this.itemService.addMetadata( itemService.addMetadata(
context, item, dcSchema, contributorElement, authorQualifier, Item.ANY, "test, latest", null, 0 context, item, dcSchema, contributorElement, authorQualifier, Item.ANY, "test, latest", null, 0
); );
// now just remove first metadata // now just remove first metadata
this.itemService.removeMetadataValues(context, item, List.of(placeZero)); itemService.removeMetadataValues(context, item, List.of(placeZero));
// now just add one metadata to place 0 // now just add one metadata to place 0
this.itemService.addAndShiftRightMetadata( itemService.addAndShiftRightMetadata(
context, item, dcSchema, contributorElement, authorQualifier, Item.ANY, "test, new", null, 0, 0 context, item, dcSchema, contributorElement, authorQualifier, Item.ANY, "test, new", null, 0, 0
); );
// check the metadata using method `getMetadata` // check the metadata using method `getMetadata`
defaultMetadata = defaultMetadata =
this.itemService.getMetadata(item, dcSchema, contributorElement, authorQualifier, Item.ANY); itemService.getMetadata(item, dcSchema, contributorElement, authorQualifier, Item.ANY);
// check correct places // check correct places
assertThat(defaultMetadata,hasSize(4)); assertThat(defaultMetadata,hasSize(4));
@@ -212,7 +229,7 @@ public class ItemServiceIT extends AbstractIntegrationTestWithDatabase {
// check metadata using nonVirtualMethod // check metadata using nonVirtualMethod
nonVirtualMetadatas = nonVirtualMetadatas =
this.itemService.getMetadata(item, dcSchema, contributorElement, authorQualifier, Item.ANY, false); itemService.getMetadata(item, dcSchema, contributorElement, authorQualifier, Item.ANY, false);
// check correct places // check correct places
assertThat(nonVirtualMetadatas,hasSize(4)); assertThat(nonVirtualMetadatas,hasSize(4));
@@ -244,7 +261,7 @@ public class ItemServiceIT extends AbstractIntegrationTestWithDatabase {
// check after commit // check after commit
defaultMetadata = defaultMetadata =
this.itemService.getMetadata(item, dcSchema, contributorElement, authorQualifier, Item.ANY); itemService.getMetadata(item, dcSchema, contributorElement, authorQualifier, Item.ANY);
// check correct places // check correct places
assertThat(defaultMetadata,hasSize(4)); assertThat(defaultMetadata,hasSize(4));
@@ -264,7 +281,7 @@ public class ItemServiceIT extends AbstractIntegrationTestWithDatabase {
// check metadata using nonVirtualMethod // check metadata using nonVirtualMethod
nonVirtualMetadatas = nonVirtualMetadatas =
this.itemService.getMetadata(item, dcSchema, contributorElement, authorQualifier, Item.ANY, false); itemService.getMetadata(item, dcSchema, contributorElement, authorQualifier, Item.ANY, false);
// check correct places // check correct places
assertThat(nonVirtualMetadatas,hasSize(4)); assertThat(nonVirtualMetadatas,hasSize(4));
@@ -916,4 +933,65 @@ public class ItemServiceIT extends AbstractIntegrationTestWithDatabase {
assertThat(metadataValue.getAuthority(), equalTo(authority)); assertThat(metadataValue.getAuthority(), equalTo(authority));
assertThat(metadataValue.getPlace(), equalTo(place)); assertThat(metadataValue.getPlace(), equalTo(place));
} }
@Test
public void testFindByMetadataQuery() throws Exception {
context.turnOffAuthorisationSystem();
// Here we add an author to the item
MetadataValue mv = itemService.addMetadata(context, item, dcSchema, contributorElement,
authorQualifier, null, "test, one");
context.commit();
item = context.reloadEntity(item);
assertNotNull(mv);
MetadataField mf = mv.getMetadataField();
assertEquals(fieldAuthor, mf);
MetadataSchema ms = mf.getMetadataSchema();
assertNotNull(ms);
assertEquals(dcSchema, ms.getName());
// We check whether the author metadata was properly added.
List<MetadataValue> mvs = item.getMetadata();
MetadataValue mvAuthor1 = mvs.stream()
.filter(mv1 -> Objects.equals(mv1.getMetadataField().getElement(), "contributor"))
.filter(mv1 -> Objects.equals(mv1.getMetadataField().getQualifier(), "author"))
.findFirst()
.orElse(null);
assertNotNull(mvAuthor1);
assertEquals("test, one", mvAuthor1.getValue());
assertMetadataValue(
authorQualifier, contributorElement, dcSchema, "test, one", null, 0, mvAuthor1
);
assertEquals(collection1, item.getOwningCollection());
List<UUID> collectionUuids = List.of(collection1.getID());
// First test: we should not find anything.
QueryPredicate predicate = QueryPredicate.of(fieldAuthor, QueryOperator.MATCHES, ".*whatever.*");
List<Item> items = itemService.findByMetadataQuery(context, List.of(predicate), collectionUuids, 0, -1);
assertTrue(items.isEmpty());
// Second test: we search against the metadata value specified above.
predicate = QueryPredicate.of(fieldAuthor, QueryOperator.EQUALS, "test, one");
items = itemService.findByMetadataQuery(context, List.of(predicate), collectionUuids, 0, -1);
assertEquals(1, items.size());
Item item = items.get(0);
assertNotNull(item);
List<MetadataValue> allMetadata = item.getMetadata();
Optional<MetadataValue> mvAuthor = allMetadata.stream()
.filter(md -> Objects.equals(dcSchema, md.getMetadataField().getMetadataSchema().getName()))
.filter(md -> Objects.equals(contributorElement, md.getMetadataField().getElement()))
.filter(md -> Objects.equals(authorQualifier, md.getMetadataField().getQualifier()))
.findFirst();
assertTrue(mvAuthor.isPresent());
assertEquals("test, one", mvAuthor.get().getValue());
context.restoreAuthSystemState();
}
} }

View File

@@ -0,0 +1,41 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.util;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
import org.hibernate.dialect.H2Dialect;
import org.hibernate.dialect.function.SQLFunctionTemplate;
import org.hibernate.type.StandardBasicTypes;
/**
* H2-specific dialect that adds regular expression support as a function.
* @author Jean-François Morin (Université Laval)
*/
public class DSpaceH2Dialect extends H2Dialect {
private static Map<String, Pattern> regexCache = new HashMap<>();
public DSpaceH2Dialect() {
registerFunction("matches", new SQLFunctionTemplate(StandardBasicTypes.BOOLEAN, "matches(?1, ?2)"));
// The SQL function is registered in AbstractIntegrationTestWithDatabase.initDatabase().
}
public static boolean matches(String regex, String value) {
Pattern pattern = regexCache.get(regex);
if (pattern == null) {
pattern = Pattern.compile(regex);
regexCache.put(regex, pattern);
}
return pattern.matcher(value).matches();
}
}

View File

@@ -0,0 +1,226 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Logger;
import org.dspace.app.rest.converter.ConverterService;
import org.dspace.app.rest.model.ContentReportSupportRest;
import org.dspace.app.rest.model.FilteredCollectionsQuery;
import org.dspace.app.rest.model.FilteredCollectionsRest;
import org.dspace.app.rest.model.FilteredItemsQueryPredicate;
import org.dspace.app.rest.model.FilteredItemsQueryRest;
import org.dspace.app.rest.model.FilteredItemsRest;
import org.dspace.app.rest.model.RestModel;
import org.dspace.app.rest.model.hateoas.ContentReportSupportResource;
import org.dspace.app.rest.model.hateoas.FilteredCollectionsResource;
import org.dspace.app.rest.model.hateoas.FilteredItemsResource;
import org.dspace.app.rest.repository.ContentReportRestRepository;
import org.dspace.app.rest.utils.ContextUtil;
import org.dspace.contentreport.Filter;
import org.dspace.contentreport.service.ContentReportService;
import org.dspace.core.Context;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.rest.webmvc.ControllerUtils;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.RepresentationModel;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* This controller receives and dispatches requests related to the
* contents reports ported from DSpace 6.x (Filtered Collections
* and Filtered Items).
* @author Jean-François Morin (Université Laval)
*/
@RestController
@RequestMapping("/api/" + RestModel.CONTENT_REPORT)
public class ContentReportRestController implements InitializingBean {
private static final Logger log = org.apache.logging.log4j.LogManager.getLogger();
@Autowired
private DiscoverableEndpointsService discoverableEndpointsService;
@Autowired
private ConverterService converter;
@Autowired
private ContentReportRestRepository contentReportRestRepository;
@Autowired
private ContentReportService contentReportService;
@Override
public void afterPropertiesSet() throws Exception {
discoverableEndpointsService
.register(this, List.of(Link.of("/api/" + RestModel.CONTENT_REPORT, RestModel.CONTENT_REPORT)));
}
@RequestMapping(method = RequestMethod.GET)
public ContentReportSupportResource getContentReportSupport() {
ContentReportSupportRest contentReportSupportRest = contentReportRestRepository.getContentReportSupport();
return converter.toResource(contentReportSupportRest);
}
/**
* GET-based endpoint for the Filtered Collections contents report.
* This method also serves as a feed for the HAL Browser infrastructure.
* @param filters querying filters received as a comma-separated string
* or as a multivalued parameter
* @param request HTTP request
* @param response HTTP response
* @return the list of collections with their respective statistics
*/
@PreAuthorize("hasAuthority('ADMIN')")
@GetMapping("/filteredcollections")
public ResponseEntity<RepresentationModel<?>> getFilteredCollections(
@RequestParam(name = "filters", required = false) List<String> filters,
HttpServletRequest request, HttpServletResponse response) throws IOException {
if (contentReportService.getEnabled()) {
Context context = ContextUtil.obtainContext(request);
Set<Filter> filtersSet = listToStream(filters)
.map(Filter::get)
.filter(f -> f != null)
.collect(Collectors.toSet());
FilteredCollectionsQuery query = FilteredCollectionsQuery.of(filtersSet);
return filteredCollectionsReport(context, query);
}
error404(response);
return null;
}
private ResponseEntity<RepresentationModel<?>> filteredCollectionsReport(Context context,
FilteredCollectionsQuery query) {
FilteredCollectionsRest report = contentReportRestRepository
.findFilteredCollections(context, query);
FilteredCollectionsResource result = converter.toResource(report);
return ControllerUtils.toResponseEntity(HttpStatus.OK, new HttpHeaders(), result);
}
/**
* Endpoint for the Filtered Items contents report.
* All parameters received as comma-separated lists can also be repeated
* instead (e.g., filters=a&filters=b&...).
* @param collections comma-separated list UUIDs of collections to include in the report
* @param predicates predicates to filter the requested items.
* A given predicate has the form
* field:operator:value (if value is required by the operator), or
* field:operator (if no value is required by the operator).
* The colon is used here as a separator to avoid conflicts with the
* comma, which is already used by Spring as a multi-value separator.
* Predicates are actually retrieved directly through the request to prevent comma-containing
* predicate values from being split by the Spring infrastructure.
* @param pageNumber page number (starting at 0)
* @param pageLimit maximum number of items per page
* @param filters querying filters received as a comma-separated string
* @param additionalFields comma-separated list of extra fields to add to the report
* @param request HTTP request
* @param response HTTP response
* @param pageable paging parameters
* @return the list of items with their respective statistics
*/
@PreAuthorize("hasAuthority('ADMIN')")
@GetMapping("/filtereditems")
public ResponseEntity<RepresentationModel<?>> getFilteredItems(
@RequestParam(name = "collections", required = false) List<String> collections,
@RequestParam(name = "queryPredicates", required = false) List<String> predicates,
@RequestParam(name = "pageNumber", defaultValue = "0") String pageNumber,
@RequestParam(name = "pageLimit", defaultValue = "10") String pageLimit,
@RequestParam(name = "filters", required = false) List<String> filters,
@RequestParam(name = "additionalFields", required = false) List<String> additionalFields,
HttpServletRequest request, HttpServletResponse response, Pageable pageable) throws IOException {
if (contentReportService.getEnabled()) {
Context context = ContextUtil.obtainContext(request);
String[] realPredicates = request.getParameterValues("queryPredicates");
List<String> collUuids = Optional.ofNullable(collections).orElseGet(() -> List.of());
List<FilteredItemsQueryPredicate> preds = arrayToStream(realPredicates)
.map(FilteredItemsQueryPredicate::of)
.collect(Collectors.toList());
int pgLimit = parseInt(pageLimit, 10);
int pgNumber = parseInt(pageNumber, 0);
Pageable myPageable = pageable;
if (pageable == null || pageable.getPageNumber() != pgNumber || pageable.getPageSize() != pgLimit) {
Sort sort = Optional.ofNullable(pageable).map(Pageable::getSort).orElse(Sort.unsorted());
myPageable = PageRequest.of(pgNumber, pgLimit, sort);
}
Set<Filter> filtersMap = listToStream(filters)
.map(Filter::get)
.filter(f -> f != null)
.collect(Collectors.toSet());
List<String> addFields = Optional.ofNullable(additionalFields).orElseGet(() -> List.of());
FilteredItemsQueryRest query = FilteredItemsQueryRest.of(collUuids, preds, pgLimit, filtersMap, addFields);
return filteredItemsReport(context, query, myPageable);
}
error404(response);
return null;
}
private static Stream<String> listToStream(Collection<String> array) {
return Optional.ofNullable(array)
.stream()
.flatMap(Collection::stream)
.filter(StringUtils::isNotBlank);
}
private static Stream<String> arrayToStream(String... array) {
return Optional.ofNullable(array)
.stream()
.flatMap(Arrays::stream)
.filter(StringUtils::isNotBlank);
}
private static int parseInt(String value, int defaultValue) {
return Optional.ofNullable(value)
.stream()
.mapToInt(Integer::parseInt)
.findFirst()
.orElse(defaultValue);
}
private ResponseEntity<RepresentationModel<?>> filteredItemsReport(Context context,
FilteredItemsQueryRest query, Pageable pageable) {
FilteredItemsRest report = contentReportRestRepository
.findFilteredItems(context, query, pageable);
FilteredItemsResource result = converter.toResource(report);
return ControllerUtils.toResponseEntity(HttpStatus.OK, new HttpHeaders(), result);
}
private void error404(HttpServletResponse response) throws IOException {
log.debug("Content Reports are disabled");
String err = "Content Reports are disabled";
response.setStatus(404);
response.setContentType("text/html");
response.setContentLength(err.length());
response.getWriter().write(err);
}
}

View File

@@ -0,0 +1,125 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.converter;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Logger;
import org.dspace.app.rest.model.FilteredItemRest;
import org.dspace.app.rest.model.MetadataValueList;
import org.dspace.app.rest.projection.Projection;
import org.dspace.app.rest.utils.ContextUtil;
import org.dspace.app.util.service.MetadataExposureService;
import org.dspace.authorize.service.AuthorizeService;
import org.dspace.content.Item;
import org.dspace.content.MetadataField;
import org.dspace.content.MetadataValue;
import org.dspace.content.service.ItemService;
import org.dspace.core.Context;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
/**
* This is the converter from/to the Item in the DSpace API data model and the
* REST data model
*
* @author Andrea Bollini (andrea.bollini at 4science.it)
*/
@Component
public class FilteredItemConverter {
// Must be loaded @Lazy, as ConverterService autowires all DSpaceConverter components
@Lazy
@Autowired
ConverterService converter;
@Autowired
private ItemService itemService;
@Autowired
private CollectionConverter collectionConverter;
@Autowired
AuthorizeService authorizeService;
@Autowired
MetadataExposureService metadataExposureService;
private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(FilteredItemConverter.class);
public FilteredItemRest convert(Item obj, Projection projection) {
FilteredItemRest item = new FilteredItemRest();
item.setHandle(obj.getHandle());
if (obj.getID() != null) {
item.setUuid(obj.getID().toString());
}
item.setName(obj.getName());
MetadataValueList metadataValues = getPermissionFilteredMetadata(
ContextUtil.obtainCurrentRequestContext(), obj);
item.setMetadata(converter.toRest(metadataValues, projection));
item.setInArchive(obj.isArchived());
item.setDiscoverable(obj.isDiscoverable());
item.setWithdrawn(obj.isWithdrawn());
item.setLastModified(obj.getLastModified());
List<MetadataValue> entityTypes =
itemService.getMetadata(obj, "dspace", "entity", "type", Item.ANY, false);
if (CollectionUtils.isNotEmpty(entityTypes) && StringUtils.isNotBlank(entityTypes.get(0).getValue())) {
item.setEntityType(entityTypes.get(0).getValue());
}
Optional.ofNullable(obj.getOwningCollection())
.map(coll -> collectionConverter.convert(coll, Projection.DEFAULT))
.ifPresent(item::setOwningCollection);
return item;
}
/**
* Retrieves the metadata list filtered according to the hidden metadata configuration
* When the context is null, it will return the metadatalist as for an anonymous user
* Overrides the parent method to include virtual metadata
* @param context The context
* @param obj The object of which the filtered metadata will be retrieved
* @return A list of object metadata (including virtual metadata) filtered based on the hidden metadata
* configuration
*/
private MetadataValueList getPermissionFilteredMetadata(Context context, Item obj) {
List<MetadataValue> fullList = itemService.getMetadata(obj, Item.ANY, Item.ANY, Item.ANY, Item.ANY, true);
List<MetadataValue> returnList = new LinkedList<>();
try {
if (obj.isWithdrawn() && (Objects.isNull(context) ||
Objects.isNull(context.getCurrentUser()) || !authorizeService.isAdmin(context))) {
return new MetadataValueList(new ArrayList<>());
}
if (context != null && (authorizeService.isAdmin(context) || itemService.canEdit(context, obj))) {
return new MetadataValueList(fullList);
}
for (MetadataValue mv : fullList) {
MetadataField metadataField = mv.getMetadataField();
if (!metadataExposureService
.isHidden(context, metadataField.getMetadataSchema().getName(),
metadataField.getElement(),
metadataField.getQualifier())) {
returnList.add(mv);
}
}
} catch (SQLException e) {
log.error("Error filtering item metadata based on permissions", e);
}
return new MetadataValueList(returnList);
}
}

View File

@@ -0,0 +1,47 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.link.contentreport;
import java.util.LinkedList;
import org.dspace.app.rest.ContentReportRestController;
import org.dspace.app.rest.link.HalLinkFactory;
import org.dspace.app.rest.model.hateoas.ContentReportSupportResource;
import org.springframework.data.domain.Pageable;
import org.springframework.hateoas.IanaLinkRelations;
import org.springframework.hateoas.Link;
import org.springframework.stereotype.Component;
/**
* This class adds the self and report links to the ContentReportSupportResource.
* @author Jean-François Morin (Université Laval)
*/
@Component
public class ContentReportSupportHalLinkFactory
extends HalLinkFactory<ContentReportSupportResource, ContentReportRestController> {
@Override
protected void addLinks(ContentReportSupportResource halResource, Pageable pageable, LinkedList<Link> list)
throws Exception {
list.add(buildLink(IanaLinkRelations.SELF.value(), getMethodOn().getContentReportSupport()));
list.add(buildLink("filteredcollections", getMethodOn().getFilteredCollections(null, null, null)));
list.add(buildLink("filtereditems", getMethodOn()
.getFilteredItems(null, null, null, null, null, null, null, null, null)));
}
@Override
protected Class<ContentReportRestController> getControllerClass() {
return ContentReportRestController.class;
}
@Override
protected Class<ContentReportSupportResource> getResourceClass() {
return ContentReportSupportResource.class;
}
}

View File

@@ -0,0 +1,38 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.model;
import org.dspace.app.rest.ContentReportRestController;
public class ContentReportSupportRest extends BaseObjectRest<String> {
private static final long serialVersionUID = 9137258312781361906L;
public static final String NAME = "contentreport";
public static final String CATEGORY = RestModel.CONTENT_REPORT;
@Override
public String getCategory() {
return CATEGORY;
}
@Override
public Class<?> getController() {
return ContentReportRestController.class;
}
@Override
public String getType() {
return NAME;
}
@Override
public String getTypePlural() {
return NAME;
}
}

View File

@@ -0,0 +1,104 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.model;
import java.util.EnumMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.dspace.contentreport.Filter;
import org.dspace.contentreport.FilteredCollection;
/**
* This class serves as a REST representation of a single Collection in a {@link FilteredCollectionsRest}
* from the DSpace statistics. It takes its values from a @link FilteredCollection} instance.
* It must not extend BaseObjectRest<?>.
*
* @author Jean-François Morin (Université Laval)
*/
public class FilteredCollectionRest {
public static final String NAME = "filtered-collection";
public static final String CATEGORY = RestModel.CONTENT_REPORT;
/** Name of the collection */
private String label;
/** Handle of the collection, used to make it clickable from the generated report */
private String handle;
/** Name of the owning community */
@JsonProperty("community_label")
private String communityLabel;
/** Handle of the owning community, used to make it clickable from the generated report */
@JsonProperty("community_handle")
private String communityHandle;
/** Total number of items in the collection */
@JsonProperty("nb_total_items")
private int totalItems;
/** Number of filtered items per requested filter in the collection */
private Map<Filter, Integer> values = new EnumMap<>(Filter.class);
/** Number of items in the collection that match all requested filters */
@JsonProperty("all_filters_value")
private int allFiltersValue;
/**
* Builds a FilteredCollectionRest instance from a {@link FilteredCollection} instance.
* @param model the FilteredCollection instance that provides values to the
* FilteredCollectionRest instance to be created
* @return a FilteredCollectionRest instance built from the provided model object
*/
public static FilteredCollectionRest of(FilteredCollection model) {
Objects.requireNonNull(model);
var coll = new FilteredCollectionRest();
coll.label = model.getLabel();
coll.handle = model.getHandle();
coll.communityLabel = model.getCommunityLabel();
coll.communityHandle = model.getCommunityHandle();
coll.totalItems = model.getTotalItems();
coll.allFiltersValue = model.getAllFiltersValue();
Optional.ofNullable(model.getValues()).ifPresent(coll.values::putAll);
return coll;
}
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
public String getType() {
return NAME;
}
public Map<Filter, Integer> getValues() {
return values;
}
public String getLabel() {
return label;
}
public String getHandle() {
return handle;
}
public String getCommunityLabel() {
return communityLabel;
}
public String getCommunityHandle() {
return communityHandle;
}
public int getTotalItems() {
return totalItems;
}
public int getAllFiltersValue() {
return allFiltersValue;
}
}

View File

@@ -0,0 +1,54 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.model;
import java.util.Collection;
import java.util.EnumSet;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.dspace.contentreport.Filter;
/**
* Structured query contents for the Filtered Collections report
* @author Jean-François Morin (Université Laval)
*/
public class FilteredCollectionsQuery {
private Set<Filter> filters = EnumSet.noneOf(Filter.class);
/**
* Shortcut method that builds a FilteredCollectionsQuery instance
* from its building blocks.
* @param filters filters to apply to existing items.
* The filters mapping to true will be applied, others (either missing or
* mapping to false) will not.
* @return a FilteredCollectionsQuery instance built from the provided parameters
*/
public static FilteredCollectionsQuery of(Collection<Filter> filters) {
var query = new FilteredCollectionsQuery();
Optional.ofNullable(filters).ifPresent(query.filters::addAll);
return query;
}
public Set<Filter> getFilters() {
return filters;
}
public void setFilters(Set<Filter> filters) {
this.filters = filters;
}
public String toQueryString() {
return filters.stream()
.map(f -> "filters=" + f.getId())
.collect(Collectors.joining("&"));
}
}

View File

@@ -0,0 +1,82 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.model;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.dspace.app.rest.ContentReportRestController;
import org.dspace.contentreport.FilteredCollections;
/**
* This class serves as a REST representation of a Filtered Collections Report.
* The name must match that of the associated resource class (FilteredCollectionsResource) except for
* the suffix. This is why it is not named something like FilteredCollectionsReportRest.
*
* @author Jean-François Morin (Université Laval)
*/
public class FilteredCollectionsRest extends BaseObjectRest<String> {
private static final long serialVersionUID = -1109226348211060786L;
/** Type of instances of this class, used by the DSpace REST infrastructure */
public static final String NAME = "filteredcollectionsreport";
/** Category of instances of this class, used by the DSpace REST infrastructure */
public static final String CATEGORY = RestModel.CONTENT_REPORT;
/** Collections included in the report */
private List<FilteredCollectionRest> collections = new ArrayList<>();
/** Report summary */
private FilteredCollectionRest summary;
/**
* Builds a FilteredCollectionsRest instance from a {@link FilteredCollections} instance.
* Each underlying FilteredCollection is converted to a FilteredCollectionRest instance.
* @param model the FilteredCollections instance that provides values to the
* FilteredCollectionsRest instance to be created
* @return a FilteredCollectionsRest instance built from the provided model object
*/
public static FilteredCollectionsRest of(FilteredCollections model) {
var colls = new FilteredCollectionsRest();
Optional.ofNullable(model.getCollections()).ifPresent(cs ->
cs.stream()
.map(FilteredCollectionRest::of)
.forEach(colls.collections::add));
colls.summary = FilteredCollectionRest.of(model.getSummary());
return colls;
}
@Override
public String getCategory() {
return CATEGORY;
}
@Override
public Class<?> getController() {
return ContentReportRestController.class;
}
@Override
public String getType() {
return NAME;
}
@Override
public String getTypePlural() {
return getType();
}
public List<FilteredCollectionRest> getCollections() {
return collections;
}
public FilteredCollectionRest getSummary() {
return summary;
}
}

View File

@@ -0,0 +1,128 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.model;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* Specialization of ItemRest dedicated to the Filtered Items report.
* This class adds the owning collection property required to properly
* display search results without compromising the expected behaviour
* of standard ItemRest instances, in all other contexts, especially
* when it comes to embedded contents, a criterion that is widely checked
* against in several integration tests.
*
* @author Jean-François Morin (jean-francois.morin@bibl.ulaval.ca)
*/
public class FilteredItemRest {
public static final String NAME = "filtered-item";
public static final String CATEGORY = RestAddressableModel.CONTENT_REPORT;
public static final String OWNING_COLLECTION = "owningCollection";
private String uuid;
private String name;
private String handle;
MetadataRest metadata = new MetadataRest();
private boolean inArchive = false;
private boolean discoverable = false;
private boolean withdrawn = false;
private Date lastModified = new Date();
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
private String entityType = null;
private CollectionRest owningCollection;
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
public String getType() {
return NAME;
}
public String getUuid() {
return uuid;
}
public void setUuid(String uuid) {
this.uuid = uuid;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getHandle() {
return handle;
}
public void setHandle(String handle) {
this.handle = handle;
}
public MetadataRest getMetadata() {
return metadata;
}
public void setMetadata(MetadataRest metadata) {
this.metadata = metadata;
}
public boolean getInArchive() {
return inArchive;
}
public void setInArchive(boolean inArchive) {
this.inArchive = inArchive;
}
public boolean getDiscoverable() {
return discoverable;
}
public void setDiscoverable(boolean discoverable) {
this.discoverable = discoverable;
}
public boolean getWithdrawn() {
return withdrawn;
}
public void setWithdrawn(boolean withdrawn) {
this.withdrawn = withdrawn;
}
public Date getLastModified() {
return lastModified;
}
public void setLastModified(Date lastModified) {
this.lastModified = lastModified;
}
public String getEntityType() {
return entityType;
}
public void setEntityType(String entityType) {
this.entityType = entityType;
}
public CollectionRest getOwningCollection() {
return owningCollection;
}
public void setOwningCollection(CollectionRest owningCollection) {
this.owningCollection = owningCollection;
}
}

View File

@@ -0,0 +1,74 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.model;
import java.util.Optional;
import org.apache.commons.lang3.StringUtils;
import org.dspace.contentreport.QueryOperator;
/**
* Data structure representing a query predicate used by the Filtered Items report
* to filter items to retrieve. This version is specific to the REST layer and its
* property types are detached from the persistence layer.
* @see org.dspace.contentreport.QueryPredicate
* @author Jean-François Morin (Université Laval)
*/
public class FilteredItemsQueryPredicate {
private String field;
private QueryOperator operator;
private String value;
/**
* Shortcut method that builds a FilteredItemsQueryPredicate from a single field, an operator, and a value.
* @param field Predicate subject
* @param operator Predicate operator
* @param value Predicate object
* @return a FilteredItemsQueryPredicate instance built from the provided parameters
*/
public static FilteredItemsQueryPredicate of(String field, QueryOperator operator, String value) {
var predicate = new FilteredItemsQueryPredicate();
predicate.field = field;
predicate.operator = operator;
predicate.value = value;
return predicate;
}
/**
* Shortcut method that builds a FilteredItemsQueryPredicate from a colon-separated string value.
* @param value Colon-separated string value (field:operator:object or field:operator)
* @return a FilteredItemsQueryPredicate instance built from the provided value
*/
public static FilteredItemsQueryPredicate of(String value) {
String[] tokens = value.split("\\:");
String field = tokens.length > 0 ? tokens[0].trim() : "";
QueryOperator operator = tokens.length > 1 ? QueryOperator.get(tokens[1].trim()) : null;
String object = tokens.length > 2 ? StringUtils.trimToEmpty(tokens[2]) : "";
return of(field, operator, object);
}
public String getField() {
return field;
}
public QueryOperator getOperator() {
return operator;
}
public String getValue() {
return value;
}
@Override
public String toString() {
String op = Optional.ofNullable(operator).map(QueryOperator::getCode).orElse("");
return field + ":" + op + ":" + value;
}
}

View File

@@ -0,0 +1,148 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.model;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.lang3.StringUtils;
import org.dspace.contentreport.Filter;
import org.dspace.contentreport.QueryOperator;
/**
* REST-based version of structured query contents for the Filtered Items report
* @author Jean-François Morin (Université Laval)
*/
public class FilteredItemsQueryRest {
private List<String> collections = new ArrayList<>();
private List<FilteredItemsQueryPredicate> queryPredicates = new ArrayList<>();
private int pageLimit;
private Set<Filter> filters = EnumSet.noneOf(Filter.class);
private List<String> additionalFields = new ArrayList<>();
/**
* Shortcut method that builds a FilteredItemsQueryRest instance
* from its building blocks.
* @param collectionUuids collection UUIDs to add
* @param predicates query predicates used to filter existing items
* @param pageLimit number of items per page
* @param filters filters to apply to existing items
* The filters mapping to true will be applied, others (either missing or
* mapping to false) will not.
* @param additionalFields additional fields to display in the resulting report
* @return a FilteredItemsQueryRest instance built from the provided parameters
*/
public static FilteredItemsQueryRest of(Collection<String> collectionUuids,
Collection<FilteredItemsQueryPredicate> predicates, int pageLimit,
Collection<Filter> filters, Collection<String> additionalFields) {
var query = new FilteredItemsQueryRest();
Optional.ofNullable(collectionUuids).ifPresent(query.collections::addAll);
Optional.ofNullable(predicates).ifPresent(query.queryPredicates::addAll);
query.pageLimit = pageLimit;
Optional.ofNullable(filters).ifPresent(query.filters::addAll);
Optional.ofNullable(additionalFields).ifPresent(query.additionalFields::addAll);
return query;
}
public List<String> getCollections() {
return collections;
}
public void setCollections(List<String> collections) {
this.collections = collections;
}
public List<FilteredItemsQueryPredicate> getQueryPredicates() {
return queryPredicates;
}
public void setQueryPredicates(List<FilteredItemsQueryPredicate> queryPredicates) {
this.queryPredicates = queryPredicates;
}
public List<String> getPredicateFields() {
if (queryPredicates == null) {
return Collections.emptyList();
}
return queryPredicates.stream()
.map(FilteredItemsQueryPredicate::getField)
.collect(Collectors.toList());
}
public List<QueryOperator> getPredicateOperators() {
if (queryPredicates == null) {
return Collections.emptyList();
}
return queryPredicates.stream()
.map(FilteredItemsQueryPredicate::getOperator)
.collect(Collectors.toList());
}
public List<String> getPredicateValues() {
if (queryPredicates == null) {
return Collections.emptyList();
}
return queryPredicates.stream()
.map(FilteredItemsQueryPredicate::getValue)
.map(s -> s == null ? "" : s)
.collect(Collectors.toList());
}
public int getPageLimit() {
return pageLimit;
}
public void setPageLimit(int pageLimit) {
this.pageLimit = pageLimit;
}
public Set<Filter> getFilters() {
return filters;
}
public void setFilters(Set<Filter> filters) {
this.filters = filters;
}
public List<String> getAdditionalFields() {
return additionalFields;
}
public void setAdditionalFields(List<String> additionalFields) {
this.additionalFields = additionalFields;
}
public String toQueryString() {
String colls = collections.stream()
.map(coll -> "collection=" + coll)
.collect(Collectors.joining("&"));
String preds = queryPredicates.stream()
.map(pred -> "queryPredicates=" + pred)
.collect(Collectors.joining("&"));
String pgLimit = "pageLimit=" + pageLimit;
String fltrs = filters.stream()
.map(e -> "filters=" + e.getId())
.collect(Collectors.joining("&"));
String flds = additionalFields.stream()
.map(fld -> "additionalFields=" + fld)
.collect(Collectors.joining("&"));
return Stream.of(colls, preds, pgLimit, fltrs, flds)
.filter(StringUtils::isNotBlank)
.collect(Collectors.joining("&"));
}
}

View File

@@ -0,0 +1,88 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.model;
import java.util.ArrayList;
import java.util.List;
import org.dspace.app.rest.ContentReportRestController;
/**
* This class serves as a REST representation of a Filtered Items Report.
* The name must match that of the associated resource class (FilteredItemsResource) except for
* the suffix. This is why it is not named something like FilteredItemsReportRest.
*
* @author Jean-François Morin (Université Laval)
*/
public class FilteredItemsRest extends BaseObjectRest<String> {
private static final long serialVersionUID = -2483812920345013458L;
/** Type of instances of this class, used by the DSpace REST infrastructure */
public static final String NAME = "filtereditemsreport";
/** Category of instances of this class, used by the DSpace REST infrastructure */
public static final String CATEGORY = RestModel.CONTENT_REPORT;
/** Items included in the report */
private List<FilteredItemRest> items = new ArrayList<>();
/** Total item count (for pagination) */
private long itemCount;
/**
* Builds a FilteredItemsRest instance from a list of items and an total item count.
* To avoid adding a dependency to any Spring-managed service here, the items
* provided here are already converted to FilteredItemRest instances.
* @param items the items to add to the FilteredItemsRest instance to be created
* @param itemCount total number of items found regardless of any pagination constraint
* @return a FilteredItemsRest instance built from the provided data
*/
public static FilteredItemsRest of(List<FilteredItemRest> items, long itemCount) {
var itemsRest = new FilteredItemsRest();
itemsRest.items.addAll(items);
itemsRest.itemCount = itemCount;
return itemsRest;
}
@Override
public String getCategory() {
return CATEGORY;
}
/**
* Return controller class responsible for this Rest object
*
* @return Controller class responsible for this Rest object
*/
@Override
public Class<?> getController() {
return ContentReportRestController.class;
}
@Override
public String getType() {
return NAME;
}
@Override
public String getTypePlural() {
return getType();
}
/**
* Returns a defensive copy of the items included in this report.
*
* @return the items included in this report
*/
public List<FilteredItemRest> getItems() {
return new ArrayList<>(items);
}
public long getItemCount() {
return itemCount;
}
}

View File

@@ -20,6 +20,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
public interface RestModel extends Serializable { public interface RestModel extends Serializable {
public static final String ROOT = "root"; public static final String ROOT = "root";
public static final String CONTENT_REPORT = "contentreport";
public static final String CORE = "core"; public static final String CORE = "core";
public static final String EPERSON = "eperson"; public static final String EPERSON = "eperson";
public static final String DISCOVER = "discover"; public static final String DISCOVER = "discover";

View File

@@ -0,0 +1,18 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.model.hateoas;
import org.dspace.app.rest.model.ContentReportSupportRest;
import org.dspace.app.rest.model.hateoas.annotations.RelNameDSpaceResource;
@RelNameDSpaceResource(ContentReportSupportRest.NAME)
public class ContentReportSupportResource extends HALResource<ContentReportSupportRest> {
public ContentReportSupportResource(ContentReportSupportRest content) {
super(content);
}
}

View File

@@ -0,0 +1,21 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.model.hateoas;
import org.dspace.app.rest.model.FilteredCollectionsRest;
import org.dspace.app.rest.model.hateoas.annotations.RelNameDSpaceResource;
import org.dspace.app.rest.utils.Utils;
@RelNameDSpaceResource(FilteredCollectionsRest.NAME)
public class FilteredCollectionsResource extends DSpaceResource<FilteredCollectionsRest> {
public FilteredCollectionsResource(FilteredCollectionsRest data, Utils utils) {
super(data, utils);
}
}

View File

@@ -0,0 +1,21 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.model.hateoas;
import org.dspace.app.rest.model.FilteredItemsRest;
import org.dspace.app.rest.model.hateoas.annotations.RelNameDSpaceResource;
import org.dspace.app.rest.utils.Utils;
@RelNameDSpaceResource(FilteredItemsRest.NAME)
public class FilteredItemsResource extends DSpaceResource<FilteredItemsRest> {
public FilteredItemsResource(FilteredItemsRest data, Utils utils) {
super(data, utils);
}
}

View File

@@ -0,0 +1,97 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.repository;
import java.sql.SQLException;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.dspace.app.rest.converter.FilteredItemConverter;
import org.dspace.app.rest.model.ContentReportSupportRest;
import org.dspace.app.rest.model.FilteredCollectionsQuery;
import org.dspace.app.rest.model.FilteredCollectionsRest;
import org.dspace.app.rest.model.FilteredItemRest;
import org.dspace.app.rest.model.FilteredItemsQueryPredicate;
import org.dspace.app.rest.model.FilteredItemsQueryRest;
import org.dspace.app.rest.model.FilteredItemsRest;
import org.dspace.app.rest.projection.Projection;
import org.dspace.content.MetadataField;
import org.dspace.contentreport.Filter;
import org.dspace.contentreport.FilteredCollection;
import org.dspace.contentreport.FilteredCollections;
import org.dspace.contentreport.FilteredItems;
import org.dspace.contentreport.FilteredItemsQuery;
import org.dspace.contentreport.QueryPredicate;
import org.dspace.contentreport.service.ContentReportService;
import org.dspace.core.Context;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Component;
/**
* This repository serves the content reports ported from DSpace 6.x
* (Filtered Collections and Filtered Items).
* @author Jean-François Morin (Université Laval)
*/
@Component(ContentReportSupportRest.CATEGORY + "." + ContentReportSupportRest.NAME)
public class ContentReportRestRepository extends AbstractDSpaceRestRepository {
@Autowired
private ContentReportService contentReportService;
@Autowired
private FilteredItemConverter itemConverter;
public ContentReportSupportRest getContentReportSupport() {
return new ContentReportSupportRest();
}
public FilteredCollectionsRest findFilteredCollections(Context context, FilteredCollectionsQuery query) {
Set<Filter> filters = query.getFilters();
List<FilteredCollection> colls = contentReportService.findFilteredCollections(context, filters);
FilteredCollections report = FilteredCollections.of(colls);
FilteredCollectionsRest reportRest = FilteredCollectionsRest.of(report);
reportRest.setId("filteredcollections");
return reportRest;
}
public FilteredItemsRest findFilteredItems(Context context, FilteredItemsQueryRest queryRest, Pageable pageable) {
List<QueryPredicate> predicates = queryRest.getQueryPredicates().stream()
.map(pred -> convertPredicate(context, pred))
.collect(Collectors.toList());
FilteredItemsQuery query = new FilteredItemsQuery();
query.setCollections(queryRest.getCollections());
query.setQueryPredicates(predicates);
query.setFilters(queryRest.getFilters());
query.setAdditionalFields(queryRest.getAdditionalFields());
query.setOffset(pageable.getOffset());
query.setPageLimit(pageable.getPageSize());
FilteredItems items = contentReportService.findFilteredItems(context, query);
List<FilteredItemRest> filteredItemsRest = items.getItems().stream()
.map(item -> itemConverter.convert(item, Projection.DEFAULT))
.collect(Collectors.toList());
FilteredItemsRest report = FilteredItemsRest.of(filteredItemsRest, items.getItemCount());
report.setId("filtereditems");
return report;
}
private QueryPredicate convertPredicate(Context context, FilteredItemsQueryPredicate predicate) {
try {
List<MetadataField> fields = contentReportService.getMetadataFields(context, predicate.getField());
return QueryPredicate.of(fields, predicate.getOperator(), predicate.getValue());
} catch (SQLException e) {
throw new IllegalArgumentException(e.getMessage(), e);
}
}
}

View File

@@ -0,0 +1,263 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest;
import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.dspace.app.rest.matcher.ContentReportMatcher;
import org.dspace.app.rest.matcher.HalMatcher;
import org.dspace.app.rest.model.FilteredCollectionsQuery;
import org.dspace.app.rest.model.FilteredItemsQueryPredicate;
import org.dspace.app.rest.model.FilteredItemsQueryRest;
import org.dspace.app.rest.test.AbstractControllerIntegrationTest;
import org.dspace.builder.CollectionBuilder;
import org.dspace.builder.CommunityBuilder;
import org.dspace.builder.ItemBuilder;
import org.dspace.content.Collection;
import org.dspace.content.Item;
import org.dspace.contentreport.Filter;
import org.dspace.contentreport.FilteredCollection;
import org.dspace.contentreport.QueryOperator;
import org.dspace.services.ConfigurationService;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
/**
* Integration tests for the content reports ported from DSpace 6.x
* (Filtered Collections and Filtered Items).
* @author Jean-François Morin (Université Laval)
*/
public class ContentReportRestRepositoryIT extends AbstractControllerIntegrationTest {
@Autowired
private ConfigurationService configurationService;
@Test
public void testFilteredCollections() throws Exception {
context.turnOffAuthorisationSystem();
configurationService.setProperty("contentreport.enable", Boolean.TRUE);
TestKit testKit = setupCollectionsAndItems();
Collection col1 = testKit.collections.get(0);
Collection col2 = testKit.collections.get(1);
context.restoreAuthSystemState();
String token = getAuthToken(admin.getEmail(), password);
Map<Filter, Integer> valuesCol1 = Map.of(Filter.IS_DISCOVERABLE, 1);
FilteredCollection fcol1 = FilteredCollection.of(col1.getName(), col1.getHandle(),
parentCommunity.getName(), parentCommunity.getHandle(),
1, 1, valuesCol1, true);
Map<Filter, Integer> valuesCol2 = Map.of(Filter.IS_DISCOVERABLE, 2);
FilteredCollection fcol2 = FilteredCollection.of(col2.getName(), col2.getHandle(),
parentCommunity.getName(), parentCommunity.getHandle(),
2, 2, valuesCol2, true);
FilteredCollectionsQuery query = FilteredCollectionsQuery.of(Set.of(Filter.IS_DISCOVERABLE));
getClient(token).perform(get("/api/contentreport/filteredcollections?" + query.toQueryString()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.collections", Matchers.containsInAnyOrder(
ContentReportMatcher.matchFilteredCollectionProperties(fcol1),
ContentReportMatcher.matchFilteredCollectionProperties(fcol2)
)))
.andExpect(jsonPath("type", is("filteredcollectionsreport")))
.andExpect(jsonPath("$.summary",
ContentReportMatcher.matchFilteredCollectionSummary(3, 3)))
.andExpect(jsonPath("$._links.self.href",
Matchers.containsString("/api/contentreport/filteredcollections")));
}
@Test
public void testFilteredCollectionsUnauthorized() throws Exception {
context.turnOffAuthorisationSystem();
configurationService.setProperty("contentreport.enable", Boolean.TRUE);
setupCollectionsAndItems();
context.restoreAuthSystemState();
FilteredCollectionsQuery query = FilteredCollectionsQuery.of(Set.of(Filter.IS_DISCOVERABLE));
getClient().perform(get("/api/contentreport/filteredcollections?" + query.toQueryString()))
.andExpect(status().isUnauthorized());
}
@Test
public void testFilteredCollectionsOff() throws Exception {
context.turnOffAuthorisationSystem();
configurationService.setProperty("contentreport.enable", Boolean.FALSE);
setupCollectionsAndItems();
context.restoreAuthSystemState();
String token = getAuthToken(admin.getEmail(), password);
FilteredCollectionsQuery query = FilteredCollectionsQuery.of(Set.of(Filter.IS_DISCOVERABLE));
getClient(token).perform(get("/api/contentreport/filteredcollections?" + query.toQueryString()))
.andExpect(status().isNotFound());
}
@Test
public void testFilteredItems() throws Exception {
context.turnOffAuthorisationSystem();
configurationService.setProperty("contentreport.enable", Boolean.TRUE);
TestKit testKit = setupCollectionsAndItems();
Item publicItem2 = testKit.items.get(1);
Item publicItem3 = testKit.items.get(2);
context.restoreAuthSystemState();
String token = getAuthToken(admin.getEmail(), password);
FilteredItemsQueryRest query = FilteredItemsQueryRest.of(null,
List.of(FilteredItemsQueryPredicate.of("dc.contributor.author", QueryOperator.EQUALS, "Doe, Jane")),
100, null, List.of("dc.subject"));
getClient(token).perform(get("/api/contentreport/filtereditems?" + query.toQueryString()))
.andExpect(status().isOk())
.andExpect(jsonPath("$", HalMatcher.matchNoEmbeds()))
.andExpect(jsonPath("$.itemCount", is(2)))
.andExpect(jsonPath("$.items", Matchers.containsInAnyOrder(
matchItemProperties(publicItem2),
matchItemProperties(publicItem3)
)));
}
@Test
public void testFilteredItemsUnauthorized() throws Exception {
context.turnOffAuthorisationSystem();
configurationService.setProperty("contentreport.enable", Boolean.TRUE);
setupCollectionsAndItems();
context.restoreAuthSystemState();
FilteredItemsQueryRest query = FilteredItemsQueryRest.of(null,
List.of(FilteredItemsQueryPredicate.of("dc.contributor.author", QueryOperator.EQUALS, "Doe, Jane")),
100, null, List.of("dc.subject"));
getClient().perform(get("/api/contentreport/filtereditems?" + query.toQueryString()))
.andExpect(status().isUnauthorized());
}
@Test
public void testFilteredItemsOff() throws Exception {
context.turnOffAuthorisationSystem();
configurationService.setProperty("contentreport.enable", Boolean.FALSE);
setupCollectionsAndItems();
context.restoreAuthSystemState();
String token = getAuthToken(admin.getEmail(), password);
FilteredItemsQueryRest query = FilteredItemsQueryRest.of(null,
List.of(FilteredItemsQueryPredicate.of("dc.contributor.author", QueryOperator.EQUALS, "Doe, Jane")),
100, null, List.of("dc.subject"));
getClient(token).perform(get("/api/contentreport/filtereditems?" + query.toQueryString()))
.andExpect(status().isNotFound());
}
// Need for a specific filtered item type...
private static Matcher<? super Object> matchItemProperties(Item item) {
return allOf(
hasJsonPath("$.uuid", is(item.getID().toString())),
hasJsonPath("$.name", is(item.getName())),
hasJsonPath("$.handle", is(item.getHandle())),
hasJsonPath("$.inArchive", is(item.isArchived())),
hasJsonPath("$.discoverable", is(item.isDiscoverable())),
hasJsonPath("$.withdrawn", is(item.isWithdrawn())),
hasJsonPath("$.lastModified", is(notNullValue())),
hasJsonPath("$.type", is("filtered-item"))
);
}
private TestKit setupCollectionsAndItems() {
//** GIVEN **
//1. A community with two collections.
parentCommunity = CommunityBuilder.createCommunity(context)
.withName("My Community")
.build();
Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build();
Collection col2 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 2").build();
LocalDate today = LocalDate.now();
LocalDate pastDate = today.minusDays(10);
String fmtPastDate = DateTimeFormatter.ISO_DATE.format(pastDate);
LocalDate futureDate = today.plusDays(10);
String fmtFutureDate = DateTimeFormatter.ISO_DATE.format(futureDate);
//2. Three items, two of which are available and discoverable...
Item item1 = ItemBuilder.createItem(context, col1)
.withTitle("Public item 1")
.withIssueDate(fmtPastDate)
.withAuthor("Smith, Donald").withAuthor("Doe, John")
.withSubject("ExtraEntry")
.build();
Item item2 = ItemBuilder.createItem(context, col2)
.withTitle("Public item 2")
.withIssueDate(fmtPastDate)
.withAuthor("Smith, Maria").withAuthor("Doe, Jane")
.withSubject("TestingForMore").withSubject("ExtraEntry")
.build();
// ... and one will be available in a few days.
Item item3 = ItemBuilder.createItem(context, col2)
.withTitle("Public item 3")
.withIssueDate(fmtFutureDate)
.withAuthor("Smith, Maria").withAuthor("Doe, Jane")
.withSubject("AnotherTest").withSubject("TestingForMore")
.withSubject("ExtraEntry")
.build();
TestKit kit = new TestKit();
kit.collections.add(col1);
kit.collections.add(col2);
kit.items.add(item1);
kit.items.add(item2);
kit.items.add(item3);
return kit;
}
/**
* Data structure to help trace back the created collections and items to perform the tests.
* In a future version (Java 17 or later), this class could be turned into a record.
*/
private static class TestKit {
public final List<Collection> collections = new ArrayList<>();
public final List<Item> items = new ArrayList<>();
}
}

View File

@@ -0,0 +1,52 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.app.rest.matcher;
import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.is;
import org.dspace.content.Item;
import org.dspace.contentreport.FilteredCollection;
import org.hamcrest.Matcher;
/**
* Utility class to construct a Matcher for a FilteredCollectionRest.
* @author Jean-François Morin (Université Laval)
*/
public class ContentReportMatcher {
private ContentReportMatcher() { }
public static Matcher<? super Object> matchFilteredCollectionProperties(FilteredCollection collection) {
return allOf(
hasJsonPath("$.label", is(collection.getLabel())),
hasJsonPath("$.community_label", is(collection.getCommunityLabel())),
hasJsonPath("$.community_handle", is(collection.getCommunityHandle())),
hasJsonPath("$.nb_total_items", is(collection.getTotalItems())),
hasJsonPath("$.all_filters_value", is(collection.getAllFiltersValue()))
);
}
public static Matcher<? super Object> matchFilteredCollectionSummary(int nbTotalItems, int nbFilteredItems) {
return allOf(
hasJsonPath("$.nb_total_items", is(nbTotalItems)),
hasJsonPath("$.all_filters_value", is(nbFilteredItems)));
}
public static Matcher<? super Object> matchFilteredItemProperties(Item item) {
return allOf(
hasJsonPath("$.name", is(item.getName())),
hasJsonPath("$.inArchive", is(item.isArchived())),
hasJsonPath("$.discoverable", is(item.isDiscoverable())),
hasJsonPath("$.withdrawn", is(item.isWithdrawn())),
hasJsonPath("$.type", is("item"))
);
}
}

View File

@@ -84,7 +84,7 @@ db.url = jdbc:postgresql://localhost:5432/dspace
db.driver = org.postgresql.Driver db.driver = org.postgresql.Driver
# PostgreSQL Database Dialect (for Hibernate) # PostgreSQL Database Dialect (for Hibernate)
db.dialect = org.hibernate.dialect.PostgreSQL94Dialect db.dialect = org.dspace.util.DSpacePostgreSQLDialect
# Database username and password # Database username and password
db.username = dspace db.username = dspace
@@ -1668,3 +1668,4 @@ include = ${module_dir}/usage-statistics.cfg
include = ${module_dir}/versioning.cfg include = ${module_dir}/versioning.cfg
include = ${module_dir}/workflow.cfg include = ${module_dir}/workflow.cfg
include = ${module_dir}/external-providers.cfg include = ${module_dir}/external-providers.cfg
include = ${module_dir}/contentreport.cfg

View File

@@ -76,20 +76,10 @@ dspace.name = DSpace at My University
# URL for connecting to database # URL for connecting to database
db.url = jdbc:postgresql://localhost:5432/dspace db.url = jdbc:postgresql://localhost:5432/dspace
# JDBC Driver for PostgreSQL
db.driver = org.postgresql.Driver
# PostgreSQL Database Dialect (for Hibernate)
db.dialect = org.hibernate.dialect.PostgreSQL94Dialect
# Database username and password # Database username and password
db.username = dspace db.username = dspace
db.password = dspace db.password = dspace
# Database Schema name
# For PostgreSQL, this is often "public" (default schema)
db.schema = public
## Connection pool parameters ## Connection pool parameters
# Maximum number of DB connections in pool (default = 30) # Maximum number of DB connections in pool (default = 30)
@@ -240,4 +230,4 @@ db.schema = public
#spring.servlet.multipart.max-file-size = 512MB #spring.servlet.multipart.max-file-size = 512MB
# Maximum size of a multipart request (i.e. max total size of all files in one request) # Maximum size of a multipart request (i.e. max total size of all files in one request)
#spring.servlet.multipart.max-request-size = 512MB #spring.servlet.multipart.max-request-size = 512MB

View File

@@ -0,0 +1,10 @@
#---------------------------------------------------------------#
#--------------CONTENT REPORTS CONFIGURATIONS-------------------#
#---------------------------------------------------------------#
# Used by dspace-server-webapp and the Angular UI #
# NOTE: This is currently a beta feature. #
#---------------------------------------------------------------#
# Configuration setting to trigger showing/hiding the Content Reports
# (REST endpoints on the REST side and menu section on the Angular side)
#contentreport.enable=true

View File

@@ -54,4 +54,5 @@ rest.properties.exposed = google.recaptcha.mode
rest.properties.exposed = cc.license.jurisdiction rest.properties.exposed = cc.license.jurisdiction
rest.properties.exposed = identifiers.item-status.register-doi rest.properties.exposed = identifiers.item-status.register-doi
rest.properties.exposed = authentication-password.domain.valid rest.properties.exposed = authentication-password.domain.valid
rest.properties.exposed = handle.canonical.prefix rest.properties.exposed = handle.canonical.prefix
rest.properties.exposed = contentreport.enable

View File

@@ -77,6 +77,8 @@
<constructor-arg name='configPrefix' value='solr'/> <constructor-arg name='configPrefix' value='solr'/>
</bean> </bean>
<bean class="org.dspace.contentreport.ContentReportServiceImpl"/>
<!-- Ensure PluginService is initialized properly via init() method --> <!-- Ensure PluginService is initialized properly via init() method -->
<bean class="org.dspace.core.LegacyPluginServiceImpl" init-method="init"/> <bean class="org.dspace.core.LegacyPluginServiceImpl" init-method="init"/>
<bean class="org.dspace.core.LicenseServiceImpl"/> <bean class="org.dspace.core.LicenseServiceImpl"/>