/** * 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.util.ArrayList; import java.util.Arrays; import java.util.List; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.openmrs.Auditable; import org.openmrs.BaseOpenmrsData; import org.openmrs.Patient; /** * Utility class that exists in support of odd case of saving patient who is * already user in the system. The reason special handling is necessary is that * patient save in case of the patient already being user is currently handled * outside of normal hibernate POJO. This is because Patient inherits from * Person and so does User, therefore hibernate save on new Patient when User * already exists throws exception. This is coded around during save of patient * in HibernatePatientDAO.savePatient. Specifically, private * HibernatePatientDAO.insertPatientStubIfNeeded() uses jdbc to insert a row * into patient table. * * <p/> * Naturally, this bypasses sync all together resulting in 'missing' 'NEW' * syncItem for patient object when this scenario is run on the server with sync * enabled. * * <p/> * The solution. Sync deals with the core issue of openmrs api bypassing * hibernate in this use case by 'listening' on savePatient() method AOP and * then performing a compensating step of manufacturing necessary 'NEW' * syncItem. This is implemented as follows. * * <p/> * During the save of the patient: <br/> * 1. SavePatientAdvice AOP intercepts all savePatient() invocations and calls * to SyncService.handleInsertPatientStubIfNeeded() to perform compensating * actions, if needed. * * <br/> * 2. SyncService.handleInsertPatientStubIfNeeded() figures if the pending save * is indeed case of new patient from existing user and if it is, then calls via * static method on HibernateSyncInterceptor to add the new sync item for just * this purpose: the stub for patient table. * * <br/> * 3. In order to create new sync item, the interceptor needs to serialize * relevant state into sync record. This is done by using this utility holder * class, {@link SyncSubclassStub}. The only purpose of this class is to be serialization * vehicle for needed information: uuid, creator, dateCreated. * * <p/> * The ingest of this newly created sync item is processed as follows: <br/> * 1. Ingest logic in SyncIngestServiceImpl.processOpenmrsObject() works as for * any other openmrs object; it delegates the handling of 'NEW' on instance of * openmrs object to syncUtil.updateOpenmrsObject. * * <br/> * 2. syncUtil.updateOpenmrsObject determines that instance of {@link SyncSubclassStub} * is being processed and hence we can't just save it via hibernate * session.saveorupdate. Instead, special methods on SyncIngestService & SyncDAO * exists to handle this: dao.processSyncSubclassStub. * * <br/> * 3. As the SyncIngestService.processSyncSubclassStub() delegates to * dao.dao.processSyncSubclassStub(), the dao then performs necessary insert on * patient table. This is done by first by using the stub's uuid to look-up the * existing person record person_id which is then used as patient_id for the new * row in the patient table. * * <br/> * 4. Finally after the row was created, the DAO also invokes the * interceptor.addSyncItemForSublcassStub() so that the appropriate syncItem is * created during ingest too. Note this must be there sync the ingesting change * can be propagated later to other servers. * * * @see org.openmrs.module.sync.advice.SavePatientAdvice * @see org.openmrs.module.sync.advice.SaveConceptAdvice * @see org.openmrs.module.sync.api.SyncService#handleInsertPatientStubIfNeeded(Patient) * @see org.openmrs.module.sync.api.SyncService#handleInsertSubclassStubIfNeeded(SyncSubclassStub) * @see org.openmrs.module.sync.api.db.hibernate.HibernateSyncInterceptor#addSyncItemForSubclassStub(SyncSubclassStub) * @see org.openmrs.module.sync.api.impl.SyncIngestServiceImpl#processSyncSubclassStub(SyncSubclassStub) * @see org.openmrs.module.sync.SyncUtil#updateOpenmrsObject(org.openmrs.OpenmrsObject, * String, String) * @see org.openmrs.module.sync.api.db.hibernate.HibernateSyncDAO#processSyncSubclassStub(SyncSubclassStub) * */ public class SyncSubclassStub extends BaseOpenmrsData implements java.io.Serializable { public static final long serialVersionUID = 93124L; private transient static final Log log = LogFactory .getLog(SyncSubclassStub.class); Integer id = null; String parentTable; String parentTableId; String subclassTable; String subclassTableId; // any other column that is required to make a stub row in the subclass table. (e.g. 'precise' on concept_numeric is not nullable) List<String> requiredColumnNames; List<String> requiredColumnValues; List<String> requiredColumnClasses; // Constructors /** default constructor */ public SyncSubclassStub() { } public SyncSubclassStub(Auditable oo, String parentTable, String parentTableId, String subclassTable, String subclassTableId, String[] requiredColumnNames, String[] requiredColumnValues, String[] requiredColumnClasses) { setId(oo.getId()); setUuid(oo.getUuid()); setCreator(oo.getCreator()); setDateCreated(oo.getDateCreated()); setVoided(false); setParentTable(parentTable); setParentTableId(parentTableId); setSubclassTable(subclassTable); setSubclassTableId(subclassTableId); if (requiredColumnNames != null) setRequiredColumnNames(Arrays.asList(requiredColumnNames)); if (requiredColumnValues != null) setRequiredColumnValues(Arrays.asList(requiredColumnValues)); if (requiredColumnClasses != null) setRequiredColumnClasses(Arrays.asList(requiredColumnClasses)); } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getParentTable() { return parentTable; } public void setParentTable(String parentTable) { this.parentTable = parentTable; } public String getParentTableId() { return parentTableId; } public void setParentTableId(String parentTableid) { this.parentTableId = parentTableid; } public String getSubclassTable() { return subclassTable; } public void setSubclassTable(String subclassTable) { this.subclassTable = subclassTable; } public String getSubclassTableId() { return subclassTableId; } public void setSubclassTableId(String subclassTableId) { this.subclassTableId = subclassTableId; } public List<String> getRequiredColumnNames() { return requiredColumnNames; } public void setRequiredColumnNames(List<String> requiredColumnNames) { this.requiredColumnNames = requiredColumnNames; } public List<String> getRequiredColumnValues() { return requiredColumnValues; } public void setRequiredColumnValues(List<String> requiredColumnValues) { this.requiredColumnValues = requiredColumnValues; } public List<String> getRequiredColumnClasses() { return requiredColumnClasses; } public void setRequiredColumnClasses(List<String> requiredColumnClass) { this.requiredColumnClasses = requiredColumnClass; } /** * Convenience method to add a requiredColumnName and requiredColumnValue * * @param columnName * @param columnValue */ public void addColumn(String columnName, Object columnValue) { if (columnName == null || columnValue == null) return; if (requiredColumnNames == null) requiredColumnNames = new ArrayList<String>(); if (requiredColumnValues == null) requiredColumnValues = new ArrayList<String>(); if (requiredColumnClasses == null) requiredColumnClasses = new ArrayList<String>(); requiredColumnNames.add(columnName); requiredColumnClasses.add(columnValue.getClass().getName()); requiredColumnValues.add(SyncUtil.getNormalizer(columnValue.getClass()).toString(columnValue)); } }