/* This file is part of VoltDB.
* Copyright (C) 2008-2017 VoltDB Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with VoltDB. If not, see <http://www.gnu.org/licenses/>.
*/
package org.hsqldb_voltpatches;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* Used to fake generate XML without actually generating the text and parsing it.
* A performance optimization, and something of a simplicity win
*
*/
public class VoltXMLElement {
public String name;
public final Map<String, String> attributes = new TreeMap<String, String>();
public final List<VoltXMLElement> children = new ArrayList<VoltXMLElement>();
public VoltXMLElement(String name) {
this.name = name;
}
public VoltXMLElement withValue(String key, String value) {
attributes.put(key, value);
return this;
}
public boolean hasValue(String key, String value) {
if (key == null || value == null) {
return false;
}
return value.equals(attributes.get(key));
}
/**
* The elements generated by the HSQL output have the name field for
* tables, columns, indexes, etc set to the type of object, and the actual
* name (like, table name) is in the name attribute, if it exists. If the
* name attribute does not exist, then we fall back to the minstring
* representation of the tree. Essentially, an attribute named object is
* sufficiently unique-ified by the name, and so we can detect and
* represent changes to it, but changes to unnamed objects will just get
* represented by element deletion and addition.
*
* This has some unfortunately side effects for some interesting top-level
* elements that we work around by manually forcing them to have the same
* attribute name as their element name. In particular, the top level
* 'databaseschema' and the collections of columns, indexes, and
* constraints for a table.
*/
public String getUniqueName() {
if (attributes.containsKey("name")) {
return name + attributes.get("name");
}
else {
return toMinString();
}
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
append(sb, 0);
return sb.toString();
}
private String indentStr(int indent) {
StringBuffer sb = new StringBuffer();
while (5 <= indent) {
sb.append("....|");
indent -= 5;
}
while (0 <= indent) {
sb.append(".");
indent -= 1;
}
return sb.toString();
}
private void append(StringBuilder sb, int indent) {
sb.append(indentStr(indent)).append("ELEMENT: ").append(name).append("\n");
for (Entry<String, String> e : attributes.entrySet()) {
sb.append(indentStr(indent+2)).append(e.getKey());
sb.append(" = ").append(e.getValue()).append("\n");
}
if ( ! children.isEmpty()) {
sb.append(indentStr(indent)).append("[").append("\n");
for (VoltXMLElement e : children) {
e.append(sb, indent+4);
}
}
}
public VoltXMLElement duplicate() {
VoltXMLElement retval = new VoltXMLElement(name);
for (Entry<String, String> e : attributes.entrySet()) {
retval.attributes.put(e.getKey(), e.getValue());
}
for (VoltXMLElement child : children) {
retval.children.add(child.duplicate());
}
return retval;
}
/**
* Given a name, recursively find all the children with matching name, if any.
*/
public List<VoltXMLElement> findChildrenRecursively(String name)
{
List<VoltXMLElement> retval = new ArrayList<VoltXMLElement>();
for (VoltXMLElement vxe : children) {
if (name.equals(vxe.name)) {
retval.add(vxe);
}
retval.addAll(vxe.findChildrenRecursively(name));
}
return retval;
}
/**
* Given a name, find all the immediate children with matching name, if any.
*/
public List<VoltXMLElement> findChildren(String name)
{
List<VoltXMLElement> retval = new ArrayList<VoltXMLElement>();
for (VoltXMLElement vxe : children) {
if (name.equals(vxe.name)) {
retval.add(vxe);
}
}
return retval;
}
/**
* Given an value in the format of that returned by getUniqueName, find the
* child element which matches, if any.
*/
public VoltXMLElement findChild(String uniqueName)
{
for (VoltXMLElement vxe : children) {
if (uniqueName.equals(vxe.getUniqueName())) {
return vxe;
}
}
return null;
}
/**
* Given the element name ('table') and the attribute name ('foo'), find
* the matching child element, if any
*/
public VoltXMLElement findChild(String elementName, String attributeName)
{
String attName = attributeName;
if (attName == null) {
attName = "default";
}
return findChild(elementName + attName);
}
/**
* Get a string representation that is designed to be as short as possible
* with as much certainty of uniqueness as possible.
* A SHA-1 hash would suffice, but here's hoping just dumping to a string is
* faster. Will measure later.
*/
public String toMinString() {
StringBuilder sb = new StringBuilder();
toMinString(sb);
return sb.toString();
}
protected StringBuilder toMinString(StringBuilder sb) {
sb.append("\tE").append(name).append('\t');
for (Entry<String, String> e : attributes.entrySet()) {
sb.append('\t').append(e.getKey());
sb.append('\t').append(e.getValue());
}
sb.append("\t[");
for (VoltXMLElement e : children) {
e.toMinString(sb);
}
return sb;
}
/**
* Represent the differences between two VoltXMLElements with the same
* getUniqueName().
*/
static public class VoltXMLDiff
{
final String m_name;
List<VoltXMLElement> m_addedElements = new ArrayList<VoltXMLElement>();
List<VoltXMLElement> m_removedElements = new ArrayList<VoltXMLElement>();
Map<String, VoltXMLDiff> m_changedElements = new HashMap<String, VoltXMLDiff>();
Map<String, String> m_addedAttributes = new HashMap<String, String>();
Set<String> m_removedAttributes = new HashSet<String>();
Map<String, String> m_changedAttributes = new HashMap<String, String>();
// To maintain the final order of elements, brute force it and
// just write down the order of elements present in the 'after' state
SortedMap<String, Integer> m_elementOrder = new TreeMap<String, Integer>();
// Takes the VoltXMLElement unique name
public VoltXMLDiff(String name)
{
m_name = name;
}
public String getName()
{
return m_name;
}
public List<VoltXMLElement> getAddedNodes()
{
return m_addedElements;
}
public List<VoltXMLElement> getRemovedNodes()
{
return m_removedElements;
}
public Map<String, VoltXMLDiff> getChangedNodes()
{
return m_changedElements;
}
public Map<String, String> getAddedAttributes()
{
return m_addedAttributes;
}
public Set<String> getRemovedAttributes()
{
return m_removedAttributes;
}
public Map<String, String> getChangedAttributes()
{
return m_changedAttributes;
}
public boolean isEmpty()
{
return (m_addedElements.isEmpty() &&
m_removedElements.isEmpty() &&
m_changedElements.isEmpty() &&
m_addedAttributes.isEmpty() &&
m_removedAttributes.isEmpty() &&
m_changedAttributes.isEmpty());
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("NAME: " + m_name + "\n");
sb.append("ADDED: " + m_addedAttributes + "\n");
sb.append("REMOVED: " + m_removedAttributes + "\n");
sb.append("CHANGED: " + m_changedAttributes + "\n");
sb.append("NEW CHILDREN:\n");
for (VoltXMLElement add : m_addedElements) {
sb.append(add.toString());
}
sb.append("DEAD CHILDREN:\n");
for (VoltXMLElement remove : m_removedElements) {
sb.append(remove.toString());
}
sb.append("CHANGED CHILDREN:\n");
for (VoltXMLDiff change : m_changedElements.values()) {
sb.append(change.toString());
}
sb.append("\n\n");
return sb.toString();
}
}
/**
* Compute the diff necessary to turn the 'before' tree into the 'after'
* tree.
*/
static public VoltXMLDiff computeDiff(VoltXMLElement before, VoltXMLElement after)
{
// Top level call needs both names to match (I think this makes sense)
if (!before.getUniqueName().equals(after.getUniqueName())) {
// not sure this is best behavior, ponder as progress is made
return null;
}
VoltXMLDiff result = new VoltXMLDiff(before.getUniqueName());
// Short-circuit check for any differences first. Can return early if there are no changes
if (before.toMinString().equals(after.toMinString())) {
return result;
}
// Store the final desired element order
for (int i = 0; i < after.children.size(); i++) {
VoltXMLElement child = after.children.get(i);
result.m_elementOrder.put(child.getUniqueName(), i);
}
// first, check the attributes
Set<String> firstKeys = before.attributes.keySet();
Set<String> secondKeys = new HashSet<String>();
secondKeys.addAll(after.attributes.keySet());
// Do removed and changed attributes walking the first element's attributes
for (String firstKey : firstKeys) {
if (!secondKeys.contains(firstKey)) {
result.m_removedAttributes.add(firstKey);
}
else if (!(after.attributes.get(firstKey).equals(before.attributes.get(firstKey)))) {
result.m_changedAttributes.put(firstKey, after.attributes.get(firstKey));
}
// remove the firstKey from secondKeys to track things added
secondKeys.remove(firstKey);
}
// everything in secondKeys should be something added
for (String key : secondKeys) {
result.m_addedAttributes.put(key, after.attributes.get(key));
}
// Now, need to check the children. Each pair of children with the same names
// need to be descended to look for changes
// Probably more efficient ways to do this, but brute force it for now
// Would be helpful if the underlying children objects were Maps rather than
// Lists.
Set<String> firstChildren = new HashSet<String>();
for (VoltXMLElement child : before.children) {
firstChildren.add(child.getUniqueName());
}
Set<String> secondChildren = new HashSet<String>();
for (VoltXMLElement child : after.children) {
secondChildren.add(child.getUniqueName());
}
Set<String> commonNames = new HashSet<String>();
for (VoltXMLElement firstChild : before.children) {
if (!secondChildren.contains(firstChild.getUniqueName())) {
// Need to duplicate the
result.m_removedElements.add(firstChild);
}
else {
commonNames.add(firstChild.getUniqueName());
}
}
for (VoltXMLElement secondChild : after.children) {
if (!firstChildren.contains(secondChild.getUniqueName())) {
result.m_addedElements.add(secondChild);
}
else {
assert(commonNames.contains(secondChild.getUniqueName()));
}
}
// This set contains uniquenames
for (String name : commonNames) {
VoltXMLDiff childDiff = computeDiff(before.findChild(name), after.findChild(name));
if (!childDiff.isEmpty()) {
result.m_changedElements.put(name, childDiff);
}
}
return result;
}
public boolean applyDiff(VoltXMLDiff diff)
{
// Can only apply a diff to the root at which it was generated
assert(getUniqueName().equals(diff.m_name));
// Do the attribute changes
attributes.putAll(diff.getAddedAttributes());
for (String key : diff.getRemovedAttributes()) {
attributes.remove(key);
}
for (Entry<String,String> e : diff.getChangedAttributes().entrySet()) {
attributes.put(e.getKey(), e.getValue());
}
// Do the node removals and additions
for (VoltXMLElement e : diff.getRemovedNodes()) {
children.remove(findChild(e.getUniqueName()));
}
for (VoltXMLElement e : diff.getAddedNodes()) {
children.add(e);
}
// To do the node changes, recursively apply the inner diffs to the children
for (Entry<String, VoltXMLDiff> e : diff.getChangedNodes().entrySet()) {
findChild(e.getKey()).applyDiff(e.getValue());
}
// Hacky, we don't write down the element order if there were no diffs
if (diff.m_elementOrder.isEmpty()) {
return true;
}
// Reorder the children
// yes, not efficient. Revisit on performance pass
assert(children.size() == diff.m_elementOrder.size());
List<VoltXMLElement> temp = new ArrayList<VoltXMLElement>();
temp.addAll(children);
for (VoltXMLElement child : temp) {
String name = child.getUniqueName();
Integer position = diff.m_elementOrder.get(name);
if (position == null) {
throw new RuntimeException("You have encountered an unexpected error. Please contact VoltDB support, and include your current schema along with the DDL changes you were attempting to make.");
}
if (!name.equals(children.get(position).getUniqueName())) {
children.set(position, child);
}
}
return true;
}
/**
* Recursively extract sub elements of a given name with matching attribute if it is not null.
* @param elementName element name to look for
* @param attrName optional attribute name to look for
* @param attrValue optional attribute value to look for
* @return output collection containing the matching sub elements
*/
public List<VoltXMLElement> extractSubElements(String elementName, String attrName, String attrValue) {
assert(elementName != null);
assert((elementName != null && attrValue != null) || attrName == null);
List<VoltXMLElement> elements = new ArrayList<>();
extractSubElements(elementName, attrName, attrValue, elements);
return elements;
}
private void extractSubElements(String elementName, String attrName, String attrValue, List<VoltXMLElement> elements) {
if (elementName.equalsIgnoreCase(name)) {
if (attrName == null || attrValue.equals(attributes.get(attrName))) {
elements.add(this);
}
}
for (VoltXMLElement child : children) {
child.extractSubElements(elementName, attrName, attrValue, elements);
}
}
}