/*
* Copyright (c) 2005-2016 Vincent Vandenschrick. All rights reserved.
*
* This file is part of the Jspresso framework.
*
* Jspresso is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Jspresso 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Jspresso. If not, see <http://www.gnu.org/licenses/>.
*/
package org.jspresso.framework.binding.model;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.reflect.InvocationTargetException;
import org.jspresso.framework.binding.AbstractValueConnector;
import org.jspresso.framework.binding.ConnectorBindingException;
import org.jspresso.framework.binding.ICollectionConnector;
import org.jspresso.framework.binding.ICompositeValueConnector;
import org.jspresso.framework.model.IModelChangeListener;
import org.jspresso.framework.model.IModelProvider;
import org.jspresso.framework.model.ModelChangeEvent;
import org.jspresso.framework.model.component.IComponent;
import org.jspresso.framework.model.descriptor.IModelDescriptor;
import org.jspresso.framework.model.descriptor.IModelDescriptorAware;
import org.jspresso.framework.util.accessor.IAccessor;
import org.jspresso.framework.util.accessor.IAccessorFactory;
import org.jspresso.framework.util.bean.IPropertyChangeCapable;
import org.jspresso.framework.util.gate.IGate;
import org.jspresso.framework.util.lang.IModelAware;
import org.jspresso.framework.util.lang.ObjectUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This connector is a model property connector.
*
* @author Vincent Vandenschrick
*/
public abstract class ModelPropertyConnector extends AbstractValueConnector
implements IModelChangeListener, PropertyChangeListener {
private static final Logger LOG = LoggerFactory
.getLogger(ModelPropertyConnector.class);
private IAccessor accessor;
private final IAccessorFactory accessorFactory;
/**
* Constructs a new model connector on a model property.
*
* @param modelDescriptor
* The model descriptor to which the connector is bound at.
* @param accessorFactory
* The factory which is used to build the {@code IAccessor} used
* to access the java model property bi-directionally
*/
ModelPropertyConnector(IModelDescriptor modelDescriptor,
IAccessorFactory accessorFactory) {
super(modelDescriptor.getName());
setModelDescriptor(modelDescriptor);
this.accessorFactory = accessorFactory;
}
/**
* {@inheritDoc}
*/
@Override
public ModelPropertyConnector clone() {
return clone(getId());
}
/**
* {@inheritDoc}
*/
@Override
public ModelPropertyConnector clone(String newConnectorId) {
ModelPropertyConnector clonedConnector = (ModelPropertyConnector) super
.clone(newConnectorId);
return clonedConnector;
}
/**
* Gets the modelProvider.
*
* @return the modelProvider.
*/
@Override
public IModelProvider getModelProvider() {
if (getParentConnector() instanceof IModelProvider) {
return (IModelProvider) getParentConnector();
}
return null;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isWritable() {
boolean writable = super.isWritable();
if (accessor != null) {
writable = writable && accessor.isWritable();
}
if (getModelProvider() != null) {
writable = writable && getModelProvider().getModel() != null;
}
return writable;
}
/**
* Detaches {@code this} as {@code PropertyChangeListener} on the
* old model instance and attaches as {@code PropertyChangeListener} on
* the new model instance. When this is done, it notifies its
* {@code IValueChangeListener} s about a possible change on the model
* property value (the new model property).
* <p>
* {@inheritDoc}
*/
@Override
public void modelChange(ModelChangeEvent evt) {
// It's important to recompute the accessor each time the model actually
// changes since we may have, at run-time, a map model.
Object newModel = evt.getNewValue();
Object oldModel = evt.getOldValue();
recomputeAccessor(newModel);
ICompositeValueConnector parentConnector = getParentConnector();
if (parentConnector != null && !(parentConnector instanceof ICollectionConnector)) {
if (oldModel != null && oldModel instanceof IPropertyChangeCapable) {
((IPropertyChangeCapable) oldModel).removePropertyChangeListener(
getId(), this);
}
if (newModel != null && newModel instanceof IPropertyChangeCapable) {
((IPropertyChangeCapable) newModel).addPropertyChangeListener(getId(),
this);
}
}
if (getReadabilityGates() != null) {
for (IGate gate : getReadabilityGates()) {
if (gate instanceof IModelAware) {
((IModelAware) gate).setModel(newModel);
}
}
}
if (getWritabilityGates() != null) {
for (IGate gate : getWritabilityGates()) {
if (gate instanceof IModelAware) {
((IModelAware) gate).setModel(newModel);
}
}
}
writabilityChange();
readabilityChange();
fireConnectorValueChange();
}
private void recomputeAccessor(Object newModel) {
if (isValueAccessedAsProperty() && getModelProvider() != null
&& accessorFactory != null && (accessor == null || !accessor.appliesTo(newModel))) {
Class<?> modelType = null;
try {
if (newModel != null) {
if (newModel instanceof IComponent) {
modelType = ((IComponent) newModel).getComponentContract();
} else {
modelType = newModel.getClass();
}
} else {
modelType = getModelProvider().getModelDescriptor().getModelType();
}
accessor = accessorFactory.createPropertyAccessor(getId(), modelType);
} catch (Exception ex) {
LOG.error(
"An error occurred when creating the accessor for the {} property on {} class.", getId(), modelType, ex);
}
if (accessor instanceof IModelDescriptorAware) {
((IModelDescriptorAware) accessor)
.setModelDescriptor(getModelDescriptor());
}
}
}
/**
* Called when the underlying connectee value (the model property) changes.
* This implementation notifies its {@code IValueChangeListener} s about
* the change passing the new model property.
* <p>
* {@inheritDoc}
*/
@Override
public void propertyChange(PropertyChangeEvent evt) {
fireConnectorValueChange();
}
/**
* Since model provider is usually the parent connector for this kind of
* connector, this method is overloaded to call the
* {@code modelProviderChanged} method.
* <p>
* {@inheritDoc}
*/
@Override
public void setParentConnector(ICompositeValueConnector parentConnector) {
IModelProvider oldModelProvider = getModelProvider();
super.setParentConnector(parentConnector);
modelProviderChanged(oldModelProvider);
}
/**
* Accesses the underlying model property and gets its value.
* <p>
* {@inheritDoc}
*/
@Override
protected Object getConnecteeValue() {
try {
return accessor.getValue(getModelProvider().getModel());
} catch (IllegalAccessException ex) {
throw new ConnectorBindingException(ex);
} catch (InvocationTargetException ex) {
if (ex.getCause() instanceof RuntimeException) {
throw (RuntimeException) ex.getCause();
}
throw new ConnectorBindingException(ex.getCause());
} catch (NoSuchMethodException ex) {
// this may be a normal behaviour in case of polymorphism.
// don't throw any exception.
// throw new ConnectorBindingException(ex);
return null;
}
}
/**
* Whether this is a 'real' property connector (a opposed to a ModelConnector).
*
* @return true if this is a 'real' property connector.
*/
protected boolean isValueAccessedAsProperty() {
return true;
}
/**
* This method must be called whenever the connector's model provider changes.
* This method performs any necessary cleaning, attachments and notification
* needed.
*
* @param oldModelProvider
* the old model provider or null if none.
*/
protected void modelProviderChanged(IModelProvider oldModelProvider) {
Object oldModel = null;
Object newModel = null;
if (oldModelProvider != null) {
oldModel = oldModelProvider.getModel();
oldModelProvider.removeModelChangeListener(this);
}
if (getModelProvider() != null) {
getModelProvider().addModelChangeListener(this);
newModel = getModelProvider().getModel();
}
// The following lines are in fact handled by the modelChange call at the
// end of the method. If we called it twice, it would generate GC problems
// since the listener would also be added twice.
// if (oldModel != null && oldModel instanceof IPropertyChangeCapable) {
// ((IPropertyChangeCapable) oldModel).removePropertyChangeListener(getId(),
// this);
// }
// if (newModel != null && newModel instanceof IPropertyChangeCapable) {
// ((IPropertyChangeCapable) newModel).addPropertyChangeListener(getId(),
// this);
// }
// line below is mainly used to initialize oldConnectorValue (the model
// property connector is not used as model yet since it is just being linked
// to its parent). We would like to use the commented modelChange line but
// it
// fails if the current connector is a collection connector.
// setConnectorValue(getConnecteeValue());
recomputeAccessor(newModel);
modelChange(new ModelChangeEvent(getModelProvider(), oldModel, newModel));
}
/**
* Accesses the underlying model property and sets its value.
* <p>
* {@inheritDoc}
*/
@Override
protected void setConnecteeValue(Object aValue) {
if (!ObjectUtils.equals(aValue, getConnecteeValue())) {
try {
accessor.setValue(getModelProvider().getModel(), aValue);
} catch (IllegalAccessException | NoSuchMethodException ex) {
throw new ConnectorBindingException(ex);
} catch (InvocationTargetException ex) {
if (ex.getCause() instanceof RuntimeException) {
throw (RuntimeException) ex.getCause();
}
throw new ConnectorBindingException(ex.getCause());
}
}
}
}