blob: 8ac8d40552fa8038619fbde10b94b5c529b33b10 [file] [log] [blame]
/*
* 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.jpa.dao;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;
import javax.persistence.Query;
import javax.persistence.TemporalType;
import javax.persistence.TypedQuery;
import org.apache.commons.jexl3.parser.Parser;
import org.apache.commons.jexl3.parser.ParserConstants;
import org.apache.commons.jexl3.parser.Token;
import org.apache.commons.lang3.StringUtils;
import org.apache.syncope.core.persistence.api.dao.AllowedSchemas;
import org.apache.syncope.core.persistence.api.dao.AnyDAO;
import org.apache.syncope.core.persistence.api.dao.DerSchemaDAO;
import org.apache.syncope.core.persistence.api.dao.DynRealmDAO;
import org.apache.syncope.core.persistence.api.dao.NotFoundException;
import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
import org.apache.syncope.core.persistence.api.dao.search.AnyCond;
import org.apache.syncope.core.persistence.api.dao.search.AttrCond;
import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
import org.apache.syncope.core.persistence.api.entity.Any;
import org.apache.syncope.core.persistence.api.entity.AnyTypeClass;
import org.apache.syncope.core.persistence.api.entity.AnyUtils;
import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
import org.apache.syncope.core.persistence.api.entity.DerSchema;
import org.apache.syncope.core.persistence.api.entity.DynRealm;
import org.apache.syncope.core.persistence.api.entity.PlainAttrUniqueValue;
import org.apache.syncope.core.persistence.api.entity.PlainAttrValue;
import org.apache.syncope.core.persistence.api.entity.PlainSchema;
import org.apache.syncope.core.persistence.api.entity.Schema;
import org.apache.syncope.core.persistence.api.entity.VirSchema;
import org.apache.syncope.core.persistence.api.entity.anyobject.AnyObject;
import org.apache.syncope.core.persistence.api.entity.group.Group;
import org.apache.syncope.core.persistence.api.entity.resource.ExternalResource;
import org.apache.syncope.core.persistence.api.entity.user.User;
import org.apache.syncope.core.persistence.jpa.entity.user.JPAUser;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
public abstract class AbstractAnyDAO<A extends Any<?>> extends AbstractDAO<A> implements AnyDAO<A> {
protected final AnyUtilsFactory anyUtilsFactory;
protected final ApplicationEventPublisher publisher;
protected final PlainSchemaDAO plainSchemaDAO;
protected final DerSchemaDAO derSchemaDAO;
protected final DynRealmDAO dynRealmDAO;
private AnyUtils anyUtils;
public AbstractAnyDAO(
final AnyUtilsFactory anyUtilsFactory,
final ApplicationEventPublisher publisher,
final PlainSchemaDAO plainSchemaDAO,
final DerSchemaDAO derSchemaDAO,
final DynRealmDAO dynRealmDAO) {
this.anyUtilsFactory = anyUtilsFactory;
this.publisher = publisher;
this.plainSchemaDAO = plainSchemaDAO;
this.derSchemaDAO = derSchemaDAO;
this.dynRealmDAO = dynRealmDAO;
}
protected abstract AnyUtils init();
protected AnyUtils anyUtils() {
synchronized (this) {
if (anyUtils == null) {
anyUtils = init();
}
}
return anyUtils;
}
protected String findKey(final String name, final String table) {
Query query = entityManager().createNativeQuery(
"SELECT id FROM " + table + " WHERE " + (JPAUser.TABLE.equals(table) ? "username" : "name") + "=?");
query.setParameter(1, name);
String key = null;
for (Object resultKey : query.getResultList()) {
key = resultKey instanceof Object[]
? (String) ((Object[]) resultKey)[0]
: ((String) resultKey);
}
return key;
}
@SuppressWarnings("unchecked")
protected List<String> findAllKeys(final String table, final int page, final int itemsPerPage) {
Query query = entityManager().createNativeQuery(
"SELECT id FROM " + table + " ORDER BY id", String.class);
query.setFirstResult(itemsPerPage * (page <= 0 ? 0 : page - 1));
query.setMaxResults(itemsPerPage);
List<String> result = new ArrayList<>();
query.getResultList().stream().map(resultKey -> resultKey instanceof Object[]
? (String) ((Object[]) resultKey)[0]
: ((String) resultKey)).
forEach(actualKey -> result.add(actualKey.toString()));
return result;
}
protected Date findLastChange(final String key, final String table) {
Query query = entityManager().createNativeQuery(
"SELECT creationDate, lastChangeDate FROM " + table + " WHERE id=?");
query.setParameter(1, key);
@SuppressWarnings("unchecked")
List<Object[]> result = query.getResultList();
Date creationDate = null;
Date lastChangeDate = null;
if (!result.isEmpty()) {
creationDate = (Date) result.get(0)[0];
lastChangeDate = (Date) result.get(0)[1];
}
return Optional.ofNullable(lastChangeDate).orElse(creationDate);
}
protected abstract void securityChecks(A any);
@Transactional(readOnly = true)
@Override
public List<A> findByKeys(final List<String> keys) {
Class<A> entityClass = anyUtils().anyClass();
TypedQuery<A> query = entityManager().createQuery(
"SELECT e FROM " + entityClass.getSimpleName() + " e WHERE e.id IN (:keys)", entityClass);
query.setParameter("keys", keys);
return query.getResultList();
}
@Transactional(readOnly = true)
@Override
public A authFind(final String key) {
if (key == null) {
throw new NotFoundException("Null key");
}
A any = find(key);
if (any == null) {
throw new NotFoundException(StringUtils.substringBefore(
StringUtils.substringAfter(getClass().getSimpleName(), "JPA"), "DAO") + ' ' + key);
}
securityChecks(any);
return any;
}
@Transactional(readOnly = true)
@Override
@SuppressWarnings("unchecked")
public A find(final String key) {
return (A) entityManager().find(anyUtils().anyClass(), key);
}
private Query findByPlainAttrValueQuery(final String entityName, final boolean ignoreCaseMatch) {
String query = "SELECT e FROM " + entityName + " e"
+ " WHERE e.attribute.schema.id = :schemaKey AND ((e.stringValue IS NOT NULL"
+ " AND "
+ (ignoreCaseMatch ? "LOWER(" : "") + "e.stringValue" + (ignoreCaseMatch ? ")" : "")
+ " = "
+ (ignoreCaseMatch ? "LOWER(" : "") + ":stringValue" + (ignoreCaseMatch ? ")" : "") + ')'
+ " 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))";
return entityManager().createQuery(query);
}
@Override
@SuppressWarnings("unchecked")
public List<A> findByPlainAttrValue(
final PlainSchema schema,
final PlainAttrValue attrValue,
final boolean ignoreCaseMatch) {
if (schema == null) {
LOG.error("No PlainSchema");
return List.of();
}
String entityName = schema.isUniqueConstraint()
? anyUtils().plainAttrUniqueValueClass().getName()
: anyUtils().plainAttrValueClass().getName();
Query query = findByPlainAttrValueQuery(entityName, ignoreCaseMatch);
query.setParameter("schemaKey", schema.getKey());
query.setParameter("stringValue", attrValue.getStringValue());
query.setParameter("booleanValue", attrValue.getBooleanValue());
if (attrValue.getDateValue() == null) {
query.setParameter("dateValue", null);
} else {
query.setParameter("dateValue", attrValue.getDateValue(), TemporalType.TIMESTAMP);
}
query.setParameter("longValue", attrValue.getLongValue());
query.setParameter("doubleValue", attrValue.getDoubleValue());
List<A> result = new ArrayList<>();
((List<PlainAttrValue>) query.getResultList()).stream().forEach(value -> {
A any = (A) value.getAttr().getOwner();
if (!result.contains(any)) {
result.add(any);
}
});
return result;
}
@Override
public Optional<A> findByPlainAttrUniqueValue(
final PlainSchema schema,
final PlainAttrUniqueValue attrUniqueValue,
final boolean ignoreCaseMatch) {
if (schema == null) {
LOG.error("No PlainSchema");
return Optional.empty();
}
if (!schema.isUniqueConstraint()) {
LOG.error("This schema has not unique constraint: '{}'", schema.getKey());
return Optional.empty();
}
List<A> result = findByPlainAttrValue(schema, attrUniqueValue, ignoreCaseMatch);
return result.isEmpty()
? Optional.empty()
: Optional.of(result.get(0));
}
/**
* Split an attribute value recurring on provided literals/tokens.
*
* @param attrValue value to be split
* @param literals literals/tokens
* @return split value
*/
private static List<String> split(final String attrValue, final List<String> literals) {
final List<String> attrValues = new ArrayList<>();
if (literals.isEmpty()) {
attrValues.add(attrValue);
} else {
for (String token : attrValue.split(Pattern.quote(literals.get(0)))) {
if (!token.isEmpty()) {
attrValues.addAll(split(token, literals.subList(1, literals.size())));
}
}
}
return attrValues;
}
private Set<String> getWhereClause(final String expression, final String value, final boolean ignoreCaseMatch) {
Parser parser = new Parser(expression);
// Schema keys
List<String> identifiers = new ArrayList<>();
// Literals
List<String> literals = new ArrayList<>();
// Get schema keys and literals
for (Token token = parser.getNextToken(); token != null && StringUtils.isNotBlank(token.toString());
token = parser.getNextToken()) {
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
literals.sort((l1, l2) -> {
if (l1 == null && l2 == null) {
return 0;
} else if (l1 != null && l2 == null) {
return -1;
} else if (l1 == null) {
return 1;
} else if (l1.length() == l2.length()) {
return 0;
} else if (l1.length() > l2.length()) {
return -1;
} else {
return 1;
}
});
// Split value on provided literals
List<String> attrValues = split(value, literals);
if (attrValues.size() != identifiers.size()) {
LOG.error("Ambiguous JEXL expression resolution: literals and values have different size");
return Set.of();
}
// clauses to be used with INTERSECTed queries
Set<String> clauses = new HashSet<>();
// builder to build the clauses
StringBuilder bld = new StringBuilder();
// Contains used identifiers in order to avoid replications
Set<String> used = new HashSet<>();
// Create several clauses: one for each identifiers
for (int i = 0; i < identifiers.size(); i++) {
if (!used.contains(identifiers.get(i))) {
// verify schema existence and get schema type
PlainSchema schema = plainSchemaDAO.find(identifiers.get(i));
if (schema == null) {
LOG.error("Invalid schema '{}', ignoring", identifiers.get(i));
} else {
// clear builder
bld.delete(0, bld.length());
bld.append('(');
// set schema key
bld.append("s.id = '").append(identifiers.get(i)).append('\'');
bld.append(" AND ");
bld.append("s.id = a.schema_id").append(" AND ");
bld.append("a.id = v.attribute_id");
bld.append(" AND ");
// use a value clause different for each 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:
if (ignoreCaseMatch) {
bld.append("LOWER(v.stringValue) = '").
append(attrValues.get(i).toLowerCase()).append('\'');
} else {
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;
}
@Override
public List<A> findByDerAttrValue(final DerSchema schema, final String value, final boolean ignoreCaseMatch) {
if (schema == null) {
LOG.error("No DerSchema");
return List.of();
}
// query string
StringBuilder querystring = new StringBuilder();
boolean subquery = false;
for (String clause : getWhereClause(schema.getExpression(), value, ignoreCaseMatch)) {
if (querystring.length() > 0) {
subquery = true;
querystring.append(" AND a.owner_id IN ( ");
}
querystring.append("SELECT a.owner_id ").
append("FROM ").append(anyUtils().plainAttrClass().getSimpleName().substring(3)).append(" a, ").
append(anyUtils().plainAttrValueClass().getSimpleName().substring(3)).append(" v, ").
append(PlainSchema.class.getSimpleName()).append(" s ").
append("WHERE ").append(clause);
if (subquery) {
querystring.append(')');
}
}
List<A> result = new ArrayList<>();
if (querystring.length() > 0) {
Query query = entityManager().createNativeQuery(querystring.toString());
for (Object anyKey : query.getResultList()) {
A any = find(anyKey.toString());
if (!result.contains(any)) {
result.add(any);
}
}
}
return result;
}
@SuppressWarnings("unchecked")
@Override
public List<A> findByResource(final ExternalResource resource) {
Query query = entityManager().createQuery("SELECT e FROM " + anyUtils().anyClass().getSimpleName() + " e "
+ "WHERE :resource MEMBER OF e.resources");
query.setParameter("resource", resource);
return query.getResultList();
}
@Override
public SearchCond getAllMatchingCond() {
AnyCond idCond = new AnyCond(AttrCond.Type.ISNOTNULL);
idCond.setSchema("id");
return SearchCond.getLeaf(idCond);
}
@Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true)
@Override
@SuppressWarnings("unchecked")
public <S extends Schema> AllowedSchemas<S> findAllowedSchemas(final A any, final Class<S> reference) {
AllowedSchemas<S> result = new AllowedSchemas<>();
// schemas given by type and aux classes
Set<AnyTypeClass> typeOwnClasses = new HashSet<>();
typeOwnClasses.addAll(any.getType().getClasses());
typeOwnClasses.addAll(any.getAuxClasses());
typeOwnClasses.forEach(typeClass -> {
if (reference.equals(PlainSchema.class)) {
result.getForSelf().addAll((Collection<? extends S>) typeClass.getPlainSchemas());
} else if (reference.equals(DerSchema.class)) {
result.getForSelf().addAll((Collection<? extends S>) typeClass.getDerSchemas());
} else if (reference.equals(VirSchema.class)) {
result.getForSelf().addAll((Collection<? extends S>) typeClass.getVirSchemas());
}
});
// schemas given by type extensions
Map<Group, List<? extends AnyTypeClass>> typeExtensionClasses = new HashMap<>();
if (any instanceof User) {
((User) any).getMemberships().forEach(memb -> memb.getRightEnd().getTypeExtensions().
forEach(typeExt -> typeExtensionClasses.put(memb.getRightEnd(), typeExt.getAuxClasses())));
} else if (any instanceof AnyObject) {
((AnyObject) any).getMemberships().forEach(memb -> memb.getRightEnd().getTypeExtensions().stream().
filter(typeExt -> any.getType().equals(typeExt.getAnyType())).
forEach(typeExt -> typeExtensionClasses.put(memb.getRightEnd(), typeExt.getAuxClasses())));
}
typeExtensionClasses.entrySet().stream().map(entry -> {
result.getForMemberships().put(entry.getKey(), new HashSet<>());
return entry;
}).forEach(entry -> entry.getValue().forEach(typeClass -> {
if (reference.equals(PlainSchema.class)) {
result.getForMemberships().get(entry.getKey()).
addAll((Collection<? extends S>) typeClass.getPlainSchemas());
} else if (reference.equals(DerSchema.class)) {
result.getForMemberships().get(entry.getKey()).
addAll((Collection<? extends S>) typeClass.getDerSchemas());
} else if (reference.equals(VirSchema.class)) {
result.getForMemberships().get(entry.getKey()).
addAll((Collection<? extends S>) typeClass.getVirSchemas());
}
}));
return result;
}
@Override
public A save(final A any) {
return entityManager().merge(any);
}
@Override
public void delete(final String key) {
A any = find(key);
if (any == null) {
return;
}
delete(any);
}
@Transactional(readOnly = true)
@Override
@SuppressWarnings("unchecked")
public List<String> findDynRealms(final String key) {
Query query = entityManager().createNativeQuery(
"SELECT dynRealm_id FROM " + JPADynRealmDAO.DYNMEMB_TABLE + " WHERE any_id=?");
query.setParameter(1, key);
List<String> result = new ArrayList<>();
query.getResultList().stream().map(resultKey -> resultKey instanceof Object[]
? (String) ((Object[]) resultKey)[0]
: ((String) resultKey)).
forEach((actualKey) -> {
DynRealm dynRealm = dynRealmDAO.find(actualKey.toString());
if (dynRealm == null) {
LOG.error("Could not find dynRealm with id {}, even though returned by the native query",
actualKey);
} else if (!result.contains(actualKey.toString())) {
result.add(actualKey.toString());
}
});
return result;
}
}