From: Michael Ritter Date: Thu, 5 Dec 2024 22:05:45 +0000 (-0700) Subject: DRYD-1399: RestrictedMedia (#428) X-Git-Url: https://git.aero2k.de/?a=commitdiff_plain;h=679640d9e0adfe9464a448db0fbc80632d2d5c57;p=tmp%2Fjakarta-migration.git DRYD-1399: RestrictedMedia (#428) --- diff --git a/services/JaxRsServiceProvider/pom.xml b/services/JaxRsServiceProvider/pom.xml index 6c9de17cc..89eb64735 100644 --- a/services/JaxRsServiceProvider/pom.xml +++ b/services/JaxRsServiceProvider/pom.xml @@ -461,7 +461,11 @@ org.collectionspace.services.exit.service ${project.version} - + + org.collectionspace.services + org.collectionspace.services.restrictedmedia.service + ${project.version} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/services/restrictedmedia/client/pom.xml b/services/restrictedmedia/client/pom.xml new file mode 100644 index 000000000..1a3623c7f --- /dev/null +++ b/services/restrictedmedia/client/pom.xml @@ -0,0 +1,70 @@ + + + + org.collectionspace.services + org.collectionspace.services.restrictedmedia + ${revision} + + + 4.0.0 + org.collectionspace.services.restrictedmedia.client + services.restrictedmedia.client + + + + + org.collectionspace.services + org.collectionspace.services.jaxb + ${project.version} + + + org.collectionspace.services + org.collectionspace.services.blob.client + ${project.version} + + + org.collectionspace.services + org.collectionspace.services.client + ${project.version} + + + org.collectionspace.services + org.collectionspace.services.restrictedmedia.jaxb + ${project.version} + + + + + org.testng + testng + + + org.jboss.resteasy + resteasy-jaxrs + + + tjws + webserver + + + + + org.jboss.resteasy + resteasy-jaxb-provider + + + org.jboss.resteasy + resteasy-multipart-provider + + + commons-httpclient + commons-httpclient + + + + + collectionspace-services-restrictedmedia-client + + \ No newline at end of file diff --git a/services/restrictedmedia/client/src/main/java/org/collectionspace/services/client/RestrictedMediaClient.java b/services/restrictedmedia/client/src/main/java/org/collectionspace/services/client/RestrictedMediaClient.java new file mode 100644 index 000000000..d89ba7e02 --- /dev/null +++ b/services/restrictedmedia/client/src/main/java/org/collectionspace/services/client/RestrictedMediaClient.java @@ -0,0 +1,94 @@ +/* + * This document is a part of the source code and related artifacts + * for CollectionSpace, an open source collections management system + * for museums and related institutions: + * + * http://www.collectionspace.org + * http://wiki.collectionspace.org + * + * Licensed under the Educational Community License (ECL), Version 2.0. + * You may not use this file except in compliance with this License. + * + * You may obtain a copy of the ECL 2.0 License at + * https://source.collectionspace.org/collection-space/LICENSE.txt + */ +package org.collectionspace.services.client; + +import javax.ws.rs.core.Response; +import org.collectionspace.services.restrictedmedia.RestrictedMediaCommon; +import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataOutput; + +/** + * RestrictedMediaClient.java + */ +public class RestrictedMediaClient + extends AbstractCommonListPoxServiceClientImpl { + + public static final String SERVICE_NAME = "restrictedmedia"; + public static final String SERVICE_PATH_COMPONENT = SERVICE_NAME; + public static final String SERVICE_PATH = "/" + SERVICE_PATH_COMPONENT; + public static final String SERVICE_PATH_PROXY = SERVICE_PATH + "/"; + public static final String SERVICE_PAYLOAD_NAME = SERVICE_NAME; + + public RestrictedMediaClient() throws Exception { + super(); + } + + public RestrictedMediaClient(String clientPropertiesFilename) throws Exception { + super(clientPropertiesFilename); + } + + @Override + public String getServicePathComponent() { + return SERVICE_PATH_COMPONENT; + } + + @Override + public String getServiceName() { + return SERVICE_NAME; + } + + @Override + public Class getProxyClass() { + return RestrictedMediaProxy.class; + } + + /** + * Creates a new blob resource from the form data and associates it with an existing media resource + * + * @param csid the media resource csid + * @param formDataOutput + * @return + */ + public Response createBlobFromFormData(String csid, MultipartFormDataOutput formDataOutput) { + return getProxy().createBlobFromFormData(csid, formDataOutput); + } + + /** + * Creates a new blob + * + * @param csid + * @return + */ + public Response createBlobFromUri(String csid, String blobUri) { + // send the URI as both a query param and as content + return getProxy().createBlobFromUri(csid, blobUri, blobUri); + } + + /* + * Create both a new media record + */ + public Response createMediaAndBlobWithUri(PoxPayloadOut xmlPayload, String blobUri, boolean purgeOriginal) { + return getProxy().createMediaAndBlobWithUri(xmlPayload.getBytes(), blobUri, purgeOriginal); + } + + /** + * @param csid + * @param xmlPayload + * @param URI + * @return + */ + public Response update(String csid, PoxPayloadOut xmlPayload, String URI) { + return getProxy().update(csid, xmlPayload.getBytes()); + } +} diff --git a/services/restrictedmedia/client/src/main/java/org/collectionspace/services/client/RestrictedMediaProxy.java b/services/restrictedmedia/client/src/main/java/org/collectionspace/services/client/RestrictedMediaProxy.java new file mode 100644 index 000000000..a4ce51cda --- /dev/null +++ b/services/restrictedmedia/client/src/main/java/org/collectionspace/services/client/RestrictedMediaProxy.java @@ -0,0 +1,60 @@ +/* + * This document is a part of the source code and related artifacts + * for CollectionSpace, an open source collections management system + * for museums and related institutions: + * + * http://www.collectionspace.org + * http://wiki.collectionspace.org + * + * Licensed under the Educational Community License (ECL), Version 2.0. + * You may not use this file except in compliance with this License. + * + * You may obtain a copy of the ECL 2.0 License at + * https://source.collectionspace.org/collection-space/LICENSE.txt + */ +package org.collectionspace.services.client; + +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Response; +import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataOutput; + +/** + * RestrictedMediaProxy.java + */ +@Path(RestrictedMediaClient.SERVICE_PATH_PROXY) +@Produces({"application/xml"}) +@Consumes({"application/xml"}) +public interface RestrictedMediaProxy extends CollectionSpaceCommonListPoxProxy { + + @POST + @Path("{csid}") + @Consumes("multipart/form-data") + Response createBlobFromFormData(@PathParam("csid") String csid, MultipartFormDataOutput formDataOutput); + + /** + * + * @param csid + * @param blobUri + * @param emptyXML param to force RESTEasy to produce a Content-Type header + * @return + */ + @POST + @Path("{csid}") + @Produces("application/xml") + @Consumes("application/xml") + Response createBlobFromUri( + @PathParam("csid") String csid, @QueryParam(BlobClient.BLOB_URI_PARAM) String blobUri, String emptyXML); + + @POST + @Produces("application/xml") + @Consumes("application/xml") + Response createMediaAndBlobWithUri( + byte[] xmlPayload, + @QueryParam(BlobClient.BLOB_URI_PARAM) String blobUri, + @QueryParam(BlobClient.BLOB_PURGE_ORIGINAL) boolean purgeOriginal); +} diff --git a/services/restrictedmedia/client/src/test/java/org/collectionspace/services/client/test/RestrictedMediaAuthRefsTest.java b/services/restrictedmedia/client/src/test/java/org/collectionspace/services/client/test/RestrictedMediaAuthRefsTest.java new file mode 100644 index 000000000..dd03a2bac --- /dev/null +++ b/services/restrictedmedia/client/src/test/java/org/collectionspace/services/client/test/RestrictedMediaAuthRefsTest.java @@ -0,0 +1,192 @@ +/** + * This document is a part of the source code and related artifacts + * for CollectionSpace, an open source collections management system + * for museums and related institutions: + *

+ * http://www.collectionspace.org + * http://wiki.collectionspace.org + *

+ * Copyright © 2009 Regents of the University of California + *

+ * Licensed under the Educational Community License (ECL), Version 2.0. + * You may not use this file except in compliance with this License. + *

+ * You may obtain a copy of the ECL 2.0 License at + * https://source.collectionspace.org/collection-space/LICENSE.txt + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.collectionspace.services.client.test; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import javax.ws.rs.core.Response; +import org.collectionspace.services.PersonJAXBSchema; +import org.collectionspace.services.client.CollectionSpaceClient; +import org.collectionspace.services.client.PersonAuthorityClientUtils; +import org.collectionspace.services.client.PersonClient; +import org.collectionspace.services.client.PoxPayloadOut; +import org.collectionspace.services.client.RestrictedMediaClient; +import org.collectionspace.services.jaxb.AbstractCommonList; +import org.collectionspace.services.person.PersonTermGroup; +import org.collectionspace.services.restrictedmedia.RestrictedMediaCommon; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.annotations.AfterClass; +import org.testng.annotations.Test; + +/** + * RestrictedMediaAuthRefsTest, carries out Authority References tests against a deployed and running + * Restricted Media Service. + */ +public class RestrictedMediaAuthRefsTest extends BaseServiceTest { + private final Logger logger = LoggerFactory.getLogger(RestrictedMediaAuthRefsTest.class); + final String PERSON_AUTHORITY_NAME = "MediaPersonAuth"; + private String knownResourceId = null; + private List mediaIdsCreated = new ArrayList(); + private List personIdsCreated = new ArrayList(); + private String personAuthCSID = null; + private String publisherRefName = null; + + @Override + public String getServicePathComponent() { + return RestrictedMediaClient.SERVICE_PATH_COMPONENT; + } + + @Override + protected String getServiceName() { + return RestrictedMediaClient.SERVICE_NAME; + } + + @Override + protected CollectionSpaceClient getClientInstance() { + throw new UnsupportedOperationException(); + } + + @Override + protected CollectionSpaceClient getClientInstance(String clientPropertiesFilename) { + throw new UnsupportedOperationException(); + } + + @Override + protected AbstractCommonList getCommonList(Response response) { + throw new UnsupportedOperationException(); + } + + private PoxPayloadOut createMediaInstance(String depositorRefName, String title) throws Exception { + RestrictedMediaCommon media = new RestrictedMediaCommon(); + media.setIdentificationNumber(UUID.randomUUID().toString()); + media.setTitle(title); + media.setPublisher(depositorRefName); + + PoxPayloadOut multipart = new PoxPayloadOut(RestrictedMediaClient.SERVICE_PAYLOAD_NAME); + multipart.addPart(new RestrictedMediaClient().getCommonPartName(), media); + logger.debug("to be created, media common: {}", objectAsXmlString(media, RestrictedMediaCommon.class)); + return multipart; + } + + @Test(dataProvider = "testName") + public void createWithAuthRefs(String testName) throws Exception { + testSetup(STATUS_CREATED, ServiceRequestType.CREATE); + String identifier = createIdentifier(); + + createPersonRefs(); + + // Create a new RestrictedMedia resource. + // One or more fields in this resource will be PersonAuthority references + // and will refer to Person resources by their refNames. + RestrictedMediaClient mediaClient = new RestrictedMediaClient(); + PoxPayloadOut multipart = createMediaInstance(publisherRefName, "media.title-" + identifier); + Response res = mediaClient.create(multipart); + try { + assertStatusCode(res, testName); + // Store the ID returned from the first resource created for additional tests below. + if (knownResourceId == null) { + knownResourceId = extractId(res); + } + // Store the IDs from every resource created; delete on cleanup + mediaIdsCreated.add(extractId(res)); + } finally { + if (res != null) { + res.close(); + } + } + } + + protected void createPersonRefs() throws Exception { + PersonClient personAuthClient = new PersonClient(); + // Create a temporary PersonAuthority resource, and its corresponding refName by which it can be identified. + PoxPayloadOut multipart = PersonAuthorityClientUtils.createPersonAuthorityInstance( + PERSON_AUTHORITY_NAME, PERSON_AUTHORITY_NAME, personAuthClient.getCommonPartName()); + Response res = personAuthClient.create(multipart); + assertStatusCode(res, "createPersonRefs (not a surefire test)"); + personAuthCSID = extractId(res); + + String authRefName = PersonAuthorityClientUtils.getAuthorityRefName(personAuthCSID, null); + + String csid = createPerson("Owen the Cur", "Owner", "owenCurOwner", authRefName); + personIdsCreated.add(csid); + publisherRefName = PersonAuthorityClientUtils.getPersonRefName(personAuthCSID, csid, null); + } + + protected String createPerson(String firstName, String surName, String shortId, String authRefName) + throws Exception { + PersonClient personAuthClient = new PersonClient(); + Map personInfo = new HashMap(); + personInfo.put(PersonJAXBSchema.FORE_NAME, firstName); + personInfo.put(PersonJAXBSchema.SUR_NAME, surName); + personInfo.put(PersonJAXBSchema.SHORT_IDENTIFIER, shortId + random.nextInt(1000)); + List personTerms = new ArrayList(); + PersonTermGroup term = new PersonTermGroup(); + String termName = firstName + " " + surName; + term.setTermDisplayName(termName); + term.setTermName(termName); + personTerms.add(term); + PoxPayloadOut multipart = PersonAuthorityClientUtils.createPersonInstance( + personAuthCSID, authRefName, personInfo, personTerms, personAuthClient.getItemCommonPartName()); + Response res = personAuthClient.createItem(personAuthCSID, multipart); + assertStatusCode(res, "createPerson (not a surefire test)"); + return extractId(res); + } + + /** + * Deletes all resources created by tests, after all tests have been run. + *

+ * This cleanup method will always be run, even if one or more tests fail. + * For this reason, it attempts to remove all resources created + * at any point during testing, even if some of those resources + * may be expected to be deleted by certain tests. Non-successful deletes are ignored and not reported + * @throws Exception + */ + @AfterClass(alwaysRun = true) + public void cleanUp() throws Exception { + String noTestCleanup = System.getProperty("noTestCleanup"); + if (Boolean.parseBoolean(noTestCleanup)) { + logger.debug("Skipping Cleanup phase..."); + return; + } + logger.debug("Cleaning up temporary resources created for testing..."); + PersonClient personAuthClient = new PersonClient(); + // Delete Person resource(s) (before PersonAuthority resources). + for (String resourceId : personIdsCreated) { + // Note: Any non-success responses are ignored and not reported. + personAuthClient.deleteItem(personAuthCSID, resourceId); + } + + if (personAuthCSID != null) { + personAuthClient.delete(personAuthCSID); + } + + RestrictedMediaClient mediaClient = new RestrictedMediaClient(); + for (String resourceId : mediaIdsCreated) { + mediaClient.delete(resourceId); + } + } +} diff --git a/services/restrictedmedia/client/src/test/java/org/collectionspace/services/client/test/RestrictedMediaServiceTest.java b/services/restrictedmedia/client/src/test/java/org/collectionspace/services/client/test/RestrictedMediaServiceTest.java new file mode 100644 index 000000000..46892ce55 --- /dev/null +++ b/services/restrictedmedia/client/src/test/java/org/collectionspace/services/client/test/RestrictedMediaServiceTest.java @@ -0,0 +1,283 @@ +/** + * This document is a part of the source code and related artifacts + * for CollectionSpace, an open source collections management system + * for museums and related institutions: + * + * http://www.collectionspace.org + * http://wiki.collectionspace.org + * + * Copyright © 2009 Regents of the University of California + * + * Licensed under the Educational Community License (ECL), Version 2.0. + * You may not use this file except in compliance with this License. + * + * You may obtain a copy of the ECL 2.0 License at + * https://source.collectionspace.org/collection-space/LICENSE.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.collectionspace.services.client.test; + +import java.io.File; +import java.net.URL; +import java.util.List; +import java.util.UUID; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import org.collectionspace.services.client.CollectionSpaceClient; +import org.collectionspace.services.client.PayloadOutputPart; +import org.collectionspace.services.client.PoxPayloadOut; +import org.collectionspace.services.client.RestrictedMediaClient; +import org.collectionspace.services.jaxb.AbstractCommonList; +import org.collectionspace.services.restrictedmedia.LanguageList; +import org.collectionspace.services.restrictedmedia.RestrictedMediaCommon; +import org.collectionspace.services.restrictedmedia.SubjectList; +import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataOutput; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.Assert; +import org.testng.annotations.Test; + +/** + * RestrictedMediaServiceTest, carries out tests against a deployed and running Restricted Media Service. + */ +public class RestrictedMediaServiceTest extends AbstractPoxServiceTestImpl { + + private final Logger logger = LoggerFactory.getLogger(RestrictedMediaServiceTest.class); + private static final String PUBLIC_URL_DECK = "https://farm8.staticflickr.com/7231/6962564226_4bdfc17599_k_d.jpg"; + + private boolean mediaCleanup = true; + + /** + * Sets up create tests. + */ + @Override + protected void setupCreate() { + super.setupCreate(); + String noMediaCleanup = System.getProperty(NO_MEDIA_CLEANUP); + if (Boolean.parseBoolean(noMediaCleanup)) { + // Don't delete the blobs that we created during the test cycle + this.mediaCleanup = false; + } + } + + private boolean isMediaCleanup() { + return mediaCleanup; + } + + @Override + public String getServicePathComponent() { + return RestrictedMediaClient.SERVICE_PATH_COMPONENT; + } + + @Override + protected String getServiceName() { + return RestrictedMediaClient.SERVICE_NAME; + } + + @Override + protected CollectionSpaceClient getClientInstance() throws Exception { + return new RestrictedMediaClient(); + } + + @Override + protected CollectionSpaceClient getClientInstance(String clientPropertiesFilename) throws Exception { + return new RestrictedMediaClient(clientPropertiesFilename); + } + + @Override + protected AbstractCommonList getCommonList(Response response) { + return response.readEntity(AbstractCommonList.class); + } + + /** + * Looks in the .../src/test/resources/blobs directory for files from which to create Blob + * instances. + * + * @param testName the test name + * @param fromUri - if 'true' then send the service a URI from which to create the blob. + * @param fromUri - if 'false' then send the service a multipart/form-data POST from which to create the blob. + * @throws Exception the exception + */ + public void createBlob(String testName, boolean fromUri) throws Exception { + setupCreate(); + + // First create a restricted media record + RestrictedMediaClient client = new RestrictedMediaClient(); + PoxPayloadOut multipart = createMediaInstance(createIdentifier()); + Response mediaRes = client.create(multipart); + String mediaCsid = null; + try { + assertStatusCode(mediaRes, testName); + mediaCsid = extractId(mediaRes); + } finally { + if (mediaRes != null) { + mediaRes.close(); + } + } + + String currentDir = getResourceDir(); + File blobsDir = new File(currentDir, BLOBS_DIR); + File blob = findBlobForMedia(blobsDir); + if (blob != null) { + createBlob(blob, fromUri, testName, mediaCsid); + } else { + logger.warn("Could not find blobbable file in {}", blobsDir); + } + } + + /** + * Iterate a directory for a file which matches isBlobbable + * + * @param blobsDir the directory to iterate + * @return a blob or null + */ + public File findBlobForMedia(File blobsDir) { + if (blobsDir.exists() && blobsDir.canRead()) { + File[] children = blobsDir.listFiles(); + if (children != null && children.length > 0) { + // Since Media records can have only a single associated + // blob, we'll stop after we find a valid candidate. + for (File child : children) { + if (isBlobbable(child)) { + return child; + } + } + } else { + logger.warn("Directory: {} is empty or cannot be read.", blobsDir); + } + } else { + logger.warn("Directory: {} is missing or cannot be read.", blobsDir); + } + + return null; + } + + public void createBlob(File blobFile, boolean fromUri, String testName, String mediaCsid) throws Exception { + logger.debug("Processing file URI: {}", blobFile.getAbsolutePath()); + + String mimeType = getMimeType(blobFile); + logger.debug("MIME type is: {}", mimeType); + + Response res; + RestrictedMediaClient client = new RestrictedMediaClient(); + if (fromUri) { + URL childUrl = blobFile.toURI().toURL(); + res = client.createBlobFromUri(mediaCsid, childUrl.toString()); + } else { + MultipartFormDataOutput formData = new MultipartFormDataOutput(); + formData.addFormData("file", blobFile, MediaType.valueOf(mimeType)); + res = client.createBlobFromFormData(mediaCsid, formData); + } + + try { + assertStatusCode(res, testName); + String blobCsid = extractId(res); + if (isMediaCleanup()) { + allResourceIdsCreated.add(blobCsid); + allResourceIdsCreated.add(mediaCsid); + } + } finally { + if (res != null) { + res.close(); + } + } + } + + @Test(dataProvider = "testName", dependsOnMethods = {"CRUDTests"}) + public void createWithBlobUri(String testName) throws Exception { + createBlob(testName, true /*with URI*/); + } + + @Test(dataProvider = "testName", dependsOnMethods = {"createWithBlobUri"}) + public void createMediaAndBlobWithUri(String testName) throws Exception { + RestrictedMediaClient client = new RestrictedMediaClient(); + PoxPayloadOut multipart = createMediaInstance(createIdentifier()); + + // purge the original + Response mediaRes = client.createMediaAndBlobWithUri(multipart, PUBLIC_URL_DECK, true); + String mediaCsid = null; + try { + assertStatusCode(mediaRes, testName); + mediaCsid = extractId(mediaRes); + if (isMediaCleanup()) { + allResourceIdsCreated.add(mediaCsid); + } + } finally { + if (mediaRes != null) { + mediaRes.close(); + } + } + } + + @Test(dataProvider = "testName", dependsOnMethods = {"createWithBlobUri"}) + public void createWithBlobPost(String testName) throws Exception { + createBlob(testName, false /*with POST*/); + } + + // --------------------------------------------------------------- + // Utility tests : tests of code used in tests above + // --------------------------------------------------------------- + + @Override + protected PoxPayloadOut createInstance(String identifier) throws Exception { + return createMediaInstance(identifier); + } + + // --------------------------------------------------------------- + // Utility methods used by tests above + // --------------------------------------------------------------- + private PoxPayloadOut createMediaInstance(String title) throws Exception { + String identifier = "media.title-" + title; + RestrictedMediaCommon media = new RestrictedMediaCommon(); + media.setTitle(identifier); + media.setIdentificationNumber(UUID.randomUUID().toString()); + media.setContributor("Joe-bob briggs"); + media.setCoverage("Lots of stuff"); + media.setPublisher("Ludicrum Enterprises"); + SubjectList subjects = new SubjectList(); + List subjList = subjects.getSubject(); + subjList.add("Pints of blood"); + subjList.add("Much skin"); + media.setSubjectList(subjects); + LanguageList languages = new LanguageList(); + List langList = languages.getLanguage(); + langList.add("English"); + langList.add("German"); + media.setLanguageList(languages); + PoxPayloadOut multipart = new PoxPayloadOut(RestrictedMediaClient.SERVICE_PAYLOAD_NAME); + PayloadOutputPart commonPart = multipart.addPart(media, MediaType.APPLICATION_XML_TYPE); + commonPart.setLabel(new RestrictedMediaClient().getCommonPartName()); + + logger.debug("to be created, media common"); + logger.debug(objectAsXmlString(media, RestrictedMediaCommon.class)); + + return multipart; + } + + @Override + protected PoxPayloadOut createInstance(String commonPartName, String identifier) throws Exception { + return createMediaInstance(identifier); + } + + @Override + protected RestrictedMediaCommon updateInstance(final RestrictedMediaCommon original) { + RestrictedMediaCommon result = new RestrictedMediaCommon(); + result.setTitle("updated-" + original.getTitle()); + return result; + } + + @Override + protected void compareUpdatedInstances(RestrictedMediaCommon original, RestrictedMediaCommon updated) { + Assert.assertEquals(updated.getTitle(), original.getTitle()); + } + + @Override + @Test(dataProvider = "testName", + dependsOnMethods = {"org.collectionspace.services.client.test.AbstractServiceTestImpl.baseCRUDTests"}) + public void CRUDTests(String testName) {} +} diff --git a/services/restrictedmedia/jaxb/pom.xml b/services/restrictedmedia/jaxb/pom.xml new file mode 100644 index 000000000..b0acaec09 --- /dev/null +++ b/services/restrictedmedia/jaxb/pom.xml @@ -0,0 +1,33 @@ + + + + org.collectionspace.services.restrictedmedia + org.collectionspace.services + ${revision} + + + 4.0.0 + org.collectionspace.services.restrictedmedia.jaxb + services.restrictedmedia.jaxb + + + + org.collectionspace.services + org.collectionspace.services.jaxb + ${project.version} + + + + + collectionspace-services-restrictedmedia-jaxb + install + + + org.jvnet.jaxb2.maven2 + maven-jaxb2-plugin + + + + \ No newline at end of file diff --git a/services/restrictedmedia/jaxb/src/main/resources/restrictedmedia_common.xsd b/services/restrictedmedia/jaxb/src/main/resources/restrictedmedia_common.xsd new file mode 100644 index 000000000..aed21ba74 --- /dev/null +++ b/services/restrictedmedia/jaxb/src/main/resources/restrictedmedia_common.xsd @@ -0,0 +1,151 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/services/restrictedmedia/pom.xml b/services/restrictedmedia/pom.xml new file mode 100644 index 000000000..5abd85896 --- /dev/null +++ b/services/restrictedmedia/pom.xml @@ -0,0 +1,58 @@ + + + + org.collectionspace.services + org.collectionspace.services.main + ${revision} + + + 4.0.0 + org.collectionspace.services.restrictedmedia + services.restrictedmedia + pom + + + + 2.30.0 + + + + + + com.diffplug.spotless + spotless-maven-plugin + ${spotless.version} + + + + + + src/main/java/**/*.java + src/test/java/**/*.java + + + + + true + 4 + + + + + + + + + + + + + + jaxb + service + client + + + \ No newline at end of file diff --git a/services/restrictedmedia/service/pom.xml b/services/restrictedmedia/service/pom.xml new file mode 100644 index 000000000..6a6530f22 --- /dev/null +++ b/services/restrictedmedia/service/pom.xml @@ -0,0 +1,109 @@ + + + + + org.collectionspace.services + org.collectionspace.services.restrictedmedia + ${revision} + + + 4.0.0 + org.collectionspace.services.restrictedmedia.service + services.restrictedmedia.service + jar + + + + org.collectionspace.services + org.collectionspace.services.common + + + org.collectionspace.services + org.collectionspace.services.blob.client + ${project.version} + + + org.collectionspace.services + org.collectionspace.services.blob.service + ${project.version} + + + org.collectionspace.services + org.collectionspace.services.media.jaxb + ${project.version} + + + org.collectionspace.services + org.collectionspace.services.restrictedmedia.jaxb + ${project.version} + + + org.collectionspace.services + org.collectionspace.services.restrictedmedia.client + ${project.version} + + + + + junit + junit + test + + + org.testng + testng + test + + + + + javax.security + jaas + 1.0.01 + provided + + + + + org.jboss.resteasy + resteasy-jaxrs + + + tjws + webserver + + + + + org.jboss.resteasy + resteasy-jaxb-provider + + + org.jboss.resteasy + resteasy-multipart-provider + + + + + org.nuxeo.ecm.core + nuxeo-core-api + + + jboss-remoting + jboss + + + + + org.apache.tomcat + tomcat-servlet-api + compile + + + + + collectionspace-services-restrictedmedia + + diff --git a/services/restrictedmedia/service/src/main/java/org/collectionspace/services/restrictedmedia/RestrictedMediaResource.java b/services/restrictedmedia/service/src/main/java/org/collectionspace/services/restrictedmedia/RestrictedMediaResource.java new file mode 100644 index 000000000..2c5b5dc01 --- /dev/null +++ b/services/restrictedmedia/service/src/main/java/org/collectionspace/services/restrictedmedia/RestrictedMediaResource.java @@ -0,0 +1,387 @@ +/* + * This document is a part of the source code and related artifacts + * for CollectionSpace, an open source collections management system + * for museums and related institutions: + * + * http://www.collectionspace.org + * http://wiki.collectionspace.org + * + * Licensed under the Educational Community License (ECL), Version 2.0. + * You may not use this file except in compliance with this License. + * + * You may obtain a copy of the ECL 2.0 License at + * + * https://source.collectionspace.org/collection-space/LICENSE.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.collectionspace.services.restrictedmedia; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Request; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; +import org.collectionspace.services.blob.BlobResource; +import org.collectionspace.services.client.BlobClient; +import org.collectionspace.services.client.CollectionSpaceClientUtils; +import org.collectionspace.services.client.PayloadOutputPart; +import org.collectionspace.services.client.PoxPayloadIn; +import org.collectionspace.services.client.PoxPayloadOut; +import org.collectionspace.services.client.RestrictedMediaClient; +import org.collectionspace.services.common.NuxeoBasedResource; +import org.collectionspace.services.common.ResourceMap; +import org.collectionspace.services.common.ServiceMessages; +import org.collectionspace.services.common.UriInfoWrapper; +import org.collectionspace.services.common.blob.BlobInput; +import org.collectionspace.services.common.blob.BlobUtil; +import org.collectionspace.services.common.context.ServiceContext; +import org.collectionspace.services.nuxeo.client.java.CommonList; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Path(RestrictedMediaClient.SERVICE_PATH) +@Consumes("application/xml") +@Produces("application/xml") +public class RestrictedMediaResource extends NuxeoBasedResource { + + private static final Logger logger = LoggerFactory.getLogger(RestrictedMediaResource.class); + + static final RestrictedMediaClient mediaClient = createMediaClient(); + + @Override + protected String getVersionString() { + return "$LastChangedRevision$"; + } + + @Override + public String getServiceName() { + return RestrictedMediaClient.SERVICE_NAME; + } + + @Override + public Class getCommonPartClass() { + return RestrictedMediaCommon.class; + } + + public String getCommonPartName() { + return mediaClient.getCommonPartName(); + } + + private static RestrictedMediaClient createMediaClient() { + RestrictedMediaClient result; + + try { + result = new RestrictedMediaClient(); + } catch (Exception e) { + String errMsg = "Could not create a new restricted media client for the RestrictedMedia resource."; + logger.error(errMsg, e); + throw new RuntimeException(); + } + + return result; + } + + private String getBlobCsid(String mediaCsid) throws Exception { + ServiceContext mediaContext = createServiceContext(); + + BlobInput blobInput = BlobUtil.getBlobInput(mediaContext); + blobInput.setSchemaRequested(true); + + // set the blobInput blobCsid + get(mediaCsid, mediaContext); + + String result = blobInput.getBlobCsid(); + + ensureCSID(result, READ); + + return result; + } + + /* + * Creates a new media record/resource AND creates a new blob (using a URL pointing to a media file/resource) and + * associates it with the new media record/resource. + */ + protected Response createBlobWithUri(UriInfo uriInfo, String blobUri) { + Response response; + + try { + ServiceContext ctx = createServiceContext(BlobClient.SERVICE_NAME, uriInfo); + BlobInput blobInput = BlobUtil.getBlobInput(ctx); + blobInput.createBlobFile(blobUri); + + // By now the binary bits have been created, and we just need to create the metadata blob + // record -this info is in the blobInput var + response = create(null, ctx); + } catch (Exception e) { + throw bigReThrow(e, ServiceMessages.CREATE_FAILED); + } + + return response; + } + + /* + * Looks for a blobUri query param from a POST. If it finds one then it creates a blob AND a media resource and + * associates them. + * + * @see org.collectionspace.services.common.ResourceBase#create(org.collectionspace.services.common.context.ServiceContext, org.collectionspace.services.common.ResourceMap, javax.ws.rs.core.UriInfo, java.lang.String) + */ + @Override + public Response create(ServiceContext parentCtx, + @Context ResourceMap resourceMap, + @Context UriInfo uriInfo, + String xmlPayload) { + uriInfo = new UriInfoWrapper(uriInfo); + + // create a blob resource/record first and then the media resource/record + MultivaluedMap queryParams = uriInfo.getQueryParameters(); + String blobUri = queryParams.getFirst(BlobClient.BLOB_URI_PARAM); + if (blobUri != null && !blobUri.isEmpty()) { + // uses the blob resource and doc handler to create the blob + Response result = createBlobWithUri(uriInfo, blobUri); + String blobCsid = CollectionSpaceClientUtils.extractId(result); + + // Add the new blob's csid as an artificial query param -the media doc handler will look for this + queryParams.add(BlobClient.BLOB_CSID_PARAM, blobCsid); + } + + return super.create(parentCtx, resourceMap, uriInfo, xmlPayload); + } + + @Override + public byte[] update(ServiceContext parentCtx, + @Context ResourceMap resourceMap, + @Context UriInfo uriInfo, + @PathParam("csid") String csid, + String xmlPayload) { + uriInfo = new UriInfoWrapper(uriInfo); + // If we find a "blobUri" query param, then we need to create a blob resource/record first and then the media + // resource/record + MultivaluedMap queryParams = uriInfo.getQueryParameters(); + String blobUri = queryParams.getFirst(BlobClient.BLOB_URI_PARAM); + if (blobUri != null && !blobUri.isEmpty()) { + // uses the blob resource and doc handler to create the blob + Response blobResponse = createBlobWithUri(uriInfo, blobUri); + String blobCsid = CollectionSpaceClientUtils.extractId(blobResponse); + // Add the new blob's csid as an artificial query param -the media doc handler will look + queryParams.add(BlobClient.BLOB_CSID_PARAM, blobCsid); + } + + // finish the media resource PUT request + return super.update(parentCtx, resourceMap, uriInfo, csid, xmlPayload); + } + + /* + * Creates a new blob (using a URL pointing to a media file/resource) and associate it with + * an existing media record/resource. + */ + @POST + @Path("{csid}") + @Consumes("application/xml") + @Produces("application/xml") + public Response createBlobWithUriAndUpdateMedia(@PathParam("csid") String csid, + @QueryParam(BlobClient.BLOB_URI_PARAM) String blobUri) { + Response response; + + try { + ServiceContext ctx = createServiceContext(BlobClient.SERVICE_NAME); + BlobInput blobInput = BlobUtil.getBlobInput(ctx); + blobInput.createBlobFile(blobUri); + response = create(null, ctx); + + // Next, update the RestrictedMedia record to be linked to the blob + // and put the blobInput into the RestrictedMedia context + ServiceContext mediaContext = createServiceContext(); + BlobUtil.setBlobInput(mediaContext, blobInput); + update(csid, null, mediaContext); + } catch (Exception e) { + throw bigReThrow(e, ServiceMessages.CREATE_FAILED); + } + + return response; + } + + @PUT + @Path("{csid}/blob") + @Consumes("multipart/form-data") + @Produces("application/xml") + public Response updateMediaByCreatingBlob( + @Context HttpServletRequest req, + @PathParam("csid") String csid, + @QueryParam(BlobClient.BLOB_URI_PARAM) String blobUri) { + return createBlob(req, csid, blobUri); + } + + /* + * Creates a new blob (using the incoming multipart form data) and associates it with an existing media + * record/resource. If a URL query param is passed in as well, we use the URL to create the new blob instead of + * the multipart form data. + * + * todo: Why is this deprecated? + */ + @POST + @Path("{csid}") + @Consumes("multipart/form-data") + @Produces("application/xml") + @Deprecated + public Response createBlob( + @Context HttpServletRequest req, + @PathParam("csid") String csid, + @QueryParam(BlobClient.BLOB_URI_PARAM) String blobUri) { + Response response; + try { + if (blobUri == null) { + ServiceContext blobContext = + createServiceContext(BlobClient.SERVICE_NAME, (PoxPayloadIn) null); + BlobInput blobInput = BlobUtil.getBlobInput(blobContext); + blobInput.createBlobFile(req, null); + response = create(null, blobContext); + + // Next, update the Media record to be linked to the blob and put the blobInput into the Media context + ServiceContext mediaContext = createServiceContext(); + BlobUtil.setBlobInput(mediaContext, blobInput); + update(csid, null, mediaContext); + } else { + // A URI query param overrides the incoming multipart/form-data payload in the request + response = createBlobWithUriAndUpdateMedia(csid, blobUri); + } + } catch (Exception e) { + throw bigReThrow(e, ServiceMessages.CREATE_FAILED); + } + + return response; + } + + @GET + @Path("{csid}/blob") + public byte[] getBlobInfo(@PathParam("csid") String csid) { + PoxPayloadOut result; + + try { + String blobCsid = getBlobCsid(csid); + ServiceContext blobContext = createServiceContext(BlobClient.SERVICE_NAME); + result = get(blobCsid, blobContext); + } catch (Exception e) { + throw bigReThrow(e, ServiceMessages.READ_FAILED, csid); + } + + return result.getBytes(); + } + + @GET + @Path("{csid}/blob/content") + public Response getBlobContent( + @PathParam("csid") String csid, @Context Request requestInfo, @Context UriInfo uriInfo) { + Response result; + BlobResource blobResource = new BlobResource(); + + try { + ensureCSID(csid, READ); + String blobCsid = getBlobCsid(csid); + result = blobResource.getBlobContent(blobCsid, requestInfo, uriInfo); + } catch (Exception e) { + throw bigReThrow(e, ServiceMessages.READ_FAILED, csid); + } + + return result; + } + + @GET + @Path("{csid}/blob/derivatives/{derivativeTerm}/content") + public Response getDerivativeContent( + @PathParam("csid") String csid, + @PathParam("derivativeTerm") String derivativeTerm, + @Context Request requestInfo, + @Context UriInfo uriInfo) { + Response result; + BlobResource blobResource = new BlobResource(); + + try { + ensureCSID(csid, READ); + String blobCsid = getBlobCsid(csid); + result = blobResource.getDerivativeContent(blobCsid, derivativeTerm, requestInfo, uriInfo); + } catch (Exception e) { + throw bigReThrow(e, ServiceMessages.READ_FAILED, csid); + } + + return result; + } + + @GET + @Path("{csid}/blob/derivatives/{derivativeTerm}") + public byte[] getDerivative(@PathParam("csid") String csid, @PathParam("derivativeTerm") String derivativeTerm) { + PoxPayloadOut result; + BlobResource blobResource = new BlobResource(); + + try { + ensureCSID(csid, READ); + String blobCsid = getBlobCsid(csid); + String xmlPayload = blobResource.getDerivative(blobCsid, derivativeTerm); + result = new PoxPayloadOut(xmlPayload.getBytes()); + } catch (Exception e) { + throw bigReThrow(e, ServiceMessages.READ_FAILED, csid); + } + + return result.getBytes(); + } + + @GET + @Path("{csid}/blob/derivatives") + @Produces("application/xml") + public CommonList getDerivatives(@PathParam("csid") String csid) { + CommonList result = null; + BlobResource blobResource = new BlobResource(); + + try { + ensureCSID(csid, READ); + String blobCsid = getBlobCsid(csid); + result = blobResource.getDerivatives(blobCsid); + } catch (Exception e) { + throw bigReThrow(e, ServiceMessages.READ_FAILED, csid); + } + + return result; + } + + @DELETE + @Path("{csid}") + @Override + public Response delete(@PathParam("csid") String csid) { + BlobResource blob = new BlobResource(); + + try { + ServiceContext ctx = createServiceContext(); + PoxPayloadOut mediaPayload = get(csid, ctx); + PayloadOutputPart mediaPayloadPart = mediaPayload.getPart(getCommonPartName()); + RestrictedMediaCommon mediaCommon = (RestrictedMediaCommon) mediaPayloadPart.getBody(); + String blobCsid = mediaCommon.getBlobCsid(); + + // Delete the blob if it exists. + if (blobCsid != null && !blobCsid.isEmpty()) { + Response response = blob.delete(blobCsid); + if (response.getStatus() != Response.Status.OK.getStatusCode()) { + logger.debug("Problem deleting related blob record of RestrictedMedia record: " + + "RestrictedMedia CSID={} Blob CSID={}", csid, blobCsid); + } + } + + return super.delete(ctx, csid); + } catch (Exception e) { + throw bigReThrow(e, ServiceMessages.DELETE_FAILED, csid); + } + } +} diff --git a/services/restrictedmedia/service/src/main/java/org/collectionspace/services/restrictedmedia/nuxeo/RestrictedMediaDocumentModelHandler.java b/services/restrictedmedia/service/src/main/java/org/collectionspace/services/restrictedmedia/nuxeo/RestrictedMediaDocumentModelHandler.java new file mode 100644 index 000000000..f29ddf5da --- /dev/null +++ b/services/restrictedmedia/service/src/main/java/org/collectionspace/services/restrictedmedia/nuxeo/RestrictedMediaDocumentModelHandler.java @@ -0,0 +1,84 @@ +/* + * This document is a part of the source code and related artifacts + * for CollectionSpace, an open source collections management system + * for museums and related institutions: + * + * http://www.collectionspace.org + * http://wiki.collectionspace.org + * + * Licensed under the Educational Community License (ECL), Version 2.0. + * You may not use this file except in compliance with this License. + * + * You may obtain a copy of the ECL 2.0 License at + * + * https://source.collectionspace.org/collection-space/LICENSE.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.collectionspace.services.restrictedmedia.nuxeo; + +import javax.ws.rs.core.MultivaluedMap; +import org.collectionspace.services.MediaJAXBSchema; +import org.collectionspace.services.client.BlobClient; +import org.collectionspace.services.common.blob.BlobInput; +import org.collectionspace.services.common.blob.BlobUtil; +import org.collectionspace.services.common.context.ServiceContext; +import org.collectionspace.services.common.document.DocumentWrapper; +import org.collectionspace.services.nuxeo.client.java.NuxeoDocumentModelHandler; +import org.collectionspace.services.restrictedmedia.RestrictedMediaCommon; +import org.nuxeo.ecm.core.api.DocumentModel; + +/** + * RestrictedMediaDocumentModelHandler + */ +public class RestrictedMediaDocumentModelHandler extends NuxeoDocumentModelHandler { + + private RestrictedMediaCommon getCommonPartProperties(DocumentModel docModel) { + String label = getServiceContext().getCommonPartLabel(); + RestrictedMediaCommon result = new RestrictedMediaCommon(); + + result.setBlobCsid((String) docModel.getProperty(label, MediaJAXBSchema.blobCsid)); + return result; + } + + @Override + public void extractAllParts(DocumentWrapper wrapDoc) throws Exception { + ServiceContext ctx = getServiceContext(); + + final BlobInput blobInput = BlobUtil.getBlobInput(ctx); + if (blobInput.isSchemaRequested()) { + DocumentModel docModel = wrapDoc.getWrappedObject(); + RestrictedMediaCommon mediaCommon = getCommonPartProperties(docModel); + String blobCsid = mediaCommon.getBlobCsid(); + blobInput.setBlobCsid(blobCsid); + } else { + super.extractAllParts(wrapDoc); + } + } + + @Override + public void fillAllParts(DocumentWrapper wrapDoc, Action action) throws Exception { + String blobCsid; + ServiceContext ctx = getServiceContext(); + + BlobInput blobInput = BlobUtil.getBlobInput(ctx); + if (blobInput.getBlobCsid() != null) { + blobCsid = blobInput.getBlobCsid(); + } else { + MultivaluedMap queryParams = ctx.getQueryParams(); + blobCsid = queryParams.getFirst(BlobClient.BLOB_CSID_PARAM); + // we're creating a blob from a URI and creating a new media resource as well + // extract all the other fields from the input payload + super.fillAllParts(wrapDoc, action); + } + + if (blobCsid != null) { + DocumentModel documentModel = wrapDoc.getWrappedObject(); + documentModel.setProperty(ctx.getCommonPartLabel(), MediaJAXBSchema.blobCsid, blobCsid); + } + } +} diff --git a/services/restrictedmedia/service/src/main/java/org/collectionspace/services/restrictedmedia/nuxeo/RestrictedMediaValidatorHandler.java b/services/restrictedmedia/service/src/main/java/org/collectionspace/services/restrictedmedia/nuxeo/RestrictedMediaValidatorHandler.java new file mode 100644 index 000000000..11fe5f252 --- /dev/null +++ b/services/restrictedmedia/service/src/main/java/org/collectionspace/services/restrictedmedia/nuxeo/RestrictedMediaValidatorHandler.java @@ -0,0 +1,69 @@ +/* + * This document is a part of the source code and related artifacts + * for CollectionSpace, an open source collections management system + * for museums and related institutions: + * + * http://www.collectionspace.org + * http://wiki.collectionspace.org + * + * Licensed under the Educational Community License (ECL), Version 2.0. + * You may not use this file except in compliance with this License. + * + * You may obtain a copy of the ECL 2.0 License at + * https://source.collectionspace.org/collection-space/LICENSE.txt + */ +package org.collectionspace.services.restrictedmedia.nuxeo; + +import org.collectionspace.services.client.PoxPayloadIn; +import org.collectionspace.services.client.PoxPayloadOut; +import org.collectionspace.services.common.document.InvalidDocumentException; +import org.collectionspace.services.common.document.ValidatorHandlerImpl; +import org.collectionspace.services.restrictedmedia.RestrictedMediaCommon; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * ValidatorHandler for RestrictedMedia + * + * Checks for presence of restrictedmedia_common and restrictedmedia_common/identificationNumber + */ +public class RestrictedMediaValidatorHandler extends ValidatorHandlerImpl { + private final Logger logger = LoggerFactory.getLogger(RestrictedMediaValidatorHandler.class); + + private static final String COMMON_PART_MISSING = + "Validation exception: restrictedmedia_common part is missing"; + private static final String IDENTIFICATION_NUMBER_MISSING = + "Validation exception: The restricted media field \"identificationNumber\" cannot be empty or missing"; + + @Override + protected Class getCommonPartClass() { + return RestrictedMediaCommon.class; + } + + @Override + protected void handleCreate() throws InvalidDocumentException { + final RestrictedMediaCommon restrictedMedia = (RestrictedMediaCommon) getCommonPart(); + if (restrictedMedia == null) { + logger.error(COMMON_PART_MISSING); + throw new InvalidDocumentException(COMMON_PART_MISSING); + } + + final String identificationNumber = restrictedMedia.getIdentificationNumber(); + if (identificationNumber == null || identificationNumber.isEmpty()) { + logger.error(IDENTIFICATION_NUMBER_MISSING); + throw new InvalidDocumentException(IDENTIFICATION_NUMBER_MISSING); + } + } + + @Override + protected void handleGet() {} + + @Override + protected void handleGetAll() {} + + @Override + protected void handleUpdate() {} + + @Override + protected void handleDelete() {} +}