/** * 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 org.apache.commons.io.IOUtils; import org.apache.commons.lang3.reflect.FieldUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.hibernate.HibernateException; import org.hibernate.Transaction; import org.hibernate.proxy.HibernateProxy; import org.openmrs.Concept; import org.openmrs.Drug; import org.openmrs.DrugOrder; import org.openmrs.Encounter; import org.openmrs.Form; import org.openmrs.Obs; import org.openmrs.OpenmrsMetadata; import org.openmrs.OpenmrsObject; import org.openmrs.PatientProgram; import org.openmrs.PatientState; import org.openmrs.Person; import org.openmrs.PersonAddress; import org.openmrs.PersonAttribute; import org.openmrs.PersonName; import org.openmrs.Program; import org.openmrs.ProgramWorkflow; import org.openmrs.ProgramWorkflowState; import org.openmrs.Relationship; import org.openmrs.RelationshipType; import org.openmrs.Role; import org.openmrs.User; import org.openmrs.api.context.Context; import org.openmrs.api.db.LoginCredential; import org.openmrs.messagesource.MessageSourceService; import org.openmrs.module.sync.api.SyncIngestService; import org.openmrs.module.sync.api.SyncService; import org.openmrs.module.sync.serialization.BinaryNormalizer; import org.openmrs.module.sync.serialization.ClassNormalizer; import org.openmrs.module.sync.serialization.DefaultNormalizer; import org.openmrs.module.sync.serialization.EnumNormalizer; import org.openmrs.module.sync.serialization.FilePackage; import org.openmrs.module.sync.serialization.Item; import org.openmrs.module.sync.serialization.LocaleNormalizer; import org.openmrs.module.sync.serialization.MapNormalizer; import org.openmrs.module.sync.serialization.Normalizer; import org.openmrs.module.sync.serialization.PropertiesNormalizer; import org.openmrs.module.sync.serialization.Record; import org.openmrs.module.sync.serialization.TimestampNormalizer; import org.openmrs.module.sync.server.RemoteServer; import org.openmrs.notification.Alert; import org.openmrs.notification.MessageException; import org.openmrs.util.OpenmrsUtil; import org.openmrs.util.PrivilegeConstants; import org.openmrs.module.sync.serialization.Package; import org.springframework.util.StringUtils; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXParseException; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.UUID; import java.util.Vector; import java.util.zip.CRC32; import java.util.zip.CheckedInputStream; import java.util.zip.CheckedOutputStream; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; /** * Collection of helpful methods in sync */ public class SyncUtil { private static Log log = LogFactory.getLog(SyncUtil.class); // safetypes are *hibernate* types that we know how to serialize with help // of Normalizers public static final Map<String, Normalizer> safetypes; static { DefaultNormalizer defN = new DefaultNormalizer(); TimestampNormalizer dateN = new TimestampNormalizer(); BinaryNormalizer byteN = new BinaryNormalizer(); MapNormalizer mapN = new MapNormalizer(); ClassNormalizer classN = new ClassNormalizer(); PropertiesNormalizer propN = new PropertiesNormalizer(); EnumNormalizer enumN = new EnumNormalizer(); safetypes = new HashMap<String, Normalizer>(); // safetypes.put("binary", defN); // blob safetypes.put("boolean", defN); // safetypes.put("big_integer", defN); // safetypes.put("big_decimal", defN); safetypes.put("binary", byteN); safetypes.put("byte[]", byteN); // calendar // calendar_date // character // clob // currency safetypes.put("date", dateN); // dbtimestamp safetypes.put("double", defN); safetypes.put("enum", enumN); safetypes.put("float", defN); safetypes.put("integer", defN); safetypes.put("locale", new LocaleNormalizer()); safetypes.put("long", defN); safetypes.put("short", defN); safetypes.put("string", defN); safetypes.put("text", defN); safetypes.put("timestamp", dateN); // time // timezone safetypes.put("properties", propN); safetypes.put("map", mapN); safetypes.put("class", classN); } /** * Convenience method to get the normalizer (see {@link #safetypes}) for the given class. * * @param c class to normalize * @return the {@link Normalizer} to use * @see #getNormalizer(String) */ public static Normalizer getNormalizer(Class c) { String simpleClassName = c.getSimpleName().toLowerCase(); if (c.isEnum()) { simpleClassName = "enum"; } return getNormalizer(simpleClassName); } /** * Convenience method to get the normalizer (see {@link #safetypes}) for the given class. * * @param simpleClassName the lowercase key for the {@link #safetypes} map * @return the {@link Normalizer} for the given key */ public static Normalizer getNormalizer(String simpleClassName) { return safetypes.get(simpleClassName); } /** * Get the sync work directory in the openmrs application data directory * * @return a file pointing to the sync output dir */ public static File getSyncApplicationDir() { return OpenmrsUtil.getDirectoryInApplicationDataDirectory("sync"); } public static Object getRootObject(String incoming) throws Exception { Object o = null; if (incoming != null) { Record xml = Record.create(incoming); Item root = xml.getRootItem(); String className = root.getNode().getNodeName(); o = SyncUtil.newObject(className); } return o; } public static NodeList getChildNodes(String incoming) throws Exception { NodeList nodes = null; if (incoming != null) { Record xml = Record.create(incoming); Item root = xml.getRootItem(); nodes = root.getNode().getChildNodes(); } return nodes; } public static void setProperty(Object o, Node n, ArrayList<Field> allFields) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { String propName = n.getNodeName(); Object propVal = SyncUtil.valForField(propName, n.getTextContent(), allFields, n); log.debug("Trying to set value to " + propVal + " when propName is " + propName + " and context is " + n.getTextContent()); if (propVal != null) { SyncUtil.setProperty(o, propName, propVal); log.debug("Successfully called set" + SyncUtil.propCase(propName) + "(" + propVal + ")"); } } public static void setProperty(Object o, String propName, Object propVal) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { Object[] setterParams = new Object[] { propVal }; log.debug("getting setter method"); Method m = SyncUtil.getSetterMethod(o.getClass(), propName, propVal.getClass()); if (m == null) { // We couldn't find a setter method. Let's try setting the field directly instead. log.debug("couldn't find setter method, setting field '" + propName + "' directly."); FieldUtils.writeField(o, propName, propVal, true); return; } boolean acc = m.isAccessible(); m.setAccessible(true); log.debug("about to call " + m.getName()); try { m.invoke(o, setterParams); } finally { m.setAccessible(acc); } } public static String getAttribute(NodeList nodes, String attName, ArrayList<Field> allFields) { String ret = null; if (nodes != null && attName != null) { for (int i = 0; i < nodes.getLength(); i++) { Node n = nodes.item(i); String propName = n.getNodeName(); if (attName.equals(propName)) { Object obj = SyncUtil.valForField(propName, n.getTextContent(), allFields, n); if (obj != null) ret = obj.toString(); } } } return ret; } public static String propCase(String text) { if (text != null) { return text.substring(0, 1).toUpperCase() + text.substring(1); } else { return null; } } public static Object newObject(String className) throws Exception { Object o = null; if (className != null) { Class clazz = Context.loadClass(className); Constructor ct = clazz.getConstructor(); o = ct.newInstance(); } return o; } public static ArrayList<Field> getAllFields(Object o) { Class clazz = o.getClass(); ArrayList<Field> allFields = new ArrayList<Field>(); if (clazz != null) { Field[] nativeFields = clazz.getDeclaredFields(); Field[] superFields = null; Class superClazz = clazz.getSuperclass(); while (superClazz != null && !(superClazz.equals(Object.class))) { // loop through to make sure we get ALL relevant superclasses and their fields if (log.isDebugEnabled()) log.debug("Now inspecting superclass: " + superClazz.getName()); superFields = superClazz.getDeclaredFields(); if (superFields != null) { for (Field f : superFields) { allFields.add(f); } } superClazz = superClazz.getSuperclass(); } if (nativeFields != null) { // add native fields for (Field f : nativeFields) { allFields.add(f); } } } return allFields; } public static OpenmrsObject getOpenmrsObj(String className, String uuid) { try { OpenmrsObject o = Context.getService(SyncService.class).getOpenmrsObjectByUuid( (Class<OpenmrsObject>) Context.loadClass(className), uuid); if (log.isDebugEnabled()) { if (o == null) { log.debug("Unable to get an object of type " + className + " with Uuid " + uuid + ";"); } } return o; } catch (ClassNotFoundException ex) { log.warn("getOpenmrsObj couldn't find class: " + className, ex); return null; } } public static Object valForField(String fieldName, String fieldVal, ArrayList<Field> allFields, Node n) { Object o = null; // the String value on the node specifying the "type" String nodeDefinedClassName = null; if (n != null) { Node tmpNode = n.getAttributes().getNamedItem("type"); if (tmpNode != null) nodeDefinedClassName = tmpNode.getTextContent(); } // TODO: Speed up sync by passing in a Map of String fieldNames instead of list of Fields ? // TODO: Speed up sync by returning after "o" is first set? Or are we doing "last field wins" ? for (Field f : allFields) { //log.debug("field is " + f.getName()); if (f.getName().equals(fieldName)) { Class classType = null; String className = f.getGenericType().toString(); // the string class name for the actual field // if its a collection, set, list, etc if (ParameterizedType.class.isAssignableFrom(f.getGenericType().getClass())) { ParameterizedType pType = (ParameterizedType) f.getGenericType(); classType = (Class) pType.getRawType(); // can this be anything but Class at this point?! } if (className.startsWith("class ")) { className = className.substring("class ".length()); classType = (Class) f.getGenericType(); } else { log.trace("Abnormal className for " + f.getGenericType()); } if (classType == null) { if ("int".equals(className)) { return new Integer(fieldVal); } else if ("long".equals(className)) { return new Long(fieldVal); } else if ("double".equals(className)) { return new Double(fieldVal); } else if ("float".equals(className)) { return new Float(fieldVal); } else if ("boolean".equals(className)) { return new Boolean(fieldVal); } else if ("byte".equals(className)) { return new Byte(fieldVal); } else if ("short".equals(className)) { return new Short(fieldVal); } } // we have to explicitly create a new value object here because all we have is a string - won't know how to convert if (OpenmrsObject.class.isAssignableFrom(classType)) { o = getOpenmrsObj(className, fieldVal); } else if ("java.lang.Integer".equals(className) && !("integer".equals(nodeDefinedClassName) || "java.lang.Integer".equals(nodeDefinedClassName))) { // if we're dealing with a field like PersonAttributeType.foreignKey, the actual value was changed from // an integer to a uuid by the HibernateSyncInterceptor. The nodeDefinedClassName is the node.type which is the // actual classname as defined by the PersonAttributeType.format. However, the field.getClassName is // still an integer because thats what the db stores. we need to convert the uuid to the pk integer and return it OpenmrsObject obj = getOpenmrsObj(nodeDefinedClassName, fieldVal); o = obj.getId(); } else if ("java.lang.String".equals(className) && !("text".equals(nodeDefinedClassName) || "string".equals(nodeDefinedClassName) || "java.lang.String".equals(nodeDefinedClassName) || "integer".equals(nodeDefinedClassName) || "java.lang.Integer".equals(nodeDefinedClassName) || fieldVal.isEmpty())) { // if we're dealing with a field like PersonAttribute.value, the actual value was changed from // a string to a uuid by the HibernateSyncInterceptor. The nodeDefinedClassName is the node.type which is the // actual classname as defined by the PersonAttributeType.format. However, the field.getClassName is // still String because thats what the db stores. we need to convert the uuid to the pk integer/string and return it OpenmrsObject obj = getOpenmrsObj(nodeDefinedClassName, fieldVal); if (obj == null) { if (StringUtils.hasText(fieldVal)) { // If we make it here, and we are dealing with person attribute values, then just return the string value as-is if (PersonAttribute.class.isAssignableFrom(f.getDeclaringClass()) && "value".equals(f.getName())) { o = fieldVal; } else { // throw a warning if we're having trouble converting what should be a valid value log.error("Unable to convert value '" + fieldVal + "' into a " + nodeDefinedClassName); throw new SyncException("Unable to convert value '" + fieldVal + "' into a " + nodeDefinedClassName); } } else { // if fieldVal is empty, just save an empty string here too o = ""; } } else { o = obj.getId().toString(); // call toString so the class types match when looking up the setter } } else if (Collection.class.isAssignableFrom(classType)) { // this is a collection of items. this is intentionally not in the convertStringToObject method Collection tmpCollection = null; if (Set.class.isAssignableFrom(classType)) tmpCollection = new LinkedHashSet(); else tmpCollection = new Vector(); // get the type of class held in the collection String collectionTypeClassName = null; java.lang.reflect.Type collectionType = ((java.lang.reflect.ParameterizedType) f.getGenericType()) .getActualTypeArguments()[0]; if (collectionType.toString().startsWith("class ")) collectionTypeClassName = collectionType.toString().substring("class ".length()); // get the type of class defined in the text node // if it is different, we could be dealing with something like Cohort.memberIds // node type comes through as java.util.Set<classname> String nodeDefinedCollectionType = null; int indexOfLT = nodeDefinedClassName.indexOf("<"); if (indexOfLT > 0) nodeDefinedCollectionType = nodeDefinedClassName.substring(indexOfLT + 1, nodeDefinedClassName.length() - 1); // change the string to just a comma delimited list fieldVal = fieldVal.replaceFirst("\\[", "").replaceFirst("\\]", ""); for (String eachFieldVal : fieldVal.split(",")) { eachFieldVal = eachFieldVal.trim(); // take out whitespace if(!StringUtils.hasText(eachFieldVal)) continue; // try to convert to a simple object Object tmpObject = convertStringToObject(eachFieldVal, (Class) collectionType); // convert to an openmrs object if (tmpObject == null && nodeDefinedCollectionType != null) tmpObject = getOpenmrsObj(nodeDefinedCollectionType, eachFieldVal).getId(); if (tmpObject == null) log.error("Unable to convert: " + eachFieldVal + " to a " + collectionTypeClassName); else tmpCollection.add(tmpObject); } o = tmpCollection; } else if (Map.class.isAssignableFrom(classType) || Properties.class.isAssignableFrom(classType)) { Object tmpMap = SyncUtil.getNormalizer(classType).fromString(classType, fieldVal); //if we were able to convert and got anything at all back, assign it if (tmpMap != null) { o = tmpMap; } } else if ((o = convertStringToObject(fieldVal, classType)) != null) { log.trace("Converted " + fieldVal + " into " + classType.getName()); } else { log.debug("Don't know how to deserialize class: " + className); } } } if (o == null) log.debug("Never found a property named: " + fieldName + " for this class"); return o; } /** * Converts the given string into an object of the given className. Supports basic objects like * String, Integer, Long, Float, Double, Boolean, Date, and Locale. * * @param fieldVal the string object representation * @param clazz the {@link Class} to turn this string into * @return object of type "clazz" or null if unable to convert it * @see SyncUtil#getNormalizer(Class) */ public static Object convertStringToObject(String fieldVal, Class clazz) { Normalizer normalizer = getNormalizer(clazz); if (normalizer == null) { log.error("Unable to parse value: " + fieldVal + " into object of class: " + clazz.getName()); return null; } else { return normalizer.fromString(clazz, fieldVal); } } /** * Finds property 'get' accessor based on target type and property name. * * @return Method object matching name and param, else null */ public static Method getGetterMethod(Class objType, String propName) { String methodName = "get" + propCase(propName); return SyncUtil.getPropertyAccessor(objType, methodName, null); } /** * Finds property 'set' accessor based on target type, property name, and set method parameter * type. * * @return Method object matching name and param, else null */ public static Method getSetterMethod(Class objType, String propName, Class propValType) { String methodName = "set" + propCase(propName); return SyncUtil.getPropertyAccessor(objType, methodName, propValType); } /** * Constructs a Method object for invocation on instances of objType class based on methodName * and the method parameter type. Handles only propery accessors - thus takes Class propValType * and not Class[] propValTypes. * <p> * If necessary, this implementation traverses both objType and propValTypes type hierarchies in * search for the method signature match. * * @param objType Type to examine. * @param methodName Method name. * @param propValType Type of the parameter that method takes. If none (i.e. getter), pass null. * @return Method object matching name and param, else null */ private static Method getPropertyAccessor(Class objType, String methodName, Class propValType) { // need to try to get setter, both in this object, and its parent class Method m = null; boolean continueLoop = true; // Fix - CA - 22 Jan 2008 - extremely odd Java Bean convention that says getter/setter for fields // where 2nd letter is capitalized (like "aIsToB") first letter stays lower in getter/setter methods // like "getaIsToB()". Hence we need to try that out too String altMethodName = methodName.substring(0, 3) + methodName.substring(3, 4).toLowerCase() + methodName.substring(4); try { Class[] setterParamClasses = null; if (propValType != null) { //it is a setter setterParamClasses = new Class[1]; setterParamClasses[0] = propValType; } Class clazz = objType; // it could be that the setter method itself is in a superclass of objectClass/clazz, so loop through those while (continueLoop && m == null && clazz != null && !clazz.equals(Object.class)) { try { m = clazz.getMethod(methodName, setterParamClasses); continueLoop = false; break; //yahoo - we got it using exact type match } catch (SecurityException e) { m = null; } catch (NoSuchMethodException e) { m = null; } //not so lucky: try to find method by name, and then compare params for compatibility //instead of looking for the exact method sig match Method[] mes = objType.getMethods(); for (Method me : mes) { if (me.getName().equals(methodName) || me.getName().equals(altMethodName)) { Class[] meParamTypes = me.getParameterTypes(); if (propValType != null && meParamTypes != null && meParamTypes.length == 1 && isAssignableFrom(meParamTypes[0], propValType)) { m = me; continueLoop = false; //aha! found it break; } } } if (continueLoop) clazz = clazz.getSuperclass(); } } catch (Exception ex) { //whatever happened, we didn't find the method - return null m = null; log.warn("Unexpected exception while looking for a Method object, returning null", ex); } if (m == null) { if (log.isWarnEnabled()) log.warn("Failed to find matching method. type: " + objType.getName() + ", methodName: " + methodName); } return m; } /** * Checks if a class is assignable from another. * * @param class1 the first class. * @param class2 the second class. * @return */ private static boolean isAssignableFrom(Class class1, Class class2) { if (class1.isAssignableFrom(class2)) { return true; } else if ((class1.getName().equals("int") && class2.getName().equals("java.lang.Integer")) || (class1.getName().equals("java.lang.Integer") && class2.getName().equals("int"))) { return true; } else if ((class1.getName().equals("long") && class2.getName().equals("java.lang.Long")) || (class1.getName().equals("java.lang.Long") && class2.getName().equals("long"))) { return true; } else if ((class1.getName().equals("double") && class2.getName().equals("java.lang.Double")) || (class1.getName().equals("java.lang.Double") && class2.getName().equals("double"))) { return true; } else if ((class1.getName().equals("float") && class2.getName().equals("java.lang.Float")) || (class1.getName().equals("java.lang.Float") && class2.getName().equals("float"))) { return true; } else if ((class1.getName().equals("boolean") && class2.getName().equals("java.lang.Boolean")) || (class1.getName().equals("java.lang.Boolean") && class2.getName().equals("boolean"))) { return true; } else if ((class1.getName().equals("byte") && class2.getName().equals("java.lang.Byte")) || (class1.getName().equals("java.lang.Byte") && class2.getName().equals("byte"))) { return true; } else if ((class1.getName().equals("short") && class2.getName().equals("java.lang.Short")) || (class1.getName().equals("java.lang.Short") && class2.getName().equals("short"))) { return true; } return false; } private static OpenmrsObject findByUuid(Collection<? extends OpenmrsObject> list, OpenmrsObject toCheck) { for (OpenmrsObject element : list) { if (element.getUuid().equals(toCheck.getUuid())) return element; } return null; } /** * Uses the generic hibernate API to perform the save with the following exceptions.<br/> * Remarks: <br/> * Obs: if an obs comes through with a non-null voidReason, make sure we change it back to using * a PK. SyncSubclassStub: this is a 'special' utility object that sync uses to compensate for * presence of the prepare stmt in HibernatePatientDAO.insertPatientStubIfNeeded() that * by-passes normal hibernate interceptor behavior. For full description of how this works read * class comments for {@link SyncSubclassStub}. * * @param o object to save * @param className type * @param uuid unique id of the object that is being saved */ public static synchronized void updateOpenmrsObject(OpenmrsObject o, String className, String uuid) { if (o == null) { log.warn("Will not update OpenMRS object that is NULL"); return; } if ("org.openmrs.Obs".equals(className)) { // if an obs comes through with a non-null voidReason, make sure we change it back to using a PK Obs obs = (Obs) o; String voidReason = obs.getVoidReason(); if (StringUtils.hasLength(voidReason)) { int start = voidReason.lastIndexOf(" ") + 1; // assumes uuids don't have spaces int end = voidReason.length() - 1; try { String otherObsUuid = voidReason.substring(start, end); OpenmrsObject openmrsObject = getOpenmrsObj("org.openmrs.Obs", otherObsUuid); Integer obsId = openmrsObject.getId(); obs.setVoidReason(voidReason.substring(0, start) + obsId + ")"); } catch (Exception e) { log.trace("unable to get a uuid from obs voidReason. obs uuid: " + uuid, e); } } } else if ("org.openmrs.api.db.LoginCredential".equals(className)) { LoginCredential login = (LoginCredential) o; OpenmrsObject openmrsObject = getOpenmrsObj("org.openmrs.User", login.getUuid()); Integer userId = openmrsObject.getId(); login.setUserId(userId); } //DT: may 24 2011: I think matching by conceptId is a dead issue now that MetadataSharing is the standard for sharing objects //this never worked anyway... SYNC-160 // else if (o instanceof org.openmrs.Concept) { // Concept concept = (Concept)o; // if (!Context.getService(SyncIngestService.class) // .isConceptIdValidForUuid(concept.getConceptId(), concept.getUuid())) { // // String msg = "Data inconsistency in concepts detected." // + "Concept with conflicting pair of values (id-uuid) already exists in the database." // + "\n Concept id: " + concept.getConceptId() + " and uuid: " + concept.getUuid(); // throw new SyncException(msg); // } // // } //now do the save; see method comments to see why SyncSubclassStub is handled differently if ("org.openmrs.module.sync.SyncSubclassStub".equals(className)) { SyncSubclassStub stub = (SyncSubclassStub) o; Context.getService(SyncIngestService.class).processSyncSubclassStub(stub); } else { Context.getService(SyncService.class).saveOrUpdate(o); } return; } /** * Helper method for the {@link UUID#randomUUID()} method. * * @return a generated random uuid */ public static String generateUuid() { return UUID.randomUUID().toString(); } public static String displayName(String className, String uuid) { String ret = ""; // get more identifying info about this object so it's more user-friendly if (className.equals("Person") || className.equals("Patient")) { Person person = Context.getPersonService().getPersonByUuid(uuid); if (person != null) ret = person.getPersonName().toString(); } if (className.equals("User")) { User user = Context.getUserService().getUserByUuid(uuid); if (user != null) { ret = user.getDisplayString(); } } if (className.equals("Role") || className.equals("org.openmrs.Role")) { Role role = Context.getUserService().getRoleByUuid(uuid); if (role != null) { ret = role.getRole(); } } if (className.equals("Encounter")) { Encounter encounter = Context.getEncounterService().getEncounterByUuid(uuid); if (encounter != null) { ret = encounter.getEncounterType().getName() + (encounter.getForm() == null ? "" : " (" + encounter.getForm().getName() + ")"); } } if (className.equals("Concept")) { Concept concept = Context.getConceptService().getConceptByUuid(uuid); if (concept != null) ret = concept.getName(Context.getLocale()).getName(); } if (className.equals("Drug")) { Drug drug = Context.getConceptService().getDrugByUuid(uuid); if (drug != null) ret = drug.getName(); } if (className.equals("Obs")) { Obs obs = Context.getObsService().getObsByUuid(uuid); if (obs != null) ret = obs.getConcept().getName(Context.getLocale()).getName(); } if (className.equals("DrugOrder")) { DrugOrder drugOrder = (DrugOrder) Context.getOrderService().getOrderByUuid(uuid); if (drugOrder != null) ret = drugOrder.getDrug().getConcept().getName(Context.getLocale()).getName(); } if (className.equals("Program")) { Program program = Context.getProgramWorkflowService().getProgramByUuid(uuid); if (program != null) ret = program.getConcept().getName(Context.getLocale()).getName(); } if (className.equals("ProgramWorkflow")) { ProgramWorkflow workflow = Context.getProgramWorkflowService().getWorkflowByUuid(uuid); if (workflow != null) ret = workflow.getConcept().getName(Context.getLocale()).getName(); } if (className.equals("ProgramWorkflowState")) { ProgramWorkflowState state = Context.getProgramWorkflowService().getStateByUuid(uuid); if (state != null) ret = state.getConcept().getName(Context.getLocale()).getName(); } if (className.equals("PatientProgram")) { PatientProgram patientProgram = Context.getProgramWorkflowService().getPatientProgramByUuid(uuid); String pat = patientProgram.getPatient().getPersonName().toString(); String prog = patientProgram.getProgram().getConcept().getName(Context.getLocale()).getName(); if (pat != null && prog != null) ret = pat + " - " + prog; } if (className.equals("PatientState")) { PatientState patientState = Context.getProgramWorkflowService().getPatientStateByUuid(uuid); String pat = patientState.getPatientProgram().getPatient().getPersonName().toString(); String st = patientState.getState().getConcept().getName(Context.getLocale()).getName(); if (pat != null && st != null) ret = pat + " - " + st; } if (className.equals("PersonAddress")) { PersonAddress address = Context.getPersonService().getPersonAddressByUuid(uuid); String name = address.getPerson().getFamilyName() + " " + address.getPerson().getGivenName(); name += address.getAddress1() != null && address.getAddress1().length() > 0 ? address.getAddress1() + " " : ""; name += address.getAddress2() != null && address.getAddress2().length() > 0 ? address.getAddress2() + " " : ""; name += address.getCityVillage() != null && address.getCityVillage().length() > 0 ? address.getCityVillage() + " " : ""; name += address.getStateProvince() != null && address.getStateProvince().length() > 0 ? address .getStateProvince() + " " : ""; if (name != null) ret = name; } if (className.equals("PersonName")) { PersonName personName = Context.getPersonService().getPersonNameByUuid(uuid); String name = personName.getFamilyName() + " " + personName.getGivenName(); if (name != null) ret = name; } if (className.equals("Relationship")) { Relationship relationship = Context.getPersonService().getRelationshipByUuid(uuid); String from = relationship.getPersonA().getFamilyName() + " " + relationship.getPersonA().getGivenName(); String to = relationship.getPersonB().getFamilyName() + " " + relationship.getPersonB().getGivenName(); if (from != null && to != null) ret += from + " to " + to; } if (className.equals("RelationshipType")) { RelationshipType type = Context.getPersonService().getRelationshipTypeByUuid(uuid); ret += type.getaIsToB() + " - " + type.getbIsToA(); } // If this is OpenMRS metadata, and nothing has yet been assigned, try to use the name by default if (!StringUtils.hasText(ret)) { try { Class<?> clazz = Context.loadClass("org.openmrs."+className); if (OpenmrsMetadata.class.isAssignableFrom(clazz)) { Class<? extends OpenmrsMetadata> mdClass = (Class<? extends OpenmrsMetadata>) clazz; OpenmrsMetadata md = Context.getService(SyncService.class).getOpenmrsObjectByUuid(mdClass, uuid); ret = md.getName(); } } catch (Exception e) { ret = className + " (" + uuid + ")"; log.debug("An error occurred trying to get a name of a metadata item " + ret, e); } } return ret; } /** * Deletes instance of OpenmrsObject. Used to process SyncItems with state of deleted. Remarks: <br /> * Delete of PatientIdentifier is a special case: we need to remove it from parent collection * and then re-save patient: it has all-delete-cascade therefore it will take care of this * itself; more over attempts to delete it explicitly result in hibernate error. */ public static synchronized void deleteOpenmrsObject(OpenmrsObject o) { if (o != null && (o instanceof org.openmrs.PersonAddress || o instanceof org.openmrs.PersonName || o instanceof org.openmrs.PersonAttribute || o instanceof org.openmrs.PatientIdentifier)) { //see this method below removeFromPatientParentCollectionAndSave(o); } else if (o instanceof org.openmrs.Concept || o instanceof org.openmrs.ConceptName) { // if this is a concept or a concept name, make sure we delete concept words explicitly (since concept words don't extend OpenmrsObject) if (o instanceof org.openmrs.Concept) { Context.getAdministrationService().executeSQL("delete from concept_word where concept_id = " + o.getId(), false); } else if (o instanceof org.openmrs.ConceptName) { Context.getAdministrationService().executeSQL( "delete from concept_word where concept_name_id = " + o.getId(), false); } // now call the call plain delete via service API Context.getService(SyncService.class).deleteOpenmrsObject(o); } else { //default behavior: just call plain delete via service API Context.getService(SyncService.class).deleteOpenmrsObject(o); } } public static void sendSyncErrorMessage(SyncRecord syncRecord, RemoteServer server, Exception exception) { SyncService syncService = Context.getService(SyncService.class); try { String adminEmail = syncService.getAdminEmail(); if (adminEmail == null || adminEmail.length() == 0) { log.warn("Sync error message could not be sent because " + SyncConstants.PROPERTY_SYNC_ADMIN_EMAIL + " is not configured."); } else if (adminEmail != null) { log.info("Preparing to send sync error message via email to " + adminEmail); String subject = exception.getMessage(); String recipients = adminEmail; StringBuffer content = new StringBuffer(); content.append("ALERT: Synchronization has stopped between\n"); content.append("local server (").append(syncService.getServerName()); content.append(") and remote server ").append(server.getNickname()).append("\n\n"); content.append("Summary of failing record\n"); content.append("Original Uuid: " + syncRecord.getOriginalUuid()); content.append("Contained classes: " + syncRecord.getContainedClassSet()).append("\n"); content.append("Contents:\n"); try { for (SyncItem item : syncRecord.getItems()) { log.info("Sync item content: " + item.getContent()); } FilePackage pkg = new FilePackage(); Record record = pkg.createRecordForWrite("SyncRecord"); Item top = record.getRootItem(); syncRecord.save(record, top); content.append(record.toString()); } catch (Exception e) { StringBuilder errorMessage = new StringBuilder(); errorMessage.append("An error occurred while retrieving sync record payload. Sync record:\n"); errorMessage.append(syncRecord.toString()); errorMessage.append("Error details:\n"); errorMessage.append(e.getMessage()); log.warn(errorMessage.toString(), e); content.append(errorMessage); } SyncMailUtil.sendMessage(recipients, subject, content.toString()); log.info("Sent sync error message to " + adminEmail); sendAlert("sync.mail.sentErrorMessageTo", adminEmail); } } catch (MessageException e) { log.error("An error occurred while sending the sync error message", e); sendAlert("sync.status.email.notSentError", exception.getMessage(), e.getMessage()); } } public static Collection<SyncItem> getSyncItemsFromPayload(String payload) throws HibernateException{ Collection<SyncItem> items = null; Package pkg = new Package(); try { Record record = pkg.createRecordFromString(payload); Item root = record.getRootItem(); List<Item> itemsToDeSerialize = record.getItems(root); if (itemsToDeSerialize != null && itemsToDeSerialize.size() > 0) { items = new LinkedList<SyncItem>(); for(Item i : itemsToDeSerialize) { SyncItem syncItem = new SyncItem(); syncItem.load(record, i); items.add(syncItem); } } } catch (SAXParseException e) { log.error("Error processing XML at column " + e.getColumnNumber() + ", and line number " + e.getLineNumber() + "; public ID of entity causing error: " + e.getPublicId() + "; system id of entity causing error: " + e.getSystemId() + "; contents: " + payload.toString()); throw new HibernateException("Error processing XML while deserializing object from storage", e); } catch (Exception e) { log.error("Could not deserialize object from storage", e); throw new HibernateException("Could not deserialize object from storage", e); } return items; } public static String getSyncRecordPayload(SyncRecord syncRecord){ return getPayloadFromSyncItems(syncRecord.getItems()); } public static String getPayloadFromSyncItems(Collection<SyncItem> items) throws HibernateException { String payload = null; if (items != null && items.size() > 0) { Package pkg = new Package(); Record record = null; try { record = pkg.createRecordForWrite("items"); Item root = record.getRootItem(); for(SyncItem item : items) { item.save(record, root); } } catch (Exception e) { log.error("Could not serialize SyncItems:", e); throw new HibernateException("Could not serialize SyncItems", e); } if (record != null ) { payload = record.toStringAsDocumentFragement(); } } return payload; } private static void sendAlert(String messageCode, Object...replacements) { try { Context.addProxyPrivilege(PrivilegeConstants.MANAGE_ALERTS); Context.addProxyPrivilege(PrivilegeConstants.VIEW_USERS); Role role = null; String roleName = Context.getAdministrationService().getGlobalProperty(SyncConstants.ROLE_TO_SEND_TO_MAIL_ALERTS); if (StringUtils.hasText(roleName)) { role = Context.getUserService().getRole(roleName); } if (role != null) { List<User> users = Context.getUserService().getUsersByRole(role); MessageSourceService mss = Context.getMessageSourceService(); String message = mss.getMessage(messageCode, replacements, null); Alert alert = new Alert(message, users); alert.setSatisfiedByAny(true); Context.getAlertService().saveAlert(alert); } else { log.info("Not creating alert because no appropriate role configured to receive alerts"); } } catch (Exception e) { log.warn("An error occurred trying to alert users that a sync error message was sent", e); } finally { Context.removeProxyPrivilege(PrivilegeConstants.MANAGE_ALERTS); Context.removeProxyPrivilege(PrivilegeConstants.VIEW_USERS); } } /** * @param inputStream * @return * @throws Exception */ public static String readContents(InputStream inputStream, boolean isCompressed) throws Exception { StringBuffer contents = new StringBuffer(); BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, SyncConstants.UTF8)); String line = ""; while ((line = reader.readLine()) != null) { contents.append(line); } return contents.toString(); } public static byte[] compress(String content) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); CheckedOutputStream cos = new CheckedOutputStream(baos, new CRC32()); GZIPOutputStream zos = new GZIPOutputStream(new BufferedOutputStream(cos)); IOUtils.copy(new ByteArrayInputStream(content.getBytes()), zos); return baos.toByteArray(); } public static String decompress(byte[] data) throws IOException { ByteArrayInputStream bais2 = new ByteArrayInputStream(data); CheckedInputStream cis = new CheckedInputStream(bais2, new CRC32()); GZIPInputStream zis = new GZIPInputStream(new BufferedInputStream(cis)); InputStreamReader reader = new InputStreamReader(zis); BufferedReader br = new BufferedReader(reader); StringBuffer buffer = new StringBuffer(); String line = ""; while ((line = br.readLine()) != null) { buffer.append(line); } return buffer.toString(); } /** * Rebuilds XSN form. This is needed for ingest when form is received from remote server; * template files that are contained in xsn in fromentry_xsn table need to be updated. Supported * way to do this is to ask formentry module to rebuild XSN. Invoking method via reflection is * temporary workaround until sync is in trunk: at that point advice point should be registered * on sync service that formentry could respond to by calling rebuild. * * @param xsn the xsn to be rebuilt. */ public static void rebuildXSN(OpenmrsObject xsn) { Class c = null; Method m = null; String msg = null; if (xsn == null) { return; } try { // only rebuild non-archived xsns try { m = xsn.getClass().getDeclaredMethod("getArchived"); } catch (Exception e) {} if (m == null) { log.warn("Failed to retrieve handle to getArchived method; is formentry module loaded?"); return; } Boolean isArchived = (Boolean) m.invoke(xsn, null); if (isArchived) return; // get the form id of the xsn try { m = xsn.getClass().getDeclaredMethod("getForm"); } catch (Exception e) {} if (m == null) { log.warn("Failed to retrieve handle to getForm method in FormEntryXsn; is formentry module loaded?"); return; } Form form = (Form) m.invoke(xsn, null); msg = "Processing form with id: " + form.getFormId(); // now get methods to rebuild the form try { c = Context.loadClass("org.openmrs.module.formentry.FormEntryUtil"); } catch (Exception e) {} if (c == null) { log.warn("Failed to retrieve handle to FormEntryUtil in formentry module; is formentry module loaded? " + msg); return; } try { m = c.getDeclaredMethod("rebuildXSN", new Class[] { Form.class }); } catch (Exception e) {} if (m == null) { log.warn("Failed to retrieve handle to rebuildXSN method in FormEntryUtil; is formentry module loaded? " + msg); return; } // finally actually do the rebuilding m.invoke(null, form); } catch (Exception e) { log.error("FormEntry module present but failed to rebuild XSN, see stack for error detail." + msg, e); throw new SyncException( "FormEntry module present but failed to rebuild XSN, see server log for the stacktrace for error details " + msg, e); } return; } /** * Rebuilds XSN form. Same helper method as above, but takes Form as input. * * @param form form to rebuild xsn for */ public static void rebuildXSNForForm(Form form) { Object o = null; Class c = null; Method m = null; String msg = null; Object xsn = null; if (form == null) { return; } try { msg = "Processing form with id: " + form.getFormId().toString(); boolean rebuildXsn = true; try { c = Context.loadClass("org.openmrs.module.formentry.FormEntryService"); } catch (Exception e) {} if (c == null) { log.warn("Failed to find FormEntryService in formentry module; is module loaded? " + msg); return; } try { Object formentryservice = Context.getService(c); m = formentryservice.getClass().getDeclaredMethod("getFormEntryXsn", new Class[] { form.getClass() }); xsn = m.invoke(formentryservice, form); if (xsn == null) rebuildXsn = false; } catch (Exception e) { log.warn("Failed to test for formentry xsn existance"); } if (rebuildXsn) { SyncUtil.rebuildXSN((OpenmrsObject) xsn); } } catch (Exception e) { log.error("FormEntry module present but failed to rebuild XSN, see stack for error detail." + msg, e); throw new SyncException( "FormEntry module present but failed to rebuild XSN, see server log for the stacktrace for error details " + msg, e); } return; } /** * Checks that instances of a given class are loadable and its {@link OpenmrsObject#getId()} * does not return an {@link UnsupportedOperationException}. <br/> * The <code>entryClassName</code> should be of type {@link OpenmrsObject} * * @param entryClassName class name of OpenmrsObject to check * @return false if instance can be loaded via {@link Context#loadClass(String)} and * {@link OpenmrsObject#getId()} can be called; else returns true. */ public static boolean hasNoAutomaticPrimaryKey(String entryClassName) { try { Class<OpenmrsObject> c = (Class<OpenmrsObject>) Context.loadClass(entryClassName); OpenmrsObject o = c.newInstance(); o.getId(); return false; } catch (Exception e) { return true; } } /** * This monstrosity looks for getter(s) on the parent object of an OpenmrsObject that return a * collection of the originally passed in OpenmrsObject type. This then explicitly removes the * object from the parent collection, and if the parent is a Patient or Person, calls save on * the parent. * * @param item -- the OpenmrsObject to remove and save */ private static void removeFromPatientParentCollectionAndSave(OpenmrsObject item) { Field[] f = item.getClass().getDeclaredFields(); for (int k = 0; k < f.length; k++) { Type fieldType = f[k].getGenericType(); if (org.openmrs.OpenmrsObject.class.isAssignableFrom((Class) fieldType)) { //if the property is an OpenmrsObject (excludes lists, etc..) Method getter = getGetterMethod(item.getClass(), f[k].getName()); //get the getters OpenmrsObject parent = null; //the parent object if (getter == null) { continue; //no prob -- eliminates most utility methods on item } try { parent = (OpenmrsObject) getter.invoke(item, null); //get the parent object } catch (Exception ex) { log.debug( "in removeFromParentCollection: getter probably did not return an object that could be case as an OpenmrsObject", ex); } if (parent != null) { Method[] methods = getter.getReturnType().getDeclaredMethods(); //get the Parent's methods to inspect for (Method method : methods) { Type type = method.getGenericReturnType(); //return is a parameterizable and there are 0 arguments to method and the return is a Collection if (ParameterizedType.class.isAssignableFrom(type.getClass()) && method.getGenericParameterTypes().length == 0 && method.getName().contains("get")) { //get the methods on Person that return Lists or Sets ParameterizedType pt = (ParameterizedType) type; for (int i = 0; i < pt.getActualTypeArguments().length; i++) { Type t = pt.getActualTypeArguments()[i]; // if the return type matches the original object, and the return is not a Map if (item.getClass().equals(t) && !pt.getRawType().toString().equals(java.util.Map.class.toString()) && java.util.Collection.class.isAssignableFrom((Class) pt.getRawType())) { try { Object colObj = (Object) method.invoke(parent, null); //get the list if (colObj != null) { java.util.Collection collection = (java.util.Collection) colObj; Iterator it = collection.iterator(); boolean atLeastOneRemoved = false; while (it.hasNext()) { OpenmrsObject omrsobj = (OpenmrsObject) it.next(); if (omrsobj.getUuid() != null && omrsobj.getUuid().equals(item.getUuid())) { //compare uuid of original item with Collection contents it.remove(); atLeastOneRemoved = true; } if (atLeastOneRemoved && (parent instanceof org.openmrs.Patient || parent instanceof org.openmrs.Person)) { // this is commented out because deleting of patients fails if it is here. // we really should not need to call "save", that can only cause problems. // removing the object from the parent collection is the important part, which we're doing above //Context.getService(SyncService.class).saveOrUpdate(parent); } } } } catch (Exception ex) { log.error("Failed to build new collection", ex); } } } } } } } } } /** * Gets the global property value as an integer for the specified global property name * * @param globalPropertyName the global property name * @return the integer value */ public static Integer getGlobalPropetyValueAsInteger(String globalPropertyName) { Integer intValue = null; String stringValue = Context.getAdministrationService().getGlobalProperty(globalPropertyName); try { intValue = Integer.valueOf(stringValue); } catch (NumberFormatException e) { if (StringUtils.hasText(stringValue)) log.warn("Only Integers are allowed as values for the global property '" + globalPropertyName + "'"); } return intValue; } public static String formatEntities(Iterator entities) { StringBuilder sb = new StringBuilder(); if (entities != null) { while (entities.hasNext()) { Object entity = entities.next(); sb.append(sb.length() == 0 ? "" : ",").append(formatObject(entity)); } } return sb.toString(); } public static String formatObject(Object object) { if (object != null) { try { if (object instanceof HibernateProxy) { HibernateProxy proxy = (HibernateProxy) object; Class persistentClass = proxy.getHibernateLazyInitializer().getPersistentClass(); Object identifier = proxy.getHibernateLazyInitializer().getIdentifier(); return persistentClass.getSimpleName() + "#" + identifier; } if (object instanceof OpenmrsObject) { OpenmrsObject o = (OpenmrsObject) object; return object.getClass().getSimpleName() + (o.getId() == null ? "" : "#" + o.getId()); } if (object instanceof Collection) { Collection c = (Collection) object; StringBuilder sb = new StringBuilder(); for (Object o : c) { sb.append(sb.length() == 0 ? "" : ",").append(formatObject(o)); } return c.getClass().getSimpleName() + "[" + sb + "]"; } } catch (Exception e) { } return object.getClass().getSimpleName(); } return ""; } public static String formatTransactionStatus(Transaction tx) { if (tx == null) { return "TX IS NULL"; } if (tx.isActive()) { return "TX ACTIVE"; } if (tx.wasCommitted()) { return "TX COMMITTED"; } if (tx.wasRolledBack()) { return "TX ROLLED BACK"; } return "TX STATUS UNKNOWN"; } }