/*
* Copyright 2002-2007 the original author or authors.
*
* Licensed under the Apache license, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.internna.iwebmvc.spring.services.dwr;
import static org.internna.iwebmvc.spring.util.SecurityUtils.isUserInRole;
import static org.internna.iwebmvc.utils.JPAUtils.getReverseSetter;
import static org.springframework.util.StringUtils.hasText;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import javax.annotation.security.RolesAllowed;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.directwebremoting.annotations.RemoteMethod;
import org.directwebremoting.annotations.RemoteProxy;
import org.hibernate.validator.ClassValidator;
import org.hibernate.validator.InvalidValue;
import org.internna.iwebmvc.RollbackException;
import org.internna.iwebmvc.crypto.Decipherer;
import org.internna.iwebmvc.dao.DAO;
import org.internna.iwebmvc.javascript.EntityParser;
import org.internna.iwebmvc.metadata.EntitySecurity;
import org.internna.iwebmvc.metadata.EntitySecurityRule;
import org.internna.iwebmvc.model.AbstractOwnedDomainEntity;
import org.internna.iwebmvc.model.Captcha;
import org.internna.iwebmvc.model.Displayable;
import org.internna.iwebmvc.model.DomainEntity;
import org.internna.iwebmvc.model.Poll;
import org.internna.iwebmvc.model.PollOption;
import org.internna.iwebmvc.model.PollVote;
import org.internna.iwebmvc.model.UUID;
import org.internna.iwebmvc.model.User;
import org.internna.iwebmvc.model.security.UserImpl;
import org.internna.iwebmvc.model.ui.Filter;
import org.internna.iwebmvc.model.ui.Sort;
import org.internna.iwebmvc.security.AJAXManageableImageCaptchaService;
import org.internna.iwebmvc.security.UserManager;
import org.internna.iwebmvc.spring.i18n.WebContextLocale;
import org.internna.iwebmvc.utils.Assert;
import org.internna.iwebmvc.utils.ClassUtils;
import org.internna.iwebmvc.utils.CollectionUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.context.MessageSource;
import org.springframework.transaction.annotation.Transactional;
/**
* Default implementation based on a DAO.
*
* @author Jose Noheda
* @since 1.0
*/
@RemoteProxy(name = "EntityManager")
public class RemoteEntityManagerImpl implements RemoteEntityManager {
protected enum OPERATOR {
LESS_THAN("<"), LESS_OR_EQUAL_THAN("<="), EQUAL("="), LIKE ("LIKE"), GREATER_THAN (">"), GREATER_OR_EQUAL_THAN(">="), NOT_EQUAL("!=");
private String op;
OPERATOR(String operator) {
this.op = operator;
}
@Override public String toString() {
return op;
}
}
protected final String MAX = "_max";
protected static Log log = LogFactory.getLog(RemoteEntityManagerImpl.class);
protected DAO dao;
protected Decipherer decipherer;
protected UserManager userManager;
protected MessageSource messageResolver;
protected List<EntityParser<?>> parsers;
protected AJAXManageableImageCaptchaService captchaService;
@Required public void setDao(DAO dao) {
this.dao = dao;
}
public void setDecipherer(Decipherer decipherer) {
this.decipherer = decipherer;
}
public void setMessageResolver(MessageSource messageResolver) {
this.messageResolver = messageResolver;
}
public void setUserManager(UserManager userManager) {
this.userManager = userManager;
}
public void setCaptchaService(AJAXManageableImageCaptchaService captchaService) {
this.captchaService = captchaService;
}
public void setParsers(List<EntityParser<?>> parsers) {
this.parsers = parsers;
}
protected Object getEnum(Class<? extends DomainEntity> clazz, Filter filter, Object val) {
return "ENUM".equals(filter.getType()) & (val != null) ? ClassUtils.getEnumValue(clazz, filter.getPath(), Integer.parseInt(val.toString())) : val;
}
protected Map<String, Object> mapFilters(Class<? extends DomainEntity> clazz, List<Filter> filters, HttpServletRequest request) throws ClassNotFoundException {
Map<String, Object> parameters = null;
if (filters != null) {
parameters = new HashMap<String, Object>(filters.size());
for (Filter filter : filters) {
if (hasText(filter.getPath()) && (!filter.isNullValue())) {
if (hasText(filter.getFrom())) {
if ("COLLECTION".equals(filter.getType())) parameters.put(filter.getSanitizedPath(), dao.getReference(ClassUtils.forName(decipherer.decrypt(filter.getEntityClass())), new UUID(decipherer.decrypt(filter.get("from").toString()))));
else if (("ENTITY".equals(filter.getType())) || ("SCRIPTABLE".equals(filter.getType()))) parameters.put(filter.getSanitizedPath(), new UUID(decipherer.decrypt(filter.get("from").toString())));
else parameters.put(filter.getSanitizedPath(), getEnum(clazz, filter, filter.get("from")));
}
if (hasText(filter.getTo())) parameters.put(filter.getSanitizedPath() + MAX, filter.get("to"));
}
}
}
if ((clazz != null) && AbstractOwnedDomainEntity.class.isAssignableFrom(clazz)) {
if (parameters == null) parameters = new HashMap<String, Object>(1);
User user = userManager.getActiveUser(request);
parameters.put("viewer", user.isAnonymous() ? null : user);
}
return parameters == null ? Collections.EMPTY_MAP : Collections.unmodifiableMap(parameters);
}
protected StringBuilder queryForBasicFilter(Filter filter, OPERATOR operator) {
StringBuilder clause = new StringBuilder();
if (hasText(filter.getFrom())) {
clause.append("( LOWER(e.");
if (hasText(filter.getEmbedded())) clause.append(filter.getEmbedded()).append(".");
clause.append(filter.getPath()).append(") ").append(filter.isRange() ? OPERATOR.GREATER_OR_EQUAL_THAN : operator).append(" :").append(filter.getSanitizedPath()).append(" )");
}
if (hasText(filter.getTo())) {
if (clause.length() > 0) clause.append(" AND ");
clause.append("( LOWER(e.");
if (hasText(filter.getEmbedded())) clause.append(filter.getEmbedded()).append(".");
clause.append(filter.getPath()).append(") ").append(OPERATOR.LESS_OR_EQUAL_THAN).append(" :").append(filter.getSanitizedPath() + MAX).append(" )");
}
return clause;
}
protected StringBuilder queryForI18nFilter(Filter filter) {
if (hasText(filter.getFrom()) & filter.isRange() & hasText(filter.getTo())) {
return new StringBuilder("( LOWER(").append(filter.getPath()).append(") BETWEEN :")
.append(filter.getSanitizedPath()).append(" AND :").append(filter.getSanitizedPath() + MAX).append(" ) ");
} else return queryForBasicFilter(filter, OPERATOR.LIKE);
}
protected StringBuilder queryForTextFilter(Filter filter) {
return queryForBasicFilter(filter, OPERATOR.LIKE);
}
protected StringBuilder queryForNumberFilter(Filter filter) {
StringBuilder clause = new StringBuilder();
if (hasText(filter.getFrom()))
clause.append("( e.").append(filter.getPath()).append(" ").append(filter.isRange() ? OPERATOR.GREATER_OR_EQUAL_THAN : (filter.isInvert() ? OPERATOR.NOT_EQUAL : OPERATOR.EQUAL)).append(" :").append(filter.getSanitizedPath()).append(" )");
if (hasText(filter.getTo())) {
if (clause.length() > 0) clause.append(" AND ");
clause.append("( e.").append(filter.getPath()).append(" ").append(OPERATOR.LESS_OR_EQUAL_THAN).append(" :").append(filter.getSanitizedPath() + MAX).append(" )");
}
return clause;
}
protected StringBuilder queryForRatingFilter(Filter filter) {
return queryForNumberFilter(filter);
}
protected StringBuilder queryForAmountFilter(Filter filter) {
return queryForBasicFilter(filter, OPERATOR.GREATER_OR_EQUAL_THAN);
}
protected StringBuilder queryForDateFilter(Filter filter) {
return queryForBasicFilter(filter, OPERATOR.GREATER_OR_EQUAL_THAN);
}
protected StringBuilder queryForBooleanFilter(Filter filter) {
return hasText(filter.getFrom()) ? new StringBuilder("( e.").append(filter.getPath()).append(" = :").append(filter.getSanitizedPath()).append(" )") : new StringBuilder();
}
protected StringBuilder queryForEntityFilter(Filter filter) {
return hasText(filter.getFrom()) ? new StringBuilder("( e.").append(filter.getPath()).append(filter.isInvert() ? " != :" : " = :").append(filter.getSanitizedPath()).append(" )") : new StringBuilder();
}
protected StringBuilder queryForCollectionFilter(Filter filter) {
return hasText(filter.getFrom()) ? new StringBuilder("( :").append(filter.getSanitizedPath()).append((filter.isInvert() ? " NOT IN" :" IN") +" ELEMENTS (e.").append(filter.getPath()).append(") )") : new StringBuilder();
}
protected StringBuilder filterToQuery(Class<? extends DomainEntity> clazz, Filter filter) {
if (filter.isNullValue()) {
return new StringBuilder("( e.").append(filter.getPath()).append(filter.isInvert() ? " != NULL )" : " = NULL )");
} else {
String type = filter.getType();
if ("I18N".equals(type)) return queryForI18nFilter(filter);
if ("TEXT".equals(type)) return queryForTextFilter(filter);
if ("DATE".equals(type)) return queryForDateFilter(filter);
if ("ENUM".equals(type) | "DOUBLE".equals(type) | "NUMBER".equals(type)) return queryForNumberFilter(filter);
if ("RATING".equals(type)) return queryForRatingFilter(filter);
if ("AMOUNT".equals(type)) return queryForAmountFilter(filter);
if ("BOOLEAN".equals(type)) return queryForBooleanFilter(filter);
if ("ENTITY".equals(type) | "SCRIPTABLE".equals(type)) return queryForEntityFilter(filter);
if ("COLLECTION".equals(type)) return queryForCollectionFilter(filter);
return new StringBuilder();
}
}
protected String getQuery(Class<? extends DomainEntity> clazz, List<Filter> filters, Sort order) {
StringBuilder query = new StringBuilder("SELECT e FROM ")
.append(clazz.getSimpleName())
.append(" e WHERE ");
if (AbstractOwnedDomainEntity.class.isAssignableFrom(clazz)) query.append("( ( e.publicView = true ) OR ( :viewer IN ELEMENTS(e.viewers) ) ) AND ");
if (filters != null) {
for (Filter filter : filters) {
StringBuilder clause = filterToQuery(clazz, filter).append(" AND ");
if (clause.length() > 5) query.append(clause);
}
}
query.delete(query.lastIndexOf(query.indexOf("AND") > 0 ? " AND " : " WHERE "), query.length());
if (order != null) {
query.append(" ORDER BY e.").append(order.getAttribute());
if (order.isDescending()) query.append(" DESC");
}
String q = query.toString();
if (log.isDebugEnabled()) log.debug("Filtering using query [" + q + "]");
return q;
}
@Override
@RemoteMethod
public List<? extends DomainEntity> fetch(Class<? extends DomainEntity> entityClass, int offset, int number, List<Filter> filters, Sort order, HttpServletRequest request) throws Exception {
try {
Assert.notNull(entityClass);
Map<String, Object> parameters = mapFilters(entityClass, filters, request);
if (CollectionUtils.isNotEmpty(parameters) && log.isDebugEnabled()) log.debug("Filtering query with " + parameters);
return CollectionUtils.isEmpty(parameters) && (order == null) ? dao.find(entityClass, offset, number) : dao.findByQuery(getQuery(entityClass, filters, order), offset, number, parameters);
} catch (Exception ex) {
return new ArrayList<DomainEntity>();
}
}
@SuppressWarnings("unchecked")
protected Map<String, InvalidValue> validate(DomainEntity entity) {
Map<String, InvalidValue> fieldErrors = null;
ClassValidator<DomainEntity> validator = new ClassValidator(ClassUtils.getActualClass(entity), ResourceBundle.getBundle("org.hibernate.validator.resources.DefaultValidatorMessages", WebContextLocale.getActiveLocale()));
InvalidValue[] errors = filterErrors(validator.getInvalidValues(entity));
if ((errors != null) && (errors.length > 0)) {
fieldErrors = new HashMap<String, InvalidValue>(errors.length);
for (InvalidValue error : errors)
fieldErrors.put(messageResolver.getMessage(ClassUtils.getActualClass(entity).getName() + "." + error.getPropertyPath(), null, WebContextLocale.getActiveLocale()), error);
}
return fieldErrors;
}
/**
* Removes from the error list those errors that will be resolved during operational flow, for
* example, security constraints.
*
* @param errors any
* @return another array with some of the errors provided (zero or more)
*/
protected InvalidValue[] filterErrors(InvalidValue[] errors) {
List<InvalidValue> filteredErrors = new ArrayList<InvalidValue>();
if ((errors != null) && (errors.length > 0)) {
for (InvalidValue error : errors)
if ((error != null) && (!"owners".equals(error.getPropertyName())))
filteredErrors.add(error);
}
return filteredErrors.toArray(new InvalidValue[0]);
}
/**
* Retrieves a valid @{@link EntityParser} for this type.
*/
protected EntityParser<?> getParser(Class<?> clazz, Object received) {
if (!CollectionUtils.isEmpty(parsers)) {
for (EntityParser<?> parser : parsers)
if (parser.accept(clazz, received))
return parser;
}
return null;
}
/**
* Takes a DomainEntity converted by DWR and transforms it to
* a valid JPA object.
*/
protected DomainEntity parse(DomainEntity entity) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException {
Assert.notNull(entity);
for (PropertyDescriptor property : BeanUtils.getPropertyDescriptors(ClassUtils.getActualClass(entity))) {
Object received = property.getReadMethod().invoke(entity);
EntityParser parser = getParser(property.getPropertyType(), received);
if (parser != null) {
Displayable parsed = parser.parse((Displayable) received);
property.getWriteMethod().invoke(entity, parsed);
}
}
return entity;
}
/**
* Check the validity of the entity and persist the changes if no complains.
*/
@RemoteMethod
@Transactional
@RolesAllowed("ROLE_ENTITY_MANAGER")
@Override public Map<String, InvalidValue> saveEntity(Captcha captcha, DomainEntity entity, HttpServletRequest request) throws Exception {
if (entity.isCaptchaRequired()) {
if ((captcha == null) || (!hasText(captcha.getInput()))) throw new SecurityException("Captcha validation required");
else {
String captchaId = request.getSession().getId();
if (hasText(captcha.getId())) captchaId += captcha.getId();
if (!captchaService.validateResponse(captchaId, captcha.getInput())) {
throw new SecurityException("Captcha validation failed! This should be investigated further");
}
}
}
DomainEntity binded = parse(entity);
Map<String, InvalidValue> errors = validate(binded);
if (CollectionUtils.isEmpty(errors))
errors = entity.getId() == null ? proceedWithNewEntity(entity): proceedWithExistingEntity(entity);
if (!CollectionUtils.isEmpty(errors)) throw new RollbackException(errors);
return errors;
}
private Map<String, InvalidValue> checkPermissions(EntitySecurity constraints, EntitySecurity.CRUD operation) {
Map<String, InvalidValue> errors = new HashMap<String, InvalidValue>();
if (constraints != null) {
for (EntitySecurityRule rule : constraints.value()) {
if (operation.isCompatibleOperation(rule.operation())) {
for (String role : rule.ifAllGranted())
if (!isUserInRole(role))
errors.put(messageResolver.getMessage("iwebmvc.security.missingRequiredRole", null, WebContextLocale.getActiveLocale()), new InvalidValue(role, null, null, null, null));
for (String role : rule.ifNotGranted())
if (isUserInRole(role))
errors.put(messageResolver.getMessage("iwebmvc.security.memberBannedRole", null, WebContextLocale.getActiveLocale()), new InvalidValue(role, null, null, null, null));
boolean any = false;
for (String role : rule.ifAnyGranted()) any |= isUserInRole(role);
if (!any) errors.put(messageResolver.getMessage("iwebmvc.security.missingRequiredRole", null, WebContextLocale.getActiveLocale()), new InvalidValue(Arrays.toString(rule.ifAnyGranted()), null, null, null, null));
}
}
}
return CollectionUtils.isEmpty(errors) ? null : errors;
}
@SuppressWarnings("unchecked")
protected Map<String, InvalidValue> proceedWithNewEntity(DomainEntity entity) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException {
Map<String, InvalidValue> errors = checkPermissions(entity.getClass().getAnnotation(EntitySecurity.class), EntitySecurity.CRUD.CREATE);
if (CollectionUtils.isEmpty(errors)) {
Map<String, Collection<DomainEntity>> added = new HashMap<String, Collection<DomainEntity>>();
for (PropertyDescriptor property : BeanUtils.getPropertyDescriptors(ClassUtils.getActualClass(entity))) {
if (Collection.class.isAssignableFrom(property.getPropertyType())) {
Collection<DomainEntity> loaded = new ArrayList<DomainEntity>();
for (DomainEntity element : (Collection<DomainEntity>) property.getReadMethod().invoke(entity))
loaded.add(dao.find(ClassUtils.getActualClass(element), element.getId()));
added.put(property.getName(), loaded);
}
}
dao.create(entity);
for (String property : added.keySet()) {
Method reverseSetter = null;
for (DomainEntity element : added.get(property)) {
if (reverseSetter == null) reverseSetter = getReverseSetter(ClassUtils.getActualClass(entity), ClassUtils.getActualClass(element), property);
if (reverseSetter != null) {
reverseSetter.invoke(element, entity);
dao.update(element);
}
}
}
}
return errors;
}
@SuppressWarnings("unchecked")
protected Map<String, InvalidValue> proceedWithExistingEntity(DomainEntity entity) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException {
Map<String, InvalidValue> errors = checkPermissions(entity.getClass().getAnnotation(EntitySecurity.class), EntitySecurity.CRUD.EDIT);
if (CollectionUtils.isEmpty(errors)) {
for (PropertyDescriptor property : BeanUtils.getPropertyDescriptors(ClassUtils.getActualClass(entity))) {
if (Collection.class.isAssignableFrom(property.getPropertyType())) {
Collection<DomainEntity> oldCollection = null;
if (entity.getId() != null) oldCollection = (Collection<DomainEntity>) property.getReadMethod().invoke(dao.find(ClassUtils.getActualClass(entity), entity.getId()));
if (oldCollection == null) oldCollection = new ArrayList<DomainEntity>();
Collection<DomainEntity> removedElements = new ArrayList<DomainEntity>();
Collection<DomainEntity> newCollection = (Collection<DomainEntity>) property.getReadMethod().invoke(entity);
if (newCollection == null) newCollection = new ArrayList<DomainEntity>();
for (DomainEntity element : oldCollection) {
if (newCollection.contains(element)) newCollection.remove(element);
else removedElements.add(element);
}
Method reverseSetter = null;
for (DomainEntity element : removedElements) {
oldCollection.remove(element);
if (reverseSetter == null) reverseSetter = getReverseSetter(ClassUtils.getActualClass(entity), ClassUtils.getActualClass(element), property.getName());
if (reverseSetter != null) {
reverseSetter.invoke(element, new Object[] {null});
dao.update(element);
}
}
reverseSetter = null;
for (DomainEntity element : newCollection) {
DomainEntity newElement = dao.find(ClassUtils.getActualClass(element), element.getId());
oldCollection.add(newElement);
if (reverseSetter == null) reverseSetter = getReverseSetter(ClassUtils.getActualClass(entity), ClassUtils.getActualClass(newElement), property.getName());
if (reverseSetter != null) {
reverseSetter.invoke(newElement, entity);
dao.update(newElement);
}
}
property.getWriteMethod().invoke(entity, oldCollection);
}
}
dao.update(entity);
}
return errors;
}
/**
* Deletes (if able) a collection of entities from persistent storage.
*/
@RemoteMethod
@RolesAllowed("ROLE_ENTITY_MANAGER")
@Override public boolean deleteEntities(Class<? extends DomainEntity> entityClass, List<UUID> ids) {
dao.remove(entityClass, ids);
return true;
}
@RemoteMethod
@Override public List<? extends DomainEntity> search(Class<? extends DomainEntity> entityClass, String query, int offset, int number) throws Exception {
Assert.hasText(query);
Assert.notNull(entityClass);
return dao.search(entityClass, query, offset, number);
}
@RemoteMethod
@Override public boolean validateCaptcha(Captcha captcha, HttpServletRequest request) {
boolean validation = false;
if ((captcha != null) && hasText(captcha.getInput())) {
String captchaId = request.getSession().getId();
if (hasText(captcha.getId())) captchaId += captcha.getId();
validation = captchaService.validateResponse(captchaId, captcha.getInput());
}
return validation;
}
@RemoteMethod
@Transactional
@Override public synchronized DomainEntity vote(UUID pollId, UUID pollOptionId, HttpServletRequest request) {
Assert.notNull(pollId);
Assert.notNull(pollOptionId);
Poll poll = dao.find(Poll.class, pollId);
PollOption option = dao.find(PollOption.class, pollOptionId);
User user = userManager.getActiveUser(request);
String IPAddress = request.getRemoteAddr();
if (poll.getUserVote(user, IPAddress, dao) == null) {
if (poll.isAllowAnonymousVotes() | !user.isAnonymous()) {
option.vote();
dao.update(option);
if (poll.isStoreVotes()) {
PollVote pollVote = new PollVote();
pollVote.setVoteTime(new Date());
pollVote.setIP(IPAddress);
if (!user.isAnonymous()) pollVote.setAuthor((UserImpl) user);
pollVote.setOption(option);
dao.create(pollVote);
}
}
}
return poll;
}
}