package org.swellrt.model.generic; import com.google.common.base.Preconditions; import org.swellrt.model.shared.ModelUtils; import org.waveprotocol.wave.model.document.Doc; import org.waveprotocol.wave.model.document.Document; import org.waveprotocol.wave.model.document.ObservableDocument; import org.waveprotocol.wave.model.document.operation.impl.AttributesImpl; import org.waveprotocol.wave.model.document.util.DocHelper; import org.waveprotocol.wave.model.document.util.XmlStringBuilder; import org.waveprotocol.wave.model.id.WaveletId; import org.waveprotocol.wave.model.wave.Blip; import org.waveprotocol.wave.model.wave.ObservableWavelet; import org.waveprotocol.wave.model.wave.opbased.ObservableWaveView; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.logging.Logger; public class ModelMigrator { private static final Logger LOG = Logger.getLogger(ModelMigrator.class.getName()); public static final String PARTICIPANT_SYSTEM = "_system_"; public static final String WAVELET_ROOT = "swl+root"; public static final String TAG_MODEL_METADATA = "model"; public static final String ATTR_MODEL_VERSION = "v"; public static final String DOC_MODEL_ROOT = "model+root"; public static final String DOC_MODEL_VALUES = "model+values"; public static final String DOC_MAP_ROOT = "map+root"; @Deprecated public static final String TAG_STRINGS = "strings"; public static final String TAG_MAP = "map"; public static final String TAG_LIST = "list"; public static final String TAG_VALUES = "values"; public static final String TAG_VALUES_ITEM = "i"; public static final String ATTR_VALUE = "v"; public static final VersionNumber LAST_VERSION = new VersionNumber(1, 0); public static class VersionNumber { private int major; private int minor; public static VersionNumber fromString(String str) { String[] numbers = str.split("\\."); if (numbers == null || numbers.length != 2) return null; try { int major = Integer.valueOf(numbers[0]); int minor = Integer.valueOf(numbers[1]); return new VersionNumber(major, minor); } catch (NumberFormatException e) { return null; } } public VersionNumber(int major, int minor) { super(); this.major = major; this.minor = minor; } public int getMajor() { return major; } public int getMinor() { return minor; } @Override public boolean equals(Object obj) { if (obj instanceof VersionNumber) { VersionNumber vn = (VersionNumber) obj; return (vn.major == this.major) && (vn.minor == this.minor); } return false; } } /** * Calculate model's version according to metadata * * * @param domain * @param wave * @return * @throws NotModelWaveException */ protected static VersionNumber getVersionNumber(String domain, ObservableWaveView wave) throws NotModelWaveException { WaveletId rootWaveletId = WaveletId.of(domain, WAVELET_ROOT); ObservableWavelet rootWavelet = wave.getWavelet(rootWaveletId); if (rootWavelet == null) { throw new NotModelWaveException(); } ObservableDocument docModelRoot = null; if (rootWavelet.getDocumentIds().contains(DOC_MODEL_ROOT)) { docModelRoot = rootWavelet.getDocument(DOC_MODEL_ROOT); } else { throw new NotModelWaveException(); } Doc.E eltModelMetadata = DocHelper.getElementWithTagName(docModelRoot, TAG_MODEL_METADATA); String attrVersion = docModelRoot.getAttribute(eltModelMetadata, ATTR_MODEL_VERSION); return VersionNumber.fromString(attrVersion); } /** * Check if a Wave inner model needs to be migrated and execute incremental * migration process. * * @param domain Domain of this Wave * @param wave Wave to migrate * @throws NotModelWaveException * @return true if migration was succesfull */ public static boolean migrateIfNecessary(String domain, ObservableWaveView wave) { VersionNumber currentVersion; try { currentVersion = getVersionNumber(domain, wave); if (currentVersion.equals(new VersionNumber(0, 2))) { migrate_0_2_to_1_0(domain, wave); currentVersion = new VersionNumber(1, 0); } if (currentVersion.equals(new VersionNumber(1, 0))) { // Here, migration step from 1.0 to X.Y } } catch (NotModelWaveException e) { // TODO Log exception return false; } return true; } /** * Migrate data model from v0.2 to v1.0 * * @param domain * @param wave */ protected static void migrate_0_2_to_1_0(String domain, ObservableWaveView wave) { WaveletId rootWaveletId = WaveletId.of(domain, WAVELET_ROOT); ObservableWavelet rootWavelet = wave.getWavelet(rootWaveletId); Preconditions.checkArgument(rootWavelet != null, "Wavelet root not found"); // -------------------------------------------------------- // 1. Move root map from model+root doc to new map+root doc // -------------------------------------------------------- // The model+root Document docModelRoot = rootWavelet.getDocument(DOC_MODEL_ROOT); // New map+root blip Blip blipMapRoot = rootWavelet.createBlip(DOC_MAP_ROOT); Document docMapRoot = blipMapRoot.getContent(); // Move map's XML to map+root Doc.E eltOriginalMap = DocHelper.getElementWithTagName(docModelRoot, TAG_MAP); XmlStringBuilder xmlMapRoot = XmlStringBuilder.createChildren(docModelRoot, eltOriginalMap); xmlMapRoot.wrap(TAG_MAP); docMapRoot.appendXml(xmlMapRoot); // Delete former XML docModelRoot.deleteNode(eltOriginalMap); // -------------------------------------------------------- // 2. Add new metadata to model, update model version // -------------------------------------------------------- Doc.E eltModel = DocHelper.getElementWithTagName(docModelRoot, TAG_MODEL_METADATA); docModelRoot.setElementAttribute(eltModel, "v", "1.0"); docModelRoot.setElementAttribute(eltModel, "t", "default"); docModelRoot.setElementAttribute(eltModel, "a", "default"); // -------------------------------------------------------- // 3. Trasverse data tree: // - add metadata to each blip // - move primitive values to blips // -------------------------------------------------------- // Put all original string values in an array ArrayList<String> values = new ArrayList<String>(); Doc.E eltValuesList = DocHelper.getElementWithTagName(docModelRoot, TAG_STRINGS); Doc.E valueIndexItem = DocHelper.getFirstChildElement(docModelRoot, eltValuesList); while (valueIndexItem != null) { if (docModelRoot.getTagName(valueIndexItem).equals("s")) { // TODO be careful, we suppose xml items are ordered as array's index String v = docModelRoot.getAttribute(valueIndexItem, "v"); values.add(v); } valueIndexItem = DocHelper.getNextSiblingElement(docModelRoot, valueIndexItem); // Next } LOG.info("Start migration - wave " + ModelUtils.serialize(wave.getWaveId())); // Go into the tree processBlip(domain, blipMapRoot, values, "root", rootWavelet); // Delete old string index docModelRoot.deleteNode(eltValuesList); LOG.info("Stop migration - wave " + ModelUtils.serialize(wave.getWaveId())); } /** * Add metadata to this blip and change value references by actual value * */ private static void processBlip(String domain, Blip blip, List<String> values, String path, ObservableWavelet wavelet) { LOG.info("Processing blip " + blip.getId() + " with path " + path); if (blip.getId().startsWith("map")) { addMetadata(domain, blip, path); processMap(domain, values, blip, path, wavelet); } else if (blip.getId().startsWith("list")) { addMetadata(domain, blip, path); processList(domain, values, blip, path, wavelet); } else if (blip.getId().startsWith("b")) { addMetadataDoc(blip, path); } } /** * Add a metadata section in a Blip. * * */ private static void addMetadata(String domain, Blip blip, String path) { Date now = new Date(); String timestamp = String.valueOf(now.getTime()); // Set the actual creator of the wavelet to keep consistency String pc = blip.getAuthorId().getAddress(); String tc = String.valueOf(blip.getLastModifiedTime()); String pm = PARTICIPANT_SYSTEM + "@" + domain; String tm = timestamp; String p = path; String acl = ""; String ap = "default"; String xml = "<metadata p='" + p + "' pc='" + pc + "' tc='" + tc + "' pm='" + pm + "' tm='" + tm + "' acl='" + acl + "' ap='" + ap + "'></metadata>"; blip.getContent().insertXml(blip.getContent().locate(0), XmlStringBuilder.createFromXmlString(xml)); } private static void addMetadataDoc(Blip blip, String path) { Doc.E bodyElement = DocHelper.getElementWithTagName(blip.getContent(), "body"); if (bodyElement != null) { blip.getContent().setElementAttributes(bodyElement, AttributesImpl.fromUnsortedPairsUnchecked("p", path, "acl", "", "ap", "default")); } } private static int addValue(Document document, Doc.E elementValues, String value) { // Get last <i v="" /> element Doc.E item = DocHelper.getFirstChildElement(document, elementValues); int index = -1; while (item != null) { index++; item = DocHelper.getNextSiblingElement(document, item); } document.createChildElement(elementValues, TAG_VALUES_ITEM, Collections.<String, String> singletonMap(ATTR_VALUE, value)); return index + 1; } /** * * */ private static void processMap(String domain, List<String> values, Blip blip, String path, ObservableWavelet wavelet) { // Create <values> container section Doc.E eltValues = blip.getContent().createChildElement(blip.getContent().getDocumentElement(), TAG_VALUES, Collections.<String, String> emptyMap()); // The <map> container already exists Doc.E eltMap = DocHelper.getElementWithTagName(blip.getContent(), TAG_MAP); Doc.E eltMapEntry = DocHelper.getFirstChildElement(blip.getContent(), eltMap); while (eltMapEntry != null) { String k = blip.getContent().getAttribute(eltMapEntry, "k"); String v = blip.getContent().getAttribute(eltMapEntry, "v"); if (v.startsWith("str+")) { int index = Integer.valueOf(v.substring(4)); String value = values.get(index); // Store value in the <values> section int valueIndex = addValue(blip.getContent(), eltValues, value); // Store value ref in the map blip.getContent().setElementAttribute(eltMapEntry, "v", "str+" + valueIndex); } else { // go into String childPath = path + "." + k; Blip childBlip = wavelet.getBlip(v); processBlip(domain, childBlip, values, childPath, wavelet); } eltMapEntry = DocHelper.getNextSiblingElement(blip.getContent(), eltMapEntry); } } private static void processList(String domain, List<String> values, Blip blip, String path, ObservableWavelet wavelet) { // Create <values> container section Doc.E eltValues = blip.getContent().createChildElement(blip.getContent().getDocumentElement(), TAG_VALUES, Collections.<String, String> emptyMap()); Doc.E eltList = DocHelper.getElementWithTagName(blip.getContent(), TAG_LIST); Doc.E eltListEntry = DocHelper.getFirstChildElement(blip.getContent(), eltList); int i = 0; while (eltListEntry != null) { String r = blip.getContent().getAttribute(eltListEntry, "r"); if (r.startsWith("str+")) { int index = Integer.valueOf(r.substring(4)); String value = "null"; // Check index range to avoid inconsistency if (0 <= index && index < values.size()) value = values.get(index); else LOG.info("Index out of bounds " + index + " in blip " + blip.getId()); // Store value in the <values> section int valueIndex = addValue(blip.getContent(), eltValues, value); // Store value ref in the list blip.getContent().setElementAttribute(eltListEntry, "r", "str+" + valueIndex); blip.getContent().setElementAttribute(eltListEntry, "t", "str"); } else { // go into String childPath = path + "." + i; Blip childBlip = wavelet.getBlip(r); processBlip(domain, childBlip, values, childPath, wavelet); } eltListEntry = DocHelper.getNextSiblingElement(blip.getContent(), eltListEntry); i++; } } }