/** * 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.Serializable; import java.util.Collection; import java.util.Date; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.openmrs.module.sync.serialization.IItem; import org.openmrs.module.sync.serialization.Item; import org.openmrs.module.sync.serialization.Record; import org.openmrs.module.sync.serialization.TimestampNormalizer; import org.openmrs.module.sync.server.RemoteServer; import org.openmrs.module.sync.server.RemoteServerType; import org.openmrs.module.sync.server.SyncServerRecord; /** * SyncRecord is a collection of sync items that represents a smallest transactional unit. * In other words, all sync items within a record must be: * - transfered from/to sync source * - committed/rolled back together * * Information about sync records -- what was sent, received should be stored in DB by each * sync source. Minimally, each source should keep track of history of sync records that were * sent 'up' to parent. * * Consequently a sync 'transmission' is nothing more than a transport of a set of sync records from * source A to source B. * */ public class SyncRecord implements Serializable, IItem { public static final long serialVersionUID = 0L; // Fields private Integer recordId; private String uuid = null; private String creator = null; private String databaseVersion = null; private Date timestamp = null; private int retryCount; private SyncRecordState state = SyncRecordState.NEW; private LinkedHashMap<String, SyncItem> items = null; private String containedClasses = ""; private Set<SyncServerRecord> serverRecords = null; private RemoteServer forServer = null; private String originalUuid = null; public String getOriginalUuid() { return originalUuid; } public void setOriginalUuid(String originalUuid) { this.originalUuid = originalUuid; } // Constructors /** default constructor */ public SyncRecord() { } public String getContainedClasses() { return containedClasses; } public void setContainedClasses(String containedClasses) { if ( containedClasses != null ) { String[] splits = containedClasses.split(","); for ( String split : splits ) { this.addContainedClass(split); } } else { this.containedClasses = containedClasses; } } public Integer getRecordId() { return recordId; } public void setRecordId(Integer recordId) { this.recordId = recordId; } // Properties // globally unique id of the record public String getUuid() { return uuid; } public void setUuid(String uuid) { this.uuid = uuid; } // The uuid of the creator of the record public String getCreator() { return creator; } public void setCreator(String creator) { this.creator = creator; } // The database version used when creating this record public String getDatabaseVersion() { return databaseVersion; } public void setDatabaseVersion(String databaseVersion) { this.databaseVersion = databaseVersion; } // timestamp of last operation public Date getTimestamp() { return timestamp; } public void setTimestamp(Date timestamp) { this.timestamp = timestamp; } // retry count public int getRetryCount() { return retryCount; } public void setRetryCount(int retryCount) { this.retryCount = retryCount; } public void incrementRetryCount() { this.retryCount++; } //state public SyncRecordState getState() { return state; } public void setState(SyncRecordState state) { this.state = state; } //list of sync items public Collection<SyncItem> getItems() { if (items == null) return null; return items.values(); } public void addItem(SyncItem syncItem) { if (items == null) { items = new LinkedHashMap<String,SyncItem>(); } items.put(SyncRecord.deriveMapKey(syncItem),syncItem); } /** * If there is already an item with same key, replace it with passed in value, else add it. * It will be added as LAST in insert order. * * Note: internally key for the LinkedHashMap is uuid + action + contained type * * @param syncItem */ public void addOrRemoveAndAddItem(SyncItem syncItem) { if (syncItem == null) { return; }; String itemMapKey = SyncRecord.deriveMapKey(syncItem); if (items == null) { items = new LinkedHashMap<String,SyncItem>(); } else { if (items.containsKey(itemMapKey)) { items.remove(itemMapKey); } } //now add it this.addItem(syncItem); } public void setItems(Collection<SyncItem> newItems) { if(newItems == null) return; items = new LinkedHashMap<String,SyncItem>(); for(SyncItem newItem : newItems) { this.addItem(newItem); } } public boolean hasItems() { if (items == null) return false; if (items.size() > 0) return true; else return false; } /** * Checks if the sync record contains an item with its key equal to the one that is generated * for the passed in SyncItem, ideally it checks if a sync item exists that matches the * uuid+action+containedType combination * * @param syncItem the sync item to match against * @return true if the record contains the sync item otherwise false */ public boolean hasSyncItem(SyncItem syncItem) { if (syncItem != null && items != null) { return items.containsKey(SyncRecord.deriveMapKey(syncItem)); } return false; } // Methods @Override public boolean equals(Object o) { if (!(o instanceof SyncRecord) || o == null) return false; SyncRecord oSync = (SyncRecord) o; boolean same = ((oSync.getTimestamp() == null) ? (this.getTimestamp() == null) : oSync.getTimestamp().equals(this.getTimestamp())) && ((oSync.getUuid() == null) ? (this.getUuid() == null) : oSync.getUuid().equals(this.getUuid())) && ((oSync.getState() == null) ? (this.getState() == null) : oSync.getState().equals(this.getState())) && (oSync.getRetryCount() == this.getRetryCount()); //manually check linkedhashset Collection<SyncItem> oSyncItems = oSync.getItems(); Collection<SyncItem> thisItems = this.getItems(); if (oSyncItems == null || thisItems == null) { same = same && (thisItems == null) && (oSyncItems == null); } else { same = same && oSyncItems.containsAll(thisItems) && (oSyncItems.size() == thisItems.size()); } return same; } public Item save(Record xml, Item parent) throws Exception { Item me = xml.createItem(parent, this.getClass().getSimpleName()); //serialize primitives xml.setAttribute(me, "uuid", uuid); xml.setAttribute(me, "retryCount", Integer.toString(retryCount)); xml.setAttribute(me, "containedClasses", this.containedClasses); if ( this.originalUuid != null ) { xml.setAttribute(me, "originalUuid", originalUuid); } xml.setAttribute(me, "uuid", uuid); if ( this.getForServer() != null ) { if ( !this.getForServer().getServerType().equals(RemoteServerType.PARENT)) { SyncServerRecord serverRecord = this.getServerRecord(this.getForServer()); xml.setAttribute(me, "state", serverRecord.getState().toString()); xml.setAttribute(me, "retryCount", Integer.toString(serverRecord.getRetryCount())); } else { xml.setAttribute(me, "state", state.toString()); xml.setAttribute(me, "retryCount", Integer.toString(retryCount)); } } else { xml.setAttribute(me, "state", state.toString()); xml.setAttribute(me, "retryCount", Integer.toString(retryCount)); } if (timestamp != null) { xml.setAttribute(me, "timestamp", new TimestampNormalizer().toString(timestamp)); } //serialize IItem children Item itemsCollection = xml.createItem(me, "items"); if (items != null) { for(SyncItem item : items.values()) { item.save(xml, itemsCollection); } }; return me; } public void load(Record xml, Item me) throws Exception { //deserialize primitives this.uuid = me.getAttribute("uuid"); this.retryCount = Integer.parseInt(me.getAttribute("retryCount")); this.state = SyncRecordState.valueOf(me.getAttribute("state")); this.containedClasses = me.getAttribute("containedClasses"); if (me.getAttribute("timestamp") == null) this.timestamp = null; else { this.timestamp = (Date)new TimestampNormalizer().fromString(Date.class,me.getAttribute("timestamp")); } if (me.getAttribute("originalUuid") == null) this.originalUuid = null; else { this.originalUuid = me.getAttribute("originalUuid"); } //now get items Item itemsCollection = xml.getItem(me, "items"); if (itemsCollection.isEmpty()) { items = null; } else { //re-create linked hashmap entries with appropriate keys items = new LinkedHashMap<String,SyncItem>(); List<Item> serItems = xml.getItems(itemsCollection); for (int i = 0; i < serItems.size(); i++) { Item serItem = serItems.get(i); SyncItem syncItem = new SyncItem(); syncItem.load(xml, serItem); items.put(SyncRecord.deriveMapKey(syncItem),syncItem); } } } public Set<String> getContainedClassSet() { Set<String> ret = new HashSet<String>(); if ( this.containedClasses != null ) { String[] classes = this.containedClasses.split(","); for ( String clazz : classes ) { if ( !ret.contains(clazz) ) ret.add(clazz); } } return ret; } public void setContainedClassSet(Set<String> classes) { if ( classes != null ) { this.containedClasses = ""; for ( String clazz : classes ) { clazz = clazz.trim(); if ( clazz.length() > 0 ) { if ( this.containedClasses.length() == 0 ) this.containedClasses = clazz; else this.containedClasses += "," + clazz; } } } } /** * Auto generated method comment * * @param simpleName */ public void addContainedClass(String simpleName) { if ( simpleName != null && simpleName.length() > 0 ) { Set<String> classes = this.getContainedClassSet(); if ( classes == null ) classes = new HashSet<String>(); if ( !classes.contains(simpleName) ) classes.add(simpleName); this.setContainedClassSet(classes); } } public Set<SyncServerRecord> getServerRecords() { return serverRecords; } public void setServerRecords(Set<SyncServerRecord> serverRecords) { this.serverRecords = serverRecords; } public SyncServerRecord getServerRecord(RemoteServer server) { SyncServerRecord ret = null; if ( server != null && this.serverRecords != null ) { for ( SyncServerRecord record : this.serverRecords ) { // changed to using server ids to avoid an NPE in file transfers if ( server.getServerId().equals(record.getSyncServer().getServerId())) { ret = record; } } } return ret; } public RemoteServer getForServer() { return forServer; } public void setForServer(RemoteServer forServer) { this.forServer = forServer; } public Map<RemoteServer, SyncServerRecord> getRemoteRecords() { Map<RemoteServer, SyncServerRecord> ret = new LinkedHashMap<RemoteServer, SyncServerRecord>(); if ( this.serverRecords != null ) { for ( SyncServerRecord serverRecord : this.serverRecords ) { ret.put(serverRecord.getSyncServer(), serverRecord); } } return ret; } /** * Internally, sync items are stored as LinkedHashMap, the key into it is: uuid + action + contained type * * @param item SyncItem for which to derive map key * @return string value of the key */ private static String deriveMapKey(SyncItem item) { return item.getKey().getKeyValue().toString() + item.getState().toString() + ((item.getContainedType() == null) ? "null" : item.getContainedType().getName()); // (was getSimpleName instead of just getName) } public boolean isOutgoing() { return getUuid().equals(getOriginalUuid()); } @Override public String toString() { return "SyncRecord(" + getRecordId() + ") contains " + getContainedClasses(); } /** * Adds a SyncServerRecord to this SyncRecord for the given <code>server</code> * * @param server the RemoteServer to make this get sent to */ public void addServerRecord(RemoteServer server) { // only add this if there isn't one for this server already if (getServerRecord(server) == null) serverRecords.add(new SyncServerRecord(server, this)); } }