/* ****************************************************************************** * Copyright (c) 2006-2012 XMind Ltd. and others. * * This file is a part of XMind 3. XMind releases 3 and * above are dual-licensed under the Eclipse Public License (EPL), * which is available at http://www.eclipse.org/legal/epl-v10.html * and the GNU Lesser General Public License (LGPL), * which is available at http://www.gnu.org/licenses/lgpl.html * See http://www.xmind.net/license.html for details. * * Contributors: * XMind Ltd. - initial API and implementation *******************************************************************************/ package org.xmind.core.internal.dom; import static org.xmind.core.internal.dom.DOMConstants.ATTR_CREATOR_NAME; import static org.xmind.core.internal.dom.DOMConstants.ATTR_CREATOR_VERSION; import static org.xmind.core.internal.dom.DOMConstants.ATTR_MEDIA_TYPE; import static org.xmind.core.internal.dom.DOMConstants.ATTR_NEXT_REVISION_NUMBER; import static org.xmind.core.internal.dom.DOMConstants.ATTR_RESOURCE; import static org.xmind.core.internal.dom.DOMConstants.ATTR_RESOURCE_ID; import static org.xmind.core.internal.dom.DOMConstants.ATTR_REVISION_NUMBER; import static org.xmind.core.internal.dom.DOMConstants.ATTR_TIMESTAMP; import static org.xmind.core.internal.dom.DOMConstants.TAG_REVISION; import static org.xmind.core.internal.dom.DOMConstants.TAG_REVISION_CONTENT; import java.io.IOException; import java.io.OutputStream; import java.util.Iterator; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xmind.core.Core; import org.xmind.core.CoreException; import org.xmind.core.IAdaptable; import org.xmind.core.IFileEntry; import org.xmind.core.IMeta; import org.xmind.core.IRevision; import org.xmind.core.IWorkbook; import org.xmind.core.event.ICoreEventListener; import org.xmind.core.event.ICoreEventRegistration; import org.xmind.core.event.ICoreEventSource; import org.xmind.core.event.ICoreEventSupport; import org.xmind.core.internal.RevisionManager; import org.xmind.core.internal.zip.ArchiveConstants; import org.xmind.core.util.DOMUtils; /** * @author Frank Shaka */ public class RevisionManagerImpl extends RevisionManager implements ICoreEventSource, INodeAdaptableFactory { private Document implementation; private WorkbookImpl ownedWorkbook; private NodeAdaptableRegistry registry; private ICoreEventSupport coreEventSupport; private String dirPath; private boolean registered = false; /** * */ public RevisionManagerImpl(Document implementation, WorkbookImpl ownedWorkbook, String dirPath) { this.implementation = implementation; this.ownedWorkbook = ownedWorkbook; this.dirPath = dirPath; this.registry = new NodeAdaptableRegistry(implementation, this); } @Override public int hashCode() { return implementation.hashCode(); } @Override public boolean equals(Object obj) { if (obj == this) return true; if (obj == null || !(obj instanceof RevisionManagerImpl)) return false; RevisionManagerImpl that = (RevisionManagerImpl) obj; return implementation.equals(that.implementation); } @Override public <T> T getAdapter(Class<T> adapter) { if (ICoreEventSource.class.equals(adapter)) return adapter.cast(this); if (adapter.isAssignableFrom(Document.class)) return adapter.cast(getImplementation()); return super.getAdapter(adapter); } public Document getImplementation() { return implementation; } protected Element getRevisionsElement() { return implementation.getDocumentElement(); } protected INodeAdaptableFactory getAdaptableFactory() { return ownedWorkbook; } public IAdaptable createAdaptable(Node node) { return new RevisionImpl((Element) node, this); } protected String getDirPath() { return dirPath; } public String getResourceId() { return getRevisionsElement().getAttribute(ATTR_RESOURCE_ID); } public String getContentType() { return getRevisionsElement().getAttribute(ATTR_MEDIA_TYPE); } public Iterator<IRevision> iterRevisions() { return new DOMUtils.AdaptableIterator<IRevision>(getRevisionsElement(), TAG_REVISION, registry, false); } public Iterator<IRevision> iterRevisionsReversed() { return new DOMUtils.AdaptableIterator<IRevision>(getRevisionsElement(), TAG_REVISION, registry, true); } public IRevision getLatestRevision() { Node node = getLastRevisionNode(); return node == null ? null : (IRevision) registry.getAdaptable(node); } protected Node getLastRevisionNode() { Node node = getRevisionsElement().getLastChild(); while (node != null && !DOMUtils.isElementByTag(node, TAG_REVISION)) { node = node.getPreviousSibling(); } return node; } public int getNextRevisionNumber() { String num = getRevisionsElement() .getAttribute(ATTR_NEXT_REVISION_NUMBER); return NumberUtils.safeParseInt(num, 1); } public IRevision addRevision(IAdaptable content) throws IOException, CoreException { Node node = (Node) content.getAdapter(Node.class); if (node == null) throw new CoreException(Core.ERROR_INVALID_ARGUMENT, "Invalid content for content type: " + getContentType()); //$NON-NLS-1$ IRevision latest = getLatestRevision(); if (latest != null) { IAdaptable latestContent = latest.getContent(); if (latestContent != null) { Object latestNode = latestContent.getAdapter(Node.class); if (latestNode != null && latestNode.equals(node)) return null; } } int revNum = getNextRevisionNumber(); long timestamp = System.currentTimeMillis(); String path = takeSnapshot(node, revNum, timestamp); RevisionImpl revision = createRevision(revNum, timestamp, path); setNextRevisionNumber(revNum + 1); if (!isOrphan()) { revision.addNotify(ownedWorkbook); fireTargetEvent(Core.RevisionAdd, revision); } return revision; } private void setNextRevisionNumber(int nextRevNum) { getRevisionsElement().setAttribute(ATTR_NEXT_REVISION_NUMBER, String.valueOf(nextRevNum)); } private RevisionImpl createRevision(int revNum, long timestamp, String path) { Element ele = implementation.createElement(TAG_REVISION); ele.setAttribute(ATTR_REVISION_NUMBER, String.valueOf(revNum)); ele.setAttribute(ATTR_TIMESTAMP, String.valueOf(timestamp)); ele.setAttribute(ATTR_RESOURCE, path); ele.setAttribute(ATTR_CREATOR_NAME, getOwnedWorkbook().getMeta().getValue(IMeta.CREATOR_NAME)); ele.setAttribute(ATTR_CREATOR_VERSION, getOwnedWorkbook().getMeta().getValue(IMeta.CREATOR_VERSION)); getRevisionsElement().appendChild(ele); return new RevisionImpl(ele, this); } private String takeSnapshot(Node source, int revNum, long timestamp) throws IOException, CoreException { Document doc = DOMUtils.createDocument(TAG_REVISION_CONTENT); Element docEle = doc.getDocumentElement(); NS.setNS(NS.Revision, docEle, NS.Xhtml, NS.Xlink, NS.SVG, NS.Fo); Node snapshot = doc.importNode(source, true); docEle.appendChild(snapshot); String path = getDirPath() + getFileName(revNum, timestamp, "xml"); //$NON-NLS-1$ IFileEntry entry = ownedWorkbook.getManifest().createFileEntry(path); OutputStream stream = entry.openOutputStream(); DOMUtils.save(doc, stream, true); return path; } private String getFileName(int revNum, long timestamp, String extName) { return String.format("rev-%s-%s.%s", revNum, timestamp, extName); //$NON-NLS-1$ } public Object removeRevision(IRevision revision) { RevisionImpl rev = (RevisionImpl) revision; Element ele = rev.getImplementation(); Element parentEle = getRevisionsElement(); int index = DOMUtils.getNodeIndex(parentEle, ele); if (index < 0) return null; if (!isOrphan()) rev.removeNotify(ownedWorkbook); parentEle.removeChild(ele); if (!isOrphan()) fireTargetEvent(Core.RevisionRemove, revision); return Integer.valueOf(index); } public void restoreRevision(IRevision revision, Object removal) { if (!(removal instanceof Integer)) throw new IllegalArgumentException("Invalid removal object"); //$NON-NLS-1$ int index = ((Integer) removal).intValue(); RevisionImpl rev = (RevisionImpl) revision; Element ele = rev.getImplementation(); Element parentEle = getRevisionsElement(); NodeList childNodes = parentEle.getChildNodes(); if (index < childNodes.getLength()) { parentEle.insertBefore(ele, childNodes.item(index)); } else { parentEle.appendChild(ele); } if (!isOrphan()) { ((RevisionImpl) revision).addNotify(ownedWorkbook); fireTargetEvent(Core.RevisionAdd, revision); } } public IWorkbook getOwnedWorkbook() { return ownedWorkbook; } public boolean isOrphan() { return !registered; } protected void addNotify(WorkbookImpl workbook) { registered = true; increaseFileEntryReference(); Iterator<IRevision> it = iterRevisions(); while (it.hasNext()) { RevisionImpl rev = (RevisionImpl) it.next(); rev.addNotify(workbook); fireTargetEvent(Core.RevisionAdd, rev); } } protected void removeNotify(WorkbookImpl workbook) { Iterator<IRevision> it = iterRevisionsReversed(); while (it.hasNext()) { RevisionImpl rev = (RevisionImpl) it.next(); rev.removeNotify(workbook); fireTargetEvent(Core.RevisionRemove, rev); } decreaseFileEntryReference(); registered = false; } private void increaseFileEntryReference() { IFileEntry entry = getMetaFileEntry(); if (entry != null) { entry.increaseReference(); } } private void decreaseFileEntryReference() { IFileEntry entry = getMetaFileEntry(); if (entry != null) { entry.decreaseReference(); } } private IFileEntry getMetaFileEntry() { String path = dirPath + ArchiveConstants.REVISIONS_XML; return ownedWorkbook.getManifest().getFileEntry(path); } /* * (non-Javadoc) * @see org.xmind.core.event.ICoreEventSource#getCoreEventSupport() */ public ICoreEventSupport getCoreEventSupport() { if (coreEventSupport == null) { coreEventSupport = ownedWorkbook.getCoreEventSupport(); } return coreEventSupport; } /* * (non-Javadoc) * @see * org.xmind.core.event.ICoreEventSource#registerCoreEventListener(java. * lang.String, org.xmind.core.event.ICoreEventListener) */ public ICoreEventRegistration registerCoreEventListener(String type, ICoreEventListener listener) { return getCoreEventSupport().registerGlobalListener(type, listener); } private void fireTargetEvent(String eventType, IRevision revision) { getCoreEventSupport().dispatchTargetChange(this, eventType, revision); } }