From d59528e66b0fd36453b4c515bcb9930ca1ebe4a2 Mon Sep 17 00:00:00 2001 From: Richard Millet Date: Tue, 21 Aug 2012 00:33:44 -0700 Subject: [PATCH] CSPACE-5466: See JIRA issue for full description. In short, media from originating from an external URL is deleted from the system after a set of derivatives are produced. --- services/blob/client/pom.xml | 6 ++ .../services/client/BlobClient.java | 1 + .../blob/nuxeo/BlobDocumentModelHandler.java | 25 ++++- .../services/client/test/BaseServiceTest.java | 1 - .../services/common/ServiceMain.java | 14 +++ .../config/TenantBindingConfigReaderImpl.java | 2 +- .../common/imaging/nuxeo/NuxeoImageUtils.java | 98 +++++++++++++++--- .../services/nuxeo/util/NuxeoUtils.java | 91 ++++++++++++++++ .../src/main/resources/documentImage.jpg | Bin 28504 -> 0 bytes .../config/AbstractConfigReaderImpl.java | 4 +- .../services/common/config/ConfigReader.java | 4 +- .../services/client/MediaClient.java | 4 +- .../services/client/MediaProxy.java | 3 +- .../client/test/MediaServiceTest.java | 3 +- .../services/media/MediaResource.java | 12 ++- 15 files changed, 233 insertions(+), 35 deletions(-) delete mode 100644 services/common/src/main/resources/documentImage.jpg diff --git a/services/blob/client/pom.xml b/services/blob/client/pom.xml index 8223e96a3..3e8888d03 100644 --- a/services/blob/client/pom.xml +++ b/services/blob/client/pom.xml @@ -14,6 +14,12 @@ services.blob.client + + org.nuxeo.ecm.core + nuxeo-core-storage-sql-management + 5.5.0-HF07 + + org.slf4j diff --git a/services/blob/client/src/main/java/org/collectionspace/services/client/BlobClient.java b/services/blob/client/src/main/java/org/collectionspace/services/client/BlobClient.java index 4033f33de..5788d17cf 100644 --- a/services/blob/client/src/main/java/org/collectionspace/services/client/BlobClient.java +++ b/services/blob/client/src/main/java/org/collectionspace/services/client/BlobClient.java @@ -38,6 +38,7 @@ public class BlobClient extends AbstractCommonListPoxServiceClientImpl { // if (derivativeTerm != null || getContentFlag == true) { StringBuffer mimeTypeBuffer = new StringBuffer(); - BlobOutput blobOutput = NuxeoImageUtils.getBlobOutput(ctx, repoSession, //FIXME: REM - If the blob's binary has been removed from the file system, then this call will return null. We need to at least spit out a meaningful error/warning message + BlobOutput blobOutput = NuxeoImageUtils.getBlobOutput(ctx, repoSession, blobRepositoryId, derivativeTerm, getContentFlag, mimeTypeBuffer); if (getContentFlag == true) { - blobInput.setContentStream(blobOutput.getBlobInputStream()); + if (blobOutput != null) { + blobInput.setContentStream(blobOutput.getBlobInputStream()); + } else { + // If we can't find the blob's content, we'll return a "missing document" image + blobInput.setContentStream(NuxeoImageUtils.getResource(NuxeoImageUtils.DOCUMENT_MISSING_PLACEHOLDER_IMAGE)); + mimeTypeBuffer.append(NuxeoImageUtils.MIME_JPEG); + } } if (derivativeTerm != null) { @@ -179,7 +187,7 @@ extends DocHandlerBase { blobInput.setMimeType(mimeType); blobsCommon.setMimeType(mimeType); } else { - blobInput.setMimeType(blobsCommon.getMimeType()); + blobInput.setMimeType(blobsCommon.getMimeType()); // Set the MIME type to the one in blobsCommon } blobsCommon.setRepositoryId(null); //hide the repository id from the GET results payload since it is private @@ -202,12 +210,19 @@ extends DocHandlerBase { ServiceContext ctx = this.getServiceContext(); BlobInput blobInput = BlobUtil.getBlobInput(ctx); // The blobInput should have been put into the context by the Blob or Media resource if (blobInput != null && blobInput.getBlobFile() != null) { + boolean purgeOriginal = false; + MultivaluedMap queryParams = ctx.getQueryParams(); + String purgeOriginalStr = queryParams.getFirst(BlobClient.BLOB_PURGE_ORIGINAL); + if (purgeOriginalStr != null && purgeOriginalStr.isEmpty() == false) { // Find our if the caller wants us to purge/delete the original + purgeOriginal = true; + } // // If blobInput has a file then we just received a multipart/form-data file post or a URI query parameter // DocumentModel documentModel = wrapDoc.getWrappedObject(); - RepositoryInstance repoSession = this.getRepositorySession(); - BlobsCommon blobsCommon = NuxeoImageUtils.createBlobInRepository(ctx, repoSession, blobInput); + RepositoryInstance repoSession = this.getRepositorySession(); + + BlobsCommon blobsCommon = NuxeoImageUtils.createBlobInRepository(ctx, repoSession, blobInput, purgeOriginal); blobInput.setBlobCsid(documentModel.getName()); //Assumption here is that the documentModel "name" field is storing a CSID PoxPayloadIn input = ctx.getInput(); diff --git a/services/client/src/main/java/org/collectionspace/services/client/test/BaseServiceTest.java b/services/client/src/main/java/org/collectionspace/services/client/test/BaseServiceTest.java index a729108fd..03e4267f4 100644 --- a/services/client/src/main/java/org/collectionspace/services/client/test/BaseServiceTest.java +++ b/services/client/src/main/java/org/collectionspace/services/client/test/BaseServiceTest.java @@ -25,7 +25,6 @@ package org.collectionspace.services.client.test; import java.io.ByteArrayInputStream; import java.io.File; -import java.io.InputStream; import java.io.StringWriter; import java.lang.reflect.Method; import java.util.ArrayList; diff --git a/services/common/src/main/java/org/collectionspace/services/common/ServiceMain.java b/services/common/src/main/java/org/collectionspace/services/common/ServiceMain.java index 3a93a231b..43bdcdad0 100644 --- a/services/common/src/main/java/org/collectionspace/services/common/ServiceMain.java +++ b/services/common/src/main/java/org/collectionspace/services/common/ServiceMain.java @@ -3,6 +3,10 @@ */ package org.collectionspace.services.common; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -18,6 +22,7 @@ import org.collectionspace.authentication.AuthN; import org.collectionspace.services.config.service.InitHandler; import org.collectionspace.services.common.authorization_mgt.AuthorizationCommon; +import org.collectionspace.services.common.config.ConfigReader; import org.collectionspace.services.common.config.ServicesConfigReaderImpl; import org.collectionspace.services.common.config.TenantBindingConfigReaderImpl; import org.collectionspace.services.common.init.AddIndices; @@ -321,6 +326,15 @@ public class ServiceMain { public String getServerRootDir() { return serverRootDir; } + + public InputStream getResourceAsStream(String resourceName) throws FileNotFoundException { + InputStream result = null; + + String resourcePath = getServerRootDir() + File.separator + ConfigReader.RESOURCES_DIR_PATH + File.separator + resourceName; + result = new FileInputStream(new File(resourcePath)); + + return result; + } /* * Save a copy of the DataSource instances that exist in our initial JNDI context. For some reason, after starting up diff --git a/services/common/src/main/java/org/collectionspace/services/common/config/TenantBindingConfigReaderImpl.java b/services/common/src/main/java/org/collectionspace/services/common/config/TenantBindingConfigReaderImpl.java index f6a4295d2..e218427dd 100644 --- a/services/common/src/main/java/org/collectionspace/services/common/config/TenantBindingConfigReaderImpl.java +++ b/services/common/src/main/java/org/collectionspace/services/common/config/TenantBindingConfigReaderImpl.java @@ -470,6 +470,6 @@ public class TenantBindingConfigReaderImpl } public String getResourcesDir(){ - return getConfigRootDir() + File.separator + "resources"; + return getConfigRootDir() + File.separator + RESOURCES_DIR_NAME; } } diff --git a/services/common/src/main/java/org/collectionspace/services/common/imaging/nuxeo/NuxeoImageUtils.java b/services/common/src/main/java/org/collectionspace/services/common/imaging/nuxeo/NuxeoImageUtils.java index 704aefff3..67af453bc 100644 --- a/services/common/src/main/java/org/collectionspace/services/common/imaging/nuxeo/NuxeoImageUtils.java +++ b/services/common/src/main/java/org/collectionspace/services/common/imaging/nuxeo/NuxeoImageUtils.java @@ -30,17 +30,24 @@ import java.awt.Color; import java.awt.Font; import java.awt.Graphics; import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; import java.io.File; import java.io.ByteArrayOutputStream; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.InputStream; import java.io.BufferedInputStream; import java.io.IOException; +import java.io.Serializable; import java.math.BigDecimal; import java.math.BigInteger; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Random; +import java.util.Set; +import java.lang.reflect.Field; import javax.imageio.ImageIO; @@ -51,6 +58,7 @@ import org.nuxeo.runtime.api.Framework; //import org.nuxeo.common.utils.FileUtils; import org.nuxeo.ecm.platform.picture.api.ImageInfo; +import org.nuxeo.ecm.platform.picture.api.ImagingDocumentConstants; import org.nuxeo.ecm.platform.picture.api.ImagingService; import org.nuxeo.ecm.platform.picture.api.PictureView; @@ -69,7 +77,12 @@ import org.nuxeo.ecm.core.repository.RepositoryService; //import org.nuxeo.ecm.core.api.ejb.DocumentManagerBean; //import org.nuxeo.ecm.core.storage.sql.RepositoryImpl; //import org.nuxeo.ecm.core.storage.sql.Repository; +import org.nuxeo.ecm.core.storage.sql.Binary; +import org.nuxeo.ecm.core.storage.sql.BinaryManager; import org.nuxeo.ecm.core.storage.sql.DefaultBinaryManager; +import org.nuxeo.ecm.core.storage.sql.RepositoryImpl; +import org.nuxeo.ecm.core.storage.sql.RepositoryResolver; +import org.nuxeo.ecm.core.storage.sql.coremodel.SQLBlob; import org.nuxeo.ecm.core.storage.sql.coremodel.SQLRepository; //import org.nuxeo.ecm.core.storage.sql.RepositoryDescriptor; @@ -77,6 +90,7 @@ import org.nuxeo.ecm.core.storage.sql.coremodel.SQLRepository; import org.nuxeo.ecm.core.api.IdRef; import org.nuxeo.ecm.core.api.blobholder.BlobHolder; import org.nuxeo.ecm.core.api.blobholder.DocumentBlobHolder; +import org.nuxeo.ecm.core.api.impl.DocumentModelImpl; import org.nuxeo.ecm.core.api.impl.blob.FileBlob; import org.nuxeo.ecm.core.api.impl.blob.StreamingBlob; import org.nuxeo.ecm.core.api.impl.blob.ByteArrayBlob; @@ -95,6 +109,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; //import org.nuxeo.ecm.core.repository.jcr.testing.RepositoryOSGITestCase; +import org.collectionspace.services.common.ServiceMain; import org.collectionspace.services.common.blob.BlobInput; import org.collectionspace.services.common.context.ServiceContext; import org.collectionspace.services.common.datetime.GregorianCalendarDateTimeUtils; @@ -108,22 +123,24 @@ import org.collectionspace.services.blob.MeasuredPartGroupList; import org.collectionspace.services.jaxb.BlobJAXBSchema; import org.collectionspace.services.nuxeo.client.java.CommonList; import org.collectionspace.services.nuxeo.extension.thumbnail.ThumbnailConstants; +import org.collectionspace.services.nuxeo.util.NuxeoUtils; import org.collectionspace.services.common.blob.BlobOutput; import org.collectionspace.services.config.service.ListResultField; -//import org.collectionspace.ecm.platform.quote.api.QuoteManager; -// TODO: Auto-generated Javadoc /** * The Class NuxeoImageUtils. */ public class NuxeoImageUtils { + /** The Constant logger. */ private static final Logger logger = LoggerFactory .getLogger(NuxeoImageUtils.class); - private static final String MIME_JPEG = "image/jpeg"; + public static final String DOCUMENT_PLACEHOLDER_IMAGE = "documentImage.jpg"; + public static final String DOCUMENT_MISSING_PLACEHOLDER_IMAGE = "documentImageMissing.jpg"; + public static final String MIME_JPEG = "image/jpeg"; /* * FIXME: REM - These constants should be coming from configuration and NOT * hard coded. @@ -267,8 +284,10 @@ public class NuxeoImageUtils { // List blobListItems = result.getBlobListItem(); HashMap item = null; for (Blob blob : docBlobs) { - item = createBlobListItem(blob, uri); - commonList.addItem(item); + if (blob != null) { + item = createBlobListItem(blob, uri); + commonList.addItem(item); + } } return commonList; @@ -724,6 +743,19 @@ public class NuxeoImageUtils { } return result; } + + private static BinaryManager getBinaryManagerService() throws ClientException { + BinaryManager result = null; + try { + result = Framework.getService(BinaryManager.class); + } catch (Exception e) { + String msg = "Unable to get Nuxeo's BinaryManager service."; + logger.error(msg, e); + throw new ClientException("msg", e); + } + return result; + } + /** * Creates the picture. @@ -738,7 +770,9 @@ public class NuxeoImageUtils { * @throws Exception */ public static BlobsCommon createBlobInRepository(ServiceContext ctx, - RepositoryInstance repoSession, BlobInput blobInput) throws Exception { + RepositoryInstance repoSession, + BlobInput blobInput, + boolean purgeOriginal) throws Exception { BlobsCommon result = null; try { @@ -756,7 +790,7 @@ public class NuxeoImageUtils { } } - result = createBlobInRepository(repoSession, wspaceDoc, blobFile, null /*mime type*/); + result = createBlobInRepository(repoSession, wspaceDoc, purgeOriginal, blobFile, null /*mime type*/); } catch (Exception e) { logger.error("Could not create image blob", e); throw e; @@ -782,8 +816,9 @@ public class NuxeoImageUtils { */ static public BlobsCommon createBlobInRepository(RepositoryInstance nuxeoSession, DocumentModel blobLocation, - // InputStream file, - File file, String mimeType) { + boolean purgeOriginal, + File file, + String mimeType) { BlobsCommon result = null; try { @@ -799,8 +834,29 @@ public class NuxeoImageUtils { blobLocation.getPathAsString(), true, file.getName()); logger.debug("Stop --> Finished calling Nuxeo to create the blob document."); - + result = createBlobsCommon(documentModel, fileBlob); // Now create our metadata resource document + + // If the sender only wanted use to generate derivatives, we need to clear the original content + if (purgeOriginal == true) { + // Empty the document model's "content" property -this does not delete the actual file/blob + documentModel.setPropertyValue("file:content", (Serializable) null); + + if (documentModel.hasFacet(ImagingDocumentConstants.PICTURE_FACET)) { + // Now with no content, the derivative listener wants to update the derivatives. So to + // prevent the listener, we remove the "Picture" facet from the document + NuxeoUtils.removeFacet(documentModel, ImagingDocumentConstants.PICTURE_FACET); // Removing this facet ensures the original derivatives are unchanged. + nuxeoSession.saveDocument(documentModel); + // Now that we've emptied the document model's content field, we can add back the Picture facet + NuxeoUtils.addFacet(documentModel, ImagingDocumentConstants.PICTURE_FACET); + } + + nuxeoSession.saveDocument(documentModel); + // Next, we need to remove the actual file from Nuxeo's data directory + DocumentBlobHolder docBlobHolder = (DocumentBlobHolder) documentModel + .getAdapter(BlobHolder.class); + boolean deleteSuccess = NuxeoUtils.deleteFileOfBlob(docBlobHolder.getBlob()); + } } catch (Exception e) { result = null; logger.error("Could not create new Nuxeo blob document.", e); //FIXME: REM - This should probably be re-throwing the exception? @@ -829,6 +885,18 @@ public class NuxeoImageUtils { // } // } // } + + public static InputStream getResource(String resourceName) { + InputStream result = null; + + try { + result = ServiceMain.getInstance().getResourceAsStream(resourceName); + } catch (FileNotFoundException e) { + logger.error("Missing Services resource: " + resourceName, e); + } + + return result; + } /** * Gets the image. @@ -869,7 +937,7 @@ public class NuxeoImageUtils { if (derivativeTerm != null) { docBlob = pictureBlobHolder.getBlob(derivativeTerm); // Nuxeo derivatives are all JPEG - outMimeType.append(docBlob.getMimeType()); + outMimeType.append(MIME_JPEG); // All Nuxeo image derivatives are JPEG images. } else { docBlob = pictureBlobHolder.getBlob(); } @@ -890,15 +958,13 @@ public class NuxeoImageUtils { if (getContentFlag == true) { InputStream remoteStream = null; if (isNonImageDerivative == false) { - remoteStream = docBlob.getStream(); + remoteStream = docBlob.getStream(); // This will fail if the blob's file has been deleted. FileNotFoundException thrown. } else { - remoteStream = NuxeoImageUtils.class.getClassLoader() // for now, non-image derivatives are just placeholder document images - .getResourceAsStream("documentImage.jpg"); + remoteStream = getResource(DOCUMENT_PLACEHOLDER_IMAGE); outMimeType.append(MIME_JPEG); } BufferedInputStream bufferedInputStream = new BufferedInputStream( - remoteStream); // FIXME: REM - To improve performance, try - // BufferedInputStream(InputStream in, int size)? + remoteStream); result.setBlobInputStream(bufferedInputStream); } } catch (Exception e) { diff --git a/services/common/src/main/java/org/collectionspace/services/nuxeo/util/NuxeoUtils.java b/services/common/src/main/java/org/collectionspace/services/nuxeo/util/NuxeoUtils.java index a3ed9fb36..aa525b748 100644 --- a/services/common/src/main/java/org/collectionspace/services/nuxeo/util/NuxeoUtils.java +++ b/services/common/src/main/java/org/collectionspace/services/nuxeo/util/NuxeoUtils.java @@ -21,6 +21,7 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.File; +import java.lang.reflect.Field; import java.util.GregorianCalendar; import java.util.List; @@ -49,6 +50,7 @@ import org.nuxeo.ecm.core.api.DocumentModel; import org.nuxeo.ecm.core.api.DocumentModelList; import org.nuxeo.ecm.core.api.ClientException; import org.nuxeo.ecm.core.api.repository.RepositoryInstance; +import org.nuxeo.ecm.core.api.Blob; import org.nuxeo.ecm.core.api.CoreSession; import org.nuxeo.ecm.core.api.DocumentRef; import org.nuxeo.ecm.core.api.IdRef; @@ -64,6 +66,8 @@ import org.nuxeo.ecm.core.io.impl.plugins.XMLDocumentWriter; import org.nuxeo.ecm.core.schema.SchemaManager; import org.nuxeo.ecm.core.search.api.client.querymodel.descriptor.QueryModelDescriptor; +import org.nuxeo.ecm.core.storage.sql.Binary; +import org.nuxeo.ecm.core.storage.sql.coremodel.SQLBlob; import org.nuxeo.runtime.api.Framework; @@ -91,7 +95,94 @@ public class NuxeoUtils { //private static final String ORDER_BY_CLAUSE_REGEX = "\\w+(_\\w+)?:\\w+( ASC| DESC)?(, \\w+(_\\w+)?:\\w+( ASC| DESC)?)*"; // Allow paths so can sort on complex fields. CSPACE-4601 private static final String ORDER_BY_CLAUSE_REGEX = "\\w+(_\\w+)?:\\w+(/(\\*|\\w+))*( ASC| DESC)?(, \\w+(_\\w+)?:\\w+(/(\\*|\\w+))*( ASC| DESC)?)*"; + + /* + * Keep this method private. This method uses reflection to gain access to a protected field in Nuxeo's "Binary" class. Once we learn how + * to locate the "file" field of a Binary instance without breaking our "contract" with this class, we should minimize + * our use of this method. + */ + private static File getFileOfBlob(Blob blob) { + File result = null; + + if (blob instanceof SQLBlob) { + SQLBlob sqlBlob = (SQLBlob)blob; + Binary binary = sqlBlob.getBinary(); + try { + Field fileField = binary.getClass().getDeclaredField("file"); + boolean accessibleState = fileField.isAccessible(); + if (accessibleState == false) { + fileField.setAccessible(true); + } + result = (File)fileField.get(binary); + fileField.setAccessible(accessibleState); // set it back to its original access state + } catch (Exception e) { + logger.error("Was not able to find the 'file' field", e); + } + } + + return result; + } + + static public boolean deleteFileOfBlob(Blob blob) { + boolean result = false; + + File fileToDelete = getFileOfBlob(blob); + result = fileToDelete.delete(); + if (result == false) { + logger.warn("Could not delete the blob file at: " + fileToDelete.getAbsolutePath()); + } + + return result; + } + + /* + * This method will fail to return a facet list if non exist or if Nuxeo changes the + * DocumentModelImpl class "facets" field to be of a different type or if they remove it altogether. + */ + public static Set getFacets(DocumentModel docModel) { + Set result = null; + + try { + Field f = docModel.getClass().getDeclaredField("facets"); + f.setAccessible(true); + result = (Set) f.get(docModel); + f.setAccessible(false); + } catch (Exception e) { + logger.error("Could not remove facet from DocumentModel instance: " + docModel.getId(), e); + } + + return result; + } + + /* + * Remove a Nuxeo facet from a document model instance + */ + public static boolean removeFacet(DocumentModel docModel, String facet) { + boolean result = false; + + Set facets = getFacets(docModel); + if (facets != null && facets.contains(facet)) { + facets.remove(facet); + result = true; + } + + return result; + } + + /* + * Adds a Nuxeo facet to a document model instance + */ + public static boolean addFacet(DocumentModel docModel, String facet) { + boolean result = false; + + Set facets = getFacets(docModel); + if (facets != null && !facets.contains(facet)) { + facets.add(facet); + result = true; + } + return result; + } public static void exportDocModel(DocumentModel src) { DocumentReader reader = null; diff --git a/services/common/src/main/resources/documentImage.jpg b/services/common/src/main/resources/documentImage.jpg deleted file mode 100644 index 89d50669cb1e7a7328dd629c23c6c6052bda748d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28504 zcmdSB3p|uv_cwkOB_T-*BwfEX@P6?6 zpw-65jE+G|mMnpO1Aic1KXmbEfZKTpGBJUaAqZLlEnA`nEd@VGfXi553A7CSzXV+N zW~+bw54u8-nFJ*G`~BStW^4ZbGq?vGJm-DZWs{MoldFrli_^J4e!JVxAc$PU>x2$N zOP4I+|NY}5uuOpeSuQ9juuN#VkP!b`AuJ-iV#Uf8LP9IWR<0Bg1s@^dRpMfztN1_j ziSWDgdx5{AD}+|?Tl{Aq-X}<6#gd{WMFLBN_4*be>oF5yqlmPU9dP8lXn9a!}u zB|R{?8JgHp>YA^6ZRPeC|8tk{rx%Z^;|{FEE2ODCc1!iWX;#CcHeR<6+@JB%RM&Es z)_%)fm!UED4+quTlJ&)u>tx?%ZW~f{KGb#d+XkJZmsy_8JV>J>!%$U!0+)BGC+_gc z_u2Q{`i88|ghW1*q8Vd_*o4-yi_<6b#lfS2Vx^2~PLhGiLP2I$qgOcjYZGhR_Q zr*$3It9n^V41aH-{x!2GV1xgOK>aNx_^}*>D5N)Va=C>GPG-M$p7lFI zq!TgXzDLzU>`PD; z1kL9}iwmml*3OQMAe{0HovLPFb+ZT1jmFJy8pt||tgS#>oZBxb2}G7@K7}JXL19TN zvA**l;nqSPbnvMS4_Z3JgEn-coBVJbQ9MhP2NkOEpgZN_96^|c;X&^3saYEa{a<=u z`*mA+(3nOh59+fg4H_Z`CP=gEDKtC}^1{&9JMo|cL>?r-gQjp3@)4-xzqD7f>*X%@ z!y_Vb$OSOqMho<&G%5nxm!k@z*f9w}7v^Sg4}ZtkS0n%2_}1hZ9`x*M77x098IH!# z1v24pXL(Q=%zY16IE8bUVi9p3l#hKCq{0s1L4RpZd+Gtt%Cx|=-8g#6Kguo6irNF{ z8UnIBXg+;T{3xu7`RCS>+1}_)T{60?7TpV1tjYj7jO0PfK=XbcByk%vyMhPNuxK;- ze2hFii2hUa&wklF=$oq*5Bl^0TSa7DZ^4eK!F?383p=o<9KDV~g?La0saZwa-9ugs zRwez1L$CZ}Xp&+AbkEl+EdiPv$A7sTFleWM9^R1ti)W1Nl3R-M65KV2@5XsF0*I1+MicFwECRHo>%dxiIX-DWXKGZX76Q{YLr|JSZqqjg)) zpI@1&ZnO^yYHX|RrP<*+w!rO+^K080KlgP`pnDVbwB08N=ZxYz`fuxC-ruvLo}K(D zp7OvYUqjS$r<7_$>VweT3+!E{a#RdX;8sRtMxG?0V4V2_>EJ;(@_`W;&53tP55%|K z*3*G^Vnq?1%g(tG+dfZPJm1t~L@Z7nI+lKoq9veKC5}keqrx_Wn9dio9_n9uq-o1u zSM85Jf1$88pzhdMw)4K8^5?#{ERt<+mk(p#djD+mU$iGQ$xG2T<`KP@zx6BP^c!jw z#AL(-PKEsCG~YBF~j9wBsx*U~IEWqeOr zPu@;u=#8QEcu^r-xIFM*p9x@rM>omN+q2GMN1m{NXAxXSLcB^8>haU$VWXk+1slfe zbuM>#5GfXz6^Z>Ab_L!b7JFH*6P5Ape+Umf4Uqh!*Ud%M5OoaGv{)ESmk18kc8vQ) zAf*O3tvZZb#DRub#RUpedY*t`D=a89i2p33ed9q5`)S*G(0eg55G9od-TsZ`#)FQ` ze4?;6gIOQ+8&5t2U&1QmHe=tD;;~aFaR`#oQ^A2f5^ntfp;R&-MW_xq1d5~Tc%3d3qgk_JA7HVGRo;2iBspLLB%xC3`?EHC_ zXJ`Km@DH@39z3YR94^-g(dI!fLi>n_gdGo}OHE*0BGEuVDkQ?7UK(K%0YTu4FJY*v zC6S9-;S9rR!vyW0-)UWNg8|JDU6Bn2cP2EDRtX_M(SpG3N3;QtLN@mZFht%(0gWEj z_!$FJf%G#mCgl4Eapj zyvom0Ky3~AEIH*zW<+2Yhpb+=Ng#~Hc5M3tm_JeP(XZH5K&%*{GMJ?R6gr@+1G{3w zo%hkCZ(?aip1;$1t)o;21d>`?Kw~bx;xlBK3#103#WL!!d`;V`{+y)Iz_MAZbQAu) zpOqWMl@eZuLr#^h!YnK+0K`?Ay__OR-RWG0a@dzkqUe+^e8s9-gUn}1b?1xHFNC4M zC%upXCTj3gkN7x@tw(98saor>*;nqdZD_k2~$@^ouh0#v8=QU%lEjcE24)O_- z4}Xi<9HPq#9}vHRS$#2|yL5i$Xj`7!!cpZAvg|4u8Jv}Za$Pxa&*Uaz)HIMmD&$Dk zrwzBNnU_sMsu#`CFK>{e_dJ6Sg>7j|v8S#?vR*-0BWNsmR{suqr~e6CPtWJkOwbx5 zhsmP$-YYyv+=Q}nKr}IvQR$@LUimYxeY%(jWiKpN&r5JiXK|h4pE@00G$Jl9kEB~R zmeYa*X`>JC5}Tjz$61}{C^wmR1)LwpQ}N|9cYwfUc=>jZBC)B~=o~jmuLw)Fqn4by zdh)ZVEEUn!$G1=bC~?AfHXY;U=t+fN$fHP)$B0kq@6i@|#_Xr=lE9kZ>YKCnZrI1X zmiDlA&_{Vze)6>}59$mycx(`jhL?qS$bD83in{u`0H%W5{to^Q=A4(*Q$iSx(2mBu z;^E8HX_wdOl5kH;XVNbInAjXo@NH_Os65z7xR=;ec6rY#6Z;!JR~a75>t@${I2({H z>+xi2|9g(L`dzoA8n!@dsx`GW<4o65Ax#&IKKpf@;)|s0&;XTcR#xL!|u(UNk&jSO@Bj5A8D`hu%k~Yv;7XP(O^NY))Ah{z#-=hiT zhi}J?<74nNFNx|Z@4NVU{+j+iLb0EcRmY@VW#x1ZC>I7F!*GmUF}LsTjEFD#0FR%1 zdvvYMNch<1qiats`TNxh8$*}&z0^2=Kgk1|w)fV)Cl)fy<0hJ<#*EGO;K6?SaZ|UP z_RQlkTM7FznMT(F(`)AJ{3m;|H{0KyFnPx8$lX0!t2dwJ9~Y86`#P)rjcjsDo|#y@ zcg~H`OY=ToE%Dy?bCXz zP4|z6W^1+vCDCU(g!(KAT1J}D+cb$b2NTcAiG0Gwapbw;?u+)My~Y^J%1NP@MdR&k z_zXrd$)%xw^0dRY{3fO&+&Vgj`MGQH>V@8X@=dL}Z{vjBO`9d?wBc?kptEuHT5+v+fBne!m>XmT=aGNjns4MF*k$- zZ6--SRTr;Z08X29^Yi|LF9Qls-htiRKJG#ic%Rm5}yQeSO(6y5FZORQadmkoe=0-`X9AZALbF9P1doc5r=z)AomnnX#@V z$Fr03O@$dk3fZR(YbI?Um>k_w;CA8vm$tUcGEocJ}uW|=d#VP zv9p$P?COz=&0e!@u`dGE-D8y=*~;Fu@;b*hRkp>i3{J|pq-58)JvMvLkD7L_x4z!D zgWPLW9;za1mhg}VT`jzIa_CF9Df}pkUhpPgBbYcPNq`t z60~}q2ioocnDpotg(H!3bmkycGn-zwfRWZaj-A-74Q%6nEE2^~SqKqRoezTh@v&IR zsumt}fs}$ULUuXS_rasku(FA^FHnW zIa#6Xg!_hj*)J#yay+QZkC1LNOhn{9@}Rvo=pbqF9+^V>h#H4zb16LNw0F|v0J<)=8nWGNjK`@NL8 zDRjy|Ad3enig_Q0$F~pCI?*FnN?dqQn)=*YGkhZE`LeZ=5zbl{R*!^O2sf++!NP6j zz{5(-$9{?m2D(-e199M~n@CWT$Vk#5xif34F3zR8Rvw#Sw`|>3z4t*?NRv^6W@@GR zk+&5C&*MKmPi;83GjTBA?vU_SwsLkkEmYYEGrTXea1HIK+u9Q@`^OCy8sGZKsGs3> zG&L)Jel!2t#Ms^o*iF%t1F;}zM*G^OvJs;3&vZ!{ zjXp|r5V?b}i=hQ2K`KQR0H)QGW2_b zTVzz}z4p@B};{l5uLg!(zb3OI5J({GM39O=DK} z-wPePOT<;yJlK|HaW$UfKTRkkl*5JVh>O;&gU(LgFIl-K5BTlU_7}Xu?60jqf*ZKi z$Ad()zbE;jhe!$JY=R(yWre>+l<8^rDE*77^^#W(Bl5VO>}%V z{!Q;xc?#N+y!!NygXsZ&a>yH{njnWU7fUf@z_USWohfA&s1yPZ%_JYD#Axn8_tFH#48s}q>FauR-U%* zwtaX41R9fF&|0&j|Mrslmw+;-`xkQx}c$F0hWVyYQ_*e|h%O8oS0=%^%58y2-KD+4f*?vkln%_G> zMjl9Uf%&DTgqOSWpcM~7`BtQ1oJ1Wzer9hxJJxWF@XYjr(%LB2Ya@frV=IQVKBU|h z5|sT>hRJqYc{$;8j;r!rub&rl&)G)<40e97u1Hn z!*r|kHyH?dTA|Ydby;{E)&5et+W52gT~u~vV52D9iQo|{wbC5{7vL|I&*8oh@YTHB z3bk1fZc9h%XS5UFwmzbgoKB0;Ey@Mr2J}Thop1nTMVVSGaz6px`b~&iv4#J=Ri4bC ze2ATF0>eBNz^3gfP)YWVB&-97Eunl69RSrjx|Jm2KqGTSdqAzSEDDQQ*?n#SL#aD% zAu&~p$9jyTlRp%)DE$XPB@`PT&aH6bQ&wVC_XBll7>2_%7m(3QS-xo@{eXR?TX(?f zi{BfI<3*!yK4}9JA``L&JK{zgMMa(}8;Z3_i^AnRsM6}SB7O+=-8_)N z@A$orZ82^+eXg%nKcUiF)I<7hM>~sn@MF=_f@)FSTc!-mf#>gVjd@`f|s zbu88OwL_gh!>4QhwZ7>|Alub^a%zotn$tuqcN)H3a6>Uy+$zB<5EZBAI9rp_zqQVj zcdsYTW2-)d+A@OXH9JkWt#KX2R?7CW-uG<6%&mD3R!6q|m%{c2TYC4nw~YHxEUxqX z)?^;E-&%0tX&`xg=PniV2!$dEPTK2o2g`>-#1y3|2@gSW{#)O=l&BY5(?qy^y z{LmEWjk2~(=&gJ1G0|eC;ddgar;tl}*|2A^j-uL3Q=yi1Q?uPzR2!yV)+Kb&mnA-V z`nyq6#H`P(juT;YSx|9fp%Y`1pA2_KNV`&dGmrI{;Cr;j>9^+NR1< z2rWIL+v`_79_ZFwJ7IT$u>PNjvPbU)Y_PqUti7CkaGDasgBbcG6Vfp-Q+++^_Y72o zBKe771Hh6+lSPx>nHRm)g*$s&^ZQ6-O!lRu(2*tk^g}&@s%AF7$37)EZ2!TP+5c2e za)!FTy2K#@=hbr88>b|3faP zm!*AVij_;>$%C4=fC7x6J|X`?!($LY{5fqy>4+>pKH~>$xHmkpv%YPUrV*X4@B*A2 zWS|V!pp;bX?1Mm5mclxkGO5blk#uX|5jE+M@9{HI>_pD$)B%eA+$5Rx*a0BuT_koA zSJ0pQ6<+N)X#KGonTNS6+B$qfyp_RPVw-M{Pr;3HgfCr{E;;p~mwA*`wDsT>Vqcmoi^UTQS!n4K@YbxlXM6mr@V&AP4w4xwiPQmrmGvf8qqSX`j@N z-!kj-C`WvXZVqWTzM0edRG_xtG4vZTJ8q9=_+(Tlm7asQj@tMN6hlM*A863TiDP46x`2JHC$#m+TqZ_R^=pwomEa7 z$fCVGsL9trsRGYddW)GKMH3_=K|bpTJX^>Kph1&#^j;9x_4^s_W+oqq!~q~ujgc}? zByqxQ!4}{sxm6#m&_+H=aU?}}pwg2Zu<(rxANN-ycQ0`2d64n>6m0)V*V&i3uGwlN zO?Hyun7b^IwvbuTD$c}zIA`GJ5pr#;&W{Tg4HL>{&vm`I^ zpem1_&adB~%aHMCvO3u7Bvq9tRtCmSlnzouG_U$=eui0t#Er*87Q8+7+^tGvS)(;3{+3b5x? zw#Q%-1*y^ly14F-Mb2gc!37Igx4D|6<)EtV04j&;$%WLrU2y$Yq+u^B2SAdYaZJq@ zgQA@&#I8FbDi7yyGX^pVsIV1XisNA*f_P2CZ22rO34%{q_jwR=0}Y2RcYqoCp&cO( zn|RQ}9qc40delwybTvDvs3?%D%jY3Du-T{`jqKa4(?=lQa#t$T(bB=h{ zR%}Hz$m>V1Q)pU~px~V~b92yE2zg6n?rj>pYlyrdGBrJ-bCcNfHb(j zAJiKgT-bsUauN4aEg?yx4S3oK>>XIhK%a%L)P0%TS35hkX4am%F}u6|_|Q z5C^i9=iVmY)_TUzZTMrBK83mdZ4RbE^t)oSuR{;M$!?l%ecCy_?H##h|9fi`hgWYp@+ECGPAb*DEG_PdnYAYUmsO9&Ce40j9-V_ z@*=>w!gT7BNcu@&-PP_tVFAQT4Q!J5M}Vy)3t*@f&Gj zmT!Z22qpeT8J&e`is<>tT~#nO1MX8r?k($uM)g&2{nZuOzWw6&_JVzWmhUOjiPpWf zg1=%z*zrHp*r*LZcfsfIcY`9O!pn*(qJ%bqG$z`YB0*RlP~ESe>Tj@>)F@<2tJ? z;F`}o`LScbh9Xqg8@zR9Xs)?6_u?SAJBucFefIUD+D(?#B7qV{fEA{;)SJP^p~uo8 zi{>HA2fO~=nVY|EKX6U|Qiz57{gDSO??spgnS5c9B;cXs3b>QRu{Tvg#X3QLoJLng z%dBCJt})lC(X_quR}`Yqy!xvv#qYxND|11@(`Yk+J{+ZbFL<=jjo{B*m|sz1>VXZ9 z7)^{V*njx$I`?wjHn7cLbRD0RN&xlMvyT{OQvyp=MxsxkE6tw&aBL6>@U}W1R<}lp zC?q5`_S?HI)7_I|t3135@fMNNkDO=)e#7;lg$D6~zGk_jX(8pR$lB3^j-@g!L~gO+ zDd4A<9r@;rVW@!QnI*9?=tLM(n+NU2_fWntdnJZ@nRB63g!n4)?cA#lWA<4*OZogY z3RA_M#e>LqCW8S*C<%j-Z9+YSSqVT6j6Tl@~<5z+HK~z+*(ysaz6VGO#bLe^RyYHxMPU$FW*cz=g zXqHEc4k4-{QXk8+c#!%-puFp9R)KI|PLCX({lJfm2uI}GD?A3MWiuSz7Y=X<2LbOUkHfMtE5;ox3arsnaWke>(nU+1E)qzsO(;OJ|%?r@p_p2|FooKML zjxx$vB(X8)>R7dzO{JD?_3#R~FQZ7P3LCLOHEb$mS5|wMhs~C5rH2Dr$7=t5`!Np&-e_3TeTF?9JPYeapWpcM=0}b0BRA&OsWv!KtSM^4*N-w{F`cbsdx^N7 zSKV@>_gWg(`su7+pi7bxSfL)`X4gOhBp1|nIN$?yW7ZZTJIQmAMJc7wbLQ zy1}hd^Q47K>ygQvlKie?7T9R#9@|mOW@Pf^a&_0u{^>(-7G|NvrE8qMk5SE?bMXB7 z_!GAsnTYBz2KrQBx1F?&)3MEqBF-H~IXq~xS;%mddx{5@QaE$X^{K9?E#~WU&z+LQ z?WkX7jOd8zqa{4Zit}wRs5u z%W;|`zrKxx%yaVbQZIjpJ$*Pv6_W zMx`-C+>KHp0X=d0xVT{e?n__SgZsH_Ft-rH0ELyNuzbIv8C1%r6%ByLk27#L|$UT$C zwUVEfc;MH^=h~^DPsv}U4T!zsyRL-&uXtA|gWF^oEQHEa75(IA^4 zbu8}gUT18)K_UhdZ;<-z-<_BIi$D5LH-;(z(E139dZ;1~`mr0pykoWatRiR6L`pZg z4s57i8yxVipe!zH2Vl8fW$tVlUB$Vvgt%53QMH*3M`MX9Z}$e68yW6ll|4a8w}VTITXsy-dnmXt1!e|Fk3TMCYD*{c z6W$WD_a7%4{5~f(DBo%Ww~Rp&Js}%O2}-#XIOA~YrLXb1w%?O$CjUOjtF@*14^n-< zKG&@5Hh*7S9dsKEH33EnZnNa4+4_|uAkB^}_J8H2s+RPePsiuw<*(1DC^X!A3tMhJ zD%Vl-US#}DgJ8m{#`-sR*Goi6{t!1tz~QtTUR~4NkEQFQ z=2|VlS%@I+J63b&6BoLLaDyN z^67%WWW<%m-xexu9-cBHN0=E@)@1wa)1ewuKO8VT+(@=B4aG?vbE32sMrd5t*xg^d zUd*(LF%#W?~!|6zNx{eSQ+j8&TY*e&weK)&bC}*OFWisA#rot zV`6_$Sl(B{B*Cvl@xIdikr%<6$L3y~D{#_!q_J>@`Kt3IrrY~#q2rez{bQ{M=7aLu zMaw<4H%d`-my?M7UZweOS`Sy`+$5#9pGJf>Y&un%J)8l7z>rJ5)i{ptotl6{OTfN1(k=kf%AqYKD{)n zkH3-LC=rRk-R=Qp=Y1V*jyMoYLO@284$_Lom{C_%RPrsRF9D6EQn9S9H6SHiCeAvi z@9l-11<4=lwpdCUK@cw63cAqD@V6Dn9M5VrhKm0tc#3g(MGjFWuVDq73)-!0a8EH9Y92j0@O-av`^EHmzIbR}sck{bO2bKksy z8r=Zwt!(xL4!4}8%e^APgOCmOFx_vgA=4B)T->|R{;JdZ{nRCG?!1Rf(g%C^_kmXU+jxV6h9i_Jc2#Vb)s8m+<&ucvegtSq8_9 zf@LQd$CBfQXoDkDeFI>XM!xDrg{5d}9F~VUaGsT5p-MX-NpnBO{Q$%88N_{C8yPz~ zJqCALgHx*ck6p2|(!W?eQO6*2mj7b4?+hxugtnQ(a^=%#;L|WGL?n9qH0Eeu(?JE} zF&W+cv0d+Q0Ct!$iK2z9X4L`N88%XcnQy05<>(anHOT;fvNVBxpYb|9XB~zm3fJ_@ zqAS{IVjLD8JSuAehVY9ju}{E{R=}tFk%HBv*@)20fcWLc2=Wx9$$y(`XIg~OqTH=PcYU~^n+i>_46N+T`cZzz5hmt zo?}-pdzZX{RrfXJUSwjyRHyZo@25}d?=b(|cnbIKgd<0yetY${%mxGDQ~+D|#&@rO zG8G-DuJh_#mH6I;{Y1O4PN91qNA6sVrq4v&xKJgeB&A_w_MzcIjP#}}qPq8&9iUy? z-|}#>HN~sMz47N^7wbE4zFF~a3z}|dzG#H7UQ6nhtcucX6E$Mcn_8+}baPPaoyd8W zcT2jOO;dW(?~7ZQp-#P=%lSDA{dcHQ*=$t$4|irA1J(qGb)5(K%y4z=+Dy%WGmBFG zn==~~MnnJ^t&3s3laeBgLdC(VqsR2Ty=Yx_c#8pu(IsQGJ{)nM@$U3r*( z-&G#mt1}^j{WEeZF{ska%jC#TynV?0<#S|V?i^;-i9^U$aT+0StBK@Wn(K^ag9=;&F9H=g|zv?Mx#Vxmd_%L$x~4aChP@SshusuJog z^GXYb{IkCHj>fl*vKaD%=t?5J{z(7`ecL;LQ&^wr3zwOf>fB4#0SeI~@u|EpkUmdh zGWg6KsB%;orqp7Kut*GNachfU++Ys{^Ae2w61gIP8j_F{5b%++nYADwo3MU%>-hop z9I&7!Y5kp+s|hoHCsWCadk8Dy_+%D^b|Rdlupx3aNJ4`QtGzk$6h{#HRobSN)k72;6kgPO$W{*h&Vz=CZT_(-u5;uX3M+7-8q1N#{xPI8U(KaDuBajL579CL zoRVN_9UMYoFNFsbT>jL?SfiyXAOH+?nI3Bh9{JmtJH$cOJqV(9VlPlK=a-TlW&Wm7 z66?+fZw77}ti7#-g2FZ_SyB)Jys-s*PY88|k(3`WJ>oE+s=?}kN3H@%^uzh{1kN10 zCW6fD7bg;M#nE~=7QO?f90$+x0}5Kk;qmj?}R zDF>5cr7Gr&WwU=RkS(gLJBj{(a*|WKTUqITKEa&C+F?@#+G?bOXdy8I>i9|3IAaDtFCprN9a>^@uR13W*8)p(lkiPpoz@O$ z?InR%YXsXQff77U!es`fo51Qn@XMFF(VjC0Bb`vd;MlhJRF;Y>k}^6V#4V@H4tty# zRzcVKQ05pIj`_Y^n7!^SwhYTvBs~FW<~WvpeBtv92mJuN(EbSYMizSxsOKQiuVJY) z+%`u=W4df+rOvQGp#Ian?xDq@N3Gz=LB8h#rz!~h2(SLf4p-n<+HVF2Fcs8Aq0{_K z%wtXl?`Du{GfYKffhSfJ8F1y^d=1ZD%;(&iX$VFY$t;IoC->wr;a}VJe-)8}Yt3>w zP4Xu#%}pW@_L0T5&5RQfh$Y>#N6+vTP@<|jJa&w7!SQ6<05xKEml0wF7vf4UcGX55 z7{#j1lbaFE6OSceutPmnJ<_wOVDM0H4@lTfKK_+!1zS(+X=jnxVG~#u*k|mw%+b@x z0tT?AWv*N#z#;>@7_SN6?+Z5>{f_Sjz1u z7mp;7&Yw!2)Sq~=;P+hnW`4Ea1x`A;)CiT5MmBZya+l2E5y3b_<)y~UB69KQgYz7X zU0okWV)L>6%Ui)@hy(NM+e+#;{HNA94;-i}yZ%gXPrPEC{*RBggB?-XGav;r$O1*) zSU60+gMZw^u`V<*;ix=VM3k z0l_=w&pSu~X8!W@uNo=fpP3PNG5AtXHs^+Ku>~;D`{vk04skG4%J%fRJ=(M(348H> zRE|nB>@>4{VxCfv{#n?iF@O6Yv8qF3e|~P*Db@b1arc(_oX>ZfQ|@7~6x(W_wtMd2 zUVca%AdV1wNFA<*xo=nwJMY__ImCn9?sy%K$60i(WS69p)4;oczJ>jJd*_(aOiiYU z5x!1tzkitL0{on-^CUHEyj|Q)Su6Gta6H>Q#@6CC=4P;tv*|5`i?Q(7whx$pb|ntD zvUwg9G2hm9j>K7#a|T`64xBmrZ=kL<(G|JpLm3Q5AKcRek3KspAd2FMXY;Gt(BC&AunC=rcHqTohCRw5K|KXjE|FkJ&J zo5yn0-MP9&aYGn8j_Mj4WOwT<5fp-iu3->`@BBhgRjqc~A%=ycUDz=BhMB?e+nHUq zw2|jhw<#VwzbZuNs8@fpz8L2U+MLb)JpSCby=%^SkShn*Pr$um=-MW__%RHFi{UbQ z`yEegZ67~X{66PseA9G8k3ne6K9>5WF6v&9aHE)tZhM zjaxQu{VplAU0jWHF4&0m^!ciyTh1s1T+(sEjtc_lSv9y<9F^*4n1BP7>?~XT-7_@s z%8jFo#XJ3G#G1yKYkmPLnHU5hhKm8g=#5Li+V_!_^qRdD?P}fp;c&L_h~IvNUbx?r z<^M;6wIo_$$Uf}I4r%TpOe2%P;Vn{Y#<>T0lojaZi6yOkdvHe{3f(L;ZBlPBzD{RV%r!~EWDFLIFv@a3ao?7}n- z-B#)usH_FsrQJV;y0YN=$=){a5cmt{G~)G-t>RGW@Gy8xO`N+J$Aj*Y6VD|`N!Hk9 z_{@oq04=OxCY2-%qOMKpgXy=bkt%>QDdsSJFXmpUB!(#yj&hBFm~Hi6SmS1S@%V{p z44=azzzbOaq+pl^so0IkR_us|(C_3^vGT>>z?!BIm-B11hZ~HeQ#0*#4zttvEc%~e z+knAZ3s?+> z#5g!;R(L6#?vh)2IS{hBruY8k-o#h!bjPm(0;0RHE+z%!P@INzWPBi#3UB^7CtDxk zTo1CCo)?$3{3y#w{uQ!jXa(PIc>jyv0Qk~|TCpFu06gtCvESgbGuUAab~Sc-BghX` z?9@RPB61o}Zvws~rXdNgAxogbz+LP_M*kgJ!Bfe|8gbNIADnrny0CB{D%jR++#g&VJz?`1^_q>o9Kz*L#&X6CD=cb!}y_ujqWmv4QfNL z952q2!^{l&-!e=h2E7chtPM&oaaGBZ$1HB7+Y}074l~=#83Pm{#5I_>)*U}Lr;6V8 zmoLrIUc8*b+#dkEi_kh? zc@8!S^WijUST5i&xTD3tL)d8Vz--er4kiaJ)&y7`Mqvu58YR*%)0GPm7CsoD#f9DY z4%JWiS-pS*aMyW&`Xe(N3Ev4zz&X&-GLYry6oA6lB&{kqyE@|9)9P&n#ISuZX9dh! z31GHxKq4LuQ3V{vEP^ADd4|^>qprjiptI9sZ*=zy?+#f&dOwR#8aRVUa=D$#(SBMh|KYEKT9%P9ZMi(o9-jSg9quxBsH71a+@Rl#&hJTB)`V~JBITH?c!Dawlz>g>m zn80jW=>lpvRZsZlCub5 zuZP(v!1Yq-Z^&^n*iDxPK(&UO?M>`ihGFUk;J{h`dH(zXSA;1R4GN%wBRARr=L}5J z9V1}6=qvb-;g!%WnlPL3O9X|GAeuIJ6=5xQL7LxoJeofFX%ux;T?zMf@F$jBIRSD? znQwggr|}~oh03Rl*722}oBfxjFSV;lg7CLtaQKH01p(jnM>YG?#Mvl1jGhHryn6-> zA^)29ojgchPlCvn3ju)lCU|uc=zb%z8XmbsBcpt*$SOiEzQ_phTfpJ&PS>E+()e(JfMc zNc;0Ja)pNHit!6qWv?-a)h<#MR@gn^P3M__^zsC0=~dvJIJ*bC86V}AS&79OZGv>$ z2J`(X?{eY|J}#aL(SBQSYC$`BN7JUeMk{4HrWMWNnqpOU8iqITFpsyfUCby+I6C2W zn5bfvwSiFS-q<}y2%kK|ca&`bgMIkBrI<03R}&9?$}i-+eR$@UZ(P$gUwaE|HsQ>n zY~8mb>08@kI1YBcFWKi`Bh`CvHG6K`LEd$QVRPu%uBXH@p>FzyCT=ZuR^T+)Sf;WK zsTJaj;qZ7}Ykui+Im9TJ2VKTlj!zgqXIz9qLV;|zV0701 z9pL7u2^QM`ksE~PSl0Z1m?Y|fS0cs0ZfwjbNCpFbz+<&pvxRo|cZ#5MgbQF6DHO*3 zUrtt5oGYVGco1-X{*2OMZ)0gn#nBK>VJbZPh@|t$t6g7}f8>A~Yd`mDYf?(=#pX3% z*Oe;oG5B8d(5S?Ouri=-5><}Ip``#-e`_Gj7^%4y95Xu3^RvRbSPZgq&kvF6Tl#~A z0IH_BQDo9$H>Ix|AN@Lzj{8hSWbhzxY*a!Hr_*!M+r%&uOC8{)AzkjSOQWmE?8xvz z)Nu52-|*D_0C7f~C2@V3M!=Lw{`ZYbJUf z0G0px@ZV92&jNQ@IUN+_c#wH6m9spdA9elI9K|A+orFJk&~h8#!om$X-16R8Y4K|0CfAsX9ov8pXOosnCaKI&SmB2Uy}4po5J~>z zw?Y>q3y-<6K-b@Crf>=UGaM11!h>j zEW{HiCA}a2r&8t?!zp9CF+{cy_`Ngmx4jf`rh(v#(#3zvEOPicy%4=k3WNWcLu}i$ zJW%_40(J;9VI2rk%|-p3Ulm@}%h^DX0vUH5e+ut| zN+|w40O|cK@CDAW9pws~pn=HQXZ-(j1(e|Cf$NRyVD0BY&Q+0MZ<1!y@^2I{na6!h z>ATS@jfxY#%P*y0O&`GBZW=FLN0%pMbys`5a?E2K_4bv^d3!t0YPZ!2=#5O(@kx8z zgpKE<_QX4hTAsNSduF0O zT^F6@Le`Pc(th-OsiBf4OIn0HuZ=2kFds&HXRwN#2x^k?c5<4Y1(^kySKLD7*21#_ zw)nsJV8Sv`0zdwz6SnzhC;Zw6!`T0s2i+r~52Jil*-~fW|F}?x`zMrZum-NlBy&Sx z&dEa&+sW19EldA*zxCuVZ#?}cFA$R8GD@}lUREMV*|jhn^&svR`rb$RxCKfY!3&rW zgdOtz_0am)LpgfzK;3Sb$l1W5tj?!q1pydlWcY;!tnF5W<2!(O6C-z3?~`&EuC_ZD zclLRp244O}XykqNwuaL*v>4skwu>CI&afb~e|_HusvCBtv;LVI9`Ug+n~Lvxzut-W)KqM=t4H#NC6?46-q&tMCFntKQM> zE=lu*GMF``5v4~qsB??>lnKaPrCh-#Qp zZS^RssY&s83Vz-Ke$Ee+U4Z5~P7svDY zu*V>a2#7K`jo&+{ao^?hJ9uCgIkQcRuI`7?-wH4bbCZ=lb|C%FT9gDU-2iOE0)Cn# zLC=^erTOp8plB%K1T^CqL-8~@34d8vj-n+2kNN25ghClirBnilCazft1D-qxox0C} z(iOwG+#ok@Vg5ldo<0`kLHYr~MoH1Bm3B36u*?}cJff5|Kp`>oYtXI@{2uZoFhugB z-okB}*b$+09@INZ76n;<$ZV$VIw3Ta4=)q39X}|e2+_~GGsOzST-rpLVK%}2Mfu*G zC2qY7?8(osGSRY6Ff||iS{L!Di}?T5-jzl*m1S$3pinecK|rB2Dk=gZg^09@+;Tuf zp(8RWP(+Io1w^buWe{>zL?$JID1u6qPz)kOML-4(G6;$i1r$LbB#Z(=!gv#IuDyfR zU6%Ii*ZtOdZ>@f-f5Ql#TgGJZ)v*Vpt2i-_TbuKw1>#=Cr>D=a4=7JO}#E zgFvZZQk>>--)CQ)k@SS{PrnmB@g0iMZr%pTq+<-UQt_3bwrBWH>C7Q#tb@*n8imaY#mHC>6e!-WabNY`rODEN%5VLMLi9YVnsf_fDn8(Ja4UEC>&iE1KR zhc{rjb%*XopgpswI0FzA;BC1WdUw*W>BwdcVuo^SaBb;)U0mi^bpT-c(cU8PwRZiD z@^J)Gw;X_-vauF+a-PZNcn0P;N9M#nwhFNW1AkJ2+EN#790a{B7W}#=vxFwf7B z2w^Kvbl{0Tax9vIb~=g7I~0J$?32q<304PE2^Qm0{gK*LaCud8;0#dR#ef&j@&%~u z^_Gaxo+8f_$C*&YppGhZTW@NAkV~KIFv!6bLnU3%-FWoa74(q<{w><+EHa*Wc?t#7 z;RQ4Bs>CI-VGDX&B{Hft6I7AXU!H+F3aT8x4?MS%(f#ln9gCQs**YLm3$*&!8DCvS zwwosF?02%l&sy0O!yI;Uruz*%E-(%Cx{jUEIOitOn3H^d(;fBQWvo1k$Cinho;p2KMgUa=f+PRR8qK0$VMIi{P{J%zRji<@wjP z2~*m0+z%FHkLW~<@}hXzsuzSA#ou_02ApB}tfYRsVQE92lE29^BY_>3LekhyrZ!p! zxK#A{T{YUK+@5~l+Kl|#=Nekf5o@cW{oBC+RtIzTR4)3|l)eCM0+a90_Bk2 zIR&N_?akzIu=QqrBTa-OO#`m6&qFYXt^DzULoHh7T8UnQgd#_**@+0q`+wjAY8@dq zBU}B;D(H07(4F9K=uGJkg zB)(lTz8)6XxSWQqMTM*{K5Ne3gy$eaaQL1m;2UrC79m0{*tE-RveVb~vtY3`|WP8Et(B*%`){ z56=3N2J1eiqZd;|)3dX{xhw|5Y7u?zOIkit$k%TeAWlBi0i^{SIfU}X1JRs8N=LdZ zmPu^=|Frr6P-zLw{T!h$QE?8Oz3r{+dgkZ+%YCQ;j8P`!1GTa6>wc`Qw?&IQS~Tjs zMx2($Iry3-D%$U=TCqkW_9x#gXWz*Dc?XTQ?Xc5dcJINfS5D%ntCSFN`SQ&=PfA|8 z<5U*ZaqXk!0!(N(Tsi2cbb7W2NZLj#U}<#`X;3NtJdi<)J)Adw1r!HBk`jZ6p+Q<2 z=-T1X2(Sz$!>&sP(tLg5gs1Ko$rtuN*2YnQX!84*dwChP4>rIpDh4|-IeUmnC@OrA z)1EK8l1i&bB<4TUUf7|goG)P|>7U|9v`VOAGDtlej1Y@37$wG+T(>q;k^6i9KCfd_ z*Mr{yg%O8#MnIt(ABdp88Y9%uCsa7A^FX0Cal=JwghijZG#?@dWkZ}V6{@jdomUN< z!!m{16^GB(Ahtk?k2%tn3hK<4EFp_lwQUV13emolhoua#u^c80(XR)KKztW@&_>6I z*lMGWwD=*b{pDth->?&`O@F<+<$}tk#@NQ78Y{2mx2@(j#cb)=O-<8`wrklKP^-SJ z_L5}fnnDdPkye=|;o8V0)6j{4zlkbx(8-~ zw6iVHQZyT!8X^ajXAPK*wvVxE0t1Nj$isXmp4gqPvGrQwSZYJqjVZssi5iYSe~mH! z*aoO>2&j@dN4Yp@q9;{ZPp{SEpr<%T^cgYa55+%X(PyP_rzemzQ40ivkL6k5YREqC zS#>!r{z=9z4;oY`+|ITUGh2?3L=I)VVFOPNMgfr^TY5)4)S?U-^zyLpWPM}>fyX=e z>Xwmy2kn=I_31cwV67TY^NF3;Wx3oams+IG5BR zq^SF?U@U^u?Iuznl-$7NN)HvlfX4lpWe8>fViJ*cWcrxXL$D-o zFw1DzN?8*Yp*j$Wbnl1#q?7{h$hel2(Nl*)AL}Q^^%5;<7VVcR5sN_7V|ln81rp&P zW}A>Oj&PrBpw91Af*cux`uPg~d>4!iYr<@7!RQDQFYPjeyeHHj8a9-nP33J+k?rSw z`rNY~@jxmcw$E>wm^X)i2!mKL`Ze16)SJ*@d_JkHV-cpA=3=#`;J0oJw`}I| zGW#uWcf1}sKK|pTIa&IPFDlNPH77cuGAW{F_F226Tbq;H2eb|+Md*Y>R`Mh|xD(ja z1PlDV(M15G9RyfMApSiRWAi1hW_mib((u?wWNZl>ZZr3Np2@A41lG^rG7VsxN84-t z>ktuJ+!H9vDUxB%aQX$l79MdEt}%cgX1LFOfkc|C>*Yy(Kgcy$1u^EqiN4{lD&;Eqca=i!-~x1FKHMX+ z_0r?o29H=4^QIGoRy$LR57z-h4kT^u1mKR1I<Z^vjnF>dzIlpz}X1uL@V`38d)6=!C z$y8omJtghvpcbzBb~J$5R?(+-tNG2wT8~8ed`8)hByGLL))~JRx7|Ktet~EpWd?=H zx$=VrN_qiNRaZb1CjS{pA8hhEm8h#(W}TYR!67EsaW?U}*n&fli&v&Ee0QtZx%u6( zJcF!&*gU2W^?uSYfq}260;-Z{H)IJ*46jhWxfhMYqFkPUi5LF1wnVKRPkTyF zeYN1!`g3%qz*IhkvU*qUgU0aP;$Ph!_9F4(`bn;P9rPr3>BAbePYP4T?C3VHkJn3& zxGcro)7+J_owkf!s_4J`*!eiQS8T6*z@dw9_C^M((2&5`C3AICf`X~(yOVK`)fYq= zR@Xfg{`&2f7D^ozzwZL^HaLu@zcOmNWG4%ywr5cJQgg`WqXVUA$d;CQpSH?bHny-S zt;X##HzJpqSI6n51bvXBQ*a$EVLzP5-}erTgW<`yy?Xq7zpcNA)}<`tGdYG33G9V< zi~=f5dt18pG8~UykSBsTk$reY{@U1v9^*Lj&qaneDdJEXoL}p}K;6=!_G+DbL8z>i zowj-Y>U~-2$)CehvWk6yN{i*Y#Husf54nHfK4p zs_$P8O>}nk?^C$_e#A6K47AfY(wkl;G=Y~U|Jb=BakDYH_xpM7tb=I<%j}o=^n~^A z3aDh=OsGF|IQyC5(-ko_Gc7pF3JHB`eAmxQb>>*6u^iIir%^JSv$Y>FBj=`{KZqKh);7orVIZ_zcu{eOPx~I#0=}|3!DKBk*u$(1M zkwLEQ>F57T$o0JDWQWnak4&tePpx!~S);OYYx16Sg;QVkd2Cx|NqPhq*z0j+)(&Yn zq5u45_v>t`u&u0~wBo`qti!d*h~e(7$H`CLPjFQy4?rLLp)cS1`ZYUdV; zC?`uhr-h$d6 zUyFh@ackzco|>feo$ZMYQ>V~kdAWf8crT#N^vGMweRQR}XhU-4cBnqt<1k~|2s`MG z<(nwG-g6yult@?G>xU^yqW;}iTJ7!1ytFe_S|aQlhJxaKz0G+}lu(hvG`G2K2A#WS zHo1tLGWs(|Sq+B@-F1+>pe;PJ?#44vOvsI{FAg20FB15+py-(bQqs1d5-wKk)fr z@xGVl_(nhUg$>9&GQJ0*92fN^3SeGv#%&}^KWn}UE!hW-IQZ*eT##UhR!U=a&V zkZx_zkQC`{v}{|PbPBkNJoo_*<x67Ni(B zJnv%H5&roZE0-!R)~)z4MXGObYQGz5pS=8~}Roxng8hXu?*lDt1{YmP5< zM5NP>@Nh+{h~4EDfe4gKiBn8s5(Ll`@XhJy3xoh}G%{Zrf7+Dw>8jNaWt&k%ihNi% z>Ie88D!KQ;#3EZbras64hu>5Gv&O&eh<~p&zJ;jcK|L*jt9oUh#US!p7G}^P6*T9aG)uoNsb?vi6#G`i~4OTUl8(iPO?Zvc=<+- z!h$P)P4d3OzLU&%XyiR&PNvIaoY5TY protected String getAbsoluteFileName(String configFileName) { return serverRootDir + File.separator + CSPACE_DIR_NAME - + File.separator + CONFIG_DIR_NAME + + File.separator + CONFIG_DIR_PATH + File.separator + configFileName; } @@ -155,6 +155,6 @@ public abstract class AbstractConfigReaderImpl protected String getConfigRootDir() { return serverRootDir + File.separator + CSPACE_DIR_NAME - + File.separator + CONFIG_DIR_NAME; + + File.separator + CONFIG_DIR_PATH; } } diff --git a/services/config/src/main/java/org/collectionspace/services/common/config/ConfigReader.java b/services/config/src/main/java/org/collectionspace/services/common/config/ConfigReader.java index 74bf767d2..0ba485543 100644 --- a/services/config/src/main/java/org/collectionspace/services/common/config/ConfigReader.java +++ b/services/config/src/main/java/org/collectionspace/services/common/config/ConfigReader.java @@ -31,7 +31,9 @@ import java.io.File; public interface ConfigReader { final public static String CSPACE_DIR_NAME = "cspace"; - final public static String CONFIG_DIR_NAME = "config" + File.separator + "services"; + final public static String CONFIG_DIR_PATH = "config" + File.separator + "services"; + final public static String RESOURCES_DIR_NAME = "resources"; + final public static String RESOURCES_DIR_PATH = CSPACE_DIR_NAME + File.separator + CONFIG_DIR_PATH + File.separator + RESOURCES_DIR_NAME; /** * getFileName - get configuration file name diff --git a/services/media/client/src/main/java/org/collectionspace/services/client/MediaClient.java b/services/media/client/src/main/java/org/collectionspace/services/client/MediaClient.java index 7943c13df..0a28ffc57 100644 --- a/services/media/client/src/main/java/org/collectionspace/services/client/MediaClient.java +++ b/services/media/client/src/main/java/org/collectionspace/services/client/MediaClient.java @@ -78,8 +78,8 @@ public class MediaClient extends AbstractCommonListPoxServiceClientImpl createMediaAndBlobWithUri(PoxPayloadOut xmlPayload, String blobUri) { - return getProxy().createMediaAndBlobWithUri(xmlPayload.getBytes(), blobUri); + public ClientResponse createMediaAndBlobWithUri(PoxPayloadOut xmlPayload, String blobUri, boolean purgeOriginal) { + return getProxy().createMediaAndBlobWithUri(xmlPayload.getBytes(), blobUri, purgeOriginal); } /** diff --git a/services/media/client/src/main/java/org/collectionspace/services/client/MediaProxy.java b/services/media/client/src/main/java/org/collectionspace/services/client/MediaProxy.java index d91b08ecb..9d39f503e 100644 --- a/services/media/client/src/main/java/org/collectionspace/services/client/MediaProxy.java +++ b/services/media/client/src/main/java/org/collectionspace/services/client/MediaProxy.java @@ -39,5 +39,6 @@ public interface MediaProxy extends CollectionSpaceCommonListPoxProxy { @Produces("application/xml") @Consumes("application/xml") ClientResponsecreateMediaAndBlobWithUri(byte[] xmlPayload, - @QueryParam(BlobClient.BLOB_URI_PARAM) String blobUri); + @QueryParam(BlobClient.BLOB_URI_PARAM) String blobUri, + @QueryParam(BlobClient.BLOB_PURGE_ORIGINAL) boolean purgeOriginal); } diff --git a/services/media/client/src/test/java/org/collectionspace/services/client/test/MediaServiceTest.java b/services/media/client/src/test/java/org/collectionspace/services/client/test/MediaServiceTest.java index b6c3318d2..d491d90d6 100644 --- a/services/media/client/src/test/java/org/collectionspace/services/client/test/MediaServiceTest.java +++ b/services/media/client/src/test/java/org/collectionspace/services/client/test/MediaServiceTest.java @@ -206,11 +206,12 @@ public class MediaServiceTest extends AbstractPoxServiceTestImpl mediaRes = client.createMediaAndBlobWithUri(multipart, PUBLIC_URL_DECK); + ClientResponse mediaRes = client.createMediaAndBlobWithUri(multipart, PUBLIC_URL_DECK, true); // purge the original String mediaCsid = null; try { assertStatusCode(mediaRes, testName); mediaCsid = extractId(mediaRes); +// allResourceIdsCreated.add(mediaCsid); // Re-enable this and also add code to delete the associated blob } finally { if (mediaRes != null) { mediaRes.releaseConnection(); diff --git a/services/media/service/src/main/java/org/collectionspace/services/media/MediaResource.java b/services/media/service/src/main/java/org/collectionspace/services/media/MediaResource.java index 0afbf1717..eaa97f7c0 100644 --- a/services/media/service/src/main/java/org/collectionspace/services/media/MediaResource.java +++ b/services/media/service/src/main/java/org/collectionspace/services/media/MediaResource.java @@ -124,9 +124,11 @@ public class MediaResource extends ResourceBase { Response response = null; try { - ServiceContext ctx = createServiceContext(BlobClient.SERVICE_NAME, (PoxPayloadIn)null); // The blobUri argument is our payload + MultivaluedMap queryParams = ui.getQueryParameters(); + ServiceContext ctx = createServiceContext(BlobClient.SERVICE_NAME, + queryParams); BlobInput blobInput = BlobUtil.getBlobInput(ctx); // the blob doc handler will look for this in the context - blobInput.createBlobFile(blobUri); + blobInput.createBlobFile(blobUri); // The blobUri argument is our payload response = this.create((PoxPayloadIn)null, ctx); // 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 } catch (Exception e) { throw bigReThrow(e, ServiceMessages.CREATE_FAILED); @@ -136,7 +138,7 @@ public class MediaResource extends ResourceBase { } /* - * 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. + * 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. * (non-Javadoc) * @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) */ @@ -153,7 +155,7 @@ public class MediaResource extends ResourceBase { MultivaluedMap queryParams = ui.getQueryParameters(); String blobUri = queryParams.getFirst(BlobClient.BLOB_URI_PARAM); if (blobUri != null && blobUri.isEmpty() == false) { - result = createBlobWithUri(resourceMap, ui, xmlPayload, blobUri); + result = createBlobWithUri(resourceMap, ui, xmlPayload, blobUri); // uses the blob resource and doc handler to create the blob String blobCsid = CollectionSpaceClientUtils.extractId(result); queryParams.add(BlobClient.BLOB_CSID_PARAM, blobCsid); // Add the new blob's csid as an artificial query param -the media doc handler will look for this } @@ -180,7 +182,7 @@ public class MediaResource extends ResourceBase { ServiceContext ctx = createServiceContext(BlobClient.SERVICE_NAME, input); BlobInput blobInput = BlobUtil.getBlobInput(ctx); blobInput.createBlobFile(blobUri); - response = this.create(input, ctx); + response = this.create(input, ctx); // calls the blob resource/doc-handler to create the blob // // Next, update the Media record to be linked to the blob // -- 2.47.3