
com.zipwhip.binding.RecordBase Maven / Gradle / Ivy
package com.zipwhip.binding;
import com.zipwhip.events.DataEventObject;
import com.zipwhip.binding.fields.Field;
import com.zipwhip.binding.fields.LongField;
import com.zipwhip.events.Observable;
import com.zipwhip.events.ObservableHelper;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
/**
* Created by IntelliJ IDEA.
* User: Michael
* Date: 11/26/11
* Time: 8:18 PM
*
* Records are mutable containers of data (represented as "fields").
* They throw events when changed (when u commit the changes).
*/
public abstract class RecordBase implements Record {
/**
* The recordId is required for all records. We need to be able to uniquely identify a record (and its dupes)
* in the stores.
*/
protected static final Field FIELD_RECORD_ID = new LongField("id");
protected static final Object NULL_VALUE = new Object();
// the "change" event
protected ObservableHelper> onChange;
// For efficiency we use the same instance over and over again for events.
private DataEventObject eventObject;
// By default we're committing all changes synchronously
private boolean autoCommit = true;
// The "perspective" is a running tally of original + changes merged together
protected Map perspective = new TreeMap();
protected Map values = new TreeMap();
protected Map changes;
protected Map fields;
/**
* This helps us keep track of what changed.
*/
private enum Changes {
SAME, NOT_EXISTS, DIFFERENT
}
protected RecordBase(Collection fields) {
if (fields == null){
throw new NullPointerException("Fields cannot be null");
}
this.fields = new HashMap(fields.size());
for (Field field : fields) {
this.fields.put(field.getName(), field);
}
validateFieldsExist();
}
protected RecordBase(Field... fields) {
if (fields == null){
throw new NullPointerException("Fields cannot be null");
}
this.fields = new HashMap(fields.length);
for (Field field : fields) {
this.fields.put(field.getName(), field);
}
validateFieldsExist();
}
protected RecordBase(Map fields) {
this.fields = fields;
validateFieldsExist();
}
/**
* Auto commit means that it will internally call commit every time you change a record.
* This has implications in how often the event is fired.
*
* @return true if autoCommit is true, false otherwise
*/
@Override
public boolean isAutoCommit() {
return autoCommit;
}
/**
* Enable/disable auto committing changes.
*
* @param autoCommit true if the transactions should be automatically committed
*/
@Override
public boolean setAutoCommit(boolean autoCommit) throws Exception {
this.autoCommit = autoCommit;
// only commit if auto commit is true.
if (autoCommit && isDirty()){
return commit();
}
return false;
}
/**
* Alias for setAutoCommit. (It does NOT do internal counting to ensure 3 begins are followed by 3 ends.
*/
@Override
public void beginEdit() {
try {
this.setAutoCommit(false);
} catch (Exception e) {
// can't happen, we're not doing a commit.'
}
}
/**
* Alias for setAutoCommit. (It does NOT do internal counting to ensure 3 begins are followed by 3 ends.
*/
@Override
public boolean endEdit() throws Exception {
return this.setAutoCommit(true);
}
@Override
public boolean commit() throws Exception {
if (changes != null) {
// validate that this record has good data.
if (!validate()){
return false;
}
// do the commit here.
values.putAll(changes);
changes.clear();
// the perspective does not need to change, it's already up to date.
changes = null;
// finish the commit, let everyone know.
if (this.onChange != null) {
this.onChange.notifyObservers(this, getEventObject());
}
return true;
}
return false;
}
@Override
public boolean isDirty() {
return changes != null;
}
@Override
public boolean validate() throws Exception {
// the fields/data have already been validated when they were inserted.
// this is more of a "full record wide" validation.
// check a "set" of the fields
// subclasses should override this and provide different implementations of it.
// only throw if you have no idea if the data is valid or not.
return true;
}
@Override
public void revert() {
if (changes != null) {
// replace the values (eliminating the changes)
perspective.putAll(values);
// clear/null the changes
changes.clear();
changes = null;
}
}
protected void set(String field, Object value) throws Exception {
Field f = fields.get(field);
_set(f, value);
}
/**
* If you are absolutely sure that your input is good (type safe) then use this method to not have to do
* a manual try/catch
*
* @param field
* @param value
*/
protected void set(Field field, T value) {
try {
_set(field, (Object) value);
} catch (Exception e) {
// this wont happen, we did pre-type checking.
// (might happen if value out of bounds?)
e.printStackTrace();
}
}
/**
* This one is a little more private, so i prefixed it with an underscore
*
* @param field
* @param crazyValue
* @param
* @throws Exception
*/
protected void _set(Field field, Object crazyValue) throws Exception {
if (field == null) {
throw new NullPointerException("The field cannot be null");
}
// prep the value
Object value = prepareValue(field, crazyValue);
// check to see if it already existed in values.
// if it returns false it's a new addition, so we won't flag for changes.
Changes modification = detectChanges(values, field, value);
switch (modification) {
case SAME:
// noop we dont care.
return;
case NOT_EXISTS:
// values.put(field, value);
// perspective.put(field, value);
// // let's not throw an event, since this is not considered a "value change"
// return;
break;
case DIFFERENT:
// shit, we need to start noticing complicated changes.
break;
}
if (autoCommit) {
values.put(field, value);
perspective.put(field, value);
// we dont need to store the perspective here, because we're in auto commit mode.
// NOTE: only throw the event when you commit
// we're in autoCommit mode, so throw the change
if (onChange != null) {
onChange.notifyObservers(this, getEventObject());
}
return;
}
if (changes == null) {
// this needs to be populated no matter what.
changes = new HashMap();
}
modification = detectChanges(changes, field, value);
switch (modification) {
case SAME:
// noop we dont care.
break;
case NOT_EXISTS:
case DIFFERENT:
changes.put(field, value);
// update the perspective so it has the new data
perspective.put(field, value);
// NOTE: only throw event when commit
// fire that we changed a value.
// if (onChange != null){
// onChange.fireEvent(getEventObject());
// }
}
}
protected T prepareValue(Field field, Object crazyValue) throws Exception {
// null wrap
if (crazyValue == null) {
crazyValue = NULL_VALUE;
}
T value;
// field definitions are optional
if (field.validateRawInput(crazyValue)) {
value = field.convert(crazyValue);
if (!field.validateBeforeSet(value)) {
throw new IllegalArgumentException("Validation failed");
}
} else {
throw new IllegalArgumentException("Validation failed");
}
return value;
}
protected Object get(String key) {
Field field = fields.get(key);
return get(field);
}
protected T get(Field field) {
Object result;
if (changes != null) {
// "contains" would be a double tap to the data structure.
result = changes.get(field);
// we are protecting NULL from being put in here.
// if it's null, then it's not found.
if (result != null) {
if (result == NULL_VALUE) {
return null;
} else {
return (T) result;
}
}
}
result = values.get(field);
if (result == NULL_VALUE) {
return null;
} else {
return (T) result;
}
}
/**
* Caching the singleton instance
*
* @return
*/
protected DataEventObject getEventObject() {
// lazy create single guy
if (eventObject == null) {
eventObject = new DataEventObject(this, this);
}
return eventObject;
}
protected Changes detectChanges(Map data, Field key, Object value) {
if (data == null) {
return Changes.NOT_EXISTS;
}
Object v = data.get(key);
if (v == null) {
// since we protect against null values, this means it's not in the store.
return Changes.NOT_EXISTS;
}
// lets see if these are the same?
if (v.equals(value)) {
return Changes.SAME;
} else {
return Changes.DIFFERENT;
}
}
private void validateFieldsExist() {
if (this.fields == null || this.fields.isEmpty()){
throw new RuntimeException("The fields must be defined for this record");
}
}
public Long getRecordId() {
return this.get(FIELD_RECORD_ID);
}
public void setRecordId(Long id) throws Exception {
if (get(FIELD_RECORD_ID) != null){
throw new Exception("Not allowed to update the id if it's already set.");
}
set(FIELD_RECORD_ID, id);
}
/**
* onChange will only fire if they commit their changes.
*
* @return The observer that will fire on change.
*/
public Observable> onChange() {
if (onChange == null) {
onChange = new ObservableHelper>();
}
return onChange;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy