From 765809a426a61c3b488fd9be60a465bc647b742d Mon Sep 17 00:00:00 2001 From: Ben Pennell Date: Fri, 6 Dec 2024 10:01:24 -0500 Subject: [PATCH] BXC-4786 - Custom alt text (#1849) * Add enums for alt text datastreams, permissions. Add test coverage for datastream classes * Add minimal service for updating alt text. Add minimal controller for performing service. Add relation * Add alt text field to solr and add filter for indexing it. Add facet values to track when it is present. Add tests, plus some for cases that weren't previously covered * Add serialization of alt text field. Some cleanup of serialization, and added some additional test coverage * Add admin ui form for setting alt text * Fix how alt text is retrieved. Add missing annotation so that alt text indexes. Add alt text to search result field lists * Only submit text as body param, otherwise text gets duplicated * Use custom alt text for thumbnails if available * Start adding method for getting thumbnail record, so we can get the alt text at the same time * Add alt text during deposit. Make sending a message configurable, and allow for shared transfer session * Populate alt text for works from thumbnail object. Change accessCopiesService methods to work with object records and set alt text * Codeclimate and add missing property injection * Address comments and make input larger * Use different verb for link text versus image text, but use custom alt text for both --- .../services/DatastreamPermissionUtil.java | 2 + .../fcrepo4/IngestContentObjectsJob.java | 23 ++ .../boxc/deposit/work/AbstractDepositJob.java | 15 ++ .../webapp/WEB-INF/deposit-jobs-context.xml | 8 + .../fcrepo4/IngestContentObjectsJobTest.java | 49 ++++- .../src/test/resources/examples/image.jpg | 0 .../spring-test/cdr-client-container.xml | 8 + .../boxc/deposit/api/DepositConstants.java | 1 + etc/solr-config/access/conf/schema.xml | 3 + .../solr/filter/SetAltTextFilter.java | 50 +++++ .../solr/filter/SetContentStatusFilter.java | 43 ++-- .../solr/filter/SetAltTextFilterTest.java | 126 +++++++++++ .../filter/SetContentStatusFilterTest.java | 119 +++++++++-- .../spring-test/solr-indexing-context.xml | 5 + .../lib/boxc/model/api/DatastreamType.java | 16 +- .../edu/unc/lib/boxc/model/api/rdf/Cdr.java | 3 + .../boxc/model/api/DatastreamTypeTest.java | 58 +++++ .../boxc/model/fcrepo/ids/DatastreamPids.java | 4 + .../model/fcrepo/ids/DatastreamPidsTest.java | 95 +++++++++ .../jms/altText/AltTextUpdateRequest.java | 51 +++++ .../impl/altText/AltTextUpdateService.java | 94 +++++++++ .../altText/AltTextUpdateServiceTest.java | 198 ++++++++++++++++++ .../lib/boxc/search/api/FacetConstants.java | 2 + .../lib/boxc/search/api/SearchFieldKey.java | 1 + .../api/models/ContentObjectRecord.java | 7 + .../GroupedContentObjectSolrRecord.java | 10 + .../search/solr/models/IndexDocumentBean.java | 9 + .../solr/services/AbstractQueryService.java | 1 + .../webapp/WEB-INF/solr-indexing-context.xml | 6 + .../resources/solr-indexing-it-context.xml | 1 + .../spring-test/solr-indexing-context.xml | 5 + static/js/admin/src/EditAltTextForm.js | 51 +++++ static/js/admin/src/ResultObjectActionMenu.js | 50 ++++- .../src/components/full_record/thumbnail.vue | 29 ++- .../tests/unit/thumbnail.spec.js | 17 +- .../admin/.editCollectionSettings.html.swp | Bin 12288 -> 0 bytes static/templates/admin/editAltTextForm.html | 16 ++ .../controllers/FullRecordController.java | 7 +- .../controllers/SearchActionController.java | 4 +- .../controllers/AbstractSearchController.java | 2 +- .../controllers/ResultEntryController.java | 3 +- .../common/services/AccessCopiesService.java | 43 ++-- .../web/common/utils/SerializationUtil.java | 14 +- .../services/AccessCopiesServiceTest.java | 130 ++++++++++-- .../common/utils/SerializationUtilTest.java | 59 ++++++ .../services/rest/DatastreamController.java | 8 +- .../services/rest/SearchRestController.java | 4 +- .../rest/modify/AltTextController.java | 48 +++++ .../main/webapp/WEB-INF/service-context.xml | 9 + .../rest/modify/AltTextControllerTest.java | 108 ++++++++++ .../spring-test/solr-indexing-context.xml | 5 + 51 files changed, 1505 insertions(+), 115 deletions(-) create mode 100644 deposit-app/src/test/resources/examples/image.jpg create mode 100644 indexing-solr/src/main/java/edu/unc/lib/boxc/indexing/solr/filter/SetAltTextFilter.java create mode 100644 indexing-solr/src/test/java/edu/unc/lib/boxc/indexing/solr/filter/SetAltTextFilterTest.java create mode 100644 model-api/src/test/java/edu/unc/lib/boxc/model/api/DatastreamTypeTest.java create mode 100644 model-fcrepo/src/test/java/edu/unc/lib/boxc/model/fcrepo/ids/DatastreamPidsTest.java create mode 100644 operations-jms/src/main/java/edu/unc/lib/boxc/operations/jms/altText/AltTextUpdateRequest.java create mode 100644 operations/src/main/java/edu/unc/lib/boxc/operations/impl/altText/AltTextUpdateService.java create mode 100644 operations/src/test/java/edu/unc/lib/boxc/operations/impl/altText/AltTextUpdateServiceTest.java create mode 100644 static/js/admin/src/EditAltTextForm.js delete mode 100644 static/templates/admin/.editCollectionSettings.html.swp create mode 100644 static/templates/admin/editAltTextForm.html create mode 100644 web-services-app/src/main/java/edu/unc/lib/boxc/web/services/rest/modify/AltTextController.java create mode 100644 web-services-app/src/test/java/edu/unc/lib/boxc/web/services/rest/modify/AltTextControllerTest.java diff --git a/auth-api/src/main/java/edu/unc/lib/boxc/auth/api/services/DatastreamPermissionUtil.java b/auth-api/src/main/java/edu/unc/lib/boxc/auth/api/services/DatastreamPermissionUtil.java index 44aa219d96..d751ef790e 100644 --- a/auth-api/src/main/java/edu/unc/lib/boxc/auth/api/services/DatastreamPermissionUtil.java +++ b/auth-api/src/main/java/edu/unc/lib/boxc/auth/api/services/DatastreamPermissionUtil.java @@ -25,6 +25,8 @@ public class DatastreamPermissionUtil { DS_PERMISSION_MAP.put(DatastreamType.JP2_ACCESS_COPY, Permission.viewAccessCopies); DS_PERMISSION_MAP.put(DatastreamType.AUDIO_ACCESS_COPY, Permission.viewAccessCopies); DS_PERMISSION_MAP.put(DatastreamType.ACCESS_SURROGATE, Permission.viewAccessCopies); + DS_PERMISSION_MAP.put(DatastreamType.ALT_TEXT, Permission.viewMetadata); + DS_PERMISSION_MAP.put(DatastreamType.ALT_TEXT_HISTORY, Permission.viewHidden); DS_PERMISSION_MAP.put(DatastreamType.MD_DESCRIPTIVE, Permission.viewMetadata); DS_PERMISSION_MAP.put(DatastreamType.MD_DESCRIPTIVE_HISTORY, Permission.viewHidden); DS_PERMISSION_MAP.put(DatastreamType.MD_EVENTS, Permission.viewHidden); diff --git a/deposit-app/src/main/java/edu/unc/lib/boxc/deposit/fcrepo4/IngestContentObjectsJob.java b/deposit-app/src/main/java/edu/unc/lib/boxc/deposit/fcrepo4/IngestContentObjectsJob.java index 74059b8269..584d1526db 100644 --- a/deposit-app/src/main/java/edu/unc/lib/boxc/deposit/fcrepo4/IngestContentObjectsJob.java +++ b/deposit-app/src/main/java/edu/unc/lib/boxc/deposit/fcrepo4/IngestContentObjectsJob.java @@ -14,6 +14,7 @@ import java.io.IOException; import java.io.InputStream; import java.net.URI; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -25,6 +26,8 @@ import java.util.Set; import edu.unc.lib.boxc.model.api.exceptions.NotFoundException; +import edu.unc.lib.boxc.operations.impl.altText.AltTextUpdateService; +import edu.unc.lib.boxc.operations.jms.altText.AltTextUpdateRequest; import org.apache.http.HttpStatus; import org.apache.jena.datatypes.xsd.XSDDatatype; import org.apache.jena.rdf.model.Bag; @@ -137,6 +140,9 @@ public class IngestContentObjectsJob extends AbstractDepositJob { @Autowired private UpdateDescriptionService updateDescService; + @Autowired + private AltTextUpdateService altTextUpdateService; + private AccessGroupSet groupSet; private AgentPrincipals agent; @@ -342,6 +348,8 @@ private void ingestFileObject(ContentObject parent, Resource parentResc, Resourc addPremisEvents(obj); // add MODS addDescription(obj, childResc); + // Add alt text if present + addAltText(obj); overrideModifiedTimestamp(obj, childResc); log.debug("Finished all updates for file {} in work {}", pid, work.getPid()); @@ -824,6 +832,21 @@ private void addAclProperties(Resource dResc, Resource aResc) { } } + private void addAltText(ContentObject obj) throws IOException { + Path altTextPath = getAltTextPath(obj.getPid(), false); + if (!Files.exists(altTextPath)) { + return; + } + + var altText = Files.readString(altTextPath, StandardCharsets.UTF_8); + var request = new AltTextUpdateRequest(); + request.setAgent(agent); + request.setPidString(obj.getPid().getId()); + request.setAltText(altText); + request.setTransferSession(logTransferSession); + altTextUpdateService.updateAltText(request); + } + private void addDescription(ContentObject obj, Resource dResc) throws IOException { addDescriptionHistory(obj, dResc); diff --git a/deposit-app/src/main/java/edu/unc/lib/boxc/deposit/work/AbstractDepositJob.java b/deposit-app/src/main/java/edu/unc/lib/boxc/deposit/work/AbstractDepositJob.java index 1e62a10d7b..756b6c2fe6 100644 --- a/deposit-app/src/main/java/edu/unc/lib/boxc/deposit/work/AbstractDepositJob.java +++ b/deposit-app/src/main/java/edu/unc/lib/boxc/deposit/work/AbstractDepositJob.java @@ -1,5 +1,6 @@ package edu.unc.lib.boxc.deposit.work; +import static edu.unc.lib.boxc.deposit.api.DepositConstants.ALT_TEXT_DIR; import static edu.unc.lib.boxc.deposit.api.DepositConstants.DESCRIPTION_DIR; import static edu.unc.lib.boxc.deposit.api.DepositConstants.HISTORY_DIR; import static edu.unc.lib.boxc.deposit.api.DepositConstants.TECHMD_DIR; @@ -239,6 +240,10 @@ public String getDepositField(DepositField field) { return getDepositStatus().get(field.name()); } + public File getAltTextDir() { + return new File(getDepositDirectory(), ALT_TEXT_DIR); + } + public File getDescriptionDir() { return new File(getDepositDirectory(), DESCRIPTION_DIR); } @@ -268,6 +273,16 @@ public Path getModsPath(PID pid, boolean createDirs) { return getMetadataPath(getDescriptionDir(), pid, ".xml", createDirs); } + /** + * Get the path where alt text should be stored for the given pid + * @param pid pid of the object + * @param createDirs if true, then parent directories for path will be created + * @return Path for the alt text + */ + public Path getAltTextPath(PID pid, boolean createDirs) { + return getMetadataPath(getAltTextDir(), pid, ".txt", createDirs); + } + /** * Get path to where MODS history should be stored * diff --git a/deposit-app/src/main/webapp/WEB-INF/deposit-jobs-context.xml b/deposit-app/src/main/webapp/WEB-INF/deposit-jobs-context.xml index 98ede9e199..75a5eeb30f 100644 --- a/deposit-app/src/main/webapp/WEB-INF/deposit-jobs-context.xml +++ b/deposit-app/src/main/webapp/WEB-INF/deposit-jobs-context.xml @@ -106,6 +106,14 @@ + + + + + + + + diff --git a/deposit-app/src/test/java/edu/unc/lib/boxc/deposit/fcrepo4/IngestContentObjectsJobTest.java b/deposit-app/src/test/java/edu/unc/lib/boxc/deposit/fcrepo4/IngestContentObjectsJobTest.java index ec699e843b..0f3cc0f2e2 100644 --- a/deposit-app/src/test/java/edu/unc/lib/boxc/deposit/fcrepo4/IngestContentObjectsJobTest.java +++ b/deposit-app/src/test/java/edu/unc/lib/boxc/deposit/fcrepo4/IngestContentObjectsJobTest.java @@ -8,6 +8,7 @@ import static edu.unc.lib.boxc.model.api.StreamingConstants.STREAMREAPER_PREFIX_URL; import static edu.unc.lib.boxc.persist.impl.storage.StorageLocationTestHelper.LOC1_ID; import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.ArgumentMatchers.any; @@ -33,6 +34,8 @@ import java.util.Map; import edu.unc.lib.boxc.model.api.exceptions.NotFoundException; +import edu.unc.lib.boxc.operations.impl.altText.AltTextUpdateService; +import edu.unc.lib.boxc.operations.jms.altText.AltTextUpdateRequest; import org.apache.commons.io.FileUtils; import org.apache.http.HttpStatus; import org.apache.jena.rdf.model.Bag; @@ -159,8 +162,12 @@ public class IngestContentObjectsJobTest extends AbstractDepositJobTest { private BinaryTransferSession mockTransferSession; @Mock private UpdateDescriptionService updateDescService; + @Mock + private AltTextUpdateService altTextUpdateService; @Captor private ArgumentCaptor modelCaptor; + @Captor + private ArgumentCaptor altTextRequestCaptor; private Path storageLocPath; @@ -192,6 +199,7 @@ public void init() throws Exception { setField(job, "locationManager", storageLocationManager); setField(job, "updateDescService", updateDescService); setField(job, "depositModelManager", depositModelManager); + setField(job, "altTextUpdateService", altTextUpdateService); job.init(); @@ -379,7 +387,6 @@ public void ingestWorkWithFileWithStreamingPropertiesAndOriginalFile() throws Ex fileResc.addProperty(CdrDeposit.mimetype, "text/plain"); fileResc.addProperty(Cdr.streamingUrl, STREAMREAPER_PREFIX_URL); fileResc.addProperty(Cdr.streamingType, STREAMING_TYPE); - workBag.add(fileResc); job.closeModel(); @@ -394,7 +401,45 @@ public void ingestWorkWithFileWithStreamingPropertiesAndOriginalFile() throws Ex verify(repoObjFactory).createWorkObject(eq(workPid), any(Model.class)); verify(destinationObj).addMember(eq(work)); - verify(jobStatusFactory, times(3)).incrCompletion(eq(jobUUID), eq(1)); + verify(jobStatusFactory, times(2)).incrCompletion(eq(jobUUID), eq(1)); + } + + @Test + public void ingestWorkWithFileWithAltText() throws Exception { + PID workPid = makePid(RepositoryPathConstants.CONTENT_BASE); + WorkObject work = mock(WorkObject.class); + Bag workBag = setupWork(workPid, work); + + String loc = "image.jpg"; + String mime = "image/jpeg"; + PID filePid = addFileObject(workBag, loc, mime); + + var fileResc = model.getResource(filePid.getRepositoryPath()); + fileResc.addProperty(RDF.type, Cdr.FileObject); + fileResc.addProperty(CdrDeposit.mimetype, mime); + + Path altTextPath = job.getAltTextPath(filePid, true); + FileUtils.writeStringToFile(altTextPath.toFile(), "Alternative text", UTF_8); + + job.closeModel(); + + when(work.addDataFile(any(PID.class), any(URI.class), + anyString(), anyString(), isNull(), isNull(), any(Model.class))) + .thenReturn(mockFileObj); + when(mockFileObj.getPid()).thenReturn(filePid); + when(repoObjLoader.getWorkObject(eq(workPid))).thenReturn(work); + + job.run(); + + verify(repoObjFactory).createWorkObject(eq(workPid), any(Model.class)); + verify(destinationObj).addMember(eq(work)); + + verify(jobStatusFactory, times(2)).incrCompletion(eq(jobUUID), eq(1)); + + verify(altTextUpdateService).updateAltText(altTextRequestCaptor.capture()); + AltTextUpdateRequest request = altTextRequestCaptor.getValue(); + assertEquals("Alternative text", request.getAltText()); + assertEquals(filePid.getId(), request.getPidString()); } /** diff --git a/deposit-app/src/test/resources/examples/image.jpg b/deposit-app/src/test/resources/examples/image.jpg new file mode 100644 index 0000000000..e69de29bb2 diff --git a/deposit-app/src/test/resources/spring-test/cdr-client-container.xml b/deposit-app/src/test/resources/spring-test/cdr-client-container.xml index ca6b3c559d..670458bcf4 100644 --- a/deposit-app/src/test/resources/spring-test/cdr-client-container.xml +++ b/deposit-app/src/test/resources/spring-test/cdr-client-container.xml @@ -149,6 +149,14 @@ + + + + + + + + diff --git a/deposit-utils/src/main/java/edu/unc/lib/boxc/deposit/api/DepositConstants.java b/deposit-utils/src/main/java/edu/unc/lib/boxc/deposit/api/DepositConstants.java index 11ad5531b3..26712f4079 100644 --- a/deposit-utils/src/main/java/edu/unc/lib/boxc/deposit/api/DepositConstants.java +++ b/deposit-utils/src/main/java/edu/unc/lib/boxc/deposit/api/DepositConstants.java @@ -7,6 +7,7 @@ */ public class DepositConstants { public static final String DESCRIPTION_DIR = "description"; + public static final String ALT_TEXT_DIR = "altText"; public static final String HISTORY_DIR = "history"; public static final String JENA_TDB_DIR = "jena-tdb-model"; public static final String EVENTS_FILE = "events.xml"; diff --git a/etc/solr-config/access/conf/schema.xml b/etc/solr-config/access/conf/schema.xml index 9bb6f9b5b4..108f26470c 100644 --- a/etc/solr-config/access/conf/schema.xml +++ b/etc/solr-config/access/conf/schema.xml @@ -117,6 +117,7 @@ + @@ -310,4 +312,5 @@ + diff --git a/indexing-solr/src/main/java/edu/unc/lib/boxc/indexing/solr/filter/SetAltTextFilter.java b/indexing-solr/src/main/java/edu/unc/lib/boxc/indexing/solr/filter/SetAltTextFilter.java new file mode 100644 index 0000000000..1486c41f20 --- /dev/null +++ b/indexing-solr/src/main/java/edu/unc/lib/boxc/indexing/solr/filter/SetAltTextFilter.java @@ -0,0 +1,50 @@ +package edu.unc.lib.boxc.indexing.solr.filter; + +import edu.unc.lib.boxc.indexing.solr.exception.IndexingException; +import edu.unc.lib.boxc.indexing.solr.indexing.DocumentIndexingPackage; +import edu.unc.lib.boxc.model.api.exceptions.NotFoundException; +import edu.unc.lib.boxc.model.api.objects.ContentObject; +import edu.unc.lib.boxc.model.api.objects.FileObject; +import edu.unc.lib.boxc.model.api.objects.RepositoryObjectLoader; +import edu.unc.lib.boxc.model.fcrepo.ids.DatastreamPids; +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * Filter which populates alt text for the object being indexed + * + * @author bbpennel + */ +public class SetAltTextFilter implements IndexDocumentFilter { + private static final Logger log = LoggerFactory.getLogger(SetAltTextFilter.class); + private RepositoryObjectLoader repositoryObjectLoader; + + @Override + public void filter(DocumentIndexingPackage dip) throws IndexingException { + ContentObject contentObj = dip.getContentObject(); + // object being indexed must be a file object + if (!(contentObj instanceof FileObject)) { + return; + } + + try { + var altTextPid = DatastreamPids.getAltTextPid(contentObj.getPid()); + var altTextBinary = repositoryObjectLoader.getBinaryObject(altTextPid); + var altText = IOUtils.toString(altTextBinary.getBinaryStream(), UTF_8); + dip.getDocument().setAltText(altText); + } catch (NotFoundException e) { + log.debug("No alt text datastream found for {}", dip.getPid()); + } catch (IOException e) { + throw new IndexingException("Failed to retrieve alt text datastream for {}" + dip.getPid(), e); + } + } + + public void setRepositoryObjectLoader(RepositoryObjectLoader repositoryObjectLoader) { + this.repositoryObjectLoader = repositoryObjectLoader; + } +} diff --git a/indexing-solr/src/main/java/edu/unc/lib/boxc/indexing/solr/filter/SetContentStatusFilter.java b/indexing-solr/src/main/java/edu/unc/lib/boxc/indexing/solr/filter/SetContentStatusFilter.java index d30d6b0972..4cd54c63ab 100644 --- a/indexing-solr/src/main/java/edu/unc/lib/boxc/indexing/solr/filter/SetContentStatusFilter.java +++ b/indexing-solr/src/main/java/edu/unc/lib/boxc/indexing/solr/filter/SetContentStatusFilter.java @@ -56,28 +56,37 @@ private List determineContentStatus(DocumentIndexingPackage dip) } if (obj instanceof FileObject) { - Resource parentResc = obj.getParent().getResource(); - if (parentResc.hasProperty(Cdr.primaryObject, resc)) { - status.add(FacetConstants.IS_PRIMARY_OBJECT); - } - if (parentResc.hasProperty(Cdr.useAsThumbnail, resc)) { - status.add(FacetConstants.ASSIGNED_AS_THUMBNAIL); - } - if (hasAccessSurrogate(obj.getPid())) { - status.add(FacetConstants.HAS_ACCESS_SURROGATE); - } else { - status.add(FacetConstants.NO_ACCESS_SURROGATE); - } - if (resc.hasProperty(Cdr.streamingUrl)) { - status.add(FacetConstants.HAS_STREAMING); - } else { - status.add(FacetConstants.NO_STREAMING); - } + addFileObjectStatuses(obj, resc, status); } return status; } + private void addFileObjectStatuses(ContentObject obj, Resource resc, List status) { + Resource parentResc = obj.getParent().getResource(); + if (parentResc.hasProperty(Cdr.primaryObject, resc)) { + status.add(FacetConstants.IS_PRIMARY_OBJECT); + } + if (parentResc.hasProperty(Cdr.useAsThumbnail, resc)) { + status.add(FacetConstants.ASSIGNED_AS_THUMBNAIL); + } + if (hasAccessSurrogate(obj.getPid())) { + status.add(FacetConstants.HAS_ACCESS_SURROGATE); + } else { + status.add(FacetConstants.NO_ACCESS_SURROGATE); + } + if (resc.hasProperty(Cdr.streamingUrl)) { + status.add(FacetConstants.HAS_STREAMING); + } else { + status.add(FacetConstants.NO_STREAMING); + } + if (resc.hasProperty(Cdr.hasAltText)) { + status.add(FacetConstants.HAS_ALT_TEXT); + } else { + status.add(FacetConstants.NO_ALT_TEXT); + } + } + private void addWorkObjectStatuses(List status, Resource resource) { if (resource.hasProperty(Cdr.primaryObject)) { status.add(FacetConstants.HAS_PRIMARY_OBJECT); diff --git a/indexing-solr/src/test/java/edu/unc/lib/boxc/indexing/solr/filter/SetAltTextFilterTest.java b/indexing-solr/src/test/java/edu/unc/lib/boxc/indexing/solr/filter/SetAltTextFilterTest.java new file mode 100644 index 0000000000..7d6ee4db6f --- /dev/null +++ b/indexing-solr/src/test/java/edu/unc/lib/boxc/indexing/solr/filter/SetAltTextFilterTest.java @@ -0,0 +1,126 @@ +package edu.unc.lib.boxc.indexing.solr.filter; + +import edu.unc.lib.boxc.indexing.solr.exception.IndexingException; +import edu.unc.lib.boxc.indexing.solr.indexing.DocumentIndexingPackage; +import edu.unc.lib.boxc.indexing.solr.indexing.DocumentIndexingPackageFactory; +import edu.unc.lib.boxc.model.api.DatastreamType; +import edu.unc.lib.boxc.model.api.exceptions.NotFoundException; +import edu.unc.lib.boxc.model.api.ids.PID; +import edu.unc.lib.boxc.model.api.objects.BinaryObject; +import edu.unc.lib.boxc.model.api.objects.ContentObject; +import edu.unc.lib.boxc.model.api.objects.FileObject; +import edu.unc.lib.boxc.model.api.objects.RepositoryObjectLoader; +import edu.unc.lib.boxc.model.fcrepo.test.TestHelper; +import edu.unc.lib.boxc.search.solr.config.SearchSettings; +import edu.unc.lib.boxc.search.solr.models.IndexDocumentBean; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.MockitoAnnotations; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * @author bbpennel + */ +public class SetAltTextFilterTest { + private SetAltTextFilter filter; + + @Mock + private ContentObject contentObject; + + @Mock + private FileObject fileObject; + @Mock + private BinaryObject binaryObject; + @Mock + private RepositoryObjectLoader repositoryObjectLoader; + + private PID pid; + private IndexDocumentBean document; + private DocumentIndexingPackage dip; + private DocumentIndexingPackageFactory factory; + + private AutoCloseable closeable; + + @BeforeEach + public void setup() { + closeable = MockitoAnnotations.openMocks(this); + pid = TestHelper.makePid(); + factory = new DocumentIndexingPackageFactory(); + filter = new SetAltTextFilter(); + filter.setRepositoryObjectLoader(repositoryObjectLoader); + dip = factory.createDip(pid); + document = dip.getDocument(); + when(fileObject.getPid()).thenReturn(pid); + when(repositoryObjectLoader.getBinaryObject(any(PID.class))).thenReturn(binaryObject); + } + + @AfterEach + void closeService() throws Exception { + closeable.close(); + } + + @Test + public void testFilterSetsAltText() throws Exception { + String altTextContent = "Sample Alt Text"; + var altTextStream = new ByteArrayInputStream(altTextContent.getBytes()); + + dip.setContentObject(fileObject); + when(binaryObject.getBinaryStream()).thenReturn(altTextStream); + + filter.filter(dip); + + assertEquals(altTextContent, document.getAltText()); + } + + @Test + public void testFilterHandlesMissingAltText() throws Exception { + dip.setContentObject(fileObject); + when(repositoryObjectLoader.getBinaryObject(any(PID.class))).thenThrow(new NotFoundException("Not found")); + + filter.filter(dip); + + assertNull(document.getAltText()); + } + + @Test + public void testFilterHandlesIOException() throws Exception { + dip.setContentObject(fileObject); + String altTextContent = "Bad alt text"; + var altTextStream = new ByteArrayInputStream(altTextContent.getBytes()); + when(binaryObject.getBinaryStream()).thenReturn(altTextStream); + + try (MockedStatic mockedStatic = mockStatic(IOUtils.class)) { + mockedStatic.when(() -> IOUtils.toString(any(InputStream.class), eq(UTF_8))) + .thenThrow(new IOException("Test IO Exception")); + + assertThrows(IndexingException.class, () -> filter.filter(dip)); + } + } + + @Test + public void testFilterNonFileObject() throws Exception { + dip.setContentObject(contentObject); + + filter.filter(dip); + + assertNull(document.getAltText()); + } +} diff --git a/indexing-solr/src/test/java/edu/unc/lib/boxc/indexing/solr/filter/SetContentStatusFilterTest.java b/indexing-solr/src/test/java/edu/unc/lib/boxc/indexing/solr/filter/SetContentStatusFilterTest.java index 508b626206..3dd0539119 100644 --- a/indexing-solr/src/test/java/edu/unc/lib/boxc/indexing/solr/filter/SetContentStatusFilterTest.java +++ b/indexing-solr/src/test/java/edu/unc/lib/boxc/indexing/solr/filter/SetContentStatusFilterTest.java @@ -1,5 +1,6 @@ package edu.unc.lib.boxc.indexing.solr.filter; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -14,9 +15,12 @@ import java.util.List; import edu.unc.lib.boxc.model.api.DatastreamType; +import edu.unc.lib.boxc.model.api.rdf.CdrView; import edu.unc.lib.boxc.model.fcrepo.services.DerivativeService; +import edu.unc.lib.boxc.operations.jms.viewSettings.ViewSettingRequest; import org.apache.jena.rdf.model.Property; import org.apache.jena.rdf.model.Resource; +import org.apache.jena.rdf.model.Statement; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -55,6 +59,8 @@ public class SetContentStatusFilterTest { private Resource resc, fileResc; @Mock private DerivativeService derivativeService; + @Mock + private Statement statement; @Captor private ArgumentCaptor> listCaptor; @TempDir @@ -72,6 +78,7 @@ public void setUp() throws Exception { when(resc.hasProperty(any(Property.class))).thenReturn(false); when(fileObj.getParent()).thenReturn(workObj); + when(workObj.getResource()).thenReturn(resc); accessSurrogatePath = derivativeFolder.resolve("f277bb38-272c-471c-a28a-9887a1328a1f"); filter = new SetContentStatusFilter(); filter.setDerivativeService(derivativeService); @@ -87,7 +94,6 @@ void closeService() throws Exception { @Test public void testDescribedWork() throws Exception { when(dip.getContentObject()).thenReturn(workObj); - when(workObj.getResource()).thenReturn(resc); when(resc.hasProperty(Cdr.hasMods)).thenReturn(true); filter.filter(dip); @@ -100,7 +106,6 @@ public void testDescribedWork() throws Exception { @Test public void testNotDescribedWork() throws Exception { when(dip.getContentObject()).thenReturn(workObj); - when(workObj.getResource()).thenReturn(resc); filter.filter(dip); @@ -112,7 +117,6 @@ public void testNotDescribedWork() throws Exception { @Test public void testWorkNoPrimaryObject() throws Exception { when(dip.getContentObject()).thenReturn(workObj); - when(workObj.getResource()).thenReturn(resc); filter.filter(dip); @@ -134,7 +138,6 @@ public void testFolderNoPrimaryObject() throws Exception { @Test public void testWorkWithPrimaryObject() throws Exception { when(dip.getContentObject()).thenReturn(workObj); - when(workObj.getResource()).thenReturn(resc); when(resc.hasProperty(Cdr.primaryObject)).thenReturn(true); filter.filter(dip); @@ -146,7 +149,6 @@ public void testWorkWithPrimaryObject() throws Exception { @Test public void testIsPrimaryObject() throws Exception { - when(workObj.getResource()).thenReturn(resc); when(resc.hasProperty(Cdr.primaryObject, fileResc)).thenReturn(true); when(dip.getContentObject()).thenReturn(fileObj); @@ -160,8 +162,6 @@ public void testIsPrimaryObject() throws Exception { @Test public void testUnpublishedFileObject() throws Exception { - when(workObj.getResource()).thenReturn(resc); - when(dip.getContentObject()).thenReturn(fileObj); when(fileObj.getResource()).thenReturn(fileResc); when(fileResc.hasProperty(Cdr.unpublished)).thenReturn(true); @@ -174,7 +174,6 @@ public void testUnpublishedFileObject() throws Exception { @Test public void testWorkWithMemberOrder() { when(dip.getContentObject()).thenReturn(workObj); - when(workObj.getResource()).thenReturn(resc); when(resc.hasProperty(Cdr.memberOrder)).thenReturn(true); filter.filter(dip); @@ -187,7 +186,6 @@ public void testWorkWithMemberOrder() { @Test public void testWorkWithoutMemberOrder() { when(dip.getContentObject()).thenReturn(workObj); - when(workObj.getResource()).thenReturn(resc); filter.filter(dip); @@ -199,7 +197,6 @@ public void testWorkWithoutMemberOrder() { @Test public void testWorkWithAssignedThumbnail() throws Exception { when(dip.getContentObject()).thenReturn(workObj); - when(workObj.getResource()).thenReturn(resc); when(resc.hasProperty(Cdr.useAsThumbnail)).thenReturn(true); filter.filter(dip); @@ -212,7 +209,6 @@ public void testWorkWithAssignedThumbnail() throws Exception { @Test public void testWorkNoAssignedThumbnail() throws Exception { when(dip.getContentObject()).thenReturn(workObj); - when(workObj.getResource()).thenReturn(resc); filter.filter(dip); @@ -222,7 +218,6 @@ public void testWorkNoAssignedThumbnail() throws Exception { @Test public void testIsAssignedThumbnail() throws Exception { - when(workObj.getResource()).thenReturn(resc); when(resc.hasProperty(Cdr.useAsThumbnail, fileResc)).thenReturn(true); when(dip.getContentObject()).thenReturn(fileObj); @@ -237,7 +232,6 @@ public void testIsAssignedThumbnail() throws Exception { @Test public void testFileObjectHasAccessSurrogate() throws IOException { Files.write(accessSurrogatePath, List.of("fake image")); - when(workObj.getResource()).thenReturn(resc); when(dip.getContentObject()).thenReturn(fileObj); when(fileObj.getResource()).thenReturn(fileResc); @@ -249,7 +243,6 @@ public void testFileObjectHasAccessSurrogate() throws IOException { @Test public void testFileObjectNoAccessSurrogate() { - when(workObj.getResource()).thenReturn(resc); when(resc.hasProperty(Cdr.primaryObject, fileResc)).thenReturn(true); when(dip.getContentObject()).thenReturn(fileObj); when(fileObj.getResource()).thenReturn(fileResc); @@ -263,9 +256,7 @@ public void testFileObjectNoAccessSurrogate() { @Test public void testWorkAccessSurrogate() throws IOException { Files.write(accessSurrogatePath, List.of("fake image")); - when(workObj.getResource()).thenReturn(resc); when(dip.getContentObject()).thenReturn(workObj); - when(workObj.getResource()).thenReturn(resc); filter.filter(dip); @@ -273,4 +264,100 @@ public void testWorkAccessSurrogate() throws IOException { assertFalse(listCaptor.getValue().contains(FacetConstants.HAS_ACCESS_SURROGATE)); assertFalse(listCaptor.getValue().contains(FacetConstants.NO_ACCESS_SURROGATE)); } + + @Test + public void testFileObjectHasAltText() { + when(dip.getContentObject()).thenReturn(fileObj); + when(fileObj.getResource()).thenReturn(fileResc); + when(fileResc.hasProperty(Cdr.hasAltText)).thenReturn(true); + + filter.filter(dip); + + verify(idb).setContentStatus(listCaptor.capture()); + assertTrue(listCaptor.getValue().contains(FacetConstants.HAS_ALT_TEXT)); + assertFalse(listCaptor.getValue().contains(FacetConstants.NO_ALT_TEXT)); + } + + @Test + public void testFileObjectNoAltText() { + when(dip.getContentObject()).thenReturn(fileObj); + when(fileObj.getResource()).thenReturn(fileResc); + + filter.filter(dip); + + verify(idb).setContentStatus(listCaptor.capture()); + assertTrue(listCaptor.getValue().contains(FacetConstants.NO_ALT_TEXT)); + assertFalse(listCaptor.getValue().contains(FacetConstants.HAS_ALT_TEXT)); + } + + @Test + public void testFileObjectHasStreaming() { + when(dip.getContentObject()).thenReturn(fileObj); + when(fileObj.getResource()).thenReturn(fileResc); + when(fileResc.hasProperty(Cdr.streamingUrl)).thenReturn(true); + + filter.filter(dip); + + verify(idb).setContentStatus(listCaptor.capture()); + assertTrue(listCaptor.getValue().contains(FacetConstants.HAS_STREAMING)); + assertFalse(listCaptor.getValue().contains(FacetConstants.NO_STREAMING)); + } + + @Test + public void testWorkHasViewBehaviorPaged() { + when(dip.getContentObject()).thenReturn(workObj); + when(resc.hasProperty(CdrView.viewBehavior)).thenReturn(true); + when(resc.getProperty(CdrView.viewBehavior)).thenReturn(statement); + when(statement.getString()).thenReturn(ViewSettingRequest.ViewBehavior.PAGED.getString()); + + filter.filter(dip); + + verify(idb).setContentStatus(listCaptor.capture()); + assertTrue(listCaptor.getValue().contains(FacetConstants.VIEW_BEHAVIOR_PAGED)); + assertFalse(listCaptor.getValue().contains(FacetConstants.VIEW_BEHAVIOR_CONTINUOUS)); + assertFalse(listCaptor.getValue().contains(FacetConstants.VIEW_BEHAVIOR_INDIVIDUALS)); + } + + @Test + public void testWorkHasViewBehaviorContinuous() { + when(dip.getContentObject()).thenReturn(workObj); + when(resc.hasProperty(CdrView.viewBehavior)).thenReturn(true); + when(resc.getProperty(CdrView.viewBehavior)).thenReturn(statement); + when(statement.getString()).thenReturn(ViewSettingRequest.ViewBehavior.CONTINUOUS.getString()); + + filter.filter(dip); + + verify(idb).setContentStatus(listCaptor.capture()); + assertTrue(listCaptor.getValue().contains(FacetConstants.VIEW_BEHAVIOR_CONTINUOUS)); + assertFalse(listCaptor.getValue().contains(FacetConstants.VIEW_BEHAVIOR_PAGED)); + assertFalse(listCaptor.getValue().contains(FacetConstants.VIEW_BEHAVIOR_INDIVIDUALS)); + } + + @Test + public void testWorkHasViewBehaviorIndividuals() { + when(dip.getContentObject()).thenReturn(workObj); + when(resc.hasProperty(CdrView.viewBehavior)).thenReturn(true); + when(resc.getProperty(CdrView.viewBehavior)).thenReturn(statement); + when(statement.getString()).thenReturn(ViewSettingRequest.ViewBehavior.INDIVIDUALS.getString()); + + filter.filter(dip); + + verify(idb).setContentStatus(listCaptor.capture()); + assertTrue(listCaptor.getValue().contains(FacetConstants.VIEW_BEHAVIOR_INDIVIDUALS)); + assertFalse(listCaptor.getValue().contains(FacetConstants.VIEW_BEHAVIOR_PAGED)); + assertFalse(listCaptor.getValue().contains(FacetConstants.VIEW_BEHAVIOR_CONTINUOUS)); + } + + @Test + public void testWorkNoViewBehavior() { + when(dip.getContentObject()).thenReturn(workObj); + when(resc.hasProperty(CdrView.viewBehavior)).thenReturn(false); + + filter.filter(dip); + + verify(idb).setContentStatus(listCaptor.capture()); + assertTrue(listCaptor.getValue().contains(FacetConstants.VIEW_BEHAVIOR_INDIVIDUALS)); + assertFalse(listCaptor.getValue().contains(FacetConstants.VIEW_BEHAVIOR_PAGED)); + assertFalse(listCaptor.getValue().contains(FacetConstants.VIEW_BEHAVIOR_CONTINUOUS)); + } } diff --git a/integration/src/test/resources/spring-test/solr-indexing-context.xml b/integration/src/test/resources/spring-test/solr-indexing-context.xml index 2a34e166af..e461b1abca 100644 --- a/integration/src/test/resources/spring-test/solr-indexing-context.xml +++ b/integration/src/test/resources/spring-test/solr-indexing-context.xml @@ -24,6 +24,7 @@ + @@ -191,6 +192,10 @@ + + + + diff --git a/model-api/src/main/java/edu/unc/lib/boxc/model/api/DatastreamType.java b/model-api/src/main/java/edu/unc/lib/boxc/model/api/DatastreamType.java index d7088b81b3..1de7d74342 100644 --- a/model-api/src/main/java/edu/unc/lib/boxc/model/api/DatastreamType.java +++ b/model-api/src/main/java/edu/unc/lib/boxc/model/api/DatastreamType.java @@ -13,15 +13,17 @@ */ public enum DatastreamType { ACCESS_SURROGATE("access_surrogate", "application/octet-stream", null, null, EXTERNAL), + ALT_TEXT("alt_text", "text/plain", "txt", METADATA_CONTAINER, INTERNAL), + ALT_TEXT_HISTORY("alt_text_history", Constants.TEXT_XML, "xml", METADATA_CONTAINER, INTERNAL), AUDIO_ACCESS_COPY("audio", "audio/aac", "m4a", null, EXTERNAL), FULLTEXT_EXTRACTION("fulltext", "text/plain", "txt", null, EXTERNAL), JP2_ACCESS_COPY("jp2", "image/jp2", "jp2", null, EXTERNAL), - MD_DESCRIPTIVE("md_descriptive", "text/xml", "xml", METADATA_CONTAINER, INTERNAL), - MD_DESCRIPTIVE_HISTORY("md_descriptive_history", "text/xml", "xml", METADATA_CONTAINER, INTERNAL), + MD_DESCRIPTIVE("md_descriptive", Constants.TEXT_XML, "xml", METADATA_CONTAINER, INTERNAL), + MD_DESCRIPTIVE_HISTORY("md_descriptive_history", Constants.TEXT_XML, "xml", METADATA_CONTAINER, INTERNAL), MD_EVENTS("event_log", "application/n-triples", "nt", METADATA_CONTAINER, INTERNAL), ORIGINAL_FILE("original_file", null, null, DATA_FILE_FILESET, INTERNAL), - TECHNICAL_METADATA("techmd_fits", "text/xml", "xml", DATA_FILE_FILESET, INTERNAL), - TECHNICAL_METADATA_HISTORY("techmd_fits_history", "text/xml", "xml", DATA_FILE_FILESET, INTERNAL); + TECHNICAL_METADATA("techmd_fits", Constants.TEXT_XML, "xml", DATA_FILE_FILESET, INTERNAL), + TECHNICAL_METADATA_HISTORY("techmd_fits_history", Constants.TEXT_XML, "xml", DATA_FILE_FILESET, INTERNAL); private final String id; private final String mimetype; @@ -29,7 +31,7 @@ public enum DatastreamType { private final String container; private final StoragePolicy storagePolicy; - private DatastreamType(String identifier, String mimetype, String extension, String container, + DatastreamType(String identifier, String mimetype, String extension, String container, StoragePolicy storagePolicy) { this.id = identifier; this.mimetype = mimetype; @@ -94,4 +96,8 @@ public static DatastreamType getByIdentifier(String id) { } return null; } + + private static class Constants { + public static final String TEXT_XML = "text/xml"; + } } diff --git a/model-api/src/main/java/edu/unc/lib/boxc/model/api/rdf/Cdr.java b/model-api/src/main/java/edu/unc/lib/boxc/model/api/rdf/Cdr.java index eda7290e30..326f17cebc 100644 --- a/model-api/src/main/java/edu/unc/lib/boxc/model/api/rdf/Cdr.java +++ b/model-api/src/main/java/edu/unc/lib/boxc/model/api/rdf/Cdr.java @@ -65,6 +65,9 @@ public static String getURI() { /** Relationship indicating the resource containing the event log for this object */ public static final Property hasEvents = createProperty("http://cdr.unc.edu/definitions/model#hasEvents"); + /** Relationship indicating the binary containing the alt text for this object */ + public static final Property hasAltText = createProperty("http://cdr.unc.edu/definitions/model#hasAltText"); + /** The size (e.g., in bytes) of this binary object */ public static final Property hasSize = createProperty( "http://cdr.unc.edu/definitions/model#hasSize" ); diff --git a/model-api/src/test/java/edu/unc/lib/boxc/model/api/DatastreamTypeTest.java b/model-api/src/test/java/edu/unc/lib/boxc/model/api/DatastreamTypeTest.java new file mode 100644 index 0000000000..0a20c9c8e3 --- /dev/null +++ b/model-api/src/test/java/edu/unc/lib/boxc/model/api/DatastreamTypeTest.java @@ -0,0 +1,58 @@ +package edu.unc.lib.boxc.model.api; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * @author bbpennel + */ +public class DatastreamTypeTest { + @Test + void testGetId() { + assertEquals("access_surrogate", DatastreamType.ACCESS_SURROGATE.getId()); + assertEquals("alt_text", DatastreamType.ALT_TEXT.getId()); + } + + @Test + void testGetMimetype() { + assertEquals("application/octet-stream", DatastreamType.ACCESS_SURROGATE.getMimetype()); + assertEquals("text/plain", DatastreamType.ALT_TEXT.getMimetype()); + assertNull(DatastreamType.ORIGINAL_FILE.getMimetype()); + } + + @Test + void testGetExtension() { + assertEquals("txt", DatastreamType.ALT_TEXT.getExtension()); + assertEquals("m4a", DatastreamType.AUDIO_ACCESS_COPY.getExtension()); + assertNull(DatastreamType.ORIGINAL_FILE.getExtension()); + } + + @Test + void testGetContainer() { + assertEquals("md", DatastreamType.ALT_TEXT.getContainer()); + assertEquals("datafs", DatastreamType.ORIGINAL_FILE.getContainer()); + assertNull(DatastreamType.ACCESS_SURROGATE.getContainer()); + } + + @Test + void testGetStoragePolicy() { + assertEquals(StoragePolicy.INTERNAL, DatastreamType.ALT_TEXT.getStoragePolicy()); + assertEquals(StoragePolicy.EXTERNAL, DatastreamType.ACCESS_SURROGATE.getStoragePolicy()); + } + + @Test + void testGetDefaultFilename() { + assertEquals("alt_text.txt", DatastreamType.ALT_TEXT.getDefaultFilename()); + assertEquals("audio.m4a", DatastreamType.AUDIO_ACCESS_COPY.getDefaultFilename()); + assertEquals("original_file.null", DatastreamType.ORIGINAL_FILE.getDefaultFilename()); + } + + @Test + void testGetByIdentifier() { + assertEquals(DatastreamType.ACCESS_SURROGATE, DatastreamType.getByIdentifier("access_surrogate")); + assertEquals(DatastreamType.ALT_TEXT, DatastreamType.getByIdentifier("alt_text")); + assertNull(DatastreamType.getByIdentifier("non_existent_id")); + } +} diff --git a/model-fcrepo/src/main/java/edu/unc/lib/boxc/model/fcrepo/ids/DatastreamPids.java b/model-fcrepo/src/main/java/edu/unc/lib/boxc/model/fcrepo/ids/DatastreamPids.java index 22b7506534..2769b541e4 100644 --- a/model-fcrepo/src/main/java/edu/unc/lib/boxc/model/fcrepo/ids/DatastreamPids.java +++ b/model-fcrepo/src/main/java/edu/unc/lib/boxc/model/fcrepo/ids/DatastreamPids.java @@ -61,6 +61,10 @@ public static PID getTechnicalMetadataPid(PID pid) { return constructPid(pid, TECHNICAL_METADATA); } + public static PID getAltTextPid(PID pid) { + return constructPid(pid, DatastreamType.ALT_TEXT); + } + /** * Construct a PID for a deposit manifest datastream using the provided name. * diff --git a/model-fcrepo/src/test/java/edu/unc/lib/boxc/model/fcrepo/ids/DatastreamPidsTest.java b/model-fcrepo/src/test/java/edu/unc/lib/boxc/model/fcrepo/ids/DatastreamPidsTest.java new file mode 100644 index 0000000000..e9149d156c --- /dev/null +++ b/model-fcrepo/src/test/java/edu/unc/lib/boxc/model/fcrepo/ids/DatastreamPidsTest.java @@ -0,0 +1,95 @@ +package edu.unc.lib.boxc.model.fcrepo.ids; + +import edu.unc.lib.boxc.model.api.DatastreamType; +import edu.unc.lib.boxc.model.api.ids.PID; +import edu.unc.lib.boxc.model.fcrepo.test.TestHelper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @author bbpennel + */ +public class DatastreamPidsTest { + private PID pid; + + @BeforeEach + void setUp() { + pid = TestHelper.makePid(); + } + + @Test + void testGetDatastreamPid_withMdDescriptiveHistory() { + PID result = DatastreamPids.getDatastreamPid(pid, DatastreamType.MD_DESCRIPTIVE_HISTORY); + assertEquals(pid.getId() + "/md/md_descriptive_history", result.getComponentId()); + } + + @Test + void testGetDatastreamPid_withTechnicalMetadataHistory() { + PID result = DatastreamPids.getDatastreamPid(pid, DatastreamType.TECHNICAL_METADATA_HISTORY); + assertEquals(pid.getId() + "/datafs/techmd_fits_history", result.getComponentId()); + } + + @Test + void testGetDatastreamPid_withOtherType() { + PID result = DatastreamPids.getDatastreamPid(pid, DatastreamType.MD_DESCRIPTIVE); + assertEquals(pid.getId() + "/md/md_descriptive", result.getComponentId()); + } + + @Test + void testGetMdDescriptivePid() { + PID result = DatastreamPids.getMdDescriptivePid(pid); + assertEquals(pid.getId() + "/md/md_descriptive", result.getComponentId()); + } + + @Test + void testGetOriginalFilePid() { + PID result = DatastreamPids.getOriginalFilePid(pid); + assertEquals(pid.getId() + "/datafs/original_file", result.getComponentId()); + } + + @Test + void testGetDepositManifestPid() { + String manifestName = "manifest_name"; + PID result = DatastreamPids.getDepositManifestPid(pid, manifestName); + + assertEquals(pid.getId() + "/manifest/manifest_name", result.getComponentId()); + } + + @Test + void testGetDatastreamHistoryPid() { + PID mdPid = DatastreamPids.getMdDescriptivePid(pid); + PID result = DatastreamPids.getDatastreamHistoryPid(mdPid); + + assertEquals(pid.getId() + "/md/md_descriptive_history", result.getComponentId()); + } + + @Test + void testGetAltTextPid() { + PID result = DatastreamPids.getAltTextPid(pid); + assertEquals(pid.getId() + "/md/alt_text", result.getComponentId()); + PID history = DatastreamPids.getDatastreamHistoryPid(result); + assertEquals(pid.getId() + "/md/alt_text_history", history.getComponentId()); + } + + @Test + void testGetTechnicalMetadataPid() { + PID result = DatastreamPids.getTechnicalMetadataPid(pid); + assertEquals(pid.getId() + "/datafs/techmd_fits", result.getComponentId()); + PID history = DatastreamPids.getDatastreamHistoryPid(result); + assertEquals(pid.getId() + "/datafs/techmd_fits_history", history.getComponentId()); + } + + @Test + void testGetMdEventsPid() { + PID result = DatastreamPids.getMdEventsPid(pid); + assertEquals(pid.getId() + "/md/event_log", result.getComponentId()); + } + + @Test + void testAccessSurrogatePid() { + PID result = DatastreamPids.getAccessSurrogatePid(pid); + assertEquals(pid.getId() + "/datafs/access_surrogate", result.getComponentId()); + } +} diff --git a/operations-jms/src/main/java/edu/unc/lib/boxc/operations/jms/altText/AltTextUpdateRequest.java b/operations-jms/src/main/java/edu/unc/lib/boxc/operations/jms/altText/AltTextUpdateRequest.java new file mode 100644 index 0000000000..5e0125f764 --- /dev/null +++ b/operations-jms/src/main/java/edu/unc/lib/boxc/operations/jms/altText/AltTextUpdateRequest.java @@ -0,0 +1,51 @@ +package edu.unc.lib.boxc.operations.jms.altText; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import edu.unc.lib.boxc.auth.api.models.AgentPrincipals; +import edu.unc.lib.boxc.auth.fcrepo.models.AgentPrincipalsImpl; +import edu.unc.lib.boxc.persist.api.transfer.BinaryTransferSession; + +/** + * Request to update the alt text of a file object + * + * @author bbpennel + */ +public class AltTextUpdateRequest { + private String pidString; + @JsonDeserialize(as = AgentPrincipalsImpl.class) + private AgentPrincipals agent; + private String altText; + private BinaryTransferSession transferSession; + + public String getPidString() { + return pidString; + } + + public void setPidString(String pidString) { + this.pidString = pidString; + } + + public AgentPrincipals getAgent() { + return agent; + } + + public void setAgent(AgentPrincipals agent) { + this.agent = agent; + } + + public String getAltText() { + return altText; + } + + public void setAltText(String altText) { + this.altText = altText; + } + + public BinaryTransferSession getTransferSession() { + return transferSession; + } + + public void setTransferSession(BinaryTransferSession transferSession) { + this.transferSession = transferSession; + } +} diff --git a/operations/src/main/java/edu/unc/lib/boxc/operations/impl/altText/AltTextUpdateService.java b/operations/src/main/java/edu/unc/lib/boxc/operations/impl/altText/AltTextUpdateService.java new file mode 100644 index 0000000000..5b60efed63 --- /dev/null +++ b/operations/src/main/java/edu/unc/lib/boxc/operations/impl/altText/AltTextUpdateService.java @@ -0,0 +1,94 @@ +package edu.unc.lib.boxc.operations.impl.altText; + +import edu.unc.lib.boxc.auth.api.Permission; +import edu.unc.lib.boxc.auth.api.services.AccessControlService; +import edu.unc.lib.boxc.model.api.objects.BinaryObject; +import edu.unc.lib.boxc.model.api.objects.RepositoryObjectLoader; +import edu.unc.lib.boxc.model.api.rdf.Cdr; +import edu.unc.lib.boxc.model.api.services.RepositoryObjectFactory; +import edu.unc.lib.boxc.model.fcrepo.ids.DatastreamPids; +import edu.unc.lib.boxc.model.fcrepo.ids.PIDs; +import edu.unc.lib.boxc.operations.impl.versioning.VersionedDatastreamService; +import edu.unc.lib.boxc.operations.jms.OperationsMessageSender; +import edu.unc.lib.boxc.operations.jms.altText.AltTextUpdateRequest; +import edu.unc.lib.boxc.operations.jms.indexing.IndexingPriority; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.Collections; + +import static edu.unc.lib.boxc.model.api.DatastreamType.ALT_TEXT; +import static org.apache.jena.rdf.model.ResourceFactory.createResource; + +/** + * Service for updating the alt text of a file object + * + * @author bbpennel + */ +public class AltTextUpdateService { + private static final Logger log = LoggerFactory.getLogger(AltTextUpdateService.class); + + private AccessControlService aclService; + private RepositoryObjectLoader repositoryObjectLoader; + private RepositoryObjectFactory repositoryObjectFactory; + private VersionedDatastreamService versionedDatastreamService; + private OperationsMessageSender operationsMessageSender; + private boolean sendsMessages; + + public BinaryObject updateAltText(AltTextUpdateRequest request) { + var pid = PIDs.get(request.getPidString()); + aclService.assertHasAccess("User does not have permission to update alt text", + pid, request.getAgent().getPrincipals(), Permission.editDescription); + + var fileObj = repositoryObjectLoader.getFileObject(pid); + var altTextPid = DatastreamPids.getAltTextPid(pid); + + BinaryObject altTextBinary; + var newVersion = new VersionedDatastreamService.DatastreamVersion(altTextPid); + newVersion.setContentStream(new ByteArrayInputStream(request.getAltText().getBytes(StandardCharsets.UTF_8))); + newVersion.setContentType(ALT_TEXT.getMimetype()); + newVersion.setFilename(ALT_TEXT.getDefaultFilename()); + newVersion.setTransferSession(request.getTransferSession()); + + altTextBinary = versionedDatastreamService.addVersion(newVersion); + if (repositoryObjectFactory.objectExists(altTextPid.getRepositoryUri())) { + log.debug("Successfully updated alt text for {}", fileObj.getPid()); + } else { + repositoryObjectFactory.createRelationship(fileObj, Cdr.hasAltText, createResource(altTextPid.getRepositoryPath())); + log.debug("Successfully add new alt text for {}", fileObj.getPid()); + } + + if (sendsMessages) { + operationsMessageSender.sendUpdateDescriptionOperation( + request.getAgent().getUsername(), Collections.singletonList(fileObj.getPid()), IndexingPriority.normal); + } + + return altTextBinary; + } + + public void setAclService(AccessControlService aclService) { + this.aclService = aclService; + } + + public void setRepositoryObjectLoader(RepositoryObjectLoader repositoryObjectLoader) { + this.repositoryObjectLoader = repositoryObjectLoader; + } + + public void setRepositoryObjectFactory(RepositoryObjectFactory repositoryObjectFactory) { + this.repositoryObjectFactory = repositoryObjectFactory; + } + + public void setVersionedDatastreamService(VersionedDatastreamService versionedDatastreamService) { + this.versionedDatastreamService = versionedDatastreamService; + } + + public void setOperationsMessageSender(OperationsMessageSender operationsMessageSender) { + this.operationsMessageSender = operationsMessageSender; + } + + public void setSendsMessages(boolean sendsMessages) { + this.sendsMessages = sendsMessages; + } +} diff --git a/operations/src/test/java/edu/unc/lib/boxc/operations/impl/altText/AltTextUpdateServiceTest.java b/operations/src/test/java/edu/unc/lib/boxc/operations/impl/altText/AltTextUpdateServiceTest.java new file mode 100644 index 0000000000..c6811dffb7 --- /dev/null +++ b/operations/src/test/java/edu/unc/lib/boxc/operations/impl/altText/AltTextUpdateServiceTest.java @@ -0,0 +1,198 @@ +package edu.unc.lib.boxc.operations.impl.altText; + +import edu.unc.lib.boxc.auth.api.Permission; +import edu.unc.lib.boxc.auth.api.exceptions.AccessRestrictionException; +import edu.unc.lib.boxc.auth.api.models.AccessGroupSet; +import edu.unc.lib.boxc.auth.api.models.AgentPrincipals; +import edu.unc.lib.boxc.auth.api.services.AccessControlService; +import edu.unc.lib.boxc.auth.fcrepo.models.AccessGroupSetImpl; +import edu.unc.lib.boxc.auth.fcrepo.models.AgentPrincipalsImpl; +import edu.unc.lib.boxc.model.api.ids.PID; +import edu.unc.lib.boxc.model.api.objects.BinaryObject; +import edu.unc.lib.boxc.model.api.objects.FileObject; +import edu.unc.lib.boxc.model.api.objects.RepositoryObjectLoader; +import edu.unc.lib.boxc.model.api.rdf.Cdr; +import edu.unc.lib.boxc.model.api.services.RepositoryObjectFactory; +import edu.unc.lib.boxc.model.fcrepo.ids.DatastreamPids; +import edu.unc.lib.boxc.model.fcrepo.test.TestHelper; +import edu.unc.lib.boxc.operations.impl.versioning.VersionedDatastreamService; +import edu.unc.lib.boxc.operations.jms.OperationsMessageSender; +import edu.unc.lib.boxc.operations.jms.altText.AltTextUpdateRequest; +import edu.unc.lib.boxc.operations.jms.indexing.IndexingPriority; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.nio.charset.StandardCharsets; + +import static edu.unc.lib.boxc.model.api.DatastreamType.ALT_TEXT; +import static org.apache.jena.rdf.model.ResourceFactory.createResource; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +/** + * @author bbpennel + */ +public class AltTextUpdateServiceTest { + + private AltTextUpdateService service; + + @Mock + private AccessControlService aclService; + @Mock + private RepositoryObjectLoader repositoryObjectLoader; + @Mock + private RepositoryObjectFactory repositoryObjectFactory; + @Mock + private VersionedDatastreamService versioningService; + @Mock + private OperationsMessageSender operationsMessageSender; + @Mock + private BinaryObject binaryObject; + @Mock + private FileObject fileObject; + private PID pid; + private String pidString; + private PID altTextPid; + private AgentPrincipals agent; + private static final AccessGroupSet ACCESS_GROUPS = new AccessGroupSetImpl("test_group"); + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + service = new AltTextUpdateService(); + service.setAclService(aclService); + service.setRepositoryObjectLoader(repositoryObjectLoader); + service.setRepositoryObjectFactory(repositoryObjectFactory); + service.setVersionedDatastreamService(versioningService); + service.setOperationsMessageSender(operationsMessageSender); + service.setSendsMessages(true); + pid = TestHelper.makePid(); + pidString = pid.getId(); + altTextPid = DatastreamPids.getAltTextPid(pid); + agent = new AgentPrincipalsImpl("test_user", ACCESS_GROUPS); + } + + @Test + void testUpdateAltTextCreatesNewAltText() throws Exception { + var altTextContent = "Sample Alt Text"; + var request = new AltTextUpdateRequest(); + request.setAltText(altTextContent); + request.setPidString(pidString); + request.setAgent(agent); + + when(repositoryObjectLoader.getFileObject(eq(pid))).thenReturn(fileObject); + when(repositoryObjectFactory.objectExists(altTextPid.getRepositoryUri())).thenReturn(false); + when(versioningService.addVersion(any())).thenReturn(binaryObject); + + var result = service.updateAltText(request); + + // Verify ACL check + verify(aclService).assertHasAccess(anyString(), eq(pid), any(), eq(Permission.editDescription)); + + // Verify alt text creation + ArgumentCaptor captor = ArgumentCaptor.forClass(VersionedDatastreamService.DatastreamVersion.class); + verify(versioningService).addVersion(captor.capture()); + var capturedVersion = captor.getValue(); + assertEquals(ALT_TEXT.getMimetype(), capturedVersion.getContentType()); + assertEquals(ALT_TEXT.getDefaultFilename(), capturedVersion.getFilename()); + assertEquals(altTextContent, IOUtils.toString(capturedVersion.getContentStream(), StandardCharsets.UTF_8)); + + // Verify relationship creation + var expectedRelationObj = createResource(altTextPid.getRepositoryPath()); + verify(repositoryObjectFactory).createRelationship(eq(fileObject), eq(Cdr.hasAltText), eq(expectedRelationObj)); + + // Verify operation message + verify(operationsMessageSender).sendUpdateDescriptionOperation(eq("test_user"), anyList(), eq(IndexingPriority.normal)); + + assertEquals(binaryObject, result); + } + + @Test + void testUpdateAltTextUpdatesExistingAltText() throws Exception { + var altTextContent = "Updated Alt Text"; + var request = new AltTextUpdateRequest(); + request.setAltText(altTextContent); + request.setPidString(pidString); + request.setAgent(agent); + + when(repositoryObjectLoader.getFileObject(any())).thenReturn(fileObject); + when(repositoryObjectFactory.objectExists(altTextPid.getRepositoryUri())).thenReturn(true); + when(versioningService.addVersion(any())).thenReturn(binaryObject); + + var result = service.updateAltText(request); + + // Verify ACL check + verify(aclService).assertHasAccess(anyString(), eq(pid), any(), eq(Permission.editDescription)); + + // Verify alt text update + ArgumentCaptor captor = ArgumentCaptor.forClass(VersionedDatastreamService.DatastreamVersion.class); + verify(versioningService).addVersion(captor.capture()); + var capturedVersion = captor.getValue(); + assertEquals(ALT_TEXT.getMimetype(), capturedVersion.getContentType()); + assertEquals(ALT_TEXT.getDefaultFilename(), capturedVersion.getFilename()); + assertEquals(altTextContent, IOUtils.toString(capturedVersion.getContentStream(), StandardCharsets.UTF_8)); + + // Verify relationship creation is NOT called + verify(repositoryObjectFactory, never()).createRelationship(any(), any(), any()); + + // Verify operation message + verify(operationsMessageSender).sendUpdateDescriptionOperation(eq("test_user"), anyList(), eq(IndexingPriority.normal)); + + assertEquals(binaryObject, result); + } + + @Test + void testUpdateAltTextFailsACLCheck() { + var altTextContent = "Sample Alt Text"; + var request = new AltTextUpdateRequest(); + request.setAltText(altTextContent); + request.setPidString(pidString); + request.setAgent(agent); + + doThrow(new AccessRestrictionException("Access Denied")).when(aclService) + .assertHasAccess(any(), eq(pid), any(), eq(Permission.editDescription)); + + assertThrows(AccessRestrictionException.class, () -> service.updateAltText(request)); + + verifyNoInteractions(repositoryObjectFactory, versioningService, operationsMessageSender); + } + + @Test + void testUpdateAltTextDoesNotSendMessageWhenTurnedOff() throws Exception { + service.setSendsMessages(false); + + var altTextContent = "Sample Alt Text"; + var request = new AltTextUpdateRequest(); + request.setAltText(altTextContent); + request.setPidString(pidString); + request.setAgent(agent); + + when(repositoryObjectLoader.getFileObject(eq(pid))).thenReturn(fileObject); + when(repositoryObjectFactory.objectExists(altTextPid.getRepositoryUri())).thenReturn(false); + when(versioningService.addVersion(any())).thenReturn(binaryObject); + + var result = service.updateAltText(request); + + // Verify alt text creation + ArgumentCaptor captor = ArgumentCaptor.forClass(VersionedDatastreamService.DatastreamVersion.class); + verify(versioningService).addVersion(captor.capture()); + var capturedVersion = captor.getValue(); + assertEquals(altTextContent, IOUtils.toString(capturedVersion.getContentStream(), StandardCharsets.UTF_8)); + + // Verify operation message + verify(operationsMessageSender, never()).sendUpdateDescriptionOperation(any(), anyList(), any()); + } +} diff --git a/search-api/src/main/java/edu/unc/lib/boxc/search/api/FacetConstants.java b/search-api/src/main/java/edu/unc/lib/boxc/search/api/FacetConstants.java index 6189b26d54..b596b7126a 100644 --- a/search-api/src/main/java/edu/unc/lib/boxc/search/api/FacetConstants.java +++ b/search-api/src/main/java/edu/unc/lib/boxc/search/api/FacetConstants.java @@ -33,4 +33,6 @@ public abstract class FacetConstants { public static final String NO_ACCESS_SURROGATE = "No Access Surrogate"; public static final String HAS_STREAMING = "Has Streaming"; public static final String NO_STREAMING = "No Streaming"; + public static final String HAS_ALT_TEXT = "Has Alt Text"; + public static final String NO_ALT_TEXT = "No Alt Text"; } diff --git a/search-api/src/main/java/edu/unc/lib/boxc/search/api/SearchFieldKey.java b/search-api/src/main/java/edu/unc/lib/boxc/search/api/SearchFieldKey.java index 80cc675953..8efbadbf4e 100644 --- a/search-api/src/main/java/edu/unc/lib/boxc/search/api/SearchFieldKey.java +++ b/search-api/src/main/java/edu/unc/lib/boxc/search/api/SearchFieldKey.java @@ -14,6 +14,7 @@ public enum SearchFieldKey { ABSTRACT("abstract", "abstract", "Abstract"), ADMIN_GROUP("adminGroup", "adminGroup", "Admin Group"), + ALT_TEXT("altText", "altText", "Alt Text"), ANCESTOR_IDS("ancestorIds", "ancestorIds", "Ancestor Ids"), ANCESTOR_PATH("ancestorPath", "path", "Folders"), CITATION("citation", "citation", "Citation"), diff --git a/search-api/src/main/java/edu/unc/lib/boxc/search/api/models/ContentObjectRecord.java b/search-api/src/main/java/edu/unc/lib/boxc/search/api/models/ContentObjectRecord.java index f31b7944f0..e22aa4b0eb 100644 --- a/search-api/src/main/java/edu/unc/lib/boxc/search/api/models/ContentObjectRecord.java +++ b/search-api/src/main/java/edu/unc/lib/boxc/search/api/models/ContentObjectRecord.java @@ -146,4 +146,11 @@ public interface ContentObjectRecord { void setThumbnailId(String id); String getViewBehavior(); + + /** + * @return The alt text for this object, if one is present. + */ + String getAltText(); + + void setAltText(String altText); } diff --git a/search-solr/src/main/java/edu/unc/lib/boxc/search/solr/models/GroupedContentObjectSolrRecord.java b/search-solr/src/main/java/edu/unc/lib/boxc/search/solr/models/GroupedContentObjectSolrRecord.java index a84843e727..34bbcc137d 100644 --- a/search-solr/src/main/java/edu/unc/lib/boxc/search/solr/models/GroupedContentObjectSolrRecord.java +++ b/search-solr/src/main/java/edu/unc/lib/boxc/search/solr/models/GroupedContentObjectSolrRecord.java @@ -386,4 +386,14 @@ public void setThumbnailId(String id) { public String getViewBehavior() { return representative.getViewBehavior(); } + + @Override + public String getAltText() { + return representative.getAltText(); + } + + @Override + public void setAltText(String altText) { + representative.setAltText(altText); + } } diff --git a/search-solr/src/main/java/edu/unc/lib/boxc/search/solr/models/IndexDocumentBean.java b/search-solr/src/main/java/edu/unc/lib/boxc/search/solr/models/IndexDocumentBean.java index badd77f089..e801efb799 100644 --- a/search-solr/src/main/java/edu/unc/lib/boxc/search/solr/models/IndexDocumentBean.java +++ b/search-solr/src/main/java/edu/unc/lib/boxc/search/solr/models/IndexDocumentBean.java @@ -575,4 +575,13 @@ public void setStreamingUrl(String streamingUrl) { public Map getFields() { return fields; } + + public String getAltText() { + return (String) fields.get(SearchFieldKey.ALT_TEXT.getSolrField()); + } + + @Field + public void setAltText(String altText) { + fields.put(SearchFieldKey.ALT_TEXT.getSolrField(), altText); + } } diff --git a/search-solr/src/main/java/edu/unc/lib/boxc/search/solr/services/AbstractQueryService.java b/search-solr/src/main/java/edu/unc/lib/boxc/search/solr/services/AbstractQueryService.java index a27669736c..0e22ec185f 100644 --- a/search-solr/src/main/java/edu/unc/lib/boxc/search/solr/services/AbstractQueryService.java +++ b/search-solr/src/main/java/edu/unc/lib/boxc/search/solr/services/AbstractQueryService.java @@ -32,6 +32,7 @@ public abstract class AbstractQueryService { private final static List DEFAULT_RESULT_FIELDS = Arrays.asList( SearchFieldKey.ABSTRACT.name(), SearchFieldKey.ADMIN_GROUP.name(), + SearchFieldKey.ALT_TEXT.name(), SearchFieldKey.ANCESTOR_IDS.name(), SearchFieldKey.ANCESTOR_PATH.name(), SearchFieldKey.CITATION.name(), diff --git a/services-camel-app/src/main/webapp/WEB-INF/solr-indexing-context.xml b/services-camel-app/src/main/webapp/WEB-INF/solr-indexing-context.xml index bb3c0d71b7..80f7d959d9 100644 --- a/services-camel-app/src/main/webapp/WEB-INF/solr-indexing-context.xml +++ b/services-camel-app/src/main/webapp/WEB-INF/solr-indexing-context.xml @@ -22,6 +22,7 @@ + @@ -64,6 +65,7 @@ + @@ -286,6 +288,10 @@ class="edu.unc.lib.boxc.indexing.solr.filter.SetDescriptiveMetadataFilter"> + + + + diff --git a/services-camel-app/src/test/resources/solr-indexing-it-context.xml b/services-camel-app/src/test/resources/solr-indexing-it-context.xml index 7b9d85d56d..e6a1c8caa4 100644 --- a/services-camel-app/src/test/resources/solr-indexing-it-context.xml +++ b/services-camel-app/src/test/resources/solr-indexing-it-context.xml @@ -52,6 +52,7 @@ + diff --git a/services-camel-app/src/test/resources/spring-test/solr-indexing-context.xml b/services-camel-app/src/test/resources/spring-test/solr-indexing-context.xml index 266a4daf25..00b32f0699 100644 --- a/services-camel-app/src/test/resources/spring-test/solr-indexing-context.xml +++ b/services-camel-app/src/test/resources/spring-test/solr-indexing-context.xml @@ -24,6 +24,7 @@ + @@ -220,6 +221,10 @@ + + + + diff --git a/static/js/admin/src/EditAltTextForm.js b/static/js/admin/src/EditAltTextForm.js new file mode 100644 index 0000000000..bbc99be22f --- /dev/null +++ b/static/js/admin/src/EditAltTextForm.js @@ -0,0 +1,51 @@ +define('EditAltTextForm', [ 'jquery', 'jquery-ui', 'underscore', 'RemoteStateChangeMonitor', 'tpl!../templates/admin/editAltTextForm', + 'ModalLoadingOverlay', 'AbstractForm', 'AlertHandler'], + function($, ui, _, RemoteStateChangeMonitor, editAltTextForm, ModalLoadingOverlay, AbstractForm) { + + var defaultOptions = { + title : 'Edit Alt Text', + createFormTemplate : editAltTextForm, + submitMethod: 'POST' + }; + + function EditAltTextForm(options) { + this.options = $.extend({}, defaultOptions, options); + } + + EditAltTextForm.prototype.constructor = EditAltTextForm; + EditAltTextForm.prototype = Object.create( AbstractForm.prototype ); + + EditAltTextForm.prototype.open = function(resultObject) { + AbstractForm.prototype.open.call(this, resultObject); + }; + + EditAltTextForm.prototype.preprocessForm = function(resultObject) { + var pid = resultObject.metadata.id; + this.action_url = "/services/api/edit/altText/" + pid; + }; + + EditAltTextForm.prototype.validationErrors = function() { + return []; + }; + + EditAltTextForm.prototype.getSuccessMessage = function(data) { + return "Alt text has been successfully updated."; + }; + + EditAltTextForm.prototype.getErrorMessage = function(data) { + return "An error occurred while updating the alt text"; + }; + + EditAltTextForm.prototype.remove = function() { + AbstractForm.prototype.remove.apply(this); + if (this.submitSuccessful) { + this.options.actionHandler.addEvent({ + action : 'RefreshResult', + target : this.resultObject, + waitForUpdate : true + }); + } + }; + + return EditAltTextForm; + }); \ No newline at end of file diff --git a/static/js/admin/src/ResultObjectActionMenu.js b/static/js/admin/src/ResultObjectActionMenu.js index 70b4bf89f7..f10026225c 100644 --- a/static/js/admin/src/ResultObjectActionMenu.js +++ b/static/js/admin/src/ResultObjectActionMenu.js @@ -1,7 +1,8 @@ define('ResultObjectActionMenu', [ 'jquery', 'jquery-ui', 'StringUtilities', 'AddFileForm', 'EditAccessSurrogateForm', 'EditThumbnailForm', - 'EditFilenameForm', 'EditTitleForm', 'DeleteForm', 'IngestFromSourceForm', 'ViewSettingsForm', 'EditStreamingPropertiesForm', 'contextMenu'], - function($, ui, StringUtilities, AddFileForm, EditAccessSurrogateForm, EditThumbnailForm, EditFilenameForm, EditTitleForm, DeleteForm, IngestFromSourceForm, ViewSettingsForm, EditStreamingPropertiesForm) { + 'EditFilenameForm', 'EditTitleForm', 'DeleteForm', 'IngestFromSourceForm', 'ViewSettingsForm', 'EditStreamingPropertiesForm', + 'EditAltTextForm', 'contextMenu'], + function($, ui, StringUtilities, AddFileForm, EditAccessSurrogateForm, EditThumbnailForm, EditFilenameForm, EditTitleForm, DeleteForm, IngestFromSourceForm, ViewSettingsForm, EditStreamingPropertiesForm, EditAltTextForm) { var defaultOptions = { selector : undefined, @@ -138,6 +139,14 @@ define('ResultObjectActionMenu', [ 'jquery', 'jquery-ui', 'StringUtilities', 'A if (/techmd_fits_history/ig.test(datastreams)) { items['metadata']['items']["viewFitsHistory"] = {name: "View FITS History"} } + + if (/alt_text/ig.test(datastreams)) { + items['metadata']['items']["viewAltText"] = {name: "View Alt Text"}; + } + + if (/alt_text_history/ig.test(datastreams)) { + items['metadata']['items']["viewAltTextHistory"] = {name: "View Alt Text History"}; + } } items['metadata']['items']["viewEventLog"] = {name : "View Event Log"}; @@ -178,13 +187,6 @@ define('ResultObjectActionMenu', [ 'jquery', 'jquery-ui', 'StringUtilities', 'A items["editTitle"] = {name : 'Edit Title'}; } - - /* Evaluating if retaining feature - if ($.inArray('changePatronAccess', metadata.permissions) != -1 - && $.inArray('info:fedora/cdr-model:Collection', metadata.model) != -1) { - items["editCollectionSettings"] = {name : 'Edit Collection Settings'}; - } - */ if (!isContentRoot && $.inArray('editDescription', metadata.permissions) != -1) { items["editDescription"] = {name : 'Edit Description'}; @@ -194,6 +196,10 @@ define('ResultObjectActionMenu', [ 'jquery', 'jquery-ui', 'StringUtilities', 'A items["editThumbnail"] = {name : 'Edit Display Thumbnail'}; } + if (metadata.type === 'File' && $.inArray('editDescription', metadata.permissions) != -1) { + items["editAltText"] = {name : 'Edit Alt Text'}; + } + // Add files to work objects if (!isContentRoot && metadata.type === 'Work' && $.inArray('ingest', metadata.permissions) != -1) { items["addFile"] = {name : 'Add File'}; @@ -319,6 +325,22 @@ define('ResultObjectActionMenu', [ 'jquery', 'jquery-ui', 'StringUtilities', 'A application: "services" }); break; + case "viewAltText": + self.actionHandler.addEvent({ + action: "ChangeLocation", + url: "api/file/" + metadata.id + "/alt_text", + newWindow: true, + application: "services" + }); + break; + case "viewAltTextHistory": + self.actionHandler.addEvent({ + action: "ChangeLocation", + url: "api/file/" + metadata.id + "/alt_text_history", + newWindow: true, + application: "services" + }); + break; case "viewEventLog" : self.actionHandler.addEvent({ action : 'ChangeLocation', @@ -356,6 +378,9 @@ define('ResultObjectActionMenu', [ 'jquery', 'jquery-ui', 'StringUtilities', 'A case "editTitle" : self.editTitle(resultObject); break; + case "editAltText" : + self.editAltText(resultObject); + break; case "editType" : self.actionHandler.addEvent({ action : 'EditTypeBatch', @@ -580,7 +605,14 @@ define('ResultObjectActionMenu', [ 'jquery', 'jquery-ui', 'StringUtilities', 'A actionHandler : this.actionHandler }); editTitleForm.open(resultObject); + }; + ResultObjectActionMenu.prototype.editAltText = function(resultObject) { + var editAltTextForm = new EditAltTextForm({ + alertHandler : this.options.alertHandler, + actionHandler : this.actionHandler + }); + editAltTextForm.open(resultObject); }; ResultObjectActionMenu.prototype.editThumbnail = function(resultObject) { diff --git a/static/js/vue-cdr-access/src/components/full_record/thumbnail.vue b/static/js/vue-cdr-access/src/components/full_record/thumbnail.vue index e3b6fa74b8..d0e6d936fd 100644 --- a/static/js/vue-cdr-access/src/components/full_record/thumbnail.vue +++ b/static/js/vue-cdr-access/src/components/full_record/thumbnail.vue @@ -1,7 +1,7 @@