blob: b4a1e01189f8b7e7fd8e048a96802272066cf455 [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.Date;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.StringJoiner;
import java.util.regex.Pattern;
import javax.persistence.Query;
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.commons.lang3.tuple.Pair;
import org.apache.syncope.common.lib.types.AttrSchemaType;
import org.apache.syncope.core.persistence.api.dao.DuplicateException;
import org.apache.syncope.core.persistence.api.dao.JPAJSONAnyDAO;
import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
import org.apache.syncope.core.persistence.api.entity.Any;
import org.apache.syncope.core.persistence.api.entity.AnyUtils;
import org.apache.syncope.core.persistence.api.entity.DerSchema;
import org.apache.syncope.core.persistence.api.entity.JSONPlainAttr;
import org.apache.syncope.core.persistence.api.entity.PlainAttr;
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.jpa.entity.AbstractEntity;
import org.apache.syncope.core.spring.security.AuthContextUtils;
import org.springframework.transaction.annotation.Transactional;
abstract class AbstractJPAJSONAnyDAO extends AbstractDAO<AbstractEntity> implements JPAJSONAnyDAO {
protected final PlainSchemaDAO plainSchemaDAO;
protected AbstractJPAJSONAnyDAO(final PlainSchemaDAO plainSchemaDAO) {
this.plainSchemaDAO = plainSchemaDAO;
}
protected abstract String queryBegin(String table);
protected abstract String attrValueMatch(
AnyUtils anyUtils,
PlainSchema schema,
PlainAttrValue attrValue,
boolean ignoreCaseMatch);
protected Pair<String, Boolean> schemaInfo(final AttrSchemaType schemaType, final boolean ignoreCaseMatch) {
String key;
boolean lower = false;
switch (schemaType) {
case Boolean:
key = "booleanValue";
break;
case Date:
key = "dateValue";
break;
case Double:
key = "doubleValue";
break;
case Long:
key = "longValue";
break;
case Binary:
key = "binaryValue";
break;
default:
lower = ignoreCaseMatch;
key = "stringValue";
}
return Pair.of(key, lower);
}
protected <A extends Any<?>> List<A> buildResult(final AnyUtils anyUtils, final List<Object> queryResult) {
List<A> result = new ArrayList<>();
queryResult.forEach(anyKey -> {
A any = anyUtils.<A>dao().find(anyKey.toString());
if (any == null) {
LOG.error("Could not find any for key {}", anyKey);
} else {
result.add(any);
}
});
return result;
}
@SuppressWarnings("unchecked")
@Transactional(readOnly = true)
@Override
public <A extends Any<?>> List<A> findByPlainAttrValue(
final String table,
final AnyUtils anyUtils,
final PlainSchema schema,
final PlainAttrValue attrValue,
final boolean ignoreCaseMatch) {
if (schema == null) {
LOG.error("No PlainSchema");
return List.of();
}
Query query = entityManager().createNativeQuery(
queryBegin(table)
+ "WHERE " + attrValueMatch(anyUtils, schema, attrValue, ignoreCaseMatch));
query.setParameter(1, schema.getKey());
query.setParameter(2, attrValue.getValue());
return buildResult(anyUtils, query.getResultList());
}
@Transactional(readOnly = true)
@Override
public <A extends Any<?>> Optional<A> findByPlainAttrUniqueValue(
final String table,
final AnyUtils anyUtils,
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(table, anyUtils, 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;
}
@SuppressWarnings("unchecked")
protected List<Object> findByDerAttrValue(
final String table,
final Map<String, List<Object>> clauses) {
StringJoiner actualClauses = new StringJoiner(" AND id IN ");
List<Object> queryParams = new ArrayList<>();
clauses.forEach((clause, parameters) -> {
actualClauses.add(clause);
queryParams.addAll(parameters);
});
Query query = entityManager().createNativeQuery(
"SELECT DISTINCT id FROM " + table + " u WHERE id IN " + actualClauses.toString());
for (int i = 0; i < queryParams.size(); i++) {
query.setParameter(i + 1, queryParams.get(i));
}
return query.getResultList();
}
@SuppressWarnings("unchecked")
@Transactional(readOnly = true)
@Override
public <A extends Any<?>> List<A> findByDerAttrValue(
final String table,
final AnyUtils anyUtils,
final DerSchema derSchema,
final String value,
final boolean ignoreCaseMatch) {
if (derSchema == null) {
LOG.error("No DerSchema");
return List.of();
}
Parser parser = new Parser(derSchema.getExpression());
// 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 List.of();
}
Map<String, List<Object>> clauses = new LinkedHashMap<>();
// 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 eanch 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());
PlainAttrValue attrValue;
if (schema.isUniqueConstraint()) {
attrValue = anyUtils.newPlainAttrUniqueValue();
} else {
attrValue = anyUtils.newPlainAttrValue();
}
attrValue.setStringValue(attrValues.get(i));
bld.append('(').
append(queryBegin(table)).
append("WHERE ").
append(attrValueMatch(anyUtils, schema, attrValue, ignoreCaseMatch)).
append(')');
used.add(identifiers.get(i));
List<Object> queryParams = new ArrayList<>();
queryParams.add(schema.getKey());
queryParams.add(attrValues.get(i));
clauses.put(bld.toString(), queryParams);
}
}
}
LOG.debug("Generated where clauses {}", clauses);
return buildResult(anyUtils, findByDerAttrValue(table, clauses));
}
@Transactional
@Override
public <A extends Any<?>> void checkBeforeSave(final String table, final AnyUtils anyUtils, final A any) {
// check UNIQUE constraints
// cannot move to functional style due to the same issue reported at
// https://medium.com/xiumeteo-labs/stream-and-concurrentmodificationexception-2d14ed8ff4b2
for (PlainAttr<?> attr : any.getPlainAttrs()) {
if (attr.getUniqueValue() != null && attr instanceof JSONPlainAttr) {
PlainSchema schema = attr.getSchema();
Optional<A> other = findByPlainAttrUniqueValue(table, anyUtils, schema, attr.getUniqueValue(), false);
if (other.isEmpty() || other.get().getKey().equals(any.getKey())) {
LOG.debug("No duplicate value found for {}", attr.getUniqueValue().getValueAsString());
} else {
throw new DuplicateException(
"Value " + attr.getUniqueValue().getValueAsString()
+ " existing for " + schema.getKey());
}
}
}
// update sysInfo - as org.apache.syncope.core.persistence.jpa.entity.PlainAttrListener is not invoked
Date now = new Date();
String username = AuthContextUtils.getUsername();
LOG.debug("Set last change date '{}' and modifier '{}' for '{}'", now, username, any);
any.setLastModifier(username);
any.setLastChangeDate(now);
}
}