package org.activityinfo.server.entity.change;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Provider;
import org.activityinfo.server.database.hibernate.entity.Deleteable;
import org.activityinfo.server.entity.auth.Authorization;
import org.activityinfo.server.entity.auth.AuthorizationHandler;
import javax.persistence.EntityManager;
import javax.persistence.metamodel.Attribute;
import javax.persistence.metamodel.Attribute.PersistentAttributeType;
import javax.persistence.metamodel.EntityType;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Authorizes, validates, and effects a ChangeRequest.
*/
public class ChangeHandler {
private static final Logger LOGGER = Logger.getLogger(ChangeHandler.class.getName());
private final Injector injector;
private final Provider<EntityManager> entityManager;
private final Validator validator;
@Inject
public ChangeHandler(Injector injector, Provider<EntityManager> entityManager, Validator validator) {
super();
this.injector = injector;
this.entityManager = entityManager;
this.validator = validator;
}
public void execute(ChangeRequest request) {
Change change = forRequest(request);
change.execute();
}
private Change forRequest(ChangeRequest request) {
for (EntityType<?> type : entityManager.get().getMetamodel().getEntities()) {
if (type.getName().equalsIgnoreCase(request.getEntityType())) {
return new Change(type, request);
}
}
throw new ChangeException(ChangeFailureType.UNKNOWN_ENTITY_TYPE);
}
private class Change<T> {
private final EntityType<T> entityType;
private final ChangeRequest request;
public Change(EntityType<T> entityType, ChangeRequest request) {
this.entityType = entityType;
this.request = request;
}
public void execute() {
switch (request.getChangeType()) {
case CREATE:
create(request);
break;
case UPDATE:
update(request);
break;
case DELETE:
delete(request);
break;
}
}
private void create(ChangeRequest request) {
throw new ChangeException(ChangeFailureType.SERVER_FAULT);
}
private void delete(ChangeRequest request) {
T entity = locate();
authorize(entity);
if (entity instanceof Deleteable) {
((Deleteable) entity).delete();
}
}
public void update(ChangeRequest request) {
T entity = locate();
applyChanges(entity);
}
/**
* Applies the list of updated properties to the entity
*/
private void applyChanges(T entity) {
for (String propertyName : request.getUpdatedProperties()) {
// Get the metamodel for this entity's property
Attribute<? super T, ?> attribute = attributeForPropertyName(propertyName);
// Verify that this property may be updated by the user
verifyUpdateToPropertyIsAllowed(attribute, propertyName);
// Go ahead and update the value
updateProperty(entity, attribute, propertyName);
}
}
private void verifyUpdateToPropertyIsAllowed(Attribute<? super T, ?> attribute, String propertyName) {
AnnotatedElement member = (AnnotatedElement) attribute.getJavaMember();
AllowUserUpdate allowed = member.getAnnotation(AllowUserUpdate.class);
if (allowed == null) {
throw new ChangeException(ChangeFailureType.PROPERTY_NOT_UPDATABLE, propertyName);
}
}
private void updateProperty(T entity, Attribute<? super T, ?> attribute, String propertyName) {
if (attribute.getPersistentAttributeType() == PersistentAttributeType.BASIC) {
updateBasicProperty(entity, attribute, propertyName);
}
}
private void updateBasicProperty(T entity, Attribute<? super T, ?> attribute, String propertyName) {
Object newValue = request.getPropertyValue(attribute.getJavaType(), propertyName);
Set<ConstraintViolation<T>> violations = validator.validateValue(entityType.getJavaType(),
attribute.getName(),
newValue);
if (!violations.isEmpty()) {
throw new ChangeException(propertyName, violations);
}
Method setter = setterForAttribute(attribute);
try {
setter.invoke(entity, newValue);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
throw new ChangeException(e);
}
}
private Method setterForAttribute(Attribute<? super T, ?> attribute) {
String setterName =
"set" + attribute.getName().substring(0, 1).toUpperCase() + attribute.getName().substring(1);
try {
return entityType.getJavaType().getMethod(setterName, attribute.getJavaType());
} catch (NoSuchMethodException e) {
throw new ChangeException(ChangeFailureType.SERVER_FAULT, e);
} catch (SecurityException e) {
throw new ChangeException(ChangeFailureType.SERVER_FAULT, e);
}
}
private Attribute<? super T, ?> attributeForPropertyName(String propertyName) {
try {
return entityType.getAttribute(propertyName);
} catch (IllegalArgumentException e) {
throw new ChangeException(ChangeFailureType.PROPERTY_DOES_NOT_EXIST, propertyName);
}
}
/**
* Locates the entity to be modified or deleted based on the
* properties of the change request.
*
* @param request the change request
* @return the entity
* @throws ChangeException if the entity does not exist
*/
public T locate() {
T entity = entityManager.get().find(entityType.getJavaType(), getEntityId());
if (entity == null) {
throw new ChangeException(ChangeFailureType.ENTITY_DOES_NOT_EXIST);
}
return entity;
}
/**
* Retrieves the id of the entity to create or modify from the
* change request.
*
* @param request
* @return
*/
public Object getEntityId() {
String id = request.getEntityId();
Class<?> type = entityType.getIdType().getJavaType();
if (type.equals(Integer.class) || type.equals(int.class)) {
return Integer.parseInt(id);
} else if (type.equals(Long.class) || type.equals(long.class)) {
return Long.parseLong(id);
} else if (type.equals(String.class)) {
return id;
} else {
throw new ChangeException(ChangeFailureType.MALFORMED_ID);
}
}
/**
* Verifies that the user is authorized to make the change. Subclass <strong>must</strong>
* override, the default implementation always throws
*
* @param user the requesting user
* @param entity the entity to be created or modified
* @param request the change request
* @throws ChangeException if the user is not authorized
*/
public void authorize(T entity) {
Authorization authorization = findAuthorizationAnnotation();
boolean authorized;
try {
AuthorizationHandler<T> authorizationHandler = injector.getInstance(authorization.handler());
authorized = authorizationHandler.isAuthorized(request.getRequestingUser(), entity);
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "Exception thrown by authorization handler", e);
throw new ChangeException(ChangeFailureType.SERVER_FAULT, e);
}
if (!authorized) {
throw new ChangeException(ChangeFailureType.NOT_AUTHORIZED);
}
}
private Authorization findAuthorizationAnnotation() {
Authorization authorization = findAuthorizationAnnotation(entityType.getJavaType());
if (authorization == null) {
LOGGER.severe("Entity class " + entityType.getName() + " does not have an @Authorization annotation," +
" denying all change requests.");
throw new ChangeException(ChangeFailureType.NOT_AUTHORIZED);
}
return authorization;
}
private Authorization findAuthorizationAnnotation(Class<?> type) {
Authorization authorization = type.getAnnotation(Authorization.class);
if (authorization != null) {
return authorization;
}
if (type.getSuperclass() != null) {
authorization = findAuthorizationAnnotation(type.getSuperclass());
if (authorization != null) {
return authorization;
}
}
for (Class<?> interfaceClass : type.getInterfaces()) {
authorization = findAuthorizationAnnotation(interfaceClass);
if (authorization != null) {
return authorization;
}
}
return authorization;
}
}
}