VectorClock.java

/*
 * Syncany, www.syncany.org
 * Copyright (C) 2011-2016 Philipp C. Heckel <philipp.heckel@gmail.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.syncany.database;

import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Implements a vector clock that records the time stamps of all send and receive
 * events. It contains functions to compare and merge two vector clocks.
 *
 * <p>Vector clocks are a mechanism to track events/actions across multiple distributed
 * clients. Using vector clocks, one can determine relationships between different events.
 * In particular:
 *
 * <ul>
 *  <li>Event A happened before / was caused by event B (cause)</li>
 *  <li>Event B happened after / caused event B (effect)</li>
 *  <li>Event A and B happened simultaneously (no cause/effect relationship)</li>
 * </ul>
 *
 * @author Frits de Nijs
 * @author Peter Dijkshoorn
 * @author Philipp C. Heckel (philipp.heckel@gmail.com)
 */
public class VectorClock extends TreeMap<String, Long> {
	private static final long serialVersionUID = 109876543L;	
	
	private static final Pattern CLOCK_PATTERN = Pattern.compile("\\(([^)]*)\\)");
	private static final int CLOCK_PATTERN_GROUP_CONTENT = 1;
	
	private static final Pattern ENTRY_PATTERN = Pattern.compile("([a-zA-Z]+)(\\d+)");
	private static final int ENTRY_PATTERN_GROUP_NAME = 1;
	private static final int ENTRY_PATTERN_GROUP_TIME = 2;

	public static final Pattern MACHINE_PATTERN = Pattern.compile("[a-zA-Z]+");	

	public enum VectorClockComparison {
		SMALLER, GREATER, EQUAL, SIMULTANEOUS;
	}

	/**
	 * Increases the component of a unit by 1.
	 *
	 * @param unit The identifier of the vector element being increased
	 * @return Returns the new clock value for the given unit
	 */
	public Long incrementClock(String unit) {
		validateUnitName(unit);

		Long newValue = (containsKey(unit)) ? get(unit).longValue() + 1 : 1L;
		put(unit, newValue);

		return newValue;
	}

	private void validateUnitName(String unit) {
		if (!MACHINE_PATTERN.matcher(unit).matches()) {
			throw new RuntimeException("Machine name cannot be empty and must be only characters (A-Z).");
		}
	}

	/**
	 * Set the component of a unit.
	 *
	 * @param unit The identifier of the vector element being set
	 * @param value The new value of the unit being set
	 */
	public void setClock(String unit, long value) {
		validateUnitName(unit);
		put(unit, value);
	}

	/**
	 * Retrieve the unit's value
	 *
	 * @param unit The identifier of the vector element being retrieved
	 * @return Returns the value of the unit (if existent), or <code>null</code> if it does not exist
	 */
	public Long getClock(String unit) {
		return get(unit);
	}

	@Override
	public Long get(Object unit) { // TODO [low] This should not be used, or shoul it? Why inherit from TreeMap?
		Long lResult = super.get(unit);

		if (lResult == null) {
			lResult = 0L;
		}

		return lResult;
	}

	@Override
	public VectorClock clone() {
		return (VectorClock) super.clone();
	}

	@Override
	public String toString() {
		/*
		 * Please note that this is an incredibly important method.
		 * It is used in hundreds of tests! Don't mess with it!
		 */

		Object[] lIDs = keySet().toArray();
		Object[] lRequests = values().toArray();

		StringBuilder builder = new StringBuilder();
		builder.append('(');
		for (int i = 0; i < lRequests.length; i++) {
			builder.append(lIDs[i]);
			builder.append(lRequests[i].toString());

			if (i + 1 < lRequests.length) {
				builder.append(',');
			}
		}

		builder.append(')');

		return builder.toString();
	}
	
	/**
	 * Converts a serialized vector clock back into a {@link VectorClock} object.
	 * @see #toString()
	 */
	public static VectorClock parseVectorClock(String serializedVectorClock) {
		VectorClock vectorClock = new VectorClock();
		
		Matcher clockMatcher = CLOCK_PATTERN.matcher(serializedVectorClock);
		
		if (clockMatcher.matches()) {
			String clockContents = clockMatcher.group(CLOCK_PATTERN_GROUP_CONTENT);
			String[] clockEntries = clockContents.split(",");
			
			for (String clockEntry : clockEntries) {
				Matcher clockEntryMatcher = ENTRY_PATTERN.matcher(clockEntry);
				
				if (clockEntryMatcher.matches()) {
					String machineName = clockEntryMatcher.group(ENTRY_PATTERN_GROUP_NAME);
					Long clockValue = Long.parseLong(clockEntryMatcher.group(ENTRY_PATTERN_GROUP_TIME));
					
					vectorClock.put(machineName, clockValue);
				}
				else {
					throw new IllegalArgumentException("Not a valid vector clock, entry does not match pattern: " + clockEntry);
				}
			}
			
			return vectorClock;
		}
		else {
			throw new IllegalArgumentException("Not a valid vector clock: " + serializedVectorClock);
		}
	}

	/**
	 * VectorClock compare operation. Returns one of four possible values
	 * indicating how clock one relates to clock two:
	 *
	 * VectorComparison.GREATER If One &gt; Two. VectorComparison.EQUAL If One =
	 * Two. VectorComparison.SMALLER If One &lt; Two. VectorComparison.SIMULTANEOUS
	 * If One != Two.
	 *
	 * @param clock1 First Clock being compared.
	 * @param clock2 Second Clock being compared.
	 * @return VectorComparison value indicating how One relates to Two.
	 */
	public static VectorClockComparison compare(VectorClock clock1, VectorClock clock2) {
		// Initially we assume it is all possible things.
		boolean isEqual = true;
		boolean isGreater = true;
		boolean isSmaller = true;

		// Go over all elements in Clock one.
		for (String lEntry : clock1.keySet()) {
			// Compare if also present in clock two.
			if (clock2.containsKey(lEntry)) {
				// If there is a difference, it can never be equal.
				// Greater / smaller depends on the difference.
				if (clock1.get(lEntry) < clock2.get(lEntry)) {
					isEqual = false;
					isGreater = false;
				}
				if (clock1.get(lEntry) > clock2.get(lEntry)) {
					isEqual = false;
					isSmaller = false;
				}
			}
			// Else assume zero (default value is 0).
			else if (clock1.get(lEntry) != 0) {
				isEqual = false;
				isSmaller = false;
			}
		}

		// Go over all elements in Clock two.
		for (String lEntry : clock2.keySet()) {
			// Only elements we have not found in One still need to be checked.
			if (!clock1.containsKey(lEntry) && (clock2.get(lEntry) != 0)) {
				isEqual = false;
				isGreater = false;
			}
		}

		// Return based on determined information.
		if (isEqual) {
			return VectorClockComparison.EQUAL;
		}
		else if (isGreater && !isSmaller) {
			return VectorClockComparison.GREATER;
		}
		else if (isSmaller && !isGreater) {
			return VectorClockComparison.SMALLER;
		}
		else {
			return VectorClockComparison.SIMULTANEOUS;
		}
	}
}