/*******************************************************************************
* Copyright (c) 2010-2014 SAP AG and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* SAP AG - initial API and implementation
*******************************************************************************/
package org.eclipse.skalli.model;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import javax.xml.bind.DatatypeConverter;
import org.apache.commons.lang.StringUtils;
import org.eclipse.skalli.commons.FormatUtils;
import org.eclipse.skalli.commons.MapBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Abstract base class for all uniquely identifiable model entities.
* The identifier of an entity is a {@link UUID universally unique identifier (UUID)}.
* Once set, the <code>uuid</code> of an entity is immutable. An entity can be assigned
* to one (and only one) parent entity, so that hierarchies of entities can be built.
* Furthermore, entities can be marked as {@link #isDeleted() deleted} to hide them
* from common operations.
*/
public abstract class EntityBase {
private static final Logger LOG = LoggerFactory.getLogger(EntityBase.class);
@PropertyName
public static final String PROPERTY_UUID = "uuid"; //$NON-NLS-1$
@PropertyName
public static final String PROPERTY_DELETED = "deleted"; //$NON-NLS-1$
@PropertyName
public static final String PROPERTY_PARENT_ENTITY_ID = "parentEntityId"; //$NON-NLS-1$
@Derived
@PropertyName
public static final String PROPERTY_PARENT_ENTITY = "parentEntity"; //$NON-NLS-1$
@Derived
@PropertyName
public static final String PROPERTY_FIRST_CHILD = "firstChild"; //$NON-NLS-1$
@Derived
@PropertyName
public static final String PROPERTY_NEXT_SIBLING = "nextSibling"; //$NON-NLS-1$
@Derived
@PropertyName
public static final String PROPERTY_LAST_MODIFIED = "lastModified"; //$NON-NLS-1$
@Derived
@PropertyName
public static final String PROPERTY_LAST_MODIFIED_BY = "lastModifiedBy"; //$NON-NLS-1$
private static final Map<Class<?>,Class<?>> PRIMITIVES_MAP =
new MapBuilder<Class<?>,Class<?>>()
.put(Boolean.class, boolean.class)
.put(Character.class, char.class)
.put(Byte.class, byte.class)
.put(Short.class, short.class)
.put(Integer.class, int.class)
.put(Long.class, long.class)
.put(Float.class, float.class)
.put(Double.class, double.class)
.toMap();
/**
* Cache for property accessor methods associated with the defining entity classes.
* The inner maps are immutable, but lazily initialized when {@link #getProperty(String)}
* is called first for a given entity class. The outter concurrent hash map guarantees
* non-blocking read operations for maximum performance.
*/
private transient static ConcurrentHashMap<Class<?>, Map<String,Method>> accessorsByEntityType =
new ConcurrentHashMap<Class<?>, Map<String,Method>>();
/**
* The unique identifier of a project - set once, never changed.
*/
private UUID uuid;
/**
* Deleted entities are loaded and cached separately.
*/
private boolean deleted = false;
/**
* Persistent UUID of the parent entity,
* or <code>null</code> if this entity has no parent.
*/
private UUID parentEntityId;
/**
* Non-persistent pointer to the parent entity of this entity,
* or <code>null</code> if this entity has no parent.
*/
private transient EntityBase parentEntity;
/**
* Non-persistent pointer to the next sibling of this entity,
* or <code>null</code> if this is the last sibling in the chain
* or the entity is {@link #isDeleted() deleted}.
*/
private transient EntityBase nextSibling;
/**
* Non-persistent pointer to the first child of this entity,
* or <code>null</code> if this entity has no children
* or all children are {@link #isDeleted() deleted}.
*/
private transient EntityBase firstChild;
/**
* Date of last modification in ISO 8601 format
* and in milliseconds since midnight, January 1, 1970 UTC.
*/
private transient String lastModified;
private transient long lastModifiedMillis;
/**
* Unique identifier of the last modifier.
*/
private transient String lastModifiedBy;
/**
* Returns the unique identifier of the entity.
*/
public UUID getUuid() {
return uuid;
}
/**
* Sets the unique identifier of the entity. Note, this
* method does not change the unique identifier, if it has
* been set before.
*/
public void setUuid(UUID uuid) {
if (this.uuid == null) {
this.uuid = uuid;
}
}
/**
* Returns <code>true</code> if the entity is marked as deleted.
*/
public boolean isDeleted() {
return deleted;
}
/**
* Marks an entity as deleted or removes such a mark.
*/
public void setDeleted(boolean deleted) {
this.deleted = deleted;
}
/**
* Returns the parent entity of this entity,
* or <code>null</code> if this entity has no parent.
*/
public EntityBase getParentEntity() {
return parentEntity;
}
/**
* Sets the parent entity of this entity.
* @param parentEntity the parent entity to set, or <code>null</code>
* to reset the parent entity.
*/
public void setParentEntity(EntityBase parentEntity) {
this.parentEntity = parentEntity;
if (parentEntity != null) {
UUID parentUuid = parentEntity.getUuid();
if (parentUuid == null) {
throw new IllegalArgumentException("parentUUID is null, which makes it hard to remember");
} else {
this.parentEntityId = parentUuid;
}
} else {
this.parentEntityId = null;
}
}
/**
* Returns the unique identifier of the parent entity,
* or <code>null</code> if this entity has no parent.
*/
public UUID getParentEntityId() {
return parentEntityId;
}
/**
* Sets the unique identifier of the parent entity.
*
* Note, this method should not be called directly except for
* testing purposes. The parent entity is determined when
* the entity is loaded.
*/
public void setParentEntityId(UUID parentEntityId) {
this.parentEntityId = parentEntityId;
}
/**
* Returns the next sibling of this entity,
* or <code>null</code> if this entity has no next sibling
* or the entity is {@link #isDeleted() deleted}.
*
* Note that deleted entities never are reported as
* children or siblings of another entity, but they nevertheless
* may have a {@link #getParentEntity() parent entity}.
*/
public EntityBase getNextSibling() {
return nextSibling;
}
/**
* Sets the next sibling of this entity.
*
* Note, this method should not be called directly except for
* testing purposes. The siblings of an entity are determined
* when the entity is loaded.
*
* @param nextSibling the next sibling of this entity,
* or <code>null</code> if this entity has no further siblings.
*/
public void setNextSibling(EntityBase nextSibling) {
this.nextSibling = nextSibling;
}
/**
* Returns the first child of this entity,
* or <code>null</code> if this entity has no children
* or all children are {@link #isDeleted() deleted}.
*
* Note that deleted entities never are reported as
* children or siblings of another entity, but they nevertheless
* may have a {@link #getParentEntity() parent entity}.
*/
public EntityBase getFirstChild() {
return firstChild;
}
/**
* Sets the first child of this entity.
* Note, this method should not be called directly except for
* testing purposes. The children of an entity are determined
* when the entity is loaded.
*
* @param firstChild the first child of this entity,
* or <code>null</code> if this entity has no children.
*/
public void setFirstChild(EntityBase firstChild) {
this.firstChild = firstChild;
}
/**
* Returns the children of this entity as list. This method follows
* the {@link #getNextSibling() siblings chain} starting with the
* {@link #getFirstChild() first child}.
*
* @return the list of children, or an empty list if this entity
* has no children.
*/
public List<EntityBase> getChildren() {
ArrayList<EntityBase> subprojects = new ArrayList<EntityBase>();
EntityBase next = getFirstChild();
while (next != null) {
subprojects.add(next);
next = next.getNextSibling();
}
return subprojects;
}
/**
* Returns the date/time of the last modification of this entity.
*
* @return an ISO8061-compliant date/time string following the pattern
* <tt>[-]CCYY-MM-DDThh:mm:ss[Z|(+|-)hh:mm]</tt> (equivalent to the
* definition of the XML schema type <tt>xsd:dateTime</tt>),
* or <code>null</code> if the entity has not yet been persisted.
* Use for example {@link DatatypeConverter#parseDateTime(String)} to
* convert the result into a {@link java.util.Calendar} for further
* processing. The result is <code>null</code> if the date/time of
* the last modification is unknown.
*/
public String getLastModified() {
return lastModified;
}
/**
* Returns the date/time of the last modification of this entity
* measured in milliseconds since midnight, January 1, 1970 UTC,
* or -1 if the date/time of the last modification is unknown.
*/
public long getLastModifiedMillis() {
return lastModifiedMillis;
}
/**
* Sets the date/time of the last modification.
*
* Note, this method should not be called directly except for
* testing purposes. The date/time of the last modification
* is determined when the entity is persisted.
*
* @param lastModified an ISO8061-compliant date/time string following the
* pattern <tt>[-]CCYY-MM-DDThh:mm:ss[Z|(+|-)hh:mm]</tt> as defined for
* type <tt>xsd:dateTime</tt>) in <tt>"XML Schema Part 2: Datatypes"</tt>,
* or <code>null</code> to indicate that the date/time of the last modification
* is unknown.
*
* @throws IllegalArgumentException if the date/time string does not conform to
* type <tt>xsd:dateTime</tt>.
*/
public void setLastModified(String lastModified) {
if (StringUtils.isBlank(lastModified)) {
this.lastModifiedMillis = -1L;
this.lastModified = null;
} else {
this.lastModifiedMillis = DatatypeConverter.parseDateTime(lastModified).getTimeInMillis();
this.lastModified = lastModified;
}
}
/**
* Sets the date/time of the last modification.
*
* Note, this method should not be called directly except for
* testing purposes. The date/time of the last modification
* is determined when the entity is persisted.
*
* @param lastModifiedMillis the time of the last modification
* in milliseconds since midnight, January 1, 1970 UTC, or any negative
* value to indicate that the date/time of the last modification is unknown.
*/
public void setLastModified(long lastModifiedMillis) {
if (lastModifiedMillis < 0) {
this.lastModifiedMillis = -1L;
this.lastModified = null;
} else {
this.lastModifiedMillis = lastModifiedMillis;
this.lastModified = FormatUtils.formatUTCWithMillis(lastModifiedMillis);
}
}
/**
* Returns the unique identifier of the last modifier,
* or <code>null</code> if the entity has not yet been persisted.
*/
public String getLastModifiedBy() {
return lastModifiedBy;
}
/**
* Sets the unique identifier of the last modifier.
*
* Note, this method should not be called directly except for
* testing purposes. The last modifier is determined
* when the entity is persisted.
*
* @param lastModifiedBy the unique identifier of the last modifier, or
* <code>null</code>.
*/
public void setLastModifiedBy(String lastModifiedBy) {
if (StringUtils.isBlank(lastModifiedBy)) {
this.lastModifiedBy = null;
} else {
this.lastModifiedBy = lastModifiedBy;
}
}
/**
* Returns the names of the properties of this entity.
* <p>
* A property is declared by defining a string constant with the
* identifier of the property as value, annotating this constant
* with {@link PropertyName} and defining a corresponding
* getter method.
*
* @return an immutable set of property names, or an empty set,
* if the entity has no properties.
*/
public Set<String> getPropertyNames() {
return getReadAccessors().keySet();
}
/**
* Returns the names of the properties of a given entity class.
* <p>
* This method scans the given class for string constants annotated
* with {@link PropertyName}, which have a corresponding getter
* method. Example:
* <pre>
* @PropertyName public static final String PROPERTY_PROJECTID = "projectId";
* public String getProjectId() { ... }
* <pre>
* The capitalized value of the string constant is prefixed with either <tt>"get"</tt>,
* or with <tt>"is"</tt> in case of a boolean property. If no getter method
* is provided, the property is ignored.
*
* @return an immutable set of property names, or an empty set, if the entity
* has no properties or <code>entityClass</code> was <code>null</code>.
*/
public static Set<String> getPropertyNames(Class<? extends EntityBase> entityClass) {
return getReadAccessors(entityClass).keySet();
}
/**
* Returns <code>true</code> if this entity has a property
* with the given name.
*
* @param propertyName the identifier of the property.
* @return <code>true</code> if this entity has the requested property,
* <code>false</code> otherwise.
*
* @see org.eclipse.skalli.services.projects.PropertyName
*/
public boolean hasProperty(String propertyName) {
return getReadAccessors().containsKey(propertyName);
}
/**
* Returns the value of the given property, if that property exists.
*
* @param propertyName the identifier of the property.
*
* @throws NoSuchPropertyException if no property with the given name
* exists, or retrieving the value from that property failed.
*
* @see org.eclipse.skalli.services.projects.PropertyName
*/
public Object getProperty(String propertyName) {
Map<String,Method> accessors = getReadAccessors();
if (!accessors.containsKey(propertyName)) {
throw new NoSuchPropertyException(this, propertyName);
}
Method accessor = accessors.get(propertyName);
try {
return accessor.invoke(this, new Object[] {});
} catch (Exception e) {
throw new NoSuchPropertyException(this, propertyName, e);
}
}
/**
* Returns the value of a property specified by a series of {@link Expression expressions}
* corresponding to property accessor methods, i.e. methods annotated with {@link Property},
* or simple properties defined with {@link PropertyName}.
*
* @param expressions the list of expressions specifying the property to return.
*
* @return the return value of the property accessor method or the value of the simple
* property corresponding to the last of the given expressions, which may be <code>null</code>.
* If any of the intermediate property accessors or properties returns/is <code>null</code>,
* the result will be <code>null</code>, too.
*
* @throws NoSuchPropertyException if no property matches the given series of expressions,
* or invoking any of the property accessors failed. In the latter case, the cause
* of the failure can be retrieved with {@link NoSuchPropertyException#getCause()}.
*/
public Object getProperty(Expression...expressions) {
if (expressions == null || expressions.length == 0) {
return null;
}
Object o = this;
for (int i = 0; i < expressions.length; ++i) {
Expression expression = expressions[i];
String[] args = expression.getArguments();
Class<?>[] argTypes = new Class<?>[args.length];
Arrays.fill(argTypes, String.class);
String propertyName = expression.getName();
try {
Method accessor = getReadAccessor(o.getClass(), propertyName, argTypes);
if (accessor.getAnnotation(Property.class) != null || hasProperty(propertyName)) {
o = accessor.invoke(o, (Object[])args);
if (o == null) {
return null;
}
} else {
throw new NoSuchPropertyException(this, expression);
}
} catch (Exception e) {
throw new NoSuchPropertyException(this, expression, e);
}
}
return o;
}
/**
* Sets the value for the given property, if that property exists.
*
* @param propertyName the identifier of the property.
* @param propertyValue the new value of the property.
* @throws NoSuchPropertyException if no property with the given name exists.
* @throws PropertyUpdateException if the property value could not be changed.
*
* @see org.eclipse.skalli.services.projects.PropertyName
*/
public void setProperty(String propertyName, Object propertyValue) {
Class<?> propertyType = propertyValue != null? propertyValue.getClass() : null;
Method accessor = getWriteAccessor(propertyName, propertyType); //$NON-NLS-1$
if (accessor == null) {
throw new NoSuchPropertyException(this, propertyName);
}
try {
accessor.invoke(this, propertyValue);
} catch (Exception e) {
throw new PropertyUpdateException(MessageFormat.format(
"Property \"{0}\" could not be updated", propertyName), e);
}
}
private Map<String,Method> getReadAccessors() {
Class<? extends EntityBase> entityClass = getClass();
Map<String,Method> accessors = accessorsByEntityType.get(entityClass);
if (accessors == null) {
accessors = getReadAccessors(entityClass);
Map<String,Method> knownAccessors = accessorsByEntityType.putIfAbsent(entityClass, accessors);
if (knownAccessors != null) {
accessors = knownAccessors;
}
}
return accessors;
}
private static Map<String,Method> getReadAccessors(Class<? extends EntityBase> entityClass) {
Map<String,Method> accessors = new HashMap<String,Method>();
if (entityClass != null) {
for (Field field : entityClass.getFields()) {
if (field.getAnnotation(PropertyName.class) != null) {
try {
String propertyName = (String)field.get(null);
if (StringUtils.isBlank(propertyName)) {
throw new IllegalArgumentException(MessageFormat.format(
"@PropertyName {0} defines a blank property name", field));
}
Method accessor = getReadAccessor(entityClass, propertyName, null);
if (accessor != null) {
accessors.put(propertyName, accessor);
}
} catch (Exception e) {
throw new IllegalArgumentException(MessageFormat.format(
"Invalid @PropertyName declaration: {0}", field), e);
}
}
}
}
return Collections.unmodifiableMap(accessors);
}
private static Method getReadAccessor(Class<?> c, String propertyName, Class<?>[] argTypes) {
Method accessor = getReadAccessor(c, "get", propertyName, argTypes); //$NON-NLS-1$
if (accessor == null) {
accessor = getReadAccessor(c, "is", propertyName, argTypes); //$NON-NLS-1$
}
return accessor;
}
private static Method getReadAccessor(Class<?> c, String prefix, String propertyName, Class<?>[] argTypes) {
String name = prefix + StringUtils.capitalize(propertyName);
try {
return c.getMethod(name, argTypes != null? argTypes : new Class[0]);
} catch (NoSuchMethodException e) {
if (LOG.isDebugEnabled()) {
LOG.debug(MessageFormat.format(
"Entity of type \"{0}\" has no accessor method for property \"{1}\"",
c.getName(), propertyName));
}
}
return null;
}
private Method getWriteAccessor(String propertyName, Class<?> propertyType) {
Method accessor = null;
Class<? extends EntityBase> entityClass = getClass();
String name = "set" + StringUtils.capitalize(propertyName); //$NON-NLS-1$
if (propertyType != null && PRIMITIVES_MAP.containsKey(propertyType)) {
propertyType = PRIMITIVES_MAP.get(propertyType);
}
Class<?>[] argumentTypes = new Class<?>[] { propertyType };
try {
accessor = entityClass.getMethod(name, argumentTypes);
} catch (NoSuchMethodException e) {
accessor = getWriteAccessor(entityClass, name, argumentTypes, propertyName);
}
return accessor;
}
private Method getWriteAccessor(Class<?> c, String name, Class<?>[] argumentTypes, String propertyName)
throws PropertyUpdateException {
Method accessor = null;
try {
for (Method candidate: c.getMethods()) {
if (name.equals(candidate.getName())) {
Class<?>[] parameterTypes = candidate.getParameterTypes();
if (parameterTypes.length == 1) {
if (argumentTypes[0] == null || parameterTypes[0].isAssignableFrom(argumentTypes[0])) {
argumentTypes[0] = parameterTypes[0];
} else {
throw new PropertyUpdateException(MessageFormat.format(
"Argument of type \"{0}\" cannot be assigned to property \"{1}\" of type \"{2}\"",
argumentTypes[0], propertyName, parameterTypes[0]));
}
}
accessor = c.getMethod(name, argumentTypes);
break;
}
}
} catch (NoSuchMethodException e) {
if (LOG.isDebugEnabled()) {
LOG.debug(MessageFormat.format(
"Entity of type \"{0}\" has no accessor method matching the signature \"{1}({2})",
c.getName(), name, argumentTypes[0].getName()));
}
}
return accessor;
}
@Override
public String toString() {
if (uuid != null) {
return uuid.toString();
}
return super.toString();
}
@Override
public int hashCode() {
return (uuid == null) ? super.hashCode() : uuid.hashCode();
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null) {
return false;
}
if (getClass() != o.getClass()) {
return false;
}
EntityBase entityBase = (EntityBase) o;
if (uuid == null) {
if (entityBase.uuid != null) {
return false;
}
} else if (!uuid.equals(entityBase.uuid)) {
return false;
}
return true;
}
}