com.dmurph.mvc.model.HashModel Maven / Gradle / Ivy
/**
* Copyright (c) 2010 Daniel Murphy
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* Created on Jul 13, 2010, 3:41:56 PM
*/
package com.dmurph.mvc.model;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.HashMap;
import java.util.Iterator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.dmurph.mvc.I18n;
import com.dmurph.mvc.ICloneable;
import com.dmurph.mvc.IDirtyable;
import com.dmurph.mvc.IModel;
import com.dmurph.mvc.IRevertible;
import com.dmurph.mvc.support.AbstractMVCSupport;
import com.dmurph.mvc.support.ISupportable;
import com.dmurph.mvc.support.RevertibleSupport;
import com.dmurph.mvc.support.RevertibleSupport.PropertyWrapper;
/**
* Model that stores all properties in a HashMap, so all {@link IDirtyable}, {@link ICloneable}, and
* {@link IRevertible} functionality is handled internally.
*
* This class also will forward all calls to it's members if implement the associated interface.
* For example, if {@link #revertChanges()} is called, then, after
* reverting any changes to this model, it will call {@link IRevertible#revertChanges()} on any property
* that is {@link IRevertible}. This can get dangerous if your property tree goes in a loop (you'll
* get infinite calls). In that case you can override {@link #isDeepMVCEnabled(String)} to return false for
* properties that you don't want any calls forwarded to, or if you want more control, you can override
* {@link AbstractMVCSupport#cloneImpl(String, Object)}, {@link AbstractMVCSupport#revertChangesImpl(String, Object)},
* {@link AbstractMVCSupport#isDirtyImpl(String, Object)}, {@link AbstractMVCSupport#setDirtyImpl(String, Object)},
* or {@link AbstractMVCSupport#saveChangesImpl(String, Object)} to prevent this as well.
*
* @author Daniel Murphy
*
*/
public class HashModel extends AbstractMVCSupport implements IDirtyable, ICloneable, IRevertible, IModel{
private static final long serialVersionUID = 2L;
private static final Logger log = LoggerFactory.getLogger(HashModel.class);
private final HashMap propertyMap = new HashMap();
private final RevertibleSupport revertibleSupport;
public enum PropertyType{
/**
* Property that can't be set by
* calling {@link HashModel#setProperty(String, Object)},
* but can be set by the extending class by calling
* {@link HashModel#registerProperty(String, PropertyType, Object)}
* again.
*/
READ_ONLY,
/**
* Property that can be read and written to by the
* {@link HashModel#getProperty(String)} and
* {@link HashModel#setProperty(String, Object)}
* methods.
*/
READ_WRITE,
/**
* Property that, after registration, cannot be set again
* by either accessing classes or the implementing class.
* This guarantees that the object returned from
* {@link HashModel#getProperty(String)} will always be
* the correct reference.
*/
FINAL
}
// for listening to dirty updates from children
private final PropertyChangeListener childPropertyChangeListener = new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent argEvt) {
if(argEvt.getPropertyName().equals(IModel.DIRTY)){
if(argEvt.getNewValue() == Boolean.TRUE){
propertyChangeSupport.firePropertyChange(argEvt);
}
}
}
};
/**
* Constructs a hash model with an {@link IModel#DIRTY} property.
*/
public HashModel(){
revertibleSupport = new RevertibleSupport(propertyChangeSupport, new ISupportable() {
public void setProperty(String argPropertyName, Object argProperty) {
HashModel.this.setProperty(argPropertyName, argProperty);
}
}, this);
registerProperty(DIRTY, PropertyType.READ_WRITE, false);
}
/**
* Constructs a hash model with an {@link IModel#DIRTY} property, and
* the given properties all with property type of {@link PropertyType#READ_WRITE}.
* @param argProperties
*/
public HashModel(String[] argProperties){
this();
registerProperty(argProperties, PropertyType.READ_WRITE);
}
private void addListener(Object argObject){
if(argObject instanceof IModel){
((IModel) argObject).addPropertyChangeListener(childPropertyChangeListener);
}
}
private void removeListener(Object argObject){
if(argObject instanceof IModel){
((IModel) argObject).removePropertyChangeListener(childPropertyChangeListener);
}
}
/**
* Register a property, with initial value of null.
* @param argKey
* @param argType the change permissions of the property.
*/
protected void registerProperty(String argKey, PropertyType argType){
registerProperty(argKey, argType, null);
}
/**
* Register a property with an initial value. This is the only way to set
* {@link PropertyType#READ_ONLY} properties.
* @param argKey
* @param argType the change permissions of the property
* @param argInitial
* @throws PropertyPermissionException thrown if someone tries to set the property of a {@link PropertyType#FINAL}
* property.
*/
protected synchronized void registerProperty(String argKey, PropertyType argType, Object argInitial){
ModelProperty mp;
if(propertyMap.containsKey(argKey)){
mp = propertyMap.get(argKey);
if(mp.type == PropertyType.FINAL){
throw new PropertyPermissionException(I18n.getText("model.propertyFinal", argKey));
}
}else{
mp = new ModelProperty();
}
mp.type = argType;
mp.prop = argInitial;
mp.name = argKey;
addListener(mp.prop);
propertyMap.put(argKey, mp);
}
/**
* Register an array of properties all of the same property type
* @param argKeys
* @param argType
*/
protected synchronized void registerProperty(String[] argKeys, PropertyType argType){
for(String s: argKeys){
registerProperty(s, argType, null);
}
}
/**
* Sets a property, and will only set the property if it's {@link PropertyType} is
* {@link PropertyType#READ_WRITE}. If the property isn't defined, it will be registered
* and set with the property type of {@link PropertyType#READ_WRITE}.
* @see #getPropertyType(String)
* @throws PropertyPermissionException thrown if someone tries to set the property of a {@link PropertyType#FINAL}
* property.
*/
public synchronized Object setProperty(String argKey, Object argProperty){
if(propertyMap.containsKey(argKey)){
ModelProperty mp = propertyMap.get(argKey);
switch(mp.type){
case READ_WRITE:
if(mp.prop == argProperty){
return argProperty;
}
Object old = mp.prop;
removeListener(old);
mp.prop = argProperty;
addListener(mp.prop);
firePropertyChange(argKey, old, argProperty);
if(!argKey.equals(DIRTY)){
setProperty(DIRTY, true);
}
return old;
case READ_ONLY:
if(log.isWarnEnabled()){
log.warn("Attempt to change the property of the READ_ONLY property '{}' from '{}' to '{}'",
new Object[]{argKey, mp.prop, argProperty});
}
return mp.prop;
case FINAL:
throw new PropertyPermissionException(I18n.getText("model.propertyFinal", argKey));
}
return null;
}else{
registerProperty(argKey, PropertyType.READ_WRITE, null);
return setProperty(argKey, argProperty);
}
}
/**
* Get a property
* @param argKey
* @return
*/
public synchronized Object getProperty(String argKey){
ModelProperty mp = propertyMap.get(argKey);
if(mp != null){
return mp.prop;
}else{
return null;
}
}
/**
* Get the {@link PropertyType} for a property.
* @param argKey
* @return
*/
public synchronized PropertyType getPropertyType(String argKey){
ModelProperty mp = propertyMap.get(argKey);
if(mp != null){
return mp.type;
}else{
return null;
}
}
/**
* Gets the names of all the properties.
* @return
*/
public synchronized String[] getPropertyNames(){
return propertyMap.keySet().toArray(new String[0]);
}
/**
* Tells you if this hash model contains the given property.
* @param argProperty
* @return
*/
public synchronized boolean containsProperty(String argProperty){
return propertyMap.containsKey(argProperty);
}
/**
* @see ICloneable#clone()
*/
@Override
public ICloneable clone(){
HashModel model = new HashModel();
model.cloneFrom(this);
return model;
}
// clears properties of the model, making sure the remove listeners
// from any properties that are IModels
private void cleanClear(){
Iterator it = propertyMap.values().iterator();
while(it.hasNext()){
removeListener(it.next().prop);
it.remove();
}
}
/**
* Clones from another HashModel, and makes sure to copy any values
* in the model that are {@link ICloneable}. It watches for references
* to argOther
and sets them to this
.
* @see com.dmurph.mvc.ICloneable#cloneFrom(com.dmurph.mvc.ICloneable)
*/
public synchronized void cloneFrom(ICloneable argOther) {
if(argOther instanceof HashModel){
cleanClear();
HashModel other = (HashModel) argOther;
for(String key: other.propertyMap.keySet()){
ModelProperty mp = other.propertyMap.get(key);
registerProperty(key, mp.type);
// references itself
if(mp.prop == argOther){
setProperty(key, this);
continue;
}
setProperty(key, cloneImpl(key, mp.prop));
}
}else{
throw new RuntimeException("Not a HashModel");
}
}
/**
* If false, this is the equivalent of {@link #saveChanges()}
* @see IDirtyable#setDirty(boolean)
*/
public synchronized void setDirty(boolean argDirty) {
setProperty(DIRTY, argDirty);
if(argDirty == false){
for(String key: propertyMap.keySet()){
ModelProperty mp = propertyMap.get(key);
setDirtyImpl(key, mp.prop);
}
saveChanges();
}
}
/**
* @see com.dmurph.mvc.IDirtyable#isDirty()
*/
public synchronized boolean isDirty() {
if((Boolean)getProperty(DIRTY)){
return true;
}
for(PropertyWrapper prop : revertibleSupport.getRecordedProperties()){
if(prop.isDirty()){
return true;
}
}
boolean ret = false;
for(String key: propertyMap.keySet()){
ModelProperty mp = propertyMap.get(key);
ret = ret || isDirtyImpl(key, mp.prop);
if(ret){
return ret;
}
}
return ret;
}
/**
* @see com.dmurph.mvc.IRevertible#revertChanges()
*/
public synchronized void revertChanges() {
revertibleSupport.revertChanges();
for(String key: propertyMap.keySet()){
ModelProperty mp = propertyMap.get(key);
revertChangesImpl(key, mp.prop);
}
setProperty(DIRTY, false);
}
/**
* @see com.dmurph.mvc.IRevertible#saveChanges()
*/
public synchronized void saveChanges() {
setProperty(DIRTY, false);
revertibleSupport.saveChanges();
for(String key: propertyMap.keySet()){
ModelProperty mp = propertyMap.get(key);
saveChangesImpl(key, mp.prop);
}
}
/**
* @see java.lang.Object#toString()
*/
public String toString(){
StringBuilder sb = new StringBuilder();
sb.append("HashModel[");
for(String s: propertyMap.keySet()){
sb.append(s);
sb.append("=");
sb.append(propertyMap.get(s).prop);
sb.append(", ");
}
sb.delete(sb.length()-2, sb.length());
sb.append("]");
return sb.toString();
}
public void printModel(){
System.out.println(toString());
}
/**
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
HashModel other = (HashModel) obj;
if (propertyMap == null) {
if (other.propertyMap != null)
return false;
}
else if (!propertyMap.equals(other.propertyMap))
return false;
return true;
}
private static class ModelProperty{
PropertyType type;
Object prop;
String name;
/**
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
ModelProperty other = (ModelProperty) obj;
if (prop == null) {
if (other.prop != null)
return false;
}
else if (!prop.equals(other.prop))
return false;
if (type != other.type)
return false;
return true;
}
/**
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return name+"-"+prop+"-"+type;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy