/** * */ package org.korsakow.ide; import java.util.ArrayList; import java.util.ConcurrentModificationException; import java.util.List; import org.w3c.dom.Document; /** * Implements simple thread-safe read/write on a dom via commits. Any thread may call getDocument() to obtain a thread-local * copy of the DOM. Changes may then be committed via commit(). * * Evidently, complex writes are not supported. Trying to commit an out-of-date document will throw a ConcurrentModificationException. * * The intended use case is either a single thread writing with multiple threads reading, or at least a situation where the writes * are otherwise regulated by the application such that invalid commits won't occur. * * TODO: not so happy with the class name * * @author d * */ public class DomSession { public static class VersionInfo { private int version = 0; private Document document = null; public VersionInfo() { } public VersionInfo(VersionInfo other) { setVersion(other.getVersion()); if (other.getDocument() != null) this.setDocument(cloneDocument(other.getDocument())); } public int getVersion() { return version; } public void setVersion(int version) { this.version = version; } public Document getDocument() { return document; } public void setDocument(Document document) { this.document = document; } } private static Document cloneDocument(Document document) { return (Document)document.cloneNode(true); } /** * The working copy. */ private final ThreadLocal<VersionInfo> threadLocal = new ThreadLocal<VersionInfo>() { @Override protected VersionInfo initialValue() { VersionInfo info = new VersionInfo(); synchronized (headLock) { info.setVersion(headVersion.getVersion()); } return info; } }; private final VersionInfo headVersion = new VersionInfo(); private final Object headLock = new Object(); private final List<VersionInfo> history = new ArrayList<VersionInfo>(); public DomSession(Document document) { headVersion.setVersion(0); headVersion.setDocument(document); } public VersionInfo getThreadLocal() { return threadLocal.get(); } /** * This method was introduced for unit testing. * @return */ public Document getHeadDocument() { synchronized (headLock) { return cloneDocument(headVersion.getDocument()); } } /** * This method was introduced for unit testing. * @return */ public long getHeadVersion() { synchronized (headLock) { return headVersion.getVersion(); } } public long getVersion() { VersionInfo localInfo = getThreadLocal(); return localInfo.getVersion(); } public boolean isUptoDate() { VersionInfo localInfo = getThreadLocal(); synchronized (headLock) { return localInfo.getVersion() == headVersion.getVersion(); } } /** * Thread-safe call which obtains a thread-local copy of the DOM. The local.getVersion() is checked against the head.getVersion() * so that a copy need not be made every call but only when the head has changed. * * That is, the same Document instance is returned unless the head has changed. * * @return */ public Document getDocument() { VersionInfo localInfo = getThreadLocal(); synchronized (headLock) { if (localInfo.getDocument() == null || localInfo.getVersion() != headVersion.getVersion()) { localInfo.setDocument(cloneDocument(headVersion.getDocument())); localInfo.setVersion(headVersion.getVersion()); } } return localInfo.getDocument(); } /** * This is effectively like doing local dom manipulations. * @param doc */ public void setDocument(Document doc) { VersionInfo localInfo = getThreadLocal(); localInfo.setDocument(doc); } /** * Commits the local document to the head. If the local.getVersion() does not match the head.getVersion() (ie another thread has committed since the last get), * a ConcurrentModificationException is thrown. * *.getVersion() is incremented even if the current document is identical to the head document * * @throws ConcurrentModificationException */ public void commit() throws ConcurrentModificationException { VersionInfo localInfo = getThreadLocal(); synchronized (headLock) { if (localInfo.getVersion() != headVersion.getVersion()) throw new ConcurrentModificationException(String.format("another thread has committed v%d since the last update v%d", headVersion.getVersion(), localInfo.getVersion())); VersionInfo historyInfo = new VersionInfo(); historyInfo.setVersion(headVersion.getVersion()); historyInfo.setDocument(cloneDocument(headVersion.getDocument())); history.add(historyInfo); while (history.size() > 1) history.remove(0); if (localInfo.getDocument() != null) { headVersion.setDocument(cloneDocument(localInfo.getDocument())); } headVersion.setVersion(headVersion.getVersion() + 1); localInfo.setVersion(headVersion.getVersion()); } localInfo = null; } /** * Reverts the local docment to the head revision, discarding any local changes. */ public void rollbackToHead() { VersionInfo localInfo = getThreadLocal(); synchronized (headLock) { localInfo.setDocument(null); localInfo.setVersion(headVersion.getVersion()); } } /** * * @return false if the rollback would cause a loss of information */ public boolean tryRollbackToHead() { VersionInfo localInfo = getThreadLocal(); synchronized (headLock) { if (localInfo.getVersion() == headVersion.getVersion()) { localInfo.setDocument(null); localInfo.setVersion(headVersion.getVersion()); return true; } } return false; } /** * Rolls back the head to the previous.getVersion() in the history. * If history is empty, equivalent to rollback() */ public void rollbackHeadToPreviousVersion() { VersionInfo localInfo = getThreadLocal(); synchronized (headLock) { VersionInfo prevVersion = null; if (!history.isEmpty()) prevVersion = history.remove(history.size()-1); else prevVersion = headVersion; localInfo.setVersion(prevVersion.getVersion()); headVersion.setVersion(localInfo.getVersion()); headVersion.setDocument(cloneDocument(prevVersion.getDocument())); localInfo.setDocument(cloneDocument(prevVersion.getDocument())); } } public List<VersionInfo> getHistory() { List<VersionInfo> copy = new ArrayList<VersionInfo>(); synchronized (headLock) { for (VersionInfo info : history) copy.add(new VersionInfo(info)); } return copy; } }