diff --git a/api/src/main/java/org/openmrs/module/sync/SyncComplexObsUtil.java b/api/src/main/java/org/openmrs/module/sync/SyncComplexObsUtil.java new file mode 100644 index 00000000..a7e13c67 --- /dev/null +++ b/api/src/main/java/org/openmrs/module/sync/SyncComplexObsUtil.java @@ -0,0 +1,121 @@ +/** + * The contents of this file are subject to the OpenMRS Public License + * Version 1.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * http://license.openmrs.org + * + * Software distributed under the License is distributed on an "AS IS" + * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the + * License for the specific language governing rights and limitations + * under the License. + * + * Copyright (C) OpenMRS, LLC. All Rights Reserved. + */ +package org.openmrs.module.sync; + +import java.io.File; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.openmrs.GlobalProperty; +import org.openmrs.api.GlobalPropertyListener; +import org.openmrs.api.context.Context; +import org.openmrs.util.OpenmrsConstants; +import org.openmrs.util.OpenmrsUtil; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; + +/** + * Utility class for working with complex obs + * This is implemented as a GlobalPropertyListener and ApplicationContextAware in order to ensure that global property + * values can be accessed reliably without the need to hit the database, as these are accessed by the Interceptor and + * can cause issues by forcing a flush upon query. + */ +public class SyncComplexObsUtil implements GlobalPropertyListener, ApplicationContextAware { + + private static final Log log = LogFactory.getLog(SyncComplexObsUtil.class); + + public static final String GP_NAME = OpenmrsConstants.GLOBAL_PROPERTY_COMPLEX_OBS_DIR; + public static final String DEFAULT_VAL = "complex_obs"; + public static String COMPLEX_OBS_DIR = DEFAULT_VAL; + + public static final String VALUE_COMPLEX = "valueComplex"; + public static final String COMPLEX_DATA = "complexData"; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + COMPLEX_OBS_DIR = Context.getAdministrationService().getGlobalProperty(GP_NAME, DEFAULT_VAL); + } + + @Override + public boolean supportsPropertyName(String s) { + return GP_NAME.equals(s); + } + + @Override + public void globalPropertyChanged(GlobalProperty globalProperty) { + log.debug("Global property <" + globalProperty.getProperty() + "> changed"); + COMPLEX_OBS_DIR = globalProperty.getPropertyValue(); + } + + @Override + public void globalPropertyDeleted(String s) { + log.debug("Global property <" + s + "> deleted"); + COMPLEX_OBS_DIR = DEFAULT_VAL; + } + + /** + * Adapted from AbstractHandler + */ + public static File getComplexDataFile(String valueComplex) { + File ret = null; + try { + if (StringUtils.isNotBlank(valueComplex)) { + String[] names = valueComplex.split("\\|"); + String filename = names.length < 2 ? names[0] : names[names.length - 1]; + File dir = OpenmrsUtil.getDirectoryInApplicationDataDirectory(COMPLEX_OBS_DIR); + if (!dir.exists()) { + dir.mkdirs(); + } + ret = new File(dir, filename); + } + } + catch (Exception e) { + log.warn("Error trying to retrieve complex data file for obs: " + valueComplex, e); + } + return ret; + } + + public static String getComplexDataEncoded(String valueComplex) { + String ret = null; + try { + File complexObsFile = getComplexDataFile(valueComplex); + if (complexObsFile != null && complexObsFile.exists()) { + log.debug("Found a complex obs file at: " + complexObsFile); + byte[] fileBytes = FileUtils.readFileToByteArray(complexObsFile); + ret = Base64.encodeBase64String(fileBytes); + } + } + catch (Exception e) { + log.warn("Error trying to retrieve complex data for obs: " + valueComplex, e); + } + return ret; + } + + public static void setComplexDataForObs(String valueComplex, String encodedData) { + try { + File complexObsFile = getComplexDataFile(valueComplex); + if (complexObsFile != null && StringUtils.isNotBlank(encodedData)) { + byte[] bytes = Base64.decodeBase64(encodedData); + FileUtils.writeByteArrayToFile(complexObsFile, bytes); + } + } + catch (Exception e) { + log.warn("Error writing complex data for obs: " + valueComplex, e); + } + } +} diff --git a/api/src/main/java/org/openmrs/module/sync/api/db/hibernate/HibernateSyncInterceptor.java b/api/src/main/java/org/openmrs/module/sync/api/db/hibernate/HibernateSyncInterceptor.java index 35abffd3..4b953cde 100644 --- a/api/src/main/java/org/openmrs/module/sync/api/db/hibernate/HibernateSyncInterceptor.java +++ b/api/src/main/java/org/openmrs/module/sync/api/db/hibernate/HibernateSyncInterceptor.java @@ -58,6 +58,7 @@ import org.openmrs.PersonAttributeType; import org.openmrs.User; import org.openmrs.api.context.Context; +import org.openmrs.module.sync.SyncComplexObsUtil; import org.openmrs.module.sync.SyncException; import org.openmrs.module.sync.SyncItem; import org.openmrs.module.sync.SyncItemKey; @@ -618,9 +619,16 @@ private void addProperty(HashMap values, OpenmrsObje Normalizer n; String propertyTypeName = propertyType.getName(); if ((n = SyncUtil.getNormalizer(propertyTypeName)) != null) { - // Handle safe types like - // boolean/String/integer/timestamp via Normalizers - values.put(propertyName, new PropertyClassValue(propertyTypeName, n.toString(propertyValue))); + // Handle safe types like boolean/String/integer/timestamp via Normalizers + String strValue = n.toString(propertyValue); + values.put(propertyName, new PropertyClassValue(propertyTypeName, strValue)); + // If there is a valueComplex defined, ensure complexData is also added to the sync item + if (entity instanceof Obs && SyncComplexObsUtil.VALUE_COMPLEX.equalsIgnoreCase(propertyName)) { + String encoded = SyncComplexObsUtil.getComplexDataEncoded(strValue); + if (encoded != null) { + values.put(SyncComplexObsUtil.COMPLEX_DATA, new PropertyClassValue("byte[]", encoded)); + } + } } else if ((n = SyncUtil.getNormalizer(propertyValue.getClass())) != null) { values.put(propertyName, new PropertyClassValue(propertyValue.getClass().getName(), n.toString(propertyValue))); } else if (propertyType.isCollectionType() && (n = isCollectionOfSafeTypes(entity, propertyName)) != null) { diff --git a/api/src/main/java/org/openmrs/module/sync/api/impl/SyncIngestServiceImpl.java b/api/src/main/java/org/openmrs/module/sync/api/impl/SyncIngestServiceImpl.java index 081faebf..b1b66d21 100644 --- a/api/src/main/java/org/openmrs/module/sync/api/impl/SyncIngestServiceImpl.java +++ b/api/src/main/java/org/openmrs/module/sync/api/impl/SyncIngestServiceImpl.java @@ -13,11 +13,18 @@ */ package org.openmrs.module.sync.api.impl; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + import org.apache.commons.lang.exception.ExceptionUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.hibernate.collection.internal.PersistentSet; import org.openmrs.Concept; +import org.openmrs.Obs; import org.openmrs.OpenmrsObject; import org.openmrs.PatientIdentifier; import org.openmrs.Person; @@ -28,6 +35,7 @@ import org.openmrs.api.context.Context; import org.openmrs.api.db.SerializedObject; import org.openmrs.module.ModuleUtil; +import org.openmrs.module.sync.SyncComplexObsUtil; import org.openmrs.module.sync.SyncConstants; import org.openmrs.module.sync.SyncItem; import org.openmrs.module.sync.SyncItemState; @@ -49,14 +57,9 @@ import org.openmrs.util.OpenmrsConstants; import org.openmrs.util.OpenmrsUtil; import org.springframework.transaction.annotation.Transactional; +import org.w3c.dom.Node; import org.w3c.dom.NodeList; -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - @Transactional public class SyncIngestServiceImpl implements SyncIngestService { @@ -535,15 +538,32 @@ private OpenmrsObject processOpenmrsObject(OpenmrsObject o, SyncItem item, Strin //if we are doing insert/update: //1. set serialized props state //2. force it down the hibernate's throat with help of openmrs api + + // If this node contains complexData, keep track of this separately to save rather than set on obs + String complexData = null; + for ( int i = 0; i < nodes.getLength(); i++ ) { try { - log.debug("trying to set property: " + nodes.item(i).getNodeName() + " in className " + className); - SyncUtil.setProperty(o, nodes.item(i), allFields); + Node node = nodes.item(i); + String nodeName = node.getNodeName(); + log.debug("trying to set property: " + nodeName + " in className " + className); + if (o instanceof Obs && "complexData".equalsIgnoreCase(nodeName)) { + complexData = node.getTextContent(); + } + else { + SyncUtil.setProperty(o, node, allFields); + } } catch ( Exception e ) { log.error("Error when trying to set " + nodes.item(i).getNodeName() + ", which is a " + className, e); throw new SyncIngestException(e, SyncConstants.ERROR_ITEM_UNSET_PROPERTY, nodes.item(i).getNodeName() + "," + className + "," + e.getMessage(), itemContent,null); } } + + // If this is an Obs and complex data was found in the sync record, save this complex data + if (o instanceof Obs && complexData != null) { + Obs obs = (Obs) o; + SyncComplexObsUtil.setComplexDataForObs(obs.getValueComplex(), complexData); + } // now try to commit this fully inflated object try { diff --git a/api/src/test/java/org/openmrs/module/sync/SyncObsTest.java b/api/src/test/java/org/openmrs/module/sync/SyncObsTest.java index 227f0170..9c400f00 100644 --- a/api/src/test/java/org/openmrs/module/sync/SyncObsTest.java +++ b/api/src/test/java/org/openmrs/module/sync/SyncObsTest.java @@ -13,14 +13,35 @@ */ package org.openmrs.module.sync; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.InputStream; +import java.util.Date; import java.util.List; -import org.junit.Assert; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; import org.junit.Test; import org.openmrs.Obs; +import org.openmrs.api.AdministrationService; +import org.openmrs.api.ConceptService; +import org.openmrs.api.LocationService; import org.openmrs.api.ObsService; +import org.openmrs.api.PersonService; import org.openmrs.api.context.Context; import org.openmrs.module.sync.api.SyncService; +import org.openmrs.obs.ComplexData; +import org.openmrs.obs.handler.BinaryDataHandler; +import org.openmrs.obs.handler.ImageHandler; +import org.openmrs.util.OpenmrsClassLoader; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @@ -28,6 +49,12 @@ * Testing syncing of the {@link Obs} object */ public class SyncObsTest extends SyncBaseTest { + + @Autowired @Qualifier("personService") PersonService ps; + @Autowired @Qualifier("obsService") ObsService os; + @Autowired @Qualifier("conceptService") ConceptService cs; + @Autowired @Qualifier("locationService") LocationService ls; + @Autowired @Qualifier("adminService") AdministrationService as; @Override public String getInitialDataset() { @@ -58,7 +85,7 @@ public void runOnChild() throws Exception { List records = ss.getSyncRecords(); SyncRecord record = records.get(records.size() - 1); SyncItem item = record.getItems().toArray(new SyncItem[] {})[1]; - Assert.assertTrue(item.getContent().contains("testing the voiding process")); + assertThat(item.getContent(), containsString("testing the voiding process")); uuid = newlySavedObs.getUuid(); // we'll check the new obs on the other side for this uuid } @@ -73,9 +100,129 @@ public void runOnParent() throws Exception { Obs voidedObs = os.getObs(3); // this is the old obs that was edited and hence voided // voidReason should be ".... (new obsId: 5)" - Assert.assertTrue(voidedObs.getVoidReason().equals("testing the voiding process")); + assertThat(voidedObs.getVoidReason(), is("testing the voiding process")); } }); } - + + @Test + @Transactional(propagation = Propagation.NOT_SUPPORTED) + public void shouldSyncComplexObsImage() throws Exception { + + final Date obsDate = new Date(); + + runSyncTest(new SyncTestHelper() { + + Obs childObs; + byte[] childComplexData; + + public void runOnChild() throws Exception { + String imgFilePath = "ComplexObsTestImage.png"; + BufferedImage img; + InputStream in = null; + try { + in = OpenmrsClassLoader.getInstance().getResourceAsStream(imgFilePath); + img = ImageIO.read(in); + } + finally { + IOUtils.closeQuietly(in); + } + + Obs complexObs = new Obs(); + complexObs.setPerson(ps.getPerson(2)); + complexObs.setObsDatetime(obsDate); + complexObs.setLocation(ls.getLocation(1)); + complexObs.setConcept(cs.getConcept(8473)); // Concept of type Complex with ImageHandler + complexObs.setComplexData(new ComplexData("ComplexTest.png", img)); + + childObs = os.saveObs(complexObs, "Test sync of complex text obs"); + + File complexObsFile = null; + try { + complexObsFile = ImageHandler.getComplexDataFile(childObs); + assertThat(complexObsFile, notNullValue()); + assertThat(complexObsFile.exists(), is(true)); + childComplexData = FileUtils.readFileToByteArray(complexObsFile); + assertThat(childComplexData.length > 0, is(true)); + } + finally { + FileUtils.deleteQuietly(complexObsFile); + } + } + + public void runOnParent() throws Exception { + Obs parentObs = os.getObsByUuid(childObs.getUuid()); + assertThat(parentObs, notNullValue()); + assertThat(parentObs.getValueComplex(), is(childObs.getValueComplex())); + File complexObsFile = ImageHandler.getComplexDataFile(parentObs); + assertThat(complexObsFile, notNullValue()); + assertThat(complexObsFile.exists(), is(true)); + byte[] parentComplexData = FileUtils.readFileToByteArray(complexObsFile); + assertThat(parentComplexData.length > 0, is(true)); + assertThat(parentComplexData, is(childComplexData)); + FileUtils.deleteQuietly(complexObsFile); + } + }); + } + + @Test + @Transactional(propagation = Propagation.NOT_SUPPORTED) + public void shouldSyncComplexObsBinaryData() throws Exception { + + final Date obsDate = new Date(); + + runSyncTest(new SyncTestHelper() { + + Obs childObs; + byte[] childComplexData; + + public void runOnChild() throws Exception { + String imgFilePath = "org/openmrs/module/sync/include/large-binary-file.pdf"; + byte[] data; + InputStream in = null; + try { + in = OpenmrsClassLoader.getInstance().getResourceAsStream(imgFilePath); + data = IOUtils.toByteArray(in); + } + finally { + IOUtils.closeQuietly(in); + } + + Obs complexObs = new Obs(); + complexObs.setPerson(ps.getPerson(2)); + complexObs.setObsDatetime(obsDate); + complexObs.setLocation(ls.getLocation(1)); + complexObs.setConcept(cs.getConcept(9473)); // Concept of type Complex with BinaryDataHandler + complexObs.setComplexData(new ComplexData("large-binary-file.pdf", data)); + + childObs = os.saveObs(complexObs, "Test sync of complex text obs"); + + File complexObsFile = null; + try { + complexObsFile = BinaryDataHandler.getComplexDataFile(childObs); + assertThat(complexObsFile, notNullValue()); + assertThat(complexObsFile.exists(), is(true)); + childComplexData = FileUtils.readFileToByteArray(complexObsFile); + assertThat(childComplexData.length > 0, is(true)); + } + finally { + FileUtils.deleteQuietly(complexObsFile); + } + } + + public void runOnParent() throws Exception { + Obs parentObs = os.getObsByUuid(childObs.getUuid()); + assertThat(parentObs, notNullValue()); + assertThat(parentObs.getValueComplex(), is(childObs.getValueComplex())); + File complexObsFile = ImageHandler.getComplexDataFile(parentObs); + assertThat(complexObsFile, notNullValue()); + assertThat(complexObsFile.exists(), is(true)); + byte[] parentComplexData = FileUtils.readFileToByteArray(complexObsFile); + assertThat(parentComplexData.length > 0, is(true)); + assertThat(parentComplexData, is(childComplexData)); + FileUtils.deleteQuietly(complexObsFile); + } + }); + } + } diff --git a/api/src/test/resources/org/openmrs/module/sync/include/SyncCreateTest-openmrs-2.3.xml b/api/src/test/resources/org/openmrs/module/sync/include/SyncCreateTest-openmrs-2.3.xml index 3e46c437..d4c1003f 100644 --- a/api/src/test/resources/org/openmrs/module/sync/include/SyncCreateTest-openmrs-2.3.xml +++ b/api/src/test/resources/org/openmrs/module/sync/include/SyncCreateTest-openmrs-2.3.xml @@ -52,6 +52,7 @@ + @@ -68,6 +69,8 @@ + + @@ -91,6 +94,8 @@ + + @@ -103,9 +108,14 @@ + + + + + @@ -134,7 +144,6 @@ - @@ -281,6 +290,7 @@ + diff --git a/api/src/test/resources/org/openmrs/module/sync/include/large-binary-file.pdf b/api/src/test/resources/org/openmrs/module/sync/include/large-binary-file.pdf new file mode 100644 index 00000000..ad1107cd Binary files /dev/null and b/api/src/test/resources/org/openmrs/module/sync/include/large-binary-file.pdf differ