/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.syncope.core.persistence.dao.impl;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import javax.persistence.NoResultException;
import javax.persistence.Query;
import javax.persistence.TemporalType;
import javax.persistence.TypedQuery;
import org.apache.commons.jexl2.parser.Parser;
import org.apache.commons.jexl2.parser.ParserConstants;
import org.apache.commons.jexl2.parser.Token;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.util.StringUtils;
import org.apache.syncope.core.persistence.beans.AbstractAttrValue;
import org.apache.syncope.core.persistence.beans.AbstractVirAttr;
import org.apache.syncope.core.persistence.beans.ExternalResource;
import org.apache.syncope.core.persistence.beans.PropagationTask;
import org.apache.syncope.core.persistence.beans.membership.Membership;
import org.apache.syncope.core.persistence.beans.user.SyncopeUser;
import org.apache.syncope.core.persistence.beans.user.UAttrUniqueValue;
import org.apache.syncope.core.persistence.beans.user.UAttrValue;
import org.apache.syncope.core.persistence.beans.user.UDerSchema;
import org.apache.syncope.core.persistence.beans.user.USchema;
import org.apache.syncope.core.persistence.dao.DerSchemaDAO;
import org.apache.syncope.core.persistence.dao.RoleDAO;
import org.apache.syncope.core.persistence.dao.SchemaDAO;
import org.apache.syncope.core.persistence.dao.TaskDAO;
import org.apache.syncope.core.persistence.dao.UserDAO;
import org.apache.syncope.core.rest.controller.InvalidSearchConditionException;
@Repository
public class UserDAOImpl extends AbstractDAOImpl implements UserDAO {
@Autowired
private SchemaDAO schemaDAO;
@Autowired
private DerSchemaDAO derSchemaDAO;
@Autowired
private RoleDAO roleDAO;
@Autowired
private TaskDAO taskDAO;
@Override
public SyncopeUser find(final Long id) {
TypedQuery<SyncopeUser> query = entityManager.createQuery("SELECT e FROM " + SyncopeUser.class.getSimpleName()
+ " e " + "WHERE e.id = :id", SyncopeUser.class);
query.setParameter("id", id);
SyncopeUser result = null;
try {
return query.getSingleResult();
} catch (NoResultException e) {
}
return result;
}
@Override
public SyncopeUser find(final String username) {
TypedQuery<SyncopeUser> query = entityManager.createQuery("SELECT e FROM " + SyncopeUser.class.getSimpleName()
+ " e " + "WHERE e.username = :username", SyncopeUser.class);
query.setParameter("username", username);
SyncopeUser result = null;
try {
return query.getSingleResult();
} catch (NoResultException e) {
}
return result;
}
@Override
public SyncopeUser findByWorkflowId(final String workflowId) {
TypedQuery<SyncopeUser> query = entityManager.createQuery("SELECT e FROM " + SyncopeUser.class.getSimpleName()
+ " e " + "WHERE e.workflowId = :workflowId", SyncopeUser.class);
query.setParameter("workflowId", workflowId);
return query.getSingleResult();
}
/**
* Find users by derived attribute value. This method could fail if one or more string literals contained into the
* derived attribute value provided derive from identifier (schema name) replacement. When you are going to specify
* a derived attribute expression you must be quite sure that string literals used to build the expression cannot be
* found into the attribute values used to replace attribute schema names used as identifiers.
*
* @param schemaName derived schema name.
* @param value derived attribute value.
* @return list of users.
* @throws InvalidSearchConditionException in case of errors retrieving schema names used to buid the derived schema
* expression.
*/
@Override
public List<SyncopeUser> findByDerAttrValue(final String schemaName, final String value)
throws InvalidSearchConditionException {
UDerSchema schema = derSchemaDAO.find(schemaName, UDerSchema.class);
if (schema == null) {
LOG.error("Invalid schema name '{}'", schemaName);
return Collections.emptyList();
}
// query string
final StringBuilder querystring = new StringBuilder();
boolean subquery = false;
for (String clause : getWhereClause(schema.getExpression(), value)) {
if (querystring.length() > 0) {
subquery = true;
querystring.append(" AND a.owner_id IN ( ");
}
querystring.append("SELECT a.owner_id ").append("FROM UAttr a, UAttrValue v, USchema s ").append("WHERE ")
.append(clause);
if (subquery) {
querystring.append(')');
}
}
LOG.debug("Execute query {}", querystring);
final Query query = entityManager.createNativeQuery(querystring.toString());
final List<SyncopeUser> result = new ArrayList<SyncopeUser>();
SyncopeUser user;
for (Object userId : query.getResultList()) {
user = find(Long.parseLong(userId.toString()));
if (!result.contains(user)) {
result.add(user);
}
}
return result;
}
@Override
public List<SyncopeUser> findByAttrValue(final String schemaName, final UAttrValue attrValue) {
USchema schema = schemaDAO.find(schemaName, USchema.class);
if (schema == null) {
LOG.error("Invalid schema name '{}'", schemaName);
return Collections.emptyList();
}
final String entityName = schema.isUniqueConstraint()
? UAttrUniqueValue.class.getName()
: UAttrValue.class.getName();
Query query = entityManager.createQuery("SELECT e FROM " + entityName + " e"
+ " WHERE e.attribute.schema.name = :schemaName " + " AND (e.stringValue IS NOT NULL"
+ " AND e.stringValue = :stringValue)" + " OR (e.booleanValue IS NOT NULL"
+ " AND e.booleanValue = :booleanValue)" + " OR (e.dateValue IS NOT NULL"
+ " AND e.dateValue = :dateValue)" + " OR (e.longValue IS NOT NULL" + " AND e.longValue = :longValue)"
+ " OR (e.doubleValue IS NOT NULL" + " AND e.doubleValue = :doubleValue)");
query.setParameter("schemaName", schemaName);
query.setParameter("stringValue", attrValue.getStringValue());
query.setParameter("booleanValue", attrValue.getBooleanValue() == null
? null
: attrValue.getBooleanAsInteger(attrValue.getBooleanValue()));
if (attrValue.getDateValue() != null) {
query.setParameter("dateValue", attrValue.getDateValue(), TemporalType.TIMESTAMP);
} else {
query.setParameter("dateValue", null);
}
query.setParameter("longValue", attrValue.getLongValue());
query.setParameter("doubleValue", attrValue.getDoubleValue());
List<SyncopeUser> result = new ArrayList<SyncopeUser>();
SyncopeUser user;
for (AbstractAttrValue value : (List<AbstractAttrValue>) query.getResultList()) {
user = (SyncopeUser) value.getAttribute().getOwner();
if (!result.contains(user)) {
result.add(user);
}
}
return result;
}
@Override
public SyncopeUser findByAttrUniqueValue(final String schemaName, final UAttrValue attrUniqueValue) {
USchema schema = schemaDAO.find(schemaName, USchema.class);
if (schema == null) {
LOG.error("Invalid schema name '{}'", schemaName);
return null;
}
if (!schema.isUniqueConstraint()) {
LOG.error("This schema has not unique constraint: '{}'", schemaName);
return null;
}
List<SyncopeUser> result = findByAttrValue(schemaName, attrUniqueValue);
return result.isEmpty()
? null
: result.iterator().next();
}
@Override
public List<SyncopeUser> findByResource(final ExternalResource resource) {
Query query = entityManager.createQuery("SELECT e FROM " + SyncopeUser.class.getSimpleName() + " e "
+ "WHERE :resource MEMBER OF e.resources");
query.setParameter("resource", resource);
return query.getResultList();
}
private StringBuilder getFindAllQuery(final Set<Long> adminRoles) {
final StringBuilder queryString = new StringBuilder("SELECT id FROM SyncopeUser WHERE id NOT IN (");
if (adminRoles == null || adminRoles.isEmpty()) {
queryString.append("SELECT syncopeUser_id AS id FROM Membership");
} else {
queryString.append("SELECT syncopeUser_id FROM Membership M1 ").append("WHERE syncopeRole_id IN (");
queryString.append("SELECT syncopeRole_id FROM Membership M2 ").append(
"WHERE M2.syncopeUser_id=M1.syncopeUser_id ").append("AND syncopeRole_id NOT IN (");
queryString.append("SELECT id AS syncopeRole_id FROM SyncopeRole");
boolean firstRole = true;
for (Long adminRoleId : adminRoles) {
if (firstRole) {
queryString.append(" WHERE");
firstRole = false;
} else {
queryString.append(" OR");
}
queryString.append(" id=").append(adminRoleId);
}
queryString.append("))");
}
queryString.append(")");
return queryString;
}
@Override
public final List<SyncopeUser> findAll(final Set<Long> adminRoles) {
return findAll(adminRoles, -1, -1);
}
@Override
public final List<SyncopeUser> findAll(final Set<Long> adminRoles, final int page, final int itemsPerPage) {
final Query query = entityManager.createNativeQuery(getFindAllQuery(adminRoles).toString());
query.setFirstResult(itemsPerPage * (page <= 0
? 0
: page - 1));
if (itemsPerPage > 0) {
query.setMaxResults(itemsPerPage);
}
List<Number> userIds = new ArrayList<Number>();
List resultList = query.getResultList();
//fix for HHH-5902 - bug hibernate
if (resultList != null) {
for (Object userId : resultList) {
if (userId instanceof Object[]) {
userIds.add((Number) ((Object[]) userId)[0]);
} else {
userIds.add((Number) userId);
}
}
}
List<SyncopeUser> result = new ArrayList<SyncopeUser>(userIds.size());
SyncopeUser user;
for (Object userId : userIds) {
user = find(((Number) userId).longValue());
if (user == null) {
LOG.error("Could not find user with id {}, " + "even though returned by the native query", userId);
} else {
result.add(user);
}
}
return result;
}
@Override
public final int count(final Set<Long> adminRoles) {
StringBuilder queryString = getFindAllQuery(adminRoles);
queryString.insert(0, "SELECT COUNT(id) FROM (");
queryString.append(") count_user_id");
Query countQuery = entityManager.createNativeQuery(queryString.toString());
return ((Number) countQuery.getSingleResult()).intValue();
}
@Override
public SyncopeUser save(final SyncopeUser user) {
SyncopeUser merged = entityManager.merge(user);
for (AbstractVirAttr virtual : merged.getVirtualAttributes()) {
virtual.setValues(user.getVirtualAttribute(virtual.getVirtualSchema().getName()).getValues());
}
return merged;
}
@Override
public void delete(final Long id) {
SyncopeUser user = find(id);
if (user == null) {
return;
}
delete(user);
}
@Override
public void delete(final SyncopeUser user) {
// Not calling membershipDAO.delete() here because it would try
// to save this user as well, thus going into
// ConcurrentModificationException
for (Membership membership : user.getMemberships()) {
membership.setSyncopeUser(null);
roleDAO.save(membership.getSyncopeRole());
membership.setSyncopeRole(null);
entityManager.remove(membership);
}
user.getMemberships().clear();
for (PropagationTask task : taskDAO.findAll(user)) {
task.setSyncopeUser(null);
}
entityManager.remove(user);
}
/**
* Generate one where clause for each different attribute schema into the derived schema expression provided.
*
* @param expression derived schema expression.
* @param value derived attribute value.
* @return where clauses to use to build the query.
* @throws InvalidSearchConditionException in case of errors retrieving identifiers.
*/
private Set<String> getWhereClause(final String expression, final String value)
throws InvalidSearchConditionException {
final Parser parser = new Parser(new StringReader(expression));
// Schema names
final List<String> identifiers = new ArrayList<String>();
// Literals
final List<String> literals = new ArrayList<String>();
// Get schema names and literals
Token token;
while ((token = parser.getNextToken()) != null && StringUtils.hasText(token.toString())) {
if (token.kind == ParserConstants.STRING_LITERAL) {
literals.add(token.toString().substring(1, token.toString().length() - 1));
}
if (token.kind == ParserConstants.IDENTIFIER) {
identifiers.add(token.toString());
}
}
// Sort literals in order to process later literals included into others
Collections.sort(literals, new Comparator<String>() {
@Override
public int compare(String t, String t1) {
if (t == null && t1 == null) {
return 0;
} else if (t != null && t1 == null) {
return -1;
} else if (t == null && t1 != null) {
return 1;
} else if (t.length() == t1.length()) {
return 0;
} else if (t.length() > t1.length()) {
return -1;
} else {
return 1;
}
}
});
// Split value on provided literals
final List<String> attrValues = split(value, literals);
if (attrValues.size() != identifiers.size()) {
LOG.error("Ambiguous jexl expression resolution.");
throw new InvalidSearchConditionException("literals and values have different size");
}
// clauses to be used with INTERSECTed queries
final Set<String> clauses = new HashSet<String>();
// builder to build the clauses
final StringBuilder bld = new StringBuilder();
// Contains used identifiers in order to avoid replications
final Set<String> used = new HashSet<String>();
USchema schema;
// Create several clauses: one for eanch identifiers
for (int i = 0; i < identifiers.size(); i++) {
if (!used.contains(identifiers.get(i))) {
// verify schema existence and get schema type
schema = schemaDAO.find(identifiers.get(i), USchema.class);
if (schema == null) {
LOG.error("Invalid schema name '{}'", identifiers.get(i));
throw new InvalidSearchConditionException("Invalid schema name " + identifiers.get(i));
}
// clear builder
bld.delete(0, bld.length());
bld.append("(");
// set schema name
bld.append("s.name = '").append(identifiers.get(i)).append("'");
bld.append(" AND ");
bld.append("s.name = a.schema_name").append(" AND ");
bld.append("a.id = v.attribute_id");
bld.append(" AND ");
// use a value clause different for eanch different schema type
switch (schema.getType()) {
case Boolean:
bld.append("v.booleanValue = '").append(attrValues.get(i)).append("'");
break;
case Long:
bld.append("v.longValue = ").append(attrValues.get(i));
break;
case Double:
bld.append("v.doubleValue = ").append(attrValues.get(i));
break;
case Date:
bld.append("v.dateValue = '").append(attrValues.get(i)).append("'");
break;
default:
bld.append("v.stringValue = '").append(attrValues.get(i)).append("'");
}
bld.append(")");
used.add(identifiers.get(i));
clauses.add(bld.toString());
}
}
LOG.debug("Generated where clauses {}", clauses);
return clauses;
}
/**
* Split an attribute value recurring on provided literals/tokens.
*
* @param attrValue value to be split.
* @param literals literals/tokens.
* @return
*/
private List<String> split(final String attrValue, final List<String> literals) {
final List<String> attrValues = new ArrayList<String>();
if (literals.isEmpty()) {
attrValues.add(attrValue);
} else {
for (String token : attrValue.split(Pattern.quote(literals.get(0)))) {
attrValues.addAll(split(token, literals.subList(1, literals.size())));
}
}
return attrValues;
}
}