/*
* 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 static org.apache.syncope.core.persistence.jpa.dao.AbstractDAO.LOG;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.lang3.tuple.Triple;
import org.apache.syncope.common.lib.SyncopeConstants;
import org.apache.syncope.common.lib.types.AnyTypeKind;
import org.apache.syncope.common.lib.types.AttrSchemaType;
import org.apache.syncope.core.persistence.api.dao.search.AnyCond;
import org.apache.syncope.core.persistence.api.dao.search.AnyTypeCond;
import org.apache.syncope.core.persistence.api.dao.search.AssignableCond;
import org.apache.syncope.core.persistence.api.dao.search.AttributeCond;
import org.apache.syncope.core.persistence.api.dao.search.MemberCond;
import org.apache.syncope.core.persistence.api.dao.search.MembershipCond;
import org.apache.syncope.core.persistence.api.dao.search.OrderByClause;
import org.apache.syncope.core.persistence.api.dao.search.RelationshipCond;
import org.apache.syncope.core.persistence.api.dao.search.RelationshipTypeCond;
import org.apache.syncope.core.persistence.api.dao.search.ResourceCond;
import org.apache.syncope.core.persistence.api.dao.search.RoleCond;
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.AnyUtils;
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.Realm;
import org.apache.syncope.core.provisioning.api.utils.RealmUtils;
import org.apache.syncope.core.spring.security.AuthContextUtils;
import org.apache.syncope.ext.elasticsearch.client.ElasticsearchUtils;
import org.elasticsearch.action.search.SearchRequestBuilder;
import org.elasticsearch.action.search.SearchType;
import org.elasticsearch.client.Client;
import org.elasticsearch.index.query.DisMaxQueryBuilder;
import org.elasticsearch.index.query.MatchNoneQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.ReflectionUtils;
/**
* Search engine implementation for users, groups and any objects, based on Elasticsearch.
*/
public class ElasticsearchAnySearchDAO extends AbstractAnySearchDAO {
private static final QueryBuilder EMPTY_QUERY_BUILDER = new MatchNoneQueryBuilder();
@Autowired
private Client client;
@Autowired
private ElasticsearchUtils elasticsearchUtils;
private DisMaxQueryBuilder adminRealmsFilter(final Set<String> adminRealms) {
DisMaxQueryBuilder builder = QueryBuilders.disMaxQuery();
for (String realmPath : RealmUtils.normalize(adminRealms)) {
Realm realm = realmDAO.findByFullPath(realmPath);
if (realm == null) {
LOG.warn("Ignoring invalid realm {}", realmPath);
} else {
for (Realm descendant : realmDAO.findDescendants(realm)) {
builder.add(QueryBuilders.termQuery("realm.keyword", descendant.getFullPath()));
}
}
}
return builder;
}
private SearchRequestBuilder searchRequestBuilder(
final Set<String> adminRealms,
final SearchCond cond,
final AnyTypeKind kind) {
return client.prepareSearch(AuthContextUtils.getDomain().toLowerCase()).
setTypes(kind.name()).
setSearchType(SearchType.QUERY_THEN_FETCH).
setQuery(SyncopeConstants.FULL_ADMIN_REALMS.equals(adminRealms)
? getQueryBuilder(cond, kind)
: QueryBuilders.boolQuery().
must(adminRealmsFilter(adminRealms)).
must(getQueryBuilder(cond, kind)));
}
@Override
protected int doCount(final Set<String> adminRealms, final SearchCond cond, final AnyTypeKind kind) {
SearchRequestBuilder builder = searchRequestBuilder(adminRealms, cond, kind).
setFrom(0).setSize(0);
return (int) builder.get().getHits().getTotalHits();
}
private void addSort(
final SearchRequestBuilder builder,
final AnyTypeKind kind,
final List<OrderByClause> orderBy) {
AnyUtils attrUtils = anyUtilsFactory.getInstance(kind);
for (OrderByClause clause : orderBy) {
String sortName = null;
// Manage difference among external key attribute and internal JPA @Id
String fieldName = "key".equals(clause.getField()) ? "id" : clause.getField();
Field anyField = ReflectionUtils.findField(attrUtils.anyClass(), fieldName);
if (anyField == null) {
PlainSchema schema = schemaDAO.find(fieldName);
if (schema != null) {
sortName = schema.getType() == AttrSchemaType.String
|| schema.getType() == AttrSchemaType.Enum
? fieldName + ".keyword"
: fieldName;
}
} else {
sortName = anyField.getType().equals(String.class)
? fieldName + ".keyword"
: fieldName;
}
if (sortName == null) {
LOG.warn("Cannot build any valid clause from {}", clause);
} else {
builder.addSort(sortName, SortOrder.valueOf(clause.getDirection().name()));
}
}
}
@Override
protected <T extends Any<?>> List<T> doSearch(
final Set<String> adminRealms,
final SearchCond cond,
final int page,
final int itemsPerPage,
final List<OrderByClause> orderBy,
final AnyTypeKind kind) {
SearchRequestBuilder builder = searchRequestBuilder(adminRealms, cond, kind).
setFrom(page <= 0 ? 0 : page - 1).
setSize(itemsPerPage < 0 ? elasticsearchUtils.getIndexMaxResultWindow() : itemsPerPage);
addSort(builder, kind, orderBy);
return buildResult(
CollectionUtils.collect(Arrays.asList(builder.get().getHits().getHits()),
new Transformer<SearchHit, Object>() {
@Override
public Object transform(final SearchHit input) {
return input.getId();
}
}, new ArrayList<>()),
kind);
}
private QueryBuilder getQueryBuilder(final SearchCond cond, final AnyTypeKind kind) {
QueryBuilder builder = EMPTY_QUERY_BUILDER;
switch (cond.getType()) {
case LEAF:
case NOT_LEAF:
if (cond.getAnyTypeCond() != null && AnyTypeKind.ANY_OBJECT == kind) {
builder = getQueryBuilder(cond.getAnyTypeCond());
} else if (cond.getRelationshipTypeCond() != null
&& (AnyTypeKind.USER == kind || AnyTypeKind.ANY_OBJECT == kind)) {
builder = getQueryBuilder(cond.getRelationshipTypeCond());
} else if (cond.getRelationshipCond() != null
&& (AnyTypeKind.USER == kind || AnyTypeKind.ANY_OBJECT == kind)) {
builder = getQueryBuilder(cond.getRelationshipCond());
} else if (cond.getMembershipCond() != null
&& (AnyTypeKind.USER == kind || AnyTypeKind.ANY_OBJECT == kind)) {
builder = getQueryBuilder(cond.getMembershipCond());
} else if (cond.getAssignableCond() != null) {
builder = getQueryBuilder(cond.getAssignableCond());
} else if (cond.getRoleCond() != null && AnyTypeKind.USER == kind) {
builder = getQueryBuilder(cond.getRoleCond());
} else if (cond.getMemberCond() != null && AnyTypeKind.GROUP == kind) {
builder = getQueryBuilder(cond.getMemberCond());
} else if (cond.getResourceCond() != null) {
builder = getQueryBuilder(cond.getResourceCond());
} else if (cond.getAttributeCond() != null) {
builder = getQueryBuilder(cond.getAttributeCond(), kind);
} else if (cond.getAnyCond() != null) {
builder = getQueryBuilder(cond.getAnyCond(), kind);
}
builder = checkNot(builder, cond.getType() == SearchCond.Type.NOT_LEAF);
break;
case AND:
builder = QueryBuilders.boolQuery().
must(getQueryBuilder(cond.getLeftSearchCond(), kind)).
must(getQueryBuilder(cond.getRightSearchCond(), kind));
break;
case OR:
builder = QueryBuilders.disMaxQuery().
add(getQueryBuilder(cond.getLeftSearchCond(), kind)).
add(getQueryBuilder(cond.getRightSearchCond(), kind));
break;
default:
}
return builder;
}
private QueryBuilder checkNot(final QueryBuilder builder, final boolean not) {
return not
? QueryBuilders.boolQuery().mustNot(builder)
: builder;
}
private QueryBuilder getQueryBuilder(final AnyTypeCond cond) {
return QueryBuilders.termQuery("anyType.keyword", cond.getAnyTypeKey());
}
private QueryBuilder getQueryBuilder(final RelationshipTypeCond cond) {
return QueryBuilders.termQuery("relationshipTypes.keyword", cond.getRelationshipTypeKey());
}
private QueryBuilder getQueryBuilder(final RelationshipCond cond) {
String rightAnyObjectKey;
try {
rightAnyObjectKey = check(cond);
} catch (IllegalArgumentException e) {
return EMPTY_QUERY_BUILDER;
}
return QueryBuilders.termQuery("relationships.keyword", rightAnyObjectKey);
}
private QueryBuilder getQueryBuilder(final MembershipCond cond) {
String groupKey;
try {
groupKey = check(cond);
} catch (IllegalArgumentException e) {
return EMPTY_QUERY_BUILDER;
}
return QueryBuilders.termQuery("memberships.keyword", groupKey);
}
private QueryBuilder getQueryBuilder(final AssignableCond cond) {
Realm realm;
try {
realm = check(cond);
} catch (IllegalArgumentException e) {
return EMPTY_QUERY_BUILDER;
}
DisMaxQueryBuilder builder = QueryBuilders.disMaxQuery();
if (cond.isFromGroup()) {
for (Realm current = realm; current.getParent() != null; current = current.getParent()) {
builder.add(QueryBuilders.termQuery("realm.keyword", current.getFullPath()));
}
builder.add(QueryBuilders.termQuery("realm.keyword", realmDAO.getRoot().getFullPath()));
} else {
for (Realm current : realmDAO.findDescendants(realm)) {
builder.add(QueryBuilders.termQuery("realm.keyword", current.getFullPath()));
}
}
return builder;
}
private QueryBuilder getQueryBuilder(final RoleCond cond) {
return QueryBuilders.termQuery("roles.keyword", cond.getRoleKey());
}
private QueryBuilder getQueryBuilder(final MemberCond cond) {
String memberKey;
try {
memberKey = check(cond);
} catch (IllegalArgumentException e) {
return EMPTY_QUERY_BUILDER;
}
return QueryBuilders.termQuery("members.keyword", memberKey);
}
private QueryBuilder getQueryBuilder(final ResourceCond cond) {
return QueryBuilders.termQuery("resources.keyword", cond.getResourceKey());
}
private QueryBuilder fillAttrQuery(
final PlainSchema schema,
final PlainAttrValue attrValue,
final AttributeCond cond) {
String name = schema.getType() == AttrSchemaType.String
|| schema.getType() == AttrSchemaType.Enum
? schema.getKey() + ".keyword"
: schema.getKey();
Object value = schema.getType() == AttrSchemaType.Date && attrValue.getDateValue() != null
? attrValue.getDateValue().getTime()
: attrValue.getValue();
QueryBuilder builder = EMPTY_QUERY_BUILDER;
switch (cond.getType()) {
case ISNOTNULL:
builder = QueryBuilders.existsQuery(name);
break;
case ISNULL:
builder = QueryBuilders.boolQuery().mustNot(QueryBuilders.existsQuery(name));
break;
case ILIKE:
builder = QueryBuilders.queryStringQuery(
schema.getKey() + ":" + cond.getExpression().replace('%', '*'));
break;
case LIKE:
builder = QueryBuilders.wildcardQuery(name, cond.getExpression().replace('%', '*'));
break;
case IEQ:
builder = QueryBuilders.matchQuery(schema.getKey(), value);
break;
case EQ:
builder = QueryBuilders.termQuery(name, value);
break;
case GE:
builder = QueryBuilders.rangeQuery(name).gte(value);
break;
case GT:
builder = QueryBuilders.rangeQuery(name).gt(value);
break;
case LE:
builder = QueryBuilders.rangeQuery(name).lte(value);
break;
case LT:
builder = QueryBuilders.rangeQuery(name).lt(value);
break;
default:
}
return builder;
}
private QueryBuilder getQueryBuilder(final AttributeCond cond, final AnyTypeKind kind) {
Pair<PlainSchema, PlainAttrValue> checked;
try {
checked = check(cond, kind);
} catch (IllegalArgumentException e) {
return EMPTY_QUERY_BUILDER;
}
return fillAttrQuery(checked.getLeft(), checked.getRight(), cond);
}
private QueryBuilder getQueryBuilder(final AnyCond cond, final AnyTypeKind kind) {
Triple<PlainSchema, PlainAttrValue, AnyCond> checked;
try {
checked = check(cond, kind);
} catch (IllegalArgumentException e) {
return EMPTY_QUERY_BUILDER;
}
return fillAttrQuery(checked.getLeft(), checked.getMiddle(), checked.getRight());
}
}