org.photovault.replication.Change.java Source code

Java tutorial

Introduction

Here is the source code for org.photovault.replication.Change.java

Source

/*
  Copyright (c) 2008 Harri Kaimio
      
  This file is part of Photovault.
    
  Photovault 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 2 of the License, or
  (at your option) any later version.
    
  Photovault 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 Photovault; if not, write to the Free Software Foundation,
  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/

package org.photovault.replication;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.Lob;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Transient;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 Description of a change made to an object.
     
 Change is an immutable object describing some atomic change made to another 
 object. In that way, it can ve considered also as a version identifier for the 
 object state at certain moment of time.
 <p>
 It consists of information about changed status (field value changes and changed 
 associations to other objects) and reference to the change identifying the version
 of changed object before the change. Based on these, an unique identifier is 
 calculated.
 <p>
 The change object will have two distinct phases in its life cycle. When it is 
 created, it is in unfrozen start, and information about the state change can be 
 modified. After the object is frozed by calling the freeze() method, it cannot 
 be modified anymore.
     
 @see ObjectHistory
     
 @author Harri Kaimio
 @since 0.6
     
 @param <T> Class of the object whose changes are being tracked.
 */
@Entity
@Table(name = "pv_changes")
public class Change<T> {

    static private UUID NULL_UUID = UUID.fromString("00000000-0000-0000-0000-000000000000");

    static private Log log = LogFactory.getLog(Change.class.getName());

    /**
     ObjectHistory object handling change history of the target object
     */
    private ObjectHistory targetHistory;

    /**
     Has this change been frozen?
     */
    private boolean frozen = false;

    /**
     Unique ID for this change, calculated when the change is frozen
     */
    private UUID uuid;

    /**
     Previous change on top of which this change can be applied
     */
    private Change prevChange;

    /**
     True if this change is a head of a branch (i.e. it does not have any child 
     changes
     */
    private boolean head = true;

    /**
     Changes that are based on this change
     */
    private Set<Change<T>> childChanges = new HashSet<Change<T>>();

    private Set<Change<T>> parentChanges = new HashSet<Change<T>>();

    /**
     Fields of {@link target} that have been changed by this change. If this field
     * is <code>null</code>, it will be initialized from {@link #serializedForm}
     * by calling {@link #initChangedFields()} before accessing it.
     */
    private Map<String, FieldChange> changedFields = null;

    /**
     If this is a merge change, conflicting fields
     */
    private Map<String, ValueFieldConflict> fieldConflicts = new HashMap<String, ValueFieldConflict>();

    /**
     This change serialized to XML. This byte string is actually defining this 
     change as UUID is calucated from it. The form is determined as part of 
     freezing the change - if change is not freezed this is <code>null</code>
     */
    private byte[] serializedForm = null;

    /**
     Default constructor, for construction by Hibernate or {@link ChangeFactory}.
     This constructor set the frozen field to true, so it is extremely important 
     that the change must not be accessed by user code before second phase of 
     construction has been finalized!!!
     */
    Change() {
        frozen = true;
    }

    /**
     Constructor. This constructor is used by ObjectHistory class to create 
     new changes
         
     @param t The ObjectHistory object handling change history of target
     */
    Change(ObjectHistory<T> t) {
        targetHistory = t;
        changedFields = new HashMap<String, FieldChange>();
    }

    @Id
    @Column(name = "change_uuid")
    @org.hibernate.annotations.Type(type = "org.photovault.persistence.UUIDUserType")
    public UUID getUuid() {
        return uuid;
    }

    void setUuid(UUID uuid) {
        this.uuid = uuid;
    }

    @ManyToOne(targetEntity = ObjectHistory.class)
    @JoinColumn(name = "target_uuid")
    ObjectHistory<T> getTargetHistory() {
        return targetHistory;
    }

    void setTargetHistory(ObjectHistory<T> h) {
        targetHistory = h;
    }

    /**
     Set field value. 
     @param fieldName The field to be changed
     @param newValue New value for the field
     @throws IllegalStateException if the change has laready been frozen.
     */
    public void setField(String fieldName, Object newValue) {
        assertNotFrozen();
        changedFields.put(fieldName, new ValueChange(fieldName, newValue));
    }

    /**
     * Change a property of a field that is a Java bean
     * @param fieldName Name of the field
     * @param subfield property to be changed
     * @param newValue New value for the property
     */
    public void setFieldProperty(String fieldName, String property, Object newValue) {
        assertNotFrozen();
        ValueChange existingChange = (ValueChange) changedFields.get(fieldName);
        if (existingChange == null) {
            changedFields.put(fieldName, new ValueChange(fieldName, property, newValue));
        } else {
            existingChange.addPropChange(property, newValue);
        }
    }

    /**
     Get field value. The If the field is not modified by this change, return 
     the value of the field after previous change.
     @param field The field that will be returned
     @return Value that the field will have after the change is applied.
     @todo What if this is a merge change and there is a conflict
     */
    public Object getField(String field) {
        if (changedFields == null) {
            initChangedFields();
        }
        if (changedFields.containsKey(field)) {
            FieldChange lastChange = changedFields.get(field);
            if (lastChange instanceof ValueChange) {
                return ((ValueChange) lastChange).getValue();
            }
        } else if (prevChange != null) {
            return prevChange.getField(field);
        }
        return null;
    }

    FieldChange getFieldChange(String field) {
        if (changedFields == null) {
            initChangedFields();
        }
        return changedFields.get(field);
    }

    void setFieldChange(String field, FieldChange c) {
        changedFields.put(field, c);
    }

    @Column(name = "serialized", length = 1048576)
    @Lob
    byte[] getSerializedChange() {
        return serializedForm;
    }

    void setSerializedChange(byte[] c) {
        if (c != null) {
            serializedForm = Arrays.copyOf(c, c.length);
        }
    }

    /**
     Get all fields modified in this change
     @return Map from the field description objects to their new values.
     */
    @Transient
    public Map<String, FieldChange> getChangedFields() {
        if (changedFields == null) {
            initChangedFields();
        }
        return Collections.unmodifiableMap(changedFields);
    }

    void setChangedFields(Map<String, FieldChange> changes) {
        changedFields = changes;
    }

    private void initChangedFields() {
        if (serializedForm == null || targetHistory == null) {
            throw new RuntimeException("Cannot initialize changed fields yet");
        }
        try {
            Class targetClass = Class.forName(targetHistory.getTargetClassName());
            ChangeDTO<T> dto = ChangeDTO.createChange(serializedForm, targetClass);
            changedFields = dto.changedFields;
        } catch (ClassNotFoundException ex) {
            throw new RuntimeException("Cannot find class " + targetHistory.getTargetClassName(), ex);
        }

    }

    @ManyToMany(targetEntity = Change.class, cascade = CascadeType.ALL)
    @JoinTable(name = "pv_change_relations", joinColumns = {
            @JoinColumn(name = "child_uuid") }, inverseJoinColumns = { @JoinColumn(name = "parent_uuid") })
    public Set<Change<T>> getParentChanges() {
        return parentChanges;
    }

    void setParentChanges(Set changes) {
        parentChanges = changes;
    }

    @Column(name = "head")
    public boolean isHead() {
        return head;
    }

    void setHead(boolean b) {
        head = b;
    }

    /**
     Returns the previous change on top of which this change is applied
     @return the first parent change
     @deprecated As there can be many parents, use {@link #getParentChanges() }
     instead.
     */
    @Transient
    public Change getPrevChange() {
        if (prevChange == null && !parentChanges.isEmpty()) {
            prevChange = parentChanges.iterator().next();
        }
        return prevChange;
    }

    /**
     Set the predecessor of this change
     @param c
     @throws IllegalStateException if the change has already been frozen
     */
    public void setPrevChange(Change<T> c) {
        assertNotFrozen();
        if (c.getTargetHistory() != targetHistory) {
            throw new IllegalArgumentException("Cannot be based on change to different object");
        }
        if (prevChange != null) {
            parentChanges.remove(prevChange);
        }
        parentChanges.add(c);
        prevChange = c;
    }

    /**
     Returns the children of this change.
     */
    @ManyToMany(mappedBy = "parentChanges", targetEntity = Change.class)
    public Set<Change<T>> getChildChanges() {
        return childChanges;
    }

    void setChildChanges(Set c) {
        childChanges = c;
    }

    /**
     Add a child to thi change
     @param child The new child
     */
    void addChildChange(Change child) {
        childChanges.add(child);
        head = false;
    }

    /**
     Removes a child change
     @param child The chipd to be removed
     */
    void removeChildChange(Change child) {
        childChanges.remove(child);
        head = childChanges.isEmpty();
    }

    @Transient
    public boolean isFrozen() {
        return frozen;
    }

    /**
     Returns <code>true</code> if this change has unresolved conflicts
     */
    public boolean hasConflicts() {
        if (changedFields == null) {
            initChangedFields();
        }
        for (FieldChange fc : changedFields.values()) {
            if (fc.getConflicts().size() > 0) {
                return true;
            }
        }
        return false;
    }

    /**
     Returns all field conflicts
     @return
     */
    @Transient
    public Collection<FieldConflictBase> getFieldConficts() {
        if (changedFields == null) {
            initChangedFields();
        }
        List<FieldConflictBase> conflicts = new ArrayList<FieldConflictBase>();
        for (FieldChange fc : changedFields.values()) {
            conflicts.addAll(fc.getConflicts());
        }
        return conflicts;
    }

    /**
     Creates a change that merges 2 branches into one. The change will contain 
     needed operations so that it will change target object to same state 
     when applied to either branch head. If a field is modified in onluy one 
     branch it will be set in this change as well. If a field is modified 
     differently in the branches, a conflict is created.
     @param other The change that will be merged with this one
     @return A new merge change.
     */
    public Change<T> merge(Change<T> other) {
        assertFrozen();
        if (other.targetHistory != targetHistory) {
            throw new IllegalArgumentException("Cannot merge changes to separate objects");
        }
        if (changedFields == null) {
            initChangedFields();
        }
        // Find the common ancestor version
        Set<Change> ancestors = new HashSet<Change>();
        for (Change c = this; c != null; c = c.getPrevChange()) {
            ancestors.add(c);
        }
        Change<T> commonBase = null;
        Map<String, FieldChange> changedFieldsOther = new HashMap();
        for (Change<T> c = other; c != null; c = c.getPrevChange()) {
            if (ancestors.contains(c)) {
                commonBase = c;
                break;
            }

            // record all field changes that are still valid currently
            for (Map.Entry<String, FieldChange> e : c.getChangedFields().entrySet()) {
                if (!changedFieldsOther.containsKey(e.getKey())) {
                    try {
                        changedFieldsOther.put(e.getKey(), (FieldChange) e.getValue().clone());
                    } catch (CloneNotSupportedException ex) {
                        log.error(ex);
                        throw new IllegalStateException("All FieldValue objects must be cloneable");
                    }
                } else {
                    FieldChange fc = changedFieldsOther.get(e.getKey());
                    fc.addEarlier(e.getValue());
                }
            }
        }

        /*
         Common ancestor is now found, collect changes done between it and 
         this change
         */
        Map<String, FieldChange> changedFieldsThis = new HashMap();
        for (Change<T> c = this; c != commonBase; c = c.getPrevChange()) {
            for (Map.Entry<String, FieldChange> e : c.getChangedFields().entrySet()) {
                FieldChange fc = (FieldChange) changedFieldsThis.get(e.getKey());
                if (fc == null) {
                    try {
                        changedFieldsThis.put(e.getKey(), (FieldChange) e.getValue().clone());
                    } catch (CloneNotSupportedException ex) {
                        log.error(ex);
                        throw new IllegalStateException("All FieldValue objects must be cloneable");
                    }
                } else {
                    fc.addEarlier(e.getValue());
                }
            }
        }

        /*
         Combine the changes. If field is changed only in one path or it gets 
         same value in both, it can be merged automatically. Otherwise, 
         conflict is created
         */
        Change<T> merged = new Change<T>(targetHistory);
        merged.parentChanges.add(this);
        merged.parentChanges.add(other);
        Set<String> allChangedFields = new HashSet<String>(changedFieldsThis.keySet());
        allChangedFields.addAll(changedFieldsOther.keySet());
        for (String f : allChangedFields) {
            boolean changedInThis = changedFieldsThis.containsKey(f);
            boolean changedInOther = changedFieldsOther.containsKey(f);
            try {
                if (changedInThis && !changedInOther) {
                    merged.setFieldChange(f, (FieldChange) changedFieldsThis.get(f).clone());
                } else if (!changedInThis && changedInOther) {
                    merged.setFieldChange(f, (FieldChange) changedFieldsOther.get(f).clone());
                } else {
                    // The field has been changed in both paths
                    FieldChange chThis = changedFieldsThis.get(f);
                    FieldChange chOther = changedFieldsOther.get(f);
                    FieldChange chMerged = chThis.merge(chOther);
                    merged.setFieldChange(f, chMerged);
                }
            } catch (CloneNotSupportedException ex) {
                log.error(ex);
                throw new IllegalStateException("FieldChanges must support cloning", ex);
            }

        }

        return merged;
    }

    /**
     Helper method to find the change in which a field was last modified
     @param field The field name
     @param start The change whose ancestors are looked
     @return The change in which f was modified
     */
    private Change<T> findLastFieldChange(String field, Change<T> start) {
        if (changedFields == null) {
            initChangedFields();
        }
        Change<T> c = start;
        while (!c.changedFields.containsKey(field)) {
            c = c.prevChange;
        }
        return c;
    }

    /**
     Add a new conflict
     @param c The new conflict
     */
    private void addFieldConflict(ValueFieldConflict c) {
        fieldConflicts.put(c.getFieldName(), c);
    }

    /**
     Calculate UUID for this change
     */
    private void calcUuid() {
        ChangeDTO<T> data = new ChangeDTO<T>(this);
        try {
            uuid = data.calcUuid();

        } catch (IOException e) {
            log.error("Error calculating change UUID: " + e.getMessage());
            log.error(e);
        }
    }

    /**
     Serializes the field changes to a stream
     @param s
     */
    private void writeFieldChanges(ObjectOutputStream s) throws IOException {
        if (changedFields == null) {
            initChangedFields();
        }
        s.writeInt(changedFields.size());
        List<String> fieldsSorted = new ArrayList<String>(changedFields.keySet());
        Collections.sort(fieldsSorted);
        for (String f : fieldsSorted) {
            s.writeObject(f);
            s.writeObject(changedFields.get(f));
        }
    }

    private void readFieldChanges(ObjectInputStream s) throws IOException, ClassNotFoundException {
        int count = s.readInt();
        changedFields = new HashMap<String, FieldChange>();
        for (int n = 0; n < count; n++) {
            String field = (String) s.readObject();
            FieldChange val = (FieldChange) s.readObject();
            changedFields.put(field, val);
        }
    }

    /**
     Freeze this change. When change is freezed
     <ul>
     <li>If the prevChange has not been set, the current version of 
     target object is set as parent of this change.</li>
     <li>The change is added to change hsitory of the target obejct</li>
     <li>UUID of this change is calculated from serialized form of the cahnge</li>
     </ul>
         
     After freezing a change it cannot be modified anymore.
         
     TODO: Now calling initFirstChange here is dangerous because it cannot 
     initialize fields with special resolvers properly. This method should 
     probably throw an exception instead.
     */
    public void freeze() {
        if (hasConflicts()) {
            throw new IllegalStateException("Cannot freeze change that has unresolved conflicts");
        }

        // TODO: Parent change handling should probably be done in editor...
        if (parentChanges.isEmpty()) {
            Change<T> targetVersion = targetHistory.getVersion();
            if (targetVersion != null) {
                parentChanges.add(targetHistory.getVersion());
            }
        }

        /* Construct serialized XML form and UUID */
        ChangeDTO<T> dto = new ChangeDTO<T>(this);
        serializedForm = dto.getXmlData();
        uuid = dto.changeUuid;
        frozen = true;
        for (Change<T> parent : parentChanges) {
            parent.addChildChange(this);
        }
        targetHistory.addChange(this);
        //        targetHistory.applyChange( this );
    }

    /**
     Helper method to check that this cahnge is frozen and throw exception if it
     is not.
     @throws IllegalStateException if the change is not frozen
     */
    private void assertFrozen() {
        if (!frozen) {
            throw new IllegalStateException("Change is not frozen");
        }
    }

    /**
     Helper method to check that this cahnge is not frozen and throw exception 
     if it is.
     @throws IllegalStateException if the change is frozen
     */
    private void assertNotFrozen() {
        if (frozen) {
            throw new IllegalStateException("Change is frozen");
        }
    }

    @Override
    public boolean equals(Object o) {
        if (o == null) {
            return false;
        }
        if (!(o instanceof Change)) {
            return false;
        }
        Change c = (Change) o;
        if (!c.frozen) {
            throw new IllegalArgumentException("Cannot compare equality to non-frozen change!!!");
        }
        return (c.uuid == uuid || (uuid != null && uuid.equals(c.uuid)));
    }

    @Override
    public int hashCode() {
        if (!frozen) {
            throw new IllegalArgumentException("Cannot compare equality to non-frozen change!!!");
        }
        int hash = 3;
        hash = 89 * hash + (this.uuid != null ? this.uuid.hashCode() : 0);
        return hash;
    }

    @Override
    public String toString() {
        StringBuffer buf = new StringBuffer();
        buf.append("UUID: ").append(this.uuid).append("\n");
        buf.append("Target: ").append(this.targetHistory.getTargetUuid()).append("\n");
        buf.append("Predecessors:\n");
        if (parentChanges.size() == 0) {
            buf.append("  None\n");
        }
        for (Change c : this.parentChanges) {
            buf.append("  ");
            buf.append(c.getUuid());
            buf.append("\n");
        }
        buf.append("Changed fields:\n");
        if (changedFields == null || changedFields.isEmpty()) {
            buf.append("  None\n");
        } else {
            for (Map.Entry<String, FieldChange> fc : this.changedFields.entrySet()) {
                buf.append("  ");
                buf.append(fc.getKey());
                buf.append(" -> ");
                buf.append(fc.getValue());
                buf.append("\n");
            }
        }
        return buf.toString();
    }
}