/*
* Rapid Beans Framework: RapidBeanImplParent.java
*
* Copyright (C) 2013 Martin Bluemel
*
* Creation Date: 02/14/2013
*
* This program 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.
* 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 Lesser General Public License for more details.
* You should have received a copies of the GNU Lesser General Public License and the
* GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
package org.rapidbeans.core.basic;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.MissingResourceException;
import java.util.logging.Logger;
import org.rapidbeans.core.common.RapidBeansLocale;
import org.rapidbeans.core.common.ThreadLocalProperties;
import org.rapidbeans.core.event.PropertyChangeEvent;
import org.rapidbeans.core.event.PropertyChangeListener;
import org.rapidbeans.core.exception.RapidBeansRuntimeException;
import org.rapidbeans.core.exception.UtilException;
import org.rapidbeans.core.type.TypeProperty;
import org.rapidbeans.core.type.TypePropertyCollection;
import org.rapidbeans.core.type.TypeRapidBean;
import org.rapidbeans.core.util.ClassHelper;
import org.rapidbeans.core.util.StringHelper;
/**
* The parent class of strict (generic) and simple (reflective) bean
* implementations of the RapidBeans framework.
*
* @author Martin Bluemel
*/
public abstract class RapidBeanImplParent implements RapidBean, Link {
private static final Logger log = Logger.getLogger(RapidBeanImplParent.class.getName());
/**
* Creates a new bean instance from a special type.
*
* @param typename
* the name of the bean's type
*
* @return the new bean instance
*/
public static RapidBeanImplParent createInstance(final String typename) {
return createInstance(TypeRapidBean.forName(typename));
}
/**
* Creates a new bean instance from a special type.
*
* @param type
* the bean's type
*
* @return the new bean instance
*/
public static RapidBeanImplParent createInstance(final TypeRapidBean type) {
Class<?> clazz = type.getImplementingClass();
if (clazz != null && RapidBeanImplSimple.class.isAssignableFrom(clazz)) {
return RapidBeanImplSimple.createInstance(type);
} else {
return RapidBeanImplStrict.createInstance(type);
}
}
/**
* @return the bean's type instance
*/
public abstract TypeRapidBean getType();
public abstract RapidBean clone();
/**
* the identity object.
*/
private Id id = null;
/**
* @return the bean's ID.
*/
public Id getId() {
if (this.id == null) {
this.id = Id.createInstance(this, null);
}
return this.id;
}
/**
* reset the ID to null. Do not use in general.
*/
public final void clearId() {
this.id = null;
}
/**
* @return the ID string
*/
public String getIdString() {
if (this.id == null) {
return this.getId().toString();
} else {
return this.id.toString();
}
}
/**
* @return the ID string
*/
public final String toString() {
return this.getIdString();
}
/**
* CAUTION: exclusively use this setter for deserialization.
*
* @param newId
* the new id.
*/
public void setId(final Id newId) {
this.id = newId;
}
// /**
// * CAUTION: exclusively use this setter for deserialization.
// * @param idString the serialized id.
// */
// public void setId(final String idString) {
// this.id = Id.createInstance(this, idString);
// }
/**
* Store the bean's state
*/
private RapidBeanState beanState = null;
/**
* @return the bean's state
*/
public RapidBeanState getBeanState() {
return this.beanState;
}
/**
* @param beanState
* the new state to set
*/
protected void setBeanState(RapidBeanState beanState) {
this.beanState = beanState;
}
/**
* the parent (composite) bean of this bean.
*/
private RapidBean parent = null;
/**
* navigate to the parent (composite) bean of this bean.
*
* @return the parent (composite) bean of this bean if any or null
*/
public RapidBean getParentBean() {
return this.parent;
}
/**
* retrieve all parent beans in the composite hierarcy.
*
* @return an array of parent beans starting with the (document) root and
* ending with the direct parent
*/
public RapidBean[] getParentBeans() {
final ArrayList<RapidBean> beans = new ArrayList<RapidBean>();
RapidBean parentBean = this.getParentBean();
while (parentBean != null) {
beans.add(parentBean);
parentBean = parentBean.getParentBean();
}
final RapidBean[] parentBeans = new RapidBean[beans.size()];
int j = 0;
for (int i = parentBeans.length - 1; i >= 0; i--) {
parentBeans[i] = beans.get(j++);
}
return parentBeans;
}
public final PropertyCollection findAssociationPropertyWithSingularName(final String singularName)
{
for (final PropertyCollection prop : getColProperties()) {
if (singularName.equals(((TypePropertyCollection) prop.getType()).getSingular())) {
return prop;
}
}
return null;
}
/**
* get the parent collection property.
*
* @return the parent collection property.
*/
public PropertyCollection getParentProperty() {
PropertyCollection parentCompColProp = null;
if (this.parent != null) {
for (PropertyCollection compColProp : this.parent.getColPropertiesComposition()) {
final Collection<?> col = (Collection<?>) compColProp.getValue();
if (col != null && col.contains(this)) {
parentCompColProp = compColProp;
break;
}
}
}
return parentCompColProp;
}
/**
* setter for the parent (composite) bean used internally when adding or
* removing a bean reference to / from a collection property of type
* composition. Also used when validating properties in the editor
*
* @param newParent
* the new parent bean
*/
public void setParentBean(final RapidBean newParent) {
// reset the id if the parent changes
if (this.parent != null && newParent != null) {
// change a component's parent
final PropertyCollection parentColProp = this.getParentProperty();
parentColProp.removeLink(this, false, true, false);
if (ClassHelper.classOf(this.parent.getClass(), newParent.getClass())) {
PropertyCollection newParentColProp = (PropertyCollection) newParent.getProperty(parentColProp
.getName());
newParentColProp.addLink(this, false, false, false);
}
}
resetId(newParent);
this.parent = newParent;
}
/**
* reset the ID.
*
* @param newParentBean
* the new parent bean
*/
private void resetId(final RapidBean newParentBean) {
if (this.id instanceof IdKeypropswithparentscope) {
boolean changeids = true;
Boolean doNotChange = (Boolean) ThreadLocalProperties.get("bean.setparentbean.donotchangeids");
if (doNotChange != null) {
changeids = !doNotChange.booleanValue();
}
if (changeids) {
if (this.container != null && (this.container.contains(this))) {
this.container.delete(this);
this.id = null;
if (newParentBean != null && this.container != null) {
((ContainerImpl) this.container).insert(this, true);
}
} else {
this.id = null;
}
}
}
for (PropertyCollection colProp : this.getColPropertiesComposition()) {
if (colProp.getValue() == null) {
continue;
}
for (Object o : (Collection<?>) colProp.getValue()) {
final RapidBeanImplParent bean = (RapidBeanImplParent) o;
bean.resetId(this);
}
}
}
/**
* the container where the bean lives in.
*/
private Container container = null;
/**
* @return Returns the container.
*/
public Container getContainer() {
return this.container;
}
/**
* @param cont
* The container to set
*/
public void setContainer(final Container cont) {
this.container = cont;
}
/**
* remove all references to and from other beans. notify the container about
* deletion.
*/
@SuppressWarnings("unchecked")
public void delete() {
final RapidBeanState stateBefore = this.beanState;
this.beanState = RapidBeanState.deleting;
Container containerFired = null;
try {
if (this.container != null) {
this.container.fireBeanRemovePre(this);
containerFired = this.container;
}
// delete the component relationship to the parent
if (this.parent != null) {
TypePropertyCollection inverseColPropType;
Collection<RapidBeanImplParent> inverseCol;
boolean removed = false;
for (PropertyCollection inverseColProp : this.parent.getColPropertiesComposition()) {
inverseColPropType = (TypePropertyCollection) inverseColProp.getType();
inverseCol = (Collection<RapidBeanImplParent>) inverseColProp.getValue();
if (TypeRapidBean.isSameOrSubtype(inverseColPropType.getTargetType(), this.getType())
&& inverseCol.contains(this)) {
inverseColProp.removeLink(this);
removed = true;
break;
}
}
if (!removed) {
throw new RapidBeansRuntimeException("Removing a bean out of a container by"
+ " breaking the collection link from it's parent bean failed.");
}
}
// delete normal association instances and start a
// delete cascade for components
Collection<RapidBeanImplParent> col;
TypePropertyCollection colPropType;
for (PropertyCollection colProp : this.getColProperties()) {
col = (Collection<RapidBeanImplParent>) colProp.getValue();
if (col == null) {
continue;
}
colPropType = (TypePropertyCollection) colProp.getType();
if (colPropType.isComposition()) {
// we have to store the component beans to delete in a
// different
// collection before we delete them otherwise we get a
// ConcurrentModification exception because col is changed
// implicitly while the component bean is deleted and at the
// same time we're iterating over it.
Collection<RapidBeanImplParent> componentBeansToDelete = new ArrayList<RapidBeanImplParent>();
for (RapidBeanImplParent bean : col) {
componentBeansToDelete.add(bean);
}
for (RapidBeanImplParent bean : componentBeansToDelete) {
bean.delete();
}
componentBeansToDelete.clear();
} else {
Collection<RapidBeanImplParent> beansToUnlink = new ArrayList<RapidBeanImplParent>();
for (RapidBeanImplParent bean : col) {
beansToUnlink.add(bean);
}
for (RapidBeanImplParent bean : beansToUnlink) {
colProp.removeLink(bean);
}
}
}
if (containerFired != null) {
containerFired.fireBeanRemoved(this);
}
} finally {
this.beanState = stateBefore;
}
}
/**
* compare method.
*
* @param o
* the bean to compare with
*
* @return the compare value
*/
public int compareTo(final Link link) {
int compare = 0;
if (link instanceof RapidBeanImplParent) {
final RapidBeanImplParent bean = (RapidBeanImplParent) link;
// Assert id's not null
if (this.id == null) {
this.getId();
if (this.id == null) {
throw new RapidBeansRuntimeException("could not determine id");
}
}
if (bean.id == null) {
bean.getId();
if (bean.id == null) {
throw new RapidBeansRuntimeException("could not determine id");
}
}
// get the property types to sort after from the thread local sorter
final TypeProperty[] propTypes = BeanSorter.get();
if (propTypes == null) {
// if sorting is not required simply sort by ID (natural order)
compare = compareIds(bean);
} else {
// if sorting by properties is required
if (sameSortingProptypes(bean, propTypes)) {
// important note:
// equality of IDs must overrule sorting by properties
if (this.id.equals(bean.id)) {
compare = 0;
} else {
for (int i = 0; i < propTypes.length; i++) {
final String propname = propTypes[i].getPropName();
compare = this.getProperty(propname).compareTo(bean.getProperty(propname));
if (compare != 0) {
break;
}
}
if (compare == 0) {
compare = this.compareIds(bean);
}
}
} else {
compare = this.compareIds(bean);
}
}
} else if (link instanceof LinkFrozen) {
compare = this.getIdString().compareTo(link.getIdString());
} else {
if (link != null) {
throw new RapidBeansRuntimeException("cannot compare a bean with " + link.getClass().getName());
} else {
throw new RapidBeansRuntimeException("cannot compare with null");
}
}
if (compare > 1) {
compare = 1;
} else if (compare < -1) {
compare = -1;
}
return compare;
}
/**
* compare method.
*
* @param o
* the bean to compare with
*
* @return the compare value
*/
public int compareToOld(final Object o) {
int compare = 0;
if (o instanceof RapidBeanImplParent) {
final RapidBeanImplParent bean = (RapidBeanImplParent) o;
// do not compare apples with peaches
if (this.getType() != bean.getType()) {
return -2;
}
// get the property types to sort after from the thread local sorter
final TypeProperty[] propTypes = BeanSorter.get();
if (propTypes == null) {
// if sorting is not required simply sort by ID (natural order)
if (this.id != null && bean.id != null) {
compare = this.id.compareTo(bean.id);
} else {
compare = this.getId().compareTo(bean.getId());
}
} else {
// if sorting by properties is required
// important note:
// equality of IDs must overrule sorting by properties
if (this.getId().equals(bean.getId())) {
compare = 0;
} else {
for (int i = 0; i < propTypes.length; i++) {
final String propname = propTypes[i].getPropName();
compare = this.getProperty(propname).compareTo(bean.getProperty(propname));
if (compare != 0) {
break;
}
}
if (compare == 0) {
if (this.id != null && bean.id != null) {
compare = this.id.compareTo(bean.id);
if (compare == 0) {
compare = this.id.compareTo(bean.id);
}
} else {
compare = this.getId().compareTo(bean.getId());
}
}
}
}
} else if (o instanceof LinkFrozen) {
LinkFrozen link = (LinkFrozen) o;
compare = this.getIdString().compareTo(link.getIdString());
} else {
throw new RapidBeansRuntimeException("cannot compare a bean with " + o.getClass().getName());
}
if (compare > 1) {
compare = 1;
} else if (compare < -1) {
compare = -1;
}
return compare;
}
/**
* @param bean
* the bean to compare with
*
* @return the compare value
*/
private int compareIds(final RapidBeanImplParent bean) {
int compare;
if (idtypesComparable(bean)) {
compare = this.id.compareTo(bean.id);
} else {
compare = (this.getIdString()).compareTo(bean.getIdString());
}
return compare;
}
/**
* Helper to determine if id types are comparable
*
* @param bean
* the bean to compare with
*
* @return true if id types are comparable, false otherwise
*/
private final boolean idtypesComparable(final RapidBeanImplParent bean) {
if (this.getType() == bean.getType()) {
return true;
}
if (this.id.getClass() == bean.id.getClass()) {
if (this.id instanceof IdKeyprops) {
final Property[] thisIdKeyprops = ((IdKeyprops) this.id).getKeyprops();
final Property[] beanIdKeyprops = ((IdKeyprops) bean.id).getKeyprops();
if (thisIdKeyprops.length != beanIdKeyprops.length) {
return false;
}
final int len = thisIdKeyprops.length;
for (int i = 0; i < len; i++) {
if (thisIdKeyprops[i] != beanIdKeyprops[i]) {
return false;
}
}
} else {
return true;
}
}
return false;
}
/**
* Helper to determine if both bean types have all properties required for
* sorting with same name and property type.
*
* @param bean
* the bean to compare with
* @param propTypes
* the property types required for sorting
*
* @return if sorting can be accomplished according to the given property
* types
*/
private boolean sameSortingProptypes(RapidBeanImplParent bean, TypeProperty[] propTypes) {
final TypeRapidBean thisType = this.getType();
final TypeRapidBean beanType = bean.getType();
for (final TypeProperty propType : propTypes) {
final String propname = propType.getPropName();
final TypeProperty thisProptype = thisType.getPropertyType(propname);
if (thisProptype == null) {
return false;
}
}
if (thisType == beanType) {
return true;
}
for (final TypeProperty propType : propTypes) {
final String propname = propType.getPropName();
final TypeProperty thisProptype = thisType.getPropertyType(propname);
if (thisProptype == null) {
return false;
}
final String thisPropClass = thisProptype.getClass().getName();
final TypeProperty beanProptype = beanType.getPropertyType(propname);
if (beanProptype == null) {
return false;
}
final String beanPropClass = beanProptype.getClass().getName();
if (!thisPropClass.equals(beanPropClass)) {
return false;
}
}
return true;
}
/**
* A bean equals another bean it has the same type and an equal identity.
*
* @param o
* the object to compare
* @return if the object equals or not
*/
public boolean equals(final Object o) {
if (o == null) {
return false;
}
try {
RapidBeanImplParent bean = (RapidBeanImplParent) o;
if (!(this.getType() == bean.getType())) {
return false;
}
return this.getId().equals(bean.getId());
} catch (ClassCastException e) {
return false;
}
}
/**
* the hashcode of a bean is the hashcode of it's id.
*
* @return the hashcode
*/
public int hashCode() {
if (this.getId() instanceof IdTransientid) {
return super.hashCode();
}
return this.getId().hashCode();
}
/**
* PropertyChangeListenr collection.
*/
private Collection<PropertyChangeListener> changeListeners = new ArrayList<PropertyChangeListener>();
/**
* adds a new PropertyChangeListener.
*
* @param l
* the new listener to add
*/
public void addPropertyChangeListener(final PropertyChangeListener l) {
this.changeListeners.add(l);
}
/**
* adds a new PropertyChangeListener.
*
* @param l
* the new listener to add
*/
public void removePropertyChangeListener(final PropertyChangeListener l) {
this.changeListeners.remove(l);
}
/**
* Fires a property pre change event for that bean. For specific processing
* simply override and call super().
*
* @param event
* the PropertyChangeEvent to fire
*/
public void propertyChangePre(final PropertyChangeEvent event) {
if (this.changeListeners != null && this.changeListeners.size() > 0) {
for (PropertyChangeListener l : this.changeListeners) {
l.propertyChangePre(event);
}
}
final Container container = this.getContainer();
if (container != null) {
container.fireBeanChangePre(event);
}
}
/**
* Fires a property post change event for that bean. For specific processing
* simply override and call super().
*
* @param event
* the PropertyChangeEvent to fire
*/
public void propertyChanged(final PropertyChangeEvent event) {
if (this.changeListeners != null && this.changeListeners.size() > 0) {
for (PropertyChangeListener l : this.changeListeners) {
l.propertyChanged(event);
}
}
final Container container = getContainer();
if (container != null) {
container.fireBeanChanged(event);
}
}
/**
* computes a localized String to present this Bean in a UI.
*
* @param locale
* the Locale
*
* @return the localized String for this Bean
*/
public String toStringGui(final RapidBeansLocale locale) {
final String sGuiType = toStringGuiType(locale);
final String sGuiId = toStringGuiId(locale);
if (sGuiType.equals(sGuiId)) {
return sGuiId;
} else {
if (this.getType().getIdtype() == IdType.transientid && sGuiId.equals(this.getIdString())) {
return sGuiType;
} else {
return sGuiType + ": " + sGuiId;
}
}
}
/**
* computes a localized String to present the Bean's type name in a UI.
*
* @param locale
* the Locale
* @return the localized String for this Bean
*/
public String toStringGuiType(final RapidBeansLocale locale) {
return this.getType().toStringGui(locale, false, null);
}
/**
* computes a localized String to present this Bean's instance name in a UI.
*
* @param locale
* the Locale
* @return the localized String for this Bean
*/
public String toStringGuiId(final RapidBeansLocale locale) {
String uistring = null;
if (locale != null) {
try {
final String key = "bean." + this.getType().getName().toLowerCase() + ".id";
uistring = locale.getStringGui(key);
uistring = expandPropertyValues(uistring, locale);
} catch (MissingResourceException e) {
uistring = null;
}
}
if (uistring == null) {
if (this.getType().getIdtype() == IdType.transientid) {
if (getProperty("name") != null && this.getPropValue("name") != null) {
uistring = this.getPropValue("name").toString();
} else {
uistring = toStringGuiType(locale);
}
} else {
uistring = this.getIdString();
}
}
return uistring;
}
/**
* expand the curly braced property names with the bean's property values.
*
* @param pattern
* the pattern with curly braces e. g. "{firstname}, {lastname}"
* @param locale
* the locale
* @return the expanded string
*/
public String expandPropertyValues(final String pattern, final RapidBeansLocale locale) {
char c;
int len = pattern.length();
int state = 0;
StringBuffer bufExpanded = new StringBuffer();
StringBuffer bufPropname = new StringBuffer();
StringBuffer bufParam = new StringBuffer();
StringBuffer sb = new StringBuffer();
String propname = null;
String methodname = null;
List<Object> parameters = new ArrayList<Object>();
Class<?>[] parameterTypes = null;
int j, ptsize;
Property prop;
Method method;
List<String> sa;
int sasize;
String s;
for (int i = 0; i < len; i++) {
c = pattern.charAt(i);
switch (state) {
case 0: // out of curley brace
switch (c) {
case '{':
state = 1;
break;
default:
bufExpanded.append(c);
break;
}
break;
case 1: // within curley brace
switch (c) {
case '(':
sa = StringHelper.split(bufPropname.toString(), ".");
sasize = sa.size();
for (j = 0; j < sasize; j++) {
if (j < (sasize - 1)) {
if (j > 0) {
sb.append('.');
}
sb.append(sa.get(j));
} else {
propname = sb.toString();
sb.setLength(0);
methodname = sa.get(j);
}
}
bufPropname.setLength(0);
parameters.clear();
parameters.add(locale);
state = 2;
break;
case '}':
if (bufPropname.length() > 0) {
prop = dereferencePropname(bufPropname.toString());
if (prop != null) {
bufExpanded.append(prop.toStringGui(locale));
prop = null;
} else {
bufExpanded.append(bufPropname.toString());
}
bufPropname.setLength(0);
}
state = 0;
break;
default:
bufPropname.append(c);
break;
}
break;
case 2: // within method parameter brace
switch (c) {
case ')':
prop = null;
if (propname != null) {
prop = dereferencePropname(propname);
}
if (bufParam.length() > 0) {
parameters.add(bufParam.toString().trim());
bufParam.setLength(0);
}
ptsize = parameters.size();
parameterTypes = new Class[ptsize];
parameterTypes[0] = RapidBeansLocale.class;
for (j = 1; j < ptsize; j++) {
parameterTypes[j] = String.class;
}
method = null;
try {
if (prop != null) {
method = prop.getClass().getMethod(methodname, parameterTypes);
}
} catch (NoSuchMethodException e) {
throw new RapidBeansRuntimeException(e);
}
if (prop != null && method != null) {
try {
s = (String) method.invoke(prop, parameters.toArray());
} catch (IllegalArgumentException e) {
throw new RapidBeansRuntimeException(e);
} catch (IllegalAccessException e) {
throw new RapidBeansRuntimeException(e);
} catch (InvocationTargetException e) {
throw new RapidBeansRuntimeException(e);
}
bufExpanded.append(s);
} else {
bufExpanded.append(bufPropname.toString());
}
parameters.clear();
state = 1;
break;
case ',':
if (bufParam.length() > 0) {
parameters.add(bufParam.toString());
bufParam.setLength(0);
}
break;
default:
bufParam.append(c);
break;
}
break;
default:
throw new UtilException("expandBeanPropertyValues(\"" + pattern + "\") failed\n" + "wrong state "
+ state);
}
}
if (state == 1) {
throw new UtilException("expandBeanPropertyValues(\"" + pattern + "\") failed\n" + "missing closing }");
}
return bufExpanded.toString();
}
/**
* find a property out of the given property name. The property name can be
* ether a simple name or a chain of reference (collection, map) properties.
* The most right property can be an arbitrary one.
*
* @param propname
* property name (chain)
*
* @return the referenced property
*/
private Property dereferencePropname(final String propname) {
Property prop = null;
if (propname.indexOf(".") == -1) {
prop = this.getProperty(propname);
} else {
RapidBean linkedBean = this;
final List<String> propnames = StringHelper.split(propname, ".");
final int propnamessize = propnames.size();
int j = 0;
for (final String pname : propnames) {
j++;
if (pname.equals("parentBean")) {
linkedBean = linkedBean.getParentBean();
if (linkedBean == null) {
prop = null;
break;
} else {
continue;
}
} else {
prop = linkedBean.getProperty(pname);
}
if (j < propnamessize) {
if (prop instanceof PropertyCollection) {
Collection<?> col = (Collection<?>) prop.getValue();
if (col != null) {
linkedBean = (RapidBeanImplParent) col.iterator().next();
} else {
break;
}
} else {
break;
}
}
}
}
return prop;
}
/**
* trace that bean.
*/
@SuppressWarnings("unchecked")
public void trace() {
log.info("BIZ BEAN: " + this.getType().getNameShort() + "::" + this.getIdString());
for (PropertyCollection prop : this.getColProperties()) {
if (prop.getValue() != null) {
for (final Link link : (Collection<Link>) prop.getValue()) {
if (link instanceof RapidBeanImplParent) {
if (((TypePropertyCollection) prop.getType()).isComposition()) {
((RapidBeanImplParent) link).trace();
} else {
log.info("BEAN LINK: " + ((RapidBeanImplParent) link).getIdString());
}
} else {
log.info("FROZEN LINK: " + ((LinkFrozen) link).getIdString());
}
}
}
}
}
}