/* * Copyright 2014 the original author or authors. * * Licensed under the Apache License, Version 2.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://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.sync.diffsync; import java.util.List; import org.springframework.sync.Diff; import org.springframework.sync.Patch; import org.springframework.sync.util.DeepCloneUtils; /** * <p> * Implements essential steps of the Differential Synchronization routine as described in Neil Fraser's paper at https://neil.fraser.name/writing/sync/eng047-fraser.pdf. * </p> * * <p> * The Differential Synchronization routine can be summarized as follows (with two nodes, A and B): * </p> * * <ol> * <li>Node A compares a resource with its local shadow of that resource to produce a patch describing the differences</li> * <li>Node A replaces the shadow with the resource.</li> * <li>Node A sends the difference patch to Node B.</li> * <li>Node B applies the patch to its copy of the resource as well as its local shadow of the resource.</li> * </ol> * * <p> * The routine then repeats with Node A and B swapping roles, forming a continuous loop. * </p> * * <p> * To fully understand the Differential Synchronization routine, it's helpful to recognize that a shadow can only be changed by applying a patch or by producing a * difference patch; a resource may be changed by applying a patch or by operations performed outside of the loop. * </p> * * <p> * This class implements the handling of an incoming patch separately from the producing of the outgoing difference patch. * It performs no persistence of the patched resources, which is the responsibility of the caller. * </p> * * @author Craig Walls * * @param <T> The entity type to perform differential synchronization against. */ public class DiffSync<T> { private ShadowStore shadowStore; private Class<T> entityType; /** * Constructs the Differential Synchronization routine instance. * @param shadowStore the shadow store * @param entityType the type of entity this DiffSync works with */ public DiffSync(ShadowStore shadowStore, Class<T> entityType) { this.shadowStore = shadowStore; this.entityType = entityType; } /** * Applies one or more patches to a target object and the target object's shadow, per the Differential Synchronization algorithm. * The target object will remain unchanged and a patched copy will be returned. * * @param target An object to apply a patch to. Will remain unchanged. * @param patches The patches to be applied. * @return a patched copy of the target. */ public T apply(T target, Patch...patches) { T result = target; for (Patch patch : patches) { result = apply(patch, result); } return result; } /** * Applies a patch to a target object and the target object's shadow, per the Differential Synchronization algorithm. * The target object will remain unchanged and a patched copy will be returned. * * @param patch The patch to be applied. * @param target An object to apply a patch to. Will remain unchanged. * @return a patched copy of the target. */ public T apply(Patch patch, T target) { if (patch.size() == 0) { return target; } Shadow<T> shadow = getShadow(target); if (patch instanceof VersionedPatch) { VersionedPatch versionedPatch = (VersionedPatch) patch; if (versionedPatch.getServerVersion() < shadow.getServerVersion()) { // e.g., if (patch.serverVersion < shadow.serverVersion) shadow = getBackupShadow(target); putShadow(shadow); } } if (shouldApplyPatch(patch, shadow)) { shadow = new Shadow<T>(patch.apply(shadow.getResource(), entityType), shadow.getServerVersion(), shadow.getClientVersion() + 1); Shadow<T> backupShadow = new Shadow<T>(shadow.getResource(), shadow.getServerVersion(), shadow.getClientVersion()); putShadow(shadow); putBackupShadow(backupShadow); return patch.apply(DeepCloneUtils.deepClone(target), entityType); } return target; } /** * Applies one or more patches to a target list and the target list's shadow, per the Differential Synchronization algorithm. * The target object will remain unchanged and a patched copy will be returned. * * @param patches The patch to be applied. * @param target A list to apply a patch to. Will remain unchanged. * @return a patched copy of the target. */ public List<T> apply(List<T> target, Patch...patches) { List<T> result = target; for (Patch patch : patches) { result = apply(patch, result); } return result; } /** * Applies a patch to a target list and the target list's shadow, per the Differential Synchronization algorithm. * The target object will remain unchanged and a patched copy will be returned. * * @param patch The patch to be applied. * @param target A list to apply a patch to. Will remain unchanged. * @return a patched copy of the target. */ public List<T> apply(Patch patch, List<T> target) { if (patch.size() == 0) { return target; } Shadow<List<T>> shadow = getShadow(target); if (patch instanceof VersionedPatch) { VersionedPatch versionedPatch = (VersionedPatch) patch; if (versionedPatch.getServerVersion() < shadow.getServerVersion()) { shadow = getBackupShadow(target); putListShadow(shadow); } } if (shouldApplyPatch(patch, shadow)) { shadow = new Shadow<List<T>>(patch.apply(shadow.getResource(), entityType), shadow.getServerVersion(), shadow.getClientVersion() + 1); Shadow<List<T>> backupShadow = new Shadow<List<T>>(shadow.getResource(), shadow.getServerVersion(), shadow.getClientVersion()); putListShadow(shadow); putBackupListShadow(backupShadow); return patch.apply(DeepCloneUtils.deepClone(target), entityType); } return target; } /** * Compares a target object with its shadow, producing a patch describing the difference. * Upon completion, the shadow will be replaced with the target, per the Differential Synchronization algorithm. * @param target The target object to produce a difference patch for. * @return a {@link VersionedPatch} describing the differences between the target and its shadow. */ public VersionedPatch diff(T target) { Shadow<T> shadow = getShadow(target); Patch diff = Diff.diff(shadow.getResource(), target); VersionedPatch vDiff = new VersionedPatch(diff.getOperations(), shadow.getServerVersion(), shadow.getClientVersion()); T patched = diff.apply(shadow.getResource(), entityType); shadow = new Shadow<T>(patched, shadow.getServerVersion() + 1, shadow.getClientVersion()); putShadow(shadow); return vDiff; } /** * Compares a target list with its shadow, producing a patch describing the difference. * Upon completion, the shadow will be replaced with the target, per the Differential Synchronization algorithm. * @param target The target list to produce a difference patch for. * @return a {@link VersionedPatch} describing the differences between the target and its shadow. */ public VersionedPatch diff(List<T> target) { Shadow<List<T>> shadow = getShadow(target); Patch diff = Diff.diff(shadow.getResource(), target); VersionedPatch vDiff = new VersionedPatch(diff.getOperations(), shadow.getServerVersion(), shadow.getClientVersion()); List<T> patched = diff.apply(shadow.getResource(), entityType); shadow = new Shadow<List<T>>(patched, shadow.getServerVersion() + 1, shadow.getClientVersion()); putListShadow(shadow); return vDiff; } // private helper methods private boolean shouldApplyPatch(Patch patch, Shadow<?> shadow) { if (!(patch instanceof VersionedPatch)) return true; VersionedPatch versionedPatch = (VersionedPatch) patch; return versionedPatch.getServerVersion() == shadow.getServerVersion() && versionedPatch.getClientVersion() == shadow.getClientVersion(); } @SuppressWarnings("unchecked") private Shadow<T> getShadow(T target) { String shadowStoreKey = getShadowStoreKey(target); Shadow<T> shadow = (Shadow<T>) shadowStore.getShadow(shadowStoreKey); if (shadow == null) { shadow = new Shadow<T>(DeepCloneUtils.deepClone(target), 0, 0); // OKAY } return shadow; } @SuppressWarnings("unchecked") private Shadow<T> getBackupShadow(T target) { String shadowStoreKey = getShadowStoreKey(target) + "_backup"; Shadow<T> shadow = (Shadow<T>) shadowStore.getShadow(shadowStoreKey); if (shadow == null) { shadow = new Shadow<T>(DeepCloneUtils.deepClone(target), 0, 0); // OKAY } return shadow; } private void putShadow(Shadow<T> shadow) { String shadowStoreKey = getShadowStoreKey(shadow.getResource()); shadowStore.putShadow(shadowStoreKey, shadow); } private void putBackupShadow(Shadow<T> shadow) { String shadowStoreKey = getShadowStoreKey(shadow.getResource()) + "_backup"; shadowStore.putShadow(shadowStoreKey, shadow); } private void putListShadow(Shadow<List<T>> shadow) { String shadowStoreKey = getShadowStoreKey(shadow.getResource()); shadowStore.putShadow(shadowStoreKey, shadow); } private void putBackupListShadow(Shadow<List<T>> shadow) { String shadowStoreKey = getShadowStoreKey(shadow.getResource()) + "_backup"; shadowStore.putShadow(shadowStoreKey, shadow); } @SuppressWarnings("unchecked") private Shadow<List<T>> getShadow(List<T> target) { String shadowStoreKey = getShadowStoreKey(target); Shadow<List<T>> shadow = (Shadow<List<T>>) shadowStore.getShadow(shadowStoreKey); if (shadow == null) { shadow = new Shadow<List<T>>(DeepCloneUtils.deepClone(target), 0, 0); // OKAY } return shadow; } @SuppressWarnings("unchecked") private Shadow<List<T>> getBackupShadow(List<T> target) { String shadowStoreKey = getShadowStoreKey(target) + "_backup"; Shadow<List<T>> shadow = (Shadow<List<T>>) shadowStore.getShadow(shadowStoreKey); if (shadow == null) { shadow = new Shadow<List<T>>(DeepCloneUtils.deepClone(target), 0, 0); // OKAY } return shadow; } private String getShadowStoreKey(T t) { return "shadow/" + entityType.getSimpleName(); } private String getShadowStoreKey(List<T> t) { return "shadow/" + entityType.getSimpleName() + "List"; } }